无限次重复一个可枚举对象

14

是否有可枚举的扩展方法可以无限地重复枚举?

例如,给定一个返回值为 ["a", "b", "c"] 的可枚举对象。我想要一个可以返回无限重复序列 ["a", "b", "c", "a", "b", "c", "a", "b", "c" ... ] 的方法。

这听起来有点像 Observable.Repeat,但我想在 IEnumerables 上操作。

Enumerable.Repeat 只能从单个元素生成可枚举对象。


为什么不在自定义类中实现 IEnumerable 呢? - Emil Lundberg
@EmilLundberg 那将是一种非常冗长的做法。 - Dave Hillier
6个回答

23

我不知道LINQ内置了什么,但是创建自己的方法非常简单:really

public static IEnumerable<T> RepeatIndefinitely<T>(this IEnumerable<T> source)
{
    while (true)
    {
        foreach (var item in source)
        {
            yield return item;
        }
    }
}

请注意,这会多次评估source - 你可能希望只评估一次并创建一个副本:

public static IEnumerable<T> RepeatIndefinitely<T>(this IEnumerable<T> source)
{
    var list = source.ToList();
    while (true)
    {
        foreach (var item in list)
        {
            yield return item;
        }
    }
}

注意:

  • 复制序列意味着原始序列可以自由修改,而不必担心此代码同时迭代它。
  • 复制序列意味着它需要足够小以适合内存,当然,这可能不理想。
  • 只有在开始迭代结果时才会创建副本。这可能会令人惊讶。替代方法是拥有一个非迭代器方法来创建一个副本,然后委托给一个私有的迭代器方法。这是LINQ中用于参数验证的方法。
  • 副本是浅层复制——如果源是一系列StringBuilder引用,那么对对象本身所做的任何更改仍将可见。

在使用.ToList创建另一个集合之前,尝试将其转换为集合会更好吗?但是我必须承认,我对ToList的原因并没有完全理解。也许你可以稍微详细解释一下这个。 - Tim Schmelter
1
@TimSchmelter:好的,ToList总是会创建一个副本,这意味着在其他代码修改原始集合时,迭代结果肯定不会出现任何问题。我没有太多时间在这个答案中详细解释,但我会添加一些说明性文字。 - Jon Skeet
你可以始终使用可选的 bool,例如 createCopy,以获得两全其美的效果。 - Wai Ha Lee

6

你不能使用Repeat + SelectMany吗?

var take100ABC = Enumerable.Repeat(new[] { "A", "B", "C" }, 100)
                           .SelectMany(col => col);

在我看来,扩展方法只有在需要频繁使用时才有用。我怀疑你并不经常需要一个 RepeatIndefinitely。但是一个 RepeatWhile 在许多情况下都很方便。你也可以用它进行无限重复。

所以这是我的第一次尝试:

public static IEnumerable<TSource> RepeatWhile<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    TSource item = default(TSource);
    do
    {
        foreach (TSource current in source)
        {
            item = current;
            yield return item;
        }
    }
    while (predicate(item));
    yield break;
}

你可以像这样使用它来实现“无限”重复:

您可以使用它进行“无限”重复,例如:

string[] collection = { "A", "B", "C"};
var infiniteCollection = collection.RepeatWhile(s => s == s);
List<string> take1000OfInfinite = infiniteCollection.Take(1000).ToList();

2
我喜欢它,但是int.MaxValue < 无穷大 - Dave Hillier
@DaveHillier:就我所知,我已经编辑了我的答案,提供了一个更可重用的RepeatWhile,也可以(滥用)用于您的要求。 - Tim Schmelter

4

如果您可以使用System.Interactive (aka Ix)的NuGet包,则另一种选择是使用Repeat()

var sequence = Enumerable.Range(1, 3).Repeat();

foreach (var item in sequence.Take(10))
{
    Console.WriteLine(item); // 1, 2, 3, 1, 2, 3, 1, 2, 3, 1
}

据推测,自原问题提出以来的四年间,此功能已被添加。 - Dave Hillier

2
您可以创建一个简单的 RepeatForever 扩展并在序列上使用它,然后对序列的序列进行SelectMany 操作以展开它。
public static IEnumerable<T> RepeatForever<T>(this T item)
{
    for(;;) yield return item;
}
public static IEnumerable<T> RepeatSequenceForever<T>(this IEnumerable<T> seq)
{
    return seq.RepeatForever().SelectMany(x => x);
}

1
那怎么处理序列?(基本上,它不符合OP的标准。) - Jon Skeet

0

如果您不介意增加一个依赖项,您可以使用MoreLinq中的Repeat方法。

它正好做你需要的事情。 例如:

    var sequence = new[] {"a", "b", "c"};
    var generator = sequence.Repeat();
    
    var boundSequence = Enumerable.Range(0,10).Zip(generator);
    
    foreach (var (i,val) in boundSequence)
    {
        Console.WriteLine($"{i}: {val}");
    }

输出这个:

    0: a
    1: b
    2: c
    3: a
    4: b
    5: c
    6: a
    7: b
    8: c
    9: a

-1
与被接受的答案相同的解决方案,但我想避免在空枚举或具有耗尽其副作用的情况下发生致命旋转。
public static IEnumerable<T> RepeatIndefinitely<T>(this IEnumerable<T> source) {
    var t = source.GetEnumerator();
    while (t.MoveNext()) {
        do {
            yield return t.Current;
        } while (t.MoveNext());
        t = source.GetEnumerator();
    }
    throw new InvalidOperationException("Sequence contains no elements, possibly after reset.");
}

Reset方法是为了COM互操作而提供的。它不一定需要被实现;相反,实现者可以简单地抛出NotSupportedException异常。 引用 - Theodor Zoulias
感谢您的评论,很高兴进行重构...这样做可以解决问题吗?您读了附加答案的原因,对吧?我认为这对使用已接受答案的人很有帮助。 - shannon
如果它解决了缺陷,请考虑修改您的反对票。 - shannon
现在情况好多了,但我仍然不喜欢枚举器没有被处理。一定要处理你的枚举器。它们实现IDisposable是有原因的。 - Theodor Zoulias

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