【问题标题】:Why can't iterator methods take either 'ref' or 'out' parameters?为什么迭代器方法不能采用“ref”或“out”参数?
【发布时间】:2010-11-03 04:43:30
【问题描述】:

我今天早些时候尝试过:

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


【解决方案1】:

C# 迭代器在内部是状态机。每次你yield return某事,你离开的地方应该和局部变量的状态一起保存,这样你就可以从那里回来继续。

为了保持这种状态,C# 编译器创建一个类来保存局部变量和它应该继续的地方。不能将 refout 值作为类中的字段。因此,如果允许您将参数声明为 refout,则无法在我们停止时保留函数的完整快照。

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

【讨论】:

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

如果你想从你的方法中返回一个迭代器和一个 int,一个解决方法是这样的:

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

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

您应该注意,在调用 Enumerator 中的 MoveNext() 方法之前,不会执行迭代器方法(即基本上包含 yield returnyield break 的方法)中的任何代码。因此,如果您能够在迭代器方法中使用 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

【讨论】:

  • Re: 使用 getter/setter lambda 进行编辑,这是一种模拟指向值类型的指针的方式(当然,虽然没有地址操作),更多信息请参见:incrediblejourneysintotheknown.blogspot.com/2008/05/…
  • 比传递 getter 和 setter 更简单的方法是将“ref”参数放在一个盒子里——如果你有多个“ref”参数,就是一个类实例,或者如果你只有一个元素数组一。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

      当我需要返回的值来自迭代项时,我已经使用函数解决了这个问题:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-05-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多