CLR Inside Out - Reflection

您清晰的组件化目标是否因在库间共享过多类型信息而落空?或许您须要高效的强类型化数据存储,但若是每次对象模型发展后都须要更新您的数据库架构,那会耗费很大成本,因此您更愿意在运行时推断出其类型架构吗?您须要交付能接受任意用户对象的组件,并以某种智能化的方式处理它们吗?您但愿库的调方者能以编程方式向您说明它们的类型吗?
若是您发现本身在苦苦维持强类型化数据结构的同时,又冀望于最大化运行时灵活性,那么您大概会愿意考虑反射,以及它如何改善您的软件。在本专栏中,我将探讨 Microsoft ® .NET Framework 中的 System.Reflection 命名空间,以及它如何为您的开发体验提供助益。我将从一些简单的示例开始,最后将讲述如何处理现实世界中的序列化情形。在此过程当中,我会展现反射和 CodeDom 如何配合工做,以有效处理运行时数据。
在深刻探究 System.Reflection 以前,我想先讨论一下通常的反射编程。首先,反射可定义为由一个编程系统提供的任何功能,此功能使程序员能够在无需提早了解其标识或正式结构的状况下检查和操做代码实体。这部份内容不少,我将逐一展开说明。
首先,反射提供了什么呢?您能用它作些什么呢?我倾向于将典型的以反射为中心的任务分为两类:检查和操做。检查须要分析对象和类型,以收集有关其定义和行为的结构化信息。除了一些基本规定以外,一般这是在事先不了解它们的状况下进行的。(例如,在 .NET Framework 中,任何东西都继承自 System.Object,而且一个对象类型的引用一般是反射的通常起点。)
操做利用经过检查收集到的信息动态地调用代码,建立已发现类型的新实例,或者甚至能够轻松地动态从新结构化类型和对象。须要指出的一个要点是,对于大多数系统,在运行时操做类型和对象,较之在源代码中静态地进行同等操做,会致使性能下降。因为反射的动态特性,所以这是个必要的取舍,不过有不少技巧和最佳作法能够优化反射的性能(有关优化使用反射的更多深刻信息,请参见 msdn.microsoft.com/msdnmag/issues/05/07/Reflection)。
那么,什么是反射的目标呢?程序员实际检查和操做什么呢?在我对反射的定义中,我用了“代码实体”这个新术语,以强调一个事实:从程序员的角度来讲,反射技术有时会使传统对象和类型之间的界限变得模糊。例如,一个典型的以反射为中心的任务多是:
  1. 从对象 O 的句柄开始,并使用反射得到其相关定义(类型 T)的句柄。
  2. 检查类型 T,得到它的方法 M 的句柄。
  3. 调用另外一个对象 O’(一样是类型 T)的方法 M。
