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

发布于 2024-07-24 19:12:33 字数 521 浏览 12 评论 0原文

我今天早些时候尝试过:

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            

    }
}

这背后的理由是什么?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

拥抱影子 2024-07-31 19:12:33

C# 迭代器内部是状态机。 每次你yield return某些东西时,你离开的地方应该与局部变量的状态一起保存,以便你可以返回并从那里继续。

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

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

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.

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.

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.

月隐月明月朦胧 2024-07-31 19:12:33

如果您想从方法中返回迭代器和 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 或 < code>yield break) 会一直执行,直到调用 Enumerator 中的 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

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

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:

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

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
爱冒险 2024-07-31 19:12:33

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

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)

北斗星光 2024-07-31 19:12:33

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

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

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

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.

久而酒知 2024-07-31 19:12:33

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

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

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 );
}
影子的影子 2024-07-31 19:12:33

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

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

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

You can easily replace the ref param with something like

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

Which is something I've done in environments where we don't care so much about performance and instead are optimizing mostly for maintainability (provided you can't just change the param to a reference type).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文