首页
登录 | 注册

C#学习笔记(三)—–C#高级特性:dynamic

C#高级特性:动态绑定

动态绑定

  • 动态绑定将类型绑定(类型解析、成员和操作过程)从编译时推迟到了运行时。在编译时,如果程序员知道某个特定函数、成员的存在而编译器不知道,那么这种操作是非常有用的,这种情况通常出现在操作动态语言和COM,如果不适用动态绑定,就只能使用反射(reflection)机制。
  • 动态类型是通过dynamic关键字声明的:
dynamic d = GetSomeObject();
d.Quack();

上面的语句告诉编译器:“不要紧张,这个事情不用你管了”。我们写下这样的代码是因为我们期望程序跑起来后(在运行时)d类型确实有一个Quack成员。没有了编译器的静态帮助(编译器帮我们识别这个成员到底有没有这个方法),我们无法在写下这个代码的时候确定d到底有没有一个Quack成员。所以只有推迟到运行时去检验d。这里面涉及到一个概念,什么是静态绑定的以及什么是动态绑定的:

  • 静态绑定和动态绑定:典型的例子就是在编译表达式时将一个名称映射到一个函数上,如果要编译下面的表达式,那么编译器必须能够找到这个Quack的实现:
    d.Quack();
    假设d的静态类型是Duck:
Duck d = ...
d.Quack();

静态绑定:最简单的情形是,编译器检查Duck中无参的Quack方法进行绑定。如果绑定失败,编译器会将搜索范围扩大到具有可选参数的方法、Duck基类中的方法以及扩展方法。如果还是没有找到,编译器会产生一个错误,无论绑定的是一个什么东西,底线是这个绑定是由编译器进行的,而且绑定是完全依赖于d这个操作数。这就是所谓的静态绑定。
现在将d的静态类型改为object:

object d = ...
d.Quack();

调用Quack时,我们会遇到一个编译时错误,因为d的静态类型是object的,虽然存储在堆上的d指向的对象可能包含一个Quack,但是只有变量类型是Duck时编译器才知道Quack的存在,object类型根本”看不到“这个成员的存在。现在,将d的静态类型定义为dynamic:

dynamic d = ...
d.Quack();

dynamic关键字其实就是告诉编译器放过这次静态类型的检查,让它在运行时在检查。动态对象是基于运行时进行绑定的,而不是基于编译时,当编译器看到dynamic时,它所做的事情仅仅是对表达式进行一个打包,具体的绑定发生在运行时。
在运行时,如果一个动态对象实现了IDynamicMetaObjectProvider接口,这个接口是用来执行绑定的。否则,绑定发生的方式就像是编译器已经知道这个对象一样去执行(编译时绑定的方式)这两种方案被称作自定义绑定和语言绑定。

自定义绑定

自定义绑定发生在当一个动态对象实现了IDynamicMetaObjectProvider接口时。虽然你可以自己定义一个实现了该接口的对象,这个对象也可以这样用,但是更普通的情形是从一种在DLR上用.NET语言已经实现了的动态语言中获取这个对象,例如:IronPython or IronRuby。这些对象已经隐式的实现了IDynamicMetaObjectProvider接口。
关于这方面的讨论会在后面进行更详细的讨论,这里给出一个简单的实例进行演示:

using System;
using System.Dynamic;
public class Test
{
static void Main()
{
dynamic d = new Duck();
d.Quack(); // Quack method was called
d.Waddle(); // Waddle method was called
}
}
public class Duck : DynamicObject
{
public override bool TryInvokeMember (
InvokeMemberBinder binder, object[] args, out object result)
{
Console.WriteLine (binder.Name + " method was called");
result = null;
return true;
}
}

Duck类型实际上根本没有那两个方法,相反,它使用自定义绑定拦截并解释所有的方法调用。

语言绑定

  • 语言绑定实在一个动态对象未实现IDynamicMetaObjectProvider接口时出现的。语言绑定在处理类型设计有缺陷和对付.NET固有类型的内在限制时非常有用。使用数值类型的一个常见问题是他们没有共同的接口,我们已经知道方法是可以动态绑定的,运算符也可以进行动态绑定:
static dynamic Mean (dynamic x, dynamic y)
{
return (x + y) / 2;
}
static void Main()
{
int x = 3, y = 4;
Console.WriteLine (Mean (x, y));
}

