Jak wyliczyć wszystkie klasy z niestandardowym atrybutem klasy?

151

Pytanie na podstawie przykładu MSDN .

Powiedzmy, że mamy kilka klas C # z HelpAttribute w samodzielnej aplikacji klasycznej. Czy można wyliczyć wszystkie klasy z takim atrybutem? Czy ma sens rozpoznawanie zajęć w ten sposób? Własny atrybut zostałby użyty do wylistowania możliwych opcji menu, wybranie pozycji spowoduje wyświetlenie na ekranie instancji takiej klasy. Liczba klas / przedmiotów będzie rosła powoli, ale myślę, że w ten sposób unikniemy wyliczania ich wszystkich gdzie indziej.

tłuc
źródło

Odpowiedzi:

205

Tak, absolutnie. Korzystanie z odbicia:

static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly) {
    foreach(Type type in assembly.GetTypes()) {
        if (type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0) {
            yield return type;
        }
    }
}
Andrew Arnott
źródło
7
Zgoda, ale w tym przypadku możemy to zrobić deklaratywnie, zgodnie z rozwiązaniem casperOne. Fajnie jest móc wykorzystać plony, jeszcze przyjemniej jest nie mieć :)
Jon Skeet
9
Lubię LINQ. Właściwie to kocham. Ale wymaga zależności od .NET 3.5, co nie daje zwrotu. Ponadto LINQ ostatecznie rozkłada się zasadniczo na to samo, co zwrot zysku. Więc co zyskałeś? Konkretna składnia języka C #, czyli preferencja.
Andrew Arnott,
1
@AndrewArnott Najmniej i najkrótsze wiersze kodu nie mają znaczenia dla wydajności, są tylko możliwymi elementami przyczyniającymi się do czytelności i łatwości utrzymania. Podważam stwierdzenie, że alokują najmniej obiektów, a wydajność będzie szybsza (zwłaszcza bez dowodów empirycznych); w zasadzie napisałeś Selectmetodę rozszerzającą, a kompilator wygeneruje maszynę stanu, tak jakbyś wywołał ją Selectz powodu użycia yield return. Wreszcie, wszelkie zyski wydajności, które mogą być uzyskane w większości przypadków być mikro optymalizacje.
casperOne
1
Całkiem dobrze, @casperOne. Bardzo niewielka różnica, zwłaszcza w porównaniu z ciężarem samego odbicia. Prawdopodobnie nigdy nie wyszedłby na ślad doskonałości.
Andrew Arnott,
1
Oczywiście Resharper mówi "że pętla foreach może zostać przekonwertowana na wyrażenie LINQ", które wygląda następująco: assembly.GetTypes (). Where (type => type.GetCustomAttributes (typeof (HelpAttribute), true) .Length> 0);
David Barrows
107

Cóż, należałoby wyliczyć wszystkie klasy we wszystkich zestawach, które są ładowane do bieżącej domeny aplikacji. Aby to zrobić, należy wywołać GetAssembliesmetodę w AppDomaininstancji dla bieżącej domeny aplikacji.

Stamtąd można wywołać GetExportedTypes(jeśli chcesz tylko typy publiczne) lub GetTypesna każdym Assemblyz nich, aby uzyskać typy zawarte w zestawie.

Następnie możesz wywołać GetCustomAttributesmetodę rozszerzenia dla każdej Typeinstancji, przekazując typ atrybutu, który chcesz znaleźć.

Możesz użyć LINQ, aby uprościć to dla siebie:

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>() };

Powyższe zapytanie dostarczy Ci każdy typ z zastosowanym do niego atrybutem, wraz z wystąpieniem przypisanego (-ych) atrybutu (-ów).

Zwróć uwagę, że jeśli masz dużą liczbę zestawów załadowanych do domeny aplikacji, ta operacja może być kosztowna. Możesz użyć Parallel LINQ, aby skrócić czas operacji, na przykład:

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>() };

Filtrowanie według konkretnego Assemblyjest proste:

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>() };

