iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
佛心分享-IT 人自學之術

30 天從 Python 轉職場 C# 新手入門系列 第 18

Day18-Lambda 表達式與匿名函數

  • 分享至 

  • xImage
  •  

前言

在前一天學習 LINQ 時,其實可能會注意到有兩種寫法:

  1. 查詢語法(from ... in ... select ...)
  2. 方法語法(.Where(n => n > 3))
    其中方法語法中出現的 n => n > 3 就是 Lambda 運算式。Lambda 是一種簡潔的「匿名函式」寫法,常用來搭配 LINQ、委派 (Delegate)、事件 (Event)。

Lambda 的基本語法

可以使用 Lambda 運算式 來建立匿名函式,Lambda 使用運算子 =>,用來分隔參數清單與主體 (body)。Lambda 運算式可以有兩種形式:

  1. 運算式 Lambda (Expression lambda)主體是一個單一運算式:
(input-parameters) => expression
  1. 陳述式 Lambda (Statement lambda)主體是一個陳述式區塊:
(input-parameters) => { <sequence-of-statements> }

要建立一個 Lambda 運算式:在 => 左邊指定輸入參數(若有的話)、在右邊則是運算式或陳述式區塊。


Lambda 與委派 (Delegates)

任何 Lambda 運算式都可以被轉換為 委派型別 (delegate type),Lambda 的參數型別與回傳值型別,會決定它可以轉換為哪種委派:

  • 若 Lambda 沒有回傳值,它可以轉換成 Action 委派型別。例如:Action<T1,T2> 代表有兩個參數且沒有回傳值。
  • 若 Lambda 有回傳值,它可以轉換成 Func 委派型別。例如:Func<T, TResult> 代表有一個參數且有回傳值。

例如:

Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
// 輸出:25

Lambda 也可以被轉換為 運算式樹 (Expression Tree):

System.Linq.Expressions.Expression<Func<int, int>> e = x => x * x;
Console.WriteLine(e);
// 輸出:x => (x * x)

Lambda 的使用場合

Lambda 運算式可以用在任何需要委派實例或運算式樹的地方,例如:背景工作:當作 Task.Run(Action) 的參數,將要在背景執行的程式碼傳入。LINQ:當作 LINQ 查詢的參數,來進行資料篩選或轉換。

int[] numbers = { 2, 3, 4, 5 };
var squaredNumbers = numbers.Select(x => x * x);
Console.WriteLine(string.Join(" ", squaredNumbers));
// 輸出:4 9 16 25

在 LINQ to Objects 或 LINQ to XML 中,呼叫 Enumerable.Select 方法時,參數型別是 Func<T,TResult>。而在 LINQ to SQL 中,呼叫 Queryable.Select 方法時,參數型別則是 Expression<Func<TSource,TResult>>。雖然底層使用的型別不同,但兩者都可以使用相同的 Lambda 運算式,因此它們的程式碼看起來非常相似。


在此段要來詳細說明上方 Lambda 的基本語法所說到的運算式Lambda以及陳述式的Lambda。

運算式 Lambda (Expression lambdas)

在 => 運算子的右側若是單一運算式,就稱為「運算式 Lambda」。運算式 Lambda 會回傳該運算式的結果,其基本形式為:

(input-parameters) => expression

運算式 Lambda 的主體也可以是方法呼叫。但如果你要建立由 查詢提供者 (query provider) 評估的運算式樹 (expression tree),就必須限制只能呼叫查詢提供者能夠轉換的方法。不同的查詢提供者有不同的支援能力,例如:SQL 型別的查詢提供者通常可以把 String.StartsWith 轉換成 SQL 的 LIKE 運算式。如果查詢提供者無法辨識某個方法呼叫,它就無法轉換或執行該運算式。

陳述式 Lambda (Statement lambdas)

陳述式 Lambda 與運算式 Lambda 類似,不同之處在於主體用大括號 { } 包住多個陳述式:

(input-parameters) => { <sequence-of-statements> }

陳述式 Lambda 的主體可以包含多個陳述式,但實務上通常不會超過兩三個。
範例:

Action<string> greet = name =>
{
    string greeting = $"Hello {name}!";
    Console.WriteLine(greeting);
};
greet("World");
// 輸出:Hello World!

⚠️ 陳述式 Lambda 無法用來建立運算式樹。


Lambda 的輸入參數

  • 零個參數:用空括號 () 表示
Action line = () => Console.WriteLine();
  • 一個參數:括號可省略
Func<double, double> cube = x => x * x * x;
  • 兩個以上參數:用逗號分隔
Func<int, int, bool> testForEquality = (x, y) => x == y;

而編譯器通常會自動推斷參數型別 (隱含型別參數清單),也可以明確指定型別:

Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;

可以用丟棄符號 _ 來忽略 Lambda 中未使用的參數:

Func<int, int, int> constant = (_, _) => 42;

這在事件處理器中很常見,但如果 Lambda 只有一個參數名為 _,它會被當作真正的參數名稱,而不是丟棄符號。


Lambda 的預設值與 params

從 C# 12 開始,Lambda 可以定義預設值,語法與方法或區域函式相同:

var IncrementBy = (int source, int increment = 1) => source + increment;

Console.WriteLine(IncrementBy(5));    // 6
Console.WriteLine(IncrementBy(5, 2)); // 7

Lambda 也可以使用 params 陣列或集合:

var sum = (params IEnumerable<int> values) =>
{
    int sum = 0;
    foreach (var value in values) 
        sum += value;
    
    return sum;
};

var empty = sum();
Console.WriteLine(empty); // 0

