C# study notes: reflection

This article re-writes reflection. In that blog a long time ago, I described the basic usage of reflection very crudely (it feels like I didn’t explain it at all). In this blog, we re-summarize reflection, just like Rainbow Six. The same as resetting Hereford Base.

The function of reflection comes from the loading requirements in the code, because a program in our code often has many supporting assemblies. These assemblies are often not familiar to us or even seen by ourselves. They may come from another department of the company or other The company may also release these assemblies after the release of our program body, so we need to load them into the program at runtime.

Load the assembly:

When JIT compiles the IL code of the method into local code, it will check which types are referenced by the IL code. The JIT will utilize the assembly's TypeRef (type reference) and AssemblyRef (assembly reference)< a i=4>'smetadata table to determine which assembly defines the referenced type. 

The definition item of the AssemblyRef metadata table includes various parts of the assemblystrong naming: name, version, language and culture, Public Key Token - The JIT will take these parts, concatenate them into a string identifier, and then attempt to load an assembly matching that identifier into the AppDomain. If the assembly is weakly named. Then the JIT will only get the name of this assembly.

In the Assembly class, there are two most commonly used methods of loading assemblies:

public static Assembly Load(AssemblyName assemblyRef);
public static Assembly Load(string assemblyString);

Both methods first cause the CLR to apply a version-binding redirection policy to the assembly and then look for the assembly in the GAC (Global Assembly Cache). If it is not found, it will search in the application's base directory, subdirectories of the private path, and codebase. If the Load method is passed a weakly named assembly, Load will not apply a version binding redirection policy to the assembly, and the CLR will not look for the assembly in the GAC.

If the Load method finds the corresponding assembly, it will return a reference to the Assembly object corresponding to the loaded assembly. If the assembly is not found, a System.IO.FileNotFoundException error will be thrown.

The Load method is very commonly used when loading an assembly, but it requires the user to master each part of the assembly mark in advance. We can also directly obtain a command line argument that refers to the path name (including the extension) of the assembly file. This This method is relatively brainless. Just call the path name directly with the actual parameters:

public static Assembly LoadFrom(string assemblyFile);

This method is simple on the surface, but actually does a lot of work internally:

  • LoadForm will first call the static method of the System.Reflection.AssemblyName classGetAssemblyName. This method opens the specified file, searches for the record items in the AssemblyRef metadata table, and extracts the information marked by the assembly. This information is returned as an AssemblyName object.
  • Then, LoadForm calls Assembly's Load method internally, changing AssemblyName< a i=4>Object argument is passed to it.
  • The Load method follows its own execution logic. The CLR binds the redirection strategy to the application version and searches for matching assemblies in various locations.
  • If the Load method finds a matching assembly, it will load and return the corresponding Assembly value. If the Load function does not find a matching assembly, it will load the assembly in the parameter passing path of the LoadForm function.
  • If an assembly with the same identifier is already loaded at this time, the LoadForm function simply returns an object of the loaded assembly.

What we need to note is that since an assembly may have multiple versions,if their version numbers have not changed, the corresponding labels may also be the same , since the Load method is called internally in the LoadForm method, the CLR may not load the specified file but a different file, resulting in unexpected behavior. Therefore, it is necessary to confirm that each version in the assembly has its own version number to determine the unique identification of this version.

reflection

In several of our methods above, we all need to query the metadata table, which is stored in a series of tables. When generating an assembly or module, the compiler creates a type definition table, a field definition table, a method definition table, and other tables. When we use reflection, these metadata tables can be parsed through code. But in reality, reflection creates an object modelfor this metadata.

This object model can enumerate all types in a type definition metadata table, and then obtain their base type, interface, and type association flag (flag) for each type. And you can also parse the corresponding metadata table to query the fields, methods, properties, and events of the type.

Reflection is not a commonly used type. It is generally used by many class libraries. They need to clearly understand the definition of the type to provide rich functions. For example, in serialization, the reflection class is used to determine which fields are defined by a type. The values ​​of these fields are then obtained through the serialization formatter and written to the byte stream.

