質問
の中に IList<> のフラグメントのみを公開するにはどうすればよいですか? 質問の 1 つの回答には次のコード スニペットが含まれていました。
IEnumerable<object> FilteredList()
{
foreach( object item in FullList )
{
if( IsItemInPartialList( item )
yield return item;
}
}
そこで、yield キーワードは何をするのでしょうか?いくつかの場所と他の質問でそれが参照されているのを見ましたが、実際に何をするのか完全には理解していません。私は、あるスレッドが別のスレッドに譲歩するという意味で利回りを考えることに慣れていますが、それはここでは関係ないようです。
解決
の yield
キーワードは実際にここで非常に多くのことを行います。
この関数は、 IEnumerable<object>
インターフェース。呼び出し関数が開始される場合 foreach
このオブジェクトに対して、関数は「降伏」するまで再度呼び出されます。これは で導入された糖衣構文です。 C# 2.0. 。以前のバージョンでは、独自のものを作成する必要がありました IEnumerable
そして IEnumerator
このようなことを行うためのオブジェクト。
このようなコードを理解する最も簡単な方法は、例を入力し、いくつかのブレークポイントを設定して、何が起こるかを確認することです。この例を段階的に実行してみてください。
public void Consumer()
{
foreach(int i in Integers())
{
Console.WriteLine(i.ToString());
}
}
public IEnumerable<int> Integers()
{
yield return 1;
yield return 2;
yield return 4;
yield return 8;
yield return 16;
yield return 16777216;
}
例を段階的に実行すると、最初の呼び出しが見つかります。 Integers()
戻り値 1
. 。2 番目の呼び出しが返されます 2
そしてライン yield return 1
再度実行されることはありません。
実際の例を次に示します。
public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
using (var connection = CreateConnection())
{
using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
{
command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return make(reader);
}
}
}
}
}
他のヒント
反復。これは、関数の追加サイクルごとにどこにいたかを記憶し、そこから再開する「内部」のステート マシンを作成します。
Yield には 2 つの優れた用途があります。
上記 2 つの点をより具体的に説明するために、簡単なビデオを作成しましたので、ご覧ください。 ここ
最近、Raymond Chen も利回りキーワードに関する興味深いシリーズ記事を連載しました。
- C# でのイテレーターの実装とその結果 (パート 1)
- C# でのイテレーターの実装とその結果 (パート 2)
- C# でのイテレーターの実装とその結果 (パート 3)
- C# でのイテレーターの実装とその結果 (パート 4)
名目上はイテレータ パターンを簡単に実装するために使用されますが、ステート マシンに一般化することもできます。Raymond を引用するのは意味がありません。最後の部分は他の用途にもリンクしています (ただし、Entin のブログの例は非常に優れており、非同期で安全なコードの書き方を示しています)。
一見すると、 yield return は IEnumerable を返す .NET のシュガーです。
利回りを指定しないと、コレクションのすべてのアイテムが一度に作成されます。
class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
return new List<SomeData> {
new SomeData(),
new SomeData(),
new SomeData()
};
}
}
yield を使用した同じコードでは、項目ごとに返されます。
class SomeData
{
public SomeData() { }
static public IEnumerable<SomeData> CreateSomeDatas()
{
yield return new SomeData();
yield return new SomeData();
yield return new SomeData();
}
}
yield を使用する利点は、データを使用する関数が単にコレクションの最初の項目を必要とする場合、残りの項目は作成されないことです。
yield 演算子を使用すると、要求に応じて項目を作成できます。それがそれを使用する良い理由です。
yield return
列挙子とともに使用されます。yield ステートメントを呼び出すたびに、制御は呼び出し元に返されますが、呼び出し先の状態は確実に維持されます。このため、呼び出し元が次の要素を列挙すると、その直後の呼び出し先メソッドの from ステートメントで実行が継続されます。 yield
声明。
例を挙げてこれを理解してみましょう。この例では、各行に対応して実行の流れの順序を示しています。
static void Main(string[] args)
{
foreach (int fib in Fibs(6))//1, 5
{
Console.WriteLine(fib + " ");//4, 10
}
}
static IEnumerable<int> Fibs(int fibCount)
{
for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
{
yield return prevFib;//3, 9
int newFib = prevFib + currFib;//6
prevFib = currFib;//7
currFib = newFib;//8
}
}
また、状態は列挙ごとに維持されます。別の電話があったとします。 Fibs()
メソッドの状態がリセットされます。
リストまたは配列の実装はすべての項目を即座にロードしますが、yield 実装は遅延実行ソリューションを提供します。
実際には、アプリケーションのリソース消費を削減するために、必要に応じて最小限の作業を実行することが望ましいことがよくあります。
たとえば、データベースから何百万ものレコードを処理するアプリケーションがあるとします。遅延実行プルベース モデルで IEnumerable を使用すると、次の利点が得られます。
- スケーラビリティ、信頼性、予測可能性 レコード数はアプリケーションのリソース要件に大きな影響を与えないため、改善される可能性があります。
- パフォーマンスと応答性 最初にコレクション全体がロードされるのを待たずに、すぐに処理を開始できるため、改善される可能性があります。
- 回復性と利用率 アプリケーションは停止、開始、中断、または失敗する可能性があるため、改善される可能性があります。実際に結果の一部のみを使用してすべてのデータをプリフェッチする場合と比較して、進行中のアイテムのみが失われます。
- 連続処理 一定のワークロード ストリームが追加される環境で可能です。
ここでは、最初にリストなどのコレクションを構築する場合と、yield を使用する場合の比較を示します。
リストの例
public class ContactListStore : IStore<ContactModel>
{
public IEnumerable<ContactModel> GetEnumerator()
{
var contacts = new List<ContactModel>();
Console.WriteLine("ContactListStore: Creating contact 1");
contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
Console.WriteLine("ContactListStore: Creating contact 2");
contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
Console.WriteLine("ContactListStore: Creating contact 3");
contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
return contacts;
}
}
static void Main(string[] args)
{
var store = new ContactListStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection.");
Console.ReadLine();
}
コンソール出力
連絡先リストストア:連絡先の作成 1
連絡先リストストア:連絡先の作成 2
連絡先リストストア:連絡先の作成 3
コレクションを反復処理する準備ができました。
注記:リスト内の単一の項目を要求することなく、コレクション全体がメモリにロードされました。
収量例
public class ContactYieldStore : IStore<ContactModel>
{
public IEnumerable<ContactModel> GetEnumerator()
{
Console.WriteLine("ContactYieldStore: Creating contact 1");
yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
Console.WriteLine("ContactYieldStore: Creating contact 2");
yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
Console.WriteLine("ContactYieldStore: Creating contact 3");
yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
}
}
static void Main(string[] args)
{
var store = new ContactYieldStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection.");
Console.ReadLine();
}
コンソール出力
コレクションを反復処理する準備ができました。
注記:コレクションはまったく実行されませんでした。これは、IEnumerable の「遅延実行」の性質によるものです。アイテムの構築は、本当に必要な場合にのみ行われます。
コレクションを再度呼び出して、コレクション内の最初の連絡先を取得するときの動作を観察してみましょう。
static void Main(string[] args)
{
var store = new ContactYieldStore();
var contacts = store.GetEnumerator();
Console.WriteLine("Ready to iterate through the collection");
Console.WriteLine("Hello {0}", contacts.First().FirstName);
Console.ReadLine();
}
コンソール出力
コレクションを反復処理する準備ができました
連絡先収量ストア:連絡先の作成 1
こんにちはボブ
ニース!クライアントがコレクションからアイテムを「プル」したときに、最初のコンタクトのみが構築されました。
この概念を理解する簡単な方法は次のとおりです。基本的な考え方は、「」を使用できるコレクションが必要な場合です。foreach
" では、アイテムをコレクションに収集するのは、何らかの理由 (データベースからアイテムをクエリするなど) でコストがかかり、多くの場合、コレクション全体が必要ない場合は、コレクションを一度に 1 つずつ構築する関数を作成します。それを消費者に返します(その後、消費者は収集作業を早期に終了できます)。
次のように考えてください。 あなたは精肉売り場に行き、1ポンドのスライスハムを買いたいと思っています。肉屋は10ポンドのハムを奥に持って行き、スライサー機に置き、全体をスライスし、それからスライスの山を持ち帰って、1ポンドを量ります。(古いやり方)。と yield
, 、肉屋はスライサーマシンをカウンターに持ってきて、スライスを開始して、各スライスを1ポンドになるまでスケールに「降ろし」、それからラップして完了です。 肉屋にとっては古い方法の方が良いかもしれません(機械を好きなように整理できる)が、消費者にとっては、ほとんどの場合、新しい方法の方が明らかに効率的です。
の yield
キーワードを使用すると、 IEnumerable<T>
上のフォームに イテレータブロック. 。この反復子ブロックはサポートしています 遅延実行 この概念に慣れていない場合、それはほとんど魔法のように見えるかもしれません。しかし、結局のところ、それは奇妙なトリックなしで実行される単なるコードです。
イテレータ ブロックは、コンパイラが列挙可能なものの列挙がどこまで進んだかを追跡するステート マシンを生成する糖衣構文として説明できます。列挙可能なものを列挙するには、多くの場合、 foreach
ループ。ただし、 foreach
ループも糖衣構文です。したがって、実際のコードから 2 つの抽象化が取り除かれているため、最初はすべてがどのように連携して機能するかを理解するのが難しいかもしれません。
非常に単純な反復子ブロックがあると仮定します。
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
実際の反復子ブロックには条件とループが含まれることがよくありますが、条件をチェックしてループを展開すると、依然として次のようになります。 yield
他のコードとインターリーブされたステートメント。
イテレータブロックを列挙するには、 foreach
ループが使用されます:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
出力は次のとおりです (ここでは驚くべきことではありません)。
Begin 1 After 1 2 After 2 42 End
上記のように foreach
糖衣構文です:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
これを解き明かすために、抽象化を削除したシーケンス図を作成しました。
コンパイラによって生成されたステート マシンも列挙子を実装しますが、図をより明確にするために、それらを別のインスタンスとして示しています。(ステート マシンが別のスレッドから列挙される場合、実際には個別のインスタンスが取得されますが、その詳細はここでは重要ではありません。)
イテレータ ブロックを呼び出すたびに、ステート マシンの新しいインスタンスが作成されます。ただし、イテレータ ブロック内のコードはいずれも実行されません。 enumerator.MoveNext()
初めて実行されます。これが遅延実行の仕組みです。以下に (かなり愚かな) 例を示します。
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
この時点ではイテレータは実行されていません。の Where
句は新しいものを作成します IEnumerable<T>
それを包むのは IEnumerable<T>
によって返されました IteratorBlock
しかし、この数え切れないものはまだ列挙されていません。これは、 foreach
ループ:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
列挙可能なものを 2 回列挙すると、そのたびにステート マシンの新しいインスタンスが作成され、反復子ブロックは同じコードを 2 回実行します。
LINQ メソッドが次のようなものであることに注意してください。 ToList()
, ToArray()
, First()
, Count()
等を使用します foreach
列挙可能なものを列挙するループ。例えば ToList()
enumerable のすべての要素を列挙し、リストに格納します。これで、反復子ブロックを再度実行することなく、リストにアクセスして列挙可能オブジェクトのすべての要素を取得できるようになりました。次のようなメソッドを使用する場合、列挙型の要素を複数回生成するために CPU を使用することと、列挙型の要素に複数回アクセスするためにメモリを使用して列挙型の要素を保存することの間にはトレードオフがあります。 ToList()
.
これを正しく理解していれば、IEnumerable を yield で実装する関数の観点からこれをどのように表現するかは次のとおりです。
- ここに 1 つあります。
- 別のものが必要な場合は、もう一度電話してください。
- 私はあなたにすでに与えたものを覚えています。
- もう一度電話してもらったときに、別のものを提供できるかどうかがわかります。
簡単に言うと、C# の yield キーワードを使用すると、反復子と呼ばれるコード本体への多数の呼び出しが可能になります。この反復子は、処理が完了する前に戻る方法を認識しており、再度呼び出されたときに、中断したところから続行します。これは、反復子が連続する呼び出しで返すシーケンス内の各項目ごとに反復子が透過的にステートフルになるのに役立ちます。
JavaScript では、同じ概念がジェネレーターと呼ばれます。
これは、オブジェクトの列挙可能オブジェクトを作成する非常にシンプルで簡単な方法です。コンパイラは、メソッドをラップし、この場合は IEnumerable<object> を実装するクラスを作成します。yield キーワードを使用しない場合は、IEnumerable<object> を実装するオブジェクトを作成する必要があります。
数え切れないほどのシーケンスが生成されます。実際に行うことは、ローカルの IEnumerable シーケンスを作成し、それをメソッドの結果として返すことです。
これ リンク 簡単な例があります
さらに単純な例はここにあります
public static IEnumerable<int> testYieldb()
{
for(int i=0;i<3;i++) yield return 4;
}
yield return はメソッドから返されないことに注意してください。を置くこともできます WriteLine
後に yield return
上記は 4 つの int 4,4,4,4 の IEnumerable を生成します。
ここで WriteLine
. 。リストに 4 を追加し、abc を出力し、リストに 4 を追加してメソッドを完了し、実際にメソッドから戻ります (リターンのないプロシージャの場合と同様に、メソッドが完了すると)。しかし、これには価値があるでしょう。 IEnumerable
のリスト int
s、完了時に返されること。
public static IEnumerable<int> testYieldb()
{
yield return 4;
console.WriteLine("abc");
yield return 4;
}
yield を使用する場合、返される値は関数と同じ型ではないことにも注意してください。これは、次の要素のタイプです。 IEnumerable
リスト。
メソッドの戻り値の型を指定して yield を使用します。 IEnumerable
. 。メソッドの戻り値の型が int
または List<int>
そしてあなたは使用します yield
, 、その場合、コンパイルされません。使用できます IEnumerable
yield のないメソッドの戻り値の型ですが、おそらく、yield なしでは yield を使用できないようです。 IEnumerable
メソッドの戻り値の型。
そして、それを実行するには、特別な方法で呼び出す必要があります。
static void Main(string[] args)
{
testA();
Console.Write("try again. the above won't execute any of the function!\n");
foreach (var x in testA()) { }
Console.ReadLine();
}
// static List<int> testA()
static IEnumerable<int> testA()
{
Console.WriteLine("asdfa");
yield return 1;
Console.WriteLine("asdf");
}
Ruby の良さを取り入れようとしています :)
コンセプト: これは、配列の各要素を出力するサンプル Ruby コードです。
rubyArray = [1,2,3,4,5,6,7,8,9,10]
rubyArray.each{|x|
puts x # do whatever with x
}
配列の各メソッドの実装 収量 呼び出し元 (「puts x」) に制御を渡します。 それぞれ 配列の要素は x としてきちんと表現されます。その後、呼び出し元は x に対して必要なことを何でも行うことができます。
しかし 。ネット ここまでは行きません..C# は、Mendelt の応答に見られるように、呼び出し側で foreach ループを記述することを強制する形で、yield と IEnumerable を組み合わせているようです。エレガントさは少し劣ります。
//calling code
foreach(int i in obCustomClass.Each())
{
Console.WriteLine(i.ToString());
}
// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
for(int iLooper=0; iLooper<data.Length; ++iLooper)
yield return data[iLooper];
}