インスタンスが属するクラスをあとから変更する操作を C# で 〜RealProxy バージョン〜

概要

元ネタにある C# 版では、オブジェクトへの参照が持っている型ハンドルを強引に書き換えることにより、CLR が内部で持っている「このオブジェクト参照の実体はどの型なのか」という情報を改ざんし、それによって「インスタンスが属するクラスをあとから変更する操作」を実現している。

が、元ネタのエントリのコメント欄で id:NyaRuRu さんがツッコンでいる通り、この方法は CLI の実装に思いっきり依存する上、ランタイムの非公開なデータ構造を弄っているため挙動が無保証である。

そこで、RealProxy と IRemotingTypeInfo を用いる事で、ランタイム*1を破壊せずに「インスタンスが属するクラスをあとから変更する操作」を実現してみた。ただし、C# (や CLI 上で動く諸々)は、インスタンスインスタンスへの参照等々の型を常に意識することもあって、後述するように、かなり無理な結果になっていることには変わりない。

RealProxy と IRemotingTypeInfo

RealProxy などについて書いていたところエントリが長くなってしまったので、それについては RealProxy (実プロクシ) と透過的プロクシと IRemotingTypeInfo - やこ〜ん SuperNova2 (一つ上のエントリ)をどうぞ。
また、下に示すコードの原理などについても、↑のリンク先のエントリを参照されたし。

インスタンスが属するクラスをあとから変更

以下が、元ネタのエントリにあるお題を RealProxy & IRemotingTypeInfo で実装してみた例である。.net framework 3.5 と mono 2.0.1 で動作を確認済み。

なお、コードが長いので分割してあるが、順番につなげると筆者が実行したコードと同じになる。

コードの先頭
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;

namespace CSharpSandBox {
	class Program {
Polar(極座標) や Cartesian(直交座標) などの型の定義
/// <summary>
/// インスタンスがどの型として振る舞うのかを変更できてしまうインターフェース
/// </summary>
interface ITypeChangeable {
	Type CurrentType { get; set; }
}
/// <summary>
/// 極座標系
/// </summary>
class Polar : MarshalByRefObject, ITypeChangeable {
	public double R { get; set; }
 	public double Theta { get; set; }
	public Type CurrentType { get { return this.GetType(); } set { throw new NotSupportedException(); } }

	public Polar(double r, double theta) {
		this.R = r;
		this.Theta = theta;
	}

	/// <summary>
	/// 極座標系 to 直交座標
	/// </summary>
	public Cartesian ToCartesian() {
		return new Cartesian(this.R * Math.Cos(this.Theta), this.R * Math.Sin(this.Theta));
	}
}
/// <summary>
/// 直交座標系
/// </summary>
class Cartesian : MarshalByRefObject, ITypeChangeable {
	public double X { get; set; }
	public double Y { get; set; }
	public Type CurrentType { get { return this.GetType(); } set { throw new NotSupportedException(); } }

	public Cartesian(double x, double y) {
		this.X = x;
		this.Y = y;
	}

	/// <summary>
	/// 直交座標 to 極座標
	/// </summary>
	public Polar ToPolar() {
		return new Polar(Math.Sqrt(this.X * this.X + this.Y * this.Y), Math.Atan2(this.Y, this.X));
	}
}
Polar(極座標), Cartesian(直交座標) として振る舞うプロクシ
/// <summary>
/// <see cref="Polar"/> または <see cref="Cartesian"/> として振る舞うプロクシ。
/// どちらとして振る舞うのかを随時変更できる。
/// </summary>
class Point2d : RealProxy , IRemotingTypeInfo{

	public Point2d() : base(typeof(MarshalByRefObject)) {
		this.polar = new Polar(0, 0);
	}

	/// <summary>
	/// このプロクシが現在 Polar として振る舞っているなら、その実体。そうでないなら null。
	/// </summary>
	protected Polar polar = null;
	/// <summary>
	/// このプロクシが現在 Cartesian として振る舞っているなら、その実体。そうでないなら null。
	/// </summary>
	protected Cartesian cartesian = null;

	public bool CanCastTo(Type fromType, object o) {
		// Polar と Cartesian 双方の基底な型か、Polar/Cartesian そのものなら true
		if (fromType.IsAssignableFrom(typeof(Polar)) && fromType.IsAssignableFrom(typeof(Cartesian))) return true;
		if (fromType == typeof(Polar)) return true;
		if (fromType == typeof(Cartesian)) return true;
		return false;
	}
	public Type CurrentType {
		get {
			if (this.polar != null) return typeof(Polar);
			return typeof(Cartesian);
		}
		set {
			if (!this.CanCastTo(value, this.GetTransparentProxy())) {
				throw new InvalidOperationException("指定された型にはキャストできません: " + value.FullName);
			}
			// これからは指定された型として振る舞うようにする
			if (value == typeof(object)) {
			} else if (value == typeof(Polar) && this.cartesian != null) {
				this.polar = this.cartesian.ToPolar();
				this.cartesian = null;
			} else if (value == typeof(Cartesian) && this.polar != null) {
				this.cartesian = this.polar.ToCartesian();
				this.polar = null;
			}
		}
	}
	public string TypeName {
		get {
			return this.CurrentType.FullName;
		}
		set {
			// 指定された名前の型を探して、その型として振る舞うように変更する。
			this.CurrentType = AppDomain.CurrentDomain.GetAssemblies()
				.Select((asm) => asm.GetType(value, false))
				.First((type) => type != null);
		}
	}

