Строки атрибута debuggerdisplay


В DebuggerDisplayAttribute Это очень полезная функция. Но создание этих строк является чрезвычайно трудоемкой задачей. Я хотел сделать это и ежу понятно, так что я могу создать их быстро и в то же время убедиться, что все они форматируются точно так же, поэтому я создал следующие инструменты.


Основной

Отправная точка DebuggerDisplayHelper<T>. Он инициализирует процесс создания string для данного типа и кэширует Func<T, string> что используется в последующих вызовах.

public static class DebuggerDisplayHelper<T>
{
    private static Func<T, string> _toString;

    public static string ToString(T obj, Action<DebuggerDisplayBuilder<T>> builderAction)
    {
        if (_toString is null)
        {
            var builder = new DebuggerDisplayBuilder<T>();
            builderAction(builder);
            _toString = builder.Build();
        }

        return _toString(obj);
    }
}

В DebuggerDisplayBuilder<T> отвечает за дом и форматирование string от членов указанного в качестве выражения. Они могут быть как простые типы или семейства.

Мне пришлось использовать другой метод названия Property & Collection вместо одного Add потому что компилятор не смогла правильно определить перегрузки для коллекций и выбрали для простых свойств.

public class DebuggerDisplayBuilder<T>
{
    private readonly IList<(string MemberName, Func<T, object> GetValue)> _members;

    public DebuggerDisplayBuilder()
    {
        _members = new List<(string MemberName, Func<T, object> GetValue)>();
    }

    public DebuggerDisplayBuilder<T> Property<TProperty>(Expression<Func<T, TProperty>> expression)
    {
        var memberExpressions = MemberFinder.FindMembers(expression);
        var getProperty = expression.Compile();

        return Add(
            memberName: memberExpressions.FormatMemberName(), 
            getValue: obj => getProperty(obj).FormatValue()
        );
    }

    public DebuggerDisplayBuilder<T> Collection<TProperty, TValue>(Expression<Func<T, IEnumerable<TProperty>>> expression, Expression<Func<TProperty, TValue>> formatExpression)
    {
        var memberExpressions = MemberFinder.FindMembers(expression);
        var getProperty = expression.Compile();
        var getValue = formatExpression.Compile();

        return Add(
            memberName: memberExpressions.FormatMemberName(),
            getValue: obj => getProperty(obj).Select(getValue).FormatCollection()
        );
    }

    public DebuggerDisplayBuilder<T> Collection<TProperty>(Expression<Func<T, IEnumerable<TProperty>>> expression)
    {
        return Collection(expression, x => x);
    }

    private DebuggerDisplayBuilder<T> Add(string memberName, Func<T, object> getValue)
    {
        _members.Add((memberName, getValue));
        return this;
    }

    public Func<T, string> Build()
    {
        return obj => string.Join(", ", _members.Select(t => $"{t.MemberName} = {t.GetValue(obj)}"));
    }
}

Форматирование поддерживается DebuggerDisplayFormatter. По умолчанию это звонки в какой-то момент ToString для всех значений, но пользователь может настроить форматирование (например, даты или числа). Это форматы строк, приведя их в одинарные кавычки. Цифры не получишь. Массивы заключенное в квадратные brackes и их значения следуют тем же правилам, как строки или числа. null и DBNull воспроизводятся соответственно в виде строк.

internal static class DebuggerDisplayFormatter
{
    public static string FormatValue<TValue>(this TValue value)
    {
        if (Type.GetTypeCode(value?.GetType()) == DBNull.Value.GetTypeCode()) return $"{nameof(DBNull)}";
        if (value == null) return "null";
        if (value.IsNumeric()) return value.ToString();

        return $"'{value}'";
    }

    public static string FormatCollection<TValue>(this IEnumerable<TValue> values)
    {
        if (values == null) return "null";

        return "[" + string.Join(", ", values.Select(FormatValue)) + "]";
    }

    public static string FormatMemberName(this IEnumerable<MemberExpression> memberExpressions)
    {
        return string.Join(".", memberExpressions.Select(m => m.Member.Name));
    }

    private static readonly ISet<TypeCode> NumericTypeCodes = new HashSet<TypeCode>
    {
        TypeCode.Byte,
        TypeCode.SByte,
        TypeCode.UInt16,
        TypeCode.UInt32,
        TypeCode.UInt64,
        TypeCode.Int16,
        TypeCode.Int32,
        TypeCode.Int64,
        TypeCode.Decimal,
        TypeCode.Double,
        TypeCode.Single,
    };

