CodeGym /课程 /C# SELF /Zip按位置合并

Zip按位置合并

C# SELF
第 33 级 , 课程 4
可用

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、字符串、对象,随你逻辑怎么写。

  • firstsecond——要合并的集合。
  • 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查询组合通常是用joinselect等,主要是按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和其他合并方法的对比

方法 本质 按什么合并… 结果长度…
Zip
按索引合并元素 索引(位置) 最短集合
Join
按key合并 公共key 所有匹配的pair
GroupJoin
每个key分组为集合 key 按第一个集合
Concat
把集合接在一起 直接拼接 长度之和

Zip就是按位置一一对应。Join是按某个公共值(比如code或id)来配对。

9. 实用建议和常见错误

你在自己项目里用Zip时,最重要的是确保:

  • 两个集合的顺序是一致的,否则“拼”出来的就是瓦夏和彼佳的分数乱配。
  • 集合已经排序好,或者已经准备好同步遍历。
  • 结果长度永远是最短集合的长度。
  • 如果你要按key配对数据,Zip不适合!用Join
  • 还有一个常见错误——一个集合有重复,另一个没有。或者你在过滤时改了顺序,结果配对关系就丢了。
1
调查/小测验
集合合并第 33 级,课程 4
不可用
集合合并
进阶LINQ:合并和投影
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION