C#中的反射

1 反射与其在C#中的用途

1.1 什么是反射

反射是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

要注意术语“反射”和“内省”(type introspection)的关系。内省(或称“自省”)机制仅指程序在运行时对自身信息(称为元数据)的检测;反射机制不仅包括要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息改变程序状态或结构。

摘自wikipedia “反射” 词条

1.2 在C#中使用反射

接下来介绍几个使用反射的实际应用,通过例程来展示反射在C#中的用途。

1.2.1 使用反射访问实体的Attribute

在先前关于Attribute的介绍中有提到这样的使用方式,通过反射来查看类或者某个成员变量的标签值,这样可以扩展Attribute的用途。例程如下:

using System;
using System.Reflection;

namespace ConsoleApp1
{
    // 自定义一个Attribute,用于存放数据字段的属性
    public class DataFieldAttribute : Attribute
    {
        private string _FieldName;
        private string _FieldType;
        public DataFieldAttribute(string fieldname, string fieldtype)
        {
            this._FieldName = fieldname;
            this._FieldType = fieldtype;
        }
        public string FieldName
        {
            get { return this._FieldName; }
            set { this._FieldName = value; }
        }
        public string FieldType
        {
            get { return this._FieldType; }
            set { this._FieldType = value; }
        }
    }
    
    // 用于测试自定义Attribute的类,其中每个字段被打上了不同的属性
    public class Person
    {
        [DataFieldAttribute("name", "nvarchar")]
        public string 姓名{ get; set; }
        [DataFieldAttribute("age", "int")]
        public int 年龄{ get; set; }
        [DataFieldAttribute("sex", "nvarchar")]
        public string 性别{ get; set; }
    }

    class program
    {
        static void Main()
        {
            Person person = new Person();
            person.姓名 = "小明";
            person.年龄 = 512;
            person.性别 = "male";
            // 通过反射来获取对象中各个字段的属性
            PropertyInfo[] infos = person.GetType().GetProperties();
            object[] objDataFieldAttribute = null;
            // 输出每个字段的属性
            foreach (PropertyInfo info in infos)
            {
                objDataFieldAttribute = info.GetCustomAttributes(typeof(DataFieldAttribute), false);
                Console.WriteLine(info.Name + " -> 该字段名称:" + ((DataFieldAttribute)objDataFieldAttribute[0]).FieldName + " 该字段属性: " + ((DataFieldAttribute)objDataFieldAttribute[0]).FieldType);
            }
        }
    }
}

在本例程中,使用了GetType().GetProperties()方法通过反射来进行一个字段属性的获取,获取到的属性的类型是System.Reflection.PropertyInfo类的数组。

1.2.2 使用反射调用对象的方法

反射还能用于对象方法的调用,如例程所示:

using System;
using System.Reflection;

namespace ConsoleApp1
{
    //实例类
    class ReflectionClass
    {
        public int id;
        // 定义三个私有成员变量以及三个修改该变量的访问器
        private string name;
        private string sex;
        private string age;
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
        public string Age
        {
            get { return age; }
            set { age = value; }
        }
        public string Sex
        {
            get { return sex; }
            set { sex = value; }
        }
        public ReflectionClass(string name, string age)
        {
            this.name = name;
            this.age = age;
        }
        public ReflectionClass(string sex)
        {
            this.sex = sex;
        }
        public ReflectionClass()
        { }
        public void Show()
        {
            Console.WriteLine("姓名:" + name + "\n" + "年龄:" + age + "\n" + "性别:" + sex);
        }
    }
    
    class program
    {
        static void Main()
        {
            ReflectionClass rc = new ReflectionClass();
            Type t = rc.GetType();
            object obj = Activator.CreateInstance(t);
            //取得ID字段
            FieldInfo fi = t.GetField("id");
            //给ID字段赋值
            fi.SetValue(obj, 5);
            //取得Name属性
            PropertyInfo piName = t.GetProperty("Name");
            //给Name属性赋值
            piName.SetValue(obj, "小明", null);
            PropertyInfo piAge = t.GetProperty("Age");
            piAge.SetValue(obj, "15", null);
            //取得Show方法
            MethodInfo mi = t.GetMethod("Show");
            //调用Show方法
            mi.Invoke(obj, null);
            Console.WriteLine("ID为:" + ((ReflectionClass)obj).id);
        }
    }
}

