Подтвердить что ты не робот

Неверная перегрузка переопределяется, когда два метода имеют одинаковые подписи после подстановки аргументов типа

Мы полагаем, что этот пример обнаруживает ошибку в компиляторе С# (смейтесь над мной, если мы ошибаемся). Эта ошибка может быть хорошо известна. В конце концов, наш пример представляет собой простое изменение описания в этом сообщении в блоге.

using System;

namespace GenericConflict
{
  class Base<T, S>
  {
    public virtual int Foo(T t)
    { return 1; }
    public virtual int Foo(S s)
    { return 2; }

    public int CallFooOfT(T t)
    { return Foo(t); }
    public int CallFooOfS(S s)
    { return Foo(s); }
  }

  class Intermediate<T, S> : Base<T, S>
  {
    public override int Foo(T t)
    { return 11; }
  }

  class Conflict : Intermediate<string, string>
  {
    public override int Foo(string t)
    { return 101;  }
  }


  static class Program
  {
    static void Main()
    {
      var conflict = new Conflict();
      Console.WriteLine(conflict.CallFooOfT("Hello mum"));
      Console.WriteLine(conflict.CallFooOfS("Hello mum"));
    }
  }
}

Идея состоит в том, чтобы создать класс Base<T, S> с двумя виртуальными методами, чьи подписи станут идентичными после "злого" выбора T и S. Класс Conflict перегружает только один из виртуальных методов, и из-за существования Intermediate<,> он должен быть хорошо определен, какой из них!

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

Когда мы читаем сообщение Sam Ng

4b9b3361

Ответ 1

Мы считаем, что этот пример обнаруживает ошибку в компиляторе С#.

Давайте делать то, что мы всегда должны делать, когда вы обнаруживаете ошибку компилятора: тщательно контрастировать с ожидаемым и наблюдаемым поведением.

Наблюдаемое поведение заключается в том, что программа производит 11 и 101 в качестве первого и второго выходов соответственно.

Каково ожидаемое поведение? Есть два "виртуальных слота". Первый вывод должен быть результатом вызова метода в слоте Foo(T). Второй вывод должен быть результатом вызова метода в слоте Foo(S).

Что происходит в этих слотах?

В экземпляре Base<T,S> метод return 1 отправляется в слот Foo(T), а метод return 2 отправляется в слот Foo(S).

В случае Intermediate<T,S> метод return 11 отправляется в слот Foo(T), а метод return 2 отправляется в слот Foo(S).

Надеюсь, до сих пор вы согласны со мной.

В случае Conflict существует четыре возможности:

  • Возможность: метод return 11 отправляется в слот Foo(T), а метод return 101 - в слот Foo(S).
  • Возможность двух: метод return 101 находится в слоте Foo(T), а метод return 2 - в слоте Foo(S).
  • Возможность три: метод return 101 идет в обоих слотах.
  • Возможность четыре: компилятор обнаруживает, что программа неоднозначна и выдает ошибку.

Вы ожидаете, что здесь будет одна из двух вещей, основанная на разделе 10.6.4 спецификации. Или:

  • Компилятор определит, что метод в Conflict переопределяет метод в Intermediate<string, string>, потому что метод в промежуточном классе найден первым. В этом случае возможность двух - правильное поведение. Или:
  • Компилятор определит, что метод в Conflict неоднозначен в отношении того, какое оригинальное объявление оно переопределяет, и, следовательно, возможность четвертая является правильной.

В любом случае вероятность невозможна.

Это не 100% ясный, я признаю, что из этих двух правильных. Мое личное чувство заключается в том, что более разумное поведение заключается в том, чтобы рассматривать метод переопределения как частную реализацию промежуточного класса; вопрос, на мой взгляд, заключается не в том, переопределяет ли промежуточный класс метод базового класса, а в том, объявляет ли он метод с соответствующей сигнатурой. В этом случае правильное поведение будет заключаться в том, чтобы выбрать возможность четырех.

