質問
コンテキスト: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);
}
スライスメソッドを定義しましたが、すでに定義されているはずです。参照のためだけに、私のスライス方法は次のとおりです。
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();
}
}
}
質問: 私がやろうとしていることを達成する簡単な方法はありますか?おそらくlinq?
注:上記の例は単純化です。私のプログラムには、非線形方法で行われたマトリックスをスキャンするイテレーターがあります。
編集: なぜ 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までに破壊するソリューションが必要です。
解決
私は同様のことをしました。しかし、私はそれをよりシンプルにしたいです:
//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());
}
他のヒント
使用 Skip
と Take
だろう 非常に悪い考え. 。電話 Skip
インデックス付きコレクションでは問題ない場合がありますが、任意の任意で呼び出します IEnumerable<T>
スキップされた要素の数にわたって列挙される可能性があります。つまり、繰り返し呼び出している場合は、シーケンスを列挙しています。 あなたがそうする必要があるよりも多くの桁.
あなたが望むすべての「時期尚早の最適化」を不平を言う。しかし、それはばかげています。
私はあなたのと思います Slice
方法は、それが得るのと同じくらい良いです。延期された実行を提供し、中間配列の割り当てを削除する別のアプローチを提案するつもりでしたが、それはプレイするのが危険なゲームです(つまり、あなたがようなものを試してみると ToList
そのような結果 IEnumerable<T>
実装は、内側のコレクションを列挙せずに、終わりのないループになります)。
(私は元々ここにあったものを削除しました。質問を投稿してからOPの改善は、ここで私の提案を冗長にしたからです。)
スライスの複雑ささえ必要かどうかを見てみましょう。 乱数の生成がステートレスである場合、それへの各呼び出しが一意の乱数を生成すると仮定するので、おそらくこれで十分です。
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);
これにより、発電機のインスタンスが維持されるため、発電機を再播種せずに値の取得を続けることができます。
私たちはandを好むようです 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>
これは、列挙者の1つのインスタンスを保持し、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
take()、take()、skip()を見てください
の使用だと思います Slice()
少し誤解を招くでしょう。私はそれを新しいアレイに配列のチャックを与え、副作用を引き起こさない手段と考えています。このシナリオでは、実際に列挙可能な前方に移動します10。
より良いアプローチは、LINQ拡張機能を使用することです Take()
. 。使用する必要はないと思います Skip()
発電機付き。
編集: ダン、私は次のコードでこの動作をテストしようとしています
ノート: これは本当に正しくありませんでした、私はここに残しているので、他の人は同じ間違いに陥らないようにします。
var numbers = RandomNumberGenerator();
var slice = numbers.Take(10);
public static IEnumerable<int> RandomNumberGenerator()
{
yield return random.Next();
}
しかし Count()
にとって slice
常に1です。私もそれを実行してみました foreach
Loopは、LINQ拡張機能が一般的に怠lazに評価され、1回だけループしていることを知っているためです。最終的には以下のコードを行いました 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
常に副作用がないとは限りません。
編集: 誰も同じ間違いに陥らないように最後の編集を残しますが、これを行うだけでうまくいきました:
var numbers = RandomNumberGenerator();
var slice1 = numbers.Take(10);
var slice2 = numbers.Take(10);
2つのスライスは異なっていました。
私は元の答えでいくつかの間違いを犯しましたが、いくつかのポイントはまだ立っています。 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;
}
私は同じ問題のためにこの解決策を得ました:
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
デフォルトのアクションとしてですが、私はまだそれを行う方法を見つけていません。