    public static bool IsNumeric<TValue>(this TValue value)
    {
        return NumericTypeCodes.Contains(Type.GetTypeCode(typeof(TValue)));
    }
}

Найти защищенные имена-это ответственность в MemberFinder[Visitor]

internal class MemberFinder : ExpressionVisitor, IEnumerable<MemberExpression>
{
    private readonly IList<MemberExpression> _members = new List<MemberExpression>();

    public static IEnumerable<MemberExpression> FindMembers(Expression expression)
    {
        var memberFinder = new MemberFinder();
        memberFinder.Visit(expression);
        return memberFinder;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        _members.Add(node);
        return base.VisitMember(node);
    }

    #region IEnumerable<MemberExpression>

    public IEnumerator<MemberExpression> GetEnumerator()
    {
        return _members.Reverse().GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion
}

Пример

Вот как все вместе выглядит при использовании Person в качестве тестовой модели

public class Person
{
    private string _testField = "TestValue";

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public double Age { get; set; }

    public object DBNullTest { get; set; } = DBNull.Value;

    public IList<int> GraduationYears { get; set; } = new List<int>();

    public IList<string> Nicknames { get; set; } = new List<string>();

    private string DebuggerDisplay() => DebuggerDisplayHelper<Person>.ToString(this, builder =>
    {
        builder.Property(x => x.FirstName);
        builder.Property(x => x.LastName);
        builder.Property(x => x._testField);
        builder.Property(x => x.DBNullTest);
        builder.Property(x => x.Age.ToString("F2"));
        builder.Property(x => x.GraduationYears.Count);
        builder.Collection(x => x.GraduationYears);
        builder.Collection(x => x.GraduationYears, x => x.ToString("X2"));
        builder.Collection(x => x.Nicknames);
    });
}

что я инициализировать со следующими значениями

var person = new Person
{
    FirstName = "John",
    LastName = null,
    Age = 123.456,
    DBNullTest = DBNull.Value,
    GraduationYears = { 1901, 1921, 1941 },
    Nicknames = { "Johny", "Doe" }
};

и как результат DebuggerDisplay() метод создает

 FirstName = 'John', LastName = null, _testField = 'TestValue', DBNullTest = DBNull, Age = '123,46', GraduationYears.Count = 3, GraduationYears = [1901, 1921, 1941], GraduationYears = ['76D', '781', '795'], Nicknames = ['Johny', 'Doe']

В Person не украшает DebuggerDisplayAttribute поскольку сборка:

[assembly: DebuggerDisplay("{DebuggerDisplay(),nq}", Target = typeof(Person))]

Если вы нашли и очевидные недостатки этого решения или думать, что все может быть улучшено, я был бы рад прочитать об этом.



694
12
задан 2 апреля 2018 в 09:04 Источник Поделиться
Комментарии
3 ответа

Это большой труд и очень полезный. У меня есть следующие замечания/предложения:

1) Обычные подозреваемые:

а) return "[" + string.Join(", ", values.Select(FormatValue)) + "]";

должны/могут быть:

  return $"[ {string.Join(", ", sample.Select(FormatValue))} ]";

Б) if (Type.GetTypeCode(value?.GetType()) == DBNull.Value.GetTypeCode()) return $"{nameof(DBNull)}";

может быть упрощен до:

  if (value is DBNull) return $"{nameof(DBNull)}";


2) предложения:

а) если коллекция имеет много элементов, вы можете хотеть удалять отладочный вывод:

public static string FormatCollection<TValue>(this IEnumerable<TValue> values, string format = null)
{
if (values == null) return "null";

TValue[] sample = values.Take(11).ToArray();
string dots = sample.Length > 10 ? "..." : "";

return $"[ {string.Join(", ", sample.Take(10).Select(x => FormatValue(x, format)))}{dots} ]";
}

б) я хотел дополнить DebuggerDisplayBuilder<T>.Property и .Collection с дополнительным формате строку:

public DebuggerDisplayBuilder<T> Property<TProperty>(Expression<Func<T, TProperty>> expression, string format = null)
public DebuggerDisplayBuilder<T> Collection<TProperty, TValue>(Expression<Func<T, IEnumerable<TProperty>>> expression, Expression<Func<TProperty, TValue>> formatExpression, string format = null)

Затем вы могли бы назвать их так:

  builder.Property(x => x.Age, "{0:F2}");