编译并执行该例程,运行结果如下:

image-20210722134403963

1.2.3 使用反射访问对象的私有成员

通过反射,我们还可以访问对象中被设置为private以及protected的成员变量,如例程所示:

using System;
using System.Reflection;

namespace ConsoleApp1
{
    //实例类,定义了一些私有成员变量与受保护的成员变量
    public class RefClass
    {
        private int test3;
        private int test1 { get; set; }
        protected int test2 { get; set; }
        public int Test3 { get; set; }
    }
    
    class program
    {
        static void Main()
        {
            Type t = typeof(RefClass);
            RefClass rc = new RefClass();
            rc.Test3 = 3;
            FieldInfo[] finfos = t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            // 使用反射查看对象中各字段的类型和值
            foreach (FieldInfo finfo in finfos)
            {
                Console.WriteLine("字段名称:{0}  字段类型:{1} rc中的值为:{2}", finfo.Name, finfo.FieldType.ToString(), finfo.GetValue(rc));
            }
            // 通过反射修改字段的值
            Console.Write("\n使用反射修改对象中的成员变量的值:\n");
            foreach (FieldInfo finfo in finfos)
            {
                // 将每个字段的值均设置为99
                finfo.SetValue(rc, 99);
                Console.WriteLine("字段名称:{0}  字段类型:{1} rc中的值为:{2}", finfo.Name, finfo.FieldType.ToString(), finfo.GetValue(rc));
            }
        }
    }
}

编译并运行该例程,其输出如下所示:

image-20210722135917192

可以看到,通过反射的方式,成功获取到了私有变量与受保护变量的类型与值,通过SetValue()方法还能对其中的值进行修改。

1.2.4 通过Reflection.Emit动态生成IL

C#的IL(中间语言)是一种运行在dotNet runtime上的语言,其被dotNet编译器通过JIT的方式动态编译到执行机器的原生代码,从而得以执行。通过创建IL码,可以一定程度上更优化性能,在C#中用上一些动态语言的特性。通过Reflection.Emit能够在程序中动态生成IL码,例程如下:

using System;
using System.Reflection;
using System.Reflection.Emit;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace ConsoleApp1
{
    // 定义一个测试类,内部只有一个简单的成员变量与一个构造函数
    class MyData
    {
        public string tst;

        public MyData()
        {
            for (int i = 0; i < 3; i++)
            {
                tst = i.ToString();
            }
        }
    }
    // 定义一个测试类,其通过生成IL来创建MyData对象
    public class PrivateFieldSetter
    {
        private static readonly FieldInfo MyDataField = typeof(PrivateFieldSetter).GetField("myData", BindingFlags.NonPublic | BindingFlags.Instance);
        private static readonly Action<MyData, object> MyDataSetter = BuildMyDataSetter();
        private MyData myData;
        // 通过反射生成IL来创建对象
        public void SetMyDataByILCode()
        {
            MyDataSetter(new MyData(), this);
            Console.WriteLine("tst 字段的值为: "+myData.tst);
        }
        // 设置IL
        private static Action<MyData, object> BuildMyDataSetter()
        {
            var setterMethod = new DynamicMethod("Set_myData_By_ILCode", null, new[] { typeof(MyData), typeof(object) }, true);
            var ilGenerator = setterMethod.GetILGenerator();

            ilGenerator.Emit(OpCodes.Ldarg_1);
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Stfld, MyDataField);
            ilGenerator.Emit(OpCodes.Ret);

            return setterMethod.CreateDelegate(typeof(Action<MyData, object>)) as Action<MyData, object>;
        }
    }
    
    class program
    {
        static void Main()
        {
            var ILtest = new PrivateFieldSetter();
            ILtest.SetMyDataByILCode();
        }
    }
}

编译并执行后,其运行结果如下:

image-20210722161723095

从运行结果中可知,该程序成功使用反射创建了IL码,并初始化了一个MyData类的对象。

1.3 小结

