Enumerable.Intersperse的扩展方法是什么?

11

我从Haskell学到了intersperse函数,并一直在寻找C#中的实现。

Intersperse函数接受两个参数:一个IEnumerable<T>类型的源和一个T类型的元素。它返回一个IEnumerable,其中在源的每个元素之间插入了指定元素。

可能的用例之一是在整数列表之间放置任意整数,例如:

// returns: {1, 0, 2, 0, 3}
(List<int>() {1, 2, 3}).Intersperse(0);

这是一个关于string.Join(...)的通用案例。

6个回答

15

其他人可能忽略的一点是:如果你只想在项目之间添加,而不是在前面或后面也添加,你需要进行额外的检查:

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, T element)
{
    bool first = true;
    foreach (T value in source)
    {
        if (!first) yield return element;
        yield return value;
        first = false;
    }
}

或者

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, T element)
{
    var e = source.GetEnumerator(); 
    bool b = e.MoveNext();
    if (b) yield return e.Current;

    while (e.MoveNext())
    {
        yield return element;
        yield return e.Current;
    }
}           

你的观点很有道理,但是你的回答让我感到困惑。在你的示例中,似乎插入项将首先出现,这不是我认为正确的。 - Daniel Pratt
我反正更喜欢你的代码,Marc。它在 if 块中只设置一次标志,不需要否定。 - Joel Coehoorn
@Daniel:第一次循环时根本没有输出,因此首个枚举值会在首个交错项之前返回。 - Joel Coehoorn
@Daniel Pratt - 没错,代码看起来很好。Joel和我对"item"和"value"的相反含义有所不同,但代码是正确的。 - Marc Gravell
把条件语句从循环中提出来,像我的解决方案一样,会更好吗? - Daniel
显示剩余4条评论

8
我已经编写了一个懒惰的解决方案,符合Linq解决方案的精神!我想到的其他解决方案都涉及遍历整个列表才能返回数据,然后返回结果列表。其他答案中的一些在循环的每次迭代中都有一个if检查。
public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, T element)
{
    using (var enumerator = source.GetEnumerator()) {
        if (enumerator.MoveNext()) {
            yield return enumerator.Current;
            while (enumerator.MoveNext()) {
                yield return element;
                yield return enumerator.Current;
            }
        }
    }
}

1
使用GetEnumerator()时,应该Dispose()迭代器。 - Marc Gravell
3
如果集合很大,则消除分支是一个不错的选择。顺便说一下,这是一个经典的“栅栏问题”,你需要n+1或n-1个东西。String.Join() 是 C# 程序员接触到这些问题最常见的方法。 - Chris Moschini
@ChrisMoschini 很方便有一个标签来贴在这个概念上! - anton.burger

2
很容易写成以下形式:

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, T value) {
    bool first = true;
    foreach(T item in source) {
         if(first) { first = false; }
         else { yield return value; }
         yield return item;
    }
}

0

这里有一个示例,可以更好地控制插入额外的项目:


public delegate T IntersperseFunc<T>(T prev, T next, int index);
public delegate T InterspersePrevNextFunc<T>(T prev, T next);
public delegate T IntersperseIndexFunc<out T>(int index);

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, IntersperseFunc<T> elementFunc)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    using (var enumerator = source.GetEnumerator())
    {
        if (enumerator.MoveNext())
        {
            var index = 0;
            var prev = enumerator.Current;
            yield return prev;
            while (enumerator.MoveNext())
            {
                var next = enumerator.Current;
                yield return elementFunc(prev, next, index++);
                yield return next;
                prev = next;
            }
        }
    }
}

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, InterspersePrevNextFunc<T> elementFunc)
    => Intersperse(source, (prev, next, index) => elementFunc(prev, next));

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, IntersperseIndexFunc<T> elementFunc)
    => Intersperse(source, (prev, next, index) => elementFunc(index));

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, Func<T> elementFunc)
    => Intersperse(source, (prev, next, index) => elementFunc());

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> source, T element)
    => Intersperse(source, (prev, next, index) => element);

-1

以下是其他实现方式。它们都应该具有良好的性能,无需首先对输入序列进行完整遍历。所有这些方法都可以正确处理空输入和单个项目输入。

SelectMany

我喜欢的方式是:

public static IEnumerable<T> Intersperse <T> (this IEnumerable<T> source, T delimiter) =>
    source.SelectMany((item) => Enumerable.Empty<T>().Append(delimiter).Append(item)).Skip(1);

或者,另一种选择:

