[C#基础知识系列]专题十七:深入理解动态类型

时间:2022-09-21 22:56:55

本专题概要:

  • 动态类型介绍
  • 为什么需要动态类型
  • 动态类型的使用
  • 动态类型背后的故事
  • 动态类型的约束
  • 实现动态行为
  • 总结

引言:

  终于迎来了我们C# 4中特性了,C# 4主要有两方面的改善——Com 互操作性的改进和动态类型的引入,然而COM互操作性这里就不详细介绍的,对于.Net 互操作性我将会在另外一个专题中详细和大家分享下我所了解到的知识,本专题就和大家分享C# 4中的动态类型,对于动态类型,我刚听到这个名词的时候会有这些疑问的——动态类型到底是什么的呢? 知道动态类型大概是个什么的时候,肯定又会有这样的疑问——C# 4中为什么要引入动态类型的?(肯定引入之后可以完成我们之前不能做的事情了,肯定是有好处的),下面就具体介绍了动态类型有哪些内容的。

一、动态类型介绍

  提到动态类型当然就要说下静态类型了,对于什么是静态类型呢? 大家都知道之前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# 为什么好端端的引入动态类型增加程序员的负担呢? 事实并不是这样的,下面就介绍了动态类型到底有什么用,它并不是所谓给程序员带来负担,一定程度上讲是福音

 2.1 使用动态类型可以减少强制类型转换

  从第一部分的补充也可以看到,使用动态类型不需要类型转换是因为编译器根本在编译时的过程知道什么类型,既然不知道是什么类型,怎么判断该类型是否能进行什么操作,所以也就不会出现类似“运算符“+”无法应用于“object”和“int”类型的操作数“或者”不存在int类型到某某类型的隐式转换“的编译时错误了,可能这点用户,开发人员可能并不觉得多好的,因为动态类型没有智能提示的功能。 但是动态类型减少了强制类型转换的代码之后,可读性还是会有所增强。(这里又涉及到个人取舍问题的, 如果自己觉得那种方式方便就用那种的,没必要一定要用动态类型,主要是看那种方式可以让自己和其他开发人员更好理解)

2.2 使用动态类型可以使C#静态语言中调用Python等动态语言

  对于这点,可能朋友有个疑问,为什么要在C#中使用Python这样的动态语言呢? 对于这个疑问,就和在C#中通过P/Invoke与本地代码交互,以及与COM互操作的道理一样,假设我们要实现的功能在C#类库中没有,然而在Python中存在时,此时我们就可以直接调用Python中存在的功能了。

 三、动态类型的使用

前面两部分和大家介绍动态类型的一些基础知识的,了解完基础知识之后,大家肯定很迫不及待地想知道如何使用动态类型的,下面给出两个例子来演示动态类型的使用的。

3.1 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();
3.2 在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# 为什么可以使用动态类型呢?C#编译器到底在背后为我们动态类型做了些什么事情的呢? 对于这些问题,答案就是DLR(Dynamic Language Runtime,动态语言运行时),DLR使得C#中可以调用动态语言以及使用dynamic的动态类型。提到DLR时,可能大家会想到.Net Framework中的CLR(公共语言运行时),然而DLR 与CLR到底是什么关系呢?下面就看看.Net 4中的组件结构图,相信大家看完之后就会明白两者之间的区别:

[C#基础知识系列]专题十七:深入理解动态类型

从图中可以看出,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中有一个缓存的概念):
[C#基础知识系列]专题十七:深入理解动态类型

五、动态类型的约束

相信通过前面几部分的介绍大家已经对动态类型有了一定的了解的,尤其是第四部分的介绍之后,大家应该对于动态类型的执行过程也有了一个清晰的认识了,然而有些函数时不能通过动态绑定来进行调用的,这里就涉及到类型类型的约束:

5.1 不能用动态类型作为参数调用扩展方法

不能用动态类型作为参数来调用扩展方法的原因是——调用点知道编译器所知道的静态类型,但是它不知道调用所在的源文件在哪里,以及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);

5.2 委托与动态类型不能隐式转换的限制

如果需要将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;

5.3 动态类型不能调用构造函数和静态方法的限制——即不能对动态类型调用构造函数或静态方法,因为此时编译器无法指定具体的类型。

  dynamic s = new dynamic();

5.4 类型声明和泛型类型参数

不能声明一个基类为dynamic的类型,也不能将dynamic用于类型参数的约束,或作为类型所实现的接口的一部分,下面看一些具体的例子来加深概念的理解:

// 基类不能为dynamic 类型
class DynamicBaseType : dynamic
{
}
// dynamic类型不能为类型参数的约束
class DynamicTypeConstrain<T> where T : dynamic
{
}
// 不能作为所实现接口的一部分
class DynamicInterface : IEnumerable<dynamic>
{
}

六、实现动态的行为

 介绍了这么动态类型,是不是大家都迫不及待地想知道如果让自己的类型具有动态的行为呢? 然而实现动态行为有三种方式:

  • 使用ExpandObject
  • 使用DynamicObject
  • 实现IDynamicMetaObjectProvider接口.

下面就从最简单的方式:

6.1 使用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();
}
}
}

运行的结果和预期的一样,运行结果为:
[C#基础知识系列]专题十七:深入理解动态类型

6.2 使用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;
}
}

运行结果为:
[C#基础知识系列]专题十七:深入理解动态类型

6.3 实现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);
}
}

运行结果为:
[C#基础知识系列]专题十七:深入理解动态类型

七、总结

  讲到这里动态类型的介绍就已经介绍完了,本专题差不多涵盖了动态类型中所有内容,希望通过本专题大家能够对C# 4.0中提出来的动态类型特性可以有进一步的了解,并且本专题也是这个系列中的最后一篇文章了,到这里C#基础知识系列也就结束了,后面我会整理出这个系列文章的一个索引,从而方便大家收藏,然而C#4中对COM互操作性也有很大的改善,关于互操作的内容将会在后面一个系列文章中和大家分享下我的学习体会。