【问题标题】:Why aren't delegate instances always cached?为什么委托实例不总是被缓存?
【发布时间】:2020-02-17 19:37:34
【问题描述】:

在这种情况下,为什么 C# 编译器不创建缓存 Action(SomeMethod) 的委托实例的代码:

void MyMethod() {
   Decorator(SomeMethod);
}

void Decorator(Action a) { a(); }
void SomeMethod() { }

仅当SomeMethod 为静态时才会这样做:

static void SomeMethod() { }

编辑:

为清楚起见,我们采用以下代码:

class A {
    public void M1() {
       var b = new B();
       b.Decorate(M2);
    }

    public void M2() {
    }
}


class B {
    public void Decorate(Action a) {
        Console.WriteLine("Calling");
        a();
    }
}

如果你想避免每次调用 M1 时分配委托,你可以很容易地做到,但它很丑:

using System;

class A {
    Action _m2;

    public A() {
        _m2 = new Action(M2);
    }

    public void M1() {
       var b = new B();
       b.Decorate(_m2);
    }

     public void M2() {
     }
}


class B {
    public void Decorate(Action a) {
        Console.WriteLine("Calling");
        a();
    }
}

所以我的问题是,编译器无法生成类似代码的原因是什么?我看不到任何副作用。

我并不是说没有理由,从事编译器工作的人可能比我永远聪明得多。我只是想了解哪些情况下这不起作用。

【问题讨论】:

  • 你做了哪些测试?
  • @Çöđěxěŕ 你可以用任何反编译器看看

标签: c# .net delegates


【解决方案1】:

它不能为 instance 方法缓存它,因为目标实例是委托的一部分,它真的想要使用缓存的静态字段。不捕获任何变量等的静态方法调用可以非常便宜地缓存,但是当涉及到状态时,它会变得更加复杂,this 也算作状态。

是的,我想是可以使用实例字段来缓存() => this.SomeMethod(),但坦率地说,this 作为目标是一种相对罕见的情况,并不能解决一般问题。

但是,它也只对 lambda 语法执行此操作,即即使 SomeMethodstatic

Decorator(SomeMethod); // not cached
Decorator(() => SomeMethod()); // cached

你可以看到区别here

这是因为差异是可检测的(不同的对象引用与相同的对象引用),并且可能理论上导致使用原始(非 lambda)语法的现有代码中的不同程序行为;因此,缓存条款迄今尚未追溯应用于旧语法。兼容性原因。不过,这已经讨论了多年。 IMO 是其中之一,例如更改 foreach L 值捕获,可能会在不破坏世界的情况下像我们想象的那样改变。


要查看基于已编辑问题的示例中的理论差异:

using System;

class A
{
    static void Main()
    {
        var obj = new A();
        Console.WriteLine("With cache...");
        for (int i = 0; i < 5; i++) obj.WithCache();
        Console.WriteLine("And without cache...");
        for (int i = 0; i < 5; i++) obj.WithoutCache();
    }
    Action _m2;
    B b = new B();
    public void WithCache() => b.Decorate(_m2 ??= M2);
    public void WithoutCache() => b.Decorate(M2);
    public void M2() => Console.WriteLine("I'm M2");
}

 class B
{
    private object _last;
    public void Decorate(Action a)
    {
        if (_last != (object)a)
        {
            a();
            _last = a;
        }
        else
        {
            Console.WriteLine("No do-overs!");
        }
    }
}

当前输出:

With cache...
I'm M2
No do-overs!
No do-overs!
No do-overs!
No do-overs!
And without cache...
I'm M2
I'm M2
I'm M2
I'm M2
I'm M2

【讨论】:

  • @DonBox:创建的每个实例都更大。使用 static 字段进行缓存对内存有线性成本,即缓存的委托数量。使用 instance 字段的缓存是 O(N*M),其中 N 是委托数,M 是实例数。您可能正在创建数百万个实例,每个实例有 50 个委托,这会花费大量内存而可能带来的好处很少(如果有的话)。这可能是一个非常大且令人不快的副作用。
  • “缓存条款迄今尚未追溯应用于旧语法” - 它已追溯应用于 ECMA 标准。该权限尚未在编译器中使用 - 尚未。
  • @DonBox:是的,这取决于“缓存的代理数量”和“每个实例调用它们的次数”之间的平衡。随着编译器按照它的方式运行,您始终可以采取措施自己为经常调用的代码缓存委托。如果编译器自动执行此操作,则您不想希望缓存它的情况会更难缓解。
  • @DonBox 查看编辑以了解如何观察差异的示例;强调:不应该影响任何合理的代码......
  • ...尤其是现在(正如@JonSkeet 指出的那样)规范使上面的代码不仅不合理,而且undefined(评论拆分为通知目的)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-01
  • 1970-01-01
  • 2014-04-25
  • 2012-03-23
相关资源
最近更新 更多