public static IEnumerable<T> Intersperse <T> (this IEnumerable<T> source, T delimiter) =>
    source.SelectMany((item) => new T[] { delimiter, item }).Skip(1);

它们都是等效的:对于每个,创建一个新序列{ delimiter,item },并使用SelectMany将它们全部连接在一起。之后,跳过第一个分隔符--它只是多余的。我喜欢这些选项的唯一原因是它们可以在紧急情况下内联使用,而不必编写额外的函数。

这里有几个其他实现(请注意,我称它们为Delimit而不是Intersperse):

聚合

虽然我认为它很笨重,但可以使用Aggregate来完成:

public static IEnumerable<T> Delimit2a <T> (this IEnumerable<T> source, T delimiter) =>
    source.Aggregate(Enumerable.Empty<T>(), (delimited, item) => delimited.Append(delimiter).Append(item)).Skip(1);

public static IEnumerable<T> Delimit2b <T> (this IEnumerable<T> source, T delimiter) =>
    source.Aggregate(null as IEnumerable<T>, (delimited, item) => (delimited?.Append(delimiter) ?? Enumerable.Empty<T>()).Append(item)) ?? Enumerable.Empty<T>();

2b 可能不值得考虑:它省略了 Skip(1),但代价是很多额外的冗长和分支。

yield return

这些与其他基于 yield return 的答案类似,但处理第一个元素的方法不同(在我看来更加简洁):

public static IEnumerable<T> Delimit3a <T> (this IEnumerable<T> source, T delimiter) {
    foreach (T item in source.Take(1)) // protects agains empty source
        yield return item;
    foreach (T item in source.Skip(1)) {
        yield return delimiter;
        yield return item;
    }
}

public static IEnumerable<T> Delimit3b <T> (this IEnumerable<T> source, T delimiter) {
    static IEnumerable<U> Helper<U> (IEnumerable<U> source, U delimiter) {
        foreach (U item in source) {
            yield return delimiter;
            yield return item;
        }
    }
    return Helper(source, delimiter).Skip(1);
}

测试代码 / 示例

这里有一个可运行的示例,包含测试代码在这里。测试部分:

public static void Main () {
    foreach (int count in new int[] { 11, 2, 1, 0 }) {
        p( Enumerable.Range(10, count).Delimit1a(-1) );
        p( Enumerable.Range(10, count).Delimit1b(-1) );
        p( Enumerable.Range(10, count).Delimit2a(-1) );
        p( Enumerable.Range(10, count).Delimit2b(-1) );
        p( Enumerable.Range(10, count).Delimit3a(-1) );
        p( Enumerable.Range(10, count).Delimit3b(-1) );
    }
}

static void p <T> (IEnumerable<T> e) =>
    Console.WriteLine($"[ {string.Join(", ", e)} ]");

输出以下内容:

[ 10, -1, 11, -1, 12, -1, 13, -1, 14, -1, 15, -1, 16, -1, 17, -1, 18, -1, 19, -1, 20 ]
[ 10, -1, 11, -1, 12, -1, 13, -1, 14, -1, 15, -1, 16, -1, 17, -1, 18, -1, 19, -1, 20 ]
[ 10, -1, 11, -1, 12, -1, 13, -1, 14, -1, 15, -1, 16, -1, 17, -1, 18, -1, 19, -1, 20 ]
[ 10, -1, 11, -1, 12, -1, 13, -1, 14, -1, 15, -1, 16, -1, 17, -1, 18, -1, 19, -1, 20 ]
[ 10, -1, 11, -1, 12, -1, 13, -1, 14, -1, 15, -1, 16, -1, 17, -1, 18, -1, 19, -1, 20 ]
[ 10, -1, 11, -1, 12, -1, 13, -1, 14, -1, 15, -1, 16, -1, 17, -1, 18, -1, 19, -1, 20 ]
[ 10, -1, 11 ]
[ 10, -1, 11 ]
[ 10, -1, 11 ]
[ 10, -1, 11 ]
[ 10, -1, 11 ]
[ 10, -1, 11 ]
[ 10 ]
[ 10 ]
[ 10 ]
[ 10 ]
[ 10 ]
[ 10 ]
[  ]
[  ]
[  ]
[  ]
[  ]
[  ]

-3

如果你想知道如何实现它,我会这样做:

public static IEnumerable<T> Intersperse<T>(this IEnumerable<T> collection, T value)
{
    foreach(T item in collection)
    {
        yield return item;
        yield return value;
    }

    yield break;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接