Попытаюсь немного осветить такую мутную тему, как кэширование экземпляров делегатов компилятором С#.

Не смотря на то, что делегаты нынче вызываются приблизительно так же быстро, как вызовы методов через интерфейс, существует проблема производительности создания экземпляров делегатов. Вообще говоря, “проблема” - это как-то громко звучит, на самом деле заметная деградация производительности может проявиться только на performance critical участках кода, в обычной разработке думать об этом вовсе не следует. Много ссылок по данной теме выкладывали недавно в этом топике на rsdn.

Интерес представляет поведение и генерируемый компилятором код, поэтому рассмотрим такой метод, содержащий лямбда-выражение:

static IEnumerable<Person> FilterDevelopers(this IEnumerable<Person> source) {
  return source.Where(x => x.IsDeveloper);
}

Эта запись для многих выглядит очень “натурально” и как-то совершенно забывается, что на самом деле здесь создаётся экземпляр типа делегата:

static IEnumerable<Person> FilterDevelopers(this IEnumerable<Person> source) {
  return source.Where(new Func<Person, bool>(x => x.IsDeveloper));
}

В данном примере лямбда-выражение не замыкается на какие-либо внешние переменные, this или поля, поэтому оно может быть (и будет) эффективно скомпилировано в виде обычного статического метода. Тогда возникает вопрос - зачем каждый раз создавать экземпляр делегата? Ведь делегаты в .NET являются неизменяемыми и несколько экземпляров делегатов на один и тот же статический метод абсолютно взаимозаменяемы. Компилятор C# использует это знание и применяет в данном случае кэширование экземпляра в статическом поле, реально скомпилированный код выглядит примерно вот так:

[CompilerGenerated]
static Func<Person, bool> CS9_CachedAnonymousMethodDelegate1;

static IEnumerable<Person> FilterDevelopers(this IEnumerable<Person> source) {
  return source.Where(
    CS9_CachedAnonymousMethodDelegate1 != null
      ? CS9_CachedAnonymousMethodDelegate1
      : (CS9_CachedAnonymousMethodDelegate1 =
                  new Func<Person, bool>(x => x.IsDeveloper)));
}

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

Долгое время я считал, что в случае появления любого замыкания, кэширование становится неприменимо (экземпляры делегатов из лямбда-выражений/анонимных методов, замыкающихся на внешний контекст, не являются взаимозаменяемыми). Однако оказалось, что статические методы - не единственный случай кэширования, подробнее - в следующих постах.

Важно помнить, что описанное кэширование - это implementation detail компилятора в чистом виде, ни в коем случае нельзя строить логику на описанных выше эффектах (ссылочное равенство создаваемых делегатов), хотя это может открыть некоторые забавные возможности…