web-dev-qa-db-ja.com

イテレータメソッドが「ref」または「out」パラメータを取得できないのはなぜですか?

私は今日早くこれを試しました:

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            

    }
}

これの背後にある理論的根拠は何ですか?

38
Trap

C#イテレータは、内部的にはステートマシンです。何かをyield returnするたびに、中断した場所をローカル変数の状態とともに保存して、そこから戻って続行できるようにする必要があります。

この状態を保持するために、C#コンパイラは、ローカル変数とそれが継続する場所を保持するクラスを作成します。クラスのフィールドとしてrefまたはoutの値を持つことはできません。したがって、パラメーターをrefまたはoutとして宣言することが許可されている場合、中断した時点で関数の完全なスナップショットを保持する方法はありません。

EDIT:技術的には、IEnumerable<T>を返すすべてのメソッドがイテレータと見なされるわけではありません。 yieldを使用してシーケンスを直接生成するものだけが、イテレータと見なされます。したがって、イテレータを2つの方法に分割することは、優れた一般的な回避策ですが、今言ったことと矛盾することはありません。外側のメソッド(yieldを直接使用しない)はnotイテレーターと見なされます。

49
Mehrdad Afshari

メソッドからイテレータとintの両方を返したい場合、回避策は次のとおりです。

_public class Bar : IFoo
{
    public IEnumerable<int> GetItems( ref int somethingElse )
    {
        somethingElse = 42;
        return GetItemsCore();
    }

    private IEnumerable<int> GetItemsCore();
    {
        yield return 7;
    }
}
_

イテレータメソッド(つまり、基本的に_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
_

EDIT:イテレータを移動すると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
_
19
Rasmus Faber

高レベルでは、ref変数は、スタック上にある値型を含む多くの場所を指すことができます。イテレータメソッドを呼び出してイテレータが最初に作成される時間と、ref変数が割り当てられる時間は、2つの非常に異なる時間です。イテレータが実際に実行されたときに、最初に参照によって渡された変数がまだ存在することを保証することはできません。したがって、許可されていません(または検証可能です)

5
JaredPar

他の人は、イテレータが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;
    }
}

渡したり渡したりするアイテムが複数ある場合は、それらを保持するクラスを定義します。

4
Jim Balter

返す必要のある値が反復アイテムから派生している場合、関数を使用してこの問題を回避しました。

// 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 );
}
1
Tony Tanzillo