明显的好处是不用为每个不同的值类型提供不同的实现,缺点是在运行时会发生异常以及没有静态类型检查导致的安全性的缺失。
提示:动态绑定会破坏静态类型的安全性,但不会影响运行时的类型安全性。与反射机制不同,不能通过动态绑定绕过成员访问规则的限制。
可以通过设计将语言运行时的绑定效果达到静态绑定的效果,是动态对象的运行时类型能够在编译器确定。在前一个例子中,如果我们直接在Mean中处理int类型,结果是一样的(意思是将dynamic改成int。)静态绑定和动态绑定之间最显著的差异是扩展方法,将在后面的章节中进行讨论。
提示:动态绑定也会对性能造成影响。因为DLR的缓存机制对反复调用一个动态表达式进行了优化,允许在一个循环中高效的调用。这个优化后的机制能够使一个动态表达式的处理负载降低在100ns以内。

RuntimeBinderException

如果绑定失败,运行时会抛出RuntimeBinderException异常。可以将它看作是一个运行时的编译错误。

dynamic d = 5;
d.Hello(); // throws RuntimeBinderException

这个异常的抛出是因为int类型没有Hello成员。

动态类型的运行时表现

在dynamic和object类型之间有一个深等价:运行时在遇到下面的表达式时会返回true:

这个原理可以扩展到下面的示例:

typeof (List<dynamic>) == typeof (List<object>)//true
typeof (dynamic[]) == typeof (object[])//true

与引用类型类似,动态引用可以指向出指针类型以外的任何类型:

dynamic x = "hello";
Console.WriteLine (x.GetType().Name); // String
x = 123; // No error (despite same variable)
Console.WriteLine (x.GetType().Name); // Int32

在结构上,对象引用和动态引用没有任何区别。动态引用可以直接在他所指的对象上执行动态操作。可以将object转换成dynamic,以便可以执行一个能在object上面执行的操作:

object o = new System.Text.StringBuilder();
dynamic d = o;
d.Append ("hello");
Console.WriteLine (o); // hello

定义一个public的dynamic类型的成员和带注解的object类型是一样的,比如:

public class Test
{
public dynamic Foo;
}
//等价于
public class Test
{
[System.Runtime.CompilerServices.DynamicAttribute]
public object Foo;
}

下面那个加注释的object的Foo表明它应该被当作是一个dynamic的类型来对待,如果遇到错误,则回退到object类型上进行操作。

动态转换

动态类型会对其他所有类型进行隐式转换:

int i = 7;
dynamic d = i;
long j = d; // No cast required (implicit conversion)

但是如果要保证运行时成功,必须保证动态类型的运行时类型能够兼容要转换的静态类型,上例中之所以能够转换成功,是因为int类型可以安全的转换为long。
下例中的转换会抛出异常,因为int类型不能安全的转换为short:

int i = 7;
dynamic d = i;
short j = d; // throws RuntimeBinderException

var与dynamic

var和dynamic看上去很像,但是实际上是有差别的:
var说:我的类型在编译时就是确定的。
dynamic说:我类型要等到运行时才能知晓。
举例说明:

dynamic x = "hello"; // 静态类型是dynamic,运行时类型是string
var y = "hello"; // 运行时类型和静态类型都是string
int i = x; // Runtime error在运行时才发现错误
int j = y; // Compile-time error编译时就可以检查出错误

一个由var声明的变量可以是dynamic:

dynamic x = "hello";
var y = x; // Static type of y is dynamic
int z = y; // Runtime error

动态表达式

Fields, properties, methods, events, constructors, indexers, operators, and conversions都可以是动态调用的。
不允许在返回类型是void的表达式上用dynamic捕获:这个静态的语义是一样的,不同的是后者会在运行时抛出异常而不是编译时:

dynamic list = new List<int>();
var result = list.Add (5); // RuntimeBinderException thrown at runtime

包含动态操作数的表达式本身就是动态的,since the effect of absent type information is cascading

dynamic x = 2;
var y = x * 3; // Static type of y is dynamic

但是这个规则有一个例外:首先,经一个动态的表达式转换为静态的:

dynamic x = 2;
var y = (int)x; // Static type of y is int

其次,调用构造函数总是产生静态的表达式,即使是传递了一个动态的参数:

dynamic capacity = 10;
var x = new System.Text.StringBuilder (capacity);//x被设置为静态的StringBuilder

此外,在少数情况下,包含动态参数的表达式也是静态的,包括传递动态参数到数组和用委托创建的表达式。

无动态接收者的动态调用

dynamic的标准用例是包含一个动态的接收者,这意味着一个动态对象是一个动态调用的接收者。

dynamic x = ...;
x.Foo(); // x是接收者

然而,还可以使用动态参数调用静态函数,这种调用受到动态重载解析的影响,包括以下:
①静态方法
②实例构造函数
③已知静态类型的实例接收者
在下面的例子中,Foo方法的动态绑定(到底用哪一个重载的Foo方法)取决于动态参数的运行时类型。