var sequence = new[] { 1, 2, 3, 4, 5 };
var total = sum(sequence);
Console.WriteLine(total); // 15

此外,若一個方法群組有預設參數或 params,指派給 Lambda 時也會保留這些參數特性。但這類 Lambda 不會自然對應到 Func<>Action<> 型別,需要自訂委派型別:

delegate int IncrementByDelegate(int source, int increment = 1);
delegate int SumDelegate(params int[] values);
delegate int SumCollectionDelegate(params IEnumerable<int> values);

或者直接用 var 讓編譯器自動合成正確的委派型別:

var add = (int x, int y = 10) => x + y;
Console.WriteLine(add(5));    // 15
Console.WriteLine(add(5, 2)); // 7

Async Lambdas(非同步 Lambda)

可以使用 async 和 await 關鍵字輕鬆建立包含非同步處理的 lambda 表達式和語句。例如下方程式碼範例, Windows 窗體範例包含一個呼叫並等待非同步方法 ExampleMethodAsync 的事件處理程序:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += button1_Click;
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await ExampleMethodAsync();
        textBox1.Text += "\r\nControl returned to Click event handler.\n";
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

可以使用非同步 lambda 新增相同的事件處理程序,若要新增此處理程序,請在 lambda 參數清單前新增 async 修飾符,如下例所示:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button1.Click += async (sender, e) =>
        {
            await ExampleMethodAsync();
            textBox1.Text += "\r\nControl returned to Click event handler.\n";
        };
    }

    private async Task ExampleMethodAsync()
    {
        // The following line simulates a task-returning asynchronous process.
        await Task.Delay(1000);
    }
}

Lambda Expressions 與 Tuples

C# 語言內建了對元組的支援,可以將元組作為參數提供給 lambda 表達式,並且 lambda 表達式也可以傳回元組。在某些情況下,C# 編譯器會使用型別推論來決定元組元素的型別。
定義元組時,需用括號括起元組的各組成部分,並以逗號分隔,以下範例使用包含三個組成部分的元組,將一個數字序列傳遞給 lambda 表達式,該表達式將每個值加倍,並傳回一個包含三個組成部分的元組,其中包含乘法的結果。

Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");
// Output:
// The set (2, 3, 4) doubled: (4, 6, 8)

通常,元組的欄位名稱為 Item1、Item2,依此類推。不過,你也可以定義一個包含命名元件的元組,如下例所示:

Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
Console.WriteLine($"The set {numbers} doubled: {doubledNumbers}");

Lambda 搭配 LINQ(標準查詢運算子)

LINQ 的許多方法(Where、Select、Count…)都需要 FuncExpression<Func> 委派作為參數。

可以先看以下例子,此案例可以看到 Func<int, bool> 的用法,其中 int 是輸入參數,bool 是回傳值。返回值始終在最後一個類型參數中指定。例如,Func<int, string, bool> 定義一個委託,它有兩個輸入參數(int 和 string),以及一個傳回型別 bool。以下 Func 委託在呼叫時傳回布林值,該值指示輸入參數是否等於 5:

Func<int, bool> equalsFive = x => x == 5;
bool result = equalsFive(4);
Console.WriteLine(result);   // False

當參數類型為 Expression<TDelegate> 時,您也可以提供 lambda 表達式,例如 Queryable 類型中定義的標準查詢運算子中。當您指定 Expression<TDelegate> 參數時,lambda 會被編譯為表達式樹。 以下範例使用了 Count 標準查詢運算子:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
int oddNumbers = numbers.Count(n => n % 2 == 1);
Console.WriteLine($"There are {oddNumbers} odd numbers in {string.Join(" ", numbers)}");

Output:
There are 5 odd numbers in 5 4 1 3 9 8 6 7 2 0

編譯器可以推斷輸入參數的類型,或者我們也可以明確指定,這個特定的 lambda 表達式會統計除以 2 後餘數為 1 的整數 (n)。 以下範例產生的序列包含 numbers 陣列中 9 之前的所有元素,因為 9 是序列中第一個不滿足條件的數字:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstNumbersLessThanSix = numbers.TakeWhile(n => n < 6);
Console.WriteLine(string.Join(" ", firstNumbersLessThanSix));
// Output:
// 5 4 1 3

以下範例透過將多個輸入參數括在括號中來指定它們。此方法傳回 numbers 陣列中的所有元素,直到找到一個值小於其在陣列中的序號位置的數字:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var firstSmallNumbers = numbers.TakeWhile((n, index) => n >= index);
Console.WriteLine(string.Join(" ", firstSmallNumbers));
// Output:
// 5 4

不能在查詢表達式中直接使用 lambda 表達式,但可以在查詢表達式中的方法呼叫中使用它們,如下例所示:

var numberSets = new List<int[]>
{
    new[] { 1, 2, 3, 4, 5 },
    new[] { 0, 0, 0 },
    new[] { 9, 8 },
    new[] { 1, 0, 1, 0, 1, 0, 1, 0 }
};

var setsWithManyPositives = 
    from numberSet in numberSets
    where numberSet.Count(n => n > 0) > 3
    select numberSet;

foreach (var numberSet in setsWithManyPositives)
{
    Console.WriteLine(string.Join(" ", numberSet));
}
// Output:
// 1 2 3 4 5
// 1 0 1 0 1 0 1 0

今天的學習主題Lambda 表達式與匿名函數內容稍微長了些,今天進度先到這邊,明天接續還沒結束的內容,參考資料在此,若順利完成進度,會進到主題:委派與事件。


上一篇
Day17- LINQ 基礎
下一篇
Day19-Lambda 表達式與匿名函數-2
系列文
30 天從 Python 轉職場 C# 新手入門22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言