目 录CONTENT

文章目录

CSharp(四十一) 匿名函数-Lambda 表达式

C# 中匿名函数-Lambda 表达式

一、什么是匿名函数

匿名函数,简单说就是“没有名字的函数”。

平时我们写一个普通方法,通常会给它起名字:

static int Add(int a, int b)
{
    return a + b;
}

这个方法的名字叫 Add,以后可以通过 Add(1, 2) 来调用。

而匿名函数没有方法名,它通常是“临时写出来,马上交给某个变量、参数或事件使用”的一段代码。

例如:

Func<int, int, int> add = (a, b) => a + b;

Console.WriteLine(add(1, 2)); // 输出 3

这里的:

(a, b) => a + b

就是一个匿名函数。

它没有名字,但可以赋值给变量 add,之后通过 add(1, 2) 来调用。


二、匿名函数的两种主要写法

C# 中匿名函数主要有两种写法:

  1. 匿名方法:delegate 写法
  2. Lambda 表达式:=> 写法

现在实际开发中,最常用的是 Lambda 表达式。

1. 匿名方法:delegate 写法

匿名方法是 C# 2.0 引入的写法。

Action sayHello = delegate
{
    Console.WriteLine("你好,C#");
};

sayHello();

如果有参数,也可以这样写:

Action<string> sayHello = delegate (string name)
{
    Console.WriteLine("你好," + name);
};

sayHello("小明");

这段代码中:

delegate (string name)
{
    Console.WriteLine("你好," + name);
}

就是匿名方法。

2. Lambda 表达式:=> 写法

Lambda 表达式是 C# 3.0 引入的写法,也是现在最常见的匿名函数写法。

Action<string> sayHello = name =>
{
    Console.WriteLine("你好," + name);
};

sayHello("小明");

如果函数体只有一句表达式,还可以写得更简洁:

Func<int, int, int> add = (a, b) => a + b;

Console.WriteLine(add(10, 20)); // 输出 30

这里的 => 可以读作“变成”或“执行后得到”。

(a, b) => a + b

可以理解为:

传入 ab,返回 a + b 的结果。


三、匿名函数通常要配合委托使用

匿名函数本身不能孤零零地存在,它通常需要赋值给一种“可以表示方法的类型”。

在 C# 中,这种类型叫做委托。

常见的委托类型有:

委托类型 说明
Action 表示没有返回值的方法
Action<T> 表示有参数、没有返回值的方法
Func<TResult> 表示没有参数、有返回值的方法
Func<T, TResult> 表示有参数、有返回值的方法
Predicate<T> 表示判断条件,返回 bool

1. Action:没有返回值

Action 表示一个没有返回值的方法。

Action print = () =>
{
    Console.WriteLine("这是一段没有返回值的代码");
};

print();

如果有一个参数:

Action<string> printName = name =>
{
    Console.WriteLine("姓名:" + name);
};

printName("张三");

如果有多个参数:

Action<string, int> printInfo = (name, age) =>
{
    Console.WriteLine($"{name} 今年 {age} 岁");
};

printInfo("张三", 18);

2. Func:有返回值

Func 表示一个有返回值的方法。

Func<int> getNumber = () =>
{
    return 100;
};

Console.WriteLine(getNumber()); // 输出 100

有参数、有返回值:

Func<int, int, int> add = (a, b) =>
{
    return a + b;
};

Console.WriteLine(add(3, 5)); // 输出 8

如果函数体只有一个表达式,可以省略 {}return

Func<int, int, int> add = (a, b) => a + b;

注意:

Func<int, int, int>

前两个 int 是参数类型,最后一个 int 是返回值类型。

也就是说:

Func<参数1类型, 参数2类型, 返回值类型>

3. Predicate:返回 bool 的判断函数

Predicate<T> 常用于判断某个对象是否满足条件。

Predicate<int> isEven = number => number % 2 == 0;

Console.WriteLine(isEven(10)); // True
Console.WriteLine(isEven(11)); // False

它等价于:

Func<int, bool> isEven = number => number % 2 == 0;

四、Lambda 表达式的常见写法

1. 没有参数

没有参数时,必须写空括号 ()

Action hello = () => Console.WriteLine("Hello");

不能写成:

// 错误
Action hello = => Console.WriteLine("Hello");

2. 一个参数

一个参数时,可以省略小括号:

Action<string> print = name => Console.WriteLine(name);

