Почему компилятор 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#:

  1. Начните с "группы методов", состоящей из набора всех перегрузок DoWorkOnBar объявлено в Foo и его базовые классы.
  2. Сузьте набор до "подходящих" методов (в основном, методов, параметры которых совместимы с предоставленными аргументами).
  3. Удалите любой метод-кандидат, затененный методом-кандидатом, в более производном классе.
  4. Выберите "лучший" из оставшихся методов-кандидатов.

Шаг 1 вызывает требование добавить ссылку на сборку Bar,

Может ли компилятор C# реализовать алгоритм по-другому? Согласно спецификации C#:

Интуитивный эффект описанных выше правил разрешения заключается в следующем: чтобы найти конкретный метод, вызываемый вызовом метода, начните с типа, указанного в вызове метода, и продолжайте цепочку наследования, пока не будет хотя бы один применимый, доступный, не переопределенный Объявление метода найдено. Затем выполните вывод типа и разрешите перегрузку для набора применимых, доступных, не переопределенных методов, объявленных в этом типе, и вызовите метод, выбранный таким образом.

Поэтому мне кажется, что ответ "Да": компилятор C# теоретически может видеть, что Foo объявляет применимым DoWorkOnBar метод и не удосужился смотреть на Bar, Однако для компилятора Roslyn это повлечет за собой серьезную переписку кода поиска членов компилятора и разрешения перегрузки - вероятно, не стоит усилий, учитывая, насколько легко разработчики могут самостоятельно устранить эту ошибку.


TL; DR - когда вы вызываете метод, компилятору необходимо, чтобы вы ссылались на сборку базового класса, потому что именно так был реализован компилятор.


¹ См. Метод LookupMembersInClass класса Microsoft.CodeAnalysis.CSharp.Binder.

² См. Метод PerformMemberOverloadResolution класса Microsoft.CodeAnalysis.CSharp.OverloadResolution.

Другие вопросы по тегам