12

上下文:C# 3.0,.Net 3.5
假设我有一个生成随机数的方法(永远):

private static IEnumerable<int> RandomNumberGenerator() {
    while (true) yield return GenerateRandomNumber(0, 100);
}

我需要将这些数字分成 10 个一组,所以我想要类似的东西:

foreach (IEnumerable<int> group in RandomNumberGenerator().Slice(10)) {
    Assert.That(group.Count() == 10);
}

我已经定义了 Slice 方法,但我觉得应该已经定义了一个。这是我的 Slice 方法,仅供参考:

    private static IEnumerable<T[]> Slice<T>(IEnumerable<T> enumerable, int size) {
        var result = new List<T>(size);
        foreach (var item in enumerable) {
            result.Add(item);
            if (result.Count == size) {
                yield return result.ToArray();
                result.Clear();
            }
        }
    }

问题:有没有更简单的方法来完成我想要做的事情?也许是林克?

注意:上面的例子是一个简化,在我的程序中我有一个迭代器,它以非线性方式扫描给定的矩阵。

编辑:为什么Skip+Take不好。

实际上我想要的是:

var group1 = RandomNumberGenerator().Skip(0).Take(10);
var group2 = RandomNumberGenerator().Skip(10).Take(10);
var group3 = RandomNumberGenerator().Skip(20).Take(10);
var group4 = RandomNumberGenerator().Skip(30).Take(10);

无需重新生成 (10+20+30+40) 次的开销。我需要一个能够准确生成 40 个数字并将 4 组中的数字除以 10 的解决方案。

4

10 回答 10

12

Skip and Take对你有用吗?

在循环中使用两者的组合来获得你想要的。

所以,

list.Skip(10).Take(10);

跳过前 10 条记录,然后获取接下来的 10 条记录。

于 2010-08-19T17:26:35.393 回答
8

我做过类似的事情。但我希望它更简单:

//Remove "this" if you don't want it to be a extension method
public static IEnumerable<IList<T>> Chunks<T>(this IEnumerable<T> xs, int size)
{
    var curr = new List<T>(size);

    foreach (var x in xs)
    {
        curr.Add(x);

        if (curr.Count == size)
        {
            yield return curr;
            curr = new List<T>(size);
        }
    }
}

我认为你的有缺陷。您为所有块/切片返回相同的数组,因此只有您采用的最后一个块/切片才会具有正确的数据。

补充:数组版本:

public static IEnumerable<T[]> Chunks<T>(this IEnumerable<T> xs, int size)
{
    var curr = new T[size];

    int i = 0;

    foreach (var x in xs)
    {
        curr[i % size] = x;

        if (++i % size == 0)
        {
            yield return curr;
            curr = new T[size];
        }
    }
}

