Почему методы итератора не могут принимать параметры ref или out? - PullRequest
36 голосов
/ 16 июня 2009

Я попробовал это ранее сегодня:

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 ]

46 голосов
/ 16 июня 2009

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

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

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

16 голосов
/ 16 июня 2009

Если вы хотите вернуть итератор и 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
5 голосов
/ 16 июня 2009

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

4 голосов
/ 12 июня 2014

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

Если у вас есть несколько предметов для входа и выхода, определите класс для них.

1 голос
/ 06 марта 2012

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

// 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 );
}
...