Неявное преобразование группы методов (часть 2)
Упрощенный от этот вопрос и избавился от возможного влияния от LinqPad (без offsensive), простого консольного приложения вроде этого:
public class Program
{
static void M() { }
static void Main(string[] args)
{
Action a = new Action(M);
Delegate b = new Action(M);
Console.WriteLine(a == b); //got False here
Console.Read();
}
}
"false" получается из оператора ceq
в CIL вышеприведенного кода (подробности см. в исходном вопросе). Поэтому мои вопросы:
(1) Почему ==
переводят на ceq
вместо call Delegate Equals
?
Здесь я не забочусь о (un) обертке между Delegate и Action. В самом последнем случае при оценке a == b
a имеет тип Action
, а b - Delegate
. Из спецификации:
7.3.4 Разрешение перегрузки двоичных операторов
Операция вида x op y, где op - перегружаемый двоичный оператор, x - выражение типа X, а y - выражение типа Y, обрабатывается следующим образом:
• Набор потенциальных пользовательских операторов, предоставляемых X и Y для операционный оператор op (x, y) определяется. Набор состоит из объединение кандидатов-операторов, предоставленных Х и кандидатом операторы, предоставленные Y, каждый определяется с использованием правил п. 7.3.5. Если X и Y являются одним и тем же типом, или если X и Y получены из общего базовый тип, то общие операторы-кандидаты встречаются только в комбинированном установить один раз.
• Если набор потенциальных пользовательских операторов не является пустой, то это становится набором операторов-кандидатов для операция. В противном случае предопределенный двоичный оператор op реализации, включая их поднятые формы, становятся кандидатов на операцию. Предопределенные реализации данного оператора указаны в описании оператора (§7.8 - §7.12).
• Правила разрешения перегрузки в §7.5.3 являются применяется к набору кандидатов-операторов для выбора лучшего оператора по отношению к списку аргументов (x, y), и этот оператор становится результат процесса разрешения перегрузки. Если разрешение перегрузки не удается выбрать один лучший оператор, возникает ошибка времени привязки.
7.3.5. Пользовательские операторы-кандидаты
Учитывая тип T и операционный оператор op (A), где op - это перегружаемый оператор, а A - список аргументов, набор потенциальных пользовательских операторов, предоставленный T для оператора op (A) определяется следующим образом:
• Определите тип T0. Если T является нулевым типом, то T0 является его базовым типом, иначе T0 равна T.
• Для всех операторов op op в T0 и всех снятых формы таких операторов, если применим хотя бы один оператор (§7.5.3.1) относительно списка аргументов А, то множество оператор-кандидат состоит из всех таких применимых операторов в T0.
• В противном случае, если T0 является объектом, набор операторов-кандидатов пуст.
• В противном случае набор операторов-кандидатов, предоставляемых T0, является множеством операторов-кандидатов, предоставляемых прямым базовым классом T0, или эффективный базовый класс T0, если T0 является параметром типа.
Из спецификации a и b имеют один и тот же базовый класс Delegate
, очевидно, что здесь должно применяться правило оператора ==
, определенное в Delegate
(оператор == вызывает делегирование .Equals). Но теперь похоже, что список кандидатов определяемых пользователем операторов пуст и, наконец, применяется Object ==
.
(2) Должен (есть) код FCL подчиняться спецификации языка С#? Если нет, мой первый вопрос бессмыслен, потому что что-то специально рассматривается. И тогда мы можем ответить на все эти вопросы, используя "о, это особое обращение в FCL, они могут делать то, что мы не можем. Спектр предназначен для внешних программистов, не будь глупым".
Ответы
Ответ 1
Существует два типа операторов: пользовательские операторы и предопределенные операторы. Раздел 7.3.5 "Пользовательские операторы-кандидаты" не применяются к предопределенным операторам.
Например, операторы на decimal
выглядят как пользовательские операторы в декомпиляторе, но С# рассматривает их как предопределенные операторы и применяет к ним числовое продвижение (числовое продвижение не применяется к пользовательским операторам).
Раздел 7.10.8 "Операторы равенства делегатов" определяют operator ==(Delegate, Delegate)
как предопределенный оператор, поэтому я думаю, что все правила о пользовательских операторах не применяются к этому оператору (хотя это не 100% ясно в спецификации, как в этом случае предопределенный оператор не применяется всякий раз, когда пользовательский оператор будет).
Every delegate type implicitly provides the following predefined comparison operators:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
Но сам System.Delegate
не считается типом делегата, поэтому единственным кандидатом на разрешение перегрузки является operator ==(object, object)
.
Ответ 2
Компилятор работает очень разные и необычные с делегатами. Существует много скрытой обработки. Обратите внимание, что правило "общий базовый тип" в этом руководстве применяется к "пользовательским операторам". Делегаты являются внутренними и системными. Например, вы можете написать Action a = M;
вместо Action a = new Action(M);
. И вы можете добавить a += M;
после этого. Проверьте, что происходит в CIL, это интересно в первый раз.
Кроме того, для сравнения делегатов это опасно и нетривиально. Каждый делегат на самом деле является делегатом многоадресной рассылки. Вы можете добавить несколько указателей на функции в один и тот же делегат. Делегаты [L(); M(); N();]
равны делегировать [M();]
? Указатель функции содержит экземпляр класса (например, метод). Имеет ли [a.M();]
значение [b.M();]
? Все, что зависит от случая, и реализация сравнения требует перехода через список вызовов.
Наследование делегатов из общего базового типа. Делегат является неявным, и эта проблема может возникнуть в других сценариях, например. generic constraint: вы не можете указать Delegate как ограничение на общий параметр T. Здесь компилятор явно отклоняет это. То же самое и о создании собственных классов, унаследованных от Delegate.
Это ответ на оба вопроса - "Делегат" не является чисто FCL, он тесно связан с компилятором. Если вы действительно хотите, чтобы Microsoft делегировал поведение сравнения - просто вызывайте явно Equals(a, b)
Ответ 3
предупреждение CS0253: возможное непреднамеренное сравнение ссылок; чтобы получить сравнение значений, введите правую сторону, чтобы ввести "System.Action"
Это предупреждение, которое вы получаете за этот код С#. Не игнорируйте это предупреждение, команда С# хорошо знала, что код, сгенерированный для этого сравнения, был неожиданным. Им не нужно было создавать этот код, они могли бы легко сделать то, что вы ожидали. Как этот код делает:
Module Module1
Sub M()
End Sub
Sub Main()
Dim a = New Action(AddressOf M)
Dim b = DirectCast(New Action(AddressOf M), [Delegate])
Console.WriteLine(a = b) ''got True here
Console.Read()
End Sub
End Module
Что генерирует почти тот же MSIL, за исключением того, что вместо ceq
вы получаете:
IL_001d: call bool [mscorlib]System.Delegate::op_Equality(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
Что вы надеялись на код С#. Это был код VB.NET, если вы его не узнали. Иначе причина, по которой Microsoft поддерживает два основных управляемых языка, хотя они имеют очень похожие возможности. Но с очень разными вариантами использования. Всякий раз, когда было создано несколько способов генерации кода, команда С# последовательно выбирала для своей производительности команду VB.NET для удобства.
И производительность, безусловно, является ключевым здесь, сравнение объектов-делегатов дорого. Правила изложены в Ecma-335, раздел II.14.6.1. Но вы можете рассуждать сами за себя, есть много проверок. Он должен проверить совместимость целевого объекта делегата. И для каждого аргумента он должен проверить, является ли значение конвертируемым. Расходы, которые команда С# не хочет скрывать.
И нет, вы получаете предупреждение, чтобы напомнить вам, что они сделали неинтуитивный выбор..
Ответ 4
Ключевым моментом здесь является то, что оператор ==
и метод Equals
для типа Delegate
- это две разные вещи. Для ссылочных типов ==
выглядит, если обе ссылки указывают на один и тот же объект, если оператор ==
не переопределен (см. == Оператор (С# )).
Поскольку вы создаете два разных объекта Action
, хотя они внутренне вызывают один и тот же метод, они представляют собой разные объекты в разных местах в памяти и не имеют значения или string
, поэтому ==
в этом случае a ReferenceEquals
и не вызывает метод Delegate.Equals
, который был переопределен, чтобы увидеть, имеют ли оба объекта одно и то же. Для ссылочных типов, отличных от string
, это поведение по умолчанию ==
или Equals
.