But reflection also has some disadvantages:

  • Reflection will cause type safety to not be guaranteed at compile time. Because reflection relies heavily on strings, safety is lost at compile time. If the string is wrong then the code will be wrong too.
  • Reflection is slow because it requires constant string searches. When using reflection, the names of types and members are unknown at compile time, so we need to use strings to identify these types and members. Such string searches can slow things down when reflecting on searching type metadata.
  • When using reflection to call a member, if parameters are involved, you need topackage the actual parameters into an Object array, and then internally unpack the array to the thread stack within reflection a>, and before calling the method, the CLR must check whether the Object array has the correct data type for the corresponding formal parameter. Finally, the CLR must ensure that the caller has the correct security permissions to access the called member.

In summary, it is best to avoid using reflection to call methods or properties. If we need to use reflection to dynamically discover and construct type instances, then the routine can be written like this:

  • caters to the inner substitution principle, allowing the type to be derived from an interface or base type known at compile time. At runtime, construct an instance of the derived type, and then pass the reference to the object of the base type (or interface), so that the reflected derived type object becomes the abstract base class or virtual Instances of base classes and interfaces. Then call the abstract method of the abstract base class (it can also be a virtual method or a method defined by the interface).

We often need to make a choice between a base class or an interface. In the case of version control, it is more appropriate to use a base class than an interface, because the base class can add new members at any time without the need for derived classes to cater for this. If an interface is used, once the interface changes, the subclasses that implement the interface need to be recompiled.

We can use Assembly'sGetExportedTypes to obtain information about an assembly. For example, in our initial Games assembly, we write some methods and types in it, and then view some calls of this assembly in another program. For example, there is the following code under Games:

namespace Games
{
    public interface IAddGames
    {
        string ShowGameCode(int x);
    }

    public class AddGames1 : IAddGames
    {
        public string ShowGameCode(int x)
        {
            Console.WriteLine("A游戏序列号是" + x);
            return "A游戏序列号是" + x.ToString();
        }
    }

    public class AddGames2 : IAddGames
    {
        public string ShowGameCode(int x)
        {
            Console.WriteLine("B游戏序列号是" + x);
            return "B游戏序列号是" + x.ToString();
        }
    }
    public class Game1
    {
        public void Shout<T>(T t)
        {
            Console.WriteLine(typeof(T));
        }
    }
}

Then when we call, we will generate the solution for this assembly and place it under the root directory where we need to call it, and then we will uniformly traverse all the programs under this root directory Setand output these types:

            string path = Assembly.GetEntryAssembly().Location;
            Console.WriteLine(path);
            string AssemblyDataLocation = Path.GetDirectoryName(path);
            string[] Assemblies = Directory.GetFiles(AssemblyDataLocation, "*.dll");
            foreach(string file in Assemblies)
            {
                Assembly assembly = Assembly.LoadFile(file);
                foreach(Type t in assembly.GetExportedTypes())
                {
                    Console.WriteLine(t.FullName);
                }
            }

Note that we use the GetFile method under Dictionary to obtain all the files in the corresponding format under a path, because we need to view Assembly, so we need to set the corresponding format to dll, that is, dynamic link library.

You can see that there are many types in this. We can see the output of the corresponding type of our Games assembly. It contains an interface IAddGames and two subclasses AddGames1 and 2 derived from it. There is also a class Game1, which is It is defined manually by ourselves.

Type objectType

In our above code, the return value of GetExportedTypes is an array of Type type. The System.Type class is the starting point for performing type and object operations. It is an abstract base class derived fromSystem.Reflection.MemberInfo. The Type type is derived from RuntimeType, ReflectionOnlyType, etc.

The RuntimeType type is only for internal use in C#.When a type is accessed for the first time in C#, the CLR will construct an instance of RuntimeType and initialize the corresponding fields< /span>. When using the public method GetType of the Object type, the CLR will determine the type of the specified object and return a reference to the RuntimeType object corresponding to the type. Since a type has only one unique RuntimeType object, we can finally use the GetType function to determine whether two objects belong to the same type.

GetType method:

