Почему методы итератора не могут принять параметры «ref» или «out»?

StackOverflow https://stackoverflow.com/questions/999020

  •  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# Итераторы являются государственными машинами внутри. Каждый раз, когда вы yield return Что -то, место, где вы остановились, должно быть сохранено вместе с состоянием местных переменных, чтобы вы могли вернуться и продолжить оттуда.

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

РЕДАКТИРОВАТЬ: Технически не все методы, которые возвращаются 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

РЕДАКТИРОВАТЬ:Если вы действительно хотите поведение, при котором перемещение итератора изменит 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 может указывать на многие места, в том числе на типы значений, которые находятся в стеке. Время, в которое итератор первоначально создается путем вызова метода итератора, и когда переменная ссылки будет назначена, - это два очень разных времена. Невозможно гарантировать, что переменная, которая первоначально была передана ссылкой, все еще вокруг, когда итератор фактически выполняется. Следовательно, это не разрешено (или поддается проверке)

Другие объяснили, почему у вашего итератора не может быть параметра ссылки. Вот простая альтернатива:

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 );
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top