上下文: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();
}
}
}
问题:有没有更简单的方法来完成我想要做的事情?也许林奇?
注意:上面的例子是一个简化,在我的程序中我有一个Iterator,它以非线性的方式扫描给定的矩阵。
编辑:为什么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组。
答案 0 :(得分:12)
答案 1 :(得分:6)
我做了类似的事情。但我希望它更简单:
//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());
}
答案 2 :(得分:6)
使用Skip
和Take
是一个非常糟糕的主意。在索引集合上调用Skip
可能没问题,但在任意IEnumerable<T>
上调用它可能会导致枚举超过跳过的元素数量,这意味着如果你反复调用它''重新计算序列比你需要的次数多一个数量级。
所有你想要的“过早优化”的抱怨;但那太荒谬了。
我认为你的Slice
方法和它一样好。我打算提出一种不同的方法来提供延迟执行并避免中间数组分配,但这是一个危险的游戏(即,如果你在这样的ToList
上尝试IEnumerable<T>
之类的东西实现,没有枚举内部集合,你将最终陷入无限循环。)
(我已经删除了原来的内容,因为自从发布问题后OP的改进已经使我的建议变得多余了。)
答案 3 :(得分:2)
让我们看看你是否需要Slice的复杂性。如果你的随机数生成是无状态的,我会假设每次调用都会生成唯一的随机数,所以这可能就足够了: / p>
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);
这维护了生成器的实例,以便您可以继续检索值而无需重新生成生成器。
答案 4 :(得分:1)
您可以将Skip和Take方法与任何Enumerable对象一起使用。
对于您的修改:
将切片编号和切片大小作为参数的函数怎么样?
private static IEnumerable<T> Slice<T>(IEnumerable<T> enumerable, int sliceSize, int sliceNumber) {
return enumerable.Skip(sliceSize * sliceNumber).Take(sliceSize);
}
答案 5 :(得分: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>
的新实现来保存它的枚举器的一个实例,并在每次调用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();
}
}
}
鉴于该课程,我们可以用
实现Slicepublic 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
答案 6 :(得分:0)
看看Take(),TakeWhile()和Skip()
答案 7 :(得分:0)
我认为使用Slice()
会有点误导。我认为这是一种方法,可以让我把一个数组放入一个新数组,而不会引起副作用。在这种情况下,您实际上会移动可枚举的前进10。
更好的方法是使用Linq扩展Take()
。我认为您不需要将Skip()
与生成器一起使用。
编辑: Dang,我一直在尝试使用以下代码测试此行为
注意:这不是真的正确,我把它放在这里,以免其他人犯同样的错误。
var numbers = RandomNumberGenerator();
var slice = numbers.Take(10);
public static IEnumerable<int> RandomNumberGenerator()
{
yield return random.Next();
}
但是Count()
的{{1}}总是1.我也尝试通过slice
循环运行它,因为我知道Linq扩展通常是懒惰的评估,它只循环一次。我最终做了下面的代码,而不是foreach
,它起作用了:
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()
并不总是无副作用。
编辑:我会离开最后一个编辑,所以没有人遇到同样的错误,但这对我来说这样做很好:
IEnumerable
两片不同。
答案 8 :(得分: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;
}
答案 9 :(得分: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
作为默认操作,但我还没有找到如何做到这一点。