Condividi tramite


Procedura: definire un metodo generico con la reflection emit

Aggiornamento: novembre 2007

Nella prima procedura viene illustrato come creare un metodo generico semplice con due parametri di tipo e applicare a questi ultimi vincoli speciali, di interfaccia e di classe.

Nella seconda procedura viene illustrato come creare il corpo del metodo e utilizzare i parametri di tipo del metodo generico per creare istanze di tipi generici e chiamare i relativi metodi.

Nella terza procedura viene illustrato come richiamare il metodo generico.

Nota importante:

Un metodo non può essere considerato generico solo perché appartiene a un tipo generico e ne utilizza i parametri di tipo. Per essere generico, un metodo deve disporre di uno specifico elenco di parametri di tipo. Un metodo generico può appartenere a un tipo non generico, come nell'esempio riportato di seguito. Per un esempio di metodo non generico incluso in un tipo generico, vedere Procedura: definire un tipo generico con la reflection emit.

Per definire un metodo generico

  1. Prima di iniziare, può essere utile esaminare un metodo generico scritto con un linguaggio ad alto livello. Il codice riportato di seguito è incluso nell'esempio relativo a questo argomento, insieme al codice utilizzato per chiamare il metodo generico. Il metodo preso in esame dispone di due parametri di tipo, TInput e TOutput, il secondo dei quali deve essere un tipo di riferimento (class), deve disporre di un costruttore senza parametri (new) e deve implementare ICollection(Of TInput) (ICollection<TInput> in C#). Questo vincolo di interfaccia garantisce la possibilità di utilizzare il metodo ICollection<T>.Add per aggiungere elementi all'insieme TOutput creato. Il metodo dispone di un unico parametro formale, input (una matrice di TInput), crea un insieme di tipo TOutput e copia gli elementi di input nell'insieme.

    Public Shared Function Factory(Of TInput, _
        TOutput As {ICollection(Of TInput), Class, New}) _
        (ByVal input() As TInput) As TOutput
    
        Dim retval As New TOutput()
        Dim ic As ICollection(Of TInput) = retval
    
        For Each t As TInput In input
            ic.Add(t)
        Next
    
        Return retval
    End Function 
    
    public static TOutput Factory<TInput, TOutput>(TInput[] tarray) 
        where TOutput : class, ICollection<TInput>, new()
    {
        TOutput ret = new TOutput();
        ICollection<TInput> ic = ret;
    
        foreach (TInput t in tarray)
        {
            ic.Add(t);
        }
        return ret;
    }
    
  2. Definire un assembly dinamico e un modulo dinamico per contenere il tipo a cui appartiene il metodo generico. In questo caso l'assembly dispone di un unico modulo, il cui nome (DemoMethodBuilder1) corrisponde a quello dell'assembly seguito da un'estensione. In questo esempio l'assembly viene salvato su disco e anche eseguito, pertanto è specificato AssemblyBuilderAccess.RunAndSave. È possibile utilizzare lo strumento Disassembler MSIL (Ildasm.exe) per esaminare DemoMethodBuilder1.dll ed eseguirne un confronto con il codice Microsoft Intermediate Language (MSIL) relativo al metodo illustrato nel primo passaggio.

    Dim asmName As New AssemblyName("DemoMethodBuilder1")
    Dim domain As AppDomain = AppDomain.CurrentDomain
    Dim demoAssembly As AssemblyBuilder = _
        domain.DefineDynamicAssembly(asmName, _
            AssemblyBuilderAccess.RunAndSave)
    
    ' Define the module that contains the code. For an 
    ' assembly with one module, the module name is the 
    ' assembly name plus a file extension.
    Dim demoModule As ModuleBuilder = _
        demoAssembly.DefineDynamicModule( _
            asmName.Name, _
            asmName.Name & ".dll")
    
    AssemblyName asmName = new AssemblyName("DemoMethodBuilder1");
    AppDomain domain = AppDomain.CurrentDomain;
    AssemblyBuilder demoAssembly = 
        domain.DefineDynamicAssembly(asmName, 
            AssemblyBuilderAccess.RunAndSave);
    
    // Define the module that contains the code. For an 
    // assembly with one module, the module name is the 
    // assembly name plus a file extension.
    ModuleBuilder demoModule = 
        demoAssembly.DefineDynamicModule(asmName.Name, 
            asmName.Name+".dll");
    
  3. Definire il tipo a cui appartiene il metodo generico. Il tipo non deve essere necessariamente generico. Un metodo generico può infatti appartenere a un tipo generico o non generico. In questo esempio il tipo è una classe, non è generico ed è denominato DemoType.

    Dim demoType As TypeBuilder = demoModule.DefineType( _
        "DemoType", _
        TypeAttributes.Public) 
    
    TypeBuilder demoType = 
        demoModule.DefineType("DemoType", TypeAttributes.Public);
    
  4. Definire il metodo generico. Se i tipi dei parametri formali di un metodo generico sono specificati dai parametri di tipo generico di tale metodo, utilizzare l'overload del metodo DefineMethod(String, MethodAttributes) per definire il metodo. I parametri di tipo generico del metodo non sono ancora definiti e non è pertanto possibile specificare i tipi dei parametri formali del metodo nella chiamata a DefineMethod. In questo esempio il metodo è denominato Factory, è pubblico e static (Shared in Visual Basic).

    Dim factory As MethodBuilder = _
        demoType.DefineMethod("Factory", _
            MethodAttributes.Public Or MethodAttributes.Static)
    
    MethodBuilder factory = 
        demoType.DefineMethod("Factory", 
            MethodAttributes.Public | MethodAttributes.Static);
    
  5. Definire i parametri di tipo generico di DemoMethod passando una matrice di stringhe contenenti i nomi dei parametri al metodo MethodBuilder.DefineGenericParameters. In questo modo si ottiene un metodo generico. Nel codice riportato di seguito, Factory diventa un metodo generico con i parametri di tipo TInput e TOutput. Per migliorare la leggibilità del codice, vengono create variabili con questi nomi per contenere gli oggetti GenericTypeParameterBuilder che rappresentano i due parametri di tipo.

    Dim typeParameterNames() As String = {"TInput", "TOutput"}
    Dim typeParameters() As GenericTypeParameterBuilder = _
        factory.DefineGenericParameters(typeParameterNames)
    
    Dim TInput As GenericTypeParameterBuilder = typeParameters(0)
    Dim TOutput As GenericTypeParameterBuilder = typeParameters(1)
    
    string[] typeParameterNames = {"TInput", "TOutput"};
    GenericTypeParameterBuilder[] typeParameters = 
        factory.DefineGenericParameters(typeParameterNames);
    
    GenericTypeParameterBuilder TInput = typeParameters[0];
    GenericTypeParameterBuilder TOutput = typeParameters[1];
    
  6. Facoltativamente, aggiungere vincoli speciali ai parametri di tipo. L'aggiunta di vincoli speciali viene eseguita tramite il metodo SetGenericParameterAttributes. In base ai vincoli definiti in questo esempio, TOutput deve essere un tipo di riferimento e deve disporre di un costruttore senza parametri.

    TOutput.SetGenericParameterAttributes( _
        GenericParameterAttributes.ReferenceTypeConstraint Or _
        GenericParameterAttributes.DefaultConstructorConstraint)
    
    TOutput.SetGenericParameterAttributes(
        GenericParameterAttributes.ReferenceTypeConstraint | 
        GenericParameterAttributes.DefaultConstructorConstraint);
    
  7. Facoltativamente, aggiungere vincoli di interfaccia e di classe ai parametri di tipo. In questo esempio il parametro di tipo TOutput è vincolato a tipi che implementano l'interfaccia ICollection(Of TInput) (ICollection<TInput> in C#). Questo vincolo garantisce la possibilità di utilizzare il metodo Add per l'aggiunta di elementi.

    Dim icoll As Type = GetType(ICollection(Of ))
    Dim icollOfTInput As Type = icoll.MakeGenericType(TInput)
    Dim constraints() As Type = { icollOfTInput }
    TOutput.SetInterfaceConstraints(constraints)
    
    Type icoll = typeof(ICollection<>);
    Type icollOfTInput = icoll.MakeGenericType(TInput);
    Type[] constraints = {icollOfTInput};
    TOutput.SetInterfaceConstraints(constraints);
    
  8. Definire i parametri formali del metodo utilizzando il metodo SetParameters. In questo esempio il metodo Factory dispone di un unico parametro, una matrice di TInput. Il tipo viene creato chiamando il metodo MakeArrayType sull'oggetto GenericTypeParameterBuilder che rappresenta TInput. L'argomento di SetParameters è una matrice di oggetti Type.

    Dim params() As Type = { TInput.MakeArrayType() }
    factory.SetParameters(params)
    
    Type[] parms = {TInput.MakeArrayType()};
    factory.SetParameters(parms);
    
  9. Definire il tipo restituito per il metodo utilizzando il metodo SetReturnType. In questo esempio viene restituita un'istanza di TOutput.

    factory.SetReturnType(TOutput)
    
    factory.SetReturnType(TOutput);
    
  10. Creare il corpo del metodo utilizzando ILGenerator. Per informazioni dettagliate, vedere la procedura associata Per creare il corpo del metodo.

    Nota importante:

    Quando si creano chiamate a metodi di tipi generici e gli argomenti di tipo di questi ultimi sono parametri di tipo del metodo generico, è necessario utilizzare gli overload dei metodi staticGetConstructor(Type, ConstructorInfo), GetMethod(Type, MethodInfo) e GetField(Type, FieldInfo) della classe TypeBuilder per ottenere i formati costruiti dei metodi. Questo concetto è illustrato nella procedura associata per la creazione del corpo del metodo.

  11. Completare il tipo contenente il metodo e salvare l'assembly. Nella procedura associata Per richiamare il metodo generico vengono indicati due modi per richiamare il metodo completato.

    ' Complete the type.
    Dim dt As Type = demoType.CreateType()
    ' Save the assembly, so it can be examined with Ildasm.exe.
    demoAssembly.Save(asmName.Name & ".dll")
    
    // Complete the type.
    Type dt = demoType.CreateType();
    // Save the assembly, so it can be examined with Ildasm.exe.
    demoAssembly.Save(asmName.Name+".dll");
    

