C# 和 LINQ,一次选择两个(连续)项

本文关键字:两个 连续 选择 一次 LINQ | 更新日期: 2023-09-27 18:34:16

在有序集(数组、列表)上使用 LINQ,有没有办法选择或以其他方式使用两个连续的项目?我在想象语法:

list.SelectTwo((x, y) => ...)

其中xy是列表/数组中索引ii + 1的项目。可能没有办法做到这一点,我接受这种可能性,但我至少想说我试图找到答案。

我知道我可以使用其他东西和 LINQ 来实现这一点。

提前谢谢你。

C# 和 LINQ,一次选择两个(连续)项

另一个答案提供了一个使用 LINQ 的 SkipZip 的漂亮而干净的解决方案。

这是绝对正确的,但我想指出它枚举了两次来源。这可能重要,也可能无关紧要,具体取决于每个单独的用例。如果这对您的情况很重要,这里有一个更长的替代方案,它在功能上是等效的,但枚举源一次:

static class EnumerableUtilities
{
    public static IEnumerable<TResult> SelectTwo<TSource, TResult>(this IEnumerable<TSource> source,
                                                                   Func<TSource, TSource, TResult> selector)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (selector == null) throw new ArgumentNullException(nameof(selector));
        return SelectTwoImpl(source, selector);
    }
    private static IEnumerable<TResult> SelectTwoImpl<TSource, TResult>(this IEnumerable<TSource> source,
                                                                        Func<TSource, TSource, TResult> selector)
    {
        using (var iterator = source.GetEnumerator())
        {
            var item2 = default(TSource);
            var i = 0;
            while (iterator.MoveNext())
            {
                var item1 = item2;
                item2 = iterator.Current;
                i++;
                if (i >= 2)
                {
                    yield return selector(item1, item2);
                }
            }
        }
    }
}

例:

var seq = new[] {"A", "B", "C", "D"}.SelectTwo((a, b) => a + b);

生成的序列包含"AB""BC""CD"

System.Linq.Enumerable.Zip通过为每个i配对第 i 个元素来组合两个IEnumerable。因此,您只需要使用其移动版本Zip列表即可。

作为一种不错的扩展方法:

using System.Collections.Generic;
using System.Linq;
static class ExtMethods
{
    public static IEnumerable<TResult> SelectTwo<TSource, TResult>(this IEnumerable<TSource> source,
                                                                        Func<TSource, TSource, TResult> selector)
    {
        return Enumerable.Zip(source, source.Skip(1), selector);
    }
}

例:

Enumerable.Range(1,5).SelectTwo((a,b) => $"({a},{b})");

结果:

(1,2) (2,3) (3,4) (4,5)

你可以做

list.Skip(i).Take(2)

这将返回仅包含两个连续项的IEnumerable<T>

我认为您可以在有序列表中选择项目和下一项数据,如下所示:

var theList = new List<T>();
theList
    .Select((item, index) => new { CurrIndex = index, item.Prop1, item.Prop2, theList[index + 1].Prop1 })
    .Where(newItem => {some condition on the item});

但是,所选项目的index应小于列表大小 - 1。

可以使用特殊的 Select重载,该重载允许您使用项的索引,并使用 GroupBy 方法将列表拆分为组。每组将有两个项目。下面是执行此操作的扩展方法:

public static class ExtensionMethods
{
    public static IEnumerable<TResult> SelectTwo<TSource, TResult>(this IEnumerable<TSource> source,
        Func<TSource, TSource, TResult> selector)
    {
        return source.Select((item, index) => new {item, index})
            .GroupBy(x => x.index/2)
            .Select(g => g.Select(i => i.item).ToArray())
            .Select(x => selector(x[0], x[1]));
    }
}

你可以像这样使用它:

var list = new[] {1, 2, 3, 4, 5, 6};
var result = list.SelectTwo((x, y) => x + y).ToList();

这将返回 {3,7,11}

请注意,上述方法在开始产生结果之前对内存中的数据进行分组。如果您有大型数据集,则可能需要使用流式处理方法(在枚举来自源的数据时生成数据),下面是一个示例:

public static class ExtensionMethods
{
    public static IEnumerable<TResult> SelectTwo<TSource, TResult>(this IEnumerable<TSource> source,
        Func<TSource, TSource, TResult> selector)
    {
        bool first_item_got = false;
        TSource first_item = default(TSource);
        foreach (var item in source)
        {
            if (first_item_got)
            {
                yield return selector(first_item, item);
            }
            else
            {
                first_item = item;
            }
            first_item_got = !first_item_got;
        }
    }
}

如果源序列有一个索引器,即至少是IReadOnlyList<T>(数组,问题中提到的列表),并且想法是在连续对上拆分序列(这从问题中不太清楚),那么它可以简单地像这样完成

var pairs = Enumerable.Range(0, list.Count / 2)
    .Select(i => Tuple.Create(list[2 * i], list[2 * i + 1]));