The System.Type class itself also has static methods GetType and their overloaded versions. This function accepts a string, which must be the full name of the specified type (including namespace). If the string passed is just a type name, GetType will check the calling assembly to see if the type with the specified name is defined. If this type exists, a reference to RuntimeType is returned.

If the corresponding type is not found in the corresponding assembly, the type corresponding to MSCCorLib.dll will be checked. If this type is not found in the end, null will be returned or thrownSystem.TypeLoadException (this depends on the overloaded version of the GetType method).

typeof operator:

We can obtain late-bound type objects through GetType. If we need to obtain a reference to the corresponding type object through a known type name, we need to use typeof Keyword to generate an object reference of type for an early bound object. This sentence:

            if(o.GetType()==typeof(object))
            {
                //
            }
            

After obtaining a Type object reference of a type, you can query various attributes of this type, such as IsClass, IsSealed, IsAbstract, and other aspects of this type.

BindFlags lookup enumeration:

This parameter is an enumeration of search methods. Its enumeration value is very easy to understand, includingInstance (search for instance members), < /span> (return public members), < /span> (returns members matching the specified string), etc. IgnoreCase (returns the base type defined Static members), FlattenHierarchy (returns non-public members), NotPublicPublic (find static members), Static

Among them, Ignore is suitable for overloaded versions of GetField and GetMethod methods of Type type. These methods that only return a MemberInfo often rely on string search, so the BindFlag here is also best IgnoreCase. If we do not pass the BindFlags parameter, then only public members will be returned, that is, the default value is: BindFlags.Public|BindFlags.Static|BingFlags.Instace.

Similarly, BindFlags also has other enumerations forInvokerMember (this will be discussed below), for example (call a method). InvokerMethod (get or set the value of a field), GetField/SetField (create instance), CreateInstance

Constructed type Activator

After we know a type, we can construct instances of this type very conveniently. In the System.Activator class, a way to construct types is provided:

  • System.Activator'sCreateInstance method: Pass a reference to a Type object or pass a string. If a string is used, the string must first identify the assembly in which the type is defined, and in this case a reference to the new object will not be returned, but a System.Runtime.Remoting.ObjectHandle object will be returned.
  • System.Activator'sCreateInstanceForm method: Pass a string to specify the type and assembly. The assembly itself is obtained through the LoadForm method. And since this method does not accept formal parameters of Type type, it returns an ObjectHandle reference. We need to call the Unwarp method of the ObjectHandle type to materialize it.
  • System.Type'sInvokerMember method: Use a reference to the Type object to call an InvokerMember method. This method will look for a passed actual parameter. constructor and constructs the type.
  • System.Array'sCreateInstance method: dedicated to creating array types. Both it and its overloaded version need to call a reference of the Type type .
  • System.DelegateCreateDelegate method: used to create the delegate type in the assembly, its first parameter is the Type object reference , the other parameters specify the method packaged in the delegate, which can be a method of an instance or a static method.

If we create a generic type object, then we also need to pay attention to the open and closed conditions of the type. We need to first obtain the open type reference of this type, then assign a specific closed type to its generic type, and finally call CreateInstance to generate A type, which is consistent with our direct creation of generic class instances, we can write code to test the type construction method described above.

Basic usage of Type type:

We first create two types, corresponding to two commonly used C# collections: Dictionary and ArrayList. Only for ArrayList we wrote a rough constructor:

    class TestDictionary<Tkey, TValue> { }
    class TestArrayList
    {
        public TestArrayList(int i)
        {
            Console.WriteLine("构造函数输出" + i);
        }
    }

Then we have the following calls to them in our code:

        static void Main()
        {
            Type listArrayType = typeof(TestArrayList);
            Object getArrayList = Activator.CreateInstance(listArrayType, new object[] { 2 });

            Type testDictionaryOpenType = typeof(TestDictionary<,>);
            Type testDictionaryCloseType = testDictionaryOpenType.MakeGenericType(typeof(string), typeof(int));
            Object getDictionary = Activator.CreateInstance(testDictionaryCloseType);

            TestDictionary<string, int> dictionary = new TestDictionary<string, int>();
            if (getDictionary.GetType() == dictionary.GetType())
            {
                Console.WriteLine("二者是同一类型");
            }
            else
            {
                Console.WriteLine("二者并不是同一类型");
            }
        }