Per creare il corpo del metodo

  1. Ottenere un generatore di codice e dichiarare le variabili locali e le etichette. Per dichiarare le variabili locali viene utilizzato il metodo DeclareLocal. Il metodo Factory dispone di quattro variabili locali: retVal, per contenere il nuovo oggetto TOutput restituito dal metodo, ic, per contenere l'oggetto TOutput quando ne viene eseguito il cast in ICollection(Of TInput) (ICollection<TInput> in C#), input, per contenere la matrice di input di oggetti TInput, e index, per scorrere l'insieme. Il metodo dispone inoltre di due etichette, una per l'ingresso nel ciclo (enterLoop) e l'altra per l'inizio del ciclo (loopAgain), definite tramite il metodo DefineLabel.

    La prima operazione eseguita dal metodo consiste nel caricare il relativo argomento mediante il codice operativo Ldarg_0 e nel memorizzarlo nella variabile locale input mediante il codice operativo Stloc_S.

    Dim ilgen As ILGenerator = factory.GetILGenerator()
    
    Dim retVal As LocalBuilder = ilgen.DeclareLocal(TOutput)
    Dim ic As LocalBuilder = ilgen.DeclareLocal(icollOfTInput)
    Dim input As LocalBuilder = _
        ilgen.DeclareLocal(TInput.MakeArrayType())
    Dim index As LocalBuilder = _
        ilgen.DeclareLocal(GetType(Integer))
    
    Dim enterLoop As Label = ilgen.DefineLabel()
    Dim loopAgain As Label = ilgen.DefineLabel()
    
    ilgen.Emit(OpCodes.Ldarg_0)
    ilgen.Emit(OpCodes.Stloc_S, input)
    
    ILGenerator ilgen = factory.GetILGenerator();
    
    LocalBuilder retVal = ilgen.DeclareLocal(TOutput);
    LocalBuilder ic = ilgen.DeclareLocal(icollOfTInput);
    LocalBuilder input = ilgen.DeclareLocal(TInput.MakeArrayType());
    LocalBuilder index = ilgen.DeclareLocal(typeof(int));
    
    Label enterLoop = ilgen.DefineLabel();
    Label loopAgain = ilgen.DefineLabel();
    
    ilgen.Emit(OpCodes.Ldarg_0);
    ilgen.Emit(OpCodes.Stloc_S, input);
    
  2. Creare il codice per generare un'istanza di TOutput utilizzando l'overload di metodo generico del metodo Activator.CreateInstance. Per l'utilizzo di questo overload, è necessario che il tipo specificato disponga di un costruttore senza parametri. Per questo motivo viene aggiunto tale vincolo a TOutput. Creare il metodo generico costruito passando TOutput a MakeGenericMethod. Una volta creato il codice per la chiamata al metodo, creare il codice per memorizzarlo nella variabile locale retVal utilizzando Stloc_S

    Dim createInst As MethodInfo = _
        GetType(Activator).GetMethod("CreateInstance", Type.EmptyTypes)
    Dim createInstOfTOutput As MethodInfo = _
        createInst.MakeGenericMethod(TOutput)
    
    ilgen.Emit(OpCodes.Call, createInstOfTOutput)
    ilgen.Emit(OpCodes.Stloc_S, retVal)
    
    MethodInfo createInst = 
        typeof(Activator).GetMethod("CreateInstance", Type.EmptyTypes);
    MethodInfo createInstOfTOutput = 
        createInst.MakeGenericMethod(TOutput);
    
    ilgen.Emit(OpCodes.Call, createInstOfTOutput);
    ilgen.Emit(OpCodes.Stloc_S, retVal);
    
  3. Creare il codice per eseguire il cast del nuovo oggetto TOutput in ICollection(Of TInput) e memorizzarlo nella variabile locale ic.

    ilgen.Emit(OpCodes.Ldloc_S, retVal)
    ilgen.Emit(OpCodes.Box, icollOfTInput)
    ilgen.Emit(OpCodes.Castclass, icollOfTInput)
    ilgen.Emit(OpCodes.Stloc_S, ic)
    
    ilgen.Emit(OpCodes.Ldloc_S, retVal);
    ilgen.Emit(OpCodes.Box, icollOfTInput);
    ilgen.Emit(OpCodes.Castclass, icollOfTInput);
    ilgen.Emit(OpCodes.Stloc_S, ic);
    
  4. Ottenere un oggetto MethodInfo che rappresenta il metodo ICollection<T>.Add. Poiché il metodo opera su un'interfaccia ICollection(Of TInput) (ICollection<TInput> in C#), è necessario ottenere il metodo Add specifico per il tipo costruito. Non è possibile utilizzare il metodo GetMethod per ottenere MethodInfo direttamente da icollOfTInput perché GetMethod non è supportato su un tipo costruito con un oggetto GenericTypeParameterBuilder. Chiamare invece MethodInfo su icoll, che contiene la definizione di tipo generico per l'interfaccia generica ICollection<T>, e quindi utilizzare il metodo staticGetMethod(Type, MethodInfo) per generare l'oggetto MethodInfo relativo al tipo costruito. Questo concetto è illustrato nel codice riportato di seguito.

    Dim mAddPrep As MethodInfo = icoll.GetMethod("Add")
    Dim mAdd As MethodInfo = _
        TypeBuilder.GetMethod(icollOfTInput, mAddPrep)
    
    MethodInfo mAddPrep = icoll.GetMethod("Add");
    MethodInfo mAdd = TypeBuilder.GetMethod(icollOfTInput, mAddPrep);
    
  5. Creare il codice per inizializzare la variabile index caricando un valore integer 0 a 32 bit e memorizzandolo nella variabile. Creare il codice per diramare all'etichetta enterLoop. Poiché si trova all'interno del ciclo, l'etichetta non è ancora contrassegnata. Il codice per il ciclo verrà creato nel passaggio successivo.

    ' Initialize the count and enter the loop.
    ilgen.Emit(OpCodes.Ldc_I4_0)
    ilgen.Emit(OpCodes.Stloc_S, index)
    ilgen.Emit(OpCodes.Br_S, enterLoop)
    
    // Initialize the count and enter the loop.
    ilgen.Emit(OpCodes.Ldc_I4_0);
    ilgen.Emit(OpCodes.Stloc_S, index);
    ilgen.Emit(OpCodes.Br_S, enterLoop);
    
  6. Creare il codice per il ciclo. Il primo passaggio consiste nel contrassegnare l'inizio del ciclo chiamando il metodo MarkLabel con l'etichetta loopAgain. Le istruzioni di diramazione che utilizzano l'etichetta passeranno a questo punto del codice. Il passaggio successivo prevede l'inserimento nello stack dell'oggetto TOutput, di cui è stato eseguito il cast in ICollection(Of TInput). Questa operazione non deve essere eseguita immediatamente, ma deve comunque essere prevista per la chiamata al metodo Add. Dopo l'inserimento della matrice di input nello stack, è necessario inserire la variabile index contenente l'indice corrente nella matrice. Il codice operativo Ldelem estrae l'indice e la matrice dallo stack e inserisce l'elemento della matrice indicizzata nello stack. A questo punto, lo stack è pronto per la chiamata al metodo ICollection<T>.Add, che estrae l'insieme e il nuovo elemento dallo stack e aggiunge il secondo al primo.

    La parte del codice rimanente nel ciclo incrementa l'indice e verifica se il ciclo è terminato. L'indice e un valore integer 1 a 32 bit vengono inseriti nello stack e addizionati. La somma risultante rimane nello stack e viene memorizzata nella variabile index. Per impostare questo punto come punto di ingresso per il ciclo, viene chiamato il metodo MarkLabel. L'indice viene nuovamente caricato. La matrice di input viene inserita nello stack e viene creato Ldlen per ottenere la relativa lunghezza. Per eseguire il confronto tra l'indice e la lunghezza, ora presenti nello stack, viene creato Clt. Se l'indice è minore della lunghezza, Brtrue_S torna all'inizio del ciclo.

    ilgen.MarkLabel(loopAgain)
    
    ilgen.Emit(OpCodes.Ldloc_S, ic)
    ilgen.Emit(OpCodes.Ldloc_S, input)
    ilgen.Emit(OpCodes.Ldloc_S, index)
    ilgen.Emit(OpCodes.Ldelem, TInput)
    ilgen.Emit(OpCodes.Callvirt, mAdd)
    
    ilgen.Emit(OpCodes.Ldloc_S, index)
    ilgen.Emit(OpCodes.Ldc_I4_1)
    ilgen.Emit(OpCodes.Add)
    ilgen.Emit(OpCodes.Stloc_S, index)
    
    ilgen.MarkLabel(enterLoop)
    ilgen.Emit(OpCodes.Ldloc_S, index)
    ilgen.Emit(OpCodes.Ldloc_S, input)
    ilgen.Emit(OpCodes.Ldlen)
    ilgen.Emit(OpCodes.Conv_I4)
    ilgen.Emit(OpCodes.Clt)
    ilgen.Emit(OpCodes.Brtrue_S, loopAgain)
    
    ilgen.MarkLabel(loopAgain);
    
    ilgen.Emit(OpCodes.Ldloc_S, ic);
    ilgen.Emit(OpCodes.Ldloc_S, input);
    ilgen.Emit(OpCodes.Ldloc_S, index);
    ilgen.Emit(OpCodes.Ldelem, TInput);
    ilgen.Emit(OpCodes.Callvirt, mAdd);
    
    ilgen.Emit(OpCodes.Ldloc_S, index);
    ilgen.Emit(OpCodes.Ldc_I4_1);
    ilgen.Emit(OpCodes.Add);
    ilgen.Emit(OpCodes.Stloc_S, index);
    
    ilgen.MarkLabel(enterLoop);
    ilgen.Emit(OpCodes.Ldloc_S, index);
    ilgen.Emit(OpCodes.Ldloc_S, input);
    ilgen.Emit(OpCodes.Ldlen);
    ilgen.Emit(OpCodes.Conv_I4);
    ilgen.Emit(OpCodes.Clt);
    ilgen.Emit(OpCodes.Brtrue_S, loopAgain);
    
  7. Creare il codice per inserire l'oggetto TOutput nello stack e uscire dal metodo. Entrambe le variabili locali retVal e ic contengono riferimenti al nuovo oggetto TOutput. ic viene utilizzata solo per accedere al metodo ICollection<T>.Add.

    ilgen.Emit(OpCodes.Ldloc_S, retVal)
    ilgen.Emit(OpCodes.Ret)
    
    ilgen.Emit(OpCodes.Ldloc_S, retVal);
    ilgen.Emit(OpCodes.Ret);
    

