やさしいキメラの作り方

(id:NyaRuRu:20051109:p2) の解答編.

さて,トラックバックをいただいたので解答に入る前にちょっとだけ予告編.

IDog,ICatを実装しながら、Type.IsAssignableFromでFalseを返さなきゃいけない。ふつうはそんなんありえないけど、まあoverrideしちゃえばいいんでしょう。

残念ながら Object.GetType() は override 出来ません.しかしまあ発想自体はほとんど正解と言ってもいいでしょう.実際私が行ったのは,CLR の奥の方をちょこっとつついて,オブジェクトに対するキャスト操作の可否と矛盾するような型を GetType() が返す,というものです.
さてまず下準備.次のような Animal クラスを用意します.

public class Animal : MarshalByRefObject, IDog, ICat
{
    string IDog.Bark()
    {
        return "Barked as IDog";
    }
    string ICat.Bark()
    {
        return "Barked as ICat";
    }
}

MarshalByRefObject で「犯人分かっちゃったんですけどぉ」という方がぞろぞろ現れそうな.ちなみにこのクラスは interface の明示的実装の例にもなっています.
この Animal クラスではまだ題意は満たせません.具体的には Type.IsAssignableFrom が以下のように true を返してしまいます.

typeof(IDog).IsAssignableFrom(o.GetType()) : True
typeof(ICat).IsAssignableFrom(o.GetType()) : True

そこで Animal オブジェクトを次の MiscreatedAnimal クラスでラップします.

using System;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting.Messaging;

public class MiscreatedAnimal : RealProxy
{
    readonly MarshalByRefObject _target;
    static readonly MethodInfo GetTypeMethodInfo =
                           typeof(object).GetMethod("GetType");
    public MiscreatedAnimal(MarshalByRefObject target)
        : base(target.GetType())
    {
        _target = target;
    }

    private IMessage ProcessMessage(IMethodCallMessage request)
    {
        if (request.MethodBase == GetTypeMethodInfo)
        {
            // Object.GetType が呼ばれたとき typeof(object) を返す
            return new ReturnMessage(typeof(object), null, 0, null, request);
        }
        else
        {
            // メソッド呼び出しをターゲットで処理
            return RemotingServices.ExecuteMessage(_target, request);
        }
    }

    public override IMessage Invoke(IMessage msg)
    {
        return ProcessMessage((IMethodCallMessage)msg);
    }

    public static T Create<T>(T obj) where T : MarshalByRefObject
    {
        return (T) new MiscreatedAnimal(obj).GetTransparentProxy();
    }
}

このクラスは CLR の機能を利用し,GetType() メソッドの呼び出し結果を typeof(object) に差し替えるような Proxy オブジェクト作ります.それ以外のメソッド呼び出しについては全て元となったオブジェクトに転送しています.

Test(MiscreatedAnimal.Create(new Animal()));

これで題意は満たされました.
種明かしをしておきましょう.一般的な C# コンパイラは,is / as 演算子に対し MSIL の isinst 命令を出力します.また,(キャスト演算子のオーバーロード以外の) キャストに対しては,castclass 命令を出力します.CLR は isinst や castclass といった命令については,内部的に object.GetType() を呼び出すのではなく,オブジェクトのタイプハンドルを直接参照した上で変換の可否を決定します.これが「ふつうはそんなんありえない」ことが起きてしまうカラクリというわけです.
しかしながら,「Animal というオブジェクトのように振舞う」ことが要請される Proxy オブジェクトが object.GetType() に対して何を返すべきかは,ある意味深い問題です.以下の別解をご覧ください.

public class PseudoAnimal : RealProxy, IRemotingTypeInfo
{
    readonly MarshalByRefObject _target;
    readonly string _typeName;
    public PseudoAnimal(MarshalByRefObject target)
        : base(target.GetType())
    {
        _target = target;
        _typeName = _target.GetType().ToString() + "Proxy";
    }

    public bool CanCastTo(Type fromType, object o)
    {
        // true を返すことは任意の型への変換を許可することを意味する
        return true;
    }
    public string TypeName
    {
        get
        {
            return _typeName;
        }
        set
        {
            throw new NotSupportedException();
        }
    }

    private IMessage ProcessMessage(IMethodCallMessage request)
    {
        switch (request.MethodName)
        {
            case "Bark":
                // メソッド名が Bark なら何でも横取りする
                return new ReturnMessage("Barked as " + request.MethodBase.DeclaringType, null, 0, null, request);
            default:
                // メソッド呼び出しをターゲットで処理
                return RemotingServices.ExecuteMessage(_target, request);
        }
    }

    public override IMessage Invoke(IMessage msg)
    {
        return ProcessMessage((IMethodCallMessage)msg);
    }

    public static T Create<T>(T obj) where T : MarshalByRefObject
    {
        return (T)new PseudoAnimal(obj).GetTransparentProxy();
    }
}

この Proxy クラスは,もはや Animal オブジェクトを必要としません.MarshalByRefObject であれば何でも擬似イヌネコ化可能です.

Test(PseudoAnimal.Create(new MemoryStream()));

この Proxy クラスは,IRemotingTypeInfo.CanCastTo を実装することで,isinst 及び castclass 命令に対する振る舞いを上書きしています.一方で object.GetType() はそのままターゲットに転送され,今回の例では typeof(MemoryStream) が返されることでしょう.こうして,typeof(IDog).IsAssignableFrom(o.GetType()) は題意を満たします.
しかしながら MemoryStream は IDog.Bark() も ICat.Bark() も実装していません.これについては,メソッド名が Bark であるメソッド呼び出しを MemoryStream オブジェクトに転送せず,Proxy 側で処理することで回避しています.こうして Proxy オブジェクトはあたかも IDog と ICat を実装しているかのように振舞うことが出来る,というわけです.
さて先ほどの問題をもう一度思い出しましょう.後者の解において,Proxy オブジェクトが object.GetType() に対して返すべき型は何だったのでしょうか? 恐らく,MemoryStream 型ではなく,MemoryStream から派生し IDog と ICat の実装を含んだ新しい型であることが望ましいと考えられます.(とはいえ影響範囲を考えるとかなり微妙な問題です)
実行時に interface 実装を合成するようなフレームワークを作る際には,このような点に十分注意する必要があるでしょう*1.Prototype ベースの言語とどう相互運用するかについてもキーポイントになりそうですね.

*1:.NET Framework 1.1 SDK をお持ちの方は Samples\Technologies\Remoting\Advanced\CustomProxies\ProxyWithCustomCasting.cs をご覧ください.後者の解と同じ問題を抱えているように見えます.