【问题标题】:Why are lambda expressions not "interned"?为什么 lambda 表达式不是“实习的”?
【发布时间】:2011-06-16 00:30:29
【问题描述】:

字符串是引用类型,但它们是不可变的。这允许它们被编译器实习;任何地方出现相同的字符串文字,都可能引用相同的对象。

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

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

该语句的右侧是一个类型为Func&lt;int&gt; 的表达式;但我没有在任何地方显式调用 Func&lt;int&gt; 构造函数(也没有发生隐式转换)。所以我认为这本质上是一个文字。我在这里对“字面”的定义有误吗?

无论如何,这是我的问题。如果我有两个变量,例如 Func&lt;int&gt; 类型,并且我为这两个变量分配了相同的 lambda 表达式:

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

...是什么阻止编译器将它们视为相同的 Func&lt;int&gt; 对象?

我问是因为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();
    }
}

这种行为——语义相同的匿名方法的委托实例——没有实现有什么原因吗?

【问题讨论】:

  • 即使它有效,我仍然认为通过复制 lambda 中的代码来分离事件处理程序是个坏主意。
  • 我怀疑这将是另一个“因为预算无法用于设计、实施、测试、记录、发布和维护”的好东西。也许 Eric Lippert 可以提供一些内幕消息。
  • 好问题。我假设在代码中看到的 lambda 可能看起来与另一个相同,但由于外部闭包等引用差异,它不会在两个地方编译为相同的 MSIL。编译器必须将其编译为 MSIL 才能发现差异,而不是能够从源代码中“即时”发现字符串文字。由于这是任何 lambda 都需要的额外步骤,并且只能提供很小的尺寸节省和几乎没有的性能提升,因此他们可能只是跳过了它。
  • @LukeH:我同意你的怀疑;我认为让我失望的主要原因是他们在规范中明确指出了这种可能性。也许他们只是认为如果他们有时间可以选择的话会很好。但我觉得如果他们不感兴趣的话,完全省略那部分就好了。
  • @Martinho:我实际上同意你的看法。我只是觉得在规范中看到这一点很奇怪,所以我想我会抛出明显的为什么不呢?

标签: c# .net lambda delegates expression


【解决方案1】:

你错误地称它为文字,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表明它确实缓存了在静态字段中)。第二个操作为对GetSecondAction 的两次调用创建了两个不相等的Action 实例,这就是为什么“second”最后不为空的原因。

出现在代码中不同位置但具有相同源代码的实习 lambda 是另一回事。我怀疑正确地执行此操作会非常复杂(毕竟,相同的源代码在不同的地方可能意味着不同的东西),我当然不想依赖它发生。如果它不值得依赖,而且编译器团队需要做很多工作才能正确,我认为这不是他们花费时间的最佳方式。

【讨论】:

  • “相同的源代码”不是规范中定义的“语义相同”。 (x,y) =&gt; x + y 在某些情况下可能与(a,b) =&gt; a + b“语义相同”(没有本地变量、没有 this、相同的类型等)。我知道这种语义等价可能充满了极端情况并且难以检测,因此导致实现采用简单的方法。
  • ()=>5 在源代码中的一个位置不等于 ()=>5 在其他位置。每个产生的代表都需要比较不平等,如果他们被实习就不会发生这种情况。
  • @supercat:不,我不这么认为——假设委托类型是相同的,我相信问题中提到的规范是说他们不会 需要比较不等。
  • @supercat:他们需要比较不平等吗?如果是这样的话,那么基本上就在那里结束讨论。这是在哪里指定的?
  • @Dan:如果没有被禁止,如果结果证明有显着的好处,总是可以稍后再做。如果那扇门一开始就被规范关闭,那么将来就没有回旋余地了。不过只是猜测。
【解决方案2】:

这种行为——语义相同的匿名方法的一个委托实例——没有实现有什么原因吗?

是的。因为将时间花在几乎没有人受益的棘手优化上会占用设计、实施、测试和维护确实有益于人们的功能的时间。

【讨论】:

  • 这基本上证实了 Jon、LukeH 和可能其他人已经提出的建议;让我质疑这一点的原因仅仅是规范中完全提到了该功能的可能性。但正如乔恩(和其他人)指出的那样,这可能只是为了避免不必要地关门。汉斯也给出了一个相当有说服力的解释。无论如何,一如既往地感谢您插话我猜只能称为权威答案!
【解决方案3】:

其他答案提出了很好的观点。我的确实与任​​何技术无关 - Every feature starts out with -100 points

【讨论】:

    【解决方案4】:

    一般来说,引用相同字符串实例的字符串变量与引用恰好包含相同字符序列的不同字符串的两个变量之间没有区别。代表则不然。

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

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

    编辑我猜规范允许这样做,即使如果任何代码依赖于不同的实例,这可能是一个潜在的破坏性变化。

    【讨论】:

    • public static readonly Func&lt;int&gt; TheDeepThoughtLambda = () =&gt; 42;:P
    • 依赖于不同实例的代码依赖于当前的实现细节而无视规范。如果现在实施,并且对规范没有任何更改,您会称其为破坏性更改,因为不合格的代码损坏了吗?
    【解决方案5】:

    这是允许的,因为 C# 团队无法控制它。它们严重依赖委托的实现细节(CLR + BCL)和 JIT 编译器的优化器。现在已经有大量的 CLR 和抖动实现,没有理由假设这会结束。 CLI 规范对委托的规则非常简单,不足以确保所有这些不同的团队最终都会得到一个保证委托对象相等性一致的实现。至少不会,因为这会阻碍未来的创新。这里有很多需要优化的地方。

    【讨论】:

      【解决方案6】:

      Lambda 不能被实习,因为它们使用一个对象来包含捕获的局部变量。而且每次构造委托时,这个实例都是不同的。

      【讨论】:

      • 并非所有的 lambdas 都捕获局部变量。丹涛的例子就是其中之一。
      • 不是全部,但足够多的优化可能不值得。并且代码永远不应该依赖他们被实习的正确性,因为 IMO 这只是一种优化,而不是合同。特别是如果实现了优化,它将使 OPs 代码在某些实现中工作,但在其他实现中不起作用。一旦捕获变量,它就会突然停止工作。因此,它可能会比当前的行为欺骗更多的开发人员。
      • 另外,我从规范中引用的部分表明,如果同一方法中的两个 lambda 表达式捕获 same 局部变量,那么它们在语义上是相同的,并且可以支持相同的委托实例。请注意,我不是在谈论 在方法调用之间,而是在单个方法调用中,两个表达式可以使用同一个对象。
      • @Dan 因为它是规范的可选功能,您将依赖实现定义的行为来执行此操作。我宁愿避免这种情况。 supercat 的示例同时订阅同一个事件,揭示了该优化产生的更多语义差异。
      • 你是对的。我并不是说开发人员依赖这个实现是合理的;我只包含示例代码来演示当前实现 这样做。但是,是的,我同意你的看法。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-24
      相关资源
      最近更新 更多