Если вы посмотрите на IL-тип делегата в Reflector или ILSpy, вы увидите, что он выглядит примерно так:
.class public sealed System.Action extends System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed {}
.method public hidebysig newslot virtual instance void Invoke() runtime managed {}
.method public hidebysig newslot virtual instance class System.IAsyncResult BeginInvoke(class System.AsyncCallback callback, object 'object') runtime managed {}
.method public hidebysig newslot virtual instance void EndInvoke(class System.IAsyncResult result) runtime managed {}
}
, то есть методы конструктора (.ctor
), Invoke
и BeginInvoke/EndInvoke
. Вы также заметите, что эти методы не имеют реализации (тела методов пусты) и помечены runtime
.
Ключевое слово runtime
указывает CLR, что этот метод нуждается в реализации, предоставляемой самим CLR. То есть реализация делегата является полностью магической внутри самого CLR. Когда тип делегата загружен, CLR замечает, что он получен из System.Delegate
, замечает флаг runtime
и создает реализации этих методов внутри CLR для этого конкретного типа делегата.
То, как эти реализации на самом деле выглядят, полностью зависит от CLR, на котором вы его запускаете (будь то платформа .NET, Mono или что-то еще), но, скорее всего, будет непосредственно в нативном коде.
Когда компилятор компилирует тип делегата, он просто создает эти заглушки методов для соответствия этому шаблону, ожидаемому CLR, и оставляет его при этом. Как на самом деле работает делегат, зависит от времени выполнения.