ダウンキャストの適切な使用とは何ですか?


68

ダウンキャストとは、基本クラス(またはインターフェイス)からサブクラスまたはリーフクラスにキャストすることです。

ダウンキャストの例は、System.Object他のタイプにキャストする場合です。

ダウンキャストは人気がありませんが、コードの匂いかもしれません。例えば、オブジェクト指向の教義は、ダウンキャストの代わりに仮想メソッドまたは抽象メソッドを定義して呼び出すことを好むことです。

  • ダウンキャストの適切で適切なユースケースは、もしあれば、何ですか?つまり、ダウンキャストするコードを書くことが適切なのはどのような状況ですか?
  • 答えが「なし」である場合、ダウンキャストが言語でサポートされているのはなぜですか?

5
あなたが説明するものは、通常「ダウンキャスティング」と呼ばれます。その知識を武器に、最初に自分自身でさらに調査を行い、見つかった既存の理由が十分ではない理由を説明できますか?TL; DR:ダウンキャストは静的型付けを破壊しますが、型システムが制限的すぎる場合があります。したがって、言語が安全なダウンキャスティングを提供することは賢明です。
アモン

ダウンキャストを意味しますか?...スーパークラスからサブクラスに、ダウンキャストとは対照的にアップキャストは、つまり、サブクラスからスーパークラスに、クラス階層をアップだろう
アンドレイSocaciu

私の謝罪:あなたは正しい。質問を編集して、「アップキャスト」の名前を「ダウンキャスト」に変更しました。
ChrisW

@amon en.wikipedia.org/wiki/Downcastingでは、例として「文字列に変換」を提供しています(.Netはvirtualの使用をサポートしていますToString)。もう1つの例は「Javaコンテナー」です。これは、Javaがジェネリックをサポートしていないためです(C#はサポートしています)。stackoverflow.com/questions/1524197/downcast-and-upcastは、ダウンキャスティングは何であるかを示していますが、適切なタイミングのありません。
ChrisW

4
@ChrisWそうbefore Java had support for genericsではありませんbecause Java doesn't support generics。そして、amonはほとんどあなたに答えを与えました-あなたが使っている型システムがあまりにも制限的で、あなたがそれを変更する立場にないとき。
オーダス

回答:


12

ダウンキャスティングの適切な使用法を次に示します。

対応する問題を解決する他の合理的な方法はないと考えているため、ダウンキャストの使用は間違いなくコードの匂いだと言う他の人々に敬意を持っ反対します。

Equals

class A
{
    // Nothing here, but if you're a C++ programmer who dislikes object.Equals(object),
    // pretend it's not there and we have abstract bool A.Equals(A) instead.
}
class B : A
{
    private int x;
    public override bool Equals(object other)
    {
        var casted = other as B;  // cautious downcast (dynamic_cast in C++)
        return casted != null && this.x == casted.x;
    }
}

Clone

class A : ICloneable
{
    // Again, if you dislike ICloneable, that's not the point.
    // Just ignore that and pretend this method returns type A instead of object.
    public virtual object Clone()
    {
        return this.MemberwiseClone();  // some sane default behavior, whatever
    }
}
class B : A
{
    private int[] x;
    public override object Clone()
    {
        var copy = (B)base.Clone();  // known downcast (static_cast in C++)
        copy.x = (int[])this.x.Clone();  // oh hey, another downcast!!
        return copy;
    }
}

Stream.EndRead/Write

class MyStream : Stream
{
    private class AsyncResult : IAsyncResult
    {
        // ...
    }
    public override int EndRead(IAsyncResult other)
    {
        return Blah((AsyncResult)other);  // another downcast (likely ~static_cast)
    }
}

これらがコードのにおいであると言うのであれば、それらに対してより良い解決策を提供する必要があります(それらが不便のために避けられたとしても)。より良いソリューションが存在するとは思わない。


9
「コードの匂い」は「このデザインが間違いなく悪い」という意味ではなく、「このデザインが疑わしい」という意味です。本質的に疑わしい言語機能が存在することは、言語の作者側のプラグマティズムの問題です
-Caleth

5
@Caleth:そして私の全体的なポイントは、これらのデザインが常に登場し、私が示したデザインについて本質的に疑わしいものまったくないということです。したがって、キャストはコードの匂いではありません。
Mehrdad

4
@キャレス私は同意しません。私にとって、コードのにおいは、コードが悪いか、少なくとも改善できることを意味します。
コンラッドルドルフ

6
@Mehrdadあなたの意見では、コードの臭いはアプリケーションプログラマのせいだと思われます。どうして?すべてのコードの匂いが実際の問題である必要も、解決策がある必要もありません。Martin Fowlerによるコードの匂いは、単に「システムのより深い問題に通常対応する表面の兆候」であり、object.Equalsその説明に非常によく当てはまります(サブクラスが正しく実装できるスーパークラスでequalsコントラクトを正しく提供してみてください-それは絶対です)自明ではありません)。アプリケーションプログラマとしてそれを解決できない(場合によっては回避できる)ということは、別のトピックです。
Voo

