进阶系列(10)—— C#元数据和动态编程
一、元数据的介绍
元数据是用来描述数据的数据(Data that describes other data)。单单这样说,不太好理解,我来举个例子。下面是契诃夫的小说《套中人》中的一段,描写一个叫做瓦莲卡的女子:
(她)年纪已经不轻,三十岁上下,个子高挑,身材匀称,黑黑的眉毛,红红的脸蛋--一句话,不是姑娘,而是果冻,她那样活跃,吵吵嚷嚷,不停地哼着小俄罗斯的抒情歌曲,高声大笑,动不动就发出一连串响亮的笑声:哈,哈,哈!
这段话里提供了这样几个信息:年龄(三十岁上下)、身高(个子高挑)、相貌(身材匀称,黑黑的眉毛,红红的脸蛋)、性格(活跃,吵吵嚷嚷,不停地哼着小俄罗斯的抒情歌曲,高声大笑)。有了这些信息,我们就可以大致想像出瓦莲卡是个什么样的人。推而广之,只要提供这几类的信息,我们也可以推测出其他人的样子。这个例子中的"年龄"、"身高"、"相貌"、"性格",就是元数据,因为它们是用来描述具体数据/信息的数据/信息。
当然,这几个元数据用来刻画个人状况还不够精确。我们每个人从小到大,都填过《个人情况登记表》之类的东西吧,其中包括姓名、性别、民族、政治面貌、一寸照片、学历、职称等等......这一套元数据才算比较完备。
在日常生活中,元数据无所不在。有一类事物,就可以定义一套元数据。喜欢拍摄数码照片的朋友应该知道,每张数码照片都包含EXIF信息。它就是一种用来描述数码图片的元数据。按照Exif 2.1标准,其中主要包含这样一些信息:
Image Description 图像描述、来源. 指生成图像的工具
Artist 作者 有些相机可以输入使用者的名字
Make 生产者 指产品生产厂家
Model 型号 指设备型号
Orientation方向 有的相机支持,有的不支持
XResolution/YResolution X/Y方向分辨率 本栏目已有专门条目解释此问题。
ResolutionUnit分辨率单位 一般为PPI
Software软件 显示固件Firmware版本
DateTime日期和时间
YCbCrPositioning 色相定位
ExifOffsetExif信息位置,定义Exif在信息在文件中的写入,有些软件不显示。
ExposureTime 曝光时间 即快门速度
FNumber光圈系数
ExposureProgram曝光程序 指程序式自动曝光的设置,各相机不同,可能是Sutter Priority(快门优先)、Aperture Priority(快门优先)等等。
ISO speed ratings感光度
ExifVersionExif版本
DateTimeOriginal创建时间
DateTimeDigitized数字化时间
ComponentsConfiguration图像构造(多指色彩组合方案)
CompressedBitsPerPixel(BPP)压缩时每像素色彩位 指压缩程度
ExposureBiasValue曝光补偿。
MaxApertureValue最大光圈
MeteringMode测光方式, 平均式测光、中央重点测光、点测光等。
Lightsource光源 指白平衡设置
Flash是否使用闪光灯。
FocalLength焦距,一般显示镜头物理焦距,有些软件可以定义一个系数,从而显示相当于35mm相机的焦距 MakerNote(User Comment)作者标记、说明、记录
FlashPixVersionFlashPix版本 (个别机型支持)
ColorSpace色域、色彩空间
ExifImageWidth(Pixel X Dimension)图像宽度 指横向像素数
ExifImageLength(Pixel Y Dimension)图像高度 指纵向像素数
Interoperability IFD通用性扩展项定义指针 和TIFF文件相关,具体含义不详
FileSource源文件 Compression压缩比。
我再举一个例子。在电影数据库IMDB上可以查到每一部电影的信息。IMDB本身也定义了一套元数据,用来描述每一部电影。下面是它的一级元数据,每一级下面又列出了二级元数据,总共加起来,可以从100多个方面刻画一部电影:
Cast and Crew(演职人员)、Company Credits(相关公司)、Basic Data(基本情况)、Plot & Quotes(情节和引语)、Fun Stuff(趣味信息)、Links to Other Sites(外部链接)、Box Office and Business(票房和商业开发)、Technical Info(技术信息)、Literature(书面内容)、Other Data(其他信息)。
元数据最大的好处是,它使信息的描述和分类可以实现格式化,从而为机器处理创造了可能。
二、元数据的使用
你是 Adventure Works 公司的开发人员,当前你需要创建一个应用程序,以报告由该公司开发人员编写的代码。该应用程序将加载一个程序集并使用反射来生成要用于代码评审和文档化的报告。要改进性能,需要将该应用程序创建为控制台应用程序。在本练习中,将使用 Assembly 类加载程序集,而程序集名称和路径应由用户提供。
你希望创建一个对不同应用程序中编写的代码进行文档化的应用程序。在本练习中,该应用程序将使用多个信息类型反射类,以生成一个用于代码评审和文档化的基于控制台的报告。该报告会列举程序集的名称、位置、版本和其他程序集级别元数据。该报告还会列举程序集中的模块、类、成员、方法和属性。
(一)步骤1:显示程序集信息
1) 声明一个接受程序集作为参数的名为GenerateReport 的方法。
2) 编写代码,以显示程序集信息。根据程序集在全局程序集缓存中的位置,显示程序集的名称和位置以及程序集的状态。
结果:已经显示程序集信息。
(二)步骤2:显示程序集中所有模块和类型
1) 使用 GetModules 方法创建一个包含程序集中模块的报告。该代码会为程序集中的每个模块调用DisplayModuleInfo方法。
2) 声明一个接受Module 方法作为参数的名为 DisplayModuleInfo 的方法。
3) 通过使用 GetTypes 方法来显示每个方法中包含的类型列表。该代码会为模块中的每个类型调用 DisplayTypeInfo 方法。
结果:已经显示了该程序集包含的模块列表和每个模块包含的类型列表。
(三)步骤3:显示所有类型中的字段、属性和方法。
1) 声明一个接受Type 作为参数的名为 DisplayTypeInfo 的方法。
2) 使用 GetFields 方法显示所有类型中的字段列表。
该列表应包含以下信息:字段名、 字段类型、 字段是公共还是私有
3) 使用 GetProperties 方法显示所有属性列表。
该列表应包含以下信息: 属性名、属性类型、 属性是可读还是可写
4) 显示当前类型中所有方法的列表。
该代码会为类型中的每个方法调用 DisplayMethodInfo 方法。
结果:已经显示每种类型中的字段列表、属性列表以及方法列表。
(四)步骤4:显示所有方法中包含的信息
1) 声明一个接受 MethodInfo 作为参数的名为 DisplayMethodInfo 的方法。
2) 使用 GetParameters 方法显示方法头文件信息。
3) 使用 GetParameters 方法显示所有参数的列表。
该列表应包含以下信息:参数名、参数类型、参数是否为 IsOptional、IsOut 或 IsRetval。
4) 使用 GetMethodBody 方法显示关于每个方法的信息。
该信息应该包含方法中定义的堆栈大小和局部变量详细信息,例如LocalIndex和LocalType。
5) 编译并运行该应用程序。
主要代码如下:
using System; using System.Collections.Generic; using System.Text; using System.Reflection; namespace crseae_lab { class Program { static string ReadAssemblyFullPath() { string AssemblyPath, AssemblyName; Console.Write("Type the assembly path: "); AssemblyPath = Console.ReadLine(); Console.Write("Type the assembly name: "); AssemblyName = Console.ReadLine(); return AssemblyPath + @"/" + AssemblyName; } static void Main(string[] args) { string AssemblyFullPath; AssemblyFullPath = ReadAssemblyFullPath(); GenerateReport(AssemblyFullPath); Console.Read(); } static void GenerateReport(string path) { Assembly myAssemble = Assembly.LoadFile(path); Console.WriteLine(myAssemble.FullName); //myAssemble. Console.WriteLine(string.Empty.PadLeft(, '-')); foreach (Type type in myAssemble.GetTypes()) { Console.WriteLine("The name of Assemble is:"+type.Name); Console.WriteLine("The path is:" + type.FullName); } GetModules(myAssemble); } static void GetModules(Assembly name) { Console.WriteLine(string.Empty.PadLeft(, '-')); foreach (Module module in name.GetModules()) { Console.WriteLine("Module name:" + module.Name); DisplayModuleInfo(module); } } static void DisplayModuleInfo(Module module) { Console.WriteLine(string.Empty.PadLeft(, '-')); foreach (Type type in module.GetTypes()) { Console.WriteLine("Module type name is:" + type.Name); DisplayTypeInfo(type); } } static void DisplayTypeInfo(Type type) { foreach (FieldInfo fieldInfo in type.GetFields()) { Console.WriteLine("The name of this field is:" + fieldInfo.Name); Console.WriteLine("The type of this field is:" + fieldInfo.GetType().ToString()); if (fieldInfo.IsPrivate) { Console.WriteLine("This field is Private!"); } else { Console.WriteLine("This field is public"); } } Console.WriteLine(string.Empty.PadLeft(, '-')); foreach (PropertyInfo propertyInfo in type.GetProperties()) { Console.WriteLine("The name of this property is:" + propertyInfo.Name); Console.WriteLine("Type of this property is:" + propertyInfo.PropertyType.ToString()); if (propertyInfo.CanWrite) { Console.WriteLine("This property can write!"); } else { Console.WriteLine("This property can't be write"); } } Console.WriteLine(string.Empty.PadLeft(, '-')); foreach (MethodInfo method in type.GetMethods()) { Console.WriteLine("Method name:" + method.Name); DisplayMethodInfo(method); } } static void DisplayMethodInfo(MethodInfo method) { Console.WriteLine(string.Empty.PadLeft(, '-')); foreach (ParameterInfo parInfo in method.GetParameters()) { Console.WriteLine("Parameter name is:"+parInfo.Name); Console.WriteLine("Parameter type is:" + parInfo.ParameterType.ToString()); if (parInfo.IsOptional) { Console.WriteLine("This parameter is option!"); } else Console.WriteLine("This parrameter is not option!"); if (parInfo.IsOut) { Console.WriteLine("This parameter is out!"); } else Console.WriteLine("This parameter is not out!"); if (parInfo.IsRetval) Console.WriteLine("and is retval"); else Console.WriteLine("and is not retval"); } if (method.GetMethodBody() != null) { GetMethodBody(method.GetMethodBody()); } } static void GetMethodBody(MethodBody body) { Console.WriteLine(string.Empty.PadLeft(, '-')); Console.WriteLine("Body Information :"); Console.WriteLine("Max stack size:" + body.MaxStackSize); Console.WriteLine("Local variables are initialized:" + body.InitLocals); Console.WriteLine(string.Empty.PadLeft(, '-')); foreach (LocalVariableInfo local in body.LocalVariables) { Console.WriteLine("Index of local var is:" + local.LocalIndex); Console.WriteLine("Type of local var is:"+local.LocalType.ToString()); } Console.WriteLine(string.Empty.PadLeft(, '*')); } } }
三、动态类型介绍
提到动态类型当然就要说下静态类型了,对于什么是静态类型呢? 大家都知道之前C#一直都是静态语言(指定的是没有引入动态类型之前,这里说明下,不是引入了动态类型后C#就是动态语言,只是引入动态类型后,为C#语言增添了动态语言的特性,C#仍然是静态语言),之所以称为静态语言,之前我们写代码时,例如 int i =5;这样的代码,此时i 我们已经明确知道它的类型为int了,然而这样的代码,变量的类型的确定是在编译时确定的,对应的,如果类型的确定是在执行时才确定的类型,这样的类型就是动态类型(C# 4.0中新添加了一个dynamic 关键字来定义我们的动态类型)。面对动态类型,C#编译器做的工作只是完成检查语法是否正确,但无法确定所调用的方法或属性是否正确(之所以会这样,主要还是因为动态类型是运行时才知道它的具体类型,所以编译器编译的时候肯定不知道类型,就没办法判断调用的方法或属性是不是存在和正确了,所以对于动态类型,将不能使用VS提供的智能提示的功能,这样写动态类型代码时就要求开发人员对于某个动态类型必须准确知道其类型后和所具有的方法和属性了,不能这些错误只能在运行程序的过程抛出异常的方式被程序员所发现。)
补充: 讲到dynamic关键字,也许大家会想到C# 3中的var关键字,这里这里补充说明下dynamic, var区别。var 关键字不过是一个指令,它告诉编译器根据变量的初始化表达式来推断类型。(记住var并不是类型),而C# 4中引入的dynamic是类型,但是编译时不属于CLR类型(指的int,string,bool,double等类型,运行时肯定CLR类型中一种的),它是包含了System.Dynamic.DynamicAttribute特性的System.Object类型,但与object又不一样,不一样主要体现在动态类型不会在编译时时执行显式转换,下面给出一段代码代码大家就会很容易看出区别了:
object obj = 10; Console.WriteLine(obj.GetType()); // 使用object类型此时需要强制类型转换,不能编译器会出现编译错误 obj = (int)obj + 10; dynamic dynamicnum = 10; Console.WriteLine(dynamicnum.GetType()); // 对于动态类型而言,编译时编译器根本不知道它是什么类型, // 所以编译器就判断不了dynamicnum的类型了,所以下面的代码不会出现编译时错误 // 因为dynamicnum有可能是int类型,编译器不知道该变量的具体类型不能凭空推测类型 // 当然也就不能提示我们编译时错误了 dynamicnum = dynamicnum + 10;
四、为什么需要动态类型
第一部分和大家介绍了什么是动态类型,对于动态类型,总结为一句话为——运行时确定的类型。然而大家了解了动态类型到底是什么之后,当然又会出现新的问题了,即动态类型有什么用的呢? C# 为什么好端端的引入动态类型增加程序员的负担呢? 事实并不是这样的,下面就介绍了动态类型到底有什么用,它并不是所谓给程序员带来负担,一定程度上讲是福音
(一)使用动态类型可以减少强制类型转换
从第一部分的补充也可以看到,使用动态类型不需要类型转换是因为编译器根本在编译时的过程知道什么类型,既然不知道是什么类型,怎么判断该类型是否能进行什么操作,所以也就不会出现类似“运算符“+”无法应用于“object”和“int”类型的操作数“或者”不存在int类型到某某类型的隐式转换“的编译时错误了,可能这点用户,开发人员可能并不觉得多好的,因为动态类型没有智能提示的功能。 但是动态类型减少了强制类型转换的代码之后,可读性还是会有所增强。(这里又涉及到个人取舍问题的, 如果自己觉得那种方式方便就用那种的,没必要一定要用动态类型,主要是看那种方式可以让自己和其他开发人员更好理解)
(二)使用动态类型可以使C#静态语言中调用Python等动态语言
对于这点,可能朋友有个疑问,为什么要在C#中使用Python这样的动态语言呢? 对于这个疑问,就和在C#中通过P/Invoke与本地代码交互,以及与COM互操作的道理一样,假设我们要实现的功能在C#类库中没有,然而在Python中存在时,此时我们就可以直接调用Python中存在的功能了。
五、动态类型的使用
前面两部分和大家介绍动态类型的一些基础知识的,了解完基础知识之后,大家肯定很迫不及待地想知道如何使用动态类型的,下面给出两个例子来演示动态类型的使用的。
(一)C# 4 通过dynamic关键字来实现动态类型
dynamic dyn = 5; Console.WriteLine(dyn.GetType()); dyn = "test string"; Console.WriteLine(dyn.GetType()); dynamic startIndex = 2; string substring = dyn.Substring(startIndex); Console.WriteLine(substring); Console.Read();
运行结果为:
(二)在C#中调用Python动态语言(要运行下面的代码,必须下载并安装IronPython,IronPython 是在 .NET Framework 上实现的第一种动态语言。http://ironpython.codeplex.com下载 )
// 引入动态类型之后 // 可以在C#语言中与动态语言进行交互 // 下面演示在C#中使用动态语言Python ScriptEngine engine = Python.CreateEngine(); Console.Write("调用Python语言的print函数输出: "); // 调用Python语言的print函数来输出 engine.Execute("print 'Hello world'"); Console.Read();
运行结果:
六、动态类型背后的故事
知道了如何在C#中调用动态语言之后,然而为什么C# 为什么可以使用动态类型呢?C#编译器到底在背后为我们动态类型做了些什么事情的呢? 对于这些问题,答案就是DLR(Dynamic Language Runtime,动态语言运行时),DLR使得C#中可以调用动态语言以及使用dynamic的动态类型。提到DLR时,可能大家会想到.Net Framework中的CLR(公共语言运行时),然而DLR 与CLR到底是什么关系呢?下面就看看.Net 4中的组件结构图,相信大家看完之后就会明白两者之间的区别:
从图中可以看出,DLR是建立在CLR的基础之上的,其实动态语言运行时是动态语言和C#编译器用来动态执行代码的库,它不具有JIT编译,垃圾回收等功能。然而DLR在代码的执行过程中扮演的是什么样的角色呢? DLR所扮演的角色就是——DLR通过它的绑定器(binder)和调用点(callsite),元对象来把代码转换为表达式树,然后再把表达式树编译为IL代码,最后由CLR编译为本地代码(DLR就是帮助C#编译器来识别动态类型)。 这里DLR扮演的角色并不是凭空想象出来的,而且查看它的反编译代码来推出来的,下面就具体给出一个例子来说明DLR背后所做的事情。C#源代码如下:
class Program { static void Main(string[] args) { dynamic text = "test text"; int startIndex = 2; string substring = text.Substring(startIndex); Console.Read(); } }
通过Reflector工具查看生成的IL代码如下:
private static void Main(string[] args) { object text = "test text"; int startIndex = 2; if (<Main>o__SiteContainer0.<>p__Site1 == null) { // 创建用于将dynamic类型隐式转换为字符串的调用点 <Main>o__SiteContainer0.<>p__Site1 = CallSite<Func<CallSite, object, string>>.Create(Binder.Convert(CSharpBinderFlags.None, typeof(string), typeof(Program))); } if (<Main>o__SiteContainer0.<>p__Site2 == null) { // 创建用于调用Substring函数的调用点 <Main>o__SiteContainer0.<>p__Site2 = CallSite<Func<CallSite, object, int, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.None, "Substring", null, typeof(Program), new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null) })); } // 调用调用点,首先调用<>p_Site2,即Substring方法,再调用<>P_Site1来将结果进行转换 string substring = <Main>o__SiteContainer0.<>p__Site1.Target(<Main>o__SiteContainer0.<>p__Site1, <Main>o__SiteContainer0.<>p__Site2.Target(<Main>o__SiteContainer0.<>p__Site2, text, startIndex)); Console.Read(); } //编译器生成的内嵌类型为 [CompilerGenerated] private static class <Main>o__SiteContainer0 { // Fields public static CallSite<Func<CallSite, object, string>> <>p__Site1; public static CallSite<Func<CallSite, object, int, object>> <>p__Site2; }
从IL代码中可以看出Main方法内包含两个动态操作,因为编译器生成的内嵌类型包含两个调用点(CallSite<T>,CallSite<T>即是System.Runtime.CompilerServices命名空间下的一个类,关于CallSite的具体信息可以查看MSDN中的介绍——CallSite<T> )字段,一个是调用Substring方法(即<>p__Site2),一个是将结果(编译时时dynamic)动态地转换为字符串(即<>p__Site1),下面给出动态类型的执行过程(注意DLR中有一个缓存的概念):
七、动态类型的约束
相信通过前面几部分的介绍大家已经对动态类型有了一定的了解的,尤其是第四部分的介绍之后,大家应该对于动态类型的执行过程也有了一个清晰的认识了,然而有些函数时不能通过动态绑定来进行调用的,这里就涉及到类型类型的约束:
(一)不能用动态类型作为参数调用扩展方法
不能用动态类型作为参数来调用扩展方法的原因是——调用点知道编译器所知道的静态类型,但是它不知道调用所在的源文件在哪里,以及using指令引入了哪些命名空间,所以在编译时调用点就找不到哪些扩展方法可以使用,所以就会出现编译时错误。下面给出一个简单的示例程序:
var numbers = Enumerable.Range(10, 10); dynamic number = 4; var error = numbers.Take(number); // 编译时错误 // 通过下面的方式来解决这个问题 // 1. 将动态类型转换为正确的类型 var right1 = numbers.Take((int)number); // 2. 用调用静态方法的方式来进行调用 var right2 = Enumerable.Take(numbers, number);
(二) 委托与动态类型不能隐式转换的限制
如果需要将Lambda表达式,匿名方法转化为动态类型时,此时编译器必须知道委托的确切类型,不能不加强制转化就把他们设置为Delegae或object变量,此时不同string,int类型(因为前面int,string类型可以隐式转化为动态类型,编译器此时会把他们设置为object类型。但是匿名方法和Lambda表达式不能隐式转化为动态类型),如果需要完成这样的转换,此时必须强制指定委托的类型,下面是一个演示例子:
dynamic lambdarestrict = x => x + 1; // 编译时错误 // 解决方案 dynamic rightlambda =(Func<int,int>)( x=>x+1); dynamic methodrestrict = Console.WriteLine; // 编译时错误 // 解决方案 dynamic rightmethod =(Action<string>)Console.WriteLine;
(三) 动态类型不能调用构造函数和静态方法的限制——即不能对动态类型调用构造函数或静态方法,因为此时编译器无法指定具体的类型。
dynamic s = new dynamic();
(四) 类型声明和泛型类型参数
不能声明一个基类为dynamic的类型,也不能将dynamic用于类型参数的约束,或作为类型所实现的接口的一部分,下面看一些具体的例子来加深概念的理解:
// 基类不能为dynamic 类型 class DynamicBaseType : dynamic { } // dynamic类型不能为类型参数的约束 class DynamicTypeConstrain<T> where T : dynamic { } // 不能作为所实现接口的一部分 class DynamicInterface : IEnumerable<dynamic> { }
八、实现动态的行为
介绍了这么动态类型,是不是大家都迫不及待地想知道如果让自己的类型具有动态的行为呢? 然而实现动态行为有三种方式:
- 使用ExpandObject
- 使用DynamicObject
- 实现IDynamicMetaObjectProvider接口.
下面就从最简单的方式:
(一)使用ExpandObject来实现动态的行为
using System; // 引入额外的命名空间 using System.Dynamic; namespace 自定义动态类型 { class Program { static void Main(string[] args) { dynamic expand = new ExpandoObject(); // 动态为expand类型绑定属性 expand.Name = "Learning Hard"; expand.Age = 24; // 动态为expand类型绑定方法 expand.Addmethod = (Func<int, int>)(x => x + 1); // 访问expand类型的属性和方法 Console.WriteLine("expand类型的姓名为:"+expand.Name+" 年龄为: "+expand.Age); Console.WriteLine("调用expand类型的动态绑定的方法:" +expand.Addmethod(5)); Console.Read(); } } }
运行的结果和预期的一样,运行结果为:
(二)使用DynamicObject来实现动态行为
static void Main(string[] args) { dynamic dynamicobj = new DynamicType(); dynamicobj.CallMethod(); dynamicobj.Name = "Learning Hard"; dynamicobj.Age = "24"; Console.Read(); } class DynamicType : DynamicObject { // 重写方法, // TryXXX方法表示对对象的动态调用 public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { Console.WriteLine(binder.Name +" 方法正在被调用"); result = null; return true; } public override bool TrySetMember(SetMemberBinder binder, object value) { Console.WriteLine(binder.Name + " 属性被设置," + "设置的值为: " + value); return true; } }
运行结果为:
(三) 实现IDynamicMetaObjectProvider接口来实现动态行为
由于Dynamic类型在运行时来动态创建对象的,所以对该类型的每个成员的访问都会调用GetMetaObject方法来获得动态对象,然后通过这个动态对象来进行调用,所以实现IDynamicMetaObjectProvider接口,需要实现一个GetMetaObject方法来返回DynamicMetaObject对象,演示代码如下:
static void Main(string[] args) { dynamic dynamicobj2 = new DynamicType2(); dynamicobj2.Call(); Console.Read(); } public class DynamicType2 : IDynamicMetaObjectProvider { public DynamicMetaObject GetMetaObject(Expression parameter) { Console.WriteLine("开始获得元数据......"); return new Metadynamic(parameter,this); } } // 自定义Metadynamic类 public class Metadynamic : DynamicMetaObject { internal Metadynamic(Expression expression, DynamicType2 value) : base(expression, BindingRestrictions.Empty, value) { } // 重写响应成员调用方法 public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args) { // 获得真正的对象 DynamicType2 target = (DynamicType2)base.Value; Expression self = Expression.Convert(base.Expression, typeof(DynamicType2)); var restrictions = BindingRestrictions.GetInstanceRestriction(self, target); // 输出绑定方法名 Console.WriteLine(binder.Name + " 方法被调用了"); return new DynamicMetaObject(self, restrictions); } }
运行结果为: