惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
Hugging Face - Blog
Hugging Face - Blog
L
LINUX DO - 热门话题
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
D
Docker
C
Cyber Attacks, Cyber Crime and Cyber Security
MyScale Blog
MyScale Blog
P
Palo Alto Networks Blog
T
Tenable Blog
P
Privacy International News Feed
Google DeepMind News
Google DeepMind News
小众软件
小众软件
Cisco Talos Blog
Cisco Talos Blog
aimingoo的专栏
aimingoo的专栏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
Arctic Wolf
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
T
Threat Research - Cisco Blogs
NISL@THU
NISL@THU
The Hacker News
The Hacker News
Project Zero
Project Zero
AWS News Blog
AWS News Blog
Simon Willison's Weblog
Simon Willison's Weblog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threatpost
V
Visual Studio Blog
The GitHub Blog
The GitHub Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
Jina AI
Jina AI
Cyberwarzone
Cyberwarzone
The Register - Security
The Register - Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Vercel News
Vercel News
D
Darknet – Hacking Tools, Hacker News & Cyber Security
MongoDB | Blog
MongoDB | Blog
U
Unit 42
Scott Helme
Scott Helme
A
About on SuperTechFans
WordPress大学
WordPress大学
F
Fortinet All Blogs
大猫的无限游戏
大猫的无限游戏
G
GRAHAM CLULEY
Latest news
Latest news
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Schneier on Security

SamHou's Blog

总感觉有什么不对——Anemoi 全线点评 Hexo 博客接入 Fediverse —— Hatsu + Vercel 踩坑记 水星冲浪日志 3 —— Fediverse、Arch Linux 和写作风格 DroidSpaces 在安卓上跑 Linux 发行版踩坑实录 从零开始配置 VPS —— 主机名、用户组和远程权限安全实践 奶奶都能看懂的 C++ —— 类、初始化、预处理和分离式编译 让你验证请求来自 CF —— Authenticated Origin Pulls 攻略 水星冲浪日志 2 —— PT、换域名、约稿和建站机 奶奶都能看懂的 C++ —— 函数指针、decltype、类型别名和尾置返回
奶奶都能看懂的 C# —— LINQ、 Lambda 和 IEnumerable
Sam Hou · 2026-02-04 · via SamHou's Blog

上一篇,我们讲了 LINQ 的入门知识,了解了方法和声明式查询这两种形式,但是对于将声明式查询的一部分,转换为方法,还没有深入了解。

这一篇,我们将从 Lambda 表达式开始,一步一步带你走进 LINQ 方法的世界,最终自己实现 IEnumerable!

让我们开始吧。

在阅读 C# 代码的时候,你一定时常碰见这个 =>

比如:

1
public override string ToString() => "String";

上面的例子非常直观,你一定能猜到它是怎么工作的:让这个方法的返回值为 "String"

=> 这个小符号,定义的就是一个 Lambda 表达式。它声明了一个匿名函数(没有名字的方法),我们可以把它的语法总结为:

1
(参数类型 参数1, 参数类型 参数2 ...) => 表达式;

括号内部的,是匿名函数的参数列表;表达式表示的值,就是这个匿名函数的返回值

也就是说,我们可以把很多单个 return 的方法,借助 lambda 表达式,直接简化为一行!

1
2
3
4
5
static class AwesomeClass
{
public static string Shout(string s) => "OHHHHHHHHHHH THIS IS SIMPLE";
public static bool IsOverHundred(int num) => num > 100;
}

第一个不再赘述,第二个的含义是,接受 num,然后计算 => 后面的表达式,返回表达式的值(即,检测 num 是否大于100)

等等。你一定在思考一个问题:刚才提到说,lambda 表达式创建的是匿名方法,但是上面的示例代码里面,不还是有名字吗??这个匿名,到底是什么意思呢?

欸,实际上,lambda 表达式定义的是一段函数逻辑,本身确实是没有名字的。

但是,它可以用在任何需要一个函数体的地方,包括上面的 ToStringShout。正是因为你把这个匿名方法用到了一个有名字的类成员身上,所以才看起来有名字。

也就是说,上面代码中,涉及 lambda 和匿名函数的,只有:

1
2
3
4
() => "String";
(string s) => "OHHHHHHHHHHH THIS IS SIMPLE";
// 也就是……
(输入) => 输出;

并不包含括号前面的那个名字。

那么问题来了,这和我们的 LINQ 又有什么关系呢?

LINQ 中的 Lambda

入门:过滤 Where

让我们先回忆上一篇的一段声明式查询语句:

1
2
3
4
var result = from num in numbers
where num > 500
orderby num
select num + 1;

我们已经提到,声明式查询可以改写为方法串链。既然是直接在原来的集合上面调用,那么方法串链的形式不需要 from;那么,现在来试试输入 Where() 吧:

