C# 2.0 Specification(匿名方法)(一)

<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

21匿名方法

21.1.匿名方法表達式

匿名方法表達式(anonymous-method-expression)定義了匿名方法(anonymous method),它將計算爲引用該方法的一個具體值。

l         primary-no-array-creation-expression(基本非數組創建表達式:)

anonymous-method-expression
(匿名方法表達式)

l         anonymous-method-expression:
delegate   anonymous-method-signature opt   block
(匿名方法表達式: delegate 匿名方法簽名 可選  塊)

l         anonymous-method-signature:
(   anonymous-method-parameter-list opt   )
(匿名方法簽名: 匿名方法參數列表 可選)

l         anonymous-method-parameter-list:
anonymous-method-parameter
anonymous-method-parameter-list   ,   anonymous-method-parameter
(匿名方法參數列表: 匿名方法參數 匿名方法參數列表)

l         anonymous-method-parameter:
parameter-modifieropt   type   identifier
(匿名方法參數: 參數修飾符 可選 類型 標識符)

匿名方法表達式被歸類爲具有特定轉換規則21.3)的值。

匿名方法表達式爲參數、局部變量和常數定義了一個新的聲明空間,並且爲標籤(§3.3)定義了一個新的聲明空間。

 

21.2匿名方法簽名

可選的匿名方法簽名(anonymous-method-signature)爲該匿名方法定義了正式參數的名字和類型。匿名方法的參數作用域爲塊(block)。匹配其作用域包含匿名方法表達式(anonymous-method-expression)的局部變量、局部常數或參數的名字,對於匿名方法參數的名字來說是一個編譯時錯誤。

如果一個匿名方法表達式具有匿名方法簽名,那麼與之兼容的委託類型將被限制爲那些具有相同順序21.3)相同參數類型和修飾符的委託類型集合。如果匿名方法表達式不具有匿名方法簽名,那麼與之兼容的委託類型將被限制爲那些沒有輸出參數的委託類型集合。

 

請注意,匿名方法簽名不能包含特性或者參數數組。不過,匿名方法簽名可以與其參數列表包含參數數組的委託類型兼容。


21.3匿名方法轉換

匿名方法表達式被歸類爲一個無類型的值。匿名方法表達式可以用於委託創建表達式21.3.1)中。匿名方法表達式的所有其他合法的使用取決於在此定義的隱式轉換。

隱式轉換存在來自於與任何委託兼容的匿名方法表達式。如果D是一個委託類型,而A是一個匿名方法表達式,那麼如果下面的條件滿足的話,D就與A兼容:

l         首先,D的參數類型與A兼容:

n         如果A不包含匿名方法簽名,那麼D可以有零或多個任意類型的參數,前提是D沒有任何參數具有輸出參數修飾符。

n         如果A具有匿名方法簽名,那麼D必須具有相同數量的參數,A的每個參數與D的對應參數必須具有相同的類型,並且在A上的每個參數的refout修飾符的存在與否,都必須與D的對應參數相匹配。D的最後一個參數是否是參數數組和DA的兼容性無關。

l         其次,D的返回類型必須與A兼容,對於這些規則,不考慮A包含任何其他匿名方法塊的情況。

n         如果D採用void聲明返回類型,那麼包含在A中的任何返回語句都不應該指定表達式。

n         如果D採用類型R聲明返回類型,那麼那麼包含在A中的任何返回語句的都必須指定一個可以隱式轉換(§6.1)R的表達式。並且,A的塊的結束點必須是不可達的。

除了到與之兼容的委託類型的任何隱式轉換之外,不存在匿名方法的任何其他轉換,即便是對於object類型也是如此。

下面的例子說明了這些規則:

delegate void D(int x);

D d1 = delegate { };                            // Ok
D d2 = delegate() { };                      //
錯誤,簽名不匹配

D d3 = delegate(long x) { };                //
錯誤,簽名不匹配
D d4 = delegate(int x) { };             // Ok
D d5 = delegate(int x) { return; };     // Ok
D d6 = delegate(int x) { return x; };   //
錯誤,返回類型不匹配

delegate void E(out int x);

E e1 = delegate { };                            // 錯誤e具有輸出參數
E e2 = delegate(out int x) { x = 1; };  // Ok
E e3 = delegate(ref int x) { x = 1; };  //
錯誤,簽名不匹配

delegate int P(params int[] a);

P p1 = delegate { };                            // 錯誤,塊的結束點可達

