《深入理解C#》整理3-委託

一、向笨拙的委託語法說拜拜

在C# 1中,我們一般是先寫好一連串事件處理程序,然後到處寫new EventHandler,這顯得很多餘、很凌亂,因爲事件本身已經指定了它要使用哪個委託類型。有時,我們寫的方法是如此簡單,以至於它們的實現比簽名都要短。而這一切只是由於委託需要以方法的形式來執行代碼。這樣一來,在創建委託實例的代碼和調用委託實例時應該執行的代碼之間,就“多繞了一道彎子”。

二、方法組轉換

在C# 1中,如果要創建一個委託實例,就必須同時指定委託類型和要執行的操作。C# 2支持從方法組到一個兼容委託類型的隱式轉換(方法組就是一個方法名,它可以選擇添加一個目標)。例如:Thread t =new Thread(new ThreadStart(MyMethod))可以簡化爲Thread t=new Thread(MyThread)

三、協變性和逆變性

在C#4之前的版本,委託這一部分是包含協變性和逆變性的。在靜態類型的情況下,如果能調用一個方法,並且在能調用一個特定委託類型的實例並使用其返回值的任何地方都能使用該方法的返回值,就可以使用該方法來創建該委託類型的一個實例。(個人小結:協變修飾返回值,指某個返回類型可以由其父類替換;逆變修飾傳入的參數,值該參數類型可以由其父類替換)

1、委託參數的逆變性

根據約定,事件處理方法的簽名應包含兩個參數。第1個參數是object類型,代表事件的來源;第2個參數則負責攜帶與事件有關的任何額外信息,它的類型派生自EventArgs。在提供對逆變性的支持之後,你可以使用一個具有EventHandler簽名的方法,作爲符合約定的所有委託類型的操作。

2、委託返回類型的協變性

示例:

image-20201022221045607

①聲明委託類型的返回類型是Stream;②GenerateRandomData方法的返回類型是MemoryStream;③利用返回類型的協變性來允許GenerateSampleData用於StreamFactory;④調用委託實例時,編譯器已經不知道返回的是一個MemoryStream——如果將stream變量的類型變成MemoryStream,會報告一個編譯錯誤

3、不兼容的風險

C# 2的這種新的靈活性會使本來有效的C# 1代碼在C# 2編譯時產生不同的結果。假設一個派生類重載了某個基類中聲明的方法,我們打算使用方法組轉換創建一個委託的實例。由於C# 2中的協變性和逆變性,一個以前只和基類方法匹配的轉換,現在也和派生類方法相匹配。在這種情況下,編譯器將選擇派生類方法。

四、匿名方法中的捕獲變量

匿名方法允許你指定一個內聯委託實例的操作,作爲創建委託實例表達式的一部分。匿名方法還以閉包(closure)的形式提供了一些更加強大的行爲。

相關定義:

  • 閉包:一個函數除了能通過提供給它的參數交互之外,還能同環境進行更大程度的互動
  • 外部變量:作用域內包括匿名方法的局部變量或參數(不包括ref和out參數),在類的實例成員內部的匿名方法中,this引用也被認爲是一個外部變量
  • 捕獲的外部變量:通常簡稱爲捕獲變量,它是在匿名方法內部使用的外部變量

示例:

image-20201024095717856

1、捕獲變量的行爲與作用

被匿名方法捕捉到的確實是變量,而不是創建委託實例時該變量的值。簡單地說,捕獲變量能簡化避免專門創建一些類來存儲一個委託需要處理的信息。

2、捕獲變量的延長生存期

對於一個捕獲變量,只要還有任何委託實例在引用它,它就會一直存在。假設存在一個捕獲變量X,那麼編譯器將額外創建一個類來容納捕獲變量X,委託所在的類及委託自身都擁有對該類的一個實例的引用,這個實例和其他實例一樣都在堆上。除非委託準備好被垃圾回收,否則那個實例是不會被回收的。綜上:局部變量並非始終是“局部”的,即使在方法返回之後,它依然存在!

3、局部變量實例化

每聲明一次局部變量,它就被實例化一次。而當一個變量被捕獲時,捕捉的是變量的“實例”

示例:

image-20201024110118342

4、共享和非共享的變量混合使用

示例:

image-20201024114309533

首先考慮一下outside變量。聲明該變量的作用域只進入了一次①,所以很簡單——它只有一個。inside變量則不同——每次循環迭代,都會實例化一個新的inside變量。這意味着當我們創建委託實例時,outside變量將由兩個委託實例共享,但每個委託實例都有它們自己的inside變量。循環結束後,我們創建的第1個委託實例被調用了3次。由於它每次都要對捕獲到的變量進行遞增,而且每個變量的初始值都是0,所以會看到先輸出的是(0,0),然後是(1,1),再是(2,2)。執行第2個委託實例時,兩個變量在作用域上的區別就變得非常明顯了。在第2個委託實例中,有1個不同的inside變量,所以它的初始值仍爲0,但共享的outside變量已經遞增了3次。第2個委託實例被調用兩次,所以,輸出的先是(3,0),然後是(4,1)。編譯器是如何實現的?可參照下圖的簡單說明:

image-20201024114425666

5、捕獲變量的使用規則

使用捕獲變量時,請參照以下規則:

  • 如果用或不用捕獲變量時的代碼同樣簡單,那就不要用。
  • 捕獲由for或foreach語句聲明的變量之前,思考你的委託是否需要在循環迭代結束之後延續,以及是否想讓它看到那個變量的後續值。如果不是,就在循環內另建一個變量,用來複制你想要的值。(在C# 5中,你不必擔心foreach語句,但仍需小心for語句。)
  • 如果創建多個委託實例(不管是在循環內,還是顯式地創建),而且捕獲了變量,思考一下是否希望它們捕捉同一個變量。
  • 如果捕捉的變量不會發生改變(不管是在匿名方法中,還是在包圍着匿名方法的外層方法主體中),就不需要有這麼多擔心。
  • 如果你創建的委託實例永遠不從方法中“逃脫”,換言之,它們永遠不會存儲到別的地方,不會返回,也不會用於啓動線程——那麼事情就會簡單得多。
  • 從垃圾回收的角度,思考任何捕獲變量被延長的生存期。這方面的問題一般都不大,但假如捕獲的對象會產生昂貴的內存開銷,問題就會凸現出來。

6、小結

  • 捕獲的是變量,而不是創建委託實例時它的值
  • 捕獲的變量的生存期被延長了,至少和捕捉它的委託一樣長
  • 多個委託可以捕獲同一個變量;但在循環內部,同一個變量聲明實際上會引用不同的變量“實例”
  • 在for循環的聲明中創建的變量僅在循環持續期間有效——不會在每次循環迭代時都實例化。這一情況對於C# 5之前的foreach語句也適用
  • 必要時創建額外的類型來保存捕獲變量
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章