builder.Collection(x => x.GraduationYears, x => x, "{0:X2}");

вместо x.Age.ToString("F2") и т. д.

DebuggerDisplayFormatter.FormatValue(...) должен быть изменен на что-то вроде этого:

public static string FormatValue<TValue>(this TValue value, string format = null)
{
if (value is DBNull) return $"{nameof(DBNull)}";
if (value == null) return "null";
if (value.IsNumeric())
{
if (format != null)
{
return String.Format(format, value);
}
else
{
return value.ToString();
}
}

return $"'{value}'";
}

и должны причины быть вызван должным образом с аргументом format клиентами.


Обновление

Я не уверен, если это очевидно для других, но, если вы не хотите "засорять" классы с атрибута debuggerdisplay способ или вы хотите создать отладочный дисплей тип, который вы не можете изменить, то вы можете разместить атрибута debuggerdisplay как статический метод статического класса, как это:

[assembly: DebuggerDisplay("{AnotherNamespace.DebuggerDisplayers.DebuggerDisplay(this),nq}", Target = typeof(Person))]
[assembly: DebuggerDisplay("{AnotherNamespace.DebuggerDisplayers.DebuggerDisplay(this),nq}", Target = typeof(DateTime))]

namespace AnotherNamespace
{
public static class DebuggerDisplayers
{
public static string DebuggerDisplay(Person value) => DebuggerDisplayHelper<Person>.ToString(value, builder =>
{
builder.Property(x => x.FirstName);
builder.Property(x => x.LastName);
//builder.Property(x => x._testField); // Can't access a private member
builder.Property(x => x.DBNullTest);
builder.Property(x => x.Age, "{0:F2}");
builder.Property(x => x.GraduationYears.Count);
builder.Collection(x => x.GraduationYears);
builder.Collection(x => x.GraduationYears, x => x, "{0:X2}");
builder.Collection(x => x.Nicknames);
});

public static string DebuggerDisplay(DateTime value) => DebuggerDisplayHelper<DateTime>.ToString(value, builder =>
{
builder.Property(x => x.Year);
builder.Property(x => x.Month);
builder.Property(x => x.Day);
builder.Property(x => x.Ticks);
});
}
}

Пожалуйста, обратите внимание на полное имя метода в сборке директивы и this параметр DebuggerDisplay(this) в той же строке. Наказание заключается в том, что вы не можете отобразить частная и возможные внутренние члены.


Если изменение DebuggerDisplayFormatter.FormatValue ниже вы можете избежать числового проверить и можно указать строку формата для все типы недвижимости:

public static string FormatValue<TValue>(this TValue value, string format = null)
{
if (value == null) return "null";
if (value is DBNull) return $"{nameof(DBNull)}";

if (value is string)
{
if (format != null)
return $"'{string.Format(format, value)}'";
return $"'{value}'";
}

if (format != null)
return string.Format(format, value);

return value.ToString();
}

8
ответ дан 2 апреля 2018 в 01:04 Источник Поделиться

Я бы лично нравится DebuggerDisplayHelper.ToString() чтобы быть метод расширения, так что я сделала, чтобы получить его как таковой:

public static class DebuggerDisplayHelper
{
public static string ToString<T>(this T obj, Action<DebuggerDisplayBuilder<T>> builderAction)
{
return DebuggerDisplayHelperInternal<T>.ToString(obj, builderAction);
}

private static class DebuggerDisplayHelperInternal<T>
{
private static Func<T, string> _toString;

public static string ToString(T obj, Action<DebuggerDisplayBuilder<T>> builderAction)
{
if (_toString is null)
{
var builder = new DebuggerDisplayBuilder<T>();

builderAction(builder);
_toString = builder.Build();
}

return _toString(obj);
}
}
}

Это будет называться так:

private string DebuggerDisplay() => this.ToString(builder =>
{
builder.Property(x => x.FirstName);
builder.Property(x => x.LastName);
builder.Property(x => x._testField);
builder.Property(x => x.DBNullTest);
builder.Property(x => x.Age.ToString("F2"));
builder.Property(x => x.GraduationYears.Count);
builder.Collection(x => x.GraduationYears);
builder.Collection(x => x.GraduationYears, x => x.ToString("X2"));
builder.Collection(x => x.Nicknames);
});

5
ответ дан 2 апреля 2018 в 02:04 Источник Поделиться

Я добавил предложения по улучшению и пару больше.