Per richiamare il metodo generico

  1. Factory è una definizione di metodo generico. Per richiamarla, è necessario assegnare tipi ai relativi parametri di tipo generico. A questo scopo, utilizzare il metodo MakeGenericMethod. Nel codice riportato di seguito viene creato un metodo generico costruito, specificando String per TInput e List(Of String) (List<string> in C#) per TOutput, e viene visualizzata una rappresentazione in formato stringa del metodo.

    Dim m As MethodInfo = dt.GetMethod("Factory")
    Dim bound As MethodInfo = m.MakeGenericMethod( _
        GetType(String), GetType(List(Of String)))
    
    ' Display a string representing the bound method.
    Console.WriteLine(bound)
    
    MethodInfo m = dt.GetMethod("Factory");
    MethodInfo bound = 
        m.MakeGenericMethod(typeof(string), typeof(List<string>));
    
    // Display a string representing the bound method.
    Console.WriteLine(bound);
    
  2. Per richiamare il metodo con associazione tardiva, utilizzare il metodo Invoke. Nel codice riportato di seguito una matrice di Object, contenente come unico elemento una matrice di stringhe, viene creata e quindi passata come elenco di argomenti per il metodo generico. Il primo parametro di Invoke è un riferimento null perché il metodo è static. Viene eseguito il cast del valore restituito in List(Of String) e viene visualizzato il primo elemento.

    Dim o As Object = bound.Invoke(Nothing, New Object() { arr })
    Dim list2 As List(Of String) = CType(o, List(Of String))
    
    Console.WriteLine("The first element is: {0}", list2(0))
    
    object o = bound.Invoke(null, new object[]{arr});
    List<string> list2 = (List<string>) o;
    
    Console.WriteLine("The first element is: {0}", list2[0]);
    
  3. Per richiamare il metodo tramite un delegato, è necessario disporre di un delegato che corrisponda alla firma del metodo generico costruito. Il modo più semplice per eseguire questa operazione consiste nel creare un delegato generico. Nel codice riportato di seguito viene creata un'istanza del delegato generico D, definito nel codice di esempio, tramite l'overload del metodo Delegate.CreateDelegate(Type, MethodInfo) e quindi viene richiamato il delegato. I delegati offrono migliori prestazioni rispetto alle chiamate con associazione tardiva.

    Dim dType As Type = GetType(D(Of String, List(Of String)))
    Dim test As D(Of String, List(Of String))
    test = CType( _
        [Delegate].CreateDelegate(dType, bound), _
        D(Of String, List(Of String)))
    
    Dim list3 As List(Of String) = test(arr)
    Console.WriteLine("The first element is: {0}", list3(0))
    
    Type dType = typeof(D<string, List <string>>);
    D<string, List <string>> test;
    test = (D<string, List <string>>) 
        Delegate.CreateDelegate(dType, bound);
    
    List<string> list3 = test(arr);
    Console.WriteLine("The first element is: {0}", list3[0]);
    
  4. Il metodo creato può essere chiamato anche da un programma che fa riferimento all'assembly salvato.

Esempio

Nell'esempio di codice riportato di seguito viene creato un tipo non generico, DemoType, con un metodo generico, Factory. Questo metodo presenta due parametri di tipo generico, ossia TInput per specificare un tipo di input e TOutput per specificare un tipo di output. Al parametro di tipo TOutput sono applicati vincoli affinché implementi ICollection<TInput> (ICollection(Of TInput) in Visual Basic), sia un tipo di riferimento e disponga di un costruttore senza parametri.

Il metodo dispone di un unico parametro formale, che è una matrice di TInput, e restituisce un'istanza di TOutput contenente tutti gli elementi della matrice di input. TOutput può essere qualsiasi tipo di insieme generico che implementa l'interfaccia generica ICollection<T>.

Quando il codice viene eseguito, l'assembly dinamico viene salvato come DemoGenericMethod1.dll e può essere esaminato mediante lo strumento Disassembler MSIL (Ildasm.exe).

Nota:

Per acquisire familiarità con le procedure per la creazione di codice, si consiglia di scrivere un programma Visual Basic, C# o Visual C++ che esegua l'attività che si tenta di creare e utilizzare il disassembler per esaminare il codice MSIL prodotto dal compilatore.

Nell'esempio di codice è incluso il codice sorgente equivalente al metodo creato, che viene richiamato con associazione tardiva e anche tramite un delegato generico dichiarato nell'esempio di codice.

Imports System
Imports System.Collections.Generic
Imports System.Reflection
Imports System.Reflection.Emit

' Declare a generic delegate that can be used to execute the 
' finished method.
'
Delegate Function D(Of TIn, TOut)(ByVal input() As TIn) As TOut

Class GenericMethodBuilder

    ' This method shows how to declare, in Visual Basic, the generic
    ' method this program emits. The method has two type parameters,
    ' TInput and TOutput, the second of which must be a reference type
    ' (Class), must have a parameterless constructor (New), and must
    ' implement ICollection(Of TInput). This interface constraint
    ' ensures that ICollection(Of TInput).Add can be used to add
    ' elements to the TOutput object the method creates. The method 
    ' has one formal parameter, input, which is an array of TInput. 
    ' The elements of this array are copied to the new TOutput.
    '
    Public Shared Function Factory(Of TInput, _
        TOutput As {ICollection(Of TInput), Class, New}) _
        (ByVal input() As TInput) As TOutput

        Dim retval As New TOutput()
        Dim ic As ICollection(Of TInput) = retval

        For Each t As TInput In input
            ic.Add(t)
        Next

        Return retval
    End Function 


    Public Shared Sub Main()
        ' The following shows the usage syntax of the Visual Basic
        ' version of the generic method emitted by this program.
        ' Note that the generic parameters must be specified 
        ' explicitly, because the compiler does not have enough 
        ' context to infer the type of TOutput. In this case, TOutput
        ' is a generic List containing strings.
        ' 
        Dim arr() As String = {"a", "b", "c", "d", "e"}
        Dim list1 As List(Of String) = _
            GenericMethodBuilder.Factory(Of String, List(Of String))(arr)
        Console.WriteLine("The first element is: {0}", list1(0))


        ' Creating a dynamic assembly requires an AssemblyName
        ' object, and the current application domain.
        '
        Dim asmName As New AssemblyName("DemoMethodBuilder1")
        Dim domain As AppDomain = AppDomain.CurrentDomain
        Dim demoAssembly As AssemblyBuilder = _
            domain.DefineDynamicAssembly(asmName, _
                AssemblyBuilderAccess.RunAndSave)

        ' Define the module that contains the code. For an 
        ' assembly with one module, the module name is the 
        ' assembly name plus a file extension.
        Dim demoModule As ModuleBuilder = _
            demoAssembly.DefineDynamicModule( _
                asmName.Name, _
                asmName.Name & ".dll")

        ' Define a type to contain the method.
        Dim demoType As TypeBuilder = demoModule.DefineType( _
            "DemoType", _
            TypeAttributes.Public) 

        ' Define a Shared, Public method with standard calling
        ' conventions. Do not specify the parameter types or the
        ' return type, because type parameters will be used for 
        ' those types, and the type parameters have not been
        ' defined yet.
        '
        Dim factory As MethodBuilder = _
            demoType.DefineMethod("Factory", _
                MethodAttributes.Public Or MethodAttributes.Static)

        ' Defining generic type parameters for the method makes it a
        ' generic method. To make the code easier to read, each
        ' type parameter is copied to a variable of the same name.
        '
        Dim typeParameterNames() As String = {"TInput", "TOutput"}
        Dim typeParameters() As GenericTypeParameterBuilder = _
            factory.DefineGenericParameters(typeParameterNames)

        Dim TInput As GenericTypeParameterBuilder = typeParameters(0)
        Dim TOutput As GenericTypeParameterBuilder = typeParameters(1)

        ' Add special constraints.
        ' The type parameter TOutput is constrained to be a reference
        ' type, and to have a parameterless constructor. This ensures
        ' that the Factory method can create the collection type.
        ' 
        TOutput.SetGenericParameterAttributes( _
            GenericParameterAttributes.ReferenceTypeConstraint Or _
            GenericParameterAttributes.DefaultConstructorConstraint)

        ' Add interface and base type constraints.
        ' The type parameter TOutput is constrained to types that
        ' implement the ICollection(Of T) interface, to ensure that
        ' they have an Add method that can be used to add elements.
        '
        ' To create the constraint, first use MakeGenericType to bind 
        ' the type parameter TInput to the ICollection(Of T) interface,
        ' returning the type ICollection(Of TInput), then pass
        ' the newly created type to the SetInterfaceConstraints
        ' method. The constraints must be passed as an array, even if
        ' there is only one interface.
        '
        Dim icoll As Type = GetType(ICollection(Of ))
        Dim icollOfTInput As Type = icoll.MakeGenericType(TInput)
        Dim constraints() As Type = { icollOfTInput }
        TOutput.SetInterfaceConstraints(constraints)

        ' Set parameter types for the method. The method takes
        ' one parameter, an array of type TInput.
        Dim params() As Type = { TInput.MakeArrayType() }
        factory.SetParameters(params)

        ' Set the return type for the method. The return type is
        ' the generic type parameter TOutput.
        factory.SetReturnType(TOutput)

        ' Generate a code body for the method. 
        ' -----------------------------------
        ' Get a code generator and declare local variables and
        ' labels. Save the input array to a local variable.
        '
        Dim ilgen As ILGenerator = factory.GetILGenerator()

        Dim retVal As LocalBuilder = ilgen.DeclareLocal(TOutput)
        Dim ic As LocalBuilder = ilgen.DeclareLocal(icollOfTInput)
        Dim input As LocalBuilder = _
            ilgen.DeclareLocal(TInput.MakeArrayType())
        Dim index As LocalBuilder = _
            ilgen.DeclareLocal(GetType(Integer))

        Dim enterLoop As Label = ilgen.DefineLabel()
        Dim loopAgain As Label = ilgen.DefineLabel()

        ilgen.Emit(OpCodes.Ldarg_0)
        ilgen.Emit(OpCodes.Stloc_S, input)

        ' Create an instance of TOutput, using the generic method 
        ' overload of the Activator.CreateInstance method. 
        ' Using this overload requires the specified type to have
        ' a parameterless constructor, which is the reason for adding 
        ' that constraint to TOutput. Create the constructed generic
        ' method by passing TOutput to MakeGenericMethod. After
        ' emitting code to call the method, emit code to store the
        ' new TOutput in a local variable. 
        '
        Dim createInst As MethodInfo = _
            GetType(Activator).GetMethod("CreateInstance", Type.EmptyTypes)
        Dim createInstOfTOutput As MethodInfo = _
            createInst.MakeGenericMethod(TOutput)

        ilgen.Emit(OpCodes.Call, createInstOfTOutput)
        ilgen.Emit(OpCodes.Stloc_S, retVal)

        ' Load the reference to the TOutput object, cast it to
        ' ICollection(Of TInput), and save it.
        ilgen.Emit(OpCodes.Ldloc_S, retVal)
        ilgen.Emit(OpCodes.Box, icollOfTInput)
        ilgen.Emit(OpCodes.Castclass, icollOfTInput)
        ilgen.Emit(OpCodes.Stloc_S, ic)

        ' Loop through the array, adding each element to the new
        ' instance of TOutput. Note that in order to get a MethodInfo
        ' for ICollection(Of TInput).Add, it is necessary to first 
        ' get the Add method for the generic type defintion,
        ' ICollection(Of T).Add. This is because it is not possible
        ' to call GetMethod on icollOfTInput. The static overload of
        ' TypeBuilder.GetMethod produces the correct MethodInfo for
        ' the constructed type.
        '
        Dim mAddPrep As MethodInfo = icoll.GetMethod("Add")
        Dim mAdd As MethodInfo = _
            TypeBuilder.GetMethod(icollOfTInput, mAddPrep)

        ' Initialize the count and enter the loop.
        ilgen.Emit(OpCodes.Ldc_I4_0)
        ilgen.Emit(OpCodes.Stloc_S, index)
        ilgen.Emit(OpCodes.Br_S, enterLoop)

        ' Mark the beginning of the loop. Push the ICollection
        ' reference on the stack, so it will be in position for the
        ' call to Add. Then push the array and the index on the 
        ' stack, get the array element, and call Add (represented
        ' by the MethodInfo mAdd) to add it to the collection.
        ' 
        ' The other ten instructions just increment the index
        ' and test for the end of the loop. Note the MarkLabel
        ' method, which sets the point in the code where the 
        ' loop is entered. (See the earlier Br_S to enterLoop.)
        '
        ilgen.MarkLabel(loopAgain)

        ilgen.Emit(OpCodes.Ldloc_S, ic)
        ilgen.Emit(OpCodes.Ldloc_S, input)
        ilgen.Emit(OpCodes.Ldloc_S, index)
        ilgen.Emit(OpCodes.Ldelem, TInput)
        ilgen.Emit(OpCodes.Callvirt, mAdd)

        ilgen.Emit(OpCodes.Ldloc_S, index)
        ilgen.Emit(OpCodes.Ldc_I4_1)
        ilgen.Emit(OpCodes.Add)
        ilgen.Emit(OpCodes.Stloc_S, index)

        ilgen.MarkLabel(enterLoop)
        ilgen.Emit(OpCodes.Ldloc_S, index)
        ilgen.Emit(OpCodes.Ldloc_S, input)
        ilgen.Emit(OpCodes.Ldlen)
        ilgen.Emit(OpCodes.Conv_I4)
        ilgen.Emit(OpCodes.Clt)
        ilgen.Emit(OpCodes.Brtrue_S, loopAgain)

        ilgen.Emit(OpCodes.Ldloc_S, retVal)
        ilgen.Emit(OpCodes.Ret)

        ' Complete the type.
        Dim dt As Type = demoType.CreateType()
        ' Save the assembly, so it can be examined with Ildasm.exe.
        demoAssembly.Save(asmName.Name & ".dll")

        ' To create a constructed generic method that can be
        ' executed, first call the GetMethod method on the completed 
        ' type to get the generic method definition. Call MakeGenericType
        ' on the generic method definition to obtain the constructed
        ' method, passing in the type arguments. In this case, the
        ' constructed method has String for TInput and List(Of String)
        ' for TOutput. 
        '
        Dim m As MethodInfo = dt.GetMethod("Factory")
        Dim bound As MethodInfo = m.MakeGenericMethod( _
            GetType(String), GetType(List(Of String)))

        ' Display a string representing the bound method.
        Console.WriteLine(bound)


        ' Once the generic method is constructed, 
        ' you can invoke it and pass in an array of objects 
        ' representing the arguments. In this case, there is only
        ' one element in that array, the argument 'arr'.
        '
        Dim o As Object = bound.Invoke(Nothing, New Object() { arr })
        Dim list2 As List(Of String) = CType(o, List(Of String))

        Console.WriteLine("The first element is: {0}", list2(0))


        ' You can get better performance from multiple calls if
        ' you bind the constructed method to a delegate. The 
        ' following code uses the generic delegate D defined 
        ' earlier.
        '
        Dim dType As Type = GetType(D(Of String, List(Of String)))
        Dim test As D(Of String, List(Of String))
        test = CType( _
            [Delegate].CreateDelegate(dType, bound), _
            D(Of String, List(Of String)))

        Dim list3 As List(Of String) = test(arr)
        Console.WriteLine("The first element is: {0}", list3(0))

    End Sub  
End Class 

' This code example produces the following output:
'
'The first element is: a
'System.Collections.Generic.List`1[System.String] Factory[String,List`1](System.String[])
'The first element is: a
'The first element is: a
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

// Declare a generic delegate that can be used to execute the 
// finished method.
//
public delegate TOut D<TIn, TOut>(TIn[] input);

class GenericMethodBuilder
{
    // This method shows how to declare, in Visual Basic, the generic
    // method this program emits. The method has two type parameters,
    // TInput and TOutput, the second of which must be a reference type
    // (class), must have a parameterless constructor (new()), and must
    // implement ICollection<TInput>. This interface constraint
    // ensures that ICollection<TInput>.Add can be used to add
    // elements to the TOutput object the method creates. The method 
    // has one formal parameter, input, which is an array of TInput. 
    // The elements of this array are copied to the new TOutput.
    //
    public static TOutput Factory<TInput, TOutput>(TInput[] tarray) 
        where TOutput : class, ICollection<TInput>, new()
    {
        TOutput ret = new TOutput();
        ICollection<TInput> ic = ret;

        foreach (TInput t in tarray)
        {
            ic.Add(t);
        }
        return ret;
    }

    public static void Main()
    {
        // The following shows the usage syntax of the C#
        // version of the generic method emitted by this program.
        // Note that the generic parameters must be specified 
        // explicitly, because the compiler does not have enough 
        // context to infer the type of TOutput. In this case, TOutput
        // is a generic List containing strings.
        // 
        string[] arr = {"a", "b", "c", "d", "e"};
        List<string> list1 = 
            GenericMethodBuilder.Factory<string, List <string>>(arr);
        Console.WriteLine("The first element is: {0}", list1[0]);


        // Creating a dynamic assembly requires an AssemblyName
        // object, and the current application domain.
        //
        AssemblyName asmName = new AssemblyName("DemoMethodBuilder1");
        AppDomain domain = AppDomain.CurrentDomain;
        AssemblyBuilder demoAssembly = 
            domain.DefineDynamicAssembly(asmName, 
                AssemblyBuilderAccess.RunAndSave);

        // Define the module that contains the code. For an 
        // assembly with one module, the module name is the 
        // assembly name plus a file extension.
        ModuleBuilder demoModule = 
            demoAssembly.DefineDynamicModule(asmName.Name, 
                asmName.Name+".dll");

        // Define a type to contain the method.
        TypeBuilder demoType = 
            demoModule.DefineType("DemoType", TypeAttributes.Public);

        // Define a public static method with standard calling
        // conventions. Do not specify the parameter types or the
        // return type, because type parameters will be used for 
        // those types, and the type parameters have not been
        // defined yet.
        //
        MethodBuilder factory = 
            demoType.DefineMethod("Factory", 
                MethodAttributes.Public | MethodAttributes.Static);

        // Defining generic type parameters for the method makes it a
        // generic method. To make the code easier to read, each
        // type parameter is copied to a variable of the same name.
        //
        string[] typeParameterNames = {"TInput", "TOutput"};
        GenericTypeParameterBuilder[] typeParameters = 
            factory.DefineGenericParameters(typeParameterNames);

        GenericTypeParameterBuilder TInput = typeParameters[0];
        GenericTypeParameterBuilder TOutput = typeParameters[1];

        // Add special constraints.
        // The type parameter TOutput is constrained to be a reference
        // type, and to have a parameterless constructor. This ensures
        // that the Factory method can create the collection type.
        // 
        TOutput.SetGenericParameterAttributes(
            GenericParameterAttributes.ReferenceTypeConstraint | 
            GenericParameterAttributes.DefaultConstructorConstraint);

        // Add interface and base type constraints.
        // The type parameter TOutput is constrained to types that
        // implement the ICollection<T> interface, to ensure that
        // they have an Add method that can be used to add elements.
        //
        // To create the constraint, first use MakeGenericType to bind 
        // the type parameter TInput to the ICollection<T> interface,
        // returning the type ICollection<TInput>, then pass
        // the newly created type to the SetInterfaceConstraints
        // method. The constraints must be passed as an array, even if
        // there is only one interface.
        //
        Type icoll = typeof(ICollection<>);
        Type icollOfTInput = icoll.MakeGenericType(TInput);
        Type[] constraints = {icollOfTInput};
        TOutput.SetInterfaceConstraints(constraints);

        // Set parameter types for the method. The method takes
        // one parameter, an array of type TInput.
        Type[] parms = {TInput.MakeArrayType()};
        factory.SetParameters(parms);

        // Set the return type for the method. The return type is
        // the generic type parameter TOutput.
        factory.SetReturnType(TOutput);

        // Generate a code body for the method. 
        // -----------------------------------
        // Get a code generator and declare local variables and
        // labels. Save the input array to a local variable.
        //
        ILGenerator ilgen = factory.GetILGenerator();

        LocalBuilder retVal = ilgen.DeclareLocal(TOutput);
        LocalBuilder ic = ilgen.DeclareLocal(icollOfTInput);
        LocalBuilder input = ilgen.DeclareLocal(TInput.MakeArrayType());
        LocalBuilder index = ilgen.DeclareLocal(typeof(int));

        Label enterLoop = ilgen.DefineLabel();
        Label loopAgain = ilgen.DefineLabel();

        ilgen.Emit(OpCodes.Ldarg_0);
        ilgen.Emit(OpCodes.Stloc_S, input);

        // Create an instance of TOutput, using the generic method 
        // overload of the Activator.CreateInstance method. 
        // Using this overload requires the specified type to have
        // a parameterless constructor, which is the reason for adding 
        // that constraint to TOutput. Create the constructed generic
        // method by passing TOutput to MakeGenericMethod. After
        // emitting code to call the method, emit code to store the
        // new TOutput in a local variable. 
        //
        MethodInfo createInst = 
            typeof(Activator).GetMethod("CreateInstance", Type.EmptyTypes);
        MethodInfo createInstOfTOutput = 
            createInst.MakeGenericMethod(TOutput);

        ilgen.Emit(OpCodes.Call, createInstOfTOutput);
        ilgen.Emit(OpCodes.Stloc_S, retVal);

        // Load the reference to the TOutput object, cast it to
        // ICollection<TInput>, and save it.
        //
        ilgen.Emit(OpCodes.Ldloc_S, retVal);
        ilgen.Emit(OpCodes.Box, icollOfTInput);
        ilgen.Emit(OpCodes.Castclass, icollOfTInput);
        ilgen.Emit(OpCodes.Stloc_S, ic);

        // Loop through the array, adding each element to the new
        // instance of TOutput. Note that in order to get a MethodInfo
        // for ICollection<TInput>.Add, it is necessary to first 
        // get the Add method for the generic type defintion,
        // ICollection<T>.Add. This is because it is not possible
        // to call GetMethod on icollOfTInput. The static overload of
        // TypeBuilder.GetMethod produces the correct MethodInfo for
        // the constructed type.
        //
        MethodInfo mAddPrep = icoll.GetMethod("Add");
        MethodInfo mAdd = TypeBuilder.GetMethod(icollOfTInput, mAddPrep);

        // Initialize the count and enter the loop.
        ilgen.Emit(OpCodes.Ldc_I4_0);
        ilgen.Emit(OpCodes.Stloc_S, index);
        ilgen.Emit(OpCodes.Br_S, enterLoop);

        // Mark the beginning of the loop. Push the ICollection
        // reference on the stack, so it will be in position for the
        // call to Add. Then push the array and the index on the 
        // stack, get the array element, and call Add (represented
        // by the MethodInfo mAdd) to add it to the collection.
        //
        // The other ten instructions just increment the index
        // and test for the end of the loop. Note the MarkLabel
        // method, which sets the point in the code where the 
        // loop is entered. (See the earlier Br_S to enterLoop.)
        //
        ilgen.MarkLabel(loopAgain);

        ilgen.Emit(OpCodes.Ldloc_S, ic);
        ilgen.Emit(OpCodes.Ldloc_S, input);
        ilgen.Emit(OpCodes.Ldloc_S, index);
        ilgen.Emit(OpCodes.Ldelem, TInput);
        ilgen.Emit(OpCodes.Callvirt, mAdd);

        ilgen.Emit(OpCodes.Ldloc_S, index);
        ilgen.Emit(OpCodes.Ldc_I4_1);
        ilgen.Emit(OpCodes.Add);
        ilgen.Emit(OpCodes.Stloc_S, index);

        ilgen.MarkLabel(enterLoop);
        ilgen.Emit(OpCodes.Ldloc_S, index);
        ilgen.Emit(OpCodes.Ldloc_S, input);
        ilgen.Emit(OpCodes.Ldlen);
        ilgen.Emit(OpCodes.Conv_I4);
        ilgen.Emit(OpCodes.Clt);
        ilgen.Emit(OpCodes.Brtrue_S, loopAgain);

        ilgen.Emit(OpCodes.Ldloc_S, retVal);
        ilgen.Emit(OpCodes.Ret);

        // Complete the type.
        Type dt = demoType.CreateType();
        // Save the assembly, so it can be examined with Ildasm.exe.
        demoAssembly.Save(asmName.Name+".dll");

        // To create a constructed generic method that can be
        // executed, first call the GetMethod method on the completed 
        // type to get the generic method definition. Call MakeGenericType
        // on the generic method definition to obtain the constructed
        // method, passing in the type arguments. In this case, the
        // constructed method has string for TInput and List<string>
        // for TOutput. 
        //
        MethodInfo m = dt.GetMethod("Factory");
        MethodInfo bound = 
            m.MakeGenericMethod(typeof(string), typeof(List<string>));

        // Display a string representing the bound method.
        Console.WriteLine(bound);


        // Once the generic method is constructed, 
        // you can invoke it and pass in an array of objects 
        // representing the arguments. In this case, there is only
        // one element in that array, the argument 'arr'.
        //
        object o = bound.Invoke(null, new object[]{arr});
        List<string> list2 = (List<string>) o;

        Console.WriteLine("The first element is: {0}", list2[0]);


        // You can get better performance from multiple calls if
        // you bind the constructed method to a delegate. The 
        // following code uses the generic delegate D defined 
        // earlier.
        //
        Type dType = typeof(D<string, List <string>>);
        D<string, List <string>> test;
        test = (D<string, List <string>>) 
            Delegate.CreateDelegate(dType, bound);

        List<string> list3 = test(arr);
        Console.WriteLine("The first element is: {0}", list3[0]);
    }
}

/* This code example produces the following output:

The first element is: a
System.Collections.Generic.List`1[System.String] Factory[String,List`1](System.String[])
The first element is: a
The first element is: a
 */

Compilazione del codice

  • Nel codice sono incluse le istruzioni using di C# (Imports in Visual Basic) necessarie per la compilazione.

  • Non sono necessari altri riferimenti ad assembly.

  • Compilare il codice dalla riga di comando utilizzando csc.exe, vbc.exe o cl.exe. Per compilare il codice in Visual Studio, inserirlo in un modello di progetto di applicazione console.

Vedere anche

Attività

Procedura: definire un tipo generico con la reflection emit

Riferimenti

MethodBuilder