也可以不省略:

Action<string> print = (name) => Console.WriteLine(name);

3. 多个参数

多个参数时,必须写小括号:

Func<int, int, int> add = (a, b) => a + b;

4. 显式写参数类型

多数时候,C# 可以自动推断参数类型:

Func<int, int, int> add = (a, b) => a + b;

也可以显式写出来:

Func<int, int, int> add = (int a, int b) => a + b;

显式写类型可以让代码更清楚,但有时会显得啰嗦。

5. 表达式 Lambda

函数体只有一个表达式时,可以写成表达式 Lambda:

Func<int, int> square = x => x * x;

这表示:

int Square(int x)
{
    return x * x;
}

6. 语句 Lambda

函数体有多行代码时,需要使用 {}

Func<int, int> square = x =>
{
    int result = x * x;
    return result;
};

如果委托有返回值,在 {} 里面通常需要写 return


五、匿名函数的常见使用场景

1. 简化临时代码

如果某段逻辑只用一次,没有必要单独写一个命名方法。

普通方法写法:

static bool IsAdult(int age)
{
    return age >= 18;
}

匿名函数写法:

Func<int, bool> isAdult = age => age >= 18;

Console.WriteLine(isAdult(20)); // True

这种写法适合短小、明确、局部使用的逻辑。

2. 用在 LINQ 查询中

匿名函数在 LINQ 中非常常见。

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };

var evenNumbers = numbers.Where(n => n % 2 == 0);

foreach (var n in evenNumbers)
{
    Console.WriteLine(n);
}

这里的:

n => n % 2 == 0

表示筛选条件:只保留偶数。

再看一个对象列表的例子:

class Student
{
    public string Name { get; set; }
    public int Score { get; set; }
}

List<Student> students = new List<Student>
{
    new Student { Name = "小明", Score = 90 },
    new Student { Name = "小红", Score = 75 },
    new Student { Name = "小刚", Score = 60 }
};

var highScoreStudents = students.Where(s => s.Score >= 80);

foreach (var student in highScoreStudents)
{
    Console.WriteLine(student.Name);
}

这里的:

s => s.Score >= 80

表示筛选出成绩大于等于 80 的学生。

3. 用在排序中

匿名函数经常用来告诉程序“按照什么规则排序”。

List<int> numbers = new List<int> { 5, 2, 8, 1 };

numbers.Sort((a, b) => a.CompareTo(b));

foreach (var n in numbers)
{
    Console.WriteLine(n);
}

从大到小排序:

numbers.Sort((a, b) => b.CompareTo(a));

4. 用在事件中

按钮点击、定时器触发等事件中,也经常使用匿名函数。

例如 WinForms 中:

button1.Click += (sender, e) =>
{
    MessageBox.Show("按钮被点击了");
};

这表示:

当按钮被点击时,执行 {} 里面的代码。

如果用普通方法写,可能是这样:

button1.Click += Button1_Click;

private void Button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("按钮被点击了");
}

匿名函数适合逻辑很短、只在这里使用的事件处理。

5. 用作回调函数

回调函数就是“把一段代码交给别人,等合适的时候再执行”。

static void DoWork(Action callback)
{
    Console.WriteLine("正在执行任务...");
    callback();
}

DoWork(() =>
{
    Console.WriteLine("任务完成后的操作");
});

这里的匿名函数:

() =>
{
    Console.WriteLine("任务完成后的操作");
}

会被传给 DoWork 方法,等任务执行完后再调用。


六、匿名函数可以访问外部变量:闭包

匿名函数可以访问它外面的局部变量,这种现象叫闭包。

int count = 0;

Action increase = () =>
{
    count++;
    Console.WriteLine(count);
};

increase(); // 1
increase(); // 2
increase(); // 3

匿名函数内部访问了外部变量 count

这很方便,但也容易出错。

闭包的重点理解

匿名函数捕获的不是变量当时的值,而是变量本身。

看下面的例子:

List<Action> actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (var action in actions)
{
    action();
}

很多初学者会以为输出:

0
1
2

但在一些场景中,可能会因为变量捕获导致结果不是自己预期的。

为了写得更稳妥,可以在循环内部创建一个临时变量:

List<Action> actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    int current = i;
    actions.Add(() => Console.WriteLine(current));
}

foreach (var action in actions)
{
    action();
}

