为什么迭代器方法不能使用'ref'或'out'参数?

50

我今天早些时候尝试了这个:

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            

    }
}

这背后的原理是什么?


你在尝试过程中遇到了什么问题,还是想请我们为你的尝试制定合理性? - Paul Sonier
2
我在这里讨论了一些设计考虑因素:http://blogs.msdn.com/ericlippert/archive/2009/05/04/the-stack-is-an-implementation-detail-part-two.aspx - Eric Lippert
现代解决方案:http://answers.unity3d.com/answers/551381/view.html - Fattie
5个回答

62

C#迭代器在内部是状态机。每次你使用yield return返回值时,离开的位置应该保存下来,并且还要保存局部变量的状态,这样您就可以回到那个位置并继续执行。

为了保存这个状态,C#编译器创建一个类来保存局部变量和应该继续执行的位置。不可能将refout值作为类中的字段。因此,如果允许将参数声明为refout,则没有办法在我们离开时保留函数的完整快照。

编辑:技术上讲,不是所有返回IEnumerable<T>的方法都被视为迭代器。只有那些使用yield直接生成序列的方法才被视为迭代器。因此,虽然将迭代器拆分为两个方法是一种好的常见解决方法,但它并不与我刚才说的相矛盾。外部方法(不直接使用yield的方法)不被视为迭代器。


4
在类中,不能将 ref 或 out 值作为字段。编译器可以通过在调用程序中分配单个元素数组、将参数放入其中并将该数组传递到迭代器来轻松实现对迭代器的引用参数,并让迭代器对 array[0] 进行操作。相比将迭代器转换为状态机,这对于编译器来说是非常少量的工作。 - Jim Balter
@JimBalter 如果编译器控制了运行的每一段代码,那么这就是真的。不幸的是,这个计划需要在二进制文件中生成一个不同的API签名——也就是说,来自外部世界的调用者传递“ref”变量时将无法看到它们的变化。 - Mehrdad Afshari
1
嗯,来自外部世界的调用者目前无法传递任何 ref 参数...没有人可以。因此,在迭代器中指定 ref T 参数实际上表示为 T[] 是没有害处的。无论如何,使用数组或类实例作为盒子是绕过限制的简单方法...比这里提供的其他解决方案更简单。 - Jim Balter
@JimBalter 如果您允许ref T,那就是全部意义所在。 - Mehrdad Afshari
1
如果C#生成带有T[]的签名,那么外部调用者不能使用ref T来调用它,就像现在一样。当人们谈论“整个重点”时,总是让我感到很有趣,因为这完全忽略了其他人所说的内容。当我遇到这种情况时,我对进一步讨论没有兴趣...结束。 - Jim Balter

26

如果你想从方法中返回迭代器和整数,一个解决方法是:

public class Bar : IFoo
{
    public IEnumerable<int> GetItems( ref int somethingElse )
    {
        somethingElse = 42;
        return GetItemsCore();
    }

    private IEnumerable<int> GetItemsCore();
    {
        yield return 7;
    }
}

需要注意的是,在迭代器方法中(即基本上包含yield returnyield break的方法),直到在枚举器中调用MoveNext() 方法,该方法内部的任何代码都不会执行。因此,如果您能够在迭代器方法中使用outref,则可能会出现意外行为,例如:

// 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

4
关于使用getter/setter lambdas的编辑,这是一种模拟指向值类型的指针的方式(当然不包括地址操作),更多信息请参见:http://incrediblejourneysintotheknown.blogspot.com/2008/05/pointers-to-value-types-in-c.html。 - Daniel Earwicker
比传递getter和setter更简单的方法是将“ref”参数放入一个盒子中——如果您有多个“ref”参数,则是一个类实例,如果只有一个,则是一个单元素数组。 - Jim Balter

6

其他人已经解释了为什么您的迭代器不能有一个引用参数。这里有一个简单的替代方案:

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;
    }
}

如果你需要传入和传出多个项目,请定义一个类来保存它们。

5

在较高层面上,引用变量可以指向许多位置,包括位于堆栈上的值类型。通过调用迭代器方法创建迭代器的时间和分配ref变量的时间是两个非常不同的时间。无法保证最初通过引用传递的变量仍然存在于迭代器实际执行时。因此,这是不允许的(或不可验证的)。


2

当我需要返回从迭代项派生的值时,我使用函数来解决这个问题:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接