Почему компилятор C# может "видеть" статические свойства, но не методы экземпляра класса в DLL, на которую нет ссылок?
Предпосылка моего вопроса, на простом английском:
- Библиотека имени
Foo
зависит от библиотеки с именемBar
- Класс в Foo расширяет класс в Bar
- Foo определяет свойства / методы, которые просто передаются в Bar
- Приложение,
FooBar
, зависит только от Фу
Рассмотрим следующий пример:
class Program
{
static void Main(string[] args)
{
Foo foo = Foo.Instance;
int id = foo.Id; // Compiler is happy
foo.DoWorkOnBar(); // Compiler is not happy
}
}
Foo определяется следующим образом
public class Foo : Bar
{
public new static Foo Instance { get => (Foo)Bar.Instance; }
public new int Id { get => Bar.Id; }
public void DoWorkOnBar()
{
Instance.DoWork();
}
}
Бар определяется следующим образом
public class Bar
{
public static Bar Instance { get => new Bar(); }
public static int Id { get => 5; }
public void DoWork() { }
}
Та часть, которая полностью озадачивает меня:
Без ссылки на Bar
библиотека
FooBar
может получить идентификатор, который предоставляетсяBar
(или, по крайней мере, он компилируется)FooBar
не может просить Фу сделать работу, которая в конечном итоге выполняетсяBar
Ошибка компилятора, связанная с foo.DoWorkOnBar();
является
Тип "Бар" определен в сборке, на которую нет ссылок. Необходимо добавить ссылку на сборку 'Bar, версия 1.0.0.0, Culture=Neutral, PublicKeyToken=null' .
Почему в компиляторе есть несоответствие?
Я бы предположил, что ни одна из этих операций не будет компилироваться без FooBar
добавив ссылку на Bar
,
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
в Bar
,
За foo.DoWorkOnBar()
компилятор сначала ищет элемент с именем DoWorkOnBar
в Foo
, Когда компилятор видит это Foo
имеет метод с именем DoWorkOnBar
компилятор продолжает поиск во всех базовых классах методов с именем DoWorkOnBar
, Компилятор делает это потому, что (в отличие от свойств) методы могут быть перегружены, а компилятор реализует алгоритм разрешения перегрузки по существу так же, как это описано в спецификации C#:
- Начните с "группы методов", состоящей из набора всех перегрузок
DoWorkOnBar
объявлено вFoo
и его базовые классы. - Сузьте набор до "подходящих" методов (в основном, методов, параметры которых совместимы с предоставленными аргументами).
- Удалите любой метод-кандидат, затененный методом-кандидатом, в более производном классе.
- Выберите "лучший" из оставшихся методов-кандидатов.
Шаг 1 вызывает требование добавить ссылку на сборку Bar
,
Может ли компилятор C# реализовать алгоритм по-другому? Согласно спецификации C#:
Интуитивный эффект описанных выше правил разрешения заключается в следующем: чтобы найти конкретный метод, вызываемый вызовом метода, начните с типа, указанного в вызове метода, и продолжайте цепочку наследования, пока не будет хотя бы один применимый, доступный, не переопределенный Объявление метода найдено. Затем выполните вывод типа и разрешите перегрузку для набора применимых, доступных, не переопределенных методов, объявленных в этом типе, и вызовите метод, выбранный таким образом.
Поэтому мне кажется, что ответ "Да": компилятор C# теоретически может видеть, что Foo
объявляет применимым DoWorkOnBar
метод и не удосужился смотреть на Bar
, Однако для компилятора Roslyn это повлечет за собой серьезную переписку кода поиска членов компилятора и разрешения перегрузки - вероятно, не стоит усилий, учитывая, насколько легко разработчики могут самостоятельно устранить эту ошибку.
TL; DR - когда вы вызываете метод, компилятору необходимо, чтобы вы ссылались на сборку базового класса, потому что именно так был реализован компилятор.
¹ См. Метод LookupMembersInClass класса Microsoft.CodeAnalysis.CSharp.Binder.
² См. Метод PerformMemberOverloadResolution класса Microsoft.CodeAnalysis.CSharp.OverloadResolution.