这样每个匿名函数捕获的是各自的 current,更容易符合预期。

教学时可以这样解释:

匿名函数像是把外面的变量“记住了”,但它记住的是变量这个盒子,不一定只是盒子里当时的值。


七、匿名函数和普通方法的对比

对比项 普通方法 匿名函数
是否有名字 没有
适合场景 逻辑较复杂、需要复用 逻辑较短、临时使用
可读性 名字能表达含义 过长时可读性下降
使用位置 通常写在类中 可以直接写在变量、参数、事件中
是否能访问外部局部变量 通常不能直接访问 可以捕获外部变量

普通方法示例:

static bool IsEven(int number)
{
    return number % 2 == 0;
}

var result = numbers.Where(IsEven);

匿名函数示例:

var result = numbers.Where(number => number % 2 == 0);

如果逻辑短,用匿名函数会更简洁。

如果逻辑长、业务含义强,建议写成普通方法,并起一个清楚的方法名。


八、匿名函数的注意事项

1. 不要把匿名函数写得太长

匿名函数适合短小逻辑。

不推荐:

button1.Click += (sender, e) =>
{
    // 这里写了几十行代码
    // 又查数据库,又处理文件,又更新界面
    // 代码会越来越难读
};

更推荐:

button1.Click += (sender, e) =>
{
    SaveUserInfo();
};

或者直接:

button1.Click += Button1_Click;

原则:

匿名函数越短越好,复杂逻辑应该提取成有名字的方法。

2. 注意参数类型和返回值类型要匹配

例如:

Func<int, int> square = x => x * x;

表示传入一个 int,返回一个 int

下面这样就不匹配:

// 错误:Func<int, int> 要求返回 int,但这里返回 string
Func<int, int> wrong = x => "结果是:" + x;

如果要返回字符串,应该改成:

Func<int, string> right = x => "结果是:" + x;

3. 有返回值时,不要忘记 return

表达式 Lambda 可以自动返回结果:

Func<int, int> square = x => x * x;

但语句 Lambda 需要显式写 return

Func<int, int> square = x =>
{
    return x * x;
};

下面这样是错误的:

// 错误:有大括号时,需要 return
Func<int, int> square = x =>
{
    x * x;
};

4. 注意闭包带来的变量捕获问题

匿名函数可以访问外部变量,但要小心变量在之后发生变化。

int number = 10;

Func<int> getNumber = () => number;

number = 20;

Console.WriteLine(getNumber()); // 输出 20

很多人会以为输出 10,但实际输出 20。

原因是匿名函数捕获的是变量 number 本身,而不是创建匿名函数那一刻的值。

如果希望固定当时的值,可以复制一份:

int number = 10;
int snapshot = number;

Func<int> getNumber = () => snapshot;

number = 20;

Console.WriteLine(getNumber()); // 输出 10

5. 事件中的匿名函数不容易取消订阅

事件订阅时可以使用匿名函数:

button1.Click += (sender, e) =>
{
    Console.WriteLine("点击了按钮");
};

但是如果之后想取消订阅,会比较麻烦。

下面这种写法不能取消上面的订阅:

button1.Click -= (sender, e) =>
{
    Console.WriteLine("点击了按钮");
};

因为这两个匿名函数看起来一样,但其实是两个不同的函数对象。

如果需要取消订阅,应该先保存起来:

EventHandler handler = (sender, e) =>
{
    Console.WriteLine("点击了按钮");
};

button1.Click += handler;

// 需要取消时
button1.Click -= handler;

6. 不要滥用匿名函数影响可读性

下面代码虽然可以运行,但不适合教学和维护:

var result = students
    .Where(s => s.Score > 60 && s.Name.StartsWith("小") && s.Name.Length > 1)
    .OrderByDescending(s => s.Score)
    .Select(s => new { s.Name, Level = s.Score >= 90 ? "优秀" : "合格" });

如果规则变复杂,可以拆成有名字的方法:

static bool IsQualifiedStudent(Student student)
{
    return student.Score > 60
        && student.Name.StartsWith("小")
        && student.Name.Length > 1;
}

var result = students.Where(IsQualifiedStudent);

有名字的方法能让代码更容易读,也更方便调试。

7. 异步匿名函数要配合 async 和 await

匿名函数也可以写成异步的:

Func<Task> loadDataAsync = async () =>
{
    await Task.Delay(1000);
    Console.WriteLine("数据加载完成");
};

