(转).NET面试题系列[6] - 反射

在面试中,通常会考察反射的定义(操作元数据),可以用反射做什么(获得程序集及其各个部件),反射有什么使用场景(ORM,序列化,反序列化,值类型比较等)。如果答得好,还可能会问一下如何优化反射(Emit法,委托法)。

反射的性能远远低于直接调用,但对于必须要使用的场景,它的性能并非不可接受。对于“反射肯定是造成性能差的主要原因”这种说法,要冷静客观的分析。

.NET平台可以使用元数据完整的描述类型(类,结构,委托,枚举,接口)。许多.NET技术,例如WCF或序列化都需要在运行时发现类型格式。在.NET中,查看和操作元数据的动作称为反射(也称为元编程)。

image

反射就是和程序集打交道。上图显示了程序集的阶层关系。通过反射我们可以:

  • 获得一个程序集:这称为动态加载程序集,或者晚期绑定。相对的,早期绑定就是引用程序集,从而在运行时之前就加载它。获得程序集之后,就可以进一步获得其中的类型,然后再进一步获得类型中的方法,属性的值等等。

  • 获得程序集的托管模块。一个程序集可以包含多个托管模块。通常我们对程序集和类型的名字很熟悉,对模块则一无所知,所以这通常没什么用,因为我们获得模块的最终目的还是为了模块中的类型。

  • 获得程序集中(托管模块中的)的类型。此时System.Type类起着十分关键的作用。它可以返回类型对象,之后,我们就可以获得类型的成员和方法表。获得类型对象之后,我们就可以进一步获得类型的成员。

  • 获得类型的成员。常见的情境有遍历所有属性并打印其值,反射调用方法等。ORM通过反射获得类型及其成员,然后为其赋值。

使用反射时,一个重要的类型是System.Type类,其会返回加载堆上的类型对象(包括静态成员和方法表)。当我们要反射一个类的方法时,首先要获得它的类型对象,然后再使用GetMethods方法获得某个方法。获得方法之后,可以使用Invoke执行方法。

反射带来了非常强大的元编程能力,例如动态生成代码。如Ruby的元编程能力,它的ORM可以从数据库的Schema中直接“挖出”字段,而类本身几乎无需定义任何内容,这就是元编程的威力表现之一。

反射有什么应用场景?

在很多时候反射是唯一的选择。

当我们需要动态加载某个程序集(而不是在程序开始时就加载),需要使用反射。但反射最常见的场景是,对象是未知的,或来自外部,或是一个通用的模型例如ORM框架,其针对的对象可以是任何类型。例如:对象的序列化和反序列化。

为什么我们会选择使用反射?因为我们没有办法在编译期通过静态绑定的方式来确定我们要调用的对象。例如一个ORM框架,它要面对的是通用的模型,此时无论是方法也好属性也罢都是随应用场景而改变的,这种完全需要动态绑定的场景下自然需要运用反射。还例如插件系统,在完全不知道外部插件究竟是什么东西的情况下,是一定无法在编译期确定的,因此只能使用动态加载进行加载,然后通过反射探查其方法,并反射调用方法。

.NET中的反射一例

当我们比较两个引用类型的变量是否相等时,我们比较的是这两个变量所指向的是不是堆上的同一个实例(内存地址是否相同)。而当我们比较两个结构体是否相等时,怎么做呢?因为变量本身包含了结构体所有的字段(数据),所以在比较时,就需要对两个结构体的字段进行逐个的一对一的比较,看看每个字段的值是否都相等,如果任何一个字段的值不等,就返回false。

实际上,执行这样的一个比较并不需要我们自己编写代码,Microsoft已经为我们提供了实现的方法:所有的值类型继承自System.ValueType,ValueType和所有的类型都继承自System.Object,Object提供了一个Equals()方法,用来判断两个对象是否相等。但是ValueType覆盖了Object的Equals()方法。当我们比较两个值类型变量是否相等时,可以调用继承自ValueType类型的Equals()方法。这个复写的方法内部使用了反射,获得值类型所有的字段,然后进行比较。

加载程序集(晚期绑定)

先写一个用于演示的类型:

