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

时间:2022-12-27 21:13:14

I tried this earlier today:

我今天早些时候试过这个:

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            

    }
}

What's the rationale behind this?

这背后的理由是什么?

5 个解决方案

#1


C# iterators are state machines internally. Every time you yield return something, the place where you left off should be saved along with the state of local variables so that you could get back and continue from there.

C#迭代器是内部的状态机。每次你返回一些东西时,你应该保留你离开的地方以及局部变量的状态,这样你就可以回来并从那里继续。

To hold this state, C# compiler creates a class to hold local variables and the place it should continue from. It's not possible to have a ref or out value as a field in a class. Consequently, if you were allowed to declare a parameter as ref or out, there would be no way to keep the complete snapshot of the function at the time we had left off.

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

EDIT: Technically, not all methods that return IEnumerable<T> are considered iterators. Just those that use yield to produce a sequence directly are considered iterators. Therefore, while the splitting the iterator into two methods is a nice and common workaround, it doesn't contradict with what I just said. The outer method (that doesn't use yield directly) is not considered an iterator.

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

#2


If you want to return both an iterator and an int from your method, a workaround is this:

如果要从方法返回迭代器和int,则解决方法是:

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

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

You should note that none of the code inside an iterator method (i.e. basically a method that contains yield return or yield break) is executed until the MoveNext() method in the Enumerator is called. So if you were able to use out or ref in your iterator method, you would get surprising behavior like this:

您应该注意,在调用Enumerator中的MoveNext()方法之前,迭代器方法中的所有代码(即基本上包含yield return或yield break的方法)都不会执行。因此,如果你能够使用out或ref in iterator方法,你会得到如下令人惊讶的行为:

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

This is a common pitfall, a related issue is this:

这是一个常见的陷阱,相关的问题是:

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

So a good pattern is to separate iterator methods into two parts: one to execute immediately and one that contains the code that should be lazily executed.

因此,一个好的模式是将迭代器方法分成两部分:一部分立即执行,另一部分包含应该延迟执行的代码。

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

EDIT: If you really want the behavior where moving the iterator would modify the ref-parameter, you could do something like this:

编辑:如果你真的想要移动迭代器的行为会修改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

#3


At a highish level, A ref variable can point to many locations including to value types that are on the stack. The time at which the iterator is initially created by calling the iterator method and when the ref variable would be assigned are two very different times. It is not possible to guarantee that the variable which originally was passed by reference is still around when the iterator actually executes. Hence it is not allowed (or verifiable)

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

#4


Others have explained why your iterator can't have a ref parameter. Here's a simple alternative:

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

If you have several items to pass in and out, define a class to hold them.

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

#5


I've gotten around this problem using functions, when the value that I need to return is derived from the iterated items:

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

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

#1


C# iterators are state machines internally. Every time you yield return something, the place where you left off should be saved along with the state of local variables so that you could get back and continue from there.

C#迭代器是内部的状态机。每次你返回一些东西时,你应该保留你离开的地方以及局部变量的状态,这样你就可以回来并从那里继续。

To hold this state, C# compiler creates a class to hold local variables and the place it should continue from. It's not possible to have a ref or out value as a field in a class. Consequently, if you were allowed to declare a parameter as ref or out, there would be no way to keep the complete snapshot of the function at the time we had left off.

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

EDIT: Technically, not all methods that return IEnumerable<T> are considered iterators. Just those that use yield to produce a sequence directly are considered iterators. Therefore, while the splitting the iterator into two methods is a nice and common workaround, it doesn't contradict with what I just said. The outer method (that doesn't use yield directly) is not considered an iterator.

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

#2


If you want to return both an iterator and an int from your method, a workaround is this:

如果要从方法返回迭代器和int,则解决方法是:

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

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

You should note that none of the code inside an iterator method (i.e. basically a method that contains yield return or yield break) is executed until the MoveNext() method in the Enumerator is called. So if you were able to use out or ref in your iterator method, you would get surprising behavior like this:

您应该注意,在调用Enumerator中的MoveNext()方法之前,迭代器方法中的所有代码(即基本上包含yield return或yield break的方法)都不会执行。因此,如果你能够使用out或ref in iterator方法,你会得到如下令人惊讶的行为:

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

This is a common pitfall, a related issue is this:

这是一个常见的陷阱,相关的问题是:

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

So a good pattern is to separate iterator methods into two parts: one to execute immediately and one that contains the code that should be lazily executed.

因此,一个好的模式是将迭代器方法分成两部分:一部分立即执行,另一部分包含应该延迟执行的代码。

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

EDIT: If you really want the behavior where moving the iterator would modify the ref-parameter, you could do something like this:

编辑:如果你真的想要移动迭代器的行为会修改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

#3


At a highish level, A ref variable can point to many locations including to value types that are on the stack. The time at which the iterator is initially created by calling the iterator method and when the ref variable would be assigned are two very different times. It is not possible to guarantee that the variable which originally was passed by reference is still around when the iterator actually executes. Hence it is not allowed (or verifiable)

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

#4


Others have explained why your iterator can't have a ref parameter. Here's a simple alternative:

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

If you have several items to pass in and out, define a class to hold them.

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

#5


I've gotten around this problem using functions, when the value that I need to return is derived from the iterated items:

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

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