补充: Linq 版本(不是 C# 2.0)。正如所指出的,它不适用于无限序列,并且会比替代方案慢很多:

public static IEnumerable<T[]> Chunks<T>(this IEnumerable<T> xs, int size)
{
    return xs.Select((x, i) => new { x, i })
             .GroupBy(xi => xi.i / size, xi => xi.x)
             .Select(g => g.ToArray());
}
于 2010-08-19T17:33:38.910 回答
8

使用SkipandTake将是一个非常糟糕的主意。调用Skip索引集合可能没问题,但任意调用它IEnumerable<T>可能会导致枚举跳过的元素数量,这意味着如果您重复调用它,您将枚举序列一个数量级以上比你需要的时间

随心所欲地抱怨“过早的优化”;但这太荒谬了。

我认为你的Slice方法和它一样好。我将建议一种不同的方法来提供延迟执行并避免中间数组分配,但这是一个危险的游戏(即,如果你ToList在这样的结果IEnumerable<T>实现上尝试类似的东西,而不枚举内部集合,你' 将进入一个无限循环)。

(我已经删除了最初的内容,因为自从发布问题以来 OP 的改进已经使我在这里的建议变得多余。)

于 2010-08-19T17:45:34.043 回答
2

让我们看看您是否甚至需要 Slice 的复杂性。如果您生成的随机数是无状态的,我会假设每次调用它都会生成唯一的随机数,所以这可能就足够了:

var group1 = RandomNumberGenerator().Take(10);  
var group2 = RandomNumberGenerator().Take(10);  
var group3 = RandomNumberGenerator().Take(10);  
var group4 = RandomNumberGenerator().Take(10);

每次调用Take都会返回一组新的 10 个号码。

现在,如果您的随机数生成器在每次迭代时都使用特定值重新播种,这将不起作用。您只需为每个组获得相同的 10 个值。因此,您将使用:

var generator  = RandomNumberGenerator();
var group1     = generator.Take(10);  
var group2     = generator.Take(10);  
var group3     = generator.Take(10);  
var group4     = generator.Take(10);

这维护了生成器的一个实例,以便您可以继续检索值而无需重新播种生成器。

于 2010-08-19T17:42:27.250 回答
1

您可以对任何 Enumerable 对象使用SkipTake方法。

对于您的编辑:

将切片编号和切片大小作为参数的函数怎么样?

private static IEnumerable<T> Slice<T>(IEnumerable<T> enumerable, int sliceSize, int sliceNumber) {
    return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize);
}
于 2010-08-19T17:26:59.627 回答
1

看起来我们更喜欢IEnumerable<T>有一个固定位置的计数器,这样我们就可以做到

var group1 = items.Take(10);
var group2 = items.Take(10);
var group3 = items.Take(10);
var group4 = items.Take(10);

并获得连续的切片,而不是每次都获得前 10 个项目。我们可以通过一个新的实现来做到这一点,该实现IEnumerable<T>保留其 Enumerator 的一个实例并在每次调用 GetEnumerator 时返回它:

public class StickyEnumerable<T> : IEnumerable<T>, IDisposable
{
    private IEnumerator<T> innerEnumerator;

    public StickyEnumerable( IEnumerable<T> items )
    {
        innerEnumerator = items.GetEnumerator();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return innerEnumerator;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return innerEnumerator;
    }

    public void Dispose()
    {
        if (innerEnumerator != null)
        {
            innerEnumerator.Dispose();
        }
    }
}

给定那个类,我们可以用

public static IEnumerable<IEnumerable<T>> Slices<T>(this IEnumerable<T> items, int size)
{
    using (StickyEnumerable<T> sticky = new StickyEnumerable<T>(items))
    {
        IEnumerable<T> slice;
        do
        {
            slice = sticky.Take(size).ToList();
            yield return slice;
        } while (slice.Count() == size);
    }
    yield break;
}

这在这种情况下有效,但StickyEnumerable<T>如果消费代码不期望它,通常是一个危险的类。例如,

using (var sticky = new StickyEnumerable<int>(Enumerable.Range(1, 10)))
{
    var first = sticky.Take(2);
    var second = sticky.Take(2);
    foreach (int i in second)
    {
        Console.WriteLine(i);
    }
    foreach (int i in first)
    {
        Console.WriteLine(i);
    }
}

印刷

1
2
3
4

而不是

3
4
1
2
于 2010-08-19T18:47:09.450 回答
0

看看 Take()、TakeWhile() 和 Skip()

于 2010-08-19T17:26:53.660 回答
0

我认为使用Slice()会有点误导。我认为这是一种将数组放入新数组并且不会造成副作用的方法。在这种情况下,您实际上会将可枚举向前移动 10。

一个可能更好的方法是只使用 Linq 扩展Take()。我认为您不需要使用Skip()发电机。

编辑:当,我一直在尝试使用以下代码测试此行为

注意:这不是真的正确,我把它留在这里,这样其他人就不会陷入同样的​​错误。

var numbers = RandomNumberGenerator();
var slice = numbers.Take(10);

public static IEnumerable<int> RandomNumberGenerator()
{
    yield return random.Next();
}

Count()forslice始终为 1。我还尝试通过foreach循环运行它,因为我知道 Linq 扩展通常是惰性求值的,并且它只循环一次。我最终做了下面的代码而不是Take()它,它可以工作:

public static IEnumerable<int> Slice(this IEnumerable<int> enumerable, int size)
{
    var list = new List<int>();
    foreach (var count in Enumerable.Range(0, size)) list.Add(enumerable.First());
    return list;
}

如果您注意到我First()每次都将其添加到列表中,但是由于传入的可枚举是生成器,RandomNumberGenerator()因此每次都不同。

因此,再次使用生成器Skip()不需要使用,因为结果会有所不同。在 an 上循环IEnumerable并不总是没有副作用。

编辑:我会留下最后一次编辑,这样没有人会陷入同样的​​错误,但这样做对我来说效果很好:

var numbers = RandomNumberGenerator();

var slice1 = numbers.Take(10);
var slice2 = numbers.Take(10);

两片不同。

于 2010-08-19T17:37:57.193 回答
0

我在原来的答案中犯了一些错误,但有些观点仍然成立。Skip() 和 Take() 与生成器的工作方式与列表不同。在 IEnumerable 上循环并不总是没有副作用。无论如何,这是我对获取切片列表的看法。

    public static IEnumerable<int> RandomNumberGenerator()
    {
        while(true) yield return random.Next();
    }

    public static IEnumerable<IEnumerable<int>> Slice(this IEnumerable<int> enumerable, int size, int count)
    {
        var slices = new List<List<int>>();
        foreach (var iteration in Enumerable.Range(0, count)){
            var list = new List<int>();
            list.AddRange(enumerable.Take(size));
            slices.Add(list);
        }
        return slices;
    }
于 2010-08-19T18:23:44.480 回答
0

对于同样的问题,我得到了这个解决方案:

int[] ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
IEnumerable<IEnumerable<int>> chunks = Chunk(ints, 2, t => t.Dump());
//won't enumerate, so won't do anything unless you force it:
chunks.ToList();

IEnumerable<T> Chunk<T, R>(IEnumerable<R> src, int n, Func<IEnumerable<R>, T> action){
  IEnumerable<R> head;
  IEnumerable<R> tail = src;
  while (tail.Any())
  {
    head = tail.Take(n);
    tail = tail.Skip(n);
    yield return action(head);
  }
}

如果您只想返回块,而不是对它们做任何事情,请使用chunks = Chunk(ints, 2, t => t). 我真正想要的是必须具有t=>t默认操作,但我还没有找到如何做到这一点。

于 2012-06-21T14:19:27.957 回答