C# необязательные параметры для переопределенных методов


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

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();
    }
}
9 68

9 ответов:

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

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

и выход:

derived: bbb
derived: aaa

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

  1. он определяет интерфейс для другого кода для вызова.
  2. он определяет реализацию для выполнения при вызове.

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

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

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

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

  1. подпись void MyMethod(string).
  2. (для тех языков, которые поддерживают его) значение по умолчанию для одного параметра -"aaa" и поэтому при компиляции код вида MyMethod() если нет метода соответствия MyMethod() можно найти, вы можете заменить его вызовом ' MyMethod ("aaa").

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

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

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

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

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

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

если бы я кодировал BBB тогда вызов или переопределение MyMethod(string), я бы подумал об этом как "делать AAA вещи" - это BBBs взять на " делать AAA вещи", но это делает AAA вещи все же. Следовательно, будь то вызов или переопределение, я буду знать о том, что это было AAA, что определено MyMethod(string).

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

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

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

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

this.MyMethod();

(in MyMethod2())

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

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


Edit:

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

например, набор кандидатов для вызова метода не включает методы, отмеченные override (§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).

это похоже на ошибку для меня. Я верю в это и также указано, и что он должен вести себя так же, как если бы вы называли метод с явным 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 значение не может прийти из любого места действительны. (Это null из-за того, что по умолчанию значение string тип; всегда использовать значение по умолчанию для типа параметра.)

раздел 7.6.8 спецификации имеет дело с базой.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();
    }
}

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

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

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

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

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

возможно, это связано с неоднозначностью, и компилятор отдает приоритет базовому / суперклассу. Ниже изменить код вашего класса 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 ключевое слово всякий раз, когда вы вызываете свойства или методы на текущий экземпляр класса, как лучшие практики.

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

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

EDIT: рассмотрим ниже примеры выдержек из этих ссылок:

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

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

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

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

   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: } 

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

1: подметка 2: BaseTag 3: ITag

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

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

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

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

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

согласен в целом с @Marc Gravell.

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

В Любом Случае Это Нужно Исправить

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

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

Но Это Правильно Или Неправильно?

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

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

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

есть два способа реализовать это:

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

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

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

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

  1. в .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)} 
    
  2. вы можете перегрузите метод с аргументами по умолчанию другим методом без аргументов (это имеет катастрофические последствия, как я буду обсуждать через несколько минут), поэтому код folloing является законным:

    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!
    
  3. при использовании отражения необходимо всегда указывать значение по умолчанию.

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

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

однако есть реальные проблемы с подходом .Net.

  1. последовательность

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

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

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

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

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

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

резюме: держитесь подальше от необязательных аргументов и используйте вместо этого перегрузки (как это делает сама платформа .NET FRAMEWORK)