请注意,我在从一个实例穿梭到它的底层类型,从这一类型到一个方法,以后又使用此方法的句柄在另外一个实例上调用它 — 显然这是在源代码中使用传统的 C# 编程技术没法实现的。在下文中探讨 .NET Framework 的 System.Reflection 以后,我会再次经过一个具体的例子来解释这一情形。
某些编程语言自己能够经过语法提供反射,而另外一些平台和框架(如 .NET Framework)则将其做为系统库。无论以何种方式提供反射,在给定情形下使用反射技术的可能性至关复杂。编程系统提供反射的能力取决于诸多因素:程序员很好地利用了编程语言的功能表达了他的概念吗?编译器是否在输出中嵌入足够的结构化信息(元数据),以方便往后的解读?有没有一个运行时子系统或主机解释器来消化这些元数据?平台库是否以对程序员有用的方式,展现此解释结果?
若是您头脑中想象的是一个复杂的、面向对象类型的系统,但在代码中却表现为简单的、C 语言风格的函数,并且没有正式的数据结构,那么显然您的程序不可能动态地推断出,某变量 v1 的指针指向某种类型 T 的对象实例。由于毕竟类型 T 是您头脑中的概念,它从未在您的编程语句中明确地出现。但若是您使用一种更为灵活的面向对象语言(如 C#)来表达程序的抽象结构,并直接引入类型 T 的概念,那么编译器就会把您的想法转换成某种往后能够经过合适的逻辑来理解的形式,就象公共语言运行时 (CLR) 或某种动态语言解释器所提供的同样。
反射彻底是动态、运行时的技术吗?简单的说,不是这样。整个开发和执行周期中,不少时候反射对开发人员均可用且有用。一些编程语言经过独立编译器实现,这些编译器将高级代码直接转换成机器可以识别的指令。输出文件只包括编译过的输入,而且运行时没有用于接受不透明对象并动态分析其定义的支持逻辑。这正是许多传统 C 编译器的情形。由于在目标可执行文件中几乎没有支持逻辑,所以您没法完成太多动态反射,然而编译器会不时提供静态反射 — 例如,广泛运用的 typeof 运算符容许程序员在编译时检查类型标识。
另外一种彻底不一样的状况是,解释性编程语言老是经过主进程得到执行(脚本语言一般属于此类)。因为程序的完整定义是可用的(做为输入源代码),并跟完整的语言实现结合在一块儿(做为解释器自己),所以全部支持自我分析所需的技术都到位了。这种动态语言频繁地提供全面反射功能,以及一组用于动态分析和操做程序的丰富工具。
.NET Framework CLR 和它的承载语言如 C# 属于中间形态。编译器用来把源代码转换成 IL 和元数据,后者与源代码相比虽属于较低级别或者较低“逻辑性”,但仍然保留了不少抽象结构和类型信息。一旦 CLR 启动和承载了此程序,基类库 (BCL) 的 System.Reflection 库即可以使用此信息,并返回关于对象类型、类型成员、成员签名等的信息。此外,它也能够支持调用,包括后期绑定调用。

.NET 中的反射
要在用 .NET Framework 编程时利用反射,您可使用 System.Reflection 命名空间。此命名空间提供封装了不少运行时概念的类,例如程序集、模块、类型、方法、构造函数、字段和属性。 图 1 中的表显示,System.Reflection 中的类如何与概念上运行时的对应项对应起来。
  Figure 1 System.Reflection 类

语言组件 相应的 .NET 类
程序集 System.Reflection.Assembly
模块 System.Reflection.Module
抽象成员 System.Reflection.MemberInfo(如下全部的基类)
类型 System.Type
属性 System.Reflection.PropertyInfo
字段 System.Reflection.FieldInfo
事件 System.Reflection.EventInfo
抽象方法 System.Reflection.MethodBase(如下全部的基类)
方法 System.Reflection.MethodInfo
构造函数 System.Reflection.ConstructorInfo
尽管很重要,不过 System.Reflection.Assembly 和 System.Reflection.Module 主要用于定位新代码并将其加载到运行时。本专栏中,我暂不讨论这些部分,而且假定全部相关代码都已经加载。
要检查和操做已加载代码,典型模式主要是 System.Type。一般,您从得到一个所关注运行时类别的 System.Type 实例开始(经过 Object.GetType)。接着您可使用 System.Type 的各类方法,在 System.Reflection 中探索类型的定义并得到其它类的实例。例如,若是您对某特定方法感兴趣,并但愿得到此方法的一个 System.Reflection.MethodInfo 实例(可能经过 Type.GetMethod)。一样,若是您对某字段感兴趣,并但愿得到此字段的一个 System.Reflection.FieldInfo 实例(可能经过 Type.GetField)。
一旦得到全部必要的反射实例对象,便可根据须要遵循检查或操做的步骤继续。检查时,您在反射类中使用各类描述性属性,得到您须要的信息(这是通用类型吗?这是实例方法吗?)。操做时,您能够动态地调用并执行方法,经过调用构造函数建立新对象,等等。

检查类型和成员
让咱们跳转到一些代码中,探索如何运用基本反射进行检查。我将集中讨论类型分析。从一个对象开始,我将检索它的类型,然后考察几个有意思的成员(请参见 图 2)。
  Figure 2 检索对象类型和成员

代码
using System;
using System.Reflection;

// use Reflection to enumerate some basic properties of a type...

namespace Example1
{
    class MyClass
    {
        private int MyField = 0;
        public void MyMethod1() { return; }
        public int MyMethod2(int i) { return i; }
        public int MyProperty { get { return MyField; } }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Reflection Demo Example 1");

            MyClass mc = new MyClass();
            Type t = mc.GetType();
            Console.WriteLine("Type Name: {0}", t.Name);
    
            foreach(MethodInfo m in t.GetMethods())
                Console.WriteLine("Method Name: {0}", m.Name);

            foreach (PropertyInfo p in t.GetProperties())
                Console.WriteLine("Property Name: {0}", p.Name);
        }
    }
}
输出
Reflection Demo Example 1
Type Name: MyClass
Method Name: MyMethod1
Method Name: MyMethod2
Method Name: get_MyProperty
Method Name: GetType
Method Name: ToString
Method Name: Equals
Method Name: GetHashCode
Property Name: MyProperty
首先须要注意的是,在类定义中,乍看起来讲明方法的篇幅比我预期的要多不少。这些额外的方法是从哪里来的呢?任何精通 .NET Framework 对象层次结构的人,都会识别从通用基类 Object 自身继承的这些方法。(事实上,我首先使用了 Object.GetType 检索其类型。)此外,您能够看到属性的 getter 函数。如今,若是您只须要 MyClass 自身显式定义的函数,该怎么办呢?换句话说,您如何隐藏继承的函数?或者您可能只须要显式定义的实例函数?
随便在线看看 MSDN ®,就会发现你们都愿意使用 GetMethods 第二个重载方法,它接受 BindingFlags 参数。经过结合来自 BindingFlags 枚举中不一样的值,您可让函数仅返回所需的方法子集。替换 GetMethods 调用,代之以:
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | 
           BindingFlags.Public)
