Как перечислить все классы с пользовательским атрибутом класса?

Вопрос основан на примере MSDN.

Допустим, у нас есть несколько классов C# с HelpAttribute в автономном настольном приложении. Можно ли перечислить все классы с таким атрибутом? Имеет ли смысл распознавать классы таким образом? Пользовательский атрибут будет использоваться для отображения списка возможных вариантов меню, выбор пункта приведет к выводу на экран экземпляра такого класса. Количество классов / предметов будет расти медленно, но, таким образом, мы можем избежать их перечисления в другом месте, я думаю.

8 ответов

Решение

Да, конечно. Используя отражение:

static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly) {
    foreach(Type type in assembly.GetTypes()) {
        if (type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0) {
            yield return type;
        }
    }
}

Что ж, вам нужно будет перечислить все классы во всех сборках, которые загружены в текущий домен приложения. Чтобы сделать это, вы бы позвонили GetAssemblies метод на AppDomain экземпляр для текущего домена приложения.

Оттуда вы бы позвонили GetExportedTypes (если вы хотите только публичные типы) или GetTypes на каждой Assembly чтобы получить типы, содержащиеся в сборке.

Затем вы бы назвали GetCustomAttributes метод на каждом Type экземпляр, передавая тип атрибута, который вы хотите найти.

Вы можете использовать LINQ, чтобы упростить это для вас:

var typesWithMyAttribute =
    from a in AppDomain.CurrentDomain.GetAssemblies()
    from t in a.GetTypes()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };

Приведенный выше запрос даст вам каждый тип с примененным к нему атрибутом, а также экземпляр присвоенного ему атрибута (ов).

Обратите внимание, что если в домен приложения загружено большое количество сборок, эта операция может быть дорогой. Вы можете использовать Parallel LINQ для сокращения времени выполнения операции, например:

var typesWithMyAttribute =
    // Note the AsParallel here, this will parallelize everything after.
    from a in AppDomain.CurrentDomain.GetAssemblies().AsParallel()
    from t in a.GetTypes()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };

Фильтрация по конкретному Assembly это просто:

Assembly assembly = ...;

var typesWithMyAttribute =
    from t in assembly.GetTypes()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };

И если в сборке много типов, вы можете снова использовать Parallel LINQ:

Assembly assembly = ...;

var typesWithMyAttribute =
    // Partition on the type list initially.
    from t in assembly.GetTypes().AsParallel()
    let attributes = t.GetCustomAttributes(typeof(HelpAttribute), true)
    where attributes != null && attributes.Length > 0
    select new { Type = t, Attributes = attributes.Cast<HelpAttribute>() };

Другие ответы ссылаются на GetCustomAttributes. Добавление этого в качестве примера использования IsDefined

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;

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

Это фрагмент моего кода, который проходит через все типы во всех загруженных сборках:

// this is making the assumption that all assemblies we need are already loaded.
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) 
{
    foreach (Type type in assembly.GetTypes())
    {
        var attribs = type.GetCustomAttributes(typeof(MyCustomAttribute), false);
        if (attribs != null && attribs.Length > 0)
        {
            // add to a cache.
        }
    }
}

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

Например, если вы ищете атрибут, который вы объявили сами, вы не ожидаете, что какие-либо системные библиотеки DLL будут содержать какие-либо типы с этим атрибутом. Свойство Assembly.GlobalAssemblyCache - это быстрый способ проверки системных библиотек DLL. Когда я попробовал это в реальной программе, я обнаружил, что могу пропустить 30 101 тип, и мне нужно только проверить 1 983 типов.

Другой способ фильтрации - использовать Assembly.ReferencedAssemblies. Предположительно, если вам нужны классы с определенным атрибутом, и этот атрибут определен в конкретной сборке, вам нужно заботиться только об этой сборке и других сборках, которые на нее ссылаются. В моих тестах это помогло немного больше, чем проверка свойства GlobalAssemblyCache.

Я объединил оба из них и получил это еще быстрее. Код ниже включает оба фильтра.

        string definedIn = typeof(XmlDecoderAttribute).Assembly.GetName().Name;
        foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            // Note that we have to call GetName().Name.  Just GetName() will not work.  The following
            // if statement never ran when I tried to compare the results of GetName().
            if ((!assembly.GlobalAssemblyCache) && ((assembly.GetName().Name == definedIn) || assembly.GetReferencedAssemblies().Any(a => a.Name == definedIn)))
                foreach (Type type in assembly.GetTypes())
                    if (type.GetCustomAttributes(typeof(XmlDecoderAttribute), true).Length > 0)

В случае ограничений Portable .NET должен работать следующий код:

    public static IEnumerable<TypeInfo> GetAtributedTypes( Assembly[] assemblies, 
                                                           Type attributeType )
    {
        var typesAttributed =
            from assembly in assemblies
            from type in assembly.DefinedTypes
            where type.IsDefined(attributeType, false)
            select type;
        return typesAttributed;
    }

или для большого количества сборок, использующих петлевое состояние yield return:

    public static IEnumerable<TypeInfo> GetAtributedTypes( Assembly[] assemblies, 
                                                           Type attributeType )
    {
        foreach (var assembly in assemblies)
        {
            foreach (var typeInfo in assembly.DefinedTypes)
            {
                if (typeInfo.IsDefined(attributeType, false))
                {
                    yield return typeInfo;
                }
            }
        }
    }

Это еще одна версия кода, предоставленного Trade-Ideas philip, я преобразовал код в linq, подключил его к хорошей статической функции, которую вы можете просто добавить в проект.

Оригинал: /questions/23853967/kak-perechislit-vse-klassyi-s-polzovatelskim-atributom-klassa/23853998#23853998

Я также добавил - на моей машине с достаточным количеством ядер и т. д. и с проектом «нормального» размера (что полностью субъективно) это было самым быстрым /

Без AsParallel()это заняло 1,5 секунды примерно для 200 результатов, а вместе с этим потребовалось около пары миллисекунд - поэтому мне это кажется самым быстрым.

Обратите внимание, что при этом пропускаются сборки в GAC.

      private static IEnumerable<IEnumerable<T>> GetAllAttributesInAppDomain<T>()
{
    var definedIn = typeof(T).Assembly.GetName().Name;
    var assemblies = AppDomain.CurrentDomain.GetAssemblies();

   var res = assemblies.AsParallel()
        .Where(assembly => (!assembly.GlobalAssemblyCache) && ((assembly.GetName().Name == definedIn) ||
                                                               assembly.GetReferencedAssemblies()
                                                                   .Any(a => a.Name == definedIn))
            )
        .SelectMany(c => c.GetTypes())
        .Select(type => type.GetCustomAttributes(typeof(T), true)
            .Cast<T>()
            )
        .Where(c => c.Any());

    return res;
}

Применение:

      var allAttributesInAppDomain = GetAllAttributesInAppDomain<ExportViewAttribute>();

Обратите внимание, что если у вас есть только 1 атрибут на класс, а не несколько, проще сгладить результат из IEnumerable<IEnumerable<T>>к IEnumerable<T>вот так:

      var allAttributesInAppDomainFlattened = allAttributesInAppDomain.SelectMany(c => c);

Помните, что это использует IEnumerableтак позвони ToList()для фактического запуска функции.

Мы можем улучшить ответ Эндрю и преобразовать все это в один запрос LINQ.

    public static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly)
    {
        return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0);
    }
Другие вопросы по тегам