智能提示

来了点奇怪的东西!IDE 告诉我们,Where 这个方法,需要一个 Func<int,bool> 作为参数……?

点进去看一下(反编译):

1
public delegate TResult Func<in T, out TResult>(T arg);

delegate!这是一个委托
关于委托,这并不在本文的讨论范围内。先用 LINQ 和 Lambda 的思维看看吧。

你可以简单理解为,这里需要一个方法,以 T 作为参数,TResult 作为结果。就这么个简单的 LINQ 查询,你肯定不想独立写一个方法出来——因此,是时候让匿名函数出手了:

1
numbers.Where(num => num > 500)

Func<int,bool> 表示参数类型 int,返回值类型 bool。上面的查询中需要一种逻辑

“如果通过参数传入的 int 满足某些条件,那么返回 true 保留;否则,返回 false 丢掉。”

因此我们写了下面这样的 lambda 表达式,让这个匿名函数对于参数大于 500 的情况返回 True,否则返回 False。

1
num => num > 500

我知道你一定又有问题了。

明明我们的 lambda 表达式应该是这样的:

1
(参数类型 参数) => 表达式;

为什么括号没了,甚至参数类型也没了!??

这是因为——如果这个 lambda 作为一个参数 (Func<T,T>) 传入另一个方法,那么这个匿名函数的参数返回值类型,是确定已知的,因此,没有必要去写参数的类型。

而又由于只有一个参数名称,所以括号是不必要的,可以删除,最终得到这样的简化式子:

1
参数 => 表达式

要提醒的是,千万别忘了,lambda 的参数和方法一样,是要自己命名的,这里省略的是参数类型,不是参数名字本身!

因此,当你看到 Func<T,T> 类型的参数的时候,请明白,这里需要一个处理逻辑,也就是一个 lambda 表达式

排序 Orderby

声明式查询的第二个子句,是 orderby。

1
orderby num

现在我们在刚才的 Where 后面,串接上 orderby:

1
2
numbers.Where((int num) => num > 500)
.OrderBy(num=>num);

来自己试试吧!把鼠标悬浮在 orderby 上面,你会发现它需要的是 Func<int,TKey> 类型的一个参数。

TKey 是啥?看看注释:

1
... in ascending order according to a key.

哦!我们知道了,我们需要一种逻辑,传入一个列表元素,返回一个可以排序的东西(叫做 key)。然后 orderby 会根据这个 key,排序原始传入的列表元素。

在我们的情况下,由于数据源是 int 类型列表,所以传入的对象是 int。

仔细想想之后,会发现——我们就是要根据这个 int 类型的列表元素本身,来进行排序!

所以根本就不需要进行任何处理,直接返回 num 本身不就好了?

所以……

1
num => num

我知道这个看起来多此一举,但是总不能什么都不填……这个 lambda 的含义是,接收到参数命名为 num,按照原样返回 num!

实际开发中肯定不是排序数字列表这么简单,再给你举一个不那么“多此一举”的例子(继续拿上一篇中的 User 类):

1
2
3
4
5
6
7
8
9
10
enum Status
{
Offline,
Online
}
class User
{
public int Id { get; set; }
public Status Status { get; set; }
}

如果有个 User 列表,就能这么写:

1
users.OrderBy(user=>user.Id)

这里的含义是,把 users 列表,按照其每个 User 类实例(命名为 user)的 Id 排序。

我们把其等价声明式查询拿出来——

1
from user in users orderby user.Id select user

比对一下吧!把每个部分这么一对应,是不是声明式查询和方法查询都变得十分清晰了呢?

分组 Group

还记得上一篇文章里面介绍的分组吗?现在也来介绍一下吧!

我们先拿出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
User[] users = new User[]
{
new User { Id = 1, Status = Status.Online },
new User { Id = 2, Status = Status.Online },
new User { Id = 3, Status = Status.Offline },
new User { Id = 4, Status = Status.Online },
};
var result=from user in users orderby user.Id
group user by user.Status into userGroup
select userGroup;
foreach (var group in result)
{
Console.WriteLine("Group " + group.Key);
foreach (var item in group)
{
Console.WriteLine("User ID #" + item.Id
+ " Status: " + item.Status);
}
}

先自己尝试一下吧!先按照上面的讲解写出 OrderBy(),然后输入 GroupBy(),把鼠标放上去查看它需要接受一个怎样的参数。

答案是:

1
2
var result = users.OrderBy(user => user.Id)
.GroupBy(user => user.Status);

写出来了吗?

现在来拆解一下:

GroupBy 接受一个 Func<User,TKey> 参数,含义是输入一个 User,根据 lambda 中匿名函数返回的 TKey 类型的数据来分组。相同的 TKey 类型分到一组。因此,我们这里是根据 user.Status 进行分组,TKey 这个泛型就变成了 Status enum。

