Прочитать на английском

Поделиться через


Практическое руководство. Определение универсального типа с порождаемым отражением

В этой статье показано, как создать простой универсальный тип с двумя параметрами типа, применение ограничений классов, ограничений интерфейса и специальных ограничений к параметрам типа, а также создание элементов, использующих параметры типа класса в качестве типов параметров и возвращаемых типов.

Важно!

Метод не может являться универсальным только потому, что он принадлежит универсальному типу и использует параметры этого типа. Метод является универсальным только в том случае, если он имеет свой собственный список параметров типа. Большинство методов в универсальных типах не являются универсальными, как в этом примере. Пример выпуска универсального метода см. в разделе Практическое руководство. Определение универсального метода с порождаемым отражением.

Определение универсального типа

  1. Определите динамическую сборку с именем GenericEmitExample1. В этом примере сборка выполняется и сохраняется на диске, так что указан флаг AssemblyBuilderAccess.RunAndSave.

    AppDomain myDomain = AppDomain.CurrentDomain;
    AssemblyName myAsmName = new AssemblyName("GenericEmitExample1");
    AssemblyBuilder myAssembly =
        myDomain.DefineDynamicAssembly(myAsmName,
            AssemblyBuilderAccess.RunAndSave);
    
  2. Определите динамический модуль. Сборка состоит из выполняемых модулей. Для сборки с одним модулем именем модуля является имя сборки, а именем файла — имя сборки с расширением.

    ModuleBuilder myModule =
        myAssembly.DefineDynamicModule(myAsmName.Name,
           myAsmName.Name + ".dll");
    
  3. Определите класс . В этом примере класс называется Sample.

    TypeBuilder myType =
        myModule.DefineType("Sample", TypeAttributes.Public);
    
  4. Определите параметры универсального типа метода Sample посредством передачи массива строк, содержащего имена параметров, в метод TypeBuilder.DefineGenericParameters. Этот класс станет универсального типа. Возвращаемое значение — это массив объектов GenericTypeParameterBuilder, представляющих параметры типа, который может использоваться в выпущенном коде.

    В следующем коде Sample становится универсальным типом с параметрами типа TFirst и TSecond. Чтобы сделать код более удобными для чтения, каждый объект GenericTypeParameterBuilder помещается в переменную с тем же именем, что параметр типа.

    string[] typeParamNames = {"TFirst", "TSecond"};
    GenericTypeParameterBuilder[] typeParams =
        myType.DefineGenericParameters(typeParamNames);
    
    GenericTypeParameterBuilder TFirst = typeParams[0];
    GenericTypeParameterBuilder TSecond = typeParams[1];
    
  5. Добавьте специальные ограничения параметров типа. В этом примере параметр типа TFirst ограничен типами, имеющими конструкторы без параметров, а также ссылочными типами.

    TFirst.SetGenericParameterAttributes(
        GenericParameterAttributes.DefaultConstructorConstraint |
        GenericParameterAttributes.ReferenceTypeConstraint);
    
  6. Дополнительно добавьте ограничения класса и интерфейса в параметры типа. В этом примере параметр типа TFirst ограничен типами, которые являются производными из базового класса, представленного объектом Type, содержащимся в переменной baseType, а также которые реализуют интерфейсы, содержащие типы в переменных interfaceA и interfaceB. Объявление и назначение этих переменных см. в примере кода.

    TSecond.SetBaseTypeConstraint(baseType);
    Type[] interfaceTypes = {interfaceA, interfaceB};
    TSecond.SetInterfaceConstraints(interfaceTypes);
    
  7. Определите поле. В этом примере тип поля задается параметром типа TFirst. GenericTypeParameterBuilder является производным от Type, поэтому параметры универсального типа можно использовать везде, где можно использовать тип.

    FieldBuilder exField =
        myType.DefineField("ExampleField", TFirst,
            FieldAttributes.Private);
    
  8. Определите метод, который использует параметры типа, относящиеся к универсальному типу. Обратите внимание, что такие методы не являются универсальными, если только они не имеют собственный список параметров типа. В следующем примере кода определяется метод static (Shared в Visual Basic), который принимает массив TFirst и возвращает List<TFirst> (List(Of TFirst) в Visual Basic), содержащий все элементы массива. Чтобы определить этот метод, необходимо создать тип List<TFirst> путем вызова метода MakeGenericType в определении универсального типа List<T>. (T опускается при использовании оператора typeof (GetType в Visual Basic), чтобы получить определение универсального типа.) Тип параметра создается с помощью метода MakeArrayType.

    Type listOf = typeof(List<>);
    Type listOfTFirst = listOf.MakeGenericType(TFirst);
    Type[] mParamTypes = {TFirst.MakeArrayType()};
    
    MethodBuilder exMethod =
        myType.DefineMethod("ExampleMethod",
            MethodAttributes.Public | MethodAttributes.Static,
            listOfTFirst,
            mParamTypes);
    
  9. Выпустите основную часть метода. Основная часть метода состоит из трех кодов операции, которые загружает входной массив в стек, вызывают конструктор List<TFirst>, который принимает IEnumerable<TFirst> (выполняет всю работу по помещению входных элементов в список), и возвращается (оставляя новый объект List<T> в стеке). Трудной частью выпуска этого кода является получение конструктора.

    Метод GetConstructor не поддерживается для GenericTypeParameterBuilder, так что невозможно напрямую получить конструктор List<TFirst>. Сначала необходимо получить конструктор определения универсального типа List<T>, а затем вызвать метод, который преобразует его в соответствующий конструктор List<TFirst>.

    Конструктор, используемый для этого примера кода принимает IEnumerable<T>. Но обратите внимание, что это не определение универсального типа универсального интерфейса IEnumerable<T>; вместо этого параметр типа T из List<T> должен быть замещен для параметра типа T объекта IEnumerable<T>. (Это кажется запутанным только потому, что оба типа имеют параметры типа с именем T. Именно поэтому в этом примере кода используются имена TFirst и TSecond.) Чтобы получить тип аргумента конструктора, начните с определения IEnumerable<T> универсального типа и вызов MakeGenericType с первым параметром List<T>универсального типа . В этом случае список аргументов конструктора должен быть передан в качестве массива только с одним аргументом.

    Примечание

    Определение общего типа выражается как IEnumerable<> при использовании оператора typeof в C# или IEnumerable(Of ) при использовании оператора GetType в Visual Basic.

    Теперь возможно получить конструктор List<T> путем вызова метода GetConstructor в определении универсального типа. Чтобы преобразовать этот конструктор в соответствующий конструктор List<TFirst>, передайте List<TFirst> и конструктор из List<T> в статический метод TypeBuilder.GetConstructor(Type, ConstructorInfo).

    ILGenerator ilgen = exMethod.GetILGenerator();
    
    Type ienumOf = typeof(IEnumerable<>);
    Type TfromListOf = listOf.GetGenericArguments()[0];
    Type ienumOfT = ienumOf.MakeGenericType(TfromListOf);
    Type[] ctorArgs = {ienumOfT};
    
    ConstructorInfo ctorPrep = listOf.GetConstructor(ctorArgs);
    ConstructorInfo ctor =
        TypeBuilder.GetConstructor(listOfTFirst, ctorPrep);
    
    ilgen.Emit(OpCodes.Ldarg_0);
    ilgen.Emit(OpCodes.Newobj, ctor);
    ilgen.Emit(OpCodes.Ret);
    
  10. Создайте тип и сохраните файл.

    Type finished = myType.CreateType();
    myAssembly.Save(myAsmName.Name+".dll");
    
  11. Вызовите метод. ExampleMethodне является универсальным, но тип, к которому он принадлежит, является универсальным, поэтому для получения MethodInfo вызываемого типа необходимо создать созданный тип из определения типа.Sample Сконструированный тип использует класс Example, который согласуется с ограничениями TFirst, так как он является ссылочным типом и имеет конструктор без параметров по умолчанию, а также класс ExampleDerived, который соответствует ограничениям TSecond. (Код для ExampleDerived этого можно найти в примере раздела кода.) Эти два типа передаются для MakeGenericType создания созданного типа. Затем получается объект MethodInfo с помощью метода GetMethod.

    Type[] typeArgs = {typeof(Example), typeof(ExampleDerived)};
    Type constructed = finished.MakeGenericType(typeArgs);
    MethodInfo mi = constructed.GetMethod("ExampleMethod");
    
  12. В следующем коде создается массив объектов Example, затем этот массив размещается в массиве типа Object, представленном аргументами вызываемого метода, после чего они передаются в метод Invoke(Object, Object[]). Первый аргумент метода Invoke — это пустая ссылка, так как метод является static.

    Example[] input = {new Example(), new Example()};
    object[] arguments = {input};
    
    List<Example> listX =
        (List<Example>) mi.Invoke(null, arguments);
    
    Console.WriteLine($"\nThere are {listX.Count} elements in the List<Example>.");
    

