Подтвердить что ты не робот

Почему ссылочные типы делегатов?

Быстрая заметка о принятом ответе: я не согласен с небольшой частью ответа Джеффри, а именно, что, поскольку 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;

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

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

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


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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

Здесь неосведомленное предположение:

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

Для экземпляра делегата необходимо, по крайней мере:

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

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

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

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


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

Ответ 5

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

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

И ответ:

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

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

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

Ответ 6

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

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

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

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

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


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

Ответ 7

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

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

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