「ref」と「out」が多態性をサポートしないのはなぜですか?


124

次の点を考慮してください。

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= compile-time error: 
                     // "The 'ref' argument doesn't match the parameter type"
    }

    void Foo(A a) {}

    void Foo2(ref A a) {}  
}

上記のコンパイル時エラーが発生するのはなぜですか?これはrefout引数の両方で発生します。

回答:


169

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

更新:私はこの回答をこのブログエントリの基礎として使用しました。

refおよびoutパラメータで型のバリエーションが許可されないのはなぜですか?

この問題の詳細については、ブログページを参照してください。すばらしい質問をありがとう。

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

あなたがクラスを持っているとしましょうAnimalMammalReptileGiraffeTurtleおよびTiger、明白なサブクラスの関係を持ちます。

次に、メソッドがあるとしますvoid M(ref Mammal m)M読み取りと書き込みの両方ができますm


あなたは、型の変数を渡すことができますAnimalM

いいえ。その変数にはを含めることができますがTurtleM哺乳類のみが含まれていると想定されます。A TurtleはではありませんMammal

結論1refパラメータを「大きく」することはできません。(哺乳類よりも動物の方が多いため、より多くのものを含むことができるため、変数は「大きく」なります。)


あなたは、型の変数を渡すことができますGiraffeM

いいえMに書き込むことができm、そしてM書きたいかもしれないTigerにしmTigerこれで、実際に型である変数にを入れましたGiraffe

結論2refパラメータを「小さく」することはできません。


今考えなさいN(out Mammal n)

あなたは、型の変数を渡すことができますGiraffeN

いいえNに書き込むことができn、そしてN書きたいかもしれませんTiger

結論3outパラメータを「小さく」することはできません。


あなたは、型の変数を渡すことができますAnimalN

うーん。

さて、なぜですか? Nから読み取ることはできません。n書き込むことしかできません。Tiger型の変数にを書き込んで、Animalすべて設定しましたよね?

違う。ルールは「N書き込みのみ可能」ではありませんn

ルールは、簡単に言えば:

1)正常に戻る前Nに書き込む必要があります。(スローした場合、すべてのベットはオフになります。)nNN

2)Nが何かをn読み取る前に、何かに書き込む必要がありますn

これにより、この一連のイベントが可能になります。

  • x型のフィールドを宣言しますAnimal
  • パラメータxとしてに渡しoutますN
  • N書き込みTigernの別名です、x
  • 別のスレッドでは、誰かが書き込みを行うTurtleにはx
  • Nはの内容を読み取ろうとし、型の変数であると考えているものnを見つけTurtleますMammal

明らかにそれを違法にしたいのです。

結論4outパラメータを「大きく」することはできません。


最終的な結論どちらのパラメータもタイプを変えることはできませrefout。そうでなければ、検証可能な型安全性を破ることになります。

基本的な型理論のこれらの問題に興味がある場合は、C#4.0での共分散と反変がどのように機能するかについての私のシリーズを読むことを検討してください。


6
+1。問題を明確に示す実際のクラスの例を使用した優れた説明(つまり、A、B、およびCで説明すると、動作しない理由を示すことが難しくなります)。
グラントワグナー

4
私はこの思考過程を読んで謙虚に感じています。本に戻ったほうがいいと思います!
スコットマッケンジー

この場合、引数としてAbstractクラス変数を使用して、その派生クラスオブジェクトを渡すことはできません。
Prashant Cholachagudda

それでも、なぜoutパラメータを「大きく」できないのですか?説明したシーケンスは、outパラメーター変数だけでなく、任意の変数に適用できます。また、読者は引数Mammalにアクセスする前に引数の値をキャストする必要があります。Mammalもちろん、
思いやり

29

どちらの場合でも、ref / outパラメータに値を割り当てることができる必要があるためです。

参照としてbをFoo2メソッドに渡そうとし、Foo2でa = new A()をアサートしようとすると、これは無効になります。
あなたが書けない同じ理由:

B b = new A();

