web-dev-qa-db-ja.com

オーバーライドされたメソッドのC#オプションパラメーター

.NET Frameworkの場合、メソッドをオーバーライドするとオプションのパラメーターに問題があるようです。以下のコードの出力は、「bbb」「aaa」です。しかし、私が期待している出力は「bbb」「bbb」です。これに対する解決策はありますか。私はそれがメソッドのオーバーロードで解決できることを知っていますが、この理由を疑問に思っています。また、コードは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();
    }
}
70
SARI

ここで注目に値することの1つは、オーバーライドされたバージョンが毎回呼び出されることです。オーバーライドを次のように変更します。

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

出力は次のとおりです。

_derived: bbb
derived: aaa
_

クラスのメソッドは、次の1つまたは2つを実行できます。

  1. 他のコードを呼び出すためのインターフェイスを定義します。
  2. 呼び出されたときに実行する実装を定義します。

抽象メソッドは前者のみを行うため、両方を行うことはできません。

BBB内で、MyMethod()を呼び出すと、メソッドが呼び出されますdefined in AAA

BBBにはオーバーライドがあるため、そのメソッドを呼び出すと、BBBの実装が呼び出されます。

さて、AAAの定義は、呼び出し側のコードに2つのことを通知します(ここでは関係ない他のいくつかも)。

  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._を追加すると、検査される定義が変更されるため、呼び出しで使用される引数が変更されます。

編集:なぜこれが私にとってより直感的に見えるのですか。

個人的に、そして私が直観的であるものについて話しているので、それは個人的であることができるだけです、私はこれが以下の理由でより直観的であるとわかります

BBBをコーディングしていた場合、MyMethod(string)を呼び出すかオーバーライドするかに関係なく、「AAAを実行する」と考えます-BBBsが引き継ぎます「AAA stuff」を実行していますが、AAA stuffをすべて同じように実行しています。したがって、呼び出しまたはオーバーライドに関係なく、MyMethod(string)を定義したのはAAAであるという事実に気付くでしょう。

BBBを使用するコードを呼び出していた場合、「BBB stuffを使用する」と思います。どちらが元々AAAで定義されているかをあまり知らないかもしれませんが、これを単なる実装の詳細と考えるかもしれません(近くにAAAインターフェイスも使用しなかった場合) 。

コンパイラの動作は私の直感と一致しているため、最初に質問を読んだときにMonoにバグがあるように思われました。検討すると、どちらが指定された動作を他よりもどのように満たすかわかりません。

ただし、個人的なレベルにとどまっている間は、抽象メソッド、仮想メソッド、またはオーバーライドされたメソッドでオプションのパラメーターを使用することは決してありません。

23
Jon Hanna

以下を呼び出すことで明確にすることができます。

_this.MyMethod();
_

MyMethod2()で)

バグかどうかは注意が必要です。ただし、一貫性がありません。 Resharperは、オーバーライドでデフォルト値を変更しないように警告しますが、それが役立つ場合はもちろんです; pもちろん、resharperalsoは_this._は冗長であり、それを削除することを提案します...これは動作を変更します-したがって、再シャーパーも完璧ではありません。

見た目はcouldコンパイラのバグと見なされます。許可します。私は本当にを確認する必要があります...エリックは、彼が必要なときにどこにいるのでしょうか?


編集:

ここで重要なのは言語仕様です。 §7.5.3を見てみましょう。

たとえば、メソッド呼び出しの候補のセットにはオーバーライドとマークされたメソッドは含まれず(§7.4)、派生クラスのメソッドが適用可能な場合、基本クラスのメソッドは候補ではありません(§7.6.5.1)。

(そして実際、§7.4はoverrideメソッドを考慮から明らかに省略しています)

ここにはいくつかの競合があります...派生クラスに適用可能なメソッドがある場合、baseメソッドは使用されないと述べています-これは私たちを導くでしょうderivedメソッドに対してですが、同時に、overrideとマークされたメソッドは考慮されないと述べています。

しかし、§7.5.1.1は次のように述べています。

