C# Reflection and Attributes (8): A Complete Guide to Reflection Operations Examples

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

The "C# Reflection and Attributes" series has completed seven articles, explaining the usage and practical applications of reflection. The sixth and seventh articles summarized practical exercises related to reflection attributes, allowing learners to apply knowledge to solve common real-world problems.

Earlier discussions focused on foundational concepts and practice, ensuring that readers acquire basic knowledge after completion. This article extends some earlier topics, addressing unresolved issues and further exploring special scenarios.

This article supplements certain operational details, introducing common operational cases and demonstrations of reflection using another format.

This series has now reached its eighth article, with the next focusing on performance metrics for various reflective operations.

If there are reflection operations you wish to know about that have not been covered in this series by the end of this article, feel free to contact the author, and these will be included in future writings.

Given that this article contains numerous sections, it is recommended to save it for later reading. 😄

1. InvokeMember

Invoke a specified member (CultureInfo) using a specified binding constraint and matching parameters list along with culture.

The definition of this method can be somewhat opaque; don’t worry, just continue reading.

Previously, we used MemberInfo to obtain members of types and perform operations, also using PropertyInfo, MethodInfo, etc. The members we've accessed have all been public.

The InvokeMember method allows us to conveniently invoke members of static or instance objects, including private members, indexers, and more.

There are primarily two overloads of InvokeMember:

        public object? InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, object?[]? args);
        public object? InvokeMember(string name, BindingFlags invokeAttr, Binder? binder, object? target, object?[]? args, CultureInfo? culture);

Note: InvokeMember cannot be used to invoke generic methods.

The parameters in InvokeMember can be complex, and we generally only use the first overload. The second overload adds a CultureInfo parameter to handle language and cultural differences. This article will refer only to the first overload of InvokeMember.

1.1 InvokeMember Parameters

This section introduces the parameters of the InvokeMember method and their functions, and following the examples that appear throughout the article will help you quickly master the knowledge points.

1.1.1 name

This contains the name of the constructor, method, property, or field member to call, with case sensitivity taken into account.

1.1.2 invokeAttr

The invokeAttr parameter is of the BindingFlags enumeration, which allows us to restrict the information about the members to call.

For example, to access private members use BindingFlags.NonPublic, and for static members use BindingFlags.Static. We can use these enum combinations to filter out the required members.

1.1.3 binder

This is usually null and rarely used. The author is also not very clear on this.

The binder object defines a set of properties and enables binding, which may involve selecting overloaded methods, coercing parameter types, and invoking members via reflection.

1.1.4 target

This is the object on which to invoke the specified member.

If invoking a static member or a static member of an instance, target should be null; if invoking an instance member, this parameter should be the instance object.

1.1.5 args

These are the parameters passed, such as arguments for methods, or values for properties and fields.

1.1.6 Return

If calling a method or accessing a field value, there will be a return value; if calling a void method or setting a property field, then it returns null.

1.1.7 BindingFlags

This is an enumeration value specifying the flags that control binding and how to search for members and types through reflection.

The table below enumerates the common enumeration values applicable in typical scenarios, which can serve as notes for reference later.

Enumeration Value Explanation
CreateInstance 512 Indicates that reflection should create an instance of the specified type
DeclaredOnly 2 Indicates that only members declared at the provided type level in the hierarchy should be considered
Default 0 Indicates no binding flags are defined
FlattenHierarchy 64 Indicates that public members and protected static members in the hierarchy should be returned. Does not return private static members in derived classes. Static members include fields, methods, events, and properties. Nested types are not supported.
GetField 1024 Obtains the value of a field
GetProperty 4096 Obtains the value of a property
IgnoreCase 1 Indicates that member name case should be ignored during binding
IgnoreReturn 16777216 Used in COM interop to indicate that the return value of a member can be ignored
Instance 4 Obtains instance members
InvokeMethod 256 Invokes methods
NonPublic 32 Obtains non-public members
Public 16 Obtains public members
SetField 2048 Sets the value of a field
SetProperty 8192 Sets the value of a property
Static 8 Obtains static members
SuppressChangeType 131072 Not implemented

The above enums can be combined to filter out the required members.