Пример

В следующем примере кода определяется класс с именем Sample наряду с базовым классом и двумя интерфейсами. Программа определяет два параметра универсального типа для Sample, преобразуя его в универсальный тип. Параметры типа — это единственный фактор, делающий тип универсальным. Программа отображает это посредством сообщения проверки до и после определения параметров типа.

Параметр типа TSecond используется для демонстрации ограничений класса и интерфейса с помощью базовых классов и интерфейсов, а параметр типа TFirst применяется для демонстрации специальных ограничений.

В примере кода определяется поле и метод с использованием параметров типа класса для типа поля и параметра, а также возвращаемый тип метода.

После создания класса Sample вызывается этот метод.

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

Программа сохраняет готовый модуль на диск, GenericEmitExample1.dllчтобы открыть его с помощью Ildasm.exe (IL Disassembler) и проверить CIL для Sample класса.

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.Generic;

// Define a trivial base class and two trivial interfaces
// to use when demonstrating constraints.
//
public class ExampleBase {}

public interface IExampleA {}

public interface IExampleB {}

// Define a trivial type that can substitute for type parameter
// TSecond.
//
public class ExampleDerived : ExampleBase, IExampleA, IExampleB {}

public class Example
{
    public static void Main()
    {
        // Define a dynamic assembly to contain the sample type. The
        // assembly will not be run, but only saved to disk, so
        // AssemblyBuilderAccess.Save is specified.
        //
        AppDomain myDomain = AppDomain.CurrentDomain;
        AssemblyName myAsmName = new AssemblyName("GenericEmitExample1");
        AssemblyBuilder myAssembly =
            myDomain.DefineDynamicAssembly(myAsmName,
                AssemblyBuilderAccess.RunAndSave);

        // An assembly is made up of executable modules. For a single-
        // module assembly, the module name and file name are the same
        // as the assembly name.
        //
        ModuleBuilder myModule =
            myAssembly.DefineDynamicModule(myAsmName.Name,
               myAsmName.Name + ".dll");

        // Get type objects for the base class trivial interfaces to
        // be used as constraints.
        //
        Type baseType = typeof(ExampleBase);
        Type interfaceA = typeof(IExampleA);
        Type interfaceB = typeof(IExampleB);

        // Define the sample type.
        //
        TypeBuilder myType =
            myModule.DefineType("Sample", TypeAttributes.Public);

        Console.WriteLine($"Type 'Sample' is generic: {myType.IsGenericType}");

        // Define type parameters for the type. Until you do this,
        // the type is not generic, as the preceding and following
        // WriteLine statements show. The type parameter names are
        // specified as an array of strings. To make the code
        // easier to read, each GenericTypeParameterBuilder is placed
        // in a variable with the same name as the type parameter.
        //
        string[] typeParamNames = {"TFirst", "TSecond"};
        GenericTypeParameterBuilder[] typeParams =
            myType.DefineGenericParameters(typeParamNames);

        GenericTypeParameterBuilder TFirst = typeParams[0];
        GenericTypeParameterBuilder TSecond = typeParams[1];

        Console.WriteLine($"Type 'Sample' is generic: {myType.IsGenericType}");

        // Apply constraints to the type parameters.
        //
        // A type that is substituted for the first parameter, TFirst,
        // must be a reference type and must have a parameterless
        // constructor.
        TFirst.SetGenericParameterAttributes(
            GenericParameterAttributes.DefaultConstructorConstraint |
            GenericParameterAttributes.ReferenceTypeConstraint);

        // A type that is substituted for the second type
        // parameter must implement IExampleA and IExampleB, and
        // inherit from the trivial test class ExampleBase. The
        // interface constraints are specified as an array
        // containing the interface types.
        TSecond.SetBaseTypeConstraint(baseType);
        Type[] interfaceTypes = {interfaceA, interfaceB};
        TSecond.SetInterfaceConstraints(interfaceTypes);

        // The following code adds a private field named ExampleField,
        // of type TFirst.
        FieldBuilder exField =
            myType.DefineField("ExampleField", TFirst,
                FieldAttributes.Private);

        // Define a static method that takes an array of TFirst and
        // returns a List<TFirst> containing all the elements of
        // the array. To define this method it is necessary to create
        // the type List<TFirst> by calling MakeGenericType on the
        // generic type definition, List<T>. (The T is omitted with
        // the typeof operator when you get the generic type
        // definition.) The parameter type is created by using the
        // MakeArrayType method.
        //
        Type listOf = typeof(List<>);
        Type listOfTFirst = listOf.MakeGenericType(TFirst);
        Type[] mParamTypes = {TFirst.MakeArrayType()};

        MethodBuilder exMethod =
            myType.DefineMethod("ExampleMethod",
                MethodAttributes.Public | MethodAttributes.Static,
                listOfTFirst,
                mParamTypes);

        // Emit the method body.
        // The method body consists of just three opcodes, to load
        // the input array onto the execution stack, to call the
        // List<TFirst> constructor that takes IEnumerable<TFirst>,
        // which does all the work of putting the input elements into
        // the list, and to return, leaving the list on the stack. The
        // hard work is getting the constructor.
        //
        // The GetConstructor method is not supported on a
        // GenericTypeParameterBuilder, so it is not possible to get
        // the constructor of List<TFirst> directly. There are two
        // steps, first getting the constructor of List<T> and then
        // calling a method that converts it to the corresponding
        // constructor of List<TFirst>.
        //
        // The constructor needed here is the one that takes an
        // IEnumerable<T>. Note, however, that this is not the
        // generic type definition of IEnumerable<T>; instead, the
        // T from List<T> must be substituted for the T of
        // IEnumerable<T>. (This seems confusing only because both
        // types have type parameters named T. That is why this example
        // uses the somewhat silly names TFirst and TSecond.) To get
        // the type of the constructor argument, take the generic
        // type definition IEnumerable<T> (expressed as
        // IEnumerable<> when you use the typeof operator) and
        // call MakeGenericType with the first generic type parameter
        // of List<T>. The constructor argument list must be passed
        // as an array, with just one argument in this case.
        //
        // Now it is possible to get the constructor of List<T>,
        // using GetConstructor on the generic type definition. To get
        // the constructor of List<TFirst>, pass List<TFirst> and
        // the constructor from List<T> to the static
        // TypeBuilder.GetConstructor method.
        //
        ILGenerator ilgen = exMethod.GetILGenerator();

        Type ienumOf = typeof(IEnumerable<>);
        Type TfromListOf = listOf.GetGenericArguments()[0];
        Type ienumOfT = ienumOf.MakeGenericType(TfromListOf);
        Type[] ctorArgs = {ienumOfT};

        ConstructorInfo ctorPrep = listOf.GetConstructor(ctorArgs);
        ConstructorInfo ctor =
            TypeBuilder.GetConstructor(listOfTFirst, ctorPrep);

        ilgen.Emit(OpCodes.Ldarg_0);
        ilgen.Emit(OpCodes.Newobj, ctor);
        ilgen.Emit(OpCodes.Ret);

        // Create the type and save the assembly.
        Type finished = myType.CreateType();
        myAssembly.Save(myAsmName.Name+".dll");

        // Invoke the method.
        // ExampleMethod is not generic, but the type it belongs to is
        // generic, so in order to get a MethodInfo that can be invoked
        // it is necessary to create a constructed type. The Example
        // class satisfies the constraints on TFirst, because it is a
        // reference type and has a default constructor. In order to
        // have a class that satisfies the constraints on TSecond,
        // this code example defines the ExampleDerived type. These
        // two types are passed to MakeGenericMethod to create the
        // constructed type.
        //
        Type[] typeArgs = {typeof(Example), typeof(ExampleDerived)};
        Type constructed = finished.MakeGenericType(typeArgs);
        MethodInfo mi = constructed.GetMethod("ExampleMethod");

        // Create an array of Example objects, as input to the generic
        // method. This array must be passed as the only element of an
        // array of arguments. The first argument of Invoke is
        // null, because ExampleMethod is static. Display the count
        // on the resulting List<Example>.
        //
        Example[] input = {new Example(), new Example()};
        object[] arguments = {input};

        List<Example> listX =
            (List<Example>) mi.Invoke(null, arguments);

        Console.WriteLine($"\nThere are {listX.Count} elements in the List<Example>.");

        DisplayGenericParameters(finished);
    }

