1. 程式人生 > >C#學習筆記(三)—–C#高階特性:dynamic

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 (dynamic) == typeof (object)

這個原理可以擴充套件到下面的示例:

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無法完成這個繫結過程。消失的過程如下:

Console.WriteLine (f.GetType().Name); // Foo

類似的過程也發生在呼叫隱藏的基類成員上:必須通過一個強制轉換或換成base關鍵字來指定一個附加型別,否則附加型別會在執行時消失。

總結

型別拿動態和靜態來分的話,在C#中,靜態型別在編譯時就是確定的,會執行靜態的繫結,有dynamic關鍵字的,就是告訴編譯器,將動態型別的繫結延遲到執行時,如果一個表示式中既有靜態型別也有動態型別,那麼這個表示式是動態的,但C#會盡量執行靜態繫結;動態繫結的意思就是說在執行時執行與執行時型別的繫結,一個表示式例如:BaseClass b=new DerivedClass():b的靜態型別是基類型別,而執行時型別是子類,所謂執行動態繫結就是將執行時的型別即子類型別與b進行繫結。
dynamic寫了這麼多,總結一下我自己認為的,看到這篇文章的同學也可以留言給我,讓我們共同進步:)!!!!