Почему методы итератора не могут принимать параметры "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
}
}
В чем причина этого?
Ответы
Ответ 1
Итераторы С# являются государственными машинами внутри. Каждый раз, когда вы yield return
что-то, место, где вы остановились, должно быть сохранено вместе с состоянием локальных переменных, чтобы вы могли вернуться и продолжить оттуда.
Чтобы сохранить это состояние, компилятор С# создает класс для хранения локальных переменных и места, из которых он должен быть продолжен. Невозможно иметь значение ref
или out
как поле в классе. Следовательно, если вам разрешено объявлять параметр как ref
или out
, не было бы способа сохранить полный моментальный снимок этой функции в момент, когда мы остановились.
EDIT: Технически не все методы, возвращающие IEnumerable<T>
, считаются итераторами. Только те, которые используют yield
для создания последовательности непосредственно, считаются итераторами. Поэтому, хотя разделение итератора на два метода является хорошим и распространенным обходным путем, это не противоречит тому, что я только что сказал. Внешний метод (который не использует yield
напрямую) не считается итератором.
Ответ 2
Если вы хотите вернуть итератор и 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()
в Enumerator. Поэтому, если вы можете использовать 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
Ответ 3
На высоком уровне переменная ref может указывать на многие местоположения, включая типы значений, которые находятся в стеке. Время, в которое итератор изначально создается путем вызова метода итератора, и когда будет задана переменная ref, - это два очень разных раза. Невозможно гарантировать, что переменная, которая первоначально была передана по ссылке, все еще существует, когда итератор фактически выполняет. Следовательно, это недопустимо (или проверяемо)
Ответ 4
Я обошел эту проблему с помощью функций, когда значение, которое мне нужно вернуть, получено из итерированных элементов:
// 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 );
}
Ответ 5
Другие объяснили, почему ваш итератор не может иметь параметр 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 content
box[0] = value;
}
}
Если у вас есть несколько элементов для входа и выхода, определите класс для их хранения.