【问题标题】:using yield in C# like I would in Ruby在 C# 中使用 yield 就像在 Ruby 中一样
【发布时间】:2011-02-27 10:05:16
【问题描述】:

除了在 Ruby 中将yield 用于迭代器之外,我还使用它来将控制权短暂地传递回调用者,然后再在被调用方法中恢复控制。我想在 C# 中做的是类似的。在测试类中,我想获取一个连接实例,创建另一个使用该连接的变量实例,然后将变量传递给调用方法,以便对其进行修改。然后我希望控制返回到被调用的方法,以便可以处理连接。我想我想要一个像 Ruby 一样的块/闭包。大致思路如下:

private static MyThing getThing()
{
    using (var connection = new Connection())
    {
        yield return new MyThing(connection);
    }
}

[TestMethod]
public void MyTest1()
{
    // call getThing(), use yielded MyThing, control returns to getThing()
    // for disposal
}

[TestMethod]
public void MyTest2()
{
    // call getThing(), use yielded MyThing, control returns to getThing()
    // for disposal
}

...

这在 C# 中不起作用; ReSharper 告诉我 getThing 的主体不能是迭代器块,因为 MyThing 不是迭代器接口类型。这绝对是真的,但我不想遍历某些列表。我猜如果我不使用迭代器,我不应该使用yield。知道如何在 C# 中实现这个块/闭包,这样我就不必用getThing() 正文中的代码将我的代码包装在MyTest1MyTest2、...中?

【问题讨论】:

    标签: c# closures dry yield


    【解决方案1】:

    您说您想使用 C# 的 yield 关键字,就像使用 Ruby 的 yield 关键字一样。您似乎对两者的实际操作有些困惑:两者绝对没有彼此有任何关系,您所要求的,根本不可能。

    C# yield 关键字不是 Ruby yield 关键字的 C# 等效项。事实上,没有相当于 C# 中的 Ruby yield 关键字。而 Ruby 等价于 C# 的 yield 关键字 not yield keyword,它是 Enumerator::Yielder#yield method(也别名为 @ 987654329@)。

    IOW,它用于返回迭代器的下一个元素。这是官方 MSDN 文档中的一个节选示例:

    public static IEnumerable Power(int number, int exponent) {
        var counter = 0;
        var result = 1;
        while (counter++ < exponent) {
            result *= number;
            yield return result; }}
    

    像这样使用它:

    foreach (int i in Power(2, 8)) { Console.Write("{0} ", i); }
    

    Ruby 等价物类似于:

    def power(number, exponent)
      Enumerator.new do |yielder|
        result = 1
        1.upto(exponent-1) { yielder.yield result *= number } end end
    
    puts power(2, 8).to_a
    

    在 C# 中,yield 用于为 调用者 产生一个 ,而在 Ruby 中,yield 用于产生控制 到一个块参数

    实际上,在 Ruby 中,yield 只是Proc#call 的一个快捷方式。

    想象一下,如果yield 不存在。您将如何在 Ruby 中编写 if 方法?它看起来像这样:

    class TrueClass
      def if(code)
        code.call
      end
    end
    
    class FalseClass
      def if(_); end
    end
    
    true.if(lambda { puts "It's true!" })
    

    这有点麻烦。在 Ruby 1.9 中,我们获得了 proc 字面量和 Proc#call 的快捷语法,这使它更好一点:

    class TrueClass
      def if(code)
        code.()
      end
    end
    
    true.if(->{ puts "It's true!' })
    

    然而,Yukihiro Matsumoto 注意到,绝大多数 高阶过程只接受一个 过程参数。 (特别是因为 Ruby 在语言中内置了多个控制流结构,否则需要多个过程参数,例如需要两个参数的 if-then-else 和需要 n 个参数的 case-when。)因此,他创建了一种专门的方法来传递一个程序参数:块。 (事实上​​,我们一开始就已经看到了这样的例子,因为Kernel#lambda实际上只是一个普通的方法,它接受一个块并返回一个Proc。)

    class TrueClass
      def if(&code)
        code.()
      end
    end
    
    true.if { puts "It's true!" }
    

    现在,因为我们只能将一个块传递给一个方法,所以我们真的不需要显式地命名变量,因为无论如何都不会有歧义:

    def if
      ???.() # But what do we put here? We don't have a name to call #call on!
    end
    

    但是,由于我们现在不再有可以向其发送消息的名称,因此我们需要其他方式。再一次,我们得到了 Ruby 非常典型的 80/20 解决方案之一:人们可能想要对块做很多事情:转换它,将它存储在一个属性中,将它传递给另一个方法,检查它,打印它……但是,far 最常见的做法是调用它。因此,matz 为这种常见情况添加了另一种专门的快捷语法:yield 表示“call 传递给方法的块”。因此,我们不需要名称:

    def if; yield end
    

    那么,什么 C# 相当于 Ruby 的 yield 关键字?好吧,让我们回到第一个 Ruby 示例,我们显式地将过程作为参数传递:

    def foo(bar)
      bar.('StackOverflow')
    end
    
    foo ->name { puts "Higher-order Hello World from #{name}!" }
    

    C# 等价物一模一样

    void Foo(Action<string> bar) => bar("StackOverflow")
    
    Foo(name => { Console.WriteLine("Higher-order Hello World from {0]!", name); })
    

    【讨论】:

      【解决方案2】:

      我可能会将委托传递给迭代器。

      delegate void Action(MyThing myThing);
      private static void forEachThing(Action action) 
      { 
          using (var connection = new Connection()) 
          { 
              action(new MyThing(connection));
          } 
      }
      

      【讨论】:

      • 请注意,您对Action 的定义是有缺陷的,它必须接受一个参数。
      【解决方案3】:

      您可以让GetThing 获取包含要执行的代码的委托,然后从其他函数传递匿名方法。

      【讨论】:

        【解决方案4】:

        C# 中的yield 专门用于返回迭代集合的位。具体来说,您的函数必须返回IEnumerable&lt;Thing&gt;IEnumerable 才能使yield 工作,并且它应该在foreach 循环内部使用。它是 c# 中非常特殊的构造,不能以您尝试的方式使用。

        我不确定是否还有其他可以使用的构造,可能是 lambda 表达式。

        【讨论】:

          【解决方案5】:

          你想要的是 lambda 表达式,比如:

          // not named GetThing because it doesn't return anything
          private static void Thing(Action<MyThing> thing)
          {
              using (var connection = new Connection())
              {
                  thing(new MyThing(connection));
              }
          }
          
          // ...
          // you call it like this
          Thing(t=>{
            t.Read();
            t.Sing();
            t.Laugh();
          });
          

          这捕获t 的方式与yield 在Ruby 中的捕获方式相同。 C# yield 不同,它构造了可以迭代的生成器。

          【讨论】:

          • 这太疯狂了!不过,效果很好。我以前可能见过Action,但我从来不知道它的用法。谢谢!
          • ActionFunc 是将 LINQ 粘合在一起的粘合剂,它们旨在表示任何 lambda 表达式(我相信最多 12 个左右的参数)。
          猜你喜欢
          • 1970-01-01
          • 2013-01-16
          • 1970-01-01
          • 2023-03-31
          • 2011-02-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-24
          • 2011-06-09
          相关资源
          最近更新 更多