Почему компилятор С# видит "статические свойства, но не методы экземпляра" класса в DLL, на который не ссылаются?

Посылка моего вопроса на простом английском:

  • Библиотека под названием Foo зависит от библиотеки с именем Bar
  • Класс внутри Foo расширяет класс внутри Bar
  • Foo определяет свойства/методы, которые просто передаются в Bar
  • Приложение FooBar зависит только от Foo

Рассмотрим следующий пример:

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 не может просить Foo выполнять работу, которая в конечном итоге выполняется Bar

Ошибка компилятора, связанная с foo.DoWorkOnBar(); является

Тип "Бар" определяется в сборке, на которую не ссылаются. Вы должны добавить ссылку на сборку 'Bar, Version 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 in Bar.

Для foo.DoWorkOnBar() компилятор сначала ищет член с именем DoWorkOnBar в Foo. Когда компилятор видит, что Foo имеет метод с именем DoWorkOnBar, компилятор продолжает поиск всех базовых классов для методов с именем DoWorkOnBar. Компилятор делает это, потому что (в отличие от свойств) методы могут быть перегружены, а компилятор реализует ² алгоритм разрешения перегрузки по существу таким же образом, как описано в спецификации С#:

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

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

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

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

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


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


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

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