Come enumerare tutte le classi con l'attributo di classe personalizzato?


151

Domanda basata sull'esempio di MSDN .

Diciamo che abbiamo alcune classi C # con HelpAttribute in un'applicazione desktop autonoma. È possibile enumerare tutte le classi con tale attributo? Ha senso riconoscere le lezioni in questo modo? L'attributo personalizzato verrebbe utilizzato per elencare le possibili opzioni di menu, selezionando l'elemento si aprirà l'istanza dello schermo di tale classe. Il numero di classi / oggetti crescerà lentamente, ma in questo modo possiamo evitare di elencarli altrove, credo.

Risposte:


205

Si assolutamente. Utilizzando Reflection:

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

7
D'accordo, ma in questo caso possiamo farlo in modo dichiarativo secondo la soluzione di casperOne. È bello poter usare la resa, è anche meglio non doverlo fare :)
Jon Skeet

9
Mi piace LINQ. Lo adoro, in realtà. Ma ci vuole una dipendenza da .NET 3.5, che non lo rende. Inoltre, LINQ alla fine si riduce sostanzialmente alla stessa cosa del rendimento. Quindi cosa hai guadagnato? Una particolare sintassi C #, che è una preferenza.
Andrew Arnott,

1
@AndrewArnott Il minor numero di righe di codice più brevi sono irrilevanti per le prestazioni, possono solo contribuire alla leggibilità e alla manutenibilità. Metto in dubbio l'affermazione secondo cui allocano il minor numero di oggetti e le prestazioni saranno più veloci (soprattutto senza prove empiriche); hai praticamente scritto il Selectmetodo di estensione e il compilatore genererà una macchina a stati proprio come farebbe se lo chiamassi a Selectcausa del tuo uso di yield return. Infine, eventuali miglioramenti delle prestazioni che potrebbero essere ottenuti nella maggior parte dei casi sono microottimizzazioni.
casper:

1
Esatto, @casperOne. Una differenza molto piccola, soprattutto rispetto al peso della riflessione stessa. Probabilmente non sarebbe mai uscito in una traccia perfetta.
Andrew Arnott,

1
Naturalmente Resharper dice "che il ciclo foreach può essere convertito in un'espressione LINQ" che assomiglia a questo: assembly.GetTypes (). Where (type => type.GetCustomAttributes (typeof (HelpAttribute), true) .Length> 0);
David Barrows,

107

Bene, dovresti enumerare tutte le classi in tutti gli assembly caricati nel dominio dell'app corrente. Per fare questo, è necessario chiamare il GetAssembliesmetodo sulla AppDomainistanza per il dominio applicazione corrente.

Da lì, chiameresti GetExportedTypes(se vuoi solo tipi pubblici) o GetTypessu ciascuno Assemblyper ottenere i tipi contenuti nell'assembly.

Quindi, chiameresti il GetCustomAttributesmetodo di estensione su ogni Typeistanza, passando il tipo di attributo che desideri trovare.

Puoi usare LINQ per semplificare questo per te:

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

La query sopra ti darà ogni tipo con il tuo attributo applicato ad esso, insieme all'istanza degli attributi assegnati ad esso.

Se si dispone di un numero elevato di assembly caricati nel dominio dell'applicazione, tale operazione potrebbe essere costosa. È possibile utilizzare Parallel LINQ per ridurre il tempo dell'operazione, in questo modo:

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

Filtrarlo su uno specifico Assemblyè semplice:

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

E se l'assembly contiene un gran numero di tipi, è possibile utilizzare nuovamente 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
L'enumerazione di tutti i tipi in tutti gli assembly caricati sarebbe molto lenta e non ti farebbe guadagnare molto. È anche potenzialmente un rischio per la sicurezza. Probabilmente puoi prevedere quali assembly conterranno i tipi che ti interessano. Basta elencare i tipi in quelli.
Andrew Arnott,

@Andrew Arnott: corretto, ma questo è ciò che è stato chiesto. È abbastanza semplice eliminare la query per un determinato assembly. Questo ha anche il vantaggio di darti la mappatura tra il tipo e l'attributo.
casperOne

1
È possibile utilizzare lo stesso codice solo sull'assieme corrente con System.Reflection.Assembly.GetExecutingAssembly ()
Chris Moschini,

@ChrisMoschini Sì, puoi, ma potresti non voler sempre scansionare l'assembly corrente. Meglio lasciarlo aperto.
casper:

L'ho fatto molte volte e non ci sono molti modi per renderlo efficiente. È possibile saltare gli assembly di Microsoft (sono firmati con la stessa chiave, quindi sono abbastanza facili da evitare utilizzando AssemblyName. È possibile memorizzare nella cache i risultati in uno statico, che è unico per AppDomain in cui sono caricati gli assembly (è necessario memorizzare nella cache l'intero nomi degli assembly controllati nel caso in cui altri siano caricati nel frattempo.) Mi sono trovato qui mentre sto studiando la memorizzazione nella cache delle istanze caricate di un tipo di attributo all'interno dell'attributo. Non sono sicuro di quel modello, non sono sicuro di quando sono istanziati, ecc.

34

Altre risposte fanno riferimento a GetCustomAttributes . Aggiungendo questo come esempio di utilizzo di IsDefined

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

3
Credo che sia la soluzione corretta che utilizza il metodo previsto dal framework.
Alexey Omelchenko

11

Come già detto, la riflessione è la strada da percorrere. Se hai intenzione di chiamarlo frequentemente, ti consiglio vivamente di memorizzare nella cache i risultati, poiché la riflessione, in particolare l'enumerazione di ogni classe, può essere piuttosto lenta.

Questo è uno snippet del mio codice che scorre attraverso tutti i tipi in tutti gli assembly caricati:

// 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

Questo è un miglioramento delle prestazioni oltre alla soluzione accettata. L'iterazione sebbene tutte le classi possano essere lente perché ce ne sono così tante. A volte è possibile filtrare un intero assieme senza guardare alcuno dei suoi tipi.

Ad esempio, se stai cercando un attributo che hai dichiarato tu stesso, non ti aspetti che nessuna delle DLL di sistema contenga alcun tipo con quell'attributo. La proprietà Assembly.GlobalAssemblyCache è un modo rapido per verificare la presenza di DLL di sistema. Quando ho provato questo su un vero programma ho scoperto che potevo saltare 30.101 tipi e dovevo solo controllare 1.983 tipi.

Un altro modo per filtrare è utilizzare Assembly.ReferencedAssemblies. Presumibilmente se vuoi classi con un attributo specifico e quell'attributo è definito in un assieme specifico, ti preoccupi solo di quell'assieme e di altri assiemi che lo fanno riferimento. Nei miei test questo mi ha aiutato leggermente di più rispetto al controllo della proprietà GlobalAssemblyCache.

Ho combinato entrambi e l'ho ottenuto ancora più velocemente. Il codice seguente include entrambi i filtri.

        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

In caso di limitazioni di Portable .NET , il seguente codice dovrebbe funzionare:

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

o per un numero elevato di assiemi che utilizzano basato su loop-state 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

Possiamo migliorare la risposta di Andrew e convertire il tutto in una query LINQ.

    public static IEnumerable<Type> GetTypesWithHelpAttribute(Assembly assembly)
    {
        return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(HelpAttribute), true).Length > 0);
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.