1.1.8 Based on Visibility

  • Specify BindingFlags.Public to include public members in the search.
  • Specify BindingFlags.NonPublic to include non-public members (i.e., private members, internal members, and protected members) in the search.
  • Specify BindingFlags.FlattenHierarchy to include static members in the hierarchical structure.

1.1.9 Case Sensitivity and Search Hierarchy

The following BindingFlags modifiers can be used to alter the search behavior:

  • BindingFlags.IgnoreCase ignores case sensitivity of name.
  • BindingFlags.DeclaredOnly only searches members declared at the type level, excluding inherited members.

For DeclaredOnly, refer to section 1.4 of "C# Reflection and Attributes (Part Five): Type Member Operations".

1.1.10 Specify Operations on Members

The following BindingFlags call flags can indicate what operations to perform on members:

  • CreateInstance calls a constructor (therefore name will be ignored, as constructors do not require a name);

  • InvokeMethod invokes a method (without calling a constructor);

  • GetField retrieves a field's value;

  • SetField sets a field's value;

  • GetProperty retrieves a property’s value;

  • SetProperty sets a property’s value;

Additionally, some operations may conflict, such as InvokeMethod with SetField or SetProperty.

If InvokeMethod is used alone, it will implicitly include BindingFlags.Public, BindingFlags.Instance, and BindingFlags.Static. This is an important note.

1.2 Practical Use of InvokeMember and Member Overloading

This section introduces the use of InvokeMember and the use of BindingFlags with MethodInfo, PropertyInfo, and others.

Before to this, create a class type:

    public class MyClass
    {
}</code></pre>

In the Main method, retrieve the Type and instantiate it:

            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);

1.2.1 Static Methods and Instance Methods

Add two methods to MyClass, one static and one instance method:

        public static void A()
        {
            Console.WriteLine("A() method has been called");
        }
        public void B()
        {
            Console.WriteLine("B() method has been called");
        }

Call them using InvokeMember:

            type.InvokeMember("A", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new object[] { });
        type.InvokeMember("B", BindingFlags.InvokeMethod, null, example, new object[] { });

        type.GetMethod("A").Invoke(null, null);

        type.GetMethod("B").Invoke(example, null);</code></pre>

The first invocation calls the static method A, the second calls the instance method B, while the third and fourth use MethodInfo to execute methods.

If a method has no parameters, you may use new object[] { } or null.

When calling methods with InvokeMember, static methods use BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static; instance methods use BindingFlags.InvokeMethod.

However, using BindingFlags.InvokeMethod | BindingFlags.Public for instance methods will throw an error. Why is that?

1.2.2 Method Parameters

Passing parameters to methods is simple using new object[] { }.

For instance:

        public void Test(string a, string b)
        {
            Console.WriteLine(a + b);
        }
            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);
        type.InvokeMember("Test", BindingFlags.InvokeMethod, null, example, new object[] { "666","666" });</code></pre>

You can also specify argument names when calling the method.

Example:

            // Normal instantiation call
            (new MyClass()).Test(b: "前", a: "后");
        // Parameter values
        var parmA = new object[] { "前", "后" };
        // Specify parameter names
        var parmB = new string[] { "b", "a" };

        type.InvokeMember("Test", BindingFlags.InvokeMethod, null, example, parmA, null, null, parmB);</code></pre>

1.2.3 Field Properties

In BindingFlags:

  • GetField obtains the value of a field;
  • SetField sets the value of a field;
  • GetProperty obtains the value of a property;
  • SetProperty sets the value of a property;

Add the following code to MyClass:

        public string C = "c";
        public string D { get; set; }

In Main, use:

            type.InvokeMember("C",BindingFlags.SetField,null,example,new object[] { "666"});
            Console.WriteLine(type.InvokeMember("C", BindingFlags.GetField ,null, example, null));
        type.InvokeMember("D", BindingFlags.SetProperty, null, example, new object[] { "666" });
        Console.WriteLine(type.InvokeMember("D", BindingFlags.GetProperty, null, example, null));</code></pre>

If you are unsure whether it is a property or method, you can use BindingFlags.GetField | BindingFlags.GetProperty to try both.

1.2.4 Default Members

