《隨筆十九》——C#中的 “ 委託 、 Lambda 表示式”
目錄
什麼是委託
● 委託是一種儲存函式引用的型別, 委託的宣告類似於函式,不帶函式體, 使用關鍵字delegate 做前輟。 後面才是 返回型別、函式名、引數表。
當我們定義了委託之後, 就可以宣告該委託的變數,接著把這個變數初始化為與委託具有相同返回型別 和引數列表的函式。之後,我們就可以使用該委託變數呼叫這些函式,就像該變數是一個函式一樣。
namespace HelloWorld_Console { delegate void MyDel(int value); //宣告委託型別 class Program { void PrintLow(int value) { WriteLine($"PrintLow 函式的值為:{value}"); } void PrintHigh(int value) { WriteLine($"PrintHigh 函式的值為:{value}"); } static void Main(string[] args) { Program myProgram = new Program(); Random rand = new Random(); //建立隨機整數生成器物件 int randomValue = rand.Next(99); //初始化委託變數,建立委託物件 MyDel del = randomValue < 50 ? new MyDel(myProgram.PrintLow) : new MyDel(myProgram.PrintHigh); del(randomValue); //呼叫委託 ReadKey(); } } }
在建立了委託物件之後,並且初始化委託變數, 只有在程式執行時才能確定該委託變數執行的到底是哪些函式。
在宣告委託型別時,不需要在類內宣告, 因為它是型別宣告,所以我們一般在類外宣告委託。
使用委託的步驟
● 委託也是一種自定義的型別,可以通過以下步驟使用委託:
- 宣告一個委託型別。 類似於函式,關鍵字delegate 做前輟,沒有函式體。
- 使用委託型別宣告一個委託變數。
- 建立委託型別的物件,把它賦值賦值給委託變數。新的委託物件包括指向某個方法的引用, 這個方法和第一步定義的簽名和返回型別一致。
- 還可以選擇為委託物件增加其它方法。 這些方法必須與第一步定義的委託型別有相同的函式原型。
- 最後你就可以像呼叫其他方法一樣呼叫委託。 在呼叫委託的時候, 其包含的,每一個匹配的方法都會被執行。
● 我們可以把委託看做一個包含有序方法列表的物件, 這些方法具有相同的函式原型:
- 方法的列表稱為呼叫列表。
- 委託儲存的方法可以是來自任何類或結構, 只要它們的函式原型相同。
- 呼叫列表中的方法可以是例項方法和靜態方法。
- 在呼叫委託的時候,會執行其呼叫列表中的所有跟委託匹配的方法。
- 使用關鍵字new 為委託分配記憶體, 此時建立的委託物件會把該方法放入委託的呼叫列表。(242頁)
給委託賦值
● 由於委託是引用型別,我們可以通過給它賦值來改變包含在委託變數中的引用。舊的委託物件會被垃圾回收器回收。
namespace HelloWorld_Console
{
delegate void MyDel(int value); //宣告委託型別
class Program
{
void PrintLow(int value)
{
WriteLine($"PrintLow 函式的值為:{value}");
}
void PrintHigh(int value)
{
WriteLine($"PrintHigh 函式的值為:{value}");
}
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel mdel = myProgram.PrintLow; //委託變數首先 儲存PrintLow 函式的引用
int printValue = 5;
mdel(printValue); //使用委託
mdel = myProgram.PrintHigh; //為委託賦值,該委託是一個全新的委託
mdel(printValue); //使用委託
ReadKey();
}
}
}
輸出結果為:
PrintLow 函式的值為:5
PrintHigh 函式的值為:5
組合委託
● 委託可以使用額外的運算子來 “組合 ”, 這個運算最終會建立一個新的委託, 其呼叫列表連線了作為運算元的兩個委託的呼叫列表的副本。
namespace HelloWorld_Console
{
delegate void MyDel(int value); //宣告委託型別
class Program
{
void PrintLow(int value)
{
WriteLine($"PrintLow 函式的值為:{value}");
}
void PrintHigh(int value)
{
WriteLine($"PrintHigh 函式的值為:{value}");
}
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel delA = myProgram.PrintLow;
MyDel delB = myProgram.PrintHigh;
MyDel delC = delA + delB; //組合呼叫列表,現在delC的呼叫列表有兩個方法
Random rand = new Random(); //建立隨機整數生成器物件
int randomValue = rand.Next(99);
//初始化委託變數,建立委託物件
delC = randomValue < 50 ? new MyDel(myProgram.PrintLow) : new MyDel(myProgram.PrintHigh);
delC(randomValue); //呼叫委託
ReadKey();
}
}
}
注意: 委託是恆定的, 委託物件被建立後不能再被改變。
為委託新增方法
● 為委託新增方法使用 += 運算子。 如下程式碼為委託添加了兩個方法, 新增的方法會到呼叫列表中的底部。 在呼叫列表中的順序是你依次新增的順序。
namespace HelloWorld_Console
{
delegate void MyDel(int value); //宣告委託型別
class Program
{
void PrintLow(int value)
{
WriteLine($"PrintLow 函式的值為:{value}");
}
void PrintHigh(int value)
{
WriteLine($"PrintHigh 函式的值為:{value}");
}
void Print(int value)
{
WriteLine($"Print 函式的值為:{value}");
}
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel delVar = myProgram.PrintLow; // 建立委託變數,並且建立委託物件
delVar += myProgram.PrintHigh; // 為該委託變數新增方法。現在該委託變數的呼叫列表有三個方法
delVar += myProgram.Print;
Random rand = new Random(); //建立隨機整數生成器物件
int randomValue = rand.Next(99);
//初始化委託變數,建立委託物件
delVar = randomValue < 50 ? new MyDel(myProgram.PrintLow) : new MyDel(myProgram.PrintHigh);
delVar(randomValue); //呼叫委託
ReadKey();
}
}
}
注意: 在使用+= 運算子時, 因為委託是不可變的,所以為委託的呼叫列表添加了兩個方法後的結果其實是委託變數指向了一個新的委託。
為委託移除方法
● 使用運算子 -= 從委託刪除方法。與委託增加方法一樣,當刪除一個委託方法是,其實是建立了一個新的委託。 新的委託是舊委託的副本, 只是沒有了已經被刪除方法的引用。
namespace HelloWorld_Console
{
delegate void MyDel(int value); //宣告委託型別
class Program
{
void PrintLow(int value)
{
WriteLine($"PrintLow 函式的值為:{value}");
}
void PrintHigh(int value)
{
WriteLine($"PrintHigh 函式的值為:{value}");
}
void Print(int value)
{
WriteLine($"Print 函式的值為:{value}");
}
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel delVar = myProgram.PrintLow; // 建立委託變數,並且建立委託物件
delVar += myProgram.PrintHigh; // 為該委託變數新增方法。現在該委託變數的呼叫列表有三個方法
delVar += myProgram.Print;
delVar-= myProgram.Print; //為委託刪除一個方法, 現在委託變數的呼叫列表有兩個方法
Random rand = new Random(); //建立隨機整數生成器物件
int randomValue = rand.Next(99);
//初始化委託變數,建立委託物件
delVar = randomValue < 50 ? new MyDel(myProgram.PrintLow) : new MyDel(myProgram.PrintHigh);
delVar(randomValue); //呼叫委託
ReadKey();
}
}
}
如下是移除委託方法時應注意的有:
- 如果在呼叫列表中該委託有多個方法, - = 運算子從列表的最後開始搜尋, 並且移除第一個與委託相匹配(指的是原型匹配)的方法。
- 試圖刪除委託中不存在的方法沒有任何作用。
- 試圖呼叫空委託(說白了就是該委託變數沒有初始化為一個委託物件)會丟擲異常。 我們可以通過把委託和null 進行比較來判斷委託的呼叫列表是否為空。 如果呼叫列表為空, 則委託是null。
呼叫委託
● 可以像呼叫方法一樣簡單地呼叫委託。 用於呼叫委託的引數將會用於呼叫呼叫列表中的每一個跟該委託原型匹配的方法(除非有輸出引數)。
namespace HelloWorld_Console
{
delegate void PrintFunction(); //宣告委託型別
class Program
{
public void Print1()
{
WriteLine("呼叫 Print1 函式.");
}
public static void Print2()
{
WriteLine("呼叫 Print2 函式.");
}
static void Main(string[] args)
{
Program myProgram = new Program();
PrintFunction pf = myProgram.Print1; //例項化並且初始化該委託
//給委託新增3個另外的方法
pf += Program.Print2;
pf += myProgram.Print1;
pf += Print2;
// 現在,委託含有4個方法
if (pf != null)
{
pf();
}
else
WriteLine("該委託是空的!");
ReadKey();
}
}
}
輸出結果為:
呼叫 Print1 函式.
呼叫 Print2 函式.
呼叫 Print1 函式.
呼叫 Print2 函式.
注意 :如果一個方法在呼叫列表中出現多次,當委託被呼叫時, 每次在列表中遇到這個方法時它都會被呼叫一次。
注意: 如果初始化委託變數的 函式是靜態的, 那麼必須是 “ 類名. 成員函式名 ” 或者 直接在賦值運算子後面寫 某個函式名 。 如上面的 函式 Print2();
呼叫帶返回值的委託
● 如果委託有返回值並且在呼叫列表中有一個以上的方法,會發生下面的情況:
- 呼叫列表中最後一個方法返回的值就是委託呼叫返回的值。那麼輸出的值就是最後一個返回值的值。
- 呼叫列表中所有其它方法的返回值都會被忽略。
namespace HelloWorld_Console
{
delegate int MyDel(); //宣告委託型別
class Program
{
int IntValue = 5;
public int Add2() => IntValue += 2;
public int Add3() => IntValue += 3;
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel mDel = myProgram.Add2; //建立並初始化委託
mDel += myProgram.Add3; // 為委託新增方法
mDel += myProgram.Add2;
if (mDel != null)
{
WriteLine($"輸出值為:{mDel()}"); //呼叫委託並返回值
}
else
WriteLine("該委託是空的!");
ReadKey();
}
}
}
輸出結果為:
輸出值為:12
呼叫帶引用引數的委託
● 如果委託有引用引數, 形參的值會根據呼叫列表中的一個或多個方法的返回值而改變。
意識就是說,在呼叫委託列表中的下一個方法時, 上一個方法形參的值不管是否改變,都會傳遞給呼叫列表中下一個方法, 以此類推。
namespace HelloWorld_Console
{
delegate void MyDel (ref int X); //宣告委託型別
class Program
{
public void Print1(ref int x) => x += 2;
public void Print2(ref int x) => x += 3;
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel mDel = myProgram.Print1; //建立並初始化委託
mDel += myProgram.Print2; // 為委託新增方法
mDel += myProgram.Print1;
int x = 5;
mDel(ref x);
if (mDel!=null)
{
WriteLine($"輸出委託最後的返回值:{x}");
}
ReadKey();
}
}
}
輸出結果為:
輸出委託最後的返回值:12
匿名方法
● 我們可以使用靜態方法或例項方法來初始化委託並且建立委託物件。對於這種情況, 這些方法本身可以被程式碼的其他部分顯式呼叫。不過,這個部分也必須是某個類或結構的成員。
然而,如果方法只會被使用一次——用來初始化委託會怎麼樣呢?在這種情況下,除了建立委託的語法需要,沒有必要建立獨立的具名方法。匿名方法允許我們避免使用獨立的具名方法。
說明 : 匿名方法是在初始化委託時內聯( inline )宣告的方法。
使用匿名方法
● 我們可以在如下地方使用匿名方法:
- 宣告委託變數時作為初始化表示式
- 組合委託時在賦值語句的右邊
- 為委託新增事件時在賦值語句的右邊。
匿名方法的語法
● 使用匿名方法的語法為:
delete (Paramters){ // Implementation code}
其中 Paramters 是引數列表, 如果沒有任何想要使用的引數,可以省略。
{ } 裡是語句塊, 它包含了匿名方法的程式碼。
下面的程式碼顯式初始化委託:
namespace HelloWorld_Console
{
delegate int MyDel ( int X); //宣告委託型別
class Program
{
public int Print1(int x) => x+20 ;
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel mDel = myProgram. Print1; //建立並初始化委託
WriteLine($"輸出值為:{mDel(5)}");
ReadKey();
}
}
}
輸出值為:25
下面的程式碼隱式初始化委託:
namespace HelloWorld_Console
{
delegate int MyDel ( int X); //宣告委託型別, 該型別返回一個int
class Program
{
//public int Print1(int x) => x+20 ;
static void Main(string[] args)
{
Program myProgram = new Program();
MyDel mDel = delegate (int x)
{
return x + 20; // 匿名方法的實現程式碼本身的返回值型別 必須跟委託的返回型別相同,它們都是int
};
WriteLine($"輸出值為:{mDel(20)}");
ReadKey();
}
}
}
輸出結果為: 40
注意: 匿名方法不會顯式宣告返回型別, 然而, 匿名方法的實現程式碼本身的 返回值型別 必須跟委託的返回型別 相同。 如果委託是void 型別的返回值, 匿名方法就沒有返回值。
注意: 除了陣列引數,匿名方法的引數列表必須跟委託的 : 引數數量、引數型別、順序、修飾符都要一樣。 說白了就是它們的原型要一樣。
注意: 可以使用 圓括號為空 或者 省略圓括號來 簡化 匿名方法的引數列表, 但是必須滿足以下兩個條件:
- 委託的引數列表不包含任何 out 引數。
- 匿名方法不使用任何引數。
注意: 如果委託宣告的引數列表包含了 params 引數, 那麼匿名方法的引數列表 將忽略 params 關鍵字的前輟。
關於 params 關鍵字的 詳細內容,點選 這裡 檢視。
Lambda 表示式
● 我們可以使用下列步驟來把 匿名方法 轉換為 Lambda 表示式:
- 首先刪除 delegate 關鍵字
- 在形參列表和匿名方法的函式體之間方 Lambda 運算子 =>
如下程式碼演示了這種轉換。第一行演示了將匿名方法賦值給變數mDel。第二行演示了同樣的 ,匿名方法在被轉換成Lambda表示式之後,賦值給了變數del:
MyDel mDel = delegate (int x) { return x + 20; }; // 匿名方法
MyDel del = (int x) => { return x + 20; }; // Lambda 表示式
● 不過我們還可以更簡潔,編譯器可以通過推斷從委託的宣告中知道委託形引數型別, 因此 Lambad 表示式允許我們省略委託形引數型別。 如 le2 的賦值程式碼所示。
記住:
- 帶有委託形引數型別的列表稱為顯式型別
- 省略委託形引數型別的列表稱為隱式型別
MyDel de1 = delegate (int x) { return x + 1; }; // 匿名方法
// 都是 Lambda 表示式
MyDel le1 = (int x) => { return x + 1; };
MyDel le2= (x) => { return x + 1; };
MyDel le3= x => { return x + 1; };
MyDel le4 = x => x + 1;
- 注意 : 如果只有一個隱式型別引數, 我們可以省略周圍的圓括號, 如 le3 的賦值程式碼所示。
- 注意; Lambda 表示式允許 表示式的主體是 語句塊或者表示式。 如果語句塊包含一個返回語句, 我們可以將語句塊替換為 return 關鍵字後的表示式, 如 le4 的賦值程式碼所示 。
namespace HelloWorld_Console
{
delegate int MyDel(int par); //宣告委託型別
class Program
{
static void Main(string[] args)
{
MyDel de1 = delegate (int x) { return x + 1; }; // 匿名方法
// 都是 Lambda 表示式
MyDel le1 = (int x) => { return x + 1; };
MyDel le2 = (x) => { return x + 1; };
MyDel le3 = x => { return x + 1; };
MyDel le4 = x => x + 1;
WriteLine($"分別輸出值為:{de1(20)},{le1(20)},{le2(20)}" +
$",{le3(20)},{le4(20)}");
ReadKey();
}
}
}
輸出結果為:
分別輸出值為:21,21,21,21,21
● 有關Lambda 表示式的引數列表的要點如下:
- Lambda 表示式引數列表中的引數必須在 引數個數、型別、順序與委託匹配。
- 表示式的引數列表中的引數不一定需要 包含型別(隱式型別), 如果委託有ref或out引數——此時必須註明顯式型別.
- 如果只有一個引數,並且是隱式型別的, 周圍的圓括號可以被省略,否則必須有括號。
- 如果沒有引數, 必須使用一組空的圓括號。