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();
            }
        }
    }

質問:私がやろうとしていることを達成するためのより簡単な方法はありますか? おそらくLinq?

注: 上記の例は簡略化したものです。私のプログラムでは、特定の行列を非線形にスキャンする 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);

number (10+20+30+40) 回再生成するオーバーヘッドはありません。正確に 40 個の数字を生成し、4 つのグループの数字を 10 で割るソリューションが必要です。

4

10 に答える 10

12

スキップアンドテイクはあなたに役立っていますか?

ループ内で2つの組み合わせを使用して、必要なものを取得します。

それで、

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>スキップされた要素の数を列挙する可能性があります。つまり、繰り返し呼び出すと、シーケンスを 1 桁以上列挙することになりますあなたがする必要があるよりも倍

「時期尚早の最適化」について不平を言うのはあなたが望むすべてです。しかし、それはばかげています。

あなたの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

SkipメソッドとTakeメソッドは、任意のEnumerableオブジェクトで使用できます。

あなたの編集のために:

スライス数とスライスサイズをパラメータとする関数はどうですか?

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 の 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();
        }
    }
}

そのクラスを考えると、Slice を次のように実装できます。

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

私は同じ問題に対してこの解決策を得ました:

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 に答える
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

Slice()の使用は少し誤解を招くと思います。これは、配列を新しい配列にチャックし、副作用を引き起こさない手段として考えています。このシナリオでは、実際に列挙可能なものを 10 進めます。

考えられるより良いアプローチは、Linq 拡張機能を使用することTake()です。Skip()ジェネレーターを使用する必要はないと思います。

編集:ダン、私は次のコードでこの動作をテストしようとしています

注:これは実際には正しくありませんでした。他の人が同じ間違いに陥らないように、ここに残します。

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

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

しかし、Count()forは常に 1 です。Linq 拡張機能は一般に遅延評価され、ループが 1 回だけであることを知っているので、ループsliceを実行してみました。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常に副作用がないわけではありません。

編集:誰も同じ間違いに陥らないように最後の編集を残しますが、これを行うだけでうまくいきました:

var numbers = RandomNumberGenerator();

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

2つのスライスは異なっていました。

于 2010-08-19T17:37:57.193 に答える