Почему итераторные методы не могут принимать параметры " 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            

    }
}

каково обоснование этого?

5 ответов


итераторы C# являются государственными машинами внутри. Каждый раз, когда вы yield return что-то, место, где вы остановились, должно быть сохранено вместе с состоянием локальных переменных, чтобы вы могли вернуться и продолжить оттуда.

чтобы сохранить это состояние, компилятор C# создает класс для хранения локальных переменных и место, из которого он должен продолжаться. Невозможно иметь ref или out значение как поле в классе. Следовательно, если вам было разрешено объявить параметр как ref или out, не было бы никакого способа сохранить полный снимок функции в то время, когда мы остановились.

EDIT: технически, не все методы, которые возвращают IEnumerable<T> считаются итераторы. Только те, которые используют yield для получения последовательности непосредственно рассматриваются итераторы. Поэтому, хотя разделение итератора на два метода является хорошим и общим обходным путем, это не противоречит тому, что я только что сказал. Внешний метод (который не использует yield напрямую)не рассматривать итератор.


если вы хотите вернуть как итератор, так и 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

таким образом, хорошим шаблоном является разделение методов итератора на две части: одну для немедленного выполнения и одну, содержащую код, который должен быть лениво выполнен.

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

на высоком уровне переменная ref может указывать на многие местоположения, включая типы значений, которые находятся в стеке. Время, в которое итератор первоначально создается путем вызова метода итератора и когда переменная ref будет назначена, - это два очень разных времени. Невозможно гарантировать, что переменная, которая первоначально была передана по ссылке, все еще существует, когда итератор фактически выполняется. Следовательно, это не допускается (или не поддается проверке)


другие объяснили, почему итератор не может иметь параметр 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 );
}