天天看點

C#學習筆記(三)—–C#進階特性:Lambda表達式

Lambda表達式

Lambda表達式是寫在委托執行個體上的匿名方法。編譯器立即将Lambda表達式轉換成下面兩種形式中的一種:

①委托執行個體

Expression<TDelegate>

類型的表達式樹,該表達式樹将Lambda表達式内的代碼顯示為可周遊的對象模型。這使得對Lambda的解釋可以延遲到運作時。

下面的委托類型:

delegate int Trasform (int i);

可以指定和調用下面的Lambda表達式:

Trasform sqr = x => x * x; Console.WriteLine (sqr(3)); // 9

提示:編譯器在内部将Lambda表達式編譯成一個私有方法,并把表達式代碼移到方法中:①首先Trasform委托被編譯為下面的形式:

C#學習筆記(三)—–C#進階特性:Lambda表達式

②lambda方法被編譯成一個私有的靜态方法:

C#學習筆記(三)—–C#進階特性:Lambda表達式
  • lambda表達式有以下形式:(參數)=>表達式或語句塊

    為了友善,在隻有一個可推測類型的參數時,可以省略小括号。

  • lambda表達式使每一個參數的類型和委托的參數的類型一緻,使表達式的傳回類型和委托的傳回類型一緻。
  • lambda表達式代碼可以是表達式也可以是語句塊:在上例中,我們也可以這樣寫:
  • lambda表達式通常可Func或Action一起使用,上例也可以這樣寫:
Func<int,int> func=x =>{ x * x;};
           
  • 下面是一個帶兩個參數的lambda表達式:
Func<string,string,int> totalLength = (s1, s2) => s1.Length + s2.Length;
int total = totalLength ("hello", "world"); // total is 10;
           
  • Lambda表達式是C#3.0引入的概念。
  • 明确指定lambda表達式的參數類型:lambda表達式可以根據代碼的上下文推斷出參數類型,當不能推斷出時,可以指定lambda的參數類型。考慮下面的代碼:
Func<int,int> sqr = x => x * x;
           

編譯器可以根據類型推斷來判斷x的類型是一個int。

我們可以上例中顯示的為x指定一個類型:

Func<int,int> sqr = (int x) => x * x;
           
  • 捕獲外部變量:Lambda表達式可以引用局部變量以及定義該lambda表達式的方法的參數:
static void Main()
{
int factor = ;
Func<int, int> multiplier = n => n * factor;
Console.WriteLine (multiplier ()); // 6
}
           
  • 上例中的factor對于lambda表達式來說,叫做外部變量(outer variable),lambda引用外部變量的過程叫做捕獲外部變量。lambda捕獲外部變量就叫做閉包(closure)。
  • 捕獲的變量在真正調用委托時指派,而不是在捕獲的時候指派:
int factor = ;
Func<int, int> multiplier = n => n * factor;
factor = ;
Console.WriteLine (multiplier ()); // 30
           
  • lambda表達式可以自動更新被捕獲的變量:
int seed = ;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
Console.WriteLine (seed); // 2
           
  • 被捕獲的變量的生命周期可以延長至捕獲它的委托的生命周期(至少),在下面的例子中,局部變量seed本應在Natural()執行完後就消失了,但是由于seed被lambda表達式捕獲了,是以它的生命周期被延長了:
static Func<int> Natural()
{
int seed = ;
return () => seed++; // Returns a closure
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
}
           

在lambda表達式内捕獲已經執行個體化過的seed,在每次調用委托執行個體時都是唯一的,如果把上例中的seed的執行個體化過程放到lambda表達式内,則産生的結果不同:

static Func<int> Natural()
{
return() => { int seed = ; return seed++; };
}
static void Main()
{
Func<int> natural = Natural();
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 0
}
           

提示:捕獲局部變量的過程在内部的實施過程是這樣的:建立一個私有類,并将被捕獲的局部變量提升為這個私有類的字段(由局部變量級别提升至字段的級别)。這樣,當方法被調用時,這個私有類被執行個體化,并将其生命周期綁定到委托的執行個體上。

  • 捕獲循環變量:當捕獲到的是一個for循環的循環變量,C#把這個循環變量當作是在for循環外部定義的。這意味着lambda在每次疊代中得到的循環變量的值都是一樣的。
Action[] actions = new Action[];
for (int i = ; i < ; i++)
actions [i] = () => Console.Write (i);
foreach (Action a in actions) a(); // 333
           

上述生成的結果是333而不是012,原因是這樣的:

①還是上面講過的,捕獲的變量是在調用委托時指派,而不是在捕獲的時候指派,這意味着在調用action a in actions這句的時候捕獲的變量才被指派,而循環變量這個時候已經被疊代到了3。

②lambda表達式會自動更新值,這意味着lambda表達式總是會尋找到循環變量的最新值。循環變量的最終的最新的值就是3。

把for循環展開的話更容易了解:

Action[] actions = new Action[];
int i = ;
actions[] = () => Console.Write (i);
i = ;
actions[] = () => Console.Write (i);
i = ;
actions[] = () => Console.Write (i);
i = ;
foreach (Action a in actions) a(); // 
           

解決的方法是,把每次循環的變量都指派非一個臨時的變量,這個臨時的變量放在lambda表達式的内部:

Action[] actions = new Action[];
for (int i = ; i < ; i++)
{
int loopScopedi = i;
actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a(); // 012
           

這樣,就可使閉包在每次循環疊代的時候捕獲一個不一樣的變量。

- 在c#5.0之前,foreach循環和for循環的工作原理是一樣的。

Action[] actions = new Action[];
int i = ;
foreach (char c in "abc")
actions [i++] = () => Console.Write (c);
foreach (Action a in actions) a(); // ccc in C# 4.0
           

這會引起一些困惑,不同于for循環,foreach循環中的循環變量在每一次循環疊代中都是不可變得,人們期望的是可以将它當作是循環體中的局部變量,好消息是這個情況在C#5.0中被修改了,上例中在C#5.0中運作的結果是abc。

需要注意的是,這在技術上是一種毀滅性的改變:因為在c#5.0和C#4.0的運作結果完全不一樣,之前在C#4.0及以前版本寫的代碼在C#5.0上面的運作結果完全不同!也可以看到,之是以做這樣的修改,很明顯這就是C#4.0的一個bug了。(要不然也不會做這樣的修改)

繼續閱讀