Дополнительные параметры С# для переопределенных методов - программирование
Подтвердить что ты не робот

Дополнительные параметры С# для переопределенных методов

Похоже, что в .NET Framework возникает проблема с необязательными параметрами при переопределении метода. Вывод кода ниже: "БББ" "Ааа" , Но ожидаемый результат: "БББ" "БББ" .Есть ли решение для этого. Я знаю, что это можно решить с перегрузкой метода, но интересно, зачем это нужно. Также код отлично работает в Mono.

class Program
{
    class AAA
    {
        public virtual void MyMethod(string s = "aaa")
        {
            Console.WriteLine(s);
        }

        public virtual void MyMethod2()
        {
            MyMethod();
        }
    }

    class BBB : AAA
    {
        public override void MyMethod(string s = "bbb")
        {
            base.MyMethod(s);
        }

        public override void MyMethod2()
        {
            MyMethod();
        }
    }

    static void Main(string[] args)
    {
        BBB asd = new BBB();
        asd.MyMethod();
        asd.MyMethod2();
    }
}
4b9b3361

Ответ 1

Здесь стоит отметить, что переопределенная версия вызывается каждый раз. Измените переопределение на:

public override void MyMethod(string s = "bbb")
{
  Console.Write("derived: ");
  base.MyMethod(s);
}

И результат:

derived: bbb
derived: aaa

Метод в классе может выполнять одно или два из следующих действий:

  • Он определяет интерфейс для вызова другого кода.
  • Он определяет реализацию, выполняемую при вызове.

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

Внутри BBB вызов MyMethod() вызывает метод , определенный в AAA.

Поскольку в BBB есть переопределение, вызов этого метода приводит к вызову в BBB.

