web-dev-qa-db-ja.com

C#での「yield」キーワードの実用的な使用

ほぼ4年の経験の後、yieldキーワードが使用されているコードを見たことがありません。誰かが私にこのキーワードの実用的な使用法を(説明に沿って)教えてくれますか?そうであれば、それを実行するための簡単な方法は他にありませんか?

77
Saeed Neamati

効率

yieldキーワードを指定すると、コレクションアイテムに対する遅延列挙が効率的に作成され、効率が大幅に向上します。たとえば、foreachループが100万項目の最初の5項目のみを反復処理する場合、それはすべてyieldの戻り値であり、最初に100万項目のコレクションを内部で構築していません。同様に、独自のプログラミングシナリオでyieldIEnumerable<T>の戻り値とともに使用して、同じ効率を実現することもできます。

特定のシナリオで得られる効率の例

イテレータメソッドではなく、大きなコレクションの非効率的な使用の可能性があります
(中間コレクションは多くのアイテムを使用して構築されます)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

イテレータバージョン、効率的
(中間コレクションは作成されません)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

いくつかのプログラミングシナリオを簡略化する

別のケースでは、アイテムを中間コレクションにソートしてそこに入れ替えるのではなく、アイテムを目的の順序でyield戻すだけなので、リストのソートとマージの種類がプログラミングしやすくなります。そのようなシナリオはたくさんあります。

1つの例は、2つのリストのマージです。

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

このメソッドは、1つの連続したアイテムのリストを返します。中間コレクションを必要としない効果的なマージです。

より詳しい情報

yieldキーワードは、イテレータメソッドのコンテキストでのみ使用できます(戻りタイプはIEnumerableIEnumeratorIEnumerable<T>、またはIEnumerator<T>です) 。)そしてforeachと特別な関係があります。イテレータは特別なメソッドです。 MSDN生成ドキュメント および イテレータドキュメント には、多くの興味深い情報と概念の説明が含まれています。イテレータの理解を深めるために、それについても読んで foreachキーワード と関連付けてください。

イテレータが効率をどのように達成するかを知るための秘訣は、C#コンパイラによって生成されるILコードにあります。反復子メソッドに対して生成されたILは、通常の(非反復子)メソッドに対して生成されたILとは大きく異なります。 この記事(yieldキーワードが実際に生成するもの) は、そのような洞察を提供します。

109
John K

少し前に私は実用的な例を挙げました、あなたがこのような状況を持っていると仮定しましょう:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

ボタンオブジェクトは、コレクション内での自分の位置を知りません。同じ制限がDictionary<T>または他のコレクション型にも適用されます。

これがyieldキーワードを使用した私の解決策です:

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

そして、それは私がそれを使う方法です:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

インデックスを見つけるために、コレクションに対してforループを作成する必要はありません。私のボタンにはIDがあり、これはIndexerList<T>のインデックスとしても使用されるので、冗長なIDまたはインデックスを回避します-それが好きです。 index/Idは任意の数にすることができます。

4

実用的な例はここにあります:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

標準コードよりも歩留まりを使用することには多くの利点があります。

  • イテレータを使用してリストを作成する場合は、戻り値を生成でき、呼び出し元は、結果がリストになるかどうかをdecideできます。
  • 呼び出し元は、反復で実行していることの範囲外の理由で反復をキャンセルすることを決定する場合もあります。
  • コードは少し短いです。

しかし、Jan_Vが言ったように(数秒で私に打ち勝つだけです:-)内部的にコンパイラーは両方のケースでほぼ同じコードを生成するため、それなしで生きることができます。

2
Jalayn

SQLコマンドテキスト、コマンドタイプを設定し、「コマンドパラメーター」のIEnumerableを返すcommandクラスを持つ小さなdbデータレイヤーがあります。

基本的には、SqlCommandプロパティとパラメーターを常に手動で入力する代わりに、CLRコマンドを入力するという考え方です。

したがって、次のような関数があります。

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

このcommandクラスを継承するクラスには、AgeおよびNameプロパティがあります。

次に、そのプロパティで満たされたcommandオブジェクトを新しく作成し、実際にコマンド呼び出しを行うdbインターフェイスに渡すことができます。

全体として、SQLコマンドの操作と入力の維持が非常に簡単になります。

1
john

次に例を示します。

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

クラスは、稼働週に基づいて日付計算を実行します。クラスの例では、ボブは毎日午前9時30分から17時30分まで働いており、昼休みには12時30分に1時間の休憩があります。この知識があれば、AscendingShifts()関数は、指定された日付間の有効なシフトオブジェクトを生成します。今年1月1日から2月1日までのボブの勤務シフトをすべてリストするには、次のように使用します。

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

クラスはコレクションに対して実際には反復しません。ただし、2つの日付間のシフトはコレクションと考えることができます。 yield演算子を使用すると、コレクション自体を作成せずに、この想像上のコレクションを反復処理できます。

1
Ant

マージのケースはすでに受け入れ済みの回答でカバーされていますが、yield-merge params拡張メソッド™を紹介しましょう。

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

これを使用して、ネットワークプロトコルのパケットを構築します。

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

たとえば、MarkChecksumメソッドは次のようになります。また、yieldもあります。

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

ただし、列挙メソッドでSum()などの集計メソッドを使用する場合は、別の列挙プロセスをトリガーするので注意してください。

1
Yegor

Elastic Search .NETのサンプルリポジトリには、yield returnを使用してコレクションを指定されたサイズの複数のコレクションに分割する優れた例があります。

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }
1
pholly

Jan_Vの答えを拡張して、私はそれに関連する実際のケースにぶつかっただけです。

FindFirstFile/FindNextFileのKernel32バージョンを使用する必要がありました。最初の呼び出しからハンドルを取得し、それを後続のすべての呼び出しにフィードします。これを列挙子でラップすると、foreachで直接使用できるものが得られます。

0
Loren Pechtel