5
@Mehrdadさて、元の言語デザイナーの1人がEqualsについて何と言っているか見てみましょう。Object.Equals is horrid. Equality computation in C# is completely messed up(彼の答えに対するEricのコメント)。言語のリードデザイナーの1人があなたが間違っていると言っても、あなたの意見を再考したくないのはおかしいです。
VOO

136

ダウンキャスティングは人気がなく、コードの匂いかもしれません

同意しません。ダウンキャスティングは非常に人気があります。膨大な数の現実世界のプログラムには、1つ以上のダウンキャストが含まれています。そして、それはおそらくコードの匂いではありませ。それは間違いなくコードの匂いです。 そのため、プログラムのテキストでマニフェストをダウンキャストする必要があります。これにより、匂いに簡単に気づき、コードレビューに注意を払うことができます。

どのような状況で、ダウンキャストするコードを書くことが適切ですか?

どんな状況でも:

  • 式のコンパイル時の型よりも具体的な式の実行時の型に関する事実について100%正しい知識を持っている。
  • コンパイル時の型では使用できないオブジェクトの機能を使用するには、その事実を利用する必要があります。
  • プログラムをリファクタリングして最初の2つのポイントのいずれかを排除するよりも、キャストを作成する方が時間と労力を有効に活用できます。

コンパイラがランタイムタイプを推測できるようにプログラムを安価にリファクタリングできる場合、またはより派生したタイプの機能を必要としないようにプログラムをリファクタリングする場合は、そうします。ダウンキャストは、プログラムをリファクタリングするのが困難費用がかかる状況のために言語に追加されました。

ダウンキャストが言語でサポートされているのはなぜですか?

C#は、やるべき仕事がある実用的なプログラマーのために、やるべき仕事がある実用的なプログラマーによって発明されました。C#デザイナーはオブジェクト指向の純粋主義者ではありません。また、C#型システムは完全ではありません。設計上、変数のランタイム型に課せられる制限を過小評価しています。

また、ダウンキャストはC#では非常に安全です。ダウンキャストは実行時に検証され、検証できない場合はプログラムが正しいことを実行してクラッシュすることを強力に保証しています。これは素晴らしい; 型のセマンティクスに対する100%の正しい理解が99.99%であることが判明した場合、プログラムは予測不可能な振る舞いやユーザーデータの0.01%の破損ではなく、99.99%の時間動作し、残りの時間をクラッシュさせます。 。


演習:明示的なキャスト演算子使用せずにC#でダウンキャストを生成する方法が少なくとも1つあります。そのようなシナリオを考えることができますか?これらは潜在的なコードの匂いでもあるため、コードにマニフェストをキャストせずにダウンキャストクラッシュを引き起こす可能性のある機能の設計にどのような設計要素が組み込まれたと思いますか?


101
壁に貼られた高校のポスターを思い出してください。「何が人気なのは常に正しいとは限らず、何が正しいとは限らないのですか?」あなたは彼らがあなたに薬を飲ませたり、高校で妊娠させないようにしようとしていると思っていましたが、実際には技術的負債の危険性について警告していました。
corsiKa

14
もちろん、@ EricLippert、foreachステートメント。これは、IEnumerableがジェネリックになる前に行われましたよね?
アルトゥーロトーレスサンチェス

18
この説明は私の一日を作りました。教師として、私はなぜC#がこのように、またはそのように設計されているのかとよく聞かれます。フォローアップの質問「しかし、なぜ?」と答えるのは少し難しいです。そして、ここで完璧なフォローアップの答えを見つけます。「C#は、やるべき仕事がある実用的なプログラマーのために、やるべき仕事がある実用的なプログラマーのために発明されました。」:-)ありがとう@EricLippert!
ザノ

