我今天早些时候试过这个:
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
}
}
这背后的理由是什么?
答案 0 :(得分:46)
C#迭代器是内部的状态机。每当你yield return
某事时,你离开的地方都应该与地方变量的状态一起保存,这样你就可以回来并从那里继续。
要保持这种状态,C#编译器会创建一个类来保存局部变量以及它应该继续的位置。作为一个类中的字段,不可能有ref
或out
值。因此,如果允许您将参数声明为ref
或out
,则在我们离开时无法保留函数的完整快照。
编辑:从技术上讲,并非所有返回IEnumerable<T>
的方法都被视为迭代器。只有那些使用yield
直接生成序列的人才被认为是迭代器。因此,虽然将迭代器拆分为两种方法是一种很好的常见解决方法,但它与我刚才所说的并不矛盾。外部方法(不直接使用yield
)是而不是被视为迭代器。
答案 1 :(得分:16)
如果要从方法中返回迭代器和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
的方法)都不会被执行,直到调用Enumerator中的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
答案 2 :(得分:5)
在更高级别,一个ref变量可以指向许多位置,包括堆栈上的值类型。最初通过调用迭代器方法创建迭代器的时间以及何时分配ref变量的时间是两个非常不同的时间。当迭代器实际执行时,不可能保证最初通过引用传递的变量仍然存在。因此,不允许(或可验证)
答案 3 :(得分:3)
其他人已经解释了为什么你的迭代器不能有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 :(得分:1)
当我需要返回的值来自迭代项时,我使用函数解决了这个问题:
// 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 );
}