C# でジェネリックスを使用したシングルトンパターンを実装する
SingletonパターンとGenericsを組み合わせて使うと面白いことができたりします。
static属性メンバの挙動とSingletonパターン
まずは、static属性メンバの挙動のおさらいから。
static属性が付与されたメンバはインスタンスではなくクラス自身に属するため、継承により子クラスを複数作った場合、それら子クラスのいずれかから操作を行うとそれ以外の全ての子クラスからアクセスできるstaticメンバの値や状態も問答無用で一緒に変更されてしまいます(子クラスのインスタンスでも、staticメンバ自体は、親クラス自身に属し、単一の存在でインスタンス変数のように複製されないため)。下記にSingletonパターンを使用した例を示します。
class Program { static void Main(string[] args) { var alpha = Alpha.GetInstance(typeof(Alpha).Name); var beta = Beta.GetInstance(typeof(Beta).Name); beta.Name = "hoge"; // beta.Nameの値だけ変えたい。Beta→hoge Console.WriteLine(" alpha.Name = {0} \r\n beta.Name = {1}", alpha.Name, beta.Name); } } class Alpha : Base {} class Beta : Base {} class Base { protected static Base Instance; public string Name { get; set; } public static Base GetInstance(string name) { if (Instance == null) { Instance = new Base() { Name = name }; } return Instance; } }
このソースコードを実行すると、次のような結果になります。
alpha.Name = hoge beta.Name = hoge
コーディングしたプログラマーとしては、
alpha.Name = Alpha beta.Name = hoge
このようにbetaのNameの値だけ変えたいという意図があったと思いますが、実際の実行結果ではstatic属性の特性に従いalphaのNameの値まで変わっています(たびたび引っかかるトラップです)。なお、このソースコードでは「beta.Name = "hoge";」が無くても意図通りの結果にはなりません。
さて、どうやって所期の意図を実現させるかと言うと、ここで本題の「ジェネリックスを使用したシングルトンパターン」が登場します。
SingletonパターンとGenericsを組み合わせて使う
上記のSingletonパターンを使用したソースコードを以下のように修正します。
// Program クラスは修正部分なし class Program { static void Main(string[] args) { var alpha = Alpha.GetInstance(typeof(Alpha).Name); var beta = Beta.GetInstance(typeof(Beta).Name); beta.Name = "hoge"; // beta.Nameの値だけ変えたい。Beta→hoge Console.WriteLine(" alpha.Name = {0} \r\n beta.Name = {1}", alpha.Name, beta.Name); } } // BaseクラスをBase<T>クラスに置き換えるだけ! class Alpha : Base<Alpha> {} class Beta : Base<Beta> {} class Base<T> where T : class, new() { protected static Base<T> Instance; public string Name { get; set; } public static Base<T> GetInstance(string name) { if (Instance == null) { Instance = new Base<T>() { Name = name }; } return Instance; } }
これを実行すると、当初のコーディングの意図通り、
alpha.Name = Alpha beta.Name = hoge
という結果になります。
ちなみに「beta.Name = "hoge";」を消した場合、
alpha.Name = Alpha beta.Name = Beta
とこちらの場合も意図通りの値が表示されるようになっています。
C# で Gecko ベースのブラウザを作成する
C# で Gecko レタリングエンジンベースのブラウザを作るためのライブラリには、GeckoFx があります。
GeckoFx を Google で検索すると、開発初期に使用されていた Google Code でのページが上位に表示されるため、開発が止まっているように見えてしまいますが、現在も Bitbucket で開発が継続されています。
導入準備
1. GeckoFx のダウンロード
GeckoFx のプロジェクトホームページから、Gecko Fx 10.0 をダウンロードします。最新版でも構いませんが、ここで 10.0 を選ぶ理由は、次の手順2で説明します。
2. XULRunner のダウンロード
GeckoFx は、Geckoエンジンをコンポーネント化した XPCOM である XULRunner をラップするライブラリですので、このまま単体では動作しません。ですので、この XULRunner を ftp.mozilla.org からダウンロードします。いくつもあるバージョンの中から 10.0.4esr を選ぶ理由は、XULRunnerがラピッドリリースであり最新版では安定性を重視できないこと、esrは法人向け延長サポート版であることの二点が挙げられます。
なお、GeckoFx と XULRunner のバージョンは同じもの(XULRunnerのバージョンが x.y.z である場合は、GeckoFx は x.y のバージョン)を選ぶ必要があります(ここでは、GeckoFx 10.0 と XULRunner 10.0.4esr)。
3. 圧縮ファイルの展開
手順1と手順2で入手した圧縮ファイルをそれぞれ展開しておきます。
プロジェクト作成
1. プロジェクト作成
2. ビルド
プロジェクト固有のビルドフォルダが用意されていなければ、プロジェクトのビルドフォルダを確保するためにソリューションのビルドを実行します。
3. 参照設定の追加
プロジェクトの参照設定に「geckofx-10.dll」を追加します。
4. ソースコード修正
このままでは GeckoFx を使えない(XULRunner を初期化できない)ので、プロジェクトの「Program.cs」を以下のように修正します。ファイルパスの部分は、XULRunner が置いてある場所(導入準備の手順3で XULRunner を展開したフォルダのパス)に書き換えて下さい。
[STAThread] static void Main() { Gecko.Xpcom.Initialize(ファイルパス); // この行を追加 Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); }
コーディング
Visual Studio のデザイナーから Form の Load イベントを自動生成します。その後、Form のコードエディタを開き、以下のように編集します(「using Gecko;」をファイルの using 宣言がたくさん書かれている先頭部分の最後尾に追加して下さい)。
public partial class Form1 : Form { private GeckoWebBrowser gecko = default(GeckoWebBrowser); public Form1() { InitializeComponent(); this.SuspendLayout(); this.gecko = new GeckoWebBrowser() { Dock = DockStyle.Fill, Parent = this }; this.ResumeLayout(false); } private void Form1_Load(object sender, EventArgs e) { this.gecko.Navigate("http://google.co.jp/"); } }
以上で、完成です。
GeckoFx をツールボックスの項目に追加し、Form にドラッグ&ドロップでコントロールを追加すると、上記のうちLoad イベントの内容だけ書くということもできます。
C# で Webkit ベースのブラウザを作成する
C# で Webkit レタリングエンジンベースのブラウザを作るためのライブラリとして、Open-Webkit-Sharp や WebKit.NET などがあります。Open-Webkit-Sharp の方が Webkit.NET よりも新しく、Webkit.NETをベースにして開発されたものですので、こちらを使用します。
導入準備
1. ライブラリプロジェクトのダウンロード
Open-Webkit-Sharp から 2012年6月24日時点で最新版の OpenWebKitSharp 3.0.1.0023.rar をダウンロードします。
2. ライブラリプロジェクトのビルド
ダウンロードした rar ファイルを解凍ファイルを使用して任意のフォルダへ展開します。
すると、「open-webkit-sharp」というフォルダが生成されますので、その中にある「OpenWebKitSharp_2010.sln」をダブルクリックしてVisual Studio 2010 で開きます。
それから、メニューの「ビルド」、「ソリューションのビルド」を選び(または、F6 キー を押す)、ソリューションをビルドします(この時にもしビルドエラーが出た場合は、もう一度ビルドし直すと成功します)。
ソリューションの各プロジェクトは、「open-webkit-sharp」フォルダ内にある「build」フォルダに一括でビルドされます。これらが正しくビルドされていることを確認するために、「build」フォルダ内の「Example.exe」を実行します。エラーなく正常に起動及び動作した場合は、次の手順へ進みます。
ライブラリを導入するプロジェクトの作成
1. プロジェクトの作成
2. 対象のフレームワークの変更
プロジェクトのプロパティを開き、「対象のフレームワーク」を「.NET Framework 4 Client Profile」から「.Net Framework 4」に変更します。
この手順は、ライブラリが「.NET Framework 4」のフルパッケージでないと使用できない「System.Net」コンポーネントに依存しているために必要です。
3. ビルド
プロジェクトのビルドフォルダを確保するためにソリューションのビルドを実行します。なお、プロジェクト固有のビルドフォルダがあり、自動生成させないのであれば、この手順はスキップして構いません。
4. コンポーネントの追加
まず、「System.Net」コンポーネントをプロジェクトの参照設定に追加します。
次に、「build」フォルダ内の「OpenWebKitSharp.dll」と「WebKit.Interop.dll」を追加します。
その後、「WebKit.Interop.dll」に関しては、ソリューションエクスプローラの参照設定内の「WebKit.Interop」を右クリックし「プロパティ」をクリックします。「相互運用機能型の埋め込み」オプションが「True」になっている場合は、「False」へ変更します。
5. ライブラリコンポーネントのコピー
「open-webkit-sharp」フォルダ内の「Core」フォルダから、「Webkit.Interop.dll」以外の全てのファイルを、手順3で作成したビルドフォルダへコピーします(自動生成でないプロジェクト固有のビルドフォルダがある場合は、そちらにコピー)。
6. デバッグの設定
最重要。プロジェクトのデバッガーの設定を変更します。
プロジェクトのプロパティの「デバッグ」ページ内にある「デバッガーを有効にする」の「Visual Studio ホスティングプロセスを有効にする」のチェックを外します。このチェックが外れていないと、Visual Studio からのデバッグ起動はエラーが発生するため出来ません。
コーディング
Visual Studio のデザイナーから Form の Load イベントを自動生成します。その後、Form のコードエディタを開き、以下のように編集します。
private WebKitBrowser webkit = default(WebKitBrowser); public Form1() { InitializeComponent(); webkit = new WebKitBrowser() { Dock = DockStyle.Fill, Parent = this }; } private void Form1_Load(object sender, EventArgs e) { webkit.Navigate("http://www.google.co.jp/"); }
以上で、完成です。
トラブルシューティング
動くまでに問題が結構な程度に発生したので、トラブルシューティングをまとめておきます。
- アプリケーションの起動時に「エラーメッセージ(e0434352)」または「(HRESULT からの例外: 0x80131040)」が出る。
導入準備の手順2をやった後、ライブラリを導入するプロジェクトの作成の手順4をやり直します。 - アプリケーションの起動時に「0x80029C4A (TYPE_E_CANTLOADLIBRARY)」が出る。
ライブラリを導入するプロジェクトの作成の手順5が正しく行われていません。「Core」フォルダ内にある「OpenWebKitSharp.manifest」ファイルを、アプリケーションのビルドフォルダへコピーします。 - アプリケーションの起動時に「(HRESULT からの例外: 0x800736B1)」が出る。
Visual C++ ランタイムライブラリをインストールします。 - デバッグ開始時に「外部コンポーネントが例外をスローしました。」が出る。
ライブラリを導入するプロジェクトの作成の手順6をやり直します。
非同期処理でUIスレッドを操作する方法
非同期処理でUIスレッドを操作するやり方について、ボタン button1 とテキストボックス textbox1 のコントロールが配置された Form で、button1 のクリックする(button1_Click)と、並列処理した結果を非同期的に textbox1 へ表示するいくつかのソースコード例を簡単なサンプルとして示します。
下準備
button1 のクリックイベントハンドラに紐づけされた button1_Click メソッドと、結果表示するために文字列を整形する GetString メソッドを用意します。
なお、button1_Click メソッドの非同期処理を行うタスク内には、並列ループ処理を行う Parallel.For メソッドがありますが、下記のように lock ステートメントと同期オブジェクトを使って、ロック状態でクリティカルセクション(複数のスレッドが同時に操作してはダメな部分)を処理しないと、textbox1 へ不完全な状態で結果出力されてしまいます。
private void button1_Click(object sender, EventArgs e) { textBox1.Text = this.GetString(Thread.CurrentThread.ManagedThreadId, "NomalCall"); var task = Task.Factory.StartNew(() => { var syncObject = new Object(); var strBuilder = new StringBuilder(); Parallel.For<string>(0, 10, () => "", (i, loop, str) => str += this.GetString(Thread.CurrentThread.ManagedThreadId, "Paralell.For", i), x => { lock (syncObject) { strBuilder.Append(x); } }); return strBuilder.ToString(); }); } private string GetString(int threadID, string prefix, int index = -1, string append = "") { var basedFormat = new StringBuilder("ThreadID= {2}\r\n"); basedFormat.Insert(0, (index >= 0) ? "{3}: index= {1}, " : string.IsNullOrEmpty(append) ? "{3}: UI " : "{0}\r\n{3}: UI "); return string.Format(basedFormat.ToString(), append, index, threadID, prefix); }
ContinueWith メソッドを使用する方法
下準備した button1_Click メソッド内の最後尾(task 変数の宣言の下)に以下のソースコードを追加します。
var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); task.ContinueWith(t => { textBox1.AppendText(this.GetString( Thread.CurrentThread.ManagedThreadId, "ContinueWith", append: t.Result)); }, taskScheduler);
この状態で実行すると以下のような結果になります。
NomalCall: UI ThreadID= 10 Paralell.For: index= 1, ThreadID= 12 Paralell.For: index= 5, ThreadID= 12 Paralell.For: index= 7, ThreadID= 12 Paralell.For: index= 9, ThreadID= 12 Paralell.For: index= 0, ThreadID= 11 Paralell.For: index= 2, ThreadID= 11 Paralell.For: index= 3, ThreadID= 11 Paralell.For: index= 4, ThreadID= 11 Paralell.For: index= 6, ThreadID= 11 Paralell.For: index= 8, ThreadID= 11 ContinueWith: UI ThreadID= 10
結果から見ても分かるように、ContinueWithメソッドで実行されるタスクは、UI スレッドで実行されています。なぜ UI スレッドで実行されるのかというと、TaskScheduler クラスの FromCurrentSynchronizationContexttaskScheduler メソッドの戻り値を、ContinueWith メソッドに渡すことで、コントロールにアクセスした時に UI スレッド上で処理を行うように設定されるためです。上記例では、textbox1 へアクセスした時に UI スレッド上で処理を実行します。
メリットは、次に紹介する Control.Invoke メソッドを使用する方法よりも疎結合(他のオブジェクト等との依存度が低い)で自由度が高いということ、一方、デメリットは、次の方法と比べてコーディング量が少し増えてしまうことです。
Control.Invoke メソッドを使用する方法
下準備した button1_Click メソッドの task 変数内の最後の行(「return strBuilder.ToString();」の行)を以下のソースコードに置き換えます。
textBox1.Invoke((Action)(() => { textBox1.AppendText(this.GetString( Thread.CurrentThread.ManagedThreadId, "textBox1.Invoke", append: strBuilder.ToString())); }));
この状態で実行すると以下のようになります。
NomalCall: UI ThreadID= 9 Paralell.For: index= 1, ThreadID= 11 Paralell.For: index= 8, ThreadID= 11 Paralell.For: index= 0, ThreadID= 10 Paralell.For: index= 2, ThreadID= 10 Paralell.For: index= 3, ThreadID= 10 Paralell.For: index= 4, ThreadID= 10 Paralell.For: index= 5, ThreadID= 10 Paralell.For: index= 6, ThreadID= 10 Paralell.For: index= 7, ThreadID= 10 Paralell.For: index= 9, ThreadID= 10 textBox1.Invoke: UI ThreadID= 9
UI スレッドの非同期処理で昔からよく使われる方法です。
Control クラスから継承される Invoke メソッドを使用すると、メソッド内で実行されるコントロールに対しての全ての処理は、UI スレッド上で行われます。
メリットは、ContinueWith メソッドを使用するよりも、コーディング量が少なく理解しやすいソースコードになることですが、デメリットは、task があるクラスと ui を操作するクラスが分けられている場合には少々手間なことをしないといけないため扱いにくいことです。
また、この方法を使った場合で Parallel.For メソッドのみで非同期処理をしている時に陥りがちな落とし穴があり、Parallel.For メソッド内で Invoke メソッドを呼び出すとデッドロックが発生します。理由としては、Parallel.For メソッドを呼び出すと実行が終わるまでは呼び出し元スレッドがブロックされてしまうためです。従って、デッドロックを引き起こさないようにするためには、別スレッドで実行する機構を持つ Task や Thread の内部に Invoke メソッドを含む Parallel.For を配置します。なお、上記例ではその対策がされているためデッドロックを引き起こすことはありません。
C# のクロージャと部分適用とカリー化
C# でクロージャや部分適用やカリー化を使いこなすためのメモ。
クロージャ
英語のスペルでは「closure」、日本語で言い直すと「閉包」。
クロージャの特徴を掻い摘んで書くと、ローカル変数等をキャプチャーしたレキシカル変数を持ち、その値を操作・参照するラムダ式を返す、というものです。
つまり、どんなものかを手っ取り早くクロージャの例を示してみると、
Func<int, Func<int>> method = x => { var captured = x; return () => ++captured; }; var increment = method(100); Console.WriteLine(increment()); Console.WriteLine(increment());
となります。これを実行すると、以下のような結果が出力されます。
101 102
さて、クロージャを理解するうえでの勘所は、レキシカル変数となります。このレキシカル変数は、外側のラムダ式(エンクロージャと呼ぶ)を呼び出した時点で値が確保され、そのエンクロージャを有効範囲とする変数です。上記例で言えば、エンクロージャ method の引数に 100 を渡した時点で、レキシカル変数 captured に 100 が代入されます。
レキシカル変数がエンクロージャを有効範囲とするメリットは、上記例の8行目と9行目に活きてきます。なぜならラムダ式 increment を呼び出すたびに、エンクロージャにある実体が同一の変数 captured に対しての操作が行われるからです。従って、captured がインクリメントされた値が上記のような結果として出力されるのです。面白いですね。
ちなみに、エンクロージャ method の仮引数 x もレキシカル変数扱いです。ですので、例の1行目から4行目は、
Func<int, Func<int>> method = x => () => ++x;
と、もっと簡単に1行で書けてしまいます。加えて、例の1行目から5行目まではよりコンパクトに、
var increment = ((Func<int, Func<int>>)(x => () => ++x))(100);
と書くこともできますが、エンクロージャ部分が簡単なものであっても、あとから見直した時に訳が分からなくなりかねないので、積極的な利用は避けた方が得策です。
部分適用とカリー化
クロージャを応用したものが、 部分適用とカリー化です。
部分適用とカリー化も、エンクロージャが返すラムダ式の引数を減らすという目的があり、部分適用は、1つ以上引数を減らしたものに対して用い、一方、カリー化は、減らした後の引数が1つだけになるものに対して用います。つまり、カリー化は部分適用に含まれます。すなわち、部分適用のうち、減らした後の引数が1つだけになるものがカリー化なのです。
部分適用
Func<int, Func<int, int, int>> method = x => (y, z) => x + y + z; var multiPlus = method(100); Console.WriteLine(multiPlus(100, 1)); Console.WriteLine(multiPlus(200, 2));
実行結果
201 302
やっていることの基本は、クロージャと変わりありません。わずかな違いと言えば、エンクロージャが返すラムダ式の仮引数が一つ以上ある、ということくらいです。
部分適用は、予め使う値が決まっている変数をエンクロージャの引数とし、レキシカル変数としてキャプチャしておいて、動的に変更される一つ以上の変数をエンクロージャが返すラムダ式の引数として実行時に渡すことができるメリットがあります。
上記例はあまり良い例とは言えませんが、部分適用は、呼び出しごとに値が変わらない変数を引数として渡す必要があるメソッドを置き換えることでき、それにより可読性向上の効果が期待できます。
カリー化
Func<int, Func<int, int>> method = x => y => Math.Pow(x, y); var twoPower = method(2); Console.WriteLine(twoPower(2)); Console.WriteLine(twoPower(3));
実行結果
4 8
部分適用と実質同じなので特に言うことはないです。部分適用に比べてカリー化を使う機会は、カリー化自体の特性からあまりないかもしれません。一つ以外の引数の値が予め決められるという機会に恵まれることは稀有でしょうから。
まとめ
以下、要点。
- クロージャの概要
クロージャとは、ローカル変数等をキャプチャーしたレキシカル変数を持ち、その値を操作・参照するラムダ式を返すもの。 - クロージャの要点
クロージャを使いこなすための肝となるのは、レキシカル変数の扱い方・扱われ方について覚えること。 - 部分適用とカリー化の違い
エンクロージャが返すラムダ式の仮引数を減らす部分適用のうち、仮引数が一つだけになるものが特別にカリー化と呼ばれる。
クロージャも部分適用もカリー化も上手く使いこなせれば、オブジェクト指向パラダイムに縛られた思考から解放されます。ただ、これらを使う際に、他の言語に比べてC#ではラムダ式の宣言が長くなりがちで、可読性が落ちやすくコーディング量がちょっとばかり多めな部分が唯一挙げるデメリットかな、と思います。