こんな感じ。
class Hoge
{
}
class Fuga : Hoge
{
}
class Piyo : Hoge
{
}
class Program
{
static void Main(string[] args)
{
var hoge1 = new Hoge();
var hoge2 = new Hoge();
var fuga1 = new Fuga();
var fuga2 = new Fuga();
var piyo = new Piyo();
bool b1 = hoge1.Equals(hoge2); // T = Hoge
bool b2 = hoge1.Equals(fuga1); // T = Hoge
bool b3 = fuga1.Equals(fuga2); // T = Fuga
bool b4 = fuga1.Equals(hoge1); // T = Hoge (!!)
//bool b5 = fuga1.Equals(piyo); // Build Error !!
bool b6 = fuga1.Equals((Hoge)piyo); // T = Hoge
bool b7 = fuga1.Equals<Hoge>(piyo); // T = Hoge
}
}
[検証プログラム]
インテリセンスに Object.Equals(object) が出てこないこと、Equals の引数の型が自分自身になることを確認できます。
ビルドが出来るだけで実行は出来ません。
ダウンロードページ
public class Object
{
protected internal virtual bool Equals(object obj)
{
...
}
}
public static class ObjectExtension
{
public static bool Equals<T>(this T source, T obj)
{
if (source == null)
{
// return (obj == null); にするのも面白い
throw new NullReferenceException();
}
return source.Equals((object)obj);
}
}
あまり深く考えずに言っているので、なんらかの不都合が生じるかもしれないけど。。。
つか、拡張メソッドのサポートを全ての言語に強いることになるからダメかな。
つかつか、やっぱし自分自身の型を示すキーワードが欲しくなる。
class A
{
}
class B : A
{
}
これらのクラスそれぞれに対し、暗黙的に型変換可能なクラス、A1 クラスと B1 クラスを用意するとします。
class A1
{
public A1(A source)
{
this._source = source;
}
private readonly A _source;
public static implicit operator A(A1 target)
{
return target._source;
}
public static implicit operator A1(A target)
{
return new A1(target);
}
}
class B1
{
public B1(B source)
{
this._source = source;
}
private readonly B _source;
public static implicit operator B(B1 target)
{
return target._source;
}
public static implicit operator B1(B target)
{
return new B1(target);
}
}
A1 と B1 は継承関係にありませんが、A と B は継承関係にあります。
そこで、次のように B1 のインスタンスを A1 に型変換できるようにしたいとします。ただし、A1 と B1 はお互いの存在を直接知らないものとします。更に、A1 は A の派生クラスの存在を直接知らないものとします。
B1 b = new B();
A1 a = b;
A1 と B1 はお互いの存在を直接知らないので、まずは間接的に知ることができるよう、A1 と B1 が共通で実装するインターフェイスを用意します。
interface ITypeDef<out T>
{
T Source
{
get;
}
}
このインターフェイスを実装したコードは次のようになります。
class A1 : ITypeDef<A>
{
public A1(A source)
{
this._source = source;
}
protected readonly A _source;
public static implicit operator A(A1 target)
{
return target._source;
}
public static implicit operator A1(A target)
{
return new A1(target);
}
A ITypeDef<A>.Source
{
get
{
return this._source;
}
}
}
class B1 : ITypeDef<B>
{
public B1(B source)
{
this._source = source;
}
protected readonly B _source;
public static implicit operator B(B1 target)
{
return target._source;
}
public static implicit operator B1(B target)
{
return new B1(target);
}
B ITypeDef<B>.Source
{
get
{
return this._source;
}
}
}
ITypeDef<T> の型パラメータ T は共変ですので、ITypeDef<B> を ITypeDef<A> に型変換することが可能です。ということは、A1 が ITypeDef<A> からの変換をサポートすれば、ITypeDef<B> を実装する B1 からの変換ができるようになるはずです。
class A1 : ITypeDef<A>
{
public A1(A source)
{
this._source = source;
}
protected readonly A _source;
public static implicit operator A(A1 target)
{
return target._source;
}
public static implicit operator A1(A target)
{
return new A1(target);
}
public static implicit operator A1(ITypeDef<A> target)
{
return new A1(target.Source);
}
A ITypeDef<A>.Source
{
get
{
return this._source;
}
}
}
class B1 : ITypeDef<B>
{
public B1(B source)
{
this._source = source;
}
protected readonly B _source;
public static implicit operator B(B1 target)
{
return target._source;
}
public static implicit operator B1(B target)
{
return new B1(target);
}
public static implicit operator B1(ITypeDef<B> target)
{
return new B1(target.Source);
}
B ITypeDef<B>.Source
{
get
{
return this._source;
}
}
}
残念ながら、このコードはコンパイルが通りません。なぜか C# ではインターフェイスからの変換演算子の定義が禁止されているからです。
さて、今回の目的は恐らく正攻法では実現できません。
ということで、これから非実用的な実現方法を紹介します。
先にも記述しました通り、変換演算子で変換元または変換先をインターフェイスにすることはできません。
そこでジェネリックの型パラメータを利用します。ジェネリックの型パラメータは変換元や変換先に指定することができます。そして、型引数としてインターフェイスを指定することで、間接的に変換元または変換先をインターフェイスにすることができます。
例えば、次のコードでは IEnumerable<int> から Class1<IEnumerable<int>> への変換が実現されます。
using System.Collections.Generic;
class Class1<T>
{
public Class1(T source)
{
this._source = source;
}
private readonly T _source;
public static implicit operator Class1<T>(T target)
{
return new Class1<T>(target);
}
}
class Program
{
static void Main(string[] args)
{
Class1<IEnumerable<int>> c = new List<int>();
}
}
Class1<T> の型パラメータに IEnumerable<int> と指定していますので、変換演算子は実質次のような形と同等になります。
public static implicit operator Class1<IEnumerable<int>>(IEnumerable<int> target)
{
return new Class1<IEnumerable<int>>(target);
}
では、話を元に戻して ITypeDef<A> から A1 への変換を実現してみます。
A1 はあくまでも非ジェネリッククラスのままにしたいので、先ほどの Class1<T> よりも更に複雑になります。
A1<T> クラスを用意して、これを A1 が継承するようにします。
A1<T> は A1 のためだけに存在しますので、A1 の定義以外のコードからは直接使用しないようにします。
abstract class A1<T>
where T : ITypeDef<A>
{
public static implicit operator A1<T>(T target)
{
return (A1<T>)(object)new A1(target.Source);
}
}
class A1 : A1<ITypeDef<A>>, ITypeDef<A>
{
public A1(A source)
{
this._source = source;
}
protected readonly A _source;
public static implicit operator A(A1 target)
{
return target._source;
}
public static implicit operator A1(A target)
{
return new A1(target);
}
A ITypeDef<A>.Source
{
get
{
return this._source;
}
}
}
B1 についても同じようにしておきます。
abstract class B1<T>
where T : ITypeDef<B>
{
public static implicit operator B1<T>(T target)
{
return (B1<T>)(object)new B1(target.Source);
}
}
class B1 : B1<ITypeDef<B>>, ITypeDef<B>
{
public B1(B source)
{
this._source = source;
}
protected readonly B _source;
public static implicit operator B(B1 target)
{
return target._source;
}
public static implicit operator B1(B target)
{
return new B1(target);
}
B ITypeDef<B>.Source
{
get
{
return this._source;
}
}
}
ところで、A1<T> に定義されている変換演算子は、あくまでも A1 ではなく A1<T> への変換演算子になっています。
つまり、B1 から A1 に変換する際の内部的な流れとしては B1 → A1<T> → A1 という二段階の流れになります。
A1<T> から A1 への変換はダウンキャストとなるため明示的な変換が必要になります。
というわけで、B1 から A1 への変換は暗黙的にはできません。次のように明示的な変換となります。
B1 b = new B();
A1 a = (A1)b;
以上で当初の目的は一応達成できました。
仕組みが複雑なので実用度は低いですね。
型名の後に "`" をくっつけて ( エスケープ文字だから 2 回書く ) ジェネリックパラメータの数を書く。
$t0 = [System.Collections.Generic.Dictionary``2];
構築ジェネリック型 ( 1 )
通常、ジェネリック引数は "[アセンブリ修飾名]" という形式で指定する。複数指定する場合は ", " で区切る。ジェネリック引数全体を "[" と "]" で囲む。
$t1 = [System.Collections.Generic.Dictionary``2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089], [System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]];
構築ジェネリック型 ( 2 )
ジェネリック引数に指定する型のアセンブリが、ジェネリック型と同一のアセンブリの場合は、アセンブリ修飾名でなく、完全名でも可。
$t2 = [System.Collections.Generic.Dictionary``2[[System.String], [System.Int32]]];
構築ジェネリック型 ( 3 )
ジェネリック引数に指定する型のアセンブリが、ジェネリック型と同一のアセンブリではなく、更に GAC に登録されていない場合、記述が不可能。
代わりに、Type.MakeGenericType メソッドを使用して構築ジェネリック型を生成する。 ( 下の例では、コピペで簡単に実行できるように、String と Int32 を指定している。 )
$t3 = [System.Collections.Generic.Dictionary``2].MakeGenericType(@([System.String], [System.Int32]));
また、New-Object コマンドレットによるインスタンス生成も不可能。
代わりに、Activator.CreateInstance メソッド等を使用してインスタンスを生成する。
$d0 = [Activator]::CreateInstance($t3);
コンストラクタに渡す引数を指定してActivator.CreateInstance メソッドを使用する場合、コンストラクタに渡す引数が PSObject 型になっていないか注意する必要がある。例えば、New-Object で生成したオブジェクトは PSObject 型である。PSObject 型から生 ( ? ) のオブジェクトを取得するには、PSObject プロパティを参照し、更に BaseObject パラメータを参照する。
$i = [PSObject]100;
$d1 = [Activator]::CreateInstance($t3, @($i.PSObject.BaseObject));
# 下のコードではエラーとなる。
$d2 = [Activator]::CreateInstance($t3, @($i));
あの後、実は指定できないこともないということに気づきました。ごめんなさい。
まず、なぜうまく指定できないと書いたかですが、先日の記事であげたケースと物品の例で説明します。
ケース クラスには格納する物品を指定するためのジェネリックパラメータを用意します。
物品 クラスには親となるケースを指定するためのジェネリックパラメータを用意します。
つまり、ケース<TArticle> クラスと 物品<TCase> クラス となるわけです。
この時、ケース クラスのジェネリックパラメータ TArticle の型制約には、物品<TCase> を指定するわけですが、物品<TCase> の型パラメータ TCase を指定しなければなりません。ここで不都合が生じるわけです。TCase に対して ケース<TArticle> と指定することはできますが、これではダメなんです。
なぜ不都合かというと、ジェネリック クラスは、型パラメータが完全一致していない場合、変換不可だからです。例えば、List<object> と List<int> では、object と int の間には継承関係がありますが、そんなの関係なく変換不可です。List<int> 型の変数を List<object> 型にキャストすることはできないのです。
すると、TCase が指定できないことがわかります。例えば、ケース クラスを継承した CDケース クラスでは、TCase は、CDケース にならなければなりません。ケース<CD> ではダメなのです。 ( 物品<ケース<CD> は 物品<ケース> への変換が不可ということです。 )
と、ちょっとややこしい話になってしまいましたが、要はこのままでは型制約の指定は無理だということです。で、先日の記事には無理だよ~と書いたわけです。
では、どうすればいいか、というのが今回の記事の本題です。
答えは、「自身のクラス階層を含め、ミラー階層において関連するクラス ( 階層 ) 全てを型パラメータに持たせる」 となります。
つまり、ケースには TCase と TArticle の2つの型パラメータを持たせ、物品にも TCase と TArticle の2つの型パラメータを持たせるわけです。
こうすることによって、先ほどの問題が解決できます。先ほど指定できなかった TCase には、ケースに新しく追加した型パラメータ TCase を指定すればいいわけですから。
コードにすると以下のようになります。
public abstract class ケース<TCase, TArticle>
where TCase : ケース<TCase, TArticle>
where TArticle : 物品<TCase, TArticle>
{
}
public abstract class 物品<TCase, TArticle>
where TCase : ケース<TCase, TArticle>
where TArticle : 物品<TCase, TArticle>
{
}
これらを継承した CDケース クラスと CD クラスは以下のようになります。
public sealed class CDケース : ケース<CDケース, CD>
{
}
public sealed class CD : 物品<CDケース, CD>
{
}
TCase と TArticle に関連しない型を指定した場合 ( 例えば ケース<CDケース, ペン> ) 、コンパイルエラーとなりますので、変なバグが入り込んだりすることはないかと思います。
これで問題は解決しましたが、一つ気を付けなければならないことがあります。ケース クラスに TCase という型パラメータ、そして 物品 クラスに TArticle という型パラメータがあることは、他人から見ておかしなことをやっているように見えてしまう ( つまり理解し難い ) ということです。こればかりは仕方ありませんので、理由や使い方をしっかりとドキュメントに書き記すようにしましょう。( いや、理由はしっかり書くとややこしくなるからざっくりとの方がいいかもw )
[ 余談 ]
ちなみに、これ以外にも方法はあります。非ジェネリックな基本クラスを用意するという方法です。つまり、「ケース クラス」 とこれを継承した 「ケース<TArticle> クラス」、「物品 クラス」 とこれを継承した 「物品<TCase> クラス」 を用意するわけです。こうすれば、型制約には非ジェネリックな基本クラスを指定することができます。