为什么lambda表达式没有“实习”?

时间:2011-01-26 17:33:11

标签: c# .net lambda delegates expression

字符串是引用类型,但它们是不可变的。这允许编译器将它们 interned ;在出现相同的字符串文字的地方,可以引用相同的对象。

委托也是不可变的引用类型。 (使用+=运算符向多播委托添加方法构成赋值;这不是可变性。)而且,就像字符串一样,在代码中有一种“文字”方式来表示委托,使用lambda表达式,例如:

Func<int> func = () => 5;

该语句的右侧是一个类型为Func<int>的表达式;但我没有明确地调用Func<int>构造函数(也没有发生隐式转换)。所以我认为这基本上是一个文字。我在这里误解了我对“文字”的定义吗?

无论如何,这是我的问题。如果我有两个变量,例如Func<int>类型,我将相同的lambda表达式分配给两者:

Func<int> x = () => 5;
Func<int> y = () => 5;

...是什么阻止了编译器将它们视为同一个Func<int>对象?

我问,因为C# 4.0 language specification的第6.5.1节明确指出:

  

语义相同的转换   具有相同的匿名功能   (可能是空的)一组被捕获的外部   变量实例相同   允许委托类型(但不允许)   要求)返回同一个代表   实例。术语一词   这里使用的是相同的意思   执行匿名函数   在所有情况下,都将生产相同的产品   给出相同论点的效果。

当我读到它时,这让我感到惊讶;如果此行为明确允许,我原本希望它能够实现。但似乎并非如此。事实上,这已经让很多开发人员陷入困境,尤其是当lambda表达式用于成功附加事件处理程序而不能删除它们时。例如:

class EventSender
{
    public event EventHandler Event;
    public void Send()
    {
        EventHandler handler = this.Event;
        if (handler != null) { handler(this, EventArgs.Empty); }
    }
}

class Program
{
    static string _message = "Hello, world!";

    static void Main()
    {
        var sender = new EventSender();
        sender.Event += (obj, args) => Console.WriteLine(_message);
        sender.Send();

        // Unless I'm mistaken, this lambda expression is semantically identical
        // to the one above. However, the handler is not removed, indicating
        // that a different delegate instance is constructed.
        sender.Event -= (obj, args) => Console.WriteLine(_message);

        // This prints "Hello, world!" again.
        sender.Send();
    }
}

为什么没有实现这种行为 - 语义相同的匿名方法的一个委托实例 - 是否有任何理由?

6 个答案:

答案 0 :(得分:11)

你错误称它为文字,IMO。它只是一个可转换为委托类型的表达式。

现在对于“实习”部分 - 一些lambda表达式缓存,因为对于一个单个lambda表达式,有时可以创建并重用单个实例,但是经常会遇到这行代码。有些不是这样处理的:它通常取决于lambda表达式是否捕获任何非静态变量(无论是通过“this”还是本地方法)。

以下是此缓存的示例:

using System;

class Program
{
    static void Main()
    {
        Action first = GetFirstAction();
        first -= GetFirstAction();
        Console.WriteLine(first == null); // Prints True

        Action second = GetSecondAction();
        second -= GetSecondAction();
        Console.WriteLine(second == null); // Prints False
    }

    static Action GetFirstAction()
    {
        return () => Console.WriteLine("First");
    }

    static Action GetSecondAction()
    {
        int i = 0;
        return () => Console.WriteLine("Second " + i);
    }
}

在这种情况下,我们可以看到第一个操作被缓存(或者至少生成了两个相等的委托,实际上Reflector显示它确实 缓存在静态领域)。对于Action的两次调用,第二个操作为GetSecondAction创建了两个不相等的实例,这就是为什么“second”在结尾处为非null的原因。

在代码中的不同位置但具有相同源代码的实习lambda是另一回事。我怀疑这样做是非常复杂的(毕竟,相同的源代码在不同的地方可能意味着不同的东西)并且我当然不希望依赖来实现它。如果它不值得依赖,并且为编译器团队做好了很多工作,我认为这不是他们花时间的最佳方式。

答案 1 :(得分:6)

  

为什么没有实现这种行为 - 语义相同的匿名方法的一个委托实例 - 是否有任何理由?

是。因为花费时间进行棘手的优化,几乎没有人受益,因此设计,实施,测试和维护让人们受益的功能需要时间。

答案 2 :(得分:1)

Lambda无法实现,因为它们使用对象来包含捕获的局部变量。每次构造委托时,这个实例都不同。

答案 3 :(得分:1)

通常,引用相同String实例的字符串变量与引用包含相同字符序列的不同字符串的两个变量之间没有区别。代表们也是如此。

假设我有两个委托,分配给两个不同的lambda表达式。然后我将两个代理订阅到一个事件处理程序,并取消订阅一个。应该是什么结果?

如果在vb或C#中有一种方法可以指定一个匿名方法或不引用Me / this的lambda应该被视为一个静态方法,产生一个可以在整个过程中重用的单个委托将是有用的。申请的生命。但是,没有语法表明,并且编译器决定使用不同的lambda表达式返回相同的实例将是一个潜在的重大变化。

编辑我想规范允许它,即使它可能是一个潜在的重大变化,如果任何代码依赖于实例是不同的。

答案 4 :(得分:1)

其他答案带来了好处。我真的不需要任何技术问题 - Every feature starts out with -100 points

答案 5 :(得分:1)

这是允许的,因为C#团队无法控制它。它们严重依赖于委托(CLR + BCL)和JIT编译器的优化器的实现细节。目前已经有大量的CLR和抖动实现,没有理由认为它将会结束。 CLI规范对代表的规则非常清楚,并不足以确保所有这些不同的团队最终都有一个实现来保证委托对象的相等性是一致的。至少是因为这会妨碍未来的创新。这里有很多优化。