Почему делегаты ссылаются на типы? - PullRequest
35 голосов
/ 26 октября 2011

Краткое примечание о принятом ответе : Я не согласен с небольшой частью ответа Джеффри , а именно с тем, что, поскольку Delegate должен был быть ссылочным типом, из этого следует, что все делегаты являются ссылочными типами. (Просто неверно, что многоуровневая цепочка наследования исключает типы значений; все типы перечислений, например, наследуются от System.Enum, который, в свою очередь, наследуется от System.ValueType, который наследуется от System.Object, все ссылочные типы.) Однако я думаю, что тот факт, что, по сути, все делегаты на самом деле наследуют не только от Delegate, но и от MulticastDelegate, является здесь критической реализацией. Как Рэймонд указывает в комментарии к его ответу, как только вы взяли на себя обязательство поддерживать несколько подписчиков, на самом деле нет смысла в не использовать ссылочный тип для сам делегат, учитывая необходимость где-то в массиве.


См. Обновление внизу.

Мне всегда казалось странным, что если я делаю это:

Action foo = obj.Foo;

Я каждый раз создаю новый Action объект. Я уверен, что стоимость минимальна, но она включает выделение памяти для последующей сборки мусора.

Учитывая, что делегаты по своей природе сами неизменны, мне интересно, почему они не могут быть типами значений? Тогда строка кода, подобная приведенной выше, не принесет ничего, кроме простого присваивания адресу памяти в стеке *.

Даже с учетом анонимных функций, кажется (на мне ) это будет работать. Рассмотрим следующий простой пример.

Action foo = () => { obj.Foo(); };

В этом случае foo представляет собой замыкание , да. И во многих случаях, я полагаю, это требует фактического ссылочного типа (например, когда локальные переменные закрываются и изменяются внутри замыкания). Но в некоторых случаях это не должно быть. Например, в приведенном выше случае кажется, что тип, поддерживающий замыкание, может выглядеть следующим образом: Я возвращаюсь к своей первоначальной точке зрения по этому поводу. Нижеследующее действительно должно быть ссылочным типом (или: ему не нужно , нужно , но если это struct, то оно все равно будет упаковано). Поэтому не обращайте внимания на приведенный ниже пример кода. Я оставляю это только для обеспечения контекста для ответов, в которых конкретно упоминается.

struct CompilerGenerated
{
    Obj obj;

    public CompilerGenerated(Obj obj)
    {
        this.obj = obj;
    }

    public void CallFoo()
    {
        obj.Foo();
    }
}

// ...elsewhere...

// This would not require any long-term memory allocation
// if Action were a value type, since CompilerGenerated
// is also a value type.
Action foo = new CompilerGenerated(obj).CallFoo;

Имеет ли этот вопрос смысл? На мой взгляд, есть два возможных объяснения:

  • Для правильной реализации делегатов в качестве типов значений потребовалась бы дополнительная работа / сложность, поскольку для поддержки таких вещей, как замыкания, которые делают изменения значений локальных переменных, в любом случае потребовались бы сгенерированные компилятором ссылочные типы.
  • Существует несколько других причин, по которым делегаты просто не могут быть реализованы как типы значений.

В конце концов, я не теряю сон из-за этого; это просто то, что мне было интересно на некоторое время.


Обновление : В ответ на комментарий Ани я понимаю, почему тип CompilerGenerated в моем приведенном выше примере также может быть ссылочным типом, поскольку, если делегат будет содержать указатель на функцию и в любом случае для указателя объекта понадобится ссылочный тип (по крайней мере, для анонимных функций, использующих замыкания, поскольку, даже если вы ввели дополнительный параметр универсального типа - например, Action<TCaller> - это не будет охватывать типы, имена которых не могут быть названы!) , Однако , все, что это делает, отчасти заставляет меня сожалеть о том, что вообще привел в обсуждение вопрос о сгенерированных компилятором типах для замыканий! Мой главный вопрос касается делегатов , то есть вещи с указателем функции и указателем объекта. мне все еще кажется, что может быть типом значения.

Другими словами, даже если это ...

Action foo = () => { obj.Foo(); };

... требует создания одного объекта ссылочного типа (для поддержки замыкания и предоставления делегату чего-либо для ссылки), почему требуется создание two (поддерживающий замыкание объект плюс делегат Action)?

* Да, да, подробности реализации, я знаю!Все, что я имею в виду, это кратковременное хранение в памяти .

Ответы [ 7 ]

16 голосов
/ 26 октября 2011

Вопрос сводится к следующему: спецификация CLI (Common Language Infrastructure) гласит, что делегаты являются ссылочными типами. Почему это так?

Одна из причин ясно видна сегодня в .NET Framework. В исходном проекте было два вида делегатов: обычные делегаты и делегаты «многоадресной рассылки», которые могли иметь более одной цели в своем списке вызовов. Класс MulticastDelegate наследуется от Delegate. Поскольку вы не можете наследовать от типа значения, Delegate должен был быть ссылочным типом.

В конце концов, все действительные делегаты оказались многоадресными делегатами, но на этом этапе процесса было слишком поздно объединять два класса. См. сообщение в блоге на эту тему:

Мы отказались от различия между Delegate и MulticastDelegate. к концу V1. В то время это был бы массивный изменить, чтобы объединить два класса, чтобы мы этого не делали. Вам следует притвориться, что они объединены и существует только MulticastDelegate.