结果是,您获得如下输出(注意这里不存在静态帮助器函数和继承自 System.Object 的函数)。
Reflection Demo Example 1
Type Name: MyClass
Method Name: MyMethod1
Method Name: MyMethod2
Method Name: get_MyProperty
Property Name: MyProperty
若是您事先知道类型名称(彻底限定)和成员,又该如何?您如何完成从枚举类型向检索类型的转换? 图 3中的示例显示了如何经过 Object.GetType 和 Type.GetMethod,使用描述类型信息的字符串文字,检索实际代码对应项。有了前两个示例中的代码,您已经有了可以实现基元类浏览器的基本组件。经过名称您能够找到一个运行时实体,而后枚举其各类相关属性。
  Figure 3 经过字符串检索类型和 MethodInfo
using System;
using System.Reflection;

// use Reflection to retrieve references to a type and method via name

namespace Example2
{
    class MyClass
    {
        public void MyMethod1() { return; }
        public void MyMethod2() { return; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(“Reflection Demo Example 2”);

            // note that we must use the fully qualified name...
            Type t = Type.GetType(“Example2.MyClass”);
            MethodInfo m = t.GetMethod(“MyMethod1”);

            Console.WriteLine(“Type Name: {0}”, t.Name);
            Console.WriteLine(“Method Name: {0}”, m.Name);
        }
    }
}

动态调用代码
迄今为止,我已经得到运行时对象的句柄(如类型和方法),仅做描述用,例如输出它们的名称。可是如何作得更多呢?如何实际调用某个方法呢? 图 4 显示了如何得到某类型成员的 MethodInfo,而后使用 MethodInfo.Invoke 实际动态调用此方法。
  Figure 4 动态调用方法
using System;
using System.Reflection;

// use Reflection to retrieve a MethodInfo for an 
// instance method and invoke it upon many object instances

namespace Example3
{
    class MyClass
    {
        private int id = -1;

        public MyClass(int id) { this.id = id; }

        public void MyMethod2(object p)
        {
            Console.WriteLine(
                “MyMethod2 is being invoked on object with “ +
                “id {0} with parameter {1}...”, 
                    id.ToString(), p.ToString());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(“Reflection Demo Example 3”);

            MyClass mc1 = new MyClass(1);
            MyClass mc2 = new MyClass(2);
    
            Type t = mc1.GetType();
            MethodInfo method = t.GetMethod(“MyMethod2”);

            for(int i = 1; i <= 5; i++)
                method.Invoke(mc2, new object[]{i});
        }
    }
}
此例的几个要点是:首先,从一个 MyClass, mc1 实例检索一个 System.Type 实例,而后,从该类型检索一个 MethodInfo 实例。最后,当调用 MethodInfo 时,经过把它做为调用的第一个参数来传递,将其绑定到另外一个 MyClass (mc2) 实例中。
前面讲过,对于您预期在源代码中见到的类型和对象使用之间的区别,这个示例使这种区别变得模糊。逻辑上,您检索了一个方法的句柄,而后调用该方法,就象它属于一个不一样的对象同样。对于熟悉函数式编程语言的程序员来讲,这可能垂手可得;但对于只熟悉 C# 的程序员来讲,要分离对象实现和对象实例化,可能就不是那么直观了。

