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

151

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

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

Томаша
источник

Ответы:

205

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

static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly) {
    foreach(Type type in assembly.GetTypes()) {
        if (type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0) {
            yield return type;
        }
    }
}
Эндрю Арнотт
источник
7
Согласен, но в этом случае мы можем сделать это декларативно согласно решению casperOne. Приятно иметь возможность использовать доходность, еще приятнее этого не делать :)
Джон Скит
9
Мне нравится LINQ. Люблю это, на самом деле. Но он зависит от .NET 3.5, а доходность - нет. Кроме того, LINQ в конечном итоге разбивается на то же самое, что и доходность. Так что вы приобрели? Определенный синтаксис C #, это предпочтение.
Эндрю Арнотт
1
@AndrewArnott Самые короткие и короткие строки кода не имеют отношения к производительности, они только могут способствовать удобочитаемости и удобству обслуживания. Я оспариваю утверждение, что они выделяют наименьшее количество объектов и производительность будет выше (особенно без эмпирического доказательства); вы в основном написали Selectметод расширения, и компилятор сгенерирует конечный автомат, как если бы вы вызвали его Selectиз-за вашего использования yield return. Наконец, любой выигрыш в производительности, который может быть получен в большинстве случаев, является микрооптимизацией.
casperOne
1
Совершенно верно, @casperOne. Очень незначительная разница, особенно по сравнению с весом самого отражения. Вероятно, никогда бы не всплыл в идеале.
Эндрю Арнотт
1
Конечно, Resharper говорит, что «цикл foreach может быть преобразован в выражение LINQ», который выглядит следующим образом: assembly.GetTypes (). Где (type => type.GetCustomAttributes (typeof (HelpAttribute), true) .Length> 0);
Дэвид Барроуз
107

Ну, вам придется перечислять все классы во всех сборках, которые загружены в текущий домен приложения. Для этого необходимо вызвать 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>() };
casperOne
источник
1
Перечисление всех типов во всех загруженных сборках будет очень медленным и не принесет вам большой пользы. Это также потенциально угроза безопасности. Вы, вероятно, можете предсказать, какие сборки будут содержать интересующие вас типы. Просто перечислите типы в них.
Эндрю Арнотт
@ Эндрю Арнотт: Правильно, но об этом просили. Достаточно просто сократить запрос для конкретной сборки. Это также имеет дополнительное преимущество, поскольку дает вам соответствие между типом и атрибутом.
casperOne
1
Вы можете использовать тот же код только для текущей сборки с System.Reflection.Assembly.GetExecutingAssembly ()
Крис Москини,
@ChrisMoschini Да, вы можете, но вы не всегда можете сканировать текущую сборку. Лучше оставить его открытым.
casperOne
Я делал это много раз, и не так много способов сделать это эффективным. Вы можете пропустить сборки Microsoft (они подписаны одним и тем же ключом, поэтому их довольно легко избежать, используя AssemblyName. Вы можете кэшировать результаты в статическом файле, который уникален для домена приложений, в который загружаются сборки (необходимо полностью кэшировать). имена сборок, которые вы проверили на случай, если другие загружены за это время.) Я обнаружил, что здесь я изучаю кэширование загруженных экземпляров типа атрибута внутри атрибута. Не уверен в этом шаблоне, не уверен, когда они создаются, и т. д.
34

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

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;
Джей Уокер
источник
3
Я считаю, что это правильное решение, которое использует метод, предназначенный для метода.
Алексей Омельченко
11

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

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

// 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.
        }
    }
}
CodingWithSpike
источник
9

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

Например, если вы ищете атрибут, который вы объявили сами, вы не ожидаете, что какие-либо системные библиотеки 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)
Торговые идеи Филиппа
источник
4

В случае ограничений 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;
                }
            }
        }
    }
Лоренц Ло Зауэр
источник
0

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

    public static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly)
    {
        return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0);
    }
Тахионный
источник