We can see that for the generic type TestDictionary, you need to first create the open type, then create its closed type, and finally call the Activator method to create a specific object instance. For the type TestArrayList that has a parameterized constructor, you need to create the formal parameter list of the constructor in the overloaded version to correctly create an instance of this type.

Access the interface functions defined by the subclass:

At the same time, we can use the Type type to access the assembly of the original code Games, and use the IAddGames interface to cater to the substitution to ensure that the correct object type is obtained. Let us repeat the content defined by our Games type here:

namespace Games
{
    public interface IAddGames
    {
        string ShowGameCode(int x);
    }

    public class AddGames1 : IAddGames
    {
        public string ShowGameCode(int x)
        {
            Console.WriteLine("A游戏序列号是" + x);
            return "A游戏序列号是" + x.ToString();
        }
    }

    public class AddGames2 : IAddGames
    {
        public string ShowGameCode(int x)
        {
            Console.WriteLine("B游戏序列号是" + x);
            return "B游戏序列号是" + x.ToString();
        }
    }
}

Then in the code, we need to first introduce a reference to Games, and then check the type of IAddGames by searching the dll library in the local file. , here we put the type into a type array List<Type>, if a relevant type is found, put the type into the type array, and finally traverse the type array, create a type instance and call the interface definition method:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Games;

namespace CSharp学习
{
    class Program
    {
        static void Main()
        {
            String AddinDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
            Console.WriteLine(AddinDir); 

            string[] AddInAssenblies = Directory.GetFiles(AddinDir, "*.dll");

            List<Type> AddinTypes = new List<Type>();
            foreach (string file in AddInAssenblies)
            {
                Assembly AddinAssembly = Assembly.LoadFile(file);

                foreach(Type t in AddinAssembly.GetExportedTypes())
                {
                    if(t.IsClass&&typeof(IAddGames).IsAssignableFrom(t))
                    {
                        AddinTypes.Add(t);
                    }
                }
            }

            foreach(Type t in AddinTypes)
            {
                IAddGames iadd = (IAddGames)Activator.CreateInstance(t);
                iadd.ShowGameCode(5);
            }
        }
    }
}

Finally, we can output the implementation of the method defined by this interface in the subclass. We do not have very complicated logic here, we just simply output a statement. Finally, we output:

Type member class MemberInfo:

Fields, constructors, member functions, properties, events, and nested types in a class can all be considered members of this class. In order to obtain these members, we can receive and use these members uniformly through the MemberInfo class. MemberInfo is an abstract base class that encapsulates the common properties of all members. This picture in "CLR via C#" very concisely describes the relationship between member general attributes and MemberInfo:

Through the MemberInfo type, you can easily obtain information about all members of a type. For example, if we create a class of our own called MyType, we can obtain all member information of this type through the RuntimeType of this type:

    public class MyType
    {
        public override string ToString()
        {
            return base.ToString();
        }
        public int AAA;
        public int getProperty
        {
            get { return AAA; }
            set { AAA = value; }
        }
    }
    class Program
    {
        static bool Filter(Type m, Object a)
        {
            return m.Assembly == a as Assembly;
        }
        static void Main()
        {
            MemberInfo[] members = typeof(MyType).GetMembers();
            PropertyInfo[] propertys = typeof(MyType).GetProperties();
            FieldInfo[] fields = typeof(MyType).GetFields();
            foreach (MemberInfo member in members)
            {
                Console.WriteLine("成员名是" + member.Name + "对应的类是" + member.DeclaringType.ToString());
            }
            foreach (PropertyInfo proerty in propertys)
            {
                Console.WriteLine("属性是" + proerty.Name + "对应的类是" + proerty.DeclaringType.ToString());
            }
            foreach(FieldInfo field in fields)
            {
                Console.WriteLine("字段是" + field.Name + "对应的类是" + field.DeclaringType.ToString());
            }
        }
    }

The output is:

 