组合在一块儿
至此我已经探讨过检查和调用的基本原理,接下来我会用具体的例子把它们组合在一块儿。设想您但愿交付一个库,带有必须处理对象的静态帮助器函数。但在设计的时候,您对这些对象的类型没有任何概念!这要看函数调用方的指示,看他但愿如何从这些对象中提取有意义的信息。函数将接受一个对象集合,和一个方法的字符串描述符。而后它将遍历该集合,调用每一个对象的方法,用一些函数聚合返回值(请参见 图 5)。
  Figure 5 从对象中提取信息
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Example4
{
    class Program
    {
        static void Main(string[] args)
        {
            // prepare some objects for our function to process
            object[] objs = new object[] {
                new IntReturner(1), new IntReturner(2), 
                new IntReturner(3), new SonOfIntReturner(4), 
                new SonOfIntReturner(5), new SonOfIntReturner(6),
            };

            Console.WriteLine(
                “Attempting to compute average, “ + 
                “passing in array with {0} elements.”, objs.Length);

            int average = ComputeAverage(objs, “GetInteger”);

            Console.WriteLine(“Found an average of {0}!”, average);
        }

        public static int ComputeAverage( 
            IEnumerable<object> objs, string methodname)
        {
            int sum = 0, count = 0;

            Type firstType = null;
            MethodInfo firstMethod = null;

            foreach (object o in objs)
            {
                if (firstMethod == null)
                {
                    firstType = o.GetType();
                    firstMethod = firstType.GetMethod(methodname);
                }

                sum += (int)firstMethod.Invoke(o, null);
                count++;
            }

            // note that we use integer division here (not floating point)
            if (count == 0) return 0;
            return sum / count; 
        }
    }

    class IntReturner
    {
        protected int value = -1;
        public IntReturner(int i) { value = i; }
        public virtual int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of IntReturner, “
                “I’m returning {0}!”, value);
            return value;
        }
    }

    class SonOfIntReturner : IntReturner
    {
        public SonOfIntReturner(int i) : base(i) { }
        public override int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of SonOfIntReturner, “
                “I’m returning {0}!”, this.value);
            return value;
        }
    }

    class EnemyOfIntReturner
    {
        protected int value = -1;
        public EnemyOfIntReturner(int i) { value = i; }
        public virtual int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of EnemyOfIntReturner, “
                “I’m returning {0}!”, value);
            return value;
        }
    }
}
就此例而言,我要声明一些约束条件。首先,字符串参数描述的方法(必须由每一个对象的底层类型实现)不会接受任何参数,并将返回一个整数。代码将遍历对象集合,调用指定的方法,逐步计算出全部值的平均值。最后,由于这不是生产代码,在求和的时候我不用担忧参数验证或整数溢出。
在浏览示例代码时,能够看到主函数与静态帮助器 ComputeAverage 之间的协议除了对象自身的通用基类以外,并不依赖任何类型信息。换句话说,您能够完全改变正在传送的对象的类型和结构,但只要老是能使用字符串描述一个方法,且该方法返回整数,ComputeAverage 就能够正常工做!
须要注意的一个关键问题跟隐藏在最后这个例子中的 MethodInfo(通常反射)有关。注意,在 ComputeAverage 的 foreach 循环中,代码只从集合中的第一个对象中抓取一个 MethodInfo,而后绑定用于全部后续对象的调用。正如编码所示,它运行良好 — 这是 MethodInfo 缓存的一个简单例子。但此处有一个根本性的局限。MethodInfo 实例仅能由其检索对象同等层级类型的实例调用。由于传入了 IntReturner 和 SonOfIntReturner(继承自 IntReturner)的实例,才能这样运行。
在示例代码中,已经包含了名为 EnemyOfIntReturner 的类,它实现了与其余两个类相同的基本协议,但并无共享任何常见共享类型。换句话说,该接口逻辑上等同,但在类型层级上没有重叠。要探讨 MethodInfo 在该情形下的使用,请尝试向集合添加其余对象,经过“new EnemyOfIntReturner(10)”获得一个实例,再次运行示例。您会遇到一个异常,指出 MethodInfo 不能用于调用指定的对象,由于它和得到 MethodInfo 时的原始类型彻底无关(即便方法名称和基本协议是等同的)。要使您的代码达到生产水准,您须要作好遇到这一情形的准备。
一个可能的解决方案能够是经过本身分析全部传入对象的类型,保留对其共享的类型层级(若是有)的解释。若是下一对象的类型与任意已知类型层级相异,就须要获取和存储一个新的 MethodInfo。另外一解决方案是捕获 TargetException,并从新获取一个 MethodInfo 实例。这里提到的两种解决方案都各有其优缺点。Joel Pobar 为本杂志 2007 五月期写过一篇 优秀的文章,内容关于 MethodInfo 缓冲和我所极力推荐的反射性能。
但愿此示例演示的向应用程序或框架中添加反射,能够为往后的自定义或可扩展性增长更多的灵活性。不能否认,较之本机编程语言中的同等逻辑,使用反射可能会有些繁琐。若是您感到对您或您的客户来讲,向代码中添加基于反射的后期绑定过于麻烦(毕竟他们须要以某种方式在您的框架中说明他们的类型和代码),那么可能仅须要适度的灵活性以取得某种平衡。