12
余談:上記のコメントで明らかに、AからBへのダウンキャストがあると言っていました。クラス階層をナビゲートするときに「上」と「下」および「親」と「子」を使用するのは本当に嫌いです。私の文章では常に間違っています。タイプ間の関係はスーパーセット/サブセット関係であるため、それに関連して上方向または下方向が必要な理由はわかりません。そして、「親」を始めさせないでください。キリンの親は哺乳類ではなく、キリンの親はキリン夫人です。
エリックリッパー

9
Object.Equalsある恐ろしいです。C#での等価計算は完全に台無しになり、コメントで説明するにはあまりにも台無しです。平等を表すには非常に多くの方法があります。それらを矛盾させるのは非常に簡単です。実際には、等価演算子に必要な特性(対称性、反射性、および推移性)に違反することが非常に簡単です。今日ゼロから型システムを設計していた場合、まったく何もEqualsGetHashcodeまたはToString)ありませんObject
エリックリッパー

33

通常、イベントハンドラには署名がありますMethodName(object sender, EventArgs e)。場合によってsenderは、タイプを考慮せずに、またはsenderまったく使用しなくてもsenderイベントを処理できますが、イベントを処理するには、より具体的なタイプにキャストする必要があります。

たとえば、2つTextBoxのsがあり、それぞれからのイベントを処理するために単一のデリゲートを使用する場合があります。次に、イベントを処理するために必要なプロパティにアクセスできるようsenderに、にキャストする必要がありますTextBox

private void TextBox_TextChanged(object sender, EventArgs e)
{
   TextBox box = (TextBox)sender;
   box.BackColor = string.IsNullOrEmpty(box.Text) ? Color.Red : Color.White;
}

はい、この例TextBoxでは、内部でこれを行った独自のサブクラスを作成できます。ただし、常に実用的または可能というわけではありません。


2
ありがとう、それは良い例です。TextChangedイベントは、のメンバーであるControl-なぜ私が疑問に思うsenderタイプではないControl代わりのタイプobject?また、(理論的には)フレームワークが各サブクラスのイベントを再定義できたかどうか(たとえば、送信者のタイプとしてTextBox使用TextBoxして、新しいバージョンのイベントを持つことができたのか)... to TextBoxTextBox実装内(新しいイベントタイプを実装するため)ですが、アプリケーションコードのイベントハンドラーでダウンキャストを要求することは避けます。
ChrisW

3
すべてのイベントに独自のメソッドシグネチャがある場合、イベント処理を一般化することは難しいため、これは受け入れられると考えられます。代替案は実際に面倒なので、ベストプラクティスと見なされるものではなく、受け入れられている制限だと思います。コードを過度に複雑にTextBox senderするobject senderことなく、a ではなくa で開始することが可能であった場合、それが行われると確信しています。
ニール

6
@Neilイベントシステムは、任意のデータチャンクを任意のオブジェクトに転送できるように特別に作成されています。型の正しさの実行時チェックなしでは、これはほとんど不可能です。
Joker_vD

@Joker_vD理想的には、APIは一般的な矛盾を使用して、厳密に型指定されたコールバック署名をサポートすることが理想です。.NETが(圧倒的に)これを行わないという事実は、単にジェネリックと共分散が後で導入されたという事実によるものです。—言い換えれば、言語/ APIの不備のために、ここ(そして一般的には他の場所)でのダウンキャストが必要です。それは根本的に良いアイデアだからではありません。
コンラッドルドルフ

@KonradRudolphもちろんですが、イベントキューに任意のタイプのイベントをどのように保存しますか?あなたはキューイングを取り除き、直接派遣に行きますか?
Joker_vD

17

何かがあなたにスーパータイプを与え、サブタイプに応じて異なる方法でそれを処理する必要がある場合、ダウンキャストが必要です。

decimal value;
Donation d = user.getDonation();
if (d is CashDonation) {
     value = ((CashDonation)d).getValue();
}
if (d is ItemDonation) {
     value = itemValueEstimator.estimateValueOf(((ItemDonation)d).getItem());
}
System.out.println("Thank you for your generous donation of $" + value);

いいえ、これは臭いがしません。完全な世界でDonationは、抽象メソッドがgetValueあり、各実装サブタイプには適切な実装があります。

しかし、これらのクラスがライブラリからのものである場合、変更できない、または変更したくない場合はどうでしょうか?その後、他のオプションはありません。


これは、訪問者パターンを使用する良い機会のようです。または、dynamic同じ結果を達成するキーワード。
user2023861

3
@ user2023861いいえ訪問者のパターンはDonationサブクラスからの協力に依存すると思います。つまり、Donationは抽象を宣言する必要がvoid accept(IDonationVisitor visitor)ありIDonationVisitorます。
ChrisW