Теперь определение в AAA сообщает код вызова двух вещей (ну, некоторые другие тоже не имеют значения здесь).

  • Подпись void MyMethod(string).
  • (Для тех языков, которые его поддерживают) значение по умолчанию для одного параметра равно "aaa", и поэтому при компиляции кода формы MyMethod(), если метод, соответствующий MyMethod(), не найден, вы можете заменить его на вызовите `MyMethod ( "aaa" ).

Итак, что делает вызов в BBB: компилятор видит вызов MyMethod(), не находит метод MyMethod(), но находит метод MyMethod(string). Он также видит, что в том месте, где оно определено, есть значение по умолчанию "aaa", поэтому во время компиляции оно меняет это на вызов MyMethod("aaa").

Внутри BBB, AAA считается местом, где определены методы AAA, даже если они переопределены в BBB, чтобы они могли быть перегружены.

Во время выполнения MyMethod(string) вызывается с аргументом "aaa". Поскольку существует переопределенная форма, которая называется формой, но она не вызывается с помощью "bbb", потому что это значение не имеет ничего общего с реализацией времени выполнения, но с определением времени компиляции.

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

Изменить: почему это кажется мне более интуитивным.

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

Если бы я был кодированием BBB, то если бы я вызывал или переопределял MyMethod(string), я бы подумал об этом как о "выполнении AAA stuff" - it BBB взять "делать AAA stuff", но это все делаю AAA. Следовательно, будь то вызов или переопределение, я буду знать, что он был AAA, который определил MyMethod(string).

Если бы я вызывал код, который использовал BBB, я бы подумал о "использовании BBB stuff". Возможно, я не очень хорошо знаю, что изначально было определено в AAA, и я бы подумал об этом как о простоте реализации (если я не использовал также интерфейс AAA).

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

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

Ответ 2

Вы можете устранить неоднозначность, позвонив:

this.MyMethod();

(in MyMethod2())

Является ли это ошибкой, сложно? однако это выглядит непоследовательно. Resharper предупреждает, что вы просто не имеете изменений в значение по умолчанию в переопределении, если это помогает; p Конечно, resharper также сообщает вам, что this. является избыточным и предлагает удалить его для вас..., который изменяет поведение - поэтому resharper также не является идеальным.

Похоже, он может квалифицироваться как ошибка компилятора, я дам вам. Мне нужно было бы внимательно посмотреть на самом деле, чтобы убедиться... где Эрик, когда он вам нужен, а??


Edit:

Ключевым моментом здесь является спецификация языка; посмотрим на §7.5.3:

Например, набор кандидатов для вызова метода не включает переопределенные методы (§7.4), а методы в базовом классе не являются кандидатами, если применим какой-либо метод в производном классе (§7.6.5.1).

(и, действительно, §7.4 явно не учитывает методы override)

Здесь есть какой-то конфликт... он утверждает, что базовые методы не используются, если в производном классе есть применимый метод, который приведет нас к производному методу, но в то же время он говорит, что методы отмеченные override не рассматриваются.

Но в п. 7.5.1.1 говорится:

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

а затем в §7.5.1.2 объясняется, как значения оцениваются во время вызова:

Во время выполнения вызова функции-члена (§7.5.4) выражения или ссылки на переменные списка аргументов оцениваются по порядку слева направо следующим образом:

... (надрез)...

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

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

Это означало бы: csc имеет ошибку, и он должен использовать производную версию ( "bbb bbb" ), если только она не ограничена (через base. или литье в базовый тип), чтобы посмотреть объявления базового метода (§7.6.8).

Ответ 3

Это выглядит как ошибка для меня. Я считаю, что это хорошо указано, и что он должен вести себя так же, как если бы вы вызывали метод с явным префиксом this.

Я упростил этот пример, чтобы использовать только один виртуальный метод, и покажите, какую реализацию вызывается и что значение параметра:

using System;

class Base
{
    public virtual void M(string text = "base-default")
    {
        Console.WriteLine("Base.M: {0}", text);
    }   
}

class Derived : Base
{
    public override void M(string text = "derived-default")
    {
        Console.WriteLine("Derived.M: {0}", text);
    }

    public void RunTests()
    {
        M();      // Prints Derived.M: base-default
        this.M(); // Prints Derived.M: derived-default
        base.M(); // Prints Base.M: base-default
    }
}

class Test
{
    static void Main()
    {
        Derived d = new Derived();
        d.RunTests();
    }
}

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

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

И раздел 7.5.1.2:

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

"Соответствующий необязательный параметр" - это бит, который связывает 7.5.2 - 7.5.1.1.

Для обоих M() и this.M() этот список параметров должен быть один в Derived как статический тип приемника Derived, Действительно, вы можете сказать, что компилятор обращается что в качестве списка параметров раньше в компиляции, как если бы вы сделайте параметр обязательным в Derived.M(), оба вызовы терпят неудачу - поэтому вызов M() требует, чтобы параметр имел значение по умолчанию в Derived, но затем игнорирует его!

Действительно, это ухудшается: если вы предоставили значение по умолчанию для параметр в Derived, но сделать его обязательным в Base, вызов M() заканчивается использованием null в качестве значения аргумента. Если ничего другого, Я бы сказал, что это свидетельствует об ошибке: значение null не может прийти из любого места. (It null из-за того, что это значение по умолчанию значение типа string; он всегда просто использует значение по умолчанию для типа параметра.)

В разделе 7.6.8 spec сделок с базой .M(), в которой говорится, что а также не виртуальное поведение, выражение рассматривается как ((Base) this).M(); поэтому он полностью исправляется для базового метода для определения эффективного списка параметров. Это значит окончательная строка верна.

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

using System;

class Base
{
    public virtual void M(int x)
    {
        // This isn't called
    }   
}

class Derived : Base
{
    public override void M(int x = 5)
    {
        Console.WriteLine("Derived.M: {0}", x);
    }

    public void RunTests()
    {
        M();      // Prints Derived.M: 0
    }

    static void Main()
    {
        new Derived().RunTests();
    }
}

Ответ 4

Вы пробовали:

 public override void MyMethod2()
    {
        this.MyMethod();
    }

Итак, вы на самом деле говорите своей программе, чтобы использовать метод overriden.

Ответ 5

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

Сегодня в кампусе выпало много снега, и Сиэтл не очень хорошо справляется со снегом. Мой автобус сегодня не работает, поэтому я не смогу попасть в офис, чтобы сравнить то, что С# 4, С# 5 и Roslyn говорят об этом случае, и если они не согласны. Я попытаюсь опубликовать анализ позже на этой неделе, когда вернусь в офис и смогу использовать надлежащие инструменты для отладки.

Ответ 6

Возможно, это связано с двусмысленностью, и компилятор уделяет приоритетное внимание базовому/суперклассу. Ниже приведено изменение кода вашего класса BBB с добавлением ссылки на ключевое слово this, выдает вывод "bbb bbb":

class BBB : AAA
{
    public override void MyMethod(string s = "bbb")
    {
        base.MyMethod(s);
    }

    public override void MyMethod2()
    {
        this.MyMethod(); //added this keyword here
    }
}

Одна из вещей, которую он подразумевает, - это всегда использовать ключевое слово this всякий раз, когда вы вызываете свойства или методы в текущем экземпляре класса как наилучшая практика.

Я был бы обеспокоен, если бы эта двусмысленность в методе base и child даже не вызывала предупреждение о компиляторе (если бы не ошибка), но если это произойдет, то это было невидимым, я полагаю.

=============================================== ===================

РЕДАКТИРОВАТЬ: Рассмотрим ниже примерные отрывки из этих ссылок:

http://geekswithblogs.net/BlackRabbitCoder/archive/2011/07/28/c.net-little-pitfalls-default-parameters-are-compile-time-substitutions.aspx

http://geekswithblogs.net/BlackRabbitCoder/archive/2010/06/17/c-optional-parameters---pros-and-pitfalls.aspx

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

Pitfall: Остерегайтесь параметров по умолчанию в наследовании и реализации интерфейса

Теперь вторая потенциальная проблема связана с наследованием и реализацией интерфейса. Ill иллюстрируют головоломкой:

   1: public interface ITag 
   2: {
   3:     void WriteTag(string tagName = "ITag");
   4: } 
   5:  
   6: public class BaseTag : ITag 
   7: {
   8:     public virtual void WriteTag(string tagName = "BaseTag") { Console.WriteLine(tagName); }
   9: } 
  10:  
  11: public class SubTag : BaseTag 
  12: {
  13:     public override void WriteTag(string tagName = "SubTag") { Console.WriteLine(tagName); }
  14: } 
  15:  
  16: public static class Program 
  17: {
  18:     public static void Main() 
  19:     {
  20:         SubTag subTag = new SubTag();
  21:         BaseTag subByBaseTag = subTag;
  22:         ITag subByInterfaceTag = subTag; 
  23:  
  24:         // what happens here?
  25:         subTag.WriteTag();       
  26:         subByBaseTag.WriteTag(); 
  27:         subByInterfaceTag.WriteTag(); 
  28:     }
  29: } 

Что происходит? Ну, хотя объектом в каждом случае является SubTag, чей тег "SubTag", вы получите:

1: SubTag  2: BaseTag  3: ITag

Но не забудьте убедиться, что вы:

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

=============================================== ===========================

Ответ 7

Я думаю, это потому, что эти значения по умолчанию фиксируются во время компиляции. Если вы используете рефлектор, вы увидите следующее для MyMethod2 в BBB.

public override void MyMethod2()
{
    this.MyMethod("aaa");
}

Ответ 8

Любой способ, которым нужно исправить

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

Я бы порекомендовал вам сообщить об этом Microsoft.Connect

Но это правильно или неправильно?

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

считаем, что у нас есть следующий код:

void myfunc(int optional = 5){ /* Some code here*/ } //Function implementation
myfunc(); //Call using the default arguments

Существует два способа его реализации:

  • Эти необязательные аргументы обрабатываются как перегруженные функции, что приводит к следующему:

    void myfunc(int optional){ /* Some code here*/ } //Function implementation
    void myfunc(){ myfunc(5); } //Default arguments implementation
    myfunc(); //Call using the default arguments
    
  • Значение по умолчанию встроено в вызывающий, что приводит к следующему коду:

    void myfunc(int optional){ /* Some code here*/ } //Function implementation
    myfunc(5); //Call and embed default arguments
    

Существует много различий между двумя подходами, но мы сначала рассмотрим, как интерпретирует его .Net framework.

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

    class bassClass{ public virtual void someMethod()}
    class subClass :bassClass{ public override void someMethod()} //Legal
    //The following is illegal, although it would be called as someMethod();
    //class subClass:bassClass{ public override void someMethod(int optional = 5)} 
    
  • Вы можете перегружать метод аргументами по умолчанию другим методом без аргументов (это имеет катастрофические последствия, как я расскажу в некоторые моменты), поэтому следующий код является законным:

    void myfunc(int optional = 5){ /* Some code here*/ } //Function with default
    void myfunc(){ /* Some code here*/ } //No arguments
    myfunc(); //Call which one?, the one with no arguments!
    
  • при использовании отражения всегда необходимо указать значение по умолчанию.

Все из них достаточно, чтобы доказать, что .Net взяла вторую реализацию, поэтому поведение, которое видит ОР, является правильным, по крайней мере, согласно .Net.

Проблемы с .Net-подходом

Однако существуют реальные проблемы с подходом .Net.

  • Согласованность

    • Как и в проблеме OP при переопределении значения по умолчанию в унаследованном методе, результаты могут быть непредсказуемыми

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

    • Reflection требует, чтобы вы предоставили значение по умолчанию, которое вызывающий не должен знать
  • Разрыв кода

    • Когда у нас есть функция с аргументами по умолчанию и последняя, ​​мы добавляем функцию без аргументов, все вызовы теперь будут перенаправляться на новую функцию, тем самым нарушая весь существующий код без уведомления или предупреждения!

    • Аналогично будет, если позже мы уберем функцию без аргументов, тогда все вызовы автоматически перейдут к функции с аргументами по умолчанию, опять же без уведомления или предупреждения! хотя это может быть не намерение программиста

    • Кроме того, он не должен быть обычным методом экземпляра, метод расширения будет выполнять те же проблемы, поскольку метод расширения без параметров будет иметь приоритет над методом экземпляра с параметрами по умолчанию!

Резюме: ОСТАВАЙТЕСЬ ОТ ДОПОЛНИТЕЛЬНЫХ АРГУМЕНТОВ И ИСПОЛЬЗУЙТЕ ПЕРЕГРУЗКИ ВМЕСТЕ (КАК ОСОБЕННОСТИ СТАНДАРТА ЕСТЬ)

Ответ 9

Согласитесь в целом с @Marc Gravell.

Однако я хотел бы упомянуть, что проблема достаточно старая в мире С++ (http://www.devx.com/tips/Tip/12737), и ответ выглядит "В отличие от виртуальных функций, которые разрешены во время выполнения, аргументы по умолчанию разрешаются статически, то есть в момент компиляции". Таким образом, это поведение компилятора С# скорее было принято сознательно из-за последовательности, несмотря на его неожиданность, похоже.