privateなコンストラクタしか持たないクラスをリフレクションで生成する
テスト等で、Privateなコンストラクタしか持たないクラスを生成したくなることがある。
コンストラクタが引数を取らない場合、Activatorを利用すれば簡単にできる。
class Test { private Test() { } }
var obj = (Test)Activator.CreateInstance(typeof(Test), true);
引数を取る場合、もう少し複雑な別のオーバーロードを利用しなければならない。
以下のようになる。
class Test2 { public string Str { get; private set; } private Test2(string str) { Str = str; } }
var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; var obj2 = (Test2)Activator.CreateInstance(typeof(Test2), flags, null, new object[] { "test" }, null);
ConditionalWeakTableを利用して拡張メソッドに状態を持たせる
拡張メソッドを利用してインタフェースに実装をもたせる場合、インタフェースが公開しているもの以外の状態は利用できない。 が、ConditionalWeakTableを利用することでそれに近いことが実現できる。
class Parameter { public string str { get; set; } } interface IExtensible { } static class Extensions { static ConditionalWeakTable<IExtensible, Parameter> state = new ConditionalWeakTable<IExtensible, Parameter>(); public static void SetStr(this IExtensible target, string str) { state.GetOrCreateValue(target).str = str; } public static string GetStr(this IExtensible target) { return state.GetOrCreateValue(target).str; } }
こんな感じに利用する。
ConditionalWeakTableは連想配列のように働くのだが、Keyへの参照が弱参照で、他の参照がすべてなくなった場合GCにより回収される。
そして、その時ConditionalWeakTable内のレコードも削除されるのだそうだ。至れり尽くせりである。
ちなみに、ConditionalWeakTableのValueは参照型でなくてはいけない。設定のメソッドがないのでさもありなんである。 だが、実際は結構Enum等の値型を入れたいことがある。仕方がないのでこのようなクラスを使っている。
class Box<T>{ public T Value { get; set; } public static implicit operator T(Box<T> target) { return target.Value; } public static implicit operator Box<T>(T target) { return new Box<T> { Value = target }; } }
今ひとつすっきりしないやり方だが、いい方法が見つからなかった。 他の局面でもこのような問題が出ることもあると思うのだが、何かもう少し綺麗なやり方はないのだろうか。
BinaryWriter/Readerとエンディアン変換
.Net FrameworkのBinaryWriter/Readerはビッグエンディアンに対応していない。
ので、ビッグエンディアンを扱う場合はこんな感じで書いていた。
static void Main(string[] args) { using (var stream = new MemoryStream()) using (var writer = new BinaryWriter(stream)) { writer.Write(SwapBytes(0x1234)); } } static UInt16 SwapBytes(UInt16 value) { return (UInt16)((value << 8) | (value >> 8)); }
別にこれはこれでなんの不都合もないのだが、ふと暗黙の型変換を使うと綺麗にかけるのではないかと思い立つ。
public struct UInt16BigEndian { UInt16 value; public static implicit operator byte[](UInt16BigEndian target) { return new byte[] { (byte)(target.value >> 8), (byte)(target.value & 0xff) }; } public static implicit operator UInt16 (UInt16BigEndian target) { return target.value; } public static implicit operator UInt16BigEndian(UInt16 target) { return new UInt16BigEndian(target); } public static implicit operator UInt16BigEndian(byte[] bytes) { return new UInt16BigEndian((UInt16)((bytes[0] << 8) | bytes[1])); } public UInt16BigEndian(UInt16 value) { this.value = value; } }
こんな感じの型を定義する。
ついでにStreamの拡張メソッドも定義する。
static class StreamExtension { public static void WriteBytes(this Stream stream,byte[] bytes) { stream.Write(bytes, 0, bytes.Length); } public static byte[] ReadBytes(this Stream stream, int length) { var buf = new byte[length]; stream.Read(buf, 0, length); return buf; } }
すると、こんなふうに使える。
class Packet { public UInt16 Length { get; set; } public byte[] Data { get; set; } public byte[] ToByteArray() { using (var stream = new MemoryStream()) { stream.WriteBytes((UInt16BigEndian)Length); stream.WriteBytes(Data); return stream.ToArray(); } } public static Packet CreateFrom(byte[] bytes) { using (var stream = new MemoryStream(bytes)) { UInt16BigEndian length = stream.ReadBytes(2); byte[] data = stream.ReadBytes(length); return new Packet { Length = length, Data = data }; } } }
中々見た目が良い。
Fody.Costuraを利用して複数のアセンブリをまとめ、単一の実行ファイルを作成する
これまで、複数のアセンブリをまとめる場合MicrosoftのILMergeを利用していた。
https://www.microsoft.com/en-us/download/details.aspx?id=17630
ILMergeはコマンドラインツールなので、プロジェクトのビルド後のイベントに実行コマンドを記述して利用していたのだが、アセンブリが増えたり減ったりする度にコマンドを編集しなければならず結構不便だった。
Fodyは.Netのビルドフローの中で色々やるAPIを提供するプラットフォーム。
CosturaはFodyで動作するプラグインで、プロジェクト内のアセンブリを自動的に実行ファイルにマージしてくれる。
インストール
NuGetからCostura.Fody
を検索してインストールする。
使用方法
プロジェクト内のすべてのアセンブリを一つにまとめたいだけの場合、特に何もする必要はない。
Costuraインストール後にビルドを行うだけで自動的にアセンブリがマージされる。
特定のアセンブリを除外したい場合、Costuraインストール時にプロジェクトに追加されるFoyWeavers.xml
ファイルを編集する。
例えば、Moqを除外したい場合はこのような感じ。
<?xml version="1.0" encoding="utf-8"?> <Weavers> <Costura> <ExcludeAssemblies> Moq </ExcludeAssemblies> </Costura> </Weavers>
独自SynchronizationContextの実装
独自のスレッドでasync/awaitしたかったので、SynchronizationContextを実装する。
class SingleThreadSynchronizationContext : SynchronizationContext { BlockingCollection<Action> queue = new BlockingCollection<Action>(); Thread thread; public SingleThreadSynchronizationContext() { thread = new Thread(Run); thread.Start(); } void Run() { SynchronizationContext.SetSynchronizationContext(this); try { while (true) { var action = queue.Take(); action(); } } catch (InvalidOperationException) { // Complete } } public void Stop() { queue.CompleteAdding(); thread.Join(); } Task Exec(Action func) { var task = new TaskCompletionSource<object>(); queue.Add(() => { try { func(); task.SetResult(null); } catch (Exception e) { task.SetException(e); } }); return task.Task; } public override void Post(SendOrPostCallback d, object state) { Exec(() => d(state)); } public override void Send(SendOrPostCallback d, object state) { Exec(() => d(state)).Wait(); } public override SynchronizationContext CreateCopy() { return this; } }
こんな感じでasync/awaitが動作した。
Post、Send、CreateCopyあたりをオーバーライドすれば良いらしい。
Console.CancelKeyPressのバグ
.Net Frameworkのコンソールアプリケーションでは、Console.CancelKeyPressイベントにハンドラを登録することでCtrl+Cが押された時の動作をハンドルできる。
そのままプログラムを終了させたくない場合はConsoleCancelEventArgs.Cancel
にtrue
を設定する。
ConsoleCancelEventHandler handler = (s, e) => { Console.WriteLine("Cancelled!"); e.Cancel = true; }; Console.CancelKeyPress += handler;
特定の画面だけハンドルしたかったのでハンドラを削除すると、再登録した時に何故か反応しない。
ConsoleCancelEventHandler handler = (s, e) => { Console.WriteLine("Cancelled!"); e.Cancel = true; }; Console.CancelKeyPress += handler; // OK Console.CancelKeyPress -= handler; Console.CancelKeyPress += handler; // NG
調べてみると、.Net Frameworkのバグのようだ。(4.5.2で確認)
CancelKeyPressから最後のハンドラが削除されるとWindowsに登録されたコールバックが削除されてしまうらしい。
最後のハンドラが削除されないようにダミーを登録しておくことで回避できる。
Console.CancelKeyPress += delegate { }; ConsoleCancelEventHandler handler = (s, e) => { Console.WriteLine("Cancelled!"); e.Cancel = true; }; Console.CancelKeyPress += handler; // OK Console.CancelKeyPress -= handler; Console.CancelKeyPress += handler; // OK