为什么 lambda 表达式不被“interned”?

发布于 2024-10-14 06:52:24 字数 1989 浏览 3 评论 0原文

字符串是引用类型,但它们是不可变的。这允许编译器保留它们;只要出现相同的字符串文字,就可能引用相同的对象。

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

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

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

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

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

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

我问是因为 第 6.5.1 节C# 4.0语言规范明确指出:

语义相同的转换 具有相同功能的匿名函数 (可能为空)捕获的外部集合 变量实例相同 允许委托类型(但不允许 必需)返回相同的委托 实例。该术语在语义上 这里使用相同的意思是 匿名函数的执行 在所有情况下都会产生相同的结果 给出相同参数的效果。

当我读到它时,这让我感到惊讶;如果这种行为被明确允许,我会期望它能够实现。但似乎并非如此。事实上,这让很多开发人员陷入了麻烦,尤其是。当 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();
    }
}

没有实现此行为(语义相同的匿名方法的一个委托实例)是否有任何原因?

Strings are reference types, but they are immutable. This allows for them to be interned by the compiler; everywhere the same string literal appears, the same object may be referenced.

Delegates are also immutable reference types. (Adding a method to a multicast delegate using the += operator constitutes assignment; that's not mutability.) And, like, strings, there is a "literal" way to represent a delegate in code, using a lambda expression, e.g.:

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

The right-hand side of that statement is an expression whose type is Func<int>; but nowhere am I explicitly invoking the Func<int> constructor (nor is an implicit conversion happening). So I view this as essentially a literal. Am I mistaken about my definition of "literal" here?

Regardless, here's my question. If I have two variables for, say, the Func<int> type, and I assign identical lambda expressions to both:

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

...what's preventing the compiler from treating these as the same Func<int> object?

I ask because section 6.5.1 of the C# 4.0 language specification clearly states:

Conversions of semantically identical
anonymous functions with the same
(possibly empty) set of captured outer
variable instances to the same
delegate types are permitted (but not
required) to return the same delegate
instance. The term semantically
identical is used here to mean that
execution of the anonymous functions
will, in all cases, produce the same
effects given the same arguments.

This surprised me when I read it; if this behavior is explicitly allowed, I would have expected for it to be implemented. But it appears not to be. This has in fact gotten a lot of developers into trouble, esp. when lambda expressions have been used to attach event handlers successfully without being able to remove them. For example:

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

Is there any reason why this behavior—one delegate instance for semantically identical anonymous methods—is not implemented?

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

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

发布评论

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

评论(6

丶情人眼里出诗心の 2024-10-21 06:52:24

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 显示它确实是< /em> 缓存在静态字段中)。第二个操作为两次调用 GetSecondAction 创建了两个不相等的 Action 实例,这就是为什么“second”最后不为 null。

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

You're mistaken to call it a literal, IMO. It's just an expression which is convertible to a delegate type.

Now as for the "interning" part - some lambda expressions are cached , in that for one single lambda expression, sometimes a single instance can be created and reused however often that line of code is encountered. Some are not treated that way: it usually depends on whether the lambda expression captures any non-static variables (whether that's via "this" or local to the method).

Here's an example of this caching:

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

In this case we can see that the first action was cached (or at least, two equal delegates were produced, and in fact Reflector shows that it really is cached in a static field). The second action created two unequal instances of Action for the two calls to GetSecondAction, which is why "second" is non-null at the end.

Interning lambdas which appear in different places in the code but with the same source code is a different matter. I suspect it would be quite complex to do this properly (after all, the same source code can mean different things in different places) and I would certainly not want to rely on it taking place. If it's not going to be worth relying on, and it's a lot of work to get right for the compiler team, I don't think it's the best way they could be spending their time.

兔姬 2024-10-21 06:52:24

是否有任何原因导致这种行为(语义相同的匿名方法的一个委托实例)未实现?

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

Is there any reason why this behavior—one delegate instance for semantically identical anonymous methods—is not implemented?

Yes. Because spending time on a tricky optimization that benefits almost no one takes time away from designing, implementing, testing and maintaining features that do benefit people.

韶华倾负 2024-10-21 06:52:24

其他答案提出了很好的观点。我的确实与任​​何技术无关 - 每个功能以 -100 点开始

The other answers bring up good points. Mine really doesn't have to do with anything technical - Every feature starts out with -100 points.

无敌元气妹 2024-10-21 06:52:24

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

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

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

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

In general, there is no distinction between string variables which refer to the same String instance, versus two variables which refer to different strings that happen to contain the same sequence of characters. The same is not true of delegates.

Suppose I have two delegates, assigned to two different lambda expressions. I then subscribe both delegates to an event handler, and unsubscribe one. What should be the result?

It would be useful if there were a way in vb or C# to designate that an anonymous method or lambda which does not make reference to Me/this should be regarded as a static method, producing a single delegate which could be reused throughout the life of the application. There is no syntax to indicate that, however, and for the compiler to decide to have different lambda expressions return the same instance would be a potentially breaking change.

EDIT I guess the spec allows it, even though it could be a potentially breaking change if any code were to rely upon the instances being distinct.

吻泪 2024-10-21 06:52:24

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

This is allowed because the C# team cannot control this. They heavily rely on the implementation details of delegates (CLR + BCL) and the JIT compiler's optimizer. There already is quite a proliferation of CLR and jitter implementations right now and there is little reason to assume that's going to end. The CLI spec is very light on rules about delegates, not nearly strong enough to ensure all these different teams will end up with an implementation that guarantees that delegate object equality is consistent. Not in the least because that would hamper future innovation. There's lots to optimize here.

快乐很简单 2024-10-21 06:52:24

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

Lambdas can't be interned because they use an object to contain the captured local variables. And this instance is different every time you you construct the delegate.

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