+1要点をまっすぐにして、その理由を完璧に説明します。
Rui Craveiro、

10

共分散(および反変)の古典的なOOP問題に苦しんでいます。ウィキペディアを参照してください。この事実は直感的な期待に反する可能性があるため、可変(割り当て可能)引数の基本クラスの代わりに派生クラスの置換を許可することは数学的に不可能です(そして同じ理由でアイテムを割り当てることができるコンテナも)リスコフの原則を尊重します。なぜそうなのかは、既存の回答で概説されており、これらのWiki記事とそこからのリンクでより深く探究されています。

従来は静的に型保証されたままであるように見えるOOP言語は「不正」です(非表示の動的型チェックを挿入するか、チェックするためにすべてのソースのコンパイル時検査が必要です)。基本的な選択は次のとおりです。この共分散をあきらめて、実践者の困惑を受け入れる(C#がここで行うようにする)か、動的型付けアプローチに移行する(最初のOOP言語であるSmalltalkがそうである)か、不変(単一)に移行する割り当て)関数型言語のようにデータ(不変性の下では、共分散をサポートでき、また、変更可能なデータの世界でSquareサブクラスのRectangleを使用できないなどの他の関連するパズルを回避できます)。


4

考慮してください:

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

タイプセーフに違反します


そこに問題があるのは、変数による「b」の推測されたタイプが不明確なためです。

6行目では、=> B b = nullを意味していたと思います。
Alejandro Miralles 2015

@amiralles-はい、それvarは完全に間違っていました。修繕。
Henk Holterman、2015

4

他の応答はこの動作の背後にある理由を簡潔に説明していますが、この種の性質を実際に行う必要がある場合は、Foo2をジェネリックメソッドにすることで同様の機能を実現できることを言及する価値があると思います。

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= no compile error!
    }

    void Foo(A a) {}

    void Foo2<AType> (ref AType a) where AType: A {}  
}

2

の一部を埋める方法しかわからないため、a を指定Foo2するref Bとオブジェクトの形式が不正になるためです。Foo2AB


0

コンパイラーがオブジェクトを明示的にキャストして、意図が何であるかが確実にわかるようにしたいと言っているのではないですか?

Foo2(ref (A)b)

それはできません、「refまたはout引数は割り当て可能な変数でなければなりません」

0

安全性の観点からは理にかなっていますが、参照で渡されるpolymoprhicオブジェクトの正当な使用があるため、コンパイラーがエラーの代わりに警告を出せば、私はそれを優先しました。例えば

class Derp : interfaceX
{
   int somevalue=0; //specified that this class contains somevalue by interfaceX
   public Derp(int val)
    {
    somevalue = val;
    }

}


void Foo(ref object obj){
    int result = (interfaceX)obj.somevalue;
    //do stuff to result variable... in my case data access
    obj = Activator.CreateInstance(obj.GetType(), result);
}

main()
{
   Derp x = new Derp();
   Foo(ref Derp);
}

これはコンパイルされませんが、動作しますか?


0

タイプの実用的な例を使用すると、次のようになります。

SqlConnection connection = new SqlConnection();
Foo(ref connection);

そして今、あなたは祖先を取る(すなわち Object)関数を持っています:

void Foo2(ref Object connection) { }

何が問題になる可能性がありますか?

void Foo2(ref Object connection)
{
   connection = new Bitmap();
}

あなただけ割り当てることに成功しBitmap、あなたにSqlConnection

それは良くないね。


他のユーザーとやり直してください:

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

void Foo2(ref DbConnection connection)
{
    conn = new OracleConnection();
}

OracleConnectionオーバートップを詰め込みましたSqlConnection


0

私の場合、関数はオブジェクトを受け入れ、何も送信できなかったので、単純に行いました

object bla = myVar;
Foo(ref bla);

そしてそれはうまくいく

私のFooはVB.NETにあり、内部の型をチェックし、多くのロジックを実行します

私の答えが重複しているが、他の人が長すぎると申し訳ありません

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.