public class Class1
    {
        public int aPublicField;
        private int aPrivateField;
        public int aPublicProperty { get; set; }
        private int aPrivateProperty { get; set; }

        public event EventHandler aEvent;

        //Ctor
        public Class1()
        {

        }

        public void HelloWorld()
        {
            Console.WriteLine("Hello world!");
        }

        public int Add(int a, int b)
        {
            return a + b;
        }
    }

早期绑定就是传统的方式:CLR在运行代码之前,扫描任何可能的类型,然后建立类型对象。晚期绑定则相反,在运行时才建立类型对象。我们可以用System.Reflection中的Assembly类型动态加载程序集。(在需要的时候加载一个外部的程序集)

如果可以选择早期绑定,那么当然是早期绑定更好。因为CLR在早期绑定时会检查类型是否错误,而不是在运行时才判断。

当试图使用晚期绑定时,你是在引用一个在运行时没有加载的程序集。你需要先使用Assembly.Load或LoadFrom方法找到程序集,然后你可以使用GetType获得该程序集的一个类型,最后,使用Activator.CreateInstance(你获得的类型对象)创建该类型的一个实例。

注意,这样创建的类型实例是Object类型。(C# 4引入了动态类型之后,也可以用dynamic修饰这种类型的实例)这个类型对象的方法都不可见,如果要使用它的方法,只能使用反射(例如使用GetMethods获得方法信息,然后再Invoke)。这是反射最普遍的应用场景。

当然,你不应该引用该程序集,否则,就变成早期绑定了。假设我们将上面的演示类型放在一个class library中,然后,在另一个工程中进行晚期绑定。此时我们不将该class library加入参考,而是采用反射的方式,我们试图获取演示类,并创建一个实例,就好像我们加入了参考一样。

class Program
    {
        static void Main(string[] args)
        {
            Assembly a = null;

            try
            {
                a = Assembly.LoadFile(@"C:\CSharpBasic\ReflectionDemoClass\bin\Debug\ReflectionDemoClass.dll");
            }
            catch (Exception ex)
            {
                //Ignore
            }

            if (a != null)
            {
                CreateUsingLateBinding(a);
            }
            Console.ReadLine();
        }

        static void CreateUsingLateBinding(Assembly asm)
        {
            try
            {
                // 获得实例类型,ReflectionDemoClass是命名空间的名字
                Type t = asm.GetType("ReflectionDemoClass.Class1");

                // 晚期绑定建立一个Class1类型的实例
                object obj = Activator.CreateInstance(miniVan);

                // 获得一个方法
                MethodInfo mi = t.GetMethod("HelloWorld");

                // 方法的反射执行(没有参数)
                mi.Invoke(obj, null);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }

使用动态类型可以简化晚期绑定。

获得类型成员

获得类型成员需要先持有一个类型。我们通常通过typeof(这是GetType方法的简写)获得类型对象,然后再使用各种方法获得类型的成员:

GetMembers:默认只获得公开的成员,包括自己和类型所有父类的公开成员。成员包括字段,属性,方法,构造函数等。若想获得特定的成员,可以传入BindingFlags枚举,可以传入多个枚举值:

  • Static:静态成员
  • Instance:实例成员
  • Public:公开成员
  • NonPublic:非公开成员
  • DeclaredOnly:只返回自己类型内的成员,不返回父类的成员

BindingFlags枚举被Flags特性修饰,Flags特性非常适合这种类型的枚举:每次传入的成员数是不定的。从定义上可以看到,每个枚举对应一个数字,其都是2的整数幂:

Default = 0,
IgnoreCase = 1,
DeclaredOnly = 2,
Instance = 4,
Static = 8,
Public = 16,
NonPublic = 32,
……

这种做法有一个特性,就是假如你指定任意一个非负的数字,它都可以唯一的表示成上面各个成员的和,而且只有一种表示方法。例如3可以看成IgnoreCase加上DeclaredOnly,12可以看成Instance加上Static。所以如果你传入Static + Instance(获得静态或者实例成员),实际上你传入的是数字12,编译器将你的数字拆成基本成员的和。

至于为什么只能使用2的整数幂,这是因为2进制中,所有的数字都由0或者1构成。假如我们将上面的列表转化为2进制:

Default =         00000000,
IgnoreCase =    00000001,
DeclaredOnly = 00000010,
Instance =        00000100,
Static =           00001000,
Public =           00010000,
NonPublic =      00100000,
……

这里做了八位,实际上位数的长度由最后一个成员确定。那么对于任意一个非负整数,它的每一位要么是1要么是0。我们将1看作开,0看作关,则每个基本成员都相当于打开了一个特定的位,输入中的每一位如果是1,它就等效于对应的成员处于打开状态。例如取下面的输入00011001,它的第4,5和8位是打开的,也就是说,它等于Public + Static +IgnoreCase。这样我们就可以将它表示为基本成员的相加了。显而易见,这种相加只有一种方式,不存在第二种方式了。

若想使用Flags特性,你需要自己将值赋予各个成员。值必须是2的整数幂,否则Flags特性将失去意义。

如果只想获得方法或者属性,也可以考虑不使用GetMembers+BindingFlags枚举的方式,直接使用GetMethods或GetProperties方法。以下列出了一些获得某种特定类型成员的方法:

ConstructorInfo[] GetConstructors(); //获取指定类型包含的所有构造函数
EventInfo[] GetEvents(); // 获取指定类型包含的所有事件
FieldInfo[] GetFields(); //获取指定类型包含的所有字段
MemberInfo[] GetMembers(); //获取指定类型包含的所有成员
MethodInfo[] GetMethods(); //获取指定类型包含的所有方法
PropertyInfo[] GetProperties(); //获取指定类型包含的所有属性

获得成员之后,我们可以通过相对应的Info类中的成员,来获得成员的值,类型,以及其他信息。需要注意的是,即使成员是私有或受保护的,通过反射一样可以获得其值,甚至可以对其值进行修改。这是ORM的实现基础。这里的演示我们就省去晚期绑定,直接将演示类型写在同一个文件中,例如:

class Program
    {
        public static void Main(string[] args)
        {
            ReflectionDemoClass r = new ReflectionDemoClass();
            //不能在外界访问私有字段
            //r.APrivateField = "1";

            var t = typeof(ReflectionDemoClass);
            FieldInfo[] finfos =
                t.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);

            //通过反射获得私有成员的值 
            foreach (FieldInfo finfo in finfos)
            {
                //甚至修改私有成员的值
                if (finfo.Name == "APrivateField")
                {
                    finfo.SetValue(r, "12345");
                }
                Console.WriteLine("字段名称:{0}, 字段类型:{1}, 值为:{2}", finfo.Name, finfo.FieldType, finfo.GetValue(r));
            }
            Console.ReadKey();
        }
    }

    public class ReflectionDemoClass
    {
        private string APrivateField;

        private string AProperty { get; set; }

        public string AnotherProperty { get; set; }

        public void AMethod()
        {
            Console.WriteLine("I am a method.");
        }

        public void AnotherMethod(string s)
        {
            Console.WriteLine("I am another method, input is " + s);
        }

        public ReflectionDemoClass()
        {
            APrivateField = "a";
            AProperty = "1";
            AnotherProperty = "2";
        }
    }

类型成员除了字段,还有属性,方法,构造函数等。可以通过Invoke调用方法。

//调用方法
            var method = t.GetMethod("AMethod");
            //方法没有输入变量
            method.Invoke(r, null);

            //方法有输入变量
            method = t.GetMethod("AnotherMethod");
            object[] parameters = { "Hello world!" };
            method.Invoke(r, parameters);

方法反射调用有多慢

方法的调用可以分为三种方法:直接调用,委托调用和反射调用。

下面的例子说明了方法的反射调用。假设我们要通过反射更改某个属性的值,这需要呼叫属性的setter。

public static void Main(string[] args)
        {
            var r = new ReflectionDemoClass();
            var t = typeof(ReflectionDemoClass);

            //获得属性的setter
            var pinfo = t.GetProperty("AnotherProperty");
            var setMethod = pinfo.GetSetMethod();

            Stopwatch sw = new Stopwatch();
            sw.Start();

            for (int i = 0; i < 1000000; i++)
            {
                setMethod.Invoke(r, new object[] { "12345" });
            }

            sw.Stop();
            Console.WriteLine(sw.Elapsed + " (Reflection invoke)");

            sw.Restart();

            //直接调用setter
            for (int i = 0; i < 1000000; i++)
            {
                r.AnotherProperty = "12345";
            }
            sw.Stop();
            Console.WriteLine(sw.Elapsed + " (Directly invoke)");
        }

00:00:00.2589952 (Reflection invoke)
00:00:00.0040643 (Directly invoke)
一共调用了一百万次,从结果来看,反射消耗时间是直接调用的60多倍。

方法反射调用为什么慢

反射速度慢有如下几个原因:

  • 反射首先要操作和查找元数据,而直接调用查找元数据这一步是在编译(jit)时
  • 反射调用方法时,没有经过编译器jit的优化。而直接调用的代码是经jit优化后的本地代码
  • 反射调用方法需要检查输入参数的类型,这是在运行时才能做到的,而直接调用的代码检查类型是在编译时
  • 尽管可以通过后面所讲的几种方法优化反射,反射的性能仍然远远不如直接调用

资料:http://www.cnblogs.com/firelong/archive/2010/06/24/1764597.html

使用反射调用方法比直接调用慢上数十倍。反射优化的根本方法只有一条路:避开反射。然而,避开的方法可分为二种:

1. 用委托和表达式树去调用。(绕弯子)

2. 生成直接调用代码,替代反射调用。可以使用System.Reflection.Emit,但如果方法过于复杂,需要非常熟悉IL才可以写出正确的代码。

这两种方法的速度不相上下,扩展阅读中,有使用委托调用增强反射性能的例子。我们通过表达式树来创建强类型的委托,达到调用方法的目的(调用方法也是一个表达式)。这可以大大减少耗时,提高性能。

解决方案1:System.Reflection.Emit

简单来说,就是你完全可以创造一个动态程序集,有自己的类,方法,属性,甚至以直接写IL的方式来做。

精通C#第6版第18章对Emit有详细的论述。Emit命名空间提供了一种机制,允许在运行时构造出新的类型或程序集。这可以看成是反射的一种类型,但又高于反射(反射只是操作,而Emit可以创造)。

一个常见的Emit的应用场景是Moq,它利用Emit在运行时,动态的创建一个新的类型,实现所有的方法,但都是空方法,从而达到构建一个假的类型的目的。

使用Emit构建新的类型(以及它的属性和方法)需要对IL有一定认识。因为Emit的大部分方法是直接被转换为IL的。构建新的类型通常需要以下步骤:

    1.建立一个类,并实现一些类型和方法
    2.在主函数所在的类型中,定义一个静态方法,并传入一个应用程序域
    3.在应用程序域中创建一个新的程序集
    4.在程序集中创建一个新的模块
    5.在模块中创建我们建立的类
    6.使用ILGenerator创建类型的所有方法(包括构造函数)和属性,通常,手写是不太现实的,我们需要使用ildasm.exe获得IL代码,然后再使用ILGenerator造出这些IL代码

例如,假如我们要构造下面方法的IL代码(使用Emit):

        public void AMethod()
        {
            Console.WriteLine("I am a method.");
        }

下面是示例:

public static MethodInfo EmitDemo()
        {
            //创建程序集
            AssemblyName name = new AssemblyName { Name = "MyFirstAssembly" };

            //获取当前应用程序域的一个引用
            AppDomain appDomain = System.Threading.Thread.GetDomain();

            //定义一个AssemblyBuilder变量
            //从零开始构造一个新的程序集
            AssemblyBuilder abuilder = appDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);

            //定义一个模块(Module)
            ModuleBuilder mbuilder = abuilder.DefineDynamicModule("MyFirstModule");

            //创建一个类(Class)
            TypeBuilder emitDemoClass = mbuilder.DefineType("EmitDemoClass", TypeAttributes.Public | TypeAttributes.Class);

            Type ret = typeof(void);

            //创建方法
            MethodBuilder methodBuilder = emitDemoClass.DefineMethod("AMethod", MethodAttributes.Public | MethodAttributes.Static, ret, null);

            //为方法添加代码
            //假设代码就是ReflectionDemoClass中AMethod方法的代码
            ILGenerator il = methodBuilder.GetILGenerator();
            il.EmitWriteLine("I am a method.");
            il.Emit(OpCodes.Ret);

            //在反射中应用
            Type emitSumClassType = emitDemoClass.CreateType();
            return emitSumClassType.GetMethod("AMethod");
        }

从上面的例子可以看到,我们需要和IL打交道,才能在il.Emit中写出正确的代码。我们可以通过ildasm查看IL代码,但如果IL很长,则代码很难写对,而且异常非常难以理解。有兴趣的同学可以参考:

http://www.cnblogs.com/shinings/archive/2009/02/07/1385760.html 以及 http://sunct.iteye.com/blog/745904

http://www.cnblogs.com/fish-li/archive/2013/02/18/2916253.html 一文中有使用Emit对setter的实现。从结果来看,其速度不如委托快。对于需要大量使用反射的场景,例如ORM需要通过反射为属性一个一个赋值,那么它一般也会使用类似的机制来提高性能。

解决方案2:委托

如果需要自己写一个ORM框架,则为属性赋值和得到属性的值肯定是不可避免的操作。我们可以通过Delegate.CreateDelegate建立一个委托,其目标函数是属性的setter,故它有一个输入变量,没有返回值。当Invoke委托时,就调用了setter。编写代码时,目标在于构造一个和目标方法签名相同的委托。

代码如下:

public static void Main(string[] args)
        {
            var r = new ReflectionDemoClass();
            var t = typeof(ReflectionDemoClass);

            //获得属性的setter
            var pinfo = t.GetProperty("AnotherProperty");
            var setMethod = pinfo.GetSetMethod();

            Stopwatch sw = new Stopwatch();
            sw.Start();

            for (int i = 0; i < 1000000; i++)
            {
                setMethod.Invoke(r, new object[] { "12345" });
            }

            sw.Stop();
            Console.WriteLine(sw.Elapsed + " (Reflection invoke)");

            sw.Restart();

            //直接调用setter
            for (int i = 0; i < 1000000; i++)
            {
                r.AnotherProperty = "12345";
            }
            sw.Stop();
            Console.WriteLine(sw.Elapsed + " (Directly invoke)");

            //委托调用
            //建立一个DelegateSetter类型的委托
            //委托的目标函数是ReflectionDemoClass类型中AnotherProperty属性的setter
            DelegateSetter ds = (DelegateSetter) Delegate.CreateDelegate(typeof(DelegateSetter), r,
                    //获得属性的setter
                    typeof(ReflectionDemoClass).GetProperty("AnotherProperty").GetSetMethod());

            sw.Reset();
            sw.Start();
            for (int i = 0; i < 1000000; i++)
            {
                ds("12345");
            }

            sw.Stop();

            Console.WriteLine(sw.Elapsed + " (Delegate invoke)");

            Console.ReadKey();
        }

结果:
00:00:00.3690372 (Reflection invoke)
00:00:00.0068159 (Directly invoke)
00:00:00.0096351 (Delegate invoke)

可以看到委托调用远远胜于反射调用,虽然它还是比不上直接调用快速。对于一个通用的解决方案,我们需要定义一个最最一般类型的委托 - Func<object, object[], object>(接受一个object类型与object[]类型的参数,以及返回一个object类型的结果)。

因为任何事物都是表达式,所以当然也可以通过表达式来执行一个委托。虽然使用表达式比较复杂,但我们可以令表达式接受一般类型的委托,避免每次委托调用都要声明不同的委托。

http://www.cnblogs.com/JeffreyZhao/archive/2008/11/24/invoke-method-by-lambda-expression.html#!comments 该文章使用委托+表达式树法,给出了一个一般的解决方案。它的结果表明,委托的速度略慢于直接调用,但远快过反射。

反射:扩展阅读

扩展阅读中,详细的介绍了委托+表达式树法对反射的优化。可以使用合适的数据结构进行缓存,从而进一步提高性能。对于使用何种数据结构,扩展阅读中有详细的解释和代码。这些内容远远超过了一般公司(即使是BAT)的面试水平,如果不是有开发需求,不需要对这方面进行深入研究。

http://www.cnblogs.com/JeffreyZhao/archive/2009/10/16/jiri-reflection-argue-1-tech.html
http://www.cnblogs.com/JeffreyZhao/archive/2009/02/01/Fast-Reflection-Library.html
http://www.cnblogs.com/fish-li/archive/2013/02/18/2916253.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容