序列化的高效类型处理
至此咱们已经过若干示例讲述了 .NET 反射的基本原理,接下来让咱们看一下现实世界中的情形。若是您的软件经过 Web 服务或其余进程外远程技术与其余系统进行交互,那么您极可能已经遇到序列化问题。序列化本质上是将活动的、占用内存的对象,转变成适合线上传输或磁盘存储的数据格式。
.NET Framework 中的 System.Xml.Serialization 命名空间提供了拥有 XmlSerializer 的强大序列化引擎,它可使用任意托管对象,并将其转换成 XML(往后也可将 XML 数据转换回类型化的对象实例,这一过程称之为反序列化)。XmlSerializer 类是一种强大的、企业就绪的软件片段,若是您在项目中面临序列化问题,它将是您的首选。但为了教学目的,咱们来探讨如何实现序列化(或者其余相似的运行时类型处理实例)。
设想情形:您正在交付一个框架,须要使用任意用户类型的对象实例,并将其转换成某种智能型数据格式。例如,假定有一个驻留内存的对象,类型为以下所示的 Address:
(pseudocode)
class Address
{
    AddressID id;
    String Street, City;
    StateType State;
    ZipCodeType ZipCode;
}
如何生成适当的数据表示形式以方便往后使用?或许一个简单的文本呈现将解决这一问题:
Address: 123
    Street: 1 Microsoft Way
    City: Redmond
    State: WA
    Zip: 98052
若是事先彻底了解须要转换的正式数据类型(例如本身编写代码时),事情就变得很是简单:
foreach(Address a in AddressList)
{
    Console.WriteLine(“Address:{0}”, a.ID);
    Console.WriteLine(“\tStreet:{0}”, a.Street);
    ... // and so on
}
然而,若是预先不知道在运行时会遇到的数据类型,状况会变得十分有趣。您如何编写象这样的通常框架代码?
MyFramework.TranslateObject(object input, MyOutputWriter output)
首先,您须要决定哪些类型成员对序列化有用。可能的状况包括仅捕获特定类型的成员,例如基元系统类型,或提供一种机制以供类型做者说明哪些成员须要被序列化,例如在类型成员上使用自定义属性做为标记)。您仅能够捕获特定类型的成员,例如基元系统类型,或类型做者可以说明哪些成员须要被序列化(可能的方法是在类型成员上使用自定义属性做为标记)。
一旦记录清楚须要转换的数据结构成员,您接着须要作的是编写逻辑,从传入的对象枚举和检索它们。反射在这里担负了繁重的任务,让您既能够查询数据结构又能够查询数据值。
出于简单性考虑,咱们来设计一个轻型转换引擎,获得一个对象,获取全部其公共属性值,经过直接调用 ToString 将它们转换成字符串,而后将这些值序列化。对于一个名为“input”的给定对象,算法大体以下:
  1. 调用 input.GetType 以检索 System.Type 实例,该实例描述了 input 的底层结构。
  2. 用 Type.GetProperties 和适当的 BindingFlags 参数,将公共属性做为 PropertyInfo 实例检索。
  3. 使用 PropertyInfo.Name 和 PropertyInfo.GetValue,将属性做为键-值对检索。
  4. 在每一个值上调用 Object.ToString 将其(经过基本方式)转化为字符串格式。
  5. 将对象类型的名称和属性名称、字符串值的集合打包成正确的序列化格式。