A class can be marked with a default member using the DefaultMemberAttribute. It can be invoked using BindingFlags.Default.

    [DefaultMemberAttribute("TestA")]
    public class MyClass
    {
        public void TestA(string a, string b)
        {
            Console.WriteLine(a + b);
        }
        public void TestB(string a, string b)
        {
            Console.WriteLine(a);
        }
    }
            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);
        type.InvokeMember(string.Empty, BindingFlags.InvokeMethod | BindingFlags.Default, null, example, new object[] { "666", "666" });</code></pre>

At this point, there's no need to pass the name parameter.

1.2.5 Ref and Out Parameters of Methods

I forgot to mention the situation when method parameters are ref or out in the previous seven sections, so I'll add it here.

When the parameters are ref or out, MethodInfo can be called like this.

The usage is straightforward: no special attributes are required, and it can be called directly.

        public void Test(ref string a, ref string b)
        {
            Console.WriteLine($"Before interaction, a={a},b={b}");
            string c = a;
            b = a;
            a = c;
        }
            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);
        string[] list = new string[] { "1", "2" };
        MethodInfo method = type.GetMethod("Test");
        method.Invoke(example, list);
        Console.WriteLine($"After swapping, a={list[0]},b={list[1]}");</code></pre>

1.2.6 Creating Instances

In previous sections, we discussed instantiating types using Activator.CreateInstance and via constructors. Now, types can also be instantiated using InvokeMember.

            object example = type.InvokeMember("MyClass", BindingFlags.Public | BindingFlags.Instance | BindingFlags.CreateInstance, null, null, new object[] { });

BindingFlags.Instance indicates that the returned object is an instance, and BindingFlags.CreateInstance indicates that this operation is for instantiating a type.

If the constructor has parameters, then include the arguments in new object[] { }.

1.2.7 Accessing Members

Previously, we used the GetMembers() method to retrieve all members of a type, and the method we used was an overload with no parameters. There is an overload method that uses BindingFlags as follows:

public abstract MemberInfo[] GetMembers(BindingFlags bindingAttr);

By utilizing BindingFlags, we can acquire specific members.

            Type type = typeof(List<int>);
            MemberInfo[] memInfo = type.GetMembers(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);
        for (int i = 0; i &lt; memInfo.Length; i++)
        {
            Console.WriteLine(memInfo[i].Name);
        }</code></pre>

The aforementioned BindingFlags— BindingFlags.DeclaredOnly fetches members defined in the current type (not inherited), BindingFlags.Instance gets instance members (i.e., those that return results), and BindingFlags.Public retrieves public members.

1.2.8 Invoking Private Methods

Using BindingFlags, we can conveniently access and execute private methods of a type.

    public class MyClass
    {
        private string Test(string a, string b)
        {
            return a + b;
        }
        private void WriteLine(string message)
        {
            Console.WriteLine(message);
        }
    }
            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);
            MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.NonPublic);
        for (int i = 0; i &lt; methods.Length; i++)
        {
            Console.WriteLine(methods[i].Name);
        }

        MethodInfo method = methods.FirstOrDefault(x => x.Name == "WriteLine");
        method.Invoke(example, new object[] { "Printed output" });

The parameters above specify fetching both public and non-public member methods defined in the current type (excluding inherited members, such as the ToString() method), ultimately returning an instance.

Whether public or private, as long as you obtain MethodInfo, you can perform operations normally.

1.2.9 Private Properties

Accessing private properties is just as simple as accessing private methods:

    public class MyClass
    {
        /// 
        /// This property is meaningless
        /// 
        private string A { get; set; }
    /// <summary>
    /// This property will throw an error
    /// </summary>
    // private string B{ get; private set; }

    public string C { get; private set; }
}</code></pre>
            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);

            PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetProperty | BindingFlags.Instance);
            foreach (var item in properties)
            {
                Console.WriteLine(item.Name);
            }

            PropertyInfo property = properties.FirstOrDefault(x=>x.Name=="A");
            property.SetValue(example,"666");
            Console.WriteLine(property.GetValue(example));

            property = properties.FirstOrDefault(x=>x.Name=="C");
            property.SetValue(example,"777");
            Console.WriteLine(property.GetValue(example));

Regardless of being a public property or a private property, or even a private constructor, as long as you obtain MethodInfo, you can operate normally.

1.2.10 Private Properties of Parent Classes

Just directly code it.

