1. 入门
想象一下有两叠信:第一叠是写着名字的信封,第二叠是要寄给这些人的明信片。你的任务是把每个信封和第二叠里同样位置的明信片配对。第一个信封配第一个明信片,第二个配第二个,以此类推。你就这样一边走一边各拿一个,然后把它们“拉链”在一起。
在编程里,这种操作叫做zip——就像拉链一样,把两边“咬”在一起,严格按位置一一对应。
在LINQ里,这就是一个很方便又强大的工具,可以把两个(有时候更多)序列的数据合并,当顺序很重要的时候:第一个和第一个,第二个和第二个,依次类推。
要记住:Zip只会合并到两个集合里都还有元素为止。如果有一个短一点,结果就会被截断到最短的那个长度。
语法和工作原理
Zip拿两个(或更多)list,把它们合成一个新的:元素是按索引配对的,也就是0和0,1和1,依次类推。如果有一个list提前结束,后面就不会再“拉链”了——结果长度就是最短的那个list。
签名(主用法):
IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
this IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
Func<TFirst, TSecond, TResult> resultSelector
)
注意:Zip返回的不只是pair,而是你在resultSelector里指定的任何东西。可以是tuple、字符串、对象,随你逻辑怎么写。
- first和second——要合并的集合。
- resultSelector——一个函数,接收两个集合当前的元素,返回你想要的新结果。
简单说:我们同时遍历两个集合,每一步把它们的pair变成你想要的东西。
2. Zip的用法例子——从简单到复杂
最简单的例子:两个数字数组相加
假设我们有两个list:
- 数学分数
- 语文分数
// 两个学生分数的数组
int[] mathScores = { 5, 4, 3, 5, 2 };
int[] literatureScores = { 4, 5, 3, 4, 3 };
// 按位置合并并相加
var totalScores = mathScores.Zip(literatureScores, (math, literature) => math + literature);
foreach (var score in totalScores)
Console.WriteLine($"学生总分: {score}");
学生总分: 9
学生总分: 9
学生总分: 6
学生总分: 9
学生总分: 5
你看,很直观:第一个学生拿到第一个分数的和,第二个拿第二个,以此类推。
字符串和数字混合:给商品标上价格
比如你有一个商品list和一个价格list。要输出:"ProductName — Price"。
string[] productNames = { "苹果", "梨", "香蕉" };
decimal[] productPrices = { 50.5m, 60.0m, 35.2m };
var info = productNames.Zip(productPrices, (name, price) => $"{name} — {price} 欧元");
foreach (var s in info)
Console.WriteLine(s);
苹果 — 50,5 欧元
梨 — 60,0 欧元
香蕉 — 35,2 欧元
和对象集合一起用
假如前面例子里我们有一个Student类和学生集合。现在有一个按顺序配对的评分数组:
public class Student
{
public string Name { get; set; }
}
List<Student> students = new List<Student>
{
new Student { Name = "瓦夏" },
new Student { Name = "彼佳" },
new Student { Name = "玛莎" }
};
int[] ratings = { 7, 9, 8 };
var studentsWithRatings = students.Zip(ratings, (student, rating) =>
$"{student.Name}: 评分 {rating}");
foreach (var s in studentsWithRatings)
Console.WriteLine(s);
瓦夏: 评分 7
彼佳: 评分 9
玛莎: 评分 8
合并更复杂的对象
现实里经常要合并,比如两个不同系统的订单list:每个订单配一个处理结果或状态。
string[] orders = { "A-1", "B-2", "C-3" };
string[] statuses = { "已完成", "处理中", "已拒绝" };
var orderStatusList = orders.Zip(statuses, (order, status) => new { Order = order, Status = status });
foreach (var os in orderStatusList)
Console.WriteLine($"订单 {os.Order}: {os.Status}");
订单 A-1: 已完成
订单 B-2: 处理中
订单 C-3: 已拒绝
4. 重要细节和常见错误
用Zip时要记住:最终集合的长度就是最短输入集合的长度。
比如你有10个学生,但只有7个分数,结果只会有7个元素。这经常导致数据“丢失”。
错误1:集合长度或顺序不一致。
如果你不小心把list顺序搞错,或者没提前排序让元素一一对应,最后的pair就会错乱。
想象一下:学生list和分数顺序不一样——瓦夏可能拿到彼佳的分数。这就像穿错袜子:虽然都穿上了,但感觉怪怪的。
错误2:有一个集合是空的。
只要有一个list是空的,结果就是空的。
Zip只会在两个序列都有元素时工作。如果有一个没数据,你就啥也得不到。
5. 多集合Zip
最早LINQ只支持合并两个集合。但从.NET 6开始,出了重载版,可以一次合并三个(哇!)集合。
三个集合的例子:
string[] names = { "蕾伊", "卢克", "莱娅" };
string[] planets = { "塔图因", "塔图因", "奥德兰" };
int[] ages = { 19, 23, 19 };
// .NET 6+有重载:
var characters = names.Zip(planets, ages, (name, planet, age) =>
$"{name}({planet}),{age}岁");
foreach (var c in characters)
Console.WriteLine(c);
蕾伊(塔图因),19岁
卢克(塔图因),23岁
莱娅(奥德兰),19岁
结果的元素数量——还是最短的那个list!如果ages少一个,最后一个卢克就“没年龄”了。
6. Zip和LINQ查询语法:有吗?
你可能注意到我们一直用“方法式”语法(Method Syntax)。LINQ查询语法里没有专门的Zip关键字,不能用from ... in ...来做。原因很简单:“拉链”是按位置配对,而LINQ查询组合通常是用join、select等,主要是按key“关联”,不是按位置。
结论:用Zip就只能用点(就是方法)语法:
var zipped = collection1.Zip(collection2, (a, b) => ...);
如果你很想用query语法——最好别折腾。如果实在想,可以自己写个扩展方法。🙂
7. Zip在实际应用里的用法
你的实战例子:对比旧价和新价
比如我们在学习项目里有个商品list,偶尔会收到新价格。要输出每个商品价格的变化:
string[] productNames = { "苹果", "香蕉", "菠萝" };
decimal[] oldPrices = { 80.0m, 30.0m, 110.0m };
decimal[] newPrices = { 75.0m, 33.0m, 120.0m };
var priceChanges = productNames
.Zip(oldPrices, newPrices, (name, oldPrice, newPrice) =>
$"{name}: 原价 {oldPrice}, 现价 {newPrice}, 变化: {newPrice - oldPrice:+#;-#;0}");
foreach (var s in priceChanges)
Console.WriteLine(s);
苹果: 原价 80, 现价 75, 变化: -5
香蕉: 原价 30, 现价 33, 变化: +3
菠萝: 原价 110, 现价 120, 变化: +10
实战:合并不同来源的结果
在“实战”项目里,Zip经常用来合并不同API或表格的结果,只要数据顺序是外部保证的(比如天气预报数组和实际观测数组)。
8. Zip在LINQ链式操作里的用法:和其他操作符组合
Zip经常不是单独用,而是LINQ长链的一部分。比如,先过滤list,再拉链,然后统计结果。
var passedMath = mathScores.Where(x => x >= 3);
var passedLit = literatureScores.Where(x => x >= 3);
var passedPairs = passedMath.Zip(passedLit, (m, l) => m + l);
var avgScore = passedPairs.Average();
Console.WriteLine($"及格学生的平均总分: {avgScore}");
小提示:如果Where后有一个list变短,Zip会按最短的来截断结果。
Zip和其他合并方法的对比
| 方法 | 本质 | 按什么合并… | 结果长度… |
|---|---|---|---|
|
按索引合并元素 | 索引(位置) | 最短集合 |
|
按key合并 | 公共key | 所有匹配的pair |
|
每个key分组为集合 | key | 按第一个集合 |
|
把集合接在一起 | 直接拼接 | 长度之和 |
Zip就是按位置一一对应。Join是按某个公共值(比如code或id)来配对。
9. 实用建议和常见错误
你在自己项目里用Zip时,最重要的是确保:
- 两个集合的顺序是一致的,否则“拼”出来的就是瓦夏和彼佳的分数乱配。
- 集合已经排序好,或者已经准备好同步遍历。
- 结果长度永远是最短集合的长度。
- 如果你要按key配对数据,Zip不适合!用Join。
- 还有一个常见错误——一个集合有重复,另一个没有。或者你在过滤时改了顺序,结果配对关系就丢了。
GO TO FULL VERSION