如何用LINQ遍历列表并按条件将元素分流至两个输出?
问题
使用LINQ时,我们可以通过Where方法遍历列表并筛选符合条件的元素,但如果需要对列表中符合条件的元素做一种处理,不符合条件的做另一种处理,并且希望避免两次调用Where导致遍历数组两次,该怎么实现?
我设想的理想API使用方式如下:
var myList = new [] { 1,2,3,4,5 }; var result = myList .MyAwesomeSpecialWhereMethod(i => i % 2 == 0) // 返回包含成功列表和失败列表的对象 .SelectSuccess(i => $"{i} is even") // 将成功列表转换为字符串列表 .SelectFailure(i => $"{i} is odd") // 将失败列表转换为字符串列表 .Collect((success, failure) => success.Union(failure)) // 通过Lambda聚合两个列表 Console.WriteLine(string.Join(", ", result));
运行结果为:
2 is even, 4 is even, 1 is odd, 3 is odd, 5 is odd // 未做排序
这种场景很常见,我可以用循环实现:
var evens = new List<int>(); var odds = new List<int>(); foreach (var i in myList){ if (i%2 == 0){ evens.Add(i); } else { odds.Add(i); } } // 后续再分别处理两个列表并聚合
但我想知道有没有更具LINQ风格的实现方式。
解答
方法1:使用GroupBy一次性分组处理
这是最简洁的原生LINQ实现,仅遍历一次集合,通过条件分组后分别处理:
var myList = new[] { 1, 2, 3, 4, 5 }; var grouped = myList.GroupBy(i => i % 2 == 0); var evenStrings = grouped.First(g => g.Key).Select(i => $"{i} is even"); var oddStrings = grouped.First(g => !g.Key).Select(i => $"{i} is odd"); var result = evenStrings.Concat(oddStrings); Console.WriteLine(string.Join(", ", result));
如果担心集合全是偶数/奇数导致找不到对应键,可改用FirstOrDefault并做空值处理,避免抛出异常。
方法2:自定义扩展方法实现流畅API
如果想要贴合你设想的链式调用风格,可以封装一组扩展方法,核心是先一次性拆分出符合/不符合条件的两个集合,再提供后续转换和聚合能力:
首先定义存储拆分结果的中间类:
public class SplitResult<T> { public IEnumerable<T> Success { get; } public IEnumerable<T> Failure { get; } public SplitResult(IEnumerable<T> success, IEnumerable<T> failure) { Success = success; Failure = failure; } }
再实现扩展方法:
public static class SplitExtensions { // 核心拆分方法,仅遍历一次集合 public static SplitResult<T> Split<T>(this IEnumerable<T> source, Func<T, bool> predicate) { var success = new List<T>(); var failure = new List<T>(); foreach (var item in source) { if (predicate(item)) success.Add(item); else failure.Add(item); } return new SplitResult<T>(success, failure); } // 转换成功集合 public static SplitResult<TResult> SelectSuccess<T, TResult>(this SplitResult<T> splitResult, Func<T, TResult> selector) { return new SplitResult<TResult>(splitResult.Success.Select(selector), splitResult.Failure.Cast<TResult>()); } // 转换失败集合 public static SplitResult<TResult> SelectFailure<T, TResult>(this SplitResult<T> splitResult, Func<T, TResult> selector) { return new SplitResult<TResult>(splitResult.Success.Cast<TResult>(), splitResult.Failure.Select(selector)); } // 聚合两个集合 public static TCollect Collect<T, TCollect>(this SplitResult<T> splitResult, Func<IEnumerable<T>, IEnumerable<T>, TCollect> collector) { return collector(splitResult.Success, splitResult.Failure); } }
使用时就和你设想的逻辑完全一致:
var myList = new[] { 1, 2, 3, 4, 5 }; var result = myList .Split(i => i % 2 == 0) .SelectSuccess(i => $"{i} is even") .SelectFailure(i => $"{i} is odd") .Collect((success, failure) => success.Concat(failure)); Console.WriteLine(string.Join(", ", result));
方法3:使用Aggregate一次性聚合拆分
也可以用Aggregate实现单次遍历,直接在聚合过程中完成拆分和元素转换:
var myList = new[] { 1, 2, 3, 4, 5 }; var result = myList.Aggregate( new { Evens = new List<string>(), Odds = new List<string>() }, (acc, item) => { if (item % 2 == 0) acc.Evens.Add($"{item} is even"); else acc.Odds.Add($"{item} is odd"); return acc; }, acc => acc.Evens.Concat(acc.Odds) ); Console.WriteLine(string.Join(", ", result));
这种方式无需额外定义类,通过匿名类型在单次遍历中完成所有逻辑,代码紧凑直接。
内容的提问来源于stack exchange,提问作者ewok
相关产品推荐
相关产品推荐

