C#闭包是否支持非本地回报?

时间:2017-09-03 15:34:53

标签: c# closures

这里解释一下Scala代码:

object Scratch {
    def foo: Int = {
        val list = List(1, 2, 3, 4)
        list.foreach { each =>
            if(each > 2) {
                return each
            }
          println(each)
    }
    return 5   
}


def main(args : Array[String]) : Unit = {
    val i = foo
    println("i: " + i)   
}

上面的代码将其打印到控制台:

1
2
i: 3

请注意,list.foreach使用的关闭具有return语句,而此return语句会导致foolist.foreach的调用者,返回,中断foreach枚举并提供foo的实际返回值。此return未在foo方法本身中声明,因此是“非本地返回”。

现在的问题是如何在C#中产生同样的结果,例如:将其他东西打印到控制台?问题是关于C#内部闭包的非本地回报。只有支持它们时才会出现与上面代码相​​同的输出。

注意:这不是Scala的特色。其他语言也有像Smalltalk或Kotlin,可能还有Ruby和其他人肯定。

我想自己尝试一下,但是在下载9 GB用于安装Visual Studio 2017后,我被告知要升级到Windows 10,这不是我想要做的只是为了回答这个问题。

2 个答案:

答案 0 :(得分:1)

不,C#不支持闭包中的非本地返回。 C#闭包本身就是一种方法,它不与其封闭方法共享上下文(捕获变量除外)。当您从lambda表达式返回时,您将从返回方法,即lambda引用的匿名方法。它不会影响声明lambda的方法,也不会影响lambda的调用方法(如果与声明lambda的方法不同)。

我对Scala或Ruby并不熟悉,但看起来Scala更像是Ruby而不是C#。如果是这样,我认为非本地返回会导致调用方法返回。在你的例子中,调用方法与声明方法相同,但是由于显而易见的原因,lambda导致声明方法返回会很奇怪。即声明方法已经返回后,可能会调用lambda。在Stack Overflow问题Is Ruby's code block same as C#'s lambda expression?上有更深入的讨论Ruby(以及推理,Scala)。

当然,您仍然可以在C#中实现相同的效果。只是你正在使用的确切语法不会这样做。在.NET中,List<T>泛型类具有ForEach()方法,因此从字面上理解您的代码示例(即使用内置的ForEach()方法),这是您可以进入的最接近的方法C#:

static void Main(string[] args)
{
    var i = foo();
    WriteLine($"i: {i}");
}

static int foo()
{
    var list = new List<int> { 1, 2, 3, 4 };

    try
    {
        list.ForEach(each =>
        {
            if (each > 2)
            {
                throw new LocalReturnException(each);
            }
            WriteLine(each);
        });
    }
    catch (LocalReturnException e)
    {
        return e.Value;
    }

    return 5;
}

class LocalReturnException : Exception
{
    public int Value { get; }

    public LocalReturnException(int value)
    {
        Value = value;
    }
}

因为List<T>.ForEach()方法没有提供任何机制来中断其枚举源枚举,所以让方法过早返回的唯一方法是通过抛出异常来绕过正常的方法返回机制。 / p>

当然,例外情况相当重。 try / catch处理程序只有一个边际成本,实际投掷和捕获一个非常代价高昂。如果您需要这个习惯用法,最好创建自己的枚举方法,该方法提供了一种中断枚举和返回值的机制。例如,创建类似的扩展方法:

public static T? InterruptableForEach<T>(this IEnumerable<T> source, Func<T, T?> action)
   where T : struct
{
    foreach (T t in source)
    {
        T? result = action(t);

        if (result != null) return result;
    }

    return null;
}

public static T InterruptableForEach<T>(this IEnumerable<T> source, Func<T, T> action)
    where T : class
{
    foreach (T t in source)
    {
        T result = action(t);

        if (result != null) return result;
    }

    return null;
}

您的示例需要第一个。我展示了两个,因为当涉及到int值时,C#将null等值类型与引用类型区别对待,但此处并不严格需要第二个。

使用扩展方法,您可以执行以下操作:

static int foo()
{
    var list = new List<int> { 1, 2, 3, 4 };

    var result = list.InterruptableForEach(each =>
    {
        if (each > 2)
        {
            return each;
        }
        WriteLine(each);
        return null;
    });

    return result ?? 5;
}

请注意,调用者需要配合lambda和extension方法。也就是说,扩展方法显式地报告lambda本身返回的内容,以便它知道lambda是否过早返回,如果是,则该值是什么。

一方面,这比Scala版本更笨拙和冗长。另一方面,它与C#的显性和表达倾向一致,并避免模糊情境(例如,如果foo()方法没有返回int,但lambda做了什么呢?)

This answer显示了另一种可能的方法。我个人更喜欢上述任何一个,因为它们实际上中断枚举,而不是直到跳过主lambda主体直到枚举结束(这可能是无限枚举的问题),并且不要引入该答案所需的其他捕获变量。但它确实适用于你的例子。

答案 1 :(得分:0)

这是我对你问题的回答:

1) 你不需要为C#和LINQ安装任何东西。您只需使用https://dotnetfiddle.net

即可

2)这是我的代码https://dotnetfiddle.net/3V8vBj

using System.Collections.Generic; 

public class Program
{

    static int foo()
    {
        var list = new List<int> { 1, 2, 3, 4 };
        int ret = 5;
        bool keepGoing = true;
        list.ForEach(each =>
                 {
                    if (!keepGoing)
                         return;
                     if (each>2)
                     {
                        ret=each;
                        keepGoing = false;
                        return;
                     }

                     System.Console.WriteLine(each);

                 });
        return ret;
    }
    public static void Main(string[] args)
    {
        var i = foo();
        System.Console.WriteLine("i: " + i);

    }
}

3)当你使用LINQ Foreach时,你不能简单地将它分解为返回,因为它是一个委托函数。所以我必须实施keepGoing。

4)嵌套的委托函数不能返回值,所以我必须使用&#34; ret&#34;用于在LINQ Foreach中设置返回值。

我希望这能回答你的问题,但我并不确定我是否理解它。