ほぼ4年の経験の後、yieldキーワードが使用されているコードを見たことがありません。誰かが私にこのキーワードの実用的な使用法を(説明に沿って)教えてくれますか?そうであれば、それを実行するための簡単な方法は他にありませんか?
yield
キーワードを指定すると、コレクションアイテムに対する遅延列挙が効率的に作成され、効率が大幅に向上します。たとえば、foreach
ループが100万項目の最初の5項目のみを反復処理する場合、それはすべてyield
の戻り値であり、最初に100万項目のコレクションを内部で構築していません。同様に、独自のプログラミングシナリオでyield
をIEnumerable<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
キーワードは、イテレータメソッドのコンテキストでのみ使用できます(戻りタイプはIEnumerable
、IEnumerator
、IEnumerable<T>
、またはIEnumerator<T>
です) 。)そしてforeach
と特別な関係があります。イテレータは特別なメソッドです。 MSDN生成ドキュメント および イテレータドキュメント には、多くの興味深い情報と概念の説明が含まれています。イテレータの理解を深めるために、それについても読んで foreach
キーワード と関連付けてください。
イテレータが効率をどのように達成するかを知るための秘訣は、C#コンパイラによって生成されるILコードにあります。反復子メソッドに対して生成されたILは、通常の(非反復子)メソッドに対して生成されたILとは大きく異なります。 この記事(yieldキーワードが実際に生成するもの) は、そのような洞察を提供します。
少し前に私は実用的な例を挙げました、あなたがこのような状況を持っていると仮定しましょう:
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は任意の数にすることができます。
実用的な例はここにあります:
http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html
標準コードよりも歩留まりを使用することには多くの利点があります。
しかし、Jan_Vが言ったように(数秒で私に打ち勝つだけです:-)内部的にコンパイラーは両方のケースでほぼ同じコードを生成するため、それなしで生きることができます。
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コマンドの操作と入力の維持が非常に簡単になります。
次に例を示します。
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
演算子を使用すると、コレクション自体を作成せずに、この想像上のコレクションを反復処理できます。
マージのケースはすでに受け入れ済みの回答でカバーされていますが、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()などの集計メソッドを使用する場合は、別の列挙プロセスをトリガーするので注意してください。
Elastic Search .NETのサンプルリポジトリには、yield return
を使用してコレクションを指定されたサイズの複数のコレクションに分割する優れた例があります。
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);
}
}
Jan_Vの答えを拡張して、私はそれに関連する実際のケースにぶつかっただけです。
FindFirstFile/FindNextFileのKernel32バージョンを使用する必要がありました。最初の呼び出しからハンドルを取得し、それを後続のすべての呼び出しにフィードします。これを列挙子でラップすると、foreachで直接使用できるものが得られます。