ダウンキャストを回避することは可能でしょうか?
-
20-08-2019 - |
質問
次のようなユーザー定義型を定義して使用するロジックがあります。
class Word
{
System.Drawing.Font font; //a System type
string text;
}
class Canvass
{
System.Drawing.Graphics graphics; //another, related System type
... and other data members ...
//a method whose implementation combines the two System types
internal void draw(Word word, Point point)
{
//make the System API call
graphics.DrawString(word.text, word.font, Brushes.Block, point);
}
}
タイプを使用して計算を行った後のロジック (例:それぞれを見つけるために Word
インスタンス)、間接的にいくつかを使用します System
API (たとえば、 Canvass.draw
方法。
このロジックを独立したものにしたいのですが、 System.Drawing
名前空間:主に、単体テストを支援するためです (単体テストの出力は、 draw
メソッドは実際以外のものを描画していました System.Drawing.Graphics
実例)。
ロジックの依存関係を排除するには、 System.Drawing
名前空間のプレースホルダーとして機能する新しいインターフェイスをいくつか宣言しようと思いました。 System.Drawing
タイプは次のとおりです。
interface IMyFont
{
}
interface IMyGraphics
{
void drawString(string text, IMyFont font, Point point);
}
class Word
{
IMyFont font; //no longer depends on System.Drawing.Font
string text;
}
class Canvass
{
IMyGraphics graphics; //no longer depends on System.Drawing.Graphics
... and other data ...
internal void draw(Word word, Point point)
{
//use interface method instead of making a direct System API call
graphics.drawText(word.text, word.font, point);
}
}
これを行うと、異なるアセンブリには異なる実装が含まれる可能性があります。 IMyFont
そして IMyGraphics
インターフェイス、たとえば...
class MyFont : IMyFont
{
System.Drawing.Font theFont;
}
class MyGraphics : IMyGraphics
{
System.Drawing.Graphics theGraphics;
public void drawString(string text, IMyFont font, Point point)
{
//!!! downcast !!!
System.Drawing.Font theFont = ((MyFont)font).theFont;
//make the System API call
theGraphics.DrawString(word.text, theFont, Brushes.Block, point);
}
}
...ただし、実装には上で示したようにダウンキャストが必要になります。
私の質問は、 実装でダウンキャストを必要とせずにこれを行う方法はありますか? 「これ」とは、「次のような UDT を定義する」ことを意味します。 Word
そして Canvass
特定のコンクリートに依存しない System
種類"?
代わりに抽象 UDT を使用することもできます。
class Word
{
//System.Drawing.Font font; //declared in a subclass of Word
string text;
}
class Canvass
{
//System.Drawing.Graphics graphics; //declared in a subclass of Canvass
//concrete draw method is defined in a subclass of Canvass
internal abstract void draw(Word word, Point point);
}
...しかし、これもサブクラスの実装でダウンキャストが必要になります。
ダブルディスパッチイディオムを使用することも考えましたが、それは API 内のさまざまなサブクラスの名前に依存します。
それとも、インターフェイスやサブクラスを使用しない場合、デリゲートを使用する方法はありますか?
- 編集: -
考えられる答えは 2 つあります。
1 つの答えは、以下の「Sir Lantis」の答えで示唆されているとおり、また John Skeet がリンクしているブログ投稿で示唆されているとおり、ジェネリックを使用することです。これはほとんどのシナリオでうまく機能すると思います。私の観点から見るとマイナス面は、 TFont
テンプレートパラメータとして:のようなクラスだけではありません Word
(これには、 Font
インスタンス)はジェネリッククラスになる必要があります(次のような) WordT<TFont>
) ...を含むクラスも同様です WordT<TFont>
(例えば。 Paragraph
) を使用してジェネリックにする必要もあります。 TFont
パラメータ(例: ParagraphT<TFont>
)。最終的に、アセンブリ内のほぼすべてのクラスがジェネリック クラスになりました。これ する 型安全性を維持し、ダウンキャストの必要性を回避します... しかし これはちょっと見苦しく、カプセル化の錯覚 (「フォント」が不透明な実装の詳細であるという錯覚) を妨げます。
別の答えは、ユーザー クラスでマップまたは辞書を使用することです。の代わりに Font
再利用可能なライブラリで、抽象インターフェイスの代わりに、次のような「ハンドル」クラスを定義します。
public struct FontHandle
{
public readonly int handleValue;
FontHandle(int handleValue)
{
this.handleValue = handleValue;
}
}
そして、そこからダウンキャストするのではなく、 FontHandle
, 、保管してください Dictionary<int, Font>
マッピングするインスタンス FontHandle
値を Font
インスタンス。
解決
まず、シナリオ全体が少し人工的ではないかと思います。本当に行くつもりですか 必要 このレベルの抽象度?おそらく購読する ヤグニ?
なぜあなたの MyGraphics
のみで動作します MyFont
?で動作しますか IFont
?そうすればインターフェースをより適切に使用でき、この問題全体を回避できるでしょう...
オプションの 1 つは、少し再設計することです。 IFont
フォントのメタデータ (サイズ、フォントフェイスなど) を記述するだけで、具体的な内容が得られます。 MyGraphics
のように:
[public|internal] MyFont GetFont(IFont font) {...} // or just Font
そして、翻訳を行うのはグラフィックスの仕事になるため、次のようなものを使用します。
public void drawString(string text, IMyFont font, Point point)
{
using(System.Drawing.Font theFont = GetFont(font))
{
theGraphics.DrawString(word.text, theFont, Brushes.Block, point);
}
// etc
}
もちろん、 Point
翻訳も必要かもしれません ;-p
他のヒント
あなたは事実上、「私はコンパイラよりもよく知っています - コンパイラが必ずコンパイラのインスタンスになることを知っています」と言っていることになります。 MyFont
." その時点で、あなたは MyFont
そして MyGraphics
再び密結合されるため、インターフェイスのポイントが少し減ります。
すべき MyGraphics
どれでも作業できます IFont
, 、またはのみ MyFont
?どれでも機能させることができれば、 IFont
大丈夫ですよ。それ以外の場合は、コンパイル時にすべてを型安全にするために、複雑なジェネリックを検討する必要があるかもしれません。見つかるかもしれません プロトコルバッファーのジェネリックに関する私の投稿 同様の状況として役立ちます。
(サイド提案 - メソッドの Pascal ケースを含む命名規則に従うと、コードはより慣用的に .NET に似たものになります。)
私は今のところ、C# についてあまり意識していません。もうしばらく経ちました。ただし、すべての内容をそこにキャストしたくない場合は、ジェネリックスの使用を余儀なくされる可能性があります。
Java コードを提供するだけですが、C# でも同じことができるはずです。 where
キーワード。
インターフェースを汎用インターフェースにします。Java では次のようになります
IMyGraphics<T extends IMyFont>
その後 MyGraphics : IMyGraphics<MyFont>
次に、再定義します drawString
取る署名 T font
の代わりに 2 番目のパラメータとして IMyFont
. 。これにより、次のことが可能になります。
public void drawString(string text, MyFont font, Point point)
あなたの中に直接 MyGraphics
クラス。
C# では、 IMyGraphics<T extends IMyFont>
あるべきです public interface IMyGraphics<T> where T:IMyFont
, しかし、私はそれについて100%確信しているわけではありません。
のキャストが気に入らない IFont
に MyFont
?あなたはこれを行うことができます:
interface IFont {
object Font { get; }
}
class MyFont : IFont {
object Font { get { return ...; } }
}
確かに、まだキャストする必要があります System.Object
に System.Drawing.Font
描画メソッド内ではありますが、特定のクラス実装への依存関係を削除しただけです (MyFont
).
public void DrawString(string text, IFont font, Point point)
{
System.Drawing.Font f = (Font)font.Font;
graphics.DrawString(text, f, Brushes.Block, point);
}