То, что делает настоящий компилятор, - это то, что вы ожидаете: он выбирает возможность два. Поскольку промежуточный класс имеет член, который соответствует, мы выбираем его как "вещь для переопределения", независимо от того, что метод не объявлен в промежуточном классе. Компилятор определяет, что Intermediate<string, string>.Foo - это метод, переопределенный Conflict.Foo, и соответственно испускает код. Он не вызывает ошибку, потому что он считает, что программа не ошибается.

Итак, если компилятор правильно анализирует код, выбирая возможность два и не создавая ошибку, то почему во время выполнения кажется, что компилятор выбрал возможность один, а не возможность два?

Поскольку создание программы, которая приводит к объединению двух методов в рамках общей конструкции, - это поведение, определяемое реализацией для среды выполнения. В этом случае среда исполнения может сделать что угодно! Он может выбрать ошибку типа загрузки. Это может привести к проверке. Он может разрешить программу, но заполнить слоты в соответствии с определенным критерием по своему выбору. И на самом деле последнее - это то, что он делает. Среда выполнения просматривает программу, испускаемую компилятором С#, и сама решает, что возможность - это правильный способ анализа этой программы.

Итак, теперь у нас есть довольно философский вопрос о том, является ли это ошибкой компилятора; компилятор следует разумной интерпретации спецификации, и все же мы по-прежнему не получаем ожидаемого поведения. В этом смысле это очень сложная ошибка компилятора. Задача компилятора - перевести программу, написанную на С#, в точно эквивалентную программу, написанную в IL. Компилятор не выполняет этого; это перевод программы, написанной на С#, в программу, написанную на IL, которая имеет поведение, определенное реализацией, а не поведение, указанное спецификацией языка С#.

Как четко описывает Сэм в своем блоге, мы хорошо знаем об этом несоответствии между типами топологий, которые язык С# наделяет конкретными значениями и какие топологии CLR наделяет конкретными значениями. Язык С# достаточно понятен, что вероятность двух, возможно, правильная, но нет кода, который мы можем испустить, что делает CLR, потому что CLR в принципе имеет поведение, определенное реализацией, в любое время, когда два метода объединяются, чтобы иметь одну и ту же подпись. Поэтому наш выбор:

  • Ничего не делай. Позвольте этим сумасшедшим, нереалистичным программам продолжать поведение, которое точно не соответствует спецификации С#.
  • Используйте эвристику. Как отмечает Сэм, мы можем быть более умны в использовании механизмов метаданных, чтобы сообщить CLR, какие методы переопределяют другие методы. Но... эти механизмы используют сигнатуры метода для устранения двусмысленных случаев, и теперь мы вернулись в ту же лодку, что и раньше; теперь мы используем механизм с поведением, определенным реализацией, чтобы устранить проблему с реализацией, определяемой реализацией! Это не стартер.
  • Причина, по которой компилятор создает предупреждения или ошибки, когда он может испускать программу, поведение которой определяется реализацией среды выполнения.
  • Исправить CLR, чтобы поведение топологий типов, которые вызывают методы для унификации в сигнатуре, хорошо определено и соответствует языку С#.

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

Поэтому мы в команде компилятора С# решили использовать комбинацию первой и третьей стратегий; иногда мы создаем предупреждения или ошибки для таких ситуаций, и иногда мы ничего не делаем и позволяем программе делать что-то странное во время выполнения.

Поскольку на практике подобные программы очень редко возникают в реалистичных сценариях бизнес-программирования, я не чувствую себя очень плохо в этих случаях. Если бы они были дешевыми и их легко исправить, мы бы их исправили, но они не были дешевыми и легкими для исправления.

Если этот вопрос вас интересует, см. мою статью о еще одном способе, в котором создание двух методов для объединения приводит к предупреждению и определению, определяемому реализацией:

http://blogs.msdn.com/b/ericlippert/archive/2006/04/05/odious-ambiguous-overloads-part-one.aspx

http://blogs.msdn.com/b/ericlippert/archive/2006/04/06/odious-ambiguous-overloads-part-two.aspx