なぜIteratorメソッドは「ref」または「out」パラメーターをとることができないのですか?
-
14-09-2019 - |
質問
私は今日早くこれを試しました:
public interface IFoo
{
IEnumerable<int> GetItems_A( ref int somethingElse );
IEnumerable<int> GetItems_B( ref int somethingElse );
}
public class Bar : IFoo
{
public IEnumerable<int> GetItems_A( ref int somethingElse )
{
// Ok...
}
public IEnumerable<int> GetItems_B( ref int somethingElse )
{
yield return 7; // CS1623: Iterators cannot have ref or out parameters
}
}
この背後にある理論的根拠は何ですか?
解決
C#Iteratorsは、内部的に状態マシンです。あなたが毎回 yield return
何か、中断した場所は、ローカル変数の状態とともに保存する必要があります。そうすれば、そこから戻って続けることができます。
この状態を保持するために、C#コンパイラは、ローカル変数とそれが続けるべき場所を保持するクラスを作成します。持っていることは不可能です ref
また out
クラスのフィールドとしての価値。その結果、パラメーターを次のように宣言することを許可された場合 ref
また out
, 、中断した時点で、関数の完全なスナップショットを維持する方法はありません。
編集: 技術的には、返すすべての方法ではありません IEnumerable<T>
イテレーターと見なされます。使用するものだけ yield
シーケンスを直接生成することは、イテレーターと見なされます。したがって、イテレーターを2つの方法に分割することは素晴らしく一般的な回避策ですが、私が今言ったこととは矛盾しません。外側の方法(使用しません yield
直接) いいえ イテレーターと見なされます。
他のヒント
メソッドからイテレーターとINTの両方を返したい場合は、回避策が次のとおりです。
public class Bar : IFoo
{
public IEnumerable<int> GetItems( ref int somethingElse )
{
somethingElse = 42;
return GetItemsCore();
}
private IEnumerable<int> GetItemsCore();
{
yield return 7;
}
}
Iteratorメソッド内のコードはないことに注意してください(つまり、基本的には含まれている方法です。 yield return
また yield break
)まで実行されます MoveNext()
列挙器のメソッドが呼び出されます。したがって、使用できた場合 out
また ref
イテレーター方法では、このような驚くべき行動が得られるでしょう。
// This will not compile:
public IEnumerable<int> GetItems( ref int somethingElse )
{
somethingElse = 42;
yield return 7;
}
// ...
int somethingElse = 0;
IEnumerable<int> items = GetItems( ref somethingElse );
// at this point somethingElse would still be 0
items.GetEnumerator().MoveNext();
// but now the assignment would be executed and somethingElse would be 42
これは一般的な落とし穴であり、関連する問題は次のとおりです。
public IEnumerable<int> GetItems( object mayNotBeNull ){
if( mayNotBeNull == null )
throw new NullPointerException();
yield return 7;
}
// ...
IEnumerable<int> items = GetItems( null ); // <- This does not throw
items.GetEnumerators().MoveNext(); // <- But this does
したがって、優れたパターンは、2つの部分にイテレータメソッドを分離することです。1つはすぐに実行すると、1つはゆっくりと実行されるコードを含むものです。
public IEnumerable<int> GetItems( object mayNotBeNull ){
if( mayNotBeNull == null )
throw new NullPointerException();
// other quick checks
return GetItemsCore( mayNotBeNull );
}
private IEnumerable<int> GetItemsCore( object mayNotBeNull ){
SlowRunningMethod();
CallToDatabase();
// etc
yield return 7;
}
// ...
IEnumerable<int> items = GetItems( null ); // <- Now this will throw
編集:あなたが本当にイテレーターを移動すると、 ref
- パラメーター、あなたはこのようなことをすることができます:
public static IEnumerable<int> GetItems( Action<int> setter, Func<int> getter )
{
setter(42);
yield return 7;
}
//...
int local = 0;
IEnumerable<int> items = GetItems((x)=>{local = x;}, ()=>local);
Console.WriteLine(local); // 0
items.GetEnumerator().MoveNext();
Console.WriteLine(local); // 42
高いレベルでは、REF変数は、スタック上にあるタイプを評価するなど、多くの場所を指すことができます。イテレータが最初にIteratorメソッドを呼び出すことによって作成された時間と、REF変数が割り当てられる時間は2つの非常に異なる時間です。元々参照によって渡された変数が、イテレーターが実際に実行されたときにまだ存在することを保証することはできません。したがって、それは許可されていません(または検証可能)
他の人は、なぜあなたのイテレーターがrefパラメーターを持たないのかを説明しました。これが簡単な選択肢です:
public interface IFoo
{
IEnumerable<int> GetItems( int[] box );
...
}
public class Bar : IFoo
{
public IEnumerable<int> GetItems( int[] box )
{
int value = box[0];
// use and change value and yield to your heart's content
box[0] = value;
}
}
出入りするいくつかのアイテムがある場合は、クラスを保持するクラスを定義します。
私が返す必要がある値が反復項目から導き出された場合、私は関数を使用してこの問題を取り上げました。
// One of the problems with Enumerable.Count() is
// that it is a 'terminator', meaning that it will
// execute the expression it is given, and discard
// the resulting sequence. To count the number of
// items in a sequence without discarding it, we
// can use this variant that takes an Action<int>
// (or Action<long>), invokes it and passes it the
// number of items that were yielded.
//
// Example: This example allows us to find out
// how many items were in the original
// source sequence 'items', as well as
// the number of items consumed by the
// call to Sum(), without causing any
// LINQ expressions involved to execute
// multiple times.
//
// int start = 0; // the number of items from the original source
// int finished = 0; // the number of items in the resulting sequence
//
// IEnumerable<KeyValuePair<string, double>> items = // assumed to be an iterator
//
// var result = items.Count( i => start = i )
// .Where( p => p.Key = "Banana" )
// .Select( p => p.Value )
// .Count( i => finished = i )
// .Sum();
//
// // by getting the count of items operated
// // on by Sum(), we can calculate an average:
//
// double average = result / (double) finished;
//
// Console.WriteLine( "started with {0} items", start );
// Console.WriteLine( "finished with {0} items", finished );
//
public static IEnumerable<T> Count<T>(
this IEnumerable<T> source,
Action<int> receiver )
{
int i = 0;
foreach( T item in source )
{
yield return item;
++i ;
}
receiver( i );
}
public static IEnumerable<T> Count<T>(
this IEnumerable<T> source,
Action<long> receiver )
{
long i = 0;
foreach( T item in source )
{
yield return item;
++i ;
}
receiver( i );
}