Неявное преобразование группы методов (часть 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.