await loadDataAsync();

如果有参数:

Func<string, Task> downloadAsync = async url =>
{
    await Task.Delay(1000);
    Console.WriteLine("下载完成:" + url);
};

await downloadAsync("https://example.com");

注意:

  • async 匿名函数通常返回 TaskTask<T>
  • 尽量避免 async void,除非是在事件处理器中。

事件处理器中常见写法:

button1.Click += async (sender, e) =>
{
    await Task.Delay(1000);
    MessageBox.Show("操作完成");
};

8. 表达式树中的 Lambda 不等于普通委托

在一些高级场景中,Lambda 不只是代表一段可以执行的代码,还可以被转换成表达式树。

例如:

Expression<Func<int, bool>> expression = x => x > 10;

这里的 x => x > 10 不是直接拿来执行的普通函数,而是被保存成一种“代码结构”。

这个知识常见于 Entity Framework、LINQ to SQL 等框架中。

初学阶段只需要知道:

普通 Func 是可以直接执行的函数;Expression<Func<...>> 更像是把代码保存成数据,让框架分析。


九、课堂上可以这样讲

可以把匿名函数理解成:

一张临时小纸条,上面写着一段要执行的代码。
这张纸条可以交给变量、方法或事件,等需要的时候再执行。

例如:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

var result = numbers.Where(n => n > 3);

这里的:

n => n > 3

就像告诉 Where

你帮我遍历每个数字,每次把数字放到 n 里,只留下大于 3 的。

所以 Where 最后得到:

4
5

十、完整示例:用匿名函数处理学生成绩

using System;
using System.Collections.Generic;
using System.Linq;

class Student
{
    public string Name { get; set; }
    public int Score { get; set; }
}

class Program
{
    static void Main()
    {
        List<Student> students = new List<Student>
        {
            new Student { Name = "小明", Score = 95 },
            new Student { Name = "小红", Score = 82 },
            new Student { Name = "小刚", Score = 59 },
            new Student { Name = "小丽", Score = 76 }
        };

        // 筛选及格学生
        var passedStudents = students.Where(s => s.Score >= 60);

        // 按成绩从高到低排序
        var orderedStudents = passedStudents.OrderByDescending(s => s.Score);

        // 输出结果
        foreach (var student in orderedStudents)
        {
            Console.WriteLine($"{student.Name}:{student.Score}");
        }
    }
}

程序输出:

小明:95
小红:82
小丽:76

这个例子里使用了两个匿名函数:

s => s.Score >= 60

表示筛选及格学生。

s => s.Score

表示排序时使用学生成绩作为排序依据。


十一、练习题

练习 1:判断奇数

请使用 Func<int, bool> 写一个匿名函数,判断一个整数是否是奇数。

参考答案:

Func<int, bool> isOdd = n => n % 2 != 0;

Console.WriteLine(isOdd(3)); // True
Console.WriteLine(isOdd(4)); // False

练习 2:字符串长度

请使用匿名函数返回字符串的长度。

参考答案:

Func<string, int> getLength = text => text.Length;

Console.WriteLine(getLength("CSharp")); // 6

练习 3:筛选高分学生

从学生列表中筛选出成绩大于等于 90 的学生。

参考答案:

var excellentStudents = students.Where(s => s.Score >= 90);

练习 4:点击事件

使用匿名函数给按钮添加点击事件,点击后弹出提示。

参考答案:

button1.Click += (sender, e) =>
{
    MessageBox.Show("你好,匿名函数");
};

十二、总结

匿名函数是 C# 中非常重要的语法,尤其在 LINQ、事件、回调、异步编程中经常出现。

可以记住下面几句话:

  1. 匿名函数就是没有名字的函数。
  2. 它通常赋值给 ActionFuncPredicate 或传给方法参数。
  3. => 是 Lambda 表达式的核心符号。
  4. 没有返回值用 Action,有返回值用 Func
  5. 匿名函数适合短小、临时、局部使用的逻辑。
  6. 复杂逻辑应提取成普通方法,提高可读性。
  7. 匿名函数可以访问外部变量,但要注意闭包问题。
  8. 事件中使用匿名函数时,如果以后要取消订阅,应先把它保存到变量中。

一句话概括:

匿名函数让我们可以把“行为”当作数据一样传来传去,从而写出更灵活、更简洁的 C# 代码。

0
博主关闭了当前页面的评论