@ChrisW、あなたはそれについて正しいです。協力しなくても、dynamicキーワードを使用して実行できます。
user2023861

この特定の場合、推定値メソッドを寄付に追加できます。
user253751

@ user2023861:dynamicまだダウンキャスティングで、遅いだけです。
Mehrdad

12

コメントできないので、エリック・リッパートの答えに追加するには...

多くの場合、APIをリファクタリングしてジェネリックを使用することで、ダウンキャストを回避できます。しかし、ジェネリックはバージョン2.0まで言語に追加されませんでした。したがって、独自のコードで古代言語バージョンをサポートする必要がない場合でも、パラメータ化されていないレガシークラスを使用していることに気付くかもしれません。たとえば、一部のクラスはtypeのペイロードを運ぶように定義されている場合がObjectあります。これは、実際にあるとわかっている型にキャストする必要があります。


実際、JavaまたはC#のジェネリックは、本質的に、スーパークラスオブジェクトの保存と、要素を再度使用する前の正しい(コンパイラチェック済み)サブクラスへのダウンキャストの安全なラッパーにすぎないと言うかもしれません。(常にサブクラス自体を使用し、したがって、意気消沈することを避けるためにコードを書き換えるC ++のテンプレートとは異なり-コンパイル時や実行ファイルのサイズを犠牲にしている場合が多い、メモリのパフォーマンスを向上させることができます。)
leftaroundabout

4

静的言語と動的型付け言語の間にはトレードオフがあります。静的型付けは、コンパイラーに多くの情報を与えて、プログラム(の一部)の安全性についてかなり強力な保証を行えるようにします。ただし、これにはコストがかかります。プログラムが正しいことを知る必要があるだけでなく、適切なコードを記述して、コンパイラにこれが事実であることを納得させる必要があるからです。言い換えれば、主張することは証明するよりも簡単です。

コンパイラに対して証明されていないアサーションを作成するのに役立つ「安全でない」構成要素があります。たとえばNullable.Value、、無条件のダウンキャスト、dynamicオブジェクトなどへの無条件呼び出し。これらを使用すると、証明する必要なく、クレームをアサートできます(「オブジェクトaString間違っている場合、オブジェクトをアサートしますInvalidCastException」)。これは、証明するのが価値があるよりもかなり難しい場合に役立ちます。

これを乱用するのは危険です。明示的なダウンキャスト表記が存在し、必須である理由です。安全でない操作に注意を引くための構文ソルトです。この言語、暗黙のダウンキャスティング(推論された型が明確な場合)で実装できますが、これはこの安全でない操作を隠してしまうため、望ましくありません。


私は、この種の「証明されていない主張」をすることが必要または望ましい(つまり、良い習慣)場所/時のいくつかの例について尋ねていると思います。
ChrisW

1
リフレクション操作の結果をキャストするのはどうですか?stackoverflow.com/a/3255716/3141234
アレクサンダー

3

ダウンキャスティングは、いくつかの理由で悪いと考えられています。主に、それが反OOPだからだと思います。

OOPは、「その存在理由」が多態性であるため、行かなくてもよいということなので、ダウンキャストする必要がなかったなら、本当に気に入ってくれるでしょう。

if(object is X)
{
    //do the thing we do with X's
}
else
{
    //of the thing we do with Y's
}

あなたはただやる

x.DoThing()

そして、コードは自動的に正しいことを行います。

ダウンキャストしない「ハード」な理由がいくつかあります。

  • C ++では遅いです。
  • 間違ったタイプを選択すると、ランタイムエラーが発生します。

しかし、いくつかのシナリオでのダウンキャストに代わるものはかなりいものです。

典型的な例はメッセージ処理です。処理関数をオブジェクトに追加するのではなく、メッセージプロセッサに保持します。その後、MessageBaseClass処理する配列が1トンありますが、正しい処理を行うには、サブタイプごとに1つずつ必要です。

