Здравствуйте, VladD2, Вы писали:
VD>Причем я пробовал применять два алгоритма и оба приводят к багу. Вот они:
VD>1. Имена параметров T и S перекрывают глобальные анологичные символы. При этом компилятор должен сообщить о том, что в T и S нет не статического члена Foo.
Откуда требование про не статичность члена?
VD>На мой взгляд баг чистой воды. Не в компиляторе, так в стандарте.
Я готов обсудить вопросы неудачных решений в стандарте после того, как мы убедимся, что здесь нет ошибок
реализации, т.е. поведение компилятора соответствует текущей версии стандарта. Нельзя сказать, что я в восторге от того, как overload resolution взаимодействует с лямбдами.
VD>Попробуй обдумать простую мысль. T и S в приведенном коде — это не типы, а имена параметров лябд. Таким образом обращение к ним через точку должно приводить к поиску экзеплярных методов с именем Foo. Но таковых нет!
Опять не пойму, откуда ты берешь требование про экземплярность.
VD>У тебя будет два подходящих кандидата. А поведение компилятора C# ошибка.
Почему ты считаешь, что они оба будут подходящими? Как я покажу ниже, это не так.
VD>И как компилятор выводит, что у параметра с именем T тип T? Это не вывод типов, а телепатия.
VD>Все что сказано в 7.5.4.1 — это то, что если свойство, параметр и т.п. перкрывают тип, то все равно можно обратиться к его статическим членам через имя типа.
VD>Но на каком основании делается вывод о том, что у параметра T тип T, а у параметра S тип S?
Как я понял, твое основное возражение заключается в том, что 7.5.4.1 не может применяться, потому что он требует, чтобы имя параметра совпадало с именем его типа, а в моем примере
неизвестно, что параметр T имеет тип T. Это не так. Сейчас я продемонстрирую, почему в момент применения правила 7.5.4.1
известно, что параметр T имеет тип T. Попробуй на время отложить свою интуицию, воспитанную на немерле, и внимательно проследить по спецификации C#, что здесь происходит. Для удобства я пронумеровал шаги рассуждения. Если тебе не понятно, что происходит на некотором шаге, укажи его номер.
Компилятор видит это выражение:
Bar(T => T.Foo())
Сначала он определяет, что такое Bar. Это оказывается группа методов, состоящая из двух методов с сигнатурами Bar(Action<T>) и Bar(Action<S>). Значит, все выражение целиком — это method invocation. Алгоритм дальнейших действий описан в 7.5.5.1 Method invocations.
1) Первым делом, надо составить множество методов-кандидатов. Для этого надо перебрать все методы в группе методов Bar.
1.1) Рассматриваем метод Bar(Action<T>). Это не-generic метод, поэтому для добавления его во множество кандидатов должны выполняться следущие условия:
1.1.1) При вызове, у группы методов Bar должен отсутствовать список типов-аргументов. Условие выполняется.
1.1.2) Метод Bar(Action<T>) должен быть применим к списку аргументов (T => T.Foo()). Правила проверки применимости описаны в 7.4.3.1 Applicable function member. Так как метод не имеет модификатора params, то чтобы он был применим, должны выполняться следующие условия:
1.1.2.1) Количество аргументов должно совпадать с количеством параметров. Условие выполняется (1=1).
1.1.2.2) Если у параметра нет модификаторов ref/out, то их не должно быть и у аргумента. Условие выполняется.
1.1.2.3) Должно существовать неявное преобразование от аргумента T => T.Foo() к типу параметра Action<T>. В данном случае аргумент является лямбда-выражением, поэтому правила, определяющие наличие неявного преобразования, описаны в 6.5 Anonymous function conversions.
Внимание! Проверяя наличие неявного преобразования, компилятор совершенно не заботится о том, что overload resolution для метода Bar еще не завершен, или что где-то есть еще метод с сигнатурой Bar(Action<S>). До него дело еще дойдет. Сейчас он просто сопоставляет выражение T => T.Foo() с типом Action<T> по приведенным ниже правилам. Чтобы неявное преобразование существовало, должны выполняться следующие условия:
1.1.2.3.1) Количество параметров в списке параметров лямбды и делегата должны совпадать. Условие выполняется (1=1).
1.1.2.3.2) Если список параметров лямбды не содержит явных указаний типов, то делегат не должен иметь ref/out параметров. Условие выполняется.
1.1.2.3.3) Теперь компилятор дает каждому из параметров лямбды тип соответствующего параметра делегата. То есть, единственный параметр T
получает тип T. После того, как параметры получили типы, компилятор проверяет тело лямбды на корректность ("If... when each parameter of F is
given the type of the corresponding parameter in D, the body of F is a valid expression"). Тело лямбды — это выражение T.Foo(), где T — это параметр типа T. Это выражение обрабатывается следующим образом:
1.1.2.3.3.1) Надо определить, что такое T.Foo. Синтаксически это
member-access. Правила его обработки приведены в 7.5.4 Member access. Так как T — это переменная типа T, то следует выполнить member lookup в типе T. Правила member lookup приведены в 7.3 Member lookup.
1.1.2.3.3.1.1) Определим множество доступных членов с именем Foo в типе T.
Внимание! Здесь нигде не написано, что ищутся только экземплярные (не статические) члены. Ищем public члены с именем Foo, определенные в типе T или его базовом типе object. Находится один статический метод.
1.1.2.3.3.1.2) Выбрасываем все члены, имеющие модификатор override. Таких нет.
1.1.2.3.3.1.3) Выбрасываем все nested типы, которые имеют типы-параметры в своей декларации. Таких нет.
1.1.2.3.3.1.4) Так как выражение T.Foo является частью выражения T.Foo(), которое синтаксически является
invocation-expression, то член Foo считается invoked.
Поэтому выбрасываем все non-invocable члены. Таких нет.
1.1.2.3.3.1.5) Для каждого члена во множестве, помечаем на удаление все члены, которые он скрывает. На данный момент у нас во множестве есть один член — статический метод Foo(), поэтому помечаем все члены, которые он скрывает. Таких нет.
1.1.2.3.3.1.6) Помечаем на удаление скрытые члены интерфейсов. Таких нет, поэтому ничего не делаем.
1.1.2.3.3.1.7) Удаляем члены, помеченые на удаление. Таких нет.
1.1.2.3.3.1.8) Определяем результат member lookup. Так как множество состоит из одного статического метода Foo(), то результатом будет группа методов, состоящая из этого метода.
Возвращаемся к 1.1.2.3.3.1) Таким образом, T.Foo — это группа методов, состоящая из одного метода.
1.1.2.3.3.2) Значит T.Foo() — это method invocation. Алгоритм дальнейших действий описан в 7.5.5.1 Method invocations.
1.1.2.3.3.2.1) Первым делом, надо составить множество методов-кандидатов. Для этого надо перебрать все методы (то есть, проверить единственный метод) в группе методов T.Foo. Рассматриваем статический метод Foo(), определенный в типе T. Это не-generic метод, поэтому для добавления его во множество кандидатов должны выполняться следущие условия:
1.1.2.3.3.2.1.1) При вызове, у группы методов T.Foo должен отсутствовать список типов-аргументов. Условие выполняется.
1.1.2.3.3.2.1.2) Метод Foo() должен быть применим к списку аргументов (). Правила проверки применимости описаны в 7.4.3.1 Applicable function member. Так как метод не имеет модификатора params, то чтобы он был применим, то количество аргументов должно совпадать с количеством параметров. Условие выполняется (0=0). Так как аргументов ноль, то больше никаких требований нет.
Возвращаемся к 1.1.2.3.3.2.1) Так как все условия выполнены, метод Foo() попадает во множество методов-кандидатов. Других методов в рассматриваемой группе методов нет, поэтому множество методов-кандидатов состоит из одного статического метода Foo(), определенного в типе T.
1.1.2.3.3.2.2) Множество методов-кандидатов урезается, чтобы содержать методы только из наиболее производных типов. Так как метод Foo() определен в классе T, то удаляем все методы, определенные в его базовом типе object и в любых интерфейсах. Таких нет.
1.1.2.3.3.2.3) Определяем лучший метод с помощью правил приведенных 7.4.3 Overload resolution. Так как мы имеем единственный метод, то он автоматически становится лучшим.
1.1.2.3.3.2.4) Производим final validation выбранного метода.
Внимание! Именно на этом шаге впервые проверяется, является ли метод статическим или экземплярным. Здесь срабатывает правило 7.5.4.1: доступ идет через точку после параметра T, но этот параметр имеет тип T, поэтому допустимы и статические, и экземплярные члены. То есть наш статический метод Foo() проходит валидацию. Кстати, на этом же шаге проверялись бы и констрейнты на типах-параметрах лучшего метода, если бы он был generic.
Возвращаемся к 1.1.2.3.3.2) Значит, T.Foo() — это корректное выражение.
Возвращаемся к 1.1.2.3.3) Значит, во время анализа тела лямбды не было обнаружено ошибок, поэтому тело лямбды корректно. Теперь проверяем совпадение возвращаемого типа лямбды и делегата. возвращаемый тип делегата — void, поэтому просто требуется, чтобы тело лямбды было выражением, синтаксически допустимым в качестве
statement-expression (проверяется по 8.6 Expression statements). Тело лямбды — это
invocation-expression, поэтому оно проходит проверку.
Возвращаемся к 1.1.2.3) Таким образом, неявное преобразование от выражения T => T.Foo() к типу Action<T> существует.
Возвращаемся к 1.1.2) Таким образом, метод Bar(Action<T>) применим к списку аргументов (T => T.Foo()).
Возвращаемся к 1.1) Так как все условия выполнены, метод Bar(Action<T>) попадает во множество методов-кандидатов.
1.2) Рассматриваем метод Bar(Action<S>). Это не-generic метод, поэтому для добавления его во множество кандидатов должны выполняться следущие условия:
1.2.1) При вызове, у группы методов Bar должен отсутствовать список типов-аргументов. Условие выполняется.
1.2.2) Метод Bar(Action<S>) должен быть применим к списку аргументов (T => T.Foo()). Правила проверки применимости описаны в 7.4.3.1 Applicable function member. Так как метод не имеет модификатора params, то чтобы он был применим, должны выполняться следующие условия:
1.2.2.1) Количество аргументов должно совпадать с количеством параметров. Условие выполняется.
1.2.2.2) Если у параметра нет модификаторов ref/out, то их не должно быть и у аргумента. Условие выполняется.
1.2.2.3) Должно существовать неявное преобразование от аргумента T => T.Foo() к типу параметра Action<S>. В данном случае аргумент является лямбда-выражением, поэтому правила, определяющие наличие или отсутствие неявного преобразования, описаны в 6.5 Anonymous function conversions. Чтобы неявное преобразование существовало, должны выполняться следующие условия:
1.2.2.3.1) Количество параметров в списке параметров лямбды и делегата должны совпадать. Условие выполняется (1=1).
1.2.2.3.2) Так как список параметров лямбды неявно типизирован, то спискок делегат не должен иметь ref/out параметров. Условие выполняется.
1.2.2.3.3) Теперь компилятор дает каждому из параметров лямбды тип соответствующего параметра делегата.
Внимание! Тот факт, что эта лямбда раньше проверялась на соответствие с другим делегатом, и параметр T уже получил ранее другой тип, никакого значения не имеет. Теперь единственный параметр T получает тип S, и начинается новый, полностью независимый процесс анализа тела лямбды. Тело лямбды — это выражение T.Foo(), где T — это параметр типа S. Это выражение обрабатывается следующим образом:
1.2.2.3.3.1) Этот шаг дословно совпадает с шагом 1.1.2.3.3.1 с заменой типа T на тип S. Таким образом, T.Foo — это группа методов, состоящая из одного метода.
1.2.2.3.3.2) Значит T.Foo() — это method invocation. Алгоритм дальнейших действий описан в 7.5.5.1 Method invocations.
1.2.2.3.3.2.1-3) Эти шаги дословно совпадают с шагами 1.2.2.3.3.2.1-3 с заменой типа T на тип S. Статический метод Foo(), определенный в типе S, становится лучшим.
1.2.2.3.3.2.4) Производим final validation выбранного метода. В данном случае правило 7.5.4.1 неприменимо: доступ идет через точку после параметра T, но этот параметр имеет тип S. Поэтому лучший метод должен быть экземплярным. Но тот метод, который мы нашли — статический, поэтому он не проходит валидацию.
Возвращаемся к 1.2.2.3.3.2) Значит, T.Foo() — это некорректное выражение.
Возвращаемся к 1.2.2.3.3) Значит, во время анализа тела лямбды были обнаружены ошибки, поэтому тело лямбды некорректно.
Возвращаемся к 1.2.2.3) Таким образом, неявное преобразование от выражения T => T.Foo() к типу Action<S> не существует.
Возвращаемся к 1.2.2) Таким образом, метод Bar(Action<S>) не применим к списку аргументов (T => T.Foo()).
Возвращаемся к 1.2) Метод Bar(Action<S>) не попадает во множество методов-кандидатов.
Возвращаемся к 1) Таким образом, множество методов-кандидатов состоит из одного метода Bar(Action<T>).
2) Множество методов-кандидатов урезается, чтобы содержать методы только из наиболее производных типов. Так как метод Bar (Action<T>) определен в классе Program, то удаляем все методы, определенные в его базовом типе object и в любых интерфейсах. Таких нет.
3) Определяем лучший метод с помощью правил приведенных 7.4.3 Overload resolution. Так как мы имеем единственный метод, то он автоматически становится лучшим.
4) Производим final validation выбранного метода. Так как доступ идет через
simple-name из статического члена Main, то найденный статический метод Bar(Action<T>) проходит валидацию.
Теперь надо определить, какой же тип имеет параметр T у аргумента T => T.Foo(). Согласно 7.14 Anonymous function expressions, этот тип определяется типом параметра делегата, к которому приводится данная лямбда. Так как overload resolution выбрал метод Bar(Action<T>), то лямбда приводится к типу Action<T>, и параметр T окончательно получает тип T. Опять-таки, здесь совершенно не важно, что ранее были попытки сопоставления этой лямбды с другими делегатами. Повторив шаги 1.1.2.3.3.1-2, мы узнаем, что T.Foo() в теле лямбды — это корректное выражение, вызов статического метода, определенного в типе T.
Теперь переходим к анализу выражения
Bar(S => S.Foo())
Все шаги, приведенные выше, повторяются, только T и S меняются местами. В результате мы узнаем, что S.Foo() в теле лямбды — это корректное выражение, вызов статического метода, определенного в типе S.
Остались какие-то неясности?