クラスで定義された仮想メソッドとインデクサーの場合、パラメーターリストは、レシーバーの静的な型から開始し、その基本クラスを検索して、関数メンバーの最も具体的な宣言またはオーバーライドから選択されます。

次に、§7.5.1.2で、呼び出し時に値がどのように評価されるかを説明します。

関数メンバー呼び出しの実行時処理中(7.5.4)、引数リストの式または変数参照は、次のように左から右の順に評価されます。

...(をちょきちょきと切る)...

対応するオプションのパラメーターを持つ関数メンバーから引数が省略されると、関数メンバー宣言のデフォルト引数が暗黙的に渡されます。これらは常に一定であるため、それらの評価は残りの引数の評価順序に影響しません。

これは、最も具体的な宣言またはオーバーライドから来るものとして以前に§7.5.1.1で定義された引数リストを見ていることを明示的に強調しています。これは、§7.5.1.2で言及されている「メソッド宣言」であると考えられます。したがって、渡される値は、最も派生したものから静的型まででなければなりません。

これは、cscにバグがあり、制限されない限り(_base._を介して、または基本型にキャストする)、derived version( "bbb bbb")を使用する必要があることを示唆しています。基本メソッド宣言(§7.6.8)を見てください。

34
Marc Gravell

これは私にはバグのように見えます。 isは十分に指定されており、明示的なthisプレフィックスを付けてメソッドを呼び出すのと同じように動作するはずだと思います。

single仮想メソッドのみを使用するように例を単純化し、どの実装が呼び出され、パラメーター値が何であるかを示します。

_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内の3つの呼び出しだけです。最初の2つの呼び出しの仕様の重要な部分はセクション7.5.1.1で、対応するパラメーターを見つけるときに使用するパラメーターリストについて説明しています。

クラスで定義された仮想メソッドとインデクサーの場合、パラメーターリストは、レシーバーの静的な型から開始し、その基本クラスを検索して、関数メンバーの最も具体的な宣言またはオーバーライドから選択されます。

セクション7.5.1.2:

対応するオプションのパラメーターを持つ関数メンバーから引数が省略されると、関数メンバー宣言のデフォルト引数が暗黙的に渡されます。

「対応するオプションパラメータ」は、7.5.2から7.5.1.1を結ぶビットです。

M()this.M()の両方について、そのパラメーターリストはDerivedにあるものでなければなりません。レシーバーの静的タイプはDerivedであるため、コンパイラーはそれをコンパイルの初期のパラメーターリストとして扱い、パラメーターを作成するかのように伝えます。必須 in Derived.M()both呼び出しの失敗-したがって、M()呼び出しrequiresは、Derivedにデフォルト値を設定しますが、無視します!

実際、さらに悪化します。Derivedのパラメーターにデフォルト値を指定し、Baseで必須にすると、M()の呼び出しはnullを引数値として。他に何もなければ、それがバグであることを証明していると思います:nullの値はどこからでも有効ではありません。 (これはnullタイプのデフォルト値であるため、stringです。常にパラメータータイプのデフォルト値を使用します。)

仕様のセクション7.6.8はbase.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();
    }
}
_
15
Jon Skeet

やってみました:

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

したがって、実際にプログラムにオーバーライドされたメソッドを使用するように指示します。

10
basti

動作は間違いなく非常に奇妙です。それが実際にコンパイラのバグであるかどうかは私には明らかではありませんが、そうかもしれません。

昨夜、キャンパスはかなりの量の雪になり、シアトルは雪を扱うことについてあまり良くありません。私のバスは今朝走っていないので、オフィスに行って、このケースについてC#4、C#5、およびRoslynが言うことと、意見が一致しないかどうかを比較することはできません。オフィスに戻って適切なデバッグツールを使用できるようになったら、今週後半に分析を投稿しようとします。

9
Eric Lippert

これはあいまいさが原因であり、コンパイラがベース/スーパークラスを優先している可能性があります。 thisキーワードへの参照を追加したクラスBBBのコードの以下の変更により、出力 '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キーワードを常に使用する必要があるということの1つです。