这一算法明显简化了事情,同时也抓住了获得运行时数据结构,并将其转化为自描述型数据的要旨。但这里有一个问题:性能。以前提到,反射对于类型处理和值检索的成本都很高。本示例中,我在每一个提供类型的实例中执行了完整的类型分析。
若是以某种方式能够捕获或保留您对于类型结构的理解,以便往后不费力地检索它,并有效处理该类型的新实例;换句话说,就是往前跳到示例算法中的步骤 #3?好消息是,利用 .NET Framework 中的功能,彻底可能作到这一点。一旦您理解了类型的数据结构,即可以使用 CodeDom 动态生成绑定到该数据结构的代码。您能够生成一个帮助器程序集,其中包含帮助器类和引用了传入类型并直接访问其属性的方法(相似托管代码中的任何其余属性),所以类型检查只会对性能产生一次影响。
如今我将修正这一算法。新类型:
  1. 得到对应于该类型的 System.Type 实例。
  2. 使用各类 System.Type 访问器检索架构(或至少检索对序列化有用的架构子集),例如属性名称、字段名称等。
  3. 使用架构信息生成帮助器程序集(经过 CodeDom),该程序集与新类型相连接,并有效地处理实例。
  4. 在帮助器程序集中使用代码,提取实例数据。
  5. 根据须要序列化数据。
对于给定类型的全部传入数据,能够往前跳到步骤 #4,较之显式检查每一实例,这么作能够得到巨大的性能提高。
我开发了一个名为 SimpleSerialization 的基本序列化库,它用反射和 CodeDom(本专栏中可下载)实现了这一算法。主要组件是一个名为 SimpleSerializer 的类,是用户用一个 System.Type 实例构造所得。在构造函数中,新的 SimpleSerializer 实例会分析给定的类型,利用帮助器类生成一个临时程序集。该帮助器类会紧密绑定到给定的数据类型,并且对实例的处理方式就象本身在彻底事先了解类型的状况下编写代码那样。
SimpleSerializer 类有以下布局:
class SimpleSerializer
{
    public class SimpleSerializer(Type dataType);

    public void Serialize(object input, SimpleDataWriter writer);
}
简单地使人惊叹!构造函数承担了最繁重的任务:它使用反射来分析类型结构,而后用 CodeDom 生成帮助器程序集。SimpleDataWriter 类只是用来阐明常见序列化模式的数据接收器。
要序列化一个简单的 Address 类实例,用下面的伪代码便可完成任务:
SimpleSerializer mySerializer = new SimpleSerializer(typeof(Address));
SimpleDataWriter writer = new SimpleDataWriter();
mySerializer.Serialize(addressInstance, writer);

结束
强烈建议您亲自试用一下示例代码,尤为是 SimpleSerialization 库。我在 SimpleSerializer 一些有趣的部分都添加了注释,但愿可以有所帮助。固然,若是您须要在产品代码中进行严格的序列化,那么确实要依靠 .NET Framework 中提供的技术(例如 XmlSerializer)。但若是您发如今运行时须要使用任意类型并能高效处理它们,我但愿您采用个人 SimpleSerialization 库做为本身的方案。
对 CLR 开发人员 Weitao Su(反射)和 Pete Sheill (CodeDom) 所提供的指导和反馈,在此深表谢意。

请将您想询问的问题和提出的意见发送至  clrinout@microsoft.com. 程序员


Mike Repass是 .NET Framework CLR 团队的项目经理。他主要负责反射、CodeDom 以及执行引擎的各部分。您能够经过他的博客  blogs.msdn.com/mrepass 与他联系。
相关文章
相关标签/搜索