Почему экземпляры делегатов не всегда кэшируются? - PullRequest
2 голосов
/ 17 февраля 2020

Почему компилятор C# не создает код, который кэширует экземпляр делегата Action(SomeMethod) в этом случае:

void MyMethod() {
   Decorator(SomeMethod);
}

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

Он делает это только тогда, когда SomeMethod установлен c:

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

Итак, мой вопрос был: почему компилятор не может сгенерировать подобный код? Я не вижу никаких побочных эффектов.

Я не говорю, что нет никаких причин, люди, работающие над компилятором, намного умнее, чем я, возможно, когда-либо буду. Я только хочу понять, какой сценарий ios это не сработает.

1 Ответ

2 голосов
/ 18 февраля 2020

Он не может кэшировать его для экземпляров методов, поскольку целевой экземпляр является частью делегата , и он действительно хочет использовать данные c поле для кеша. Вызов метода stati c, который не захватывает никакие переменные и т. Д. c, может быть кэширован очень дешево, но становится намного сложнее, когда задействовано состояние, и this считается состоянием.

Да, я полагаю, можно использовать поле экземпляра для кэширования () => this.SomeMethod(), но, честно говоря, this, являющийся целью, является относительно редким случаем и не решает общую проблему.

Однако, это также только делает это для лямбда синтаксиса, т.е. даже если SomeMethod равен static

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

Вы можете увидеть разницу здесь

Это это потому, что разница обнаружима (разные ссылки на объекты и те же ссылки на объекты) и может в теории привести к другому поведению программы в существующем коде, который использовал оригинальный (не лямбда) синтаксис; таким образом, положение с кэшем до настоящего времени не применялось ретроспективно к старому синтаксису. Причины совместимости. Это обсуждалось годами, хотя; 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
...