Почему С# реализует анонимные методы и блокировки как методы экземпляра, а не как статические методы?

Поскольку я не являюсь экспертом по языкам программирования, мне хорошо известно, что это может быть глупый вопрос, но насколько я могу судить, С# обрабатывает анонимные методы и замыкания, превращая их в методы экземпляра анонимного вложенного класса [1], создавая экземпляр этого класса, а затем указывая делегаты на эти методы экземпляра.

Похоже, что этот анонимный класс может быть однажды создан только один раз (или я ошибаюсь в этом?), так почему бы не сделать анонимный класс статическим?


[1] На самом деле, он похож на один класс для закрытий и один для анонимных методов, которые не фиксируют какие-либо переменные, что я не совсем понимаю обоснование для этого.

Ответ 1

Я хорошо знаю, что это может быть глупый вопрос

Это не так.

С# обрабатывает анонимные методы и замыкания, введя их в методы экземпляра анонимного вложенного класса, создавая экземпляр этого класса, а затем указывая делегаты на эти методы экземпляра.

С# иногда это делает.

Похоже, что этот анонимный класс может быть однажды создан только один раз (или я ошибаюсь в этом?), так почему бы не сделать анонимный класс статическим?

В тех случаях, когда это будет законным, С# делает вас лучше. Это не делает класс закрытия вообще. Это делает анонимную функцию статической функцией текущего класса.

И да, вы ошибаетесь в этом. В случаях, когда вы можете уйти с распределением делегата только один раз, С# уходит с ним.

(Это, строго говоря, не совсем верно, есть некоторые неясные случаи, когда эта оптимизация не реализована, но по большей части она есть.)

На самом деле, он выглядит как один класс для замыканий и один для анонимных методов, которые не фиксируют какие-либо переменные, что я не совсем понимаю обоснование для.

Вы положили пальцем на то, что не понимаете.

Посмотрим на некоторые примеры:

class C1
{
  Func<int, int, int> M()
  {
    return (x, y) => x + y;
  }
}

Это может быть сгенерировано как

class C1
{
  static Func<int, int, int> theFunction;
  static int Anonymous(int x, int y) { return x + y; }
  Func<int, int, int> M()
  {
    if (C1.theFunction == null) C1.theFunction = C1.Anonymous;
    return C1.theFunction;
  }
}

Не требуется новый класс.

Теперь рассмотрим:

class C2
{
  static int counter = 0;
  int x = counter++;
  Func<int, int> M()
  {
    return y => this.x + y;
  }
}

Вы понимаете, почему это невозможно создать с помощью статической функции? Статической функции нужен доступ к this.x, но где this в статической функции? Существует не один.

Итак, эта функция должна быть функцией экземпляра:

class C2
{
  static int counter = 0;
  int x = counter++;
  int Anonymous(int y) { return this.x + y; }
  Func<int, int> M()
  {
    return this.Anonymous;
  }
}

Кроме того, мы не можем кэшировать делегат в статическом поле; вы видите, почему?

Упражнение: может ли делегировать кеширование в поле экземпляра? Если нет, то что мешает этому быть законным? Если да, то каковы некоторые аргументы против реализации этой "оптимизации"?

Теперь рассмотрим:

class C3
{
  static int counter = 0;
  int x = counter++;
  Func<int> M(int y)
  {
    return () => x + y;
  }
}

Это не может быть сгенерировано как функция экземпляра C3; вы видите, почему? Нам нужно сказать:

var a = new C3();
var b = a.M(123);
var c = b(); // 123 + 0
var d = new C3();
var e = d.M(456);
var f = e(); // 456 + 1
var g = a.M(789);
var h = g(); // 789 + 0

Теперь делегаты должны знать не только значение this.x, но и значение y, которое было передано. Это нужно где-то хранить, поэтому мы сохраняем его в поле. Но это не может быть поле C3, потому что тогда как мы можем сказать b использовать 123 и g для использования 789 для значения y? Они имеют один и тот же экземпляр C3, но два разных значения для y.

class C3
{
  class Locals
  {
    public C3 __this;
    public int __y;
    public int Anonymous() { return this.__this.x + this.__y; }
  }
  Func<int> M(int y)
  {
    var locals = new Locals();
    locals.__this = this;
    locals.__y = y;
    return locals.Anonymous;
  }
}

Упражнение. Предположим теперь, что у нас есть C4<T> с общим методом M<U>, где лямбда замкнута над переменными типов T и U. Опишите кодеген, который должен произойти сейчас.

Упражнение. Предположим, что у нас есть M, возвращающий набор делегатов, один из которых ()=>x + y, а другой - (int newY)=>{ y = newY; }. Опишите кодеген для двух делегатов.

Упражнение. Теперь предположим, что M(int y) возвращает тип Func<int, Func<int, int>>, и мы возвращаем a => b => this.x + y + z + a + b. Опишите кодеген.

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

Упражнение. Поместите их вместе. Как вы делаете codegen для нескольких вложенных lambdas с getter и setter lambdas для всех локальных пользователей, параметризованных родовыми типами в области класса и метода, которые выполняют вызовы base? Потому что проблема, которую мы на самом деле должны были решить.