Create a type

    public class A
    {
        private string Test { get; set; }
    }
    public class B : A
    {
    }
    public class C : B
    {
}</code></pre>
            Type type = typeof(C);
            PropertyInfo property;
            object example;

            while (true)
            {
                Console.WriteLine($"Searching for {type.Name}");
                property = type.GetProperties(
                    BindingFlags.NonPublic |
                    BindingFlags.Instance)
                    .FirstOrDefault(x => x.Name == "Test");
                // Found it
                if (property != null)
                {
                    example = Activator.CreateInstance(type);
                    break;
                }
                // Search upwards
                if (type.BaseType == null)
                    throw new NullReferenceException("Cannot find it");

                type = type.BaseType;
            }

            property.SetValue(example, "Setting property value");
            Console.WriteLine(property.GetValue(example));

The loop above continuously searches for the property Test until it is found.

1.2.11 GetGetMethod() and SetGetMethod() of Properties

After obtaining the PropertyInfo of the private property above, you can set and get values using SetValue and GetValue.

Through GetGetMethod() and SetGetMethod(), you can also achieve the above operations.

The principle is that when compiling properties, two methods are generated.

    public class MyClass
    {
        private string Test { get; set; }
    }
            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);
        // GetProperty() will throw an error and cannot access the property
        // type.GetProperty("Test", BindingFlags.DeclaredOnly |BindingFlags.NonPublic |BindingFlags.Instance);

        // Accessing the private property
        PropertyInfo property = type.GetProperties(
            BindingFlags.DeclaredOnly |
            BindingFlags.Public |
            BindingFlags.NonPublic |
            BindingFlags.Instance)
            .FirstOrDefault(x => x.Name == "Test");

        // nonPublic: true to access private methods
        MethodInfo set = property.GetSetMethod(nonPublic: true);
        set.Invoke(example, new object[] { "Test" });

        MethodInfo get = property.GetGetMethod(nonPublic:true);
        // Get property value
        Console.WriteLine(get.Invoke(example, null));
        // Get property value
        Console.WriteLine(property.GetValue(example));</code></pre>

Because GetGetMethod() and SetGetMethod() acquire methods, followed by invoking them via Invoke, it's said that this has better performance.

Of course, if you change the property definition to the following, it still holds.

        public string Test { get;private set; }

1.2.12 GetAccessors

In the previous section, "C# Reflection and Attributes (Five): Type Member Operations," section 2.2 introduced this method. Now, let's complete the operations for reading and setting property values using GetAccessors().

    public class MyClass
    {
        public string A { get; set; }
        public string B { get; private set; }
        private string C { get; set; }
    }

Get all properties

            Type type = typeof(MyClass);
            object example = Activator.CreateInstance(type);
        // GetProperty() will throw an error and cannot retrieve the property
        // type.GetProperty("Test", BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Instance);

        // Retrieve private properties
        PropertyInfo[] properties = type.GetProperties(
            BindingFlags.DeclaredOnly |
            BindingFlags.Public |
            BindingFlags.NonPublic |
            BindingFlags.Instance);</code></pre>

Start operations

            // Loop through all properties and call setters/getters
        foreach (var item in properties)
        {
            MethodInfo[] methods = item.GetAccessors(nonPublic: true);

            // Set method, Get method
            MethodInfo mSet = null;
            MethodInfo mGet = null;

            Console.WriteLine("\nProperty   " + item.Name);

            // Actually, each property has only two methods, no need to use foreach
            foreach (var itemNode in methods)
            {
                // If there is no return value, it means it's a method like Void set_B(System.String)
                // i.e., the setter
                if (itemNode.ReturnType == typeof(void))
                {
                    Console.WriteLine("Setter    " + itemNode);
                    Console.WriteLine("Is public    " + itemNode.IsPublic);
                    mSet = itemNode;
                }
                else
                {
                    Console.WriteLine("Getter    " + itemNode);
                    Console.WriteLine("Is public    " + itemNode.IsPublic);
                    mGet = itemNode;
                }
            }
            // Assign and read values
            mSet.Invoke(example, new object[] { "Set Value" });
            Console.WriteLine("Retrieved value      " + mGet.Invoke(example, null));
        }</code></pre>

痴者工良

高级程序员劝退师

文章评论