    private static void DisplayGenericParameters(Type t)
    {
        if (!t.IsGenericType)
        {
            Console.WriteLine("Type '{0}' is not generic.");
            return;
        }
        if (!t.IsGenericTypeDefinition)
        {
            t = t.GetGenericTypeDefinition();
        }

        Type[] typeParameters = t.GetGenericArguments();
        Console.WriteLine($"\nListing {typeParameters.Length} type parameters for type '{t}'.");

        foreach( Type tParam in typeParameters )
        {
            Console.WriteLine($"""
            
            Type parameter {tParam.ToString()}:
            """);

            foreach( Type c in tParam.GetGenericParameterConstraints() )
            {
                if (c.IsInterface)
                {
                    Console.WriteLine($"    Interface constraint: {c}");
                }
                else
                {
                    Console.WriteLine($"    Base type constraint: {c}");
                }
            }

            ListConstraintAttributes(tParam);
        }
    }

    // List the constraint flags. The GenericParameterAttributes
    // enumeration contains two sets of attributes, variance and
    // constraints. For this example, only constraints are used.
    //
    private static void ListConstraintAttributes(Type t)
    {
        // Mask off the constraint flags.
        GenericParameterAttributes constraints =
            t.GenericParameterAttributes & GenericParameterAttributes.SpecialConstraintMask;

        if ((constraints & GenericParameterAttributes.ReferenceTypeConstraint)
            != GenericParameterAttributes.None)
        {
            Console.WriteLine("    ReferenceTypeConstraint");
        }

        if ((constraints & GenericParameterAttributes.NotNullableValueTypeConstraint)
            != GenericParameterAttributes.None)
        {
            Console.WriteLine("    NotNullableValueTypeConstraint");
        }

        if ((constraints & GenericParameterAttributes.DefaultConstructorConstraint)
            !=GenericParameterAttributes.None)
        {
            Console.WriteLine("    DefaultConstructorConstraint");
        }
    }
}

/* This code example produces the following output:

Type 'Sample' is generic: False
Type 'Sample' is generic: True

There are 2 elements in the List<Example>.

Listing 2 type parameters for type 'Sample[TFirst,TSecond]'.

Type parameter TFirst:
    ReferenceTypeConstraint
    DefaultConstructorConstraint

Type parameter TSecond:
    Interface constraint: IExampleA
    Interface constraint: IExampleB
    Base type constraint: ExampleBase
 */

См. также