Як перерахувати всі класи з атрибутом спеціального класу?


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 (). Where (type => type.GetCustomAttributes (typeof (HelpAttribute), true). 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>() };

1
Перерахування всіх типів у всіх завантажених збірках було б дуже повільним і не принесло вам великої кількості. Це також може бути ризиком для безпеки. Ви, ймовірно, можете передбачити, які збірки будуть містити типи, які вас цікавлять. Просто перерахуйте типи в них.
Ендрю Арнотт

@Andrew Arnott: Правильно, але це те, що просили. Досить просто вирізати запит на певну збірку. Це також має додаткову перевагу в наданні відображення між типом та атрибутом.
casperOne

1
Ви можете використовувати той самий код лише для поточної збірки з System.Reflection.Assembly.GetExecutingAssembly ()
Chris Moschini

@ChrisMoschini Так, ви можете, але ви не завжди можете сканувати поточну збірку. Краще залишити його відкритим.
casperOne

Я робив це багато разів, і не так багато способів зробити це ефективним. Ви можете пропустити збірки мікрософт (вони підписані одним і тим же ключем, тому їх досить легко уникнути за допомогою AssemblyName. Ви можете кешувати результати в статичному стані, унікальному для AppDomain, в який завантажуються збірки (доведеться кешувати повністю) назви збірок, які ви перевірили, якщо інші тим часом завантажуються). Я опинився тут, коли я розслідував кешовані завантажені екземпляри типу атрибута в межах атрибута.

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.
        }
    }
}

9

Це підвищення продуктивності поверх прийнятого рішення. Ітерація, хоча всі класи можуть бути повільними, тому що їх так багато. Іноді ви можете відфільтрувати цілу збірку, не дивлячись на жоден її тип.

Наприклад, якщо ви шукаєте атрибут, який ви заявили про себе, ви не очікуєте, що жодна із системних DLL містить такі типи з цим атрибутом. Властивість Assembly.GlobalAssemblyCache - це швидкий спосіб перевірити наявність системних DLL. Коли я спробував це на реальній програмі, я виявив, що можу пропустити 30101 тип, і мені потрібно лише перевірити 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

У випадку обмежень портативного .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);
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.