好了,不啰嗦了。Group 和 Orderby 实在非常相似,自己对照一下,相信你一定可以明白。

合并 Join

上一篇,我们写了这样一个用户留言板程序,生成了匿名类型:

1
2
3
4
5
6
7
8
9
10
11
12
class User // 这个类没有改
{
public int Id { get; set; }
public Status Status { get; set; }
}
class Message // 表示用户发送的一条信息
{
// 发送者 ID
public int SenderId { get; set; }
// 发送内容
public string Text { get; set; } = "";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
User[] users = new User[] // 不变
{
new User { Id = 1, Status = Status.Online },
new User { Id = 2, Status = Status.Online },
new User { Id = 3, Status = Status.Offline },
new User { Id = 4, Status = Status.Online },
};
Message[] messages = new Message[] // 来构造一个示例消息列表
{
new Message {SenderId= 1,Text="I love this."},
new Message {SenderId= 2,Text="No wayyyyy we can leave message" },
new Message{SenderId=3,Text="OMG this is crazy"},
new Message{SenderId=3,Text="Great work!"},
new Message{SenderId=4,Text="Can I delete my message???"}
};
var result = from message in messages
join user in users
on message.SenderId equals user.Id
select new
{
SenderId = message.SenderId,
Text = message.Text,
UserStatus = user.Status,
};
foreach (var item in result)
{
Console.WriteLine("Message [" + item.Text +
"] from user #" + item.SenderId +
" whose status is " + item.UserStatus);
}

现在我们来看这个 Join——来试试输入吧!

1
messages.Join( // 试着打这些

IDE 里面智能提示太长了放不下!我们去 Microsoft Learn 查一下这个方法(阅读文档是一种非常好的学习方式)。

Correlates the elements of two sequences based on matching keys.

由于这是一个扩展方法(此处不展开),所以带着 this 的参数直接忽略,它表示 .Join 前面的那个对象。

还剩下 4 个参数:

1
2
3
4
System.Collections.Generic.IEnumerable<TInner> inner,
Func<TOuter,TKey> outerKeySelector,
Func<TInner,TKey> innerKeySelector,
Func<TOuter,TInner,TResult> resultSelector

Inner: The sequence to join to the first sequence.

也就是说,第一个参数是需要拼接到目标对象的序列。我们的情境下,是 users 序列。

下面的两个参数是:

A function to extract the join key from each element of the first sequence.
A function to extract the join key from each element of the second sequence.

哦~理解了,就是给两个序列,分别写两个 lambda 表达式,返回两个属性,会判断它们是否匹配!也就是:

1
on message.SenderId equals user.Id

那么,我们写出这样的 lambda 表达式就行了:

1
2
message=>message.SenderId
user=>user.Id

最后当然就是 select 啦,我们就在这里创建新的匿名类型:

A function to create a result element from two matching elements.

注意了!由于我们有两个序列,所以需要 2 个参数的匿名方法。两个参数就不可以省略括号了。

1
2
3
4
5
6
(message, user) => new
{
SenderId = user.Id,
Text = message.Text,
UserStatus = user.Status,
}

完美!现在让我们展示一下最终的结果。

1
2
3
4
5
6
7
var result = messages.Join(users, message => message.SenderId,
user => user.Id, (message, user) => new
{
SenderId = user.Id,
Text = message.Text,
UserStatus = user.Status,
});

你觉得哪种,声明式,还是直接写方法比较舒服呢?其实这取决于你自己——写出来的代码只要清晰易懂即可。

实现 IEnumerable

手写实现接口

对于 LINQ,现在你已经有相当深入的了解了。但是,你还记得我在上一篇开头的地方,给你留的小剧透吗?

在本文的后半,会介绍怎么自己实现这个接口,不过现在我们先记住这个前提,然后来用一下 LINQ 感受一下吧!

对于数据查询,你已经几乎离不开 LINQ 了不是吗?

因此,你当然希望你的数据——我是指,你自己创建的类,也应该能够实现 LINQ 查询对吧。

现在让我们来探索这个接口。

比如,我们来创建一个自定义的用户列表 UserList 类型!

键入下面的 class 语句:

1
class UserList : IEnumerable<User>

现在在波浪线的地方按下 alt + enter,选中“实现接口”。

1
2
3
4
5
6
7
8
9
10
11
class UserList : IEnumerable<User>
{
public IEnumerator<User> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

现在你知道这个接口必须实现这两个方法。那么问题来了,IDE 生成的这个 IEnumerator<User> 接口(更通用地,是 IEnumerator<T>),又是什么???

从名字上理解,这个叫做枚举器,也就是把一个集合里面的东西一个个枚举出来的方法。

如果你看过《奶奶都能看懂的 C++ —— vector 与迭代器》这篇文章或者你是 C++ 高手,一个很好的方法就是,把枚举器看成 C++ 迭代器,但是不需要解引用。(没看过也没关系!下面会从零开始详细解释枚举器)

我们不妨来创建一个类,来让 IDE 实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UserListEnumerator : IEnumerator<User>
{
public User Current => throw new NotImplementedException();
object IEnumerator.Current => Current;
public void Dispose()
{
throw new NotImplementedException();
}
public bool MoveNext()
{
throw new NotImplementedException();
}
public void Reset()
{
throw new NotImplementedException();
}
}

WOW,出来了一堆方法!

我们来结合含义讲解一下。

  • Current、Move、Reset,看得出来,好像有什么在移动
  • 没错,这个接口表示的就是一个枚举器,会从序列开头移动到末尾
  • Current 返回枚举器当前指向的对象
  • MoveNext 把枚举器移到下一位,如果移位后 Current 指向的对象有效,那么返回 ture;如果已经越过列表末尾了,那么返回 false
  • Reset 重置枚举器
  • Dispose 会释放枚举器使用的资源(此处略去,不在讨论范围内)

现在让我们来实现!

但是,我们的 UserList 明明自称是一个可枚举的列表,里面却没有数据。由于这个程序只是一个演示用途,因此,我们通过构造函数传入,来初始化这个列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserList : IEnumerable<User>
{
User[] _users;
public UserList(User[] users)
{
// 传入!
_users = users;
}
public IEnumerator<User> GetEnumerator()
{
// 根据现有数据生成枚举器!
return new UserListEnumerator(_users);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

再来手动实现枚举器的逻辑——注意,第一次迭代的时候就会调用 MoveNext,因此下标从 -1 开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class UserListEnumerator : IEnumerator<User>
{
private readonly User[] _users;
int _index = -1; // 从 -1 开始!
public UserListEnumerator(User[] users)
{
_users = users;
}
public User Current => _users[_index]; // 当前的元素
object IEnumerator.Current => Current;
public void Dispose()
{
// 略去。
}
public bool MoveNext()
{
// 移到下一位
_index++;
// 检查移位之后,当前的 Current 是否有效
return _index < _users.Length;
}
public void Reset()
{
_index = -1;
}
}

确实,手动写这么一大堆东西,头都大了,有什么更加简单方便的方法来实现枚举器吗?

yield return

当然!隆重介绍 yield return!它可以全自动地生成一个枚举器,请看示例代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UserList : IEnumerable<User>
{
User[] _users;
public UserList(User[] users)
{
_users = users;
}
public IEnumerator<User> GetEnumerator()
{
for(int i = 0; i < _users.Length; i++)
{
yield return _users[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

你可能已经发现,有一个类全部消失了!这就是 yield return,它可以全自动生成一个 IEnumerator<User>

但是等等

这到底是什么原理?我枚举这个列表的时候,代码究竟在怎么执行?为什么 yield return 可以返回一个枚举器?

其实,yield return 就是两句话:

  • 枚举时,yield return 返回当前枚举的元素,然后保存这个执行状态
  • 到下一次迭代时,从上一次 yield return 处,继续执行。

不妨来做个实验吧!我们把 yield return 上下改成这样:

1
2
3
4
5
6
for(int i = 0; i < _users.Length; i++)
{
Console.WriteLine("Start Yield return #"+(i+1));
yield return _users[i];
Console.WriteLine("End Yield return #"+(i+1));
}

然后写一个 foreach 方法(没错,实现了 IEnumerable 就能用 foreach 了)

1
2
3
4
5
UserList userList=new(users);
foreach (User user in userList)
{
Console.WriteLine("Getting user #" + user.Id);
}

试试看吧!

1
2
3
4
5
6
7
8
9
10
11
12
Start Yield return #1
Getting user #1
End Yield return #1
Start Yield return #2
Getting user #2
End Yield return #2
Start Yield return #3
Getting user #3
End Yield return #3
Start Yield return #4
Getting user #4
End Yield return #4

看到了吗?枚举的时候发生了这样的事情:

  • 获取第一个元素,执行到第一次 yield return,中断
  • 获取第二个元素,从上次中断处继续执行,直到遇到下一次 yield return
  • ……

因此,返回的不是一个东西,而是一组:yield return 允许你创建一个全自动管理的枚举器,不需要手写,会根据需要执行——每一次 yield return,都会吐出一个元素,都是一次中断与恢复的过程。

结语

对于 LINQ,我们已经进行了非常深入的讲解,还涉及到了 lambda 表达式,以及 yield return 的核心原理。希望你有所收获!