Кроме того, в настоящее время делегаты имеют 4-6 полей, все указатели. 16 байтов обычно считается верхней границей, где экономия памяти все еще выигрывает при дополнительном копировании. 64-битный MulticastDelegate занимает 48 байтов. Учитывая это и тот факт, что они использовали наследование, можно предположить, что класс был естественным выбором.

9 голосов
/ 30 ноября 2011

Существует только одна причина, по которой делегат должен быть классом, но он большой: хотя делегат может быть достаточно маленьким, чтобы обеспечить эффективное хранение в качестве типа значения (8 байт в 32-разрядных системах или 16 байт в64-битные системы), нет никакого способа, которым он мог бы быть достаточно маленьким, чтобы эффективно гарантировать, что если один поток пытается записать делегат, в то время как другой поток пытается его выполнить, последний поток не в конечном итоге ни вызовет старый метод для новой целиили новый метод для старой цели.Разрешение на такую ​​вещь было бы серьезной дырой в безопасности.Наличие делегатов в качестве ссылочных типов позволяет избежать этого риска.

На самом деле, даже лучше, чем делегаты быть типами структуры, если бы они были интерфейсами.Создание замыкания требует создания двух объектов кучи: сгенерированного компилятором объекта для хранения любых закрытых переменных и делегата для вызова надлежащего метода для этого объекта.Если бы делегаты были интерфейсами, объект, который содержал закрытые переменные, сам мог бы быть использован в качестве делегата, без других необходимых объектов.

7 голосов
/ 26 октября 2011

Представьте, если бы делегаты были типами значений.

public delegate void Notify();

void SignalTwice(Notify notify) { notify(); notify(); }

int counter = 0;
Notify handler = () => { counter++; }
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?

Согласно вашему предложению, это будет внутренне преобразовано в

struct CompilerGenerated
{
    int counter = 0;
    public Execute() { ++counter; }
};

Notify handler = new CompilerGenerated();
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?

Если бы delegate был типом значения, то SignalEvent получит копию handler, что означает, что будет создан новый CompilerGenerated (копия handler) и передан SignalEvent. SignalTwice выполнит делегат дважды, что увеличит counter в два раза в копии . И тогда SignalTwice возвращает, и функция печатает 0, потому что оригинал не был изменен.

4 голосов
/ 26 октября 2011

Вот неосведомленное предположение:

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

Для экземпляра делегата требуется, как минимум:

  • Ссылка на объект (ссылка "this" для упакованного метода, если это метод экземпляра).
  • Указатель на упакованную функцию.
  • Ссылка на объект, содержащий список многоадресных вызовов. Обратите внимание, что тип делегата должен по своему дизайну поддерживать многоадресную передачу с использованием того же типа делегата.

Давайте предположим, что делегаты типа значения были реализованы аналогично текущей реализации эталонного типа (возможно, это несколько неразумно; возможно, для сохранения размера можно было бы выбрать другой дизайн), чтобы проиллюстрировать это. Используя Reflector, вот поля, необходимые для экземпляра делегата:

System.Delegate: _methodBase, _methodPtr, _methodPtrAux, _target
System.MulticastDelegate: _invocationCount, _invocationList

Если реализовано как структура (без заголовка объекта), это добавит до 24 байтов в x86 и 48 байтов в x64, что является огромным для структуры.


В другой заметке я хочу спросить, как в предложенном вами проекте создание типа закрытия CompilerGenerated как структура помогает в любом случае. Куда указывает указатель объекта созданного делегата? Оставление экземпляра типа замыкания в стеке без надлежащего анализа escape будет крайне рискованным делом.

2 голосов
/ 19 марта 2012

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

Представьте, что Delegate - это структура, состоящая, скажем, из объекта target; указатель на метод

Это может быть структура, верно?Упаковка будет происходить, только если целью является структура (но сам делегат не будет упакован).

Вы можете подумать, что он не будет поддерживать MultiCastDelegate, но тогда мы можем: Создать новый объект, который будет содержатьмассив нормальных делегатов.Верните делегат (в виде структуры) этому новому объекту, который будет реализовывать Invoke, перебирая все его значения и вызывая для них Invoke.

Итак, для обычных делегатов, которые никогда не будут вызывать два или более обработчиков,это может работать как структура.К сожалению, это не изменится в .Net.


В качестве примечания: дисперсия не требует, чтобы делегат был ссылочным типом.Параметры делегата должны быть ссылочными типами.В конце концов, если вы передадите строку, в которой требуется объект (для ввода, а не для ref или out), преобразование не требуется, поскольку строка уже является объектом.

2 голосов
/ 26 октября 2011

Я видел этот интересный разговор в интернете:

Неизменяемость не означает, что это должен быть тип значения. И то, что тип значения не обязательно должен быть неизменным Двое часто идут рука об руку, но они на самом деле не одно и то же, и есть на самом деле контрпримеры каждого в .NET Framework (строка класс, например).

И ответ:

Разница в том, что хотя неизменяемые ссылочные типы достаточно распространенный и вполне разумный, что делает типы значений изменчивыми почти всегда плохая идея, и может привести к очень запутанным поведение!

Взято из здесь

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

1 голос
/ 26 октября 2011

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

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

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

...