A jeśli zestaw zawiera dużą liczbę typów, możesz ponownie użyć 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
źródło
1
Wyliczanie wszystkich typów we wszystkich załadowanych zestawach byłoby po prostu bardzo powolne i nie przyniosłoby wiele korzyści. Jest to również potencjalne zagrożenie bezpieczeństwa. Prawdopodobnie możesz przewidzieć, które zestawy będą zawierać typy, które Cię interesują. Po prostu wylicz w nich typy.
Andrew Arnott,
@Andrew Arnott: Dokładnie, ale o to pytano. Wycinanie zapytania dla konkretnego zestawu jest dość łatwe. Ma to również tę dodatkową zaletę, że umożliwia mapowanie między typem a atrybutem.
casperOne
1
Możesz użyć tego samego kodu tylko na bieżącym zestawie za pomocą System.Reflection.Assembly.GetExecutingAssembly ()
Chris Moschini,
@ChrisMoschini Tak, możesz, ale nie zawsze możesz chcieć skanować bieżący zestaw. Lepiej zostaw to otwarte.
casperOne
Robiłem to wiele razy i nie ma wielu sposobów, aby było to wydajne. Możesz pominąć zestawy Microsoft (są one podpisane tym samym kluczem, więc można ich dość łatwo uniknąć przy użyciu AssemblyName. Wyniki można buforować w ramach elementu statycznego, który jest unikatowy dla domeny AppDomain, w której są ładowane zestawy (należy buforować pełne nazwy zestawów, które sprawdziłeś na wypadek, gdyby inne zostały w międzyczasie załadowane) .Znalazłem się tutaj, gdy badam buforowanie załadowanych wystąpień typu atrybutu w atrybucie. Nie jestem pewien tego wzorca, nie jestem pewien, kiedy są one tworzone itp.
34

Inne odpowiedzi odnoszą się do GetCustomAttributes . Dodanie tego jako przykładu użycia IsDefined

Assembly assembly = ...
var typesWithHelpAttribute = 
        from type in assembly.GetTypes()
        where type.IsDefined(typeof(HelpAttribute), false)
        select type;
Jay Walker
źródło
3
Uważam, że jest to właściwe rozwiązanie wykorzystujące metodę przewidzianą przez framework.
Alexey Omelchenko
11

Jak już wspomniano, właściwą drogą jest refleksja. Jeśli zamierzasz często to wywoływać, zdecydowanie sugeruję buforowanie wyników, ponieważ refleksja, szczególnie wyliczanie w każdej klasie, może być dość powolne.

To jest fragment mojego kodu, który przebiega przez wszystkie typy we wszystkich załadowanych zestawach:

// 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
źródło
9

Jest to poprawa wydajności w stosunku do przyjętego rozwiązania. Iterowanie, chociaż wszystkie klasy mogą być powolne, ponieważ jest ich tak wiele. Czasami można odfiltrować cały zespół bez patrzenia na którykolwiek z jego typów.

Na przykład, jeśli szukasz atrybutu, który sam zadeklarowałeś, nie oczekujesz, że żadna z systemowych bibliotek DLL będzie zawierała typy z tym atrybutem. Właściwość Assembly.GlobalAssemblyCache to szybki sposób na sprawdzenie systemowych bibliotek DLL. Kiedy wypróbowałem to na prawdziwym programie, stwierdziłem, że mogę pominąć 30,101 typów i muszę sprawdzić tylko 1983 typy.

Innym sposobem filtrowania jest użycie Assembly.ReferencedAssemblies. Prawdopodobnie jeśli chcesz klas z określonym atrybutem, a ten atrybut jest zdefiniowany w określonym zestawie, to zależy Ci tylko na tym zestawie i innych zestawach, które się do niego odwołują. W moich testach pomogło to nieco bardziej niż sprawdzenie właściwości GlobalAssemblyCache.

Połączyłem oba i uzyskałem to jeszcze szybciej. Poniższy kod zawiera oba filtry.

        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)
Pomysły handlowe Philip
źródło
4

W przypadku ograniczeń Portable .NET powinien działać następujący kod:

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

lub dla dużej liczby zestawów wykorzystujących stan pętli 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;
                }
            }
        }
    }
Lorenz Lo Sauer
źródło
0

Możemy poprawić odpowiedź Andrew i przekonwertować całość na jedno zapytanie LINQ.

    public static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly)
    {
        return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0);
    }
Tachyon
źródło