C# Reflection and Attributes (Part 4): Instantiating Types

2020年1月5日 138点热度 0人点赞 0条评论
内容目录

The previous three articles introduced the usage of assemblies, how to obtain Type objects, and how to use Type objects to access member information.

From the previous study, we have a general understanding of the existence and output information of Assembly, PropertyInfo, FieldInfo, ConstructorInfo, MethodInfo, ParameterInfo, EventInfo, and MemberInfo.

This article will start with reflecting on instantiating types and will conduct a series of practical reflection operations.

This article mainly discusses instantiating types and instantiating delegates.

1. Instantiating Types

There are two ways to create an instance of a type (Type):

  • Using the Activator.CreateInstance() method, which operates on the Type.
  • Using ConstructorInfo.Invoke(), which operates on the ConstructorInfo.

When instantiating a type, the constructor of the type should be considered first.

1.1 Activator.CreateInstance()

First, this is how it's defined in Microsoft Docs:

Create an instance of the specified type using the constructor that matches the specified parameters most closely.

What does that mean?

Let's take a look at the two most commonly used overloads of Activator.CreateInstance().

object? CreateInstance(Type type);
object? CreateInstance(Type type, params object[] args);

The args are the parameters passed to the constructor when instantiating the type. Since it uses object, the constructor used for instantiation will be the one with the highest matching degree.

Alright, let's move on to the practical part.

1.1.1 Simple Types

            Type typeA = typeof(int);
            object objA = Activator.CreateInstance(typeA);

With the above code, we can easily instantiate a simple type.

Of course, you can see that the type created is object.

Now, here's the problem:

After reflection, boxing and unboxing become unavoidable.

As of now, we can't use int's methods, only those of object. What should we do?

We'll deal with that later.

Of course, we could just use int, so why use reflection?

int i = 666;

Wouldn't that work?

If you need to reference code after generating the program that references a dll, we can do it like this:

            Assembly ass = Assembly.LoadFrom(@"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\3.0.0\ref\netcoreapp3.0\System.Runtime.dll");
            Type typeA = ass.GetType("System.Int32");
            object objA = Activator.CreateInstance(typeA);

1.1.2 Constructors of Simple Types

For simple types like int, there's no other operation; we can just instantiate directly. Here, let's take the DateTime type as an example and instantiate it using different parameters to call the constructor.

            Type typeA = typeof(DateTime);
            object objA = Activator.CreateInstance(typeA, 2020, 1, 5);

Of course, if a suitable constructor cannot be found to instantiate the type, a System.MissingMethodException exception will be thrown.

1.1.3 object

Let's create a type:

        public MyClass(object a, object b)
        {
    }

    public MyClass(string a, string b)
    {

    }
    public MyClass(string a, object b)
    {

    }</code></pre>

Now, let’s create an instance via reflection:

            Type typeA = typeof(MyClass);
            object objA = Activator.CreateInstance(typeA, 2020, 666);
        Console.WriteLine(typeA.Name);</code></pre>

The code above will not throw an error.

There are two reasons for this: 1) the type conversion to object retains information about the original type; 2) Activator.CreateInstance() will look for the optimal constructor.

Thus, the instantiation above will call public MyClass(int a, int b). What if there's no match? Then it will fall back to the optimal solution.

So this little detail doesn't cause any issues.

For simple types, the lookup process is as follows:

1. Search for the corresponding constructor of the type.

Activator.CreateInstance(typeA, 2020, 666), where 2020 is typeof(int) and 666 is typeof(int).

The optimal choice is public MyClass(int a, int b).

2. If not found, look for a constructor that can be implicitly converted.

For example, int -> long;

public MyClass(long a, long b)

3. If there's no implicit conversion, then object.

public MyClass(object a, object b)

If none match, then an error will be raised.

Let's verify:

    public class MyClass
    {
        public MyClass(string a, string b) { }
        public MyClass(int a, int b) { }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Type typeA = typeof(MyClass);
            long a = 666;
            long b = 666;
        object objA = Activator.CreateInstance(typeA, a, b);

        Console.WriteLine(typeA.Name);
        Console.ReadKey();
    }
}</code></pre>

Unless something goes wrong, the above code will throw an error.

1.1.4 Intentional Error

    public class MyClass
    {
        public MyClass(string a = null) { }
        public MyClass(StringBuilder a = null) { }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Type typeA = typeof(MyClass);
        object objA = Activator.CreateInstance(typeA, null);

        Console.WriteLine(typeA.Name);
        Console.ReadKey();
    }
}</code></pre>

If all goes well, the above code will throw an error upon execution.

This is because, during instantiation, when the parameter is null, two constructors meet the requirements.

In other scenarios, following the constructor lookup steps from 1.1.3, one can generally determine whether it will throw an error.

1.1.5 Activator.CreateInstance() Performance

Let's instantiate a type normally by declaring and assigning values a total of 10 million times.

            Stopwatch time = new Stopwatch();
            time.Start();
            for (int i = 0; i < 10_000_000; i++)
            {
                int a = 666;
            }
            time.Stop();
            Console.WriteLine(time.ElapsedMilliseconds);
            time.Reset();
            time.Restart();
            for (int i = 0; i < 10_000_000; i++)
            {
                int a = 666;
            }
            time.Stop();
            Console.WriteLine(time.ElapsedMilliseconds);

Time:

24
23

Now using reflection:

            Type typeA = typeof(int);
            Stopwatch time = new Stopwatch();
            time.Start();
        for (int i = 0; i < 10_000_000; i++)
        {
            object objA = Activator.CreateInstance(typeA);
        }
        time.Stop();
        Console.WriteLine(time.ElapsedMilliseconds);
        time.Reset();
        time.Restart();
        for (int i = 0; i < 10_000_000; i++)
        {
            object objA = Activator.CreateInstance(typeA);
        }
        time.Stop();
        Console.WriteLine(time.ElapsedMilliseconds);</code></pre>