Since the ToString type has been rewritten in the MyType type, here we see that the corresponding type of the ToString type is the MyType type, but the corresponding type of other functions such as Equals inherited from the Object type is Object. And the default parameterless constructor is also output here.

Note:If we use the ReflectedType of member, MyType will always be returned regardless of whether it is an Object type definition, because the type when GetMember performs reflection is always MyType.

In addition to using the GetMember type to return references to all MemberInfo types, the Type type can also use other methods such as GetFields (obtainpublic fields), Functions such as GetMethods (getting methods) and GetProperties (getting properties) return specific member types. Similarly, since there are more than one such members in a type, the returned members are often arrays corresponding to the types in the picture above. For example, the GetFields method returns an array of FieldInfo type. The GetPropertys method returns a PropertyInfo array.

Use the type discovery interface:

Similar to the above search member, we can also use Type to find the type inheritance interface, and retrieve the interface and implementation through theInterfaceMapping type A map of the actual methods of the interface's type. For example, we created two interfaces here: IGameAAA and IGameBBB. Finally, myGame implemented these two interfaces and set the function to implement the specific interface:

    interface IGameAAA:IDisposable
    {
        void Buy();
        void Play();
    }
    interface IGameBBB
    {
        void Buy();
    }

    class myGame : IGameAAA, IGameBBB, IDisposable
    {
        void IGameAAA.Buy()
        {
            Console.WriteLine("游戏A买了");
        }

        void IGameBBB.Buy()
        {
            Console.WriteLine("游戏B买了");
        }

        public void Dispose()
        {  }

        public void Play()
        {
            Console.WriteLine("开玩");
        }
    }

Then in the main function, we can retrieve this interface through the GetInterfaces method and InterfaceMapping:

    class Program
    {
        static bool Filter(Type m,Object a)
        {
            Console.WriteLine(m.Assembly.FullName);
            return m.Assembly == a;
        }
        static void Main()
        {
            Type t = typeof(myGame);
            Type[] getInterfaces = t.FindInterfaces(Filter, typeof(Program).Assembly);

            foreach(Type i in getInterfaces)
            {
                Console.WriteLine("接口名称" + i);
                InterfaceMapping map = t.GetInterfaceMap(i);
                for (int m = 0; m < map.InterfaceMethods.Length; m++)
                {
                    Console.WriteLine(map.InterfaceMethods[m] + "被myGame类中的" + map.TargetMethods[m]);
                }
            }
        }
    }

 The output is:

What we need to note is that when calling the GetInterface interface, the first actual parameter is theTypeFilter delegate, and the second actual parameter is A reference to the assembly object. In fact, the function of the filter is to compare the assembly reference of our second argument with the assembly of the interface (this comparison is done by We manually delegate it ourselves). For example, in our example, there is actually an implementation of the IDisposable interface, but it was not reflected after we filtered it. This is because the IDisposable interface belongs to the mecorlib assembly, so it was eliminated during the filter comparison.

If we let the filter always return true here, then there will be a type object reference of the IDisposable interface in the final Type array.

Invoking members of a type using reflectionInvokerMember:

In fact, in the above, we can already obtain the reflected object through the Activator method and call the reflected method, but if we do not obtain the assembly, the created object instance can only be created through the Object object Receive, this is not convenient. If weonly call members of the assembly through strings when the assembly cannot be called, we need to useInvokerMember method is used to dynamically call late-bound assemblies through strings. The formal parameters of this method are:

  • name (string type): The name of the member.
  • flag (BindFlags type): member search method.
  • binder (Binder type): member actual parameter matching method.
  • target (Object type): the calling instance object of the member actual parameter.
  • args (Object type array): the actual parameters of the member.
  • culture (CultureInfo type): The language culture used by the binder.