このベースメソッドと子メソッドのあいまいさがコンパイラ警告(エラーでない場合)を発生させないかどうかが心配ですが、もしそうならそれは見えなかったと思います。

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

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つだけです。このことを念頭に置いておくと、その使用に伴う潜在的な落とし穴を十分に理解して回避できる可能性があります:その1つは、オプションのパラメーターがコンパイル時の構文糖です!

落とし穴:継承およびインターフェイス実装のデフォルトパラメータに注意してください

現在、2番目の潜在的な落とし穴は、継承とインターフェイスの実装に関係しています。パズルで説明します。

   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:サブタグ2:ベースタグ3:ITag

ただし、次のことを確認してください。

既存のデフォルトパラメータのセットの途中に新しいデフォルトパラメータを挿入しないでください。これにより、必ずしも構文エラーがスローされるとは限らない予測できない動作が発生する可能性があります。リストの最後に追加するか、新しいメソッドを作成します。継承階層およびインターフェイスでデフォルトパラメータを使用する方法には非常に注意してください。最も適切なレベルを選択して、予想される使用法に基づいてデフォルトを追加してください。

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

5
VS1

これは、これらのデフォルト値がコンパイル時に修正されるためだと思います。リフレクターを使用すると、BBBのMyMethod2について次のように表示されます。

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

一般的に@Marc Gravellに同意します。

ただし、この問題はC++の世界では十分に古い( http://www.devx.com/tips/Tip/12737 )ため、答えは「仮想とは異なり」実行時に解決される関数、デフォルト引数は静的に、つまりコンパイル時に解決されます。」そのため、このC#コンパイラの動作は、予期せぬことにかかわらず、一貫性のために意図的に受け入れられていたようです。

0

修正が必要な方法

結果が間違っているか、結果が予想される場合、コンパイラはそれを「オーバーライド」として宣言させたり、少なくとも警告を出すべきではないため、間違いなくバグと見なします。

これをMicrosoft.Connectに報告することをお勧めします

しかしそれは正しいか間違っていますか?

ただし、これが予想される動作であるかどうかについては、まず、2つのビューを分析してみましょう。

次のコードがあると考えてください:

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

実装するには2つの方法があります。

  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
    

2つのアプローチには多くの違いがありますが、最初に.Netフレームワークがそれをどのように解釈するかを見ていきます。

  1. .Netでは、同じ数の引数を含むメソッドでのみメソッドをオーバーライドできますが、more引数を含むメソッドでオーバーライドすることはできません。それらがすべてオプションである場合(オーバーライドされたメソッドと同じシグネチャを持つ呼び出しが発生する)、たとえば次のようになります:

    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. デフォルトの引数を持つメソッドを引数のない別のメソッドでオーバーロードすることができます(これについては後で説明しますが、これは悲惨な意味を持ちます)。したがって、次のコードは有効です。

    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が2番目の実装を採用したことを証明するのに十分なため、少なくとも.Netによれば、OPが見た動作は正しいです。

。Netアプローチの問題

ただし、.Netアプローチには実際の問題があります。

  1. 一貫性

    • 継承されたメソッドでデフォルト値をオーバーライドするときのOPの問題のように、結果は予測できない場合があります

    • デフォルト値の元の埋め込みが変更され、呼び出し元を再コンパイルする必要がないため、無効になったデフォルト値になる可能性があります

    • Reflectionでは、デフォルト値を指定する必要がありますが、呼び出し側はこれを知る必要はありません
  2. コードの破壊

    • デフォルトの引数を持つ関数があり、後者の場合、引数のない関数を追加すると、すべての呼び出しが新しい関数にルーティングされるため、通知や警告なしで既存のすべてのコードが破壊されます!

    • 後で引数なしで関数を削除すると、すべての呼び出しは自動的にデフォルトの引数で関数にルーティングされ、再び通知も警告もありません!これはプログラマの意図ではないかもしれませんが

    • さらに、通常のインスタンスメソッドである必要はありません。パラメータを持たない拡張メソッドはデフォルトパラメータをもつインスタンスメソッドよりも優先されるため、拡張メソッドも同じ問題を起こします。

概要:オプションの引数から離れて、オーバーロードをそのまま使用する(.NETフレームワーク自体が行うように)

0
yoel halb