为什么迭代器方法不能采用“ref”或“out”参数?

问题描述 投票:0回答:6

我今天早些时候尝试过:

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# parameters ref out
6个回答
62
投票

C# 迭代器内部是状态机。每次你

yield return
做某件事时,你停下来的地方都应该与局部变量的状态一起保存,以便你可以返回并从那里继续。

为了保持这种状态,C# 编译器创建一个类来保存局部变量及其应继续的位置。不可能将

ref
out
值作为类中的字段。因此,如果允许您将参数声明为
ref
out
,则无法保留我们停止时函数的完整快照。

编辑: 从技术上讲,并非所有返回

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


26
投票

如果您想从方法中返回迭代器和 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

6
投票

其他人已经解释了为什么你的迭代器不能有 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;
    }
}

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


5
投票

在较高级别上, ref 变量可以指向许多位置,包括堆栈上的值类型。最初通过调用 iterator 方法创建迭代器的时间和分配 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 );
}

0
投票

您可以轻松地将 ref 参数替换为类似的内容

public class ItRef<T> where T:struct
{
    public T Value;
}

这是我在不太关心性能的环境中所做的事情,而是主要针对可维护性进行优化(前提是您不能只将参数更改为引用类型)。

© www.soinside.com 2019 - 2024. All rights reserved.