Как я должен представлять перечисления иерархических флагов?

У меня есть следующий набор перечислений:

[Flags]
public enum Categories : uint
{
    A = (1 << 0),
    B = (1 << 1),
    B1 = B | (1 << 16),
    B2 = B | (1 << 17),
    B3 = B | (1 << 18),
    B4 = B | (1 << 19),
    B5 = B | (1 << 20),
    C = (1 << 2),
    C1 = C | (1 << 21),
    D = (1 << 3),
    D1 = D | (1 << 22),
    D2 = D | (1 << 23),
    E = (1 << 4),
    F = (1 << 5),
    F1 = F | (1 << 23),
    F2 = F | (1 << 24),
    F3 = F | (1 << 25),
    F4 = F | (1 << 26),
    F5 = F | (1 << 27),
    G = (1 << 6),
    H = (1 << 7),
    H1 = H | (1 << 28),
}

Идея состоит в том, что перечисления представляют собой иерархическую структуру, в которой дочернее перечисление подразумевает своего родителя, и может быть применено любое количество флагов.

Проблема, которую я вижу, заключается в том, что все дочерние перечисления не представляются во время отладки в виде имен или наборов имен. Т.е., Categories.F = "F", но Categories.F2 = 16777248. Я бы надеялся, что Categories.F2 = "F, F2" или хотя бы "F2"

Как я могу сделать так, чтобы мои перечисления оставались распознанными как флаги? Есть ли лучший способ выполнить то, что я пытаюсь сделать?


person dlras2    schedule 06.04.2013    source источник
comment
Я только что скомпилировал и запустил ваш код, я получил Categories.F2.ToString() = "F2"   -  person p.s.w.g    schedule 06.04.2013
comment
@ p.s.w.g Да, извините - на самом деле проблема только в отладке окон просмотра и т. Д. Я отредактировал вопрос, чтобы уточнить.   -  person dlras2    schedule 06.04.2013
comment
Значит проблема только во время отладки? Интересно, если вы добавите [DebugerDisplay("{ToString()}")], это повлияет?   -  person p.s.w.g    schedule 06.04.2013
comment
Проблема может быть воспроизведена, если у вас есть перечисление флагов, где одна составная константа перечисления состоит из именованного бита и безымянного. Пример: [Flags] enum Categories { F = 1, F2 = 1 | 2, }. Значение Categories с базовым числовым значением 3 будет отображаться отладчиком как "3", тогда как .ToString() и .ToString("F") дают "F2". Похоже на ошибку.   -  person Jeppe Stig Nielsen    schedule 06.04.2013
comment
@ p.s.w.g Добавление атрибута [DebuggerDisplay("{ToString()}")] (при сохранении [Flags], конечно) работает (пространство имен using System.Diagnostics). Затем отладчик показывает то же самое, что и ToString(). Но странно, что это необходимо. Почему алгоритм отладчика по умолчанию отличается от ToString()?   -  person Jeppe Stig Nielsen    schedule 06.04.2013
comment
@DanRasmussen спасибо, я включил некоторые подробности о DebuggerDisplay и DebuggerTypeProxy в свой ответ для полноты картины.   -  person p.s.w.g    schedule 06.04.2013


Ответы (2)


Очень странно, что значение в отладчике отличается от значения ToString. Согласно документации, они должны совпадать (поскольку тип Enum не действительно переопределить ToString).

Если объект C# имеет переопределенный ToString(), отладчик вызовет переопределение и покажет его результат вместо стандартного {<typeName>}.

Очевидно, что это не работает для перечислений. Мое предположение состоит в том, что отладчик пытается выполнить какую-то особую, недокументированную обработку типов перечисления. Добавление DebuggerDisplayAttribute, по-видимому, решает проблему, переопределяя это поведение .

[DebuggerDisplay("{ToString()}")]
[Flags]
public enum Categories : uint
{
    ...
}