Time:

589
504

500 / 25 = 20, indeed the performance difference is over 20 times.

1.2 ConstructorInfo.Invoke()

ConstructorInfo.Invoke() has stricter limitations than Activator.CreateInstance() and requires stricter correspondence.

In the code from 1.1.4 where an error was intentionally caused, it can be seen that using null resulted in multiple constructors matching, leading to an error.

We can test by creating an instance using ConstructorInfo.Invoke().

    public class MyClass
    {
        public MyClass(string a = null) { Console.WriteLine(6666); }
        public MyClass(StringBuilder a = null) { }
    }
    class Program
    {
        static void Main(string[] args)
        {
            // Get the constructor uniquely based on the parameter type and count
            ConstructorInfo conStruct = typeof(MyClass).GetConstructor(new Type[] { typeof(string) });
        // Pass parameter values and instantiate
        object objA = conStruct.Invoke(new object[] { null });

        Console.ReadKey();
    }
}</code></pre>

Here, we obtain the constructor of the type using typeof(MyClass).GetConstructor(new Type[] { typeof(string) }); and then instantiate using ConstructorInfo.Invoke().

The method GetConstructor() overload is defined as follows:

public ConstructorInfo? GetConstructor(Type[] types);

What methods can be used to instantiate a type using public constructors? What if we want to call non-public constructors?

We can use BindingFlags, which we will learn more about later.

2. Instantiating Delegates

Use Delegate.CreateDelegate() method to instantiate a delegate, and use Delegate.DynamicInvoke() to invoke the delegate and pass parameters.

The usage is:

CreateDelegate(Type, Object, MethodInfo)

Type is the type of the delegate, while Object and MethodInfo are the instance type and the method.

There are two scenarios: one for instance methods and one for static methods.

We create a delegate and a type:

    delegate int Test(int a, int b);
    public class MyClass
    {
        public int A(int a, int b)
        {
            Console.WriteLine("A");
            return a + b;
        }
        public static int B(int a, int b)
        {
            Console.WriteLine("B");
            return a - b;
        }
    }

The experimental code in Main() is as follows:

            // Bind instance method
            Delegate d1 = Delegate.CreateDelegate(typeof(Test), new MyClass(), "A");
        // Bind static method
        Delegate d2 = Delegate.CreateDelegate(typeof(Test), typeof(MyClass), "B");

        Console.WriteLine(d1.DynamicInvoke(333, 333));
        Console.WriteLine(d2.DynamicInvoke(999, 333));

        Console.ReadKey();</code></pre>

Output:

A
666
B
666

3. Instantiating Generic Types

3.1 Instantiating Generics

When instantiating a generic type, you can operate similarly to instantiating a regular type.

            // Normal instantiation
            Type type = typeof(List);
            object obj = Activator.CreateInstance(type);
        // The following will throw an error
        Type _type = typeof(List<>);
        object _obj = Activator.CreateInstance(_type);</code></pre>

When using Activator.CreateInstance to instantiate a generic type, it must be a closed generic Type with specified type parameters.

List<int> is closed √; List<> is open ×.

Additionally, the same applies when instantiating through ConstructorInfo.Invoke().

    public class MyClass
    {
        public MyClass(T a)
        {
            Console.WriteLine(a);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            // Normal
            ConstructorInfo type = typeof(MyClass).GetConstructor(new Type[] { typeof(int) });
            object obj = type.Invoke(new object[] { 666 });
        Console.ReadKey();
    }
}</code></pre>

3.2 Constructing Closed Generic Types and Reflection

3.2.1 Constructing Closed Constructors

Sometimes, what is passed may be a List<>?

Using Type.MakeGenericType(Type),

we can take the extra step to convert an open generic Type with unbound type parameters into a closed generic Type.

            Type type = typeof(List<>);
        // Constructing generic Type
        Type _type = type.MakeGenericType(typeof(int));
        object _obj = Activator.CreateInstance(_type);</code></pre>

3.2.2 Removing Generic Type Parameter Binding

Using the Type.GetGenericTypeDefinition() method allows you to remove the parameter types from a generic type with bound parameter types.

            Type type = typeof(List);
            Console.WriteLine(type.FullName);
            // Constructing generic Type
            Type _type = type.GetGenericTypeDefinition();
            Console.WriteLine(_type.FullName);

Output

System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
System.Collections.Generic.List`1

List<int> has been transformed into List<>.

3.2.3 Let's Practice

The above section introduced the instantiation of generic types and the two usages regarding parameter types. Now let's practice.

        static void Main(string[] args)
        {
            Type typeA = typeof(Console);
            Type typeB = typeof(List<>);
            Type typeC = typeof(List);
        RemoveGenericTypeParameterBinding(typeA);
        RemoveGenericTypeParameterBinding(typeB);
        RemoveGenericTypeParameterBinding(typeC);
        Console.ReadKey();
    }

    /// <summary>
    /// Converts List<T> to List<>
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static (bool, Type) RemoveGenericTypeParameterBinding(Type type)
    {
        // Check if it's a generic type
        if (type.IsGenericType == false)
        {
            Console.WriteLine("This type is not a generic type");
            return (false, type);
        }

        // Check if it's an open generic type
        if (type.IsGenericTypeDefinition)
        {
            Console.WriteLine("No processing needed");
            return (true, type);
        }

        Type _type = type.GetGenericTypeDefinition();
        Console.WriteLine("Processing complete");
        return (true, _type);
    }
}</code></pre>

痴者工良

高级程序员劝退师

文章评论