В DebuggerDisplayHelper класс получил необщего компаньон для поддержки расширений. Новый метод теперь называется ToDebuggerDisplayString.

public static class DebuggerDisplayHelper<T>
{
private static Func<T, string> _toString;

public static string ToDebuggerDisplayString([CanBeNull] T obj, Action<DebuggerDisplayBuilder<T>> builderAction)
{
if (builderAction == null) throw new ArgumentNullException(nameof(builderAction));

if (_toString is null)
{
var builder = new DebuggerDisplayBuilder<T>();
builderAction(builder);
_toString = builder.Build();
}

return _toString(obj);
}
}

public static class DebuggerDisplayHelper
{
public static string ToDebuggerDisplayString<T>([CanBeNull] this T obj, [NotNull] Action<DebuggerDisplayBuilder<T>> builderAction)
{
if (builderAction == null) throw new ArgumentNullException(nameof(builderAction));

return DebuggerDisplayHelper<T>.ToDebuggerDisplayString(obj, builderAction);
}
}


В DebuggerDisplayBuilder была добавлена поддержка форматирования и максимальная длина для коллекций. Новый null проверка предотвращает его от падения, когда this это null.

public class DebuggerDisplayBuilder<T>
{
private readonly IList<(string MemberName, Func<T, object> GetValue)> _members;

public DebuggerDisplayBuilder()
{
_members = new List<(string MemberName, Func<T, object> GetValue)>();
}

public DebuggerDisplayBuilder<T> Property<TProperty>(
[NotNull] Expression<Func<T, TProperty>> propertySelector,
[NotNull] string format)
{
if (propertySelector == null) throw new ArgumentNullException(nameof(propertySelector));
if (format == null) throw new ArgumentNullException(nameof(format));

var memberExpressions = DebuggerDisplayVisitor.EnumerateMembers(propertySelector);
var getProperty = propertySelector.Compile();

return Add(
memberName: memberExpressions.FormatMemberName(),
getValue: obj => obj == null ? null : getProperty(obj).FormatValue(format)
);
}

public DebuggerDisplayBuilder<T> Collection<TProperty, TValue>(
[NotNull] Expression<Func<T, IEnumerable<TProperty>>> propertySelector,
[NotNull] Expression<Func<TProperty, TValue>> valueSelector,
[NotNull] string format,
int max)
{
if (propertySelector == null) throw new ArgumentNullException(nameof(propertySelector));
if (valueSelector == null) throw new ArgumentNullException(nameof(valueSelector));
if (format == null) throw new ArgumentNullException(nameof(format));

var memberExpressions = DebuggerDisplayVisitor.EnumerateMembers(propertySelector);
var getProperty = propertySelector.Compile();
var getValue = valueSelector.Compile();

return Add(
memberName: memberExpressions.FormatMemberName(),
getValue: obj => obj == null ? null : getProperty(obj).Select(getValue).FormatCollection(format, max)
);
}

private DebuggerDisplayBuilder<T> Add(string memberName, Func<T, object> getValue)
{
_members.Add((memberName, getValue));
return this;
}

public Func<T, string> Build()
{
return obj => string.Join(", ", _members.Select(t => $"{t.MemberName} = {t.GetValue(obj)}"));
}
}

Основной класс не имеет необязательные параметры, но вместо расширения представлены значения по умолчанию.

public static class DebuggerDisplayBuilder
{
public static DebuggerDisplayBuilder<T> Property<T, TProperty>(
this DebuggerDisplayBuilder<T> builder,
Expression<Func<T, TProperty>> propertySelector)
{
return builder.Property(propertySelector, DebuggerDisplayFormatter.DefaultValueFormat);
}

public static DebuggerDisplayBuilder<T> Collection<T, TProperty, TValue>(
this DebuggerDisplayBuilder<T> builder,
Expression<Func<T, IEnumerable<TProperty>>> propertySelector,
Expression<Func<TProperty, TValue>> valueSelector,
string format,
int max = DebuggerDisplayFormatter.DefaultCollectionLength)
{
return builder.Collection(propertySelector, valueSelector, DebuggerDisplayFormatter.DefaultValueFormat, max);
}

public static DebuggerDisplayBuilder<T> Collection<T, TProperty>(
this DebuggerDisplayBuilder<T> builder,
Expression<Func<T, IEnumerable<TProperty>>> propertySelector)
{
return builder.Collection(propertySelector, x => x, DebuggerDisplayFormatter.DefaultValueFormat, DebuggerDisplayFormatter.DefaultCollectionLength);
}
}


