クラスをジェネレートする際にメタデータでFreezableかどうか保持して作るのが楽ちんなのかなー。
Castle.DynamicProxyとPostSharpで実装してみました。
どうやってインストールするんですか!と、思ったけど、NuGetですね。Castle.Core で。
The Creators of PostSharp – SharpCrafters
こっちもNuGetで。ただ、無料のBASICライセンスを別途取得する必要がありました。むかーし、登録したアカウントが邪魔してちょっと手間取った...。
Castle.DynamicProxy
まずはCastle。ベケットは出てこないよ。
Dynamic Proxy Tutorial « Krzysztof Koźmic on software
ここに書いてるとおりです!
ようするに動的に派生クラスを生成するFactoryを使ってインスタンスを作るのと、virtualなプロパティ/メソッドに対してインターセプトできるようになる、っていうのがCastle.DynamicProxyです。overrideね。
public interface IFreezable
{
bool IsFrozen { get; }
void Freeze();
}
Freezeできるようにするためのインターフェース。CastleでもPostSharpでも同じのを使うよ。
public class Animal
{
public virtual string Name { get; set; }
public virtual int Age { get; set; }
public override string ToString()
{
return string.Format("Name: {0}, Age: {1}", Name, Age);
}
}
こんなモデルクラスを用意します。もちろんvirtualで。だけど、これ自体がIFreezableを実装する必要はないので注意。
ここからは、ちょっと長めのコードになるけど、簡単に説明するとIFreezableを実装したInterceptorを実装し、それを注入したProxyインスタンスを生成。Proxyインスタンスではvirtualなものにアクセスした場合、Interceptorが実行されるのでその時都度Freezeされたかチェックする、っていう流れです。
こんなイメージ。公式サイトより。
class FreezableInterceptor : IInterceptor, IFreezable
{
private bool _isFrozen;
public void Freeze()
{
_isFrozen = true;
}
public bool IsFrozen
{
get { return _isFrozen; }
}
public void Intercept(IInvocation invocation)
{
if (_isFrozen && invocation.Method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase))
{
throw new FreezableException("object frozen!");
}
invocation.Proceed();
}
}
Interceptorが↑これ。続いてヘルパーのコード。
static class Freezable
{
private static readonly ProxyGenerator _generator = new ProxyGenerator();
public static bool IsFreezable(object obj)
{
return AsFreezable(obj) != null;
}
private static IFreezable AsFreezable(object target)
{
if (target == null)
return null;
var accessor = target as IProxyTargetAccessor;
if (accessor == null)
return null;
return accessor.GetInterceptors().OfType<FreezableInterceptor>().FirstOrDefault();
}
public static bool IsFrozen(object obj)
{
var freezable = AsFreezable(obj);
return freezable != null && freezable.IsFrozen;
}
public static void Freeze(object freezable)
{
var interceptor = AsFreezable(freezable);
if (interceptor == null)
throw new FreezableException("Not freezalbe!");
interceptor.Freeze();
}
public static TFreezable CreateInstance<TFreezable>() where TFreezable : class, new()
{
var freezableInterceptor = new FreezableInterceptor();
var proxy = _generator.CreateClassProxy<TFreezable>(freezableInterceptor);
return proxy;
}
}
こっちがちょっと長い。ProxyインスタンスはIProxyTargetAccessorを実装してるので、それを利用して、Interceptorを取得してFreezeしたりIsFrozeしたりです。
[TestClass]
public class UnitTest1
{
[TestMethod]
public void Freezeさせる()
{
var rex = Freezable.CreateInstance<Animal>();
rex.Name = "Rex";
Console.WriteLine(Freezable.IsFreezable(rex)
? "Rex is freezable!"
: "Rex is not freezable. Something is not working");
Console.WriteLine(rex.ToString());
Console.WriteLine("Add 50 years");
rex.Age += 50;
Console.WriteLine("Age: {0}", rex.Age);
Freezable.Freeze(rex);
Console.WriteLine("Frozen? : " + Freezable.IsFrozen(rex));
try
{
rex.Age++;
Console.WriteLine("あれー?変更できるー。");
}
catch
{
Console.WriteLine("Frozen!!");
}
}
[TestInitialize]
public void Initialize()
{
var instance1 = new Animal();
var instance2 = Freezable.CreateInstance<Animal>();
}
[TestMethod]
public void 普通のインスタンス作る()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000;i++ )
{
var instance = new Animal();
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms",sw.ElapsedMilliseconds);
}
[TestMethod]
public void Proxyのインスタンス作る()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000; i++)
{
var instance = Freezable.CreateInstance<Animal>();
//Freezable.Freeze(instance);
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
}
[TestMethod]
public void Proxyのインスタンス作ってFreezeする()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000; i++)
{
var instance = Freezable.CreateInstance<Animal>();
Freezable.Freeze(instance);
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
}
[TestMethod]
public void 普通のインスタンスでプロパティアクセス()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000; i++)
{
var instance = new Animal();
instance.Age = 200;
instance.Name = "Foo";
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
}
[TestMethod]
public void Proxyのインスタンスでプロパティアクセス()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000; i++)
{
var instance = Freezable.CreateInstance<Animal>();
instance.Age = 200;
instance.Name = "Foo";
//Freezable.Freeze(instance);
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
}
}
動かしましょう。
- Freezeさせる[0:00.218] Success
- Proxyのインスタンスでプロパティアクセス[0:00.106] Success
- Proxyのインスタンス作ってFreezeする[0:00.117] Success
- Proxyのインスタンス作る[0:00.109] Success
- 普通のインスタンスでプロパティアクセス[0:00.003] Success
- 普通のインスタンス作る[0:00.002] Success
こんな感じです。Proxy経由は若干遅くなりますね。TestInitializeでわざわざインスタンス作ってるのは、Proxyの生成をここでやらせるためです。こうしとけば、各テストの計測では実行分のみの時間になるから。
DynamicProxyで、うーんと思ったのはvirtualの強制。まぁねー。クラス生成するんだし、そーだよなー、とは思いつつも。
PostSharp
続いてPostSharp。こっちはアセンブリを書き換えてインターセプト。クラス生成よりも強力。ちょっと使い方が分かりにくいところもあるけど、大体なんでもできちゃう系。
今回はInstanceLevelAspectでモデルクラスにインターフェース(IFreezable)を実装し、LocationInterceptionAspectでプロパティアクセスをインターセプト。OnSetValueでIsFrozenを確認してるよ。
[Serializable]
[IntroduceInterface(typeof(IFreezable))]
class FreezableAttribute : InstanceLevelAspect, IFreezable
{
[NonSerialized]
private bool _isFrozen;
public bool IsFrozen
{
get { return _isFrozen; }
}
public void Freeze()
{
_isFrozen = true;
}
}
まずは、既存クラスにインターフェースを後付Mixin。
[Serializable]
class FrozenAttribute : LocationInterceptionAspect
{
public override void OnSetValue(LocationInterceptionArgs args)
{
var freezable = args.Instance as IFreezable;
if (freezable != null && freezable.IsFrozen)
{
throw new FreezableException(string.Format("frozen {0}", args.LocationFullName));
}
args.ProceedSetValue();
}
}
インターフェース実装されてるなら、Freezableだからプロパティアクセスにインターセプト。
これだけ。これだけだけど、これらを1個のAspectにしようとしてスゴイ遠回りした。結果、インターフェースのMixinとインターセプトは同一Aspectにできない、みたい。できる方法があるなら教えて!
[Freezable, Frozen]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return string.Format("Name: {0}, Age: {1}", Name, Age);
}
}
モデルね。属性に2つ書かないといけないのが残念ポイントだけど、virtualでもないし、これならCollection/List/DictionaryいろいろReadOnlyにしてしまえます。ね。
[TestClass]
public class UnitTest2
{
[TestMethod]
public void Freezeさせる()
{
var rex = new Person();
rex.Name = "Rex";
Console.WriteLine(rex.ToString());
Console.WriteLine("Add 50 years");
rex.Age += 50;
Console.WriteLine("Age: {0}", rex.Age);
rex.Freeze();
Console.WriteLine("Frozen? : " + rex.IsFrozen());
try
{
rex.Age++;
Console.WriteLine("あれー?変更できるー。");
}
catch
{
Console.WriteLine("Frozen!!");
}
}
[TestMethod]
public void Newする()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000; i++)
{
var instance = new Person();
instance.Freeze();
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
}
[TestMethod]
public void NewしてFreezeする()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000; i++)
{
var instance = new Person();
instance.Freeze();
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
}
[TestMethod]
public void Newしてプロパティアクセス()
{
var sw = new Stopwatch();
sw.Start();
for (var i = 0; i < 10000; i++)
{
var instance = new Person();
instance.Age = 200;
instance.Name = "Foo";
}
sw.Stop();
Console.WriteLine("create 10000 instances at {0} ms", sw.ElapsedMilliseconds);
}
}
こんなテストで実行。
- Freezeさせる[0:00.084] Success
- NewしてFreezeする[0:00.015] Success
- Newしてプロパティアクセス[0:00.012] Success
- Newする[0:00.004] Success
素のクラスに比べればそれでも遅くはなるけど、DynamicProxyよりは高速。そりゃそーだ。アセンブリ書き換えてんだから。
1個の属性でできるとうれしいなー。
いずれにせよ、モデルクラス1個ずつFreezableをプロパティごとに実装なんてコード生成以外にありえないと思うじゃない。でも、インターセプトするなら、それも何とか乗り越えられる。その際にProxyクラスにするか、アセンブリ書き換えるかは好みでいいとは思うけど、既存コードほとんどいじらず(Factoryでインスタンス作らなくていい)に済むし早いしでPostSharpが使いやすいかなーと思う海の日。