Изменение модификатора params в переопределении метода

Я знаю, что модификатор params (который превращается в один параметр типа массива в так называемый "массив параметров" ) специально не является частью сигнатуры метода. Теперь рассмотрим этот пример:

class Giraffid
{
    public virtual void Eat(int[] leaves)
    {
        Console.WriteLine("G");
    }
}
class Okapi : Giraffid
{
    public override void Eat(params int[] leaves)
    {
        Console.WriteLine("O");
    }
}

Это компиляция без предупреждений. Затем, сказав:

var okapi = new Okapi();
okapi.Eat(2, 4, 6);  // will not compile! 

дает ошибку (No overload for method 'Eat' takes 3 arguments).

Теперь я знаю, что компилятор переводит модификатор params в приложение System.ParamArrayAttribute на соответствующий параметр. В общем, нет никакой проблемы при применении одного набора атрибутов к параметру виртуального метода, а затем декорирования "соответствующего" параметра в переопределяющем методе в производном классе с другим набором атрибутов.

Однако компилятор предпочитает игнорировать мое ключевое слово params. И наоборот, если сделать это наоборот, и применяет params к параметру в базовом классе Giraffid, а затем опускает ключевое слово в переопределении в Okapi, компилятор решает украсить оба метода с помощью System.ParamArrayAttribute. Конечно, я проверял эти вещи с IL DASM.

Мой вопрос:

Является ли это документированным поведением? Я тщательно изучил спецификацию языка С#, не найдя упоминания об этом.

Я могу сказать, что по крайней мере среда разработки Visual Studio запуталась в этом. При вводе 2, 4, 6 в вызове метода выше intellisense показывает мне void Okapi.Eat(params int[] leaves) в подсказке.


Для сравнения я также попытался реализовать метод интерфейса и изменить присутствие/отсутствие params в интерфейсе и классе реализации, и я попытался определить тип делегата и изменить params или нет в определении типа делегата или метод, группа методов которого я назначен переменной моего типа делегата. В этих случаях было вполне возможно изменить params -ness.

Ответ 1

Поведение компилятора корректно, но это немного беспорядок. Я бы предпочел, чтобы это было как минимум предупреждением.

Неудивительно, что вы не можете найти, где в спецификации сказано, что это правильно. Соответствующие биты:

Обработка времени привязки метода вызова формы M (A), где M - группа методов, а A - необязательный список аргументов, состоит из следующих шагов: Набор методов-кандидатов для метода вызывается вызов. Для каждого метода F, связанного с группой методов M, если F не является общим, F является кандидатом, когда M не имеет списка аргументов типа, а F применим относительно A.

Каковы "методы, связанные с группой методов M"? Ну, во-первых, что такое группа методов?

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

ОК, так что это правила поиска членов?

В противном случае набор состоит из всех доступных членов с именем N в T, включая унаследованные члены и доступные члены с именем N в объекте. Члены, которые включают модификатор переопределения, исключаются из набора.

Добавлен акцент.

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

virtual void M(int x, int y) { }
...
override void M(int y, int x) { } 
...
M(x = 1, y = 2);

Разрешение перегрузки использует имена из более производной версии. Это неудачное следствие того, что именованные аргументы были добавлены очень поздно в игре.

Вкратце: для определения того, является ли метод "параметрами" или нет, анализ выполняется по исходному методу, а не по методу переопределения.

Было бы неплохо, если бы компилятор дал вам предупреждение.

может сказать, что по крайней мере среда разработки Visual Studio запуталась в этом

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

Ответ 2

Я думаю, что это описано в пункте 1.6.6.4 пункта спецификации С#:

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

В соответствии с этим, здесь действительно важно объявление метода virtual. И объявление метода virtual используется для каждого вызова этого метода. Исправить override n реализаций (если задано) взяты во время выполнения, где params не имеет ничего общего.

Это может быть подтверждено простым тестом:

class Giraffid
{
    public virtual void Eat(params int[] leaves)
    {
        Console.WriteLine("G");
    }
}
class Okapi : Giraffid
{
    public override void Eat(int[] leaves)
    {
        Console.WriteLine("O");
    }
}

С этим объявлением

var o = new Okapi();
o.Eat(1, 2, 3);

работает на 100%.