Double Dispatchを使用して問題を回避できます(https://en.wikipedia.org/wiki/Double_dispatch)...しかし、これには問題もあります。または、これらの難しい理由のリスクがある単純なコードをダウンキャストすることもできます。

しかし、これはジェネリック医薬品が発明される前のことです。詳細を後で指定するタイプを提供することで、ダウンキャストを回避できます。

MessageProccesor<T> 指定されたタイプによってメソッドパラメータと戻り値を変更できますが、汎用的な機能を提供します

もちろん、誰もあなたにOOPコードを書くことを強制することはなく、リフレクションなどの多くの言語機能が提供されていますが、それを嫌っています。


1
私はそれは、C ++で特にあなたの場合は遅いです疑うstatic_cast代わりにdynamic_cast
ChrisW

「メッセージ処理」-私はそれを、たとえばlParam特定の何かにキャストすることを意味すると考えています。ただし、これが良いC#の例かどうかはわかりません。
ChrisW

3
@ChrisW-ええ、はい、static_cast常に高速です...しかし、間違いを犯し、タイプ期待したものでない場合、未定義の方法で失敗しないという保証はありません。
ジュール

@Jules:それは完全に重要です。
Mehrdad

@Mehrdad、まさにそれがポイントです。C ++で同等のC#キャストを実行するには時間がかかります。これが、キャストに対する伝統的な理由です。代替static_castは同等ではなく、未定義の動作のためにより悪い選択と見なされます
ユアン

0

前述のすべてに加えて、オブジェクト型のTagプロパティを想像してください。必要に応じて後で使用するために、選択したオブジェクトを別のオブジェクトに保存する方法を提供します。このプロパティをダウンキャストする必要があります。

一般に、今日まで何かを使用しないことは、常に役に立たない何かを示すものではありません;-)


はい。たとえば、.netのTagプロパティの使用を参照してください。そこにある回答の1つで、誰かが書いたように、Windows Formsアプリケーションでユーザーに指示を入力するために使用していました。コントロールのGotFocusイベントがトリガーされると、命令のLabel.Textプロパティに、命令文字列を含むコントロールのTagプロパティの値が割り当てられました。Controlobject Tagプロパティを使用する代わりに)を「拡張」する別の方法Dictionary<Control, string>は、各コントロールのラベルを格納する場所を作成することだと思います。
ChrisW

1
私はこの答えが好きです。なぜならTag、.Netフレームワーククラスのパブリックプロパティであり、それが(多かれ少なかれ)それを使用することになっていることを示しているからです。
ChrisW

結論は間違っている(または右)であると言うことではないが、公共の.NETの機能がたくさんありますので、それはずさんな推論だということに注意してください。@ChrisW 廃止された(たとえば、System.Collections.ArrayList(たとえば、)、またはその他の非推奨がIEnumerator.Reset)。
Mehrdad

0

私の作品におけるダウンキャストの最も一般的な使用法は、いくつかのばかがリスコフの代用の原則を破ったためです。

サードパーティのライブラリにインターフェースがあると想像してください。

public interface Foo
{
     void SayHello();
     void SayGoodbye();
 }

そして、それを実装する2つのクラス。BarおよびQuxBarふるまいです。

public class Bar
{
    void SayHello()
    {
        Console.WriteLine(“Bar says hello.”);
    }
    void SayGoodbye()
    {
         Console.WriteLine(“Bar says goodbye”);
    }
}

しかしQux、うまく動作していません。

public class Qux
{
    void SayHello()
    {
        Console.WriteLine(“Qux says hello.”);
    }
    void SayGoodbye()
    {
        throw new NotImplementedException();
    }
}

まあ...今、私は選択肢がありません。私が持っている私のプログラムがクラッシュ停止に来るのを避けるためにチェックし、(おそらく)ダウンキャストを入力します。


1
この特定の例には当てはまりません。SayGoodbyeを呼び出すときに、NotImplementedExceptionをキャッチできます。
ペルフォンツヴァイベルク

それはだ過度多分簡略化した例はなく、古いネットの一部との仕事は少しAPIと、あなたがこのような状況に遭遇します。コードを記述する最も簡単な方法は、alaを安全にキャストすることですvar qux = foo as Qux;
ラバーダック

0

もう1つの実際の例は、WPF VisualTreeHelperです。ダウンキャストを使用DependencyObjectしてVisualとにキャストしVisual3Dます。たとえば、VisualTreeHelper.GetParentなど。ダウンキャストはVisualTreeUtils.AsVisualHelperで発生します

private static bool AsVisualHelper(DependencyObject element, out Visual visual, out Visual3D visual3D)
{
    Visual elementAsVisual = element as Visual;

    if (elementAsVisual != null)
    {
        visual = elementAsVisual;
        visual3D = null;
        return true;
    }

    Visual3D elementAsVisual3D = element as Visual3D;

    if (elementAsVisual3D != null)
    {
        visual = null;
        visual3D = elementAsVisual3D;
        return true;
    }            

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