На msdn прочитал такой абзац
A delegate is said to be bound to the method it represents. In
addition to being bound to the method, a delegate can be bound to an
object. The object represents the first parameter of the method, and
is passed to the method every time the delegate is invoked. If the
method is an instance method, the bound object is passed as the
implicit this parameter (Me in Visual Basic); if the method is static,
the object is passed as the first formal parameter of the method, and
the delegate signature must match the remaining parameters.
Объясните, пожалуйста, что это за объект, почему он представляет первый параметр метода (как это вообще понять?), и как он передается в первый формальный параметр метода, если метод статический?
Ответ
Прекрасный вопрос! Вы попали на один из «пыльных углов», это редко посещаемая область на карте .NET.
Смотрите. У .NET есть два типа делегатов: открытые и замкнутые. Если вы посмотрите на тип Delegate, у него есть свойства Method и Target. Так вот, замкнутые делегаты — это те, которые используют Target, а открытые — те, у которых Target игнорируется.*
Как вы помните, вызов нестатического метода можно представить себе мысленно как вызов функции, у которой добавлен ещё один аргумент, представляющий this. (На этой аналогии базируется понятие методов расширения в C#.)
Разница между открытыми и замкнутыми делегатами на первый взгляд напоминает разницу между статическими и простыми (нестатическими) методами: вызов простого метода привязан к целевому объекту (то есть, объекту, у которого будет этот самый метод вызван, тому, который будет доступен как this внутри метода). Кажется естественным представлять статические методы открытыми, и нестатические — замкнутыми делегатами.
Но эта аналогия обманчива. На самом деле, как статический, так и нестатический метод могут быть «упакованы» как в открытый, так и в замкнутый делегат!
В простом случае, когда замкнутый делегат ссылается на простой метод, у Target'а вызывается Method. Что же происходит, когда простой метод попадает в открытый делегат? А вот что. Для вызова такого делегата нужно указать ещё одним аргументом объект, на котором данный метод будет вызван. При этом список аргументов для вызова делегата увеличится на 1. Таким образом, мы как бы отвязываем метод от объекта.
Тестовый код. Заводим простой класс и простой метод в нём:
class Test
{
int n;
public Test(int n) { this.n = n; }
public void Display(int m) { Console.WriteLine($"object {n}, arg {m}"); }
}
Для создания замкнутых делегатов можно использовать перегрузки Delegate.CreateDelegate с параметром firstArgument, для замкнутых — без этого аргумента**. Ну или подойдут встроенные в язык средства. Тестируем:
Test t1 = new Test(1), t2 = new Test(2); // создали два экземпляра
Type typeT = typeof(Test); // запомнили тип и MethodInfo для метода
MethodInfo miDisplay = typeT.GetMethod("Display");
// замкнутый делегат можно создать так:
Action closedInstance1 = (Action)Delegate.CreateDelegate(
typeof(Action), t1, miDisplay);
// более прямой метод встроен в язык:
Action closedInstance2 = t2.Display;
// вызываем
closedInstance1(10); // object 1, arg 10
closedInstance2(11); // object 2, arg 11
// открытый делегат создаётся так:
Action openDelegate1 = (Action)Delegate.CreateDelegate(
typeof(Action), miDisplay);
// более удобного, встроенного в язык метода создать открытый делегат
// из нестатического метода нет
openDelegate1(t2, 10);
Окей, теперь случай статического метода. В случае открытого делегата вызов прост: статический метод не требует наличия объекта для своего вызова. А что же происходит в случае замкнутого делегата? А вот что. Первый параметр из списка параметров метода фиксируется: его значение берётся всегда из поля Target нашего делегата. Список параметров укорачивается на 1. Таким образом, мы видим, что происходит замыкание по первому аргументу функции.
Тестируем:
Type typeST = typeof(StaticTest); // запомнили тип и MethodInfo для метода
MethodInfo miDisplayEx = typeST.GetMethod("DisplayEx"),
miPrintTwo = typeST.GetMethod("PrintTwo");
// открытый делегат можно создать через библиотечную функцию
Action openDelegate2 = (Action)Delegate.CreateDelegate(
typeof(Action), miDisplayEx);
// или прямо через встроенный в язык метод
Action openDelegate3 = StaticTest.PrintTwo;
// работает и для методов расширения
Action openDelegate4 = StaticTest.DisplayEx;
// вызываем
openDelegate2(t1, 20); // More: object 1, arg 20
openDelegate3("hello", 3); // x = hello, y = 3
openDelegate4(t2, 21); // More: object 2, arg 21
// замкнутый делегат
Action closedDelegate2 = (Action)Delegate.CreateDelegate(
typeof(Action), "goodbye", miPrintTwo);
// для методов расширения есть удобное средство в языке
Action closedDelegate3 = t2.DisplayEx; // а вы знали, что так можно?
// вызываем
closedDelegate2(7); // x = goodbye, y = 7
closedDelegate3(30); // More: object 2, arg 30
Замыкание по первому аргументу значимого типа невозможно***.
Суммируя, у нас возможно 4 случая:
замкнутый делегат и простой метод: вызывается метод у фиксированного объекта
замкнутый делегат и статический метод: первый аргумент метода фиксируется
открытый делегат и простой метод: для вызова нужно указать и объект, и аргументы метода
открытый делегат и статический метод: вызывается метод как обычно, объект не нужен
Имеется ли у этого практическое применение? Наверное, в современном C#, где можно легко «тасовать» сигнатуры функций и проводить любые замыкания (а не только по неявному аргументу) при помощи лямбда-функций, это не так уж и необходимо, и может понадобиться лишь для экстремальной оптимизации, чтобы избежать создания лямбд.
Литература:
Schabse Laks, Open Delegates vs. Closed Delegates
MSDN, документация на класс Delegate, секция Remarks
MSDN, документация к методу Delegate.CreateDelegate, секция Remarks
*У замкнутого делегата может быть Target == null, как ни странно. При его вызове случится, как и имеет смысл ожидать, NullReferenceException
**Или с аргументом, равным null. Чем же отличается для функции Delegate.CreateDelegate замкнутый по null делегат от открытого, ссылающегося на нестатический метод? Количеством аргументов в сигнатуре делегата, которую вы передаёте первым аргументом!
***Причины этого такие же, как и причины невозможности получения делегата из метода расширения для значимого типа (оно ведёт к ошибке CS1113). О внутренних причинах этого написано здесь. Хотя сообщение об ошибке, которое производит Delegate.CreateDelegate, выглядит совершенно непонятно.