Ответ 1
Во-первых, обратите внимание, что реализация Foo.Id
и Foo.DoWorkOnBar
не имеет значения; компилятор рассматривает foo.Id
и foo.DoWorkOnBar()
иначе, даже если реализации не имеют доступа к Bar
:
// In class Foo:
public new int Id => 0;
public void DoWorkOnBar() { }
Причина, по которой foo.Id
компилируется успешно, но foo.DoWorkOnBar()
не означает, что компилятор использует другую логику¹ для поиска свойств по сравнению с методами.
Для foo.Id
компилятор сначала ищет член с именем Id
в Foo
. Когда компилятор видит, что Foo
имеет свойство Id
, компилятор останавливает поиск и не беспокоится о том, чтобы смотреть на Bar
. Компилятор может выполнить эту оптимизацию, потому что свойство в производном классе затеняет всех членов с тем же именем в базовом классе, поэтому foo.Id
всегда будет ссылаться на Foo.Id
, независимо от того, какие члены могут быть названы Id
in Bar
.
Для foo.DoWorkOnBar()
компилятор сначала ищет член с именем DoWorkOnBar
в Foo
. Когда компилятор видит, что Foo
имеет метод с именем DoWorkOnBar
, компилятор продолжает поиск всех базовых классов для методов с именем DoWorkOnBar
. Компилятор делает это, потому что (в отличие от свойств) методы могут быть перегружены, а компилятор реализует ² алгоритм разрешения перегрузки по существу таким же образом, как описано в спецификации С#:
- Начните с "группы методов", состоящей из набора всех перегрузок
DoWorkOnBar
объявленных вFoo
и его базовых классах. - Уточните набор до "кандидатных" методов (в основном, методы, параметры которых совместимы с поставленными аргументами).
- Удалите любой метод кандидата, который затенен методом кандидата в более производном классе.
- Выберите "лучший" из оставшихся методов кандидата.
Шаг 1 запускает требование для вас, чтобы добавить ссылку на сборочном Bar
.
Может ли компилятор С# реализовать алгоритм по-разному? Согласно спецификации С#:
Интуитивный эффект правил разрешения, описанных выше, заключается в следующем: чтобы найти конкретный метод, вызываемый вызовом метода, начните с типа, указанного вызовом метода, и продолжите цепочку наследования до тех пор, пока, по крайней мере, одно применимое, доступное, не переопределенное найдено объявление метода. Затем выполните вывод типа и разрешение перегрузки в наборе применимых, доступных, не переопределенных методов, объявленных в этом типе, и вызовите выбранный таким образом метод.
Поэтому мне кажется, что ответ "Да": компилятор С# теоретически может видеть, что Foo
объявляет применимый метод DoWorkOnBar
и не беспокоится о том, чтобы смотреть на Bar
. Однако для компилятора Roslyn это будет связано с перепиской кода поиска компиляторов и кода перегрузки, возможно, не стоит усилий, учитывая, как легко разработчики могут самостоятельно решить эту ошибку.
TL; DR. Когда вы вызываете метод, компилятору нужно ссылаться на сборку базового класса, потому что это способ реализации компилятора.
¹ См. Метод LookupMembersInClass класса Microsoft.CodeAnalysis.CSharp.Binder.
² См. Метод PerformMemberOverloadResolution класса Microsoft.CodeAnalysis.CSharp.OverloadResolution.