The InvokerMember method will first search for a certain member based on the string and binding method. This behavior becomes binding, and then the overloaded version of this member is determined based on the actual parameter group. The actual parameters passed to InvokerMember, except target, are helpful for the method to determine the members that need to be bound.

  • Binder type:Binder type object represents the rules for filtering members by the InvokerMember method. The Binder type defines some abstract methods, such as BindToField, BindToMethod, SelectMethod, etc. These methods are called through the Binder object inside InvokerMember.
  • args actual parameter group:As for the number and type of the object array passed in, it will assist InvokerMember to determine the final bound member. And some automatic type conversion classes will also be applied to obtain greater flexibility in types. For example, there is an Int32-bit value in the actual parameter group, but the formal parameter is required to be Int64-bit. In this case, it willConvert Int32-bit to Int64-bit value.
  • target calling object:target is an object reference that needs to be called as a member parameter. If we need to call static members of the type, then we can set the target value to NULL.

The power of the InvokerMember method is that, through the BindFlags enumeration, it can call a method, create a type instance, get or set a field or property. But what we need to note is that BindFlags can only select one enumeration in many cases, but the BindFlags enumeration can be set to both GetProperty and GetField. In this case, InvokerMember will first search for the matching field, and if it is not found, it will Find matching properties. There are two similar enumerations, SetProperty and SetField, and InvokerMember will do the same thing for them.

We try to define a type called PlayerCount, and we add various types of members to it:

    public delegate void PlayEventHandler();
    public class playerCount
    {
        public playerCount(int level)
        {
            this.level = level;
        }
        private PlayEventHandler playEventHandler;
        private int coin;
        private string playerName;
        public int level;
        public void AddCoin(ref int x)
        {
            coin += x;
            x = coin;
        }

        public override string ToString()
        {
            return "角色的金币数量是" + coin.ToString();
        }
        
        public string PlayerName
        {
            get
            {
                return playerName;
            }
            set
            {
                if(value=="")
                {
                    throw new NullReferenceException();
                }
                playerName = value;
            }
        }
        public event PlayEventHandler playEvent
        {
            add
            {
                playEventHandler += value;
            }
            remove
            {
                playEventHandler -= value;
            }
        }
        public void StartGame()
        {
            playEventHandler.Invoke();
        }
    }

Then just through this type object, and then using InvokerMember, you can call various members of the type, including properties, fields, methods, events, constructors, etc. I think this example best reflects the power of reflection:

    class Program
    {
        private const BindingFlags flags = BindingFlags.DeclaredOnly | BindingFlags.Public
                    | BindingFlags.NonPublic | BindingFlags.Instance;
        static void Main()
        {
            Type t = typeof(playerCount);
            NormalBingAndInvoke(t);
        }
        static void TestFunction(string str)
        {
            Console.WriteLine(str);
        }
        static void NormalBingAndInvoke(Type t)
        {
            object instance = t.InvokeMember(null, flags|BindingFlags.CreateInstance, null, null, new object[] { 12 });
            Console.WriteLine("此时获得的成员类型为" + instance.GetType().ToString());
    
            int level = (int)t.InvokeMember("level", flags | BindingFlags.GetField, null, instance, null);
    
            Console.WriteLine("玩家的等级为" + level);
    
            string s = t.InvokeMember("ToString", flags | BindingFlags.InvokeMethod, null, instance, null).ToString();
            Console.WriteLine(s);
    
            //读属性
            //try
            //{
            //    t.InvokeMember("Level", flags | BindingFlags.SetProperty, null, instance, new object[] { "" });
            //}
            //catch
            //{
            //    throw new ArgumentNullException();
            //}
            t.InvokeMember("PlayerName", flags | BindingFlags.SetProperty, null, instance, new object[] { "弟弟" });
            string PlayerName = (string)t.InvokeMember("PlayerName", flags | BindingFlags.GetProperty, null, instance, null);
            Console.WriteLine("玩家名字是" + PlayerName);
    
    
            PlayEventHandler playEvent = new PlayEventHandler(() => { Console.WriteLine("玩家" + PlayerName + "正在进行游戏"); });
            t.InvokeMember("add_playEvent", flags | BindingFlags.InvokeMethod, null, instance, new object[] { playEvent });
    
            t.InvokeMember("StartGame", flags | BindingFlags.InvokeMethod, null, instance, null);
        }
    }

Our final output is:

Guess you like

Origin blog.csdn.net/qq_38601621/article/details/103578732