Категории.F2.ToString() = "F, F2"

C# не сделает за вас эту магию, потому что F2 уже имеет собственное имя в перечислении. Вы можете вручную пометить отдельные элементы следующим образом:

public enum Categories
{
    [Description("F, F2")]
    F2 = F | (1 << 24),
}

А затем напишите код для преобразования в описание.

public static string ToDescription(this Categories c)
{
    var field = typeof(Categories).GetField(c.ToString());
    if (field != null)
    {
        return field.GetCustomAttributes().Cast<DescriptionAttribute>().First().Description;
    }
}
...
Categories.F2.ToDescription() == "F, F2";

Или вы можете немного поколдовать, чтобы сгенерировать это самостоятельно:

public static string ToDescription(this Categories c)
{
    var categoryNames =
        from v in Enum.GetValues(typeof(Categories)).Cast<Category>()
        where v & c == c
        orderby v
        select v.ToString();
    return String.Join(", ", categoryNames);
}

К сожалению, метод расширения нельзя использовать с DebuggerDisplayAttribute, но вы можете использовать DebuggerTypeAttribute, YMMV, но вы можете попробовать это:

[DebuggerType("CategoryDebugView")]
[Flags]
public enum Categories : uint
{
    ...
}

internal class CategoryDebugView
{
    private Category value;

    public CategoryDebugView(Category value)
    {
        this.value = value;
    }

    public override string ToString()
    {
        var categoryNames =
            from v in Enum.GetValues(typeof(Categories)).Cast<Category>()
            where v & c == c
            orderby v
            select v.ToString();
        return String.Join(", ", categoryNames);
    }
}
person p.s.w.g    schedule 06.04.2013
comment
[Тип отладчика] не существует. [DebuggerTypeProxy] существует, но не может быть применен к перечислениям: ошибка CS0592: атрибут «DebuggerTypeProxy» недопустим для этого типа объявления. Это допустимо только для объявлений «сборка, класс, структура». - person robert4; 14.09.2017
comment
@robert4 моя ошибка, я имел в виду DebuggerTypeProxy. Я перепроверю этот ответ, когда доберусь до компьютера. - person p.s.w.g; 14.09.2017

Вы можете делать то, о чем просите, приложив немного усилий. Я создал несколько методов расширения для Categories, которые используют HasFlag() для определения того, имеет ли значение перечисления определенный родитель, а затем вызывают для них ToString() и объединяют результат.

public static class CategoriesExtensionMethods
{
    public static Categories GetParentCategory(this Categories category)
    {
        Categories[] parents = 
        {
            Categories.A,
            Categories.B,
            Categories.C,
            Categories.D,
            Categories.E,
            Categories.F,
            Categories.G,
            Categories.H,
        };

        Categories? parent = parents.SingleOrDefault(e => category.HasFlag(e));
        if (parent != null)
            return (Categories)parent;
        return Categories.None;
    }

    public static string ToStringWithParent(this Categories category)
    {
        var parent = GetParentCategory(category);
        if (parent == Categories.None)
            return category.ToString();
        return string.Format("{0} | {1}", parent.ToString(), category.ToString());
    }
}

Тогда мы можем использовать его следующим образом:

var f1 = Categories.F1;

var f1ParentString = f1.ToStringWithParent();
// f1ParentString = "F | F1"

var f = Categories.F;
var fParentString = f.GetParentCategory();
// fParentString = "F"

Обновлять

Вот более красивый способ реализации GetParentCategory(), если вы не хотите указывать всех своих родителей.

public static Categories GetParentCategory(this Categories category)
{
    var values = Enum.GetValues(typeof(Categories)).Cast<Categories>();
    var parent = values.SingleOrDefault(e => category.HasFlag(e) && e != Categories.None && category != e);
    if (parent != Categories.None)
        return (Categories)parent;
    return Categories.None;
}
person Daniel Imms    schedule 06.04.2013