上文给出的四个例子说明了通过反射,我们可以对C#中对象与实体进行访问,甚至可以通过反射改写类中的私有成员,反射还带给我们了能够将C#用上部分动态语言特性的工具。无疑,作为C#中使用广泛的一种高级特性,反射具有强大的功能,但如此方便的特性并非没有任何代价,下文中我会对反射的性能开销进行一个简明的分析。

2 C#中反射的性能分析

接下来通过BenchmarkdotNet工具来对反射操作的性能开销进行分析,编写例程如下,其主要测试了三种生成对象的方式:通过new直接生成、通过Reflection.Emit动态产生IL来生成,以及通过反射生成。

using System;
using System.Reflection;
using System.Reflection.Emit;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace ConsoleApp1
{
    // 定义一个测试类,内部只有一个简单的成员变量与一个构造函数
    class MyData
    {
        private string tst;

        public MyData()
        {
            for (int i = 0; i < 3; i++)
            {
                tst = i.ToString();
            }
        }
    }
    // 定义一个测试类,其中有三种不同的方法来创建MyData对象
    public class PrivateFieldSetter
    {
        private static readonly FieldInfo MyDataField = typeof(PrivateFieldSetter).GetField("myData", BindingFlags.NonPublic | BindingFlags.Instance);
        private static readonly Action<MyData, object> MyDataSetter = BuildMyDataSetter();
        private MyData myData;
        // 使用new直接创建对象
        [Benchmark]
        public void SetMyDataDirectly()
        {
            this.myData = new MyData();
        }
        // 通过反射生成IL来创建对象
        [Benchmark]
        public void SetMyDataByILCode()
        {
            MyDataSetter(new MyData(), this);
        }
        // 通过typeof用反射来创建对象
        [Benchmark]
        public void SetMyDataByReflection()
        {
            MyDataField.SetValue(this, new MyData());
        }
        // 设置IL
        private static Action<MyData, object> BuildMyDataSetter()
        {
            var setterMethod = new DynamicMethod("Set_myData_By_ILCode", null, new[] { typeof(MyData), typeof(object) }, true);
            var ilGenerator = setterMethod.GetILGenerator();

            ilGenerator.Emit(OpCodes.Ldarg_1);
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Stfld, MyDataField);
            ilGenerator.Emit(OpCodes.Ret);

            return setterMethod.CreateDelegate(typeof(Action<MyData, object>)) as Action<MyData, object>;
        }
    }
    
    class program
    {
        static void Main()
        {
            // 启动Benchmark
            BenchmarkRunner.Run<PrivateFieldSetter>();
        }
    }
}

编译并运行例程,可以看到各方法的性能表现,如下图所示。从输出的结果可以得知,直接创建对象的性能是最好的,其次是通过Reflection.Emit产生IL来创建,直接通过反射创建的性能开销最大,与前两种方法有数量级的差异。

image-20210722160402183

3 总结

通过上文的分析,我们已经了解到了C#中反射的定义,作用,使用方式,以及使用它的代价。总而言之,反射这一工具有着广泛的适用面,用起来也无疑非常方便。但其一方面相较于平铺直叙的传统写法而言更加难以理解,在团队协作中如果注释与文档不到位,可能会一定程度上降低协作的工作效率;另一方面,反射在不使用生成IL来进行性能优化的情况下,与传统方法有着数量级上的性能差距。因此,我个人认为在实际生产项目开发中过多的使用反射,是一件需要慎重考虑的事情。

最后,引用在StackOverFlow上关于问题“What is reflection and why is it useful?(反射是什么?为什么其很有用?)”的一句评论作为结尾。

I'd like to point out that reflection (Without annotations) should be the very last tool you go to when solving a problem. I use it and love it, but it cancels out all the advantages of static typing. If you do need it, isolate it to as small an area as possible (One method or one class). It's more acceptable to use it in tests than production code.

翻译:

我想指出的是,在解决一个问题时,反射(没有注释的情况下)应该是你最后使用的工具。我使用它并喜欢它,但它抵消了静态类型的所有优点。如果你确实需要它,请把它隔离在尽可能小的范围内(一个方法或一个类)。在测试中使用它比在生产代码中使用它更容易被接受。