Action、Func、Dispatcher.Invoke(匿名メソッド、ラムダ式)
関数内に関数が書けなくても別に困らないのですが、スレッドから TextBox など UIエレメントを操作しようとすると、Actionデリゲートを使った例がでてきます。
Dispatcher.Invoke() に、関数を引数として渡す必要があり、その型は delegate だと言うことです。
- void thread()
- {
- // 実行時にエラーになる
- textBox1.Text = "スレッド実行";
- // 匿名メソッドは、delegateにキャストできない、と言うエラー
- Dispatcher.Invoke(
- (delegate)(() => { textBox1.Text = "スレッド実行"; }),
- null);
- // これは OK
- Dispatcher.Invoke(
- (Action)(() => { textBox1.Text = "スレッド実行"; }),
- null);
- }
- private void Grid_Loaded(object sender, RoutedEventArgs e)
- {
- System.Threading.Thread thread1
- = new System.Threading.Thread(thread);
- thread1.Start();
- }
上のリストが示すのは以下のことです。
-
UIスレッド以外のスレッドから、TextBox など UIエレメントを操作しようとすると、実行時に例外がスローされる。
-
これを回避するには、Dispatcher.Invoke() を使って、関数を、UIスレッドで実行することになる。
-
匿名メソッドは、delegate にキャストできない。
-
匿名メソッドは、Action にキャストできる。
-
Dispatcher.Invoke() は、Actionキャストされた匿名メソッドを受け付ける。
デリゲート
別に匿名メソッドにこだわる必要はないので、以下のようにしてみます。
- // デリゲート関数型を定義
- public delegate void aFunc();
- // 実行する関数
- void aFunc1()
- {
- textBox1.Text = "スレッド実行";
- }
- // スレッド
- void thread()
- {
- // デリゲート型メソッドを生成
- aFunc func1 = new aFunc(aFunc1);
- Dispatcher.Invoke(
- func1,
- null);
- }
これを、突き詰めれば以下のようになると言うことでした。
- // デリゲート関数型を定義
- public delegate void aFunc();
- // スレッド
- void thread()
- {
- // デリゲート型メソッドを生成
- Dispatcher.Invoke(
- new aFunc(()=>{textBox1.Text = "スレッド実行";}),
- null);
- }
匿名メソッド
関数に引数として渡す関数には、書き手にとって、名前を付ける必要性はないので、匿名が便利です。
メソッド(関数)が匿名であるためには、使われる場所に記述できなければなりません。
それがラムダ式なのだと思います。
匿名メソッドは、delegate にキャストできません。
しかし、Action にはキャストでき、Dispatcher.Invoke() の引数になります。
関数内関数とラムダ式
関数内で関数を宣言すると、その関数は外部から呼ばれていないことが明らかなので、カプセル化すると言う観点で便利です。
また、関数を定義した親の関数と同じスコープを持つので、引数の数が少なくて済みます。
しかし、「外部から呼ばれていないことが明らか」は、間違いでした。
Funcで宣言した関数は、有名、無名を問わず、後述のように外部から呼び出せます。
それでも、カプセル化の効果はあるので使います。関数をAuto変数以外に代入しない、関数の引数にか書かない、なら外部から呼び出せません。
関数内に関数を記述するには、関数の処理を =>{} のブロック内書くことになります。
本来はラムダ式は、delegate のことだと思いますが、C# の話題では =>{} の記法を指しているように見えます。
関数内に関数を記述するには、この記法しかなく、これをラムダ演算子と呼ぶので、関数内関数とラムダ式は同じもの(同じ記述方法)を指すことになります。
Action と Func
Func と Action の違いは、戻り値の有無だと考えて良さそうです。
MSDNでは、Funcデリゲート、Actionデリゲートと見出しが付いています。
デリゲートは、関数を引数で受け渡すための仕組みのように思いますが、共に普通に関数内関数を定義するのに使えます。
.NET Framework 3.0以降では、共にテンプレートとして、関数となる変数の型を宣言することになります。
Func<T, TResult>
Action<T>
そして、その変数は delegate 型ともみなされ、Dispatcher.Invoke() の引数になると言うことです。
匿名関数に名前
「匿名関数に名前」は、不自然なので、ラムダ式に名前と言ったら良いのかもしれません。
メソッドの名前とはことなり、変数に代入することを指します。
- object x;
- object y;
- private void Grid_Loaded(object sender, RoutedEventArgs e)
- {
- // デリゲート型でないのでobjectに変換できない
- object z = () => { textBox1.Text = "ABC"; };
- // ラムダ式を暗黙に型指定されたローカル変数に割り当てられない
- var z = () => { textBox1.Text = "ABC"; };
- // これは OK
- x = (Action)(() => { textBox1.Text = "ABC"; });
- y = (Func<int>)(() => { return 123; });
- }
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- ((Action)x)();
- }
-
ラムダ式は、一般的な意味での型を持たないらしく、object型に代入できない。
-
同様に、var として扱うこともできない。
-
ラムダ式は、Actionでキャストできる。
-
ラムダ式は、Funcでキャストできる。
-
ラムダ式は、object、Action、Func型の変数に代入できる。
-
したがって、ラムダ式を記述している関数外からも呼び出せる。
Listや配列にラムダ式を入れた、関数テーブルが作れそうです。
ラムダ式の実行タイミング
普通、関数内で宣言した変数(Auto変数)は、関数から復帰するとなくなります。
しかし、ラムダ式中で使われた場合、その寿命は異なっているように見えます。
- // Func<int>型の変数
- Func<int> func1;
- // 普通の関数
- void sub(Func<int> f)
- {
- Debug.WriteLine("sub "+f());
- }
- // GridのLoadedイベント
- private void Grid_Loaded(object sender, RoutedEventArgs e)
- {
- int x = 123; // Auto変数だが復帰後も参照されるように見える
- // 関数をfunc1変数に代入
- func1 = (Func<int>)(() => { Debug.WriteLine("! "+x); return x; });
- // 単に、関数に渡して実行してみる
- sub(func1);
- // Invoke()に渡す。完了復帰なので、123 が返る
- object obj = Dispatcher.Invoke(func1, null);
- Debug.WriteLine("Invoke "+obj);
- // BeginInvoke()に渡す。これは、x が 456で実行される
- Dispatcher.BeginInvoke(func1, null);
- // Auto変数 x を書き換える
- x = 456;
- }
- // ボタンが押された
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- // Grid_Loaded()から復帰した後でしかボタンは押せないので
- // この結果は、常に456となる
- Debug.WriteLine("Button "+func1());
- }
実行したときの出力は以下のようになります。
- ! 123
- sub 123
- ! 123
- Invoke 123
- ! 456
- ! 456
- Button 456
- Dispatcher.BeginInvoke() を呼び出した時点では、x=123でしたが、ラムダ式で記述したfunc1が呼び出された時には x=456 でした。
したがって、xはラムダ式の実行時に参照されると考えることができ、ラムダ式とセットで関数に渡されるようには見えません。
- Dispatcher.BeginInvoke() とスレッドを使った場合は、参照のタイミングに注意が必要。
- それ以外は、明確。
関数に渡した場合と Dispatcher.Invoke() は、完了復帰なので、呼び出し時点の値が使われる。
イベント処理関数は、排他的にしか呼び出されないので、Grid_Loaded()の復帰後にしか、Button_Click()は呼び出されない。
インスタンスとスタティックと
通常、スタティック関数(メソッド)からインスタンス変数や関数を直接アクセスするようには記述できません。
必要な場合は、スタティック関数に this を渡します。
同じことが、デリゲート(ラムダ式)で可能そうです。
VisualStudioのウイザードが作るMainWindow.csにLoadedイベントを追加し試してみます。
- int value1 = 222;
- // 普通の関数
- void instance_func()
- {
- Debug.WriteLine("instance_func " + value1);
- value1++;
- }
- // 静的関数1
- static void static_func1(MainWindow mw)
- {
- Debug.WriteLine("static_func1 " + mw.value1);
- mw.value1++;
- }
- // 静的関数2
- static void static_func2(Func<int, int> f)
- {
- Debug.WriteLine("static_func2 " + f(1));
- }
- // GridのLoadedイベント
- private void Grid_Loaded(object sender, RoutedEventArgs e)
- {
- Debug.WriteLine("! " + value1);
- instance_func();
- Debug.WriteLine("! " + value1);
- static_func1(this);
- Debug.WriteLine("! " + value1);
- static_func2((Func<int, int>)((x) => {
- int y = value1; value1 += x; return y; }));
- Debug.WriteLine("! " + value1);
- }
この実行結果は以下の通りです。
- ! 222
- instance_func 222
- ! 223
- static_func1 223
- ! 224
- static_func2 224
- ! 225
|