class Program
{
static void Foo (int x) { Console.WriteLine ("1"); }
static void Foo (string x) { Console.WriteLine ("2"); }
static void Main()
{
dynamic x = 5;
dynamic y = "watermelon";
Foo (x); // 1
Foo (y); // 2
}
}

因为其中不包括一个动态的接收者(我的理解是不是那种x.Foo来调用的),所以编译器能够执行一些静态检查来看看动态调用是否能成功。它主要检查方法名是否正确以及方法的参数数量是否符合数量。如果没有发现候选函数,那么产生一个编译时错误:

class Program
{
static void Foo (int x) { Console.WriteLine ("1"); }
static void Foo (string x) { Console.WriteLine ("2"); }
static void Main()
{
dynamic x = 5;
Foo (x, x); // Compiler error - wrong number of parameters
Fook (x); // Compiler error - no such method name
}
}

动态表达式静态类型

显然,动态类型用在动态绑定中,但是,静态类型也能用在动态绑定中。例如:

class Program
{
static void Foo (object x, object y) { Console.WriteLine ("oo"); }
static void Foo (object x, string y) { Console.WriteLine ("os"); }
static void Foo (string x, object y) { Console.WriteLine ("so"); }
static void Foo (string x, string y) { Console.WriteLine ("ss"); }
static void Main()
{
object o = "hello";
dynamic d = "goodbye";
Foo (o, d); // os
}
}

编译器会尽其所能的静态化,在这个例子 中,因为d是动态的,所以Foo (o, d);执行的是动态绑定,d执行的是动态绑定,但是,由于o是静态的,所以Foo (o, d);中的o执行的是静态的绑定。(说的有点绕,总的意思是说,Foo(o,d)执行的是动态绑定,其中o是静态绑定,d是动态绑定)。

不可调用的函数

有一些函数是不能动态调用的,例如:
①扩展方法(通过扩展方法语法)
②接口的所有成员
③子类隐藏的基类成员
理解这其中的原因对理解动态绑定是非常有用的。
动态绑定包含两部分信息:调用的函数名和调用函数的对象。然而,在上面这三种不可调用的情况中,还涉及到一个额外的类型。这个类型只能在编译时被检查到。在C#5.0中,我们是无法动态的指定这种附加类型的。
在调用扩展方法时,它的附加类型是隐式的,它是在静态类中定义的方法,编译器会根据using指令来搜索这个类,这使得扩展方法成为只适用于编译时的概念。因为using指令会在编译后消失。(当它们在绑定的过程中完成了将简单的名称映射到完整的命名空间的任务后)
当通过接口调用成员时,需要通过一个隐式或者显式的转换来指定这个附加类型。有两种情况需要执行这个操作:调用显示实现的接口成员和调用另一个程序集内部类型中实现的接口成员。下面的例子:

interface IFoo { void Test(); }
class Foo : IFoo { void IFoo.Test() {} }

要调用Test。我们必须通过一个接口类型的变量,这中情况通过静态方式实现是最简单的:

IFoo f = new Foo(); // Implicit cast to interface
f.Test();

下面是动态类型转换的例子:

IFoo f = new Foo();
dynamic d = f;
d.Test(); // Exception thrown

IFoo f = new Foo();这句话的意思是说编译器将f的成员调用绑定到IFoo上,而不是Foo上,换句话说,要通过iFoo的视角来查看对象。然而,这个视角会在运行时消失,所以DLR无法完成这个绑定过程。消失的过程如下:

类似的过程也发生在调用隐藏的基类成员上:必须通过一个强制转换或换成base关键字来指定一个附加类型,否则附加类型会在运行时消失。

总结

类型拿动态和静态来分的话,在C#中,静态类型在编译时就是确定的,会执行静态的绑定,有dynamic关键字的,就是告诉编译器,将动态类型的绑定延迟到运行时,如果一个表达式中既有静态类型也有动态类型,那么这个表达式是动态的,但C#会尽量执行静态绑定;动态绑定的意思就是说在运行时执行与运行时类型的绑定,一个表达式例如:BaseClass b=new DerivedClass():b的静态类型是基类类型,而运行时类型是子类,所谓执行动态绑定就是将运行时的类型即子类类型与b进行绑定。
dynamic写了这么多,总结一下我自己认为的,看到这篇文章的同学也可以留言给我,让我们共同进步:)!!!!



2020 jeepxie.net webmaster#jeepxie.net
10 q. 0.008 s.
京ICP备10005923号