В DebuggerDisplayFormatter потерял свое IsNumeric Helper, но приобрел два значения по умолчанию и пользовательские форматирование. Массив выходных данных теперь также содержит ... многоточие и максимальное количество элементов, которое всегда отображается. Это может быть более сложным, но может в другой раз...

internal static class DebuggerDisplayFormatter
{
public const string DefaultValueFormat = "{0}";

public const int DefaultCollectionLength = 10;

public static string FormatValue<TValue>([CanBeNull] this TValue value, [NotNull] string format)
{
if (format == null) throw new ArgumentNullException(nameof(format));

if (value == null) return "null";
if (value is DBNull) return $"{nameof(DBNull)}";

var valueFormatted = string.Format(CultureInfo.InvariantCulture, format, value);

return
value is string
? $"'{valueFormatted}'"
: valueFormatted;
}

public static string FormatValue<TValue>([CanBeNull] this TValue value)
{
return value.FormatValue(DefaultValueFormat);
}

public static string FormatCollection<TValue>([CanBeNull] this IEnumerable<TValue> values, [NotNull] string format, int max)
{
if (format == null) throw new ArgumentNullException(nameof(format));

if (values == null) return "null";

// [1, 2, 3, ...] (max = 10)
return $"[{string.Join(", ", values.Select(x => x.FormatValue(format)).Take(max))}, ...] (max {max})";
}

// Foo.Bar(..).Baz
public static string FormatMemberName([NotNull] this IEnumerable<Expression> expressions)
{
if (expressions == null) throw new ArgumentNullException(nameof(expressions));

return string.Join(".", expressions.GetMemberNames());
}

private static IEnumerable<string> GetMemberNames([NotNull] this IEnumerable<Expression> expressions)
{
if (expressions == null) throw new ArgumentNullException(nameof(expressions));

foreach (var expression in expressions)
{
switch (expression)
{
case MemberExpression memberExpression:
yield return memberExpression.Member.Name;
break;
case MethodCallExpression methodCallExpression:
// Ignore ToString calls.
if (methodCallExpression.Method.Name == nameof(ToString)) continue;
yield return $"{methodCallExpression.Method.Name}(..)";
break;
}
}
}
}


В DebuggerDisplayVisitor стал IEnumerable<Expression> будучи теперь в состоянии перечислить несколько элементов для вызова цепочки.

internal class DebuggerDisplayVisitor : ExpressionVisitor, IEnumerable<Expression>
{
// Member expressions are visited in revers order.
// This allows fast inserts at the beginning and thus to avoid reversing it back.
private readonly LinkedList<Expression> _members = new LinkedList<Expression>();

public static IEnumerable<Expression> EnumerateMembers(Expression expression)
{
var memberFinder = new DebuggerDisplayVisitor();
memberFinder.Visit(expression);
return memberFinder;
}

protected override Expression VisitMember(MemberExpression node)
{
_members.AddFirst(node);
return base.VisitMember(node);
}

protected override Expression VisitMethodCall(MethodCallExpression node)
{
_members.AddFirst(node);
return base.VisitMethodCall(node);
}

#region IEnumerable<MemberExpression>

public IEnumerator<Expression> GetEnumerator()
{
return _members.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

#endregion
}


Пример

Новые тестовые данные и строителя.

var person = new Person
{
FirstName = "John",
LastName = null,
Age = 123.456,
DBNullTest = DBNull.Value,
GraduationYears = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 },
Nicknames = { "Johny", "Doe" }
};

var toString = person.ToDebuggerDisplayString(builder =>
{
builder.Property(x => x.FirstName, "{0,8}");
builder.Property(x => x.LastName);
builder.Property(x => x.DBNullTest);
builder.Property(x => x.GraduationYears.Sum());
builder.Property(x => x.Age, "{0:F2}");
builder.Property(x => x.GraduationYears.Count);
builder.Collection(x => x.GraduationYears);
builder.Collection(x => x.GraduationYears, x => x, "{0:X2}");
builder.Collection(x => x.Nicknames);
});

и новый выход

"FirstName = '    John', LastName = null, DBNullTest = DBNull, GraduationYears.Sum(..) = 78, Age = 123.46, GraduationYears.Count = 12, GraduationYears = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...] (max 10), GraduationYears = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...] (max 10), Nicknames = ['Johny', 'Doe', ...] (max 10)"

2
ответ дан 3 апреля 2018 в 04:04 Источник Поделиться