	public override IMessage Invoke(IMessage msg) {
		var invokation = (IMethodCallMessage)msg;
		if (invokation.MethodBase.Name == "get_CurrentType") {
			return new ReturnMessage(this.CurrentType, new object[0], 0, invokation.LogicalCallContext, invokation);
		}
		if (invokation.MethodBase.Name == "set_CurrentType") {
			this.CurrentType = (Type)invokation.GetArg(0);
			return new ReturnMessage(null, new object[0], 0, invokation.LogicalCallContext, invokation);
		}

		// 現在 polar か cartesian のどちらとして振る舞っているかに応じて、メッセージの移譲先を変える
		if (this.polar != null) return RemotingServices.ExecuteMessage(this.polar, invokation);
		return RemotingServices.ExecuteMessage(this.cartesian, invokation);
	}
}
走らせてみた
public static void Main(string[] args) {
	var p = (ITypeChangeable)(new Point2d().GetTransparentProxy());
	var p_original = p;

	((Polar)p).R = 10;
	((Polar)p).Theta = Math.PI / 4;
	Trace.WriteLine(p.GetType().Name);	// Polar
	Trace.WriteLine(string.Format(
		"({0}, {1})", 
		((Polar)p).R, 
		((Polar)p).Theta)
	);		// (10, 0.785398163397448)

	// p の型を Cartesian に変更
	p.CurrentType = typeof(Cartesian);
	Trace.WriteLine(p.GetType().Name);	// Cartesian
	Trace.WriteLine(string.Format(
		"({0}, {1})", 
		((Cartesian)p).X, 
		((Cartesian)p).Y)
	);		// (7.07106781186548, 7.07106781186547)

	// p の X, Y を書き換えておく
	((Cartesian)p).X = -1;
	((Cartesian)p).Y = +1;

	// p, p_original の参照が同じであることを確認
	Trace.WriteLine(object.ReferenceEquals(p, p_original));	// True

	// p_original (== p) の型を Polar に戻す
	p_original.CurrentType = typeof(Polar);
	Trace.WriteLine(string.Format(
		"({0}, {1})",
		((Polar)p).R,
		((Polar)p).Theta)
	);		// (1.4142135623731, 2.35619449019234)
}

問題点

このコードによって一応お題は果たした(と思う)が、実は問題のある仕上がりとなっている。

例えば、上の Main() の中で、Polar と Cartesian 双方の ToStirng() を override した上で、CurrentType を書き換えた後に p.ToString() などを実行すると、「メソッドを呼び出そうとしていることが 'CSharpSandBox.Program+Cartesian' を公開するオブジェクトのタイプ 'CSharpSandBox.Program+Polar' で定義されました。」という RemotingException が発生する。Polar の ToString を override しないと発生しない辺り、どうやら透過的プロクシは自分の型を覚えてしまっているらしい。そのため、途中で型が変わっても昔の型での MethodInfo を使ってしまい、それによって型チェックに引っかかってしまうようだ。

また、ToPolar や ToCartesian というださいメソッドがあるが、これをキャスト演算子として定義してしまっても問題が出うる。なぜなら、

Cartesian cartesian = (Cartesian)p;
p.CurrentType = typeof(Cartesian);
Polar polar = (Polar)cartesian;

といったコードを書いた場合、最後の部分の (Polar)cartesian は static operator Polar(Cartesian) の呼び出しにコンパイル時に決め打ちされてしまうため、透過的プロクシなどをバイパスされてしまい、「インスタンスが属するクラスをあとから変更する操作」ではなく単なるユーザー定義のキャスト演算になってしまうのである。

といった風に、一応ランタイムを破壊しない&あまり CLI の実装に依存しないようには出来たものの、やはりまともに動作しているとは言いづらい状況である。

やはり、C# の場合、インスタンスや変数等々に静的な型が紐付いているのが良さであると思うので、各種動的さはもっと別のアプローチでやるのが現実的かつ真っ当であろう。そういう意味では、C# 4.0 から入る dynamic*2 などに期待したいものだ。・・・こういう明らかに evil なものを作るのもネタとしてはありだと思うけれどwww

*1:の抽象化

*2:透過的プロクシと似たようなことをもう少し手軽に出来るようだ