代码之家  ›  专栏  ›  技术社区  ›  Trap

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

  •  34
  • Trap  · 技术社区  · 15 年前

    今天早些时候我试过:

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

    这背后的理由是什么?

    5 回复  |  直到 15 年前
        1
  •  45
  •   Mehrdad Afshari    15 年前

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

    为了保持这种状态,C编译器创建了一个类来保存局部变量和它应该继续的位置。不可能有一个 ref out 值作为类中的字段。因此,如果允许将参数声明为 裁判 外面的 ,在我们离开时,将无法保留函数的完整快照。

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

        2
  •  15
  •   Rasmus Faber    15 年前

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

    编辑: 如果您真的想要移动迭代器将修改 裁判 -参数,可以这样做:

    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
  •  5
  •   JaredPar    15 年前

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

        4
  •  3
  •   Jim Balter    10 年前

    其他人解释了为什么迭代器不能有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
  •  1
  •   Tony Tanzillo    13 年前

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

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