P p2 = delegate { return; };                // 錯誤,返回類型不匹配

P p3 = delegate { return 1; };          // Ok
P p4 = delegate { return "Hello"; };    //
錯誤,返回類型不匹配

P p5 = delegate(int[] a) {                  // Ok
    return a[0];
};
P p6 = delegate(params int[] a) {       //
錯誤, 具有params 修飾符
    return a[0];
};
P p7 = delegate(int[] a) {                  //
錯誤,返回類型不匹配
    if (a.Length > 0) return a[0];
    return "Hello";
};

delegate object Q(params int[] a);

Q q1 = delegate(int[] a) {                  // Ok
    if (a.Length > 0) return a[0];
    return "Hello";
};

21.3.1委託創建表達式

委託創建表達式[delegate-creation-expression (§7.5.10.3)]可被用作將匿名方法轉換到一個委託類型的替代語法。如果用作委託創建表達式的實參的表達式是一個匿名方法表達式,那麼匿名方法將使用上面定義的隱式轉換規則轉換到給定的委託類型。例如,如果D是一個委託類型,那麼表達式

new D(delegate { Console.WriteLine("hello"); })

等價於

(D) delegate { Console.WriteLine("hello"); }

 

21.4匿名方法塊

匿名方法表達式的塊遵循下列規則:

l         如果匿名方法包含簽名,那麼在簽名中指定的參數在塊內是有效的。如果匿名方法不具有簽名,它可以被轉換爲具有參數的委託類型(§21.3),但參數在塊內不可訪問。

l         除了在最接近的封閉匿名方法簽名中指定的refout參數(如果有的話)以外,對於塊來說訪問ref或者out參數將導致編譯時錯誤。

l         this的類型是一個結構類型時,對於塊來說,訪問this將導致編譯時錯誤。無論該訪問是顯式的(像this.x)或者隱式的(像對於在結構實例的成員中的x),情況都是如此。該規則只是禁止此類訪問方式,但並不影響在結構中成員查找的結果。

l         塊可以訪問匿名方法的外部變量21.5)。當匿名方法表達式被計算21.6)的時候,對於外部變量的訪問,將會引用激活的(active)變量的實例。

l         對於塊來說,包含一個其目標在塊之外,或一個內嵌的匿名方法的塊之內的goto語句、break語句或continue語句,將導致編譯時錯誤。

l         在塊內的return 語句,將從最接近的封閉匿名方法調用中返回控制權,而不是從封閉函數成員中返回。在return 語句中指定的表達式必須與某個委託類型兼容,而最接近的匿名方法表達式將被轉換到該委託類型21.3)


執行一個匿名方法的程序塊,除了通過匿名方法表達式的計算和調用(evaluation and invocation)之外,是否還有其他方法,並沒有明確地詳細說明。特別的是,編譯器可以通過合成一個或多個命名方法或類型來實現匿名方法,任何此類合成的元素的名字,必須爲編譯器的使用而保留在一個地方:名字必須保留兩個連續下劃字符。

21.5外部變量

作用域包含匿名方法表達式的任何局部變量、值參數和參數數組,都被稱爲匿名方法表達式的外部變量。在類的實例函數成員中,this值被認爲是一個值參數,它也是包含在函數成員內的任何匿名方法表達式的外部變量

 

21.5.1捕獲外部變量

當外部變量通過匿名方法而被引用時,就可以說這個外部變量被匿名方法所捕獲(captured)了。通常,局部變量的生存期被限制爲它所關聯的程序塊或語句的執行區(§5.1.7)。但被捕獲的外部變量的生存期將至少被延長,直到引用匿名方法的委託可以被垃圾回收時爲止。

示例

using System;

delegate int D();

class Test
{
    static D F() {
           int x = 0;
           D result = delegate { return ++x; }
           return result;
    }

    static void <?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />Main() {
           D d = F();
           Console.WriteLine(d());
           Console.WriteLine(d());
           Console.WriteLine(d());
    }
}

局部變量x被匿名方法所捕獲,並且x的生存期至少被延長,直到從F中返回的委託可以被垃圾回收爲止(在這裏,這一點直到程序結束才滿足),既然匿名方法的每次調用都在x的相同實例上進行操作,該示例輸出的結果爲:

1
2
3

當局部變量或值參數被匿名方法所捕獲時,該局部變量和值參數將不再被認爲是固定的(fixed)變量(§18.3),相反它成了可移動的(movable)變量。因此,任何取得被捕獲的外部變量地址的不安全代碼都必須首先使用fixed語句固定該變量。

21.5.2局部變量實例化

當程序執行到變量的作用域時,局部變量就被認爲是實例化(instantiated)了。例如,當下面的方法被調用時,局部變量將被三次實例化和初始化——對於循環中的每次迭代都有一次。

static void F() {
    for (int i = 0; i < 3; i++) {
        int x = i * 2 + 1;
        ...
    }
}

但是,如果將x的聲明移出循環之外,則對於x只會產生一次實例化。

static void F() {
    int x;
    for (int i = 0; i < 3; i++) {
        x = i * 2 + 1;
        ...
    }
}

通常,我們無法確切地看到一個局部變量多久被實例化一次——因爲實例化的生命期被拆散(disjoint)了,可能的情況是,每次實例化都只是使用相同的存儲位置。然而當一個匿名方法捕獲一個局部變量的時候,實例化的影響將變得很明顯。如示例

using System;

delegate void D();

class Test
{
    static D[] F() {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++) {
            int x = i * 2 + 1;
            result[i] = delegate { Console.WriteLine(x); };
        }
        return result;
    }

    static void Main() {
        foreach (D d in F()) d();
    }
}

產生如下輸出。

1
3
5


但如果將x的聲明移到循環之外

static D[] F() {
    D[] result = new D[3];
    int x;
    for (int i = 0; i < 3; i++) {
        x = i * 2 + 1;
        result[i] =
del
egate { Console.WriteLine(x); };
    }
    return result;
}

其輸出如下。

5
5
5

請注意在F的新版本中創建的三個委託依據相等運算符21.7)是等價的。並且,允許編譯器(但不是必須的)將三次實例化優化爲一個單一的委託實例21.6)

你可以讓匿名方法委託共享某些具有其他單獨實例的被捕獲變量。例如,如果F被改變

static D[] F() {
    D[] result = new D[3];
    int x = 0;
    for (int i = 0; i < 3; i++) {
        int y = 0;
        result[i] = delegate { Console.WriteLine("{0} {1}", ++x, ++y); };
    }
    return result;
}

這三個委託捕獲了X的同一實例,但捕獲了Y的多個單獨實例,所以輸出如下。

1 1
2 1
3 1

單獨的匿名方法可以捕獲外部變量的相同實例。例如

using System;

delegate void Setter(int value);

delegate int Getter();


 

class Test
{
  static void Main() {
      int x = 0;
      Setter s = delegate(int value) { x = value; };
      Getter g = delegate { return x; };
      s(5);
      Console.WriteLine(g());
      s(10);
      Console.WriteLine(g());
  }
}

兩個匿名方法捕獲了局部變量X的同一實例,並且它們可以通過該變量“通信”。該示例輸出如下。

5
10

21.6匿名方法計算

匿名方法表達試的運行時計算產生一個引用匿名方法的委託實例,並且被捕獲的外部變量的集合(可能爲空)在計算時(the time of the evaluation)是活躍的(active)。當由匿名方法表達式所產生的委託被調用時,匿名方法體就會執行。方法體內的代碼將使用由該委託引用而被捕獲的外部變量執行。

由匿名方法表達時產生的委託調用列表包含一個單一入口。該委託的確切目標對象和目標方法都是未指定的。需要特別的注意的是,委託的目標對象是否爲null,以及封閉函數成員的this值,或其他對象都是未指定的。

語義上相同的匿名方法的計算,如果它們帶具有相同被捕獲的外部變量集合(可能爲空),可以(但不是必須)返回相同的委託實例。術語“語義上相同”用在這裏,意思是說,該匿名方法的執行期在所有情況下,都產生給定相同實參的相同效果。這條規則允許如下的代碼優化。

delegate double Function(double x);

class Test
{
  static double[] Apply(double[] a, Function f) {
      double[] result = new double[a.Length];
      for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
      return result;
  }


 

  static void F(double[] a, double[] b) {
      a = Apply(a, delegate(double x) { return Math.Sin(x); });
      b = Apply(b, delegate(double y) { return Math.Sin(y); });
      ...
  }
}

由於兩個匿名方法委託具有被捕獲外部變量的相同集合(都爲空),並且由於匿名方法在語義上是相同的,所以允許編譯器產生引用同一目標方法的委託。實際上,這裏允許編譯器從兩個匿名方法表達式返回相同的委託實例。

(to be continued)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章