はじめに
よく x => x * x とか () => Debug.Log() といったコードを見かけますが、何をしているのか仕組みがさっぱり分からなかったので調べてみました。
結論から言うとこれはラムダ式というものなのですが、ラムダ式の構造を理解するにはデリゲートや匿名メソッドという概念の理解も必要不可欠なので併せてまとめました。
概要
以下の順に説明します。
- デリゲート
- 定義済みデリゲート(Action,Func)
- 匿名メソッド
- ラムダ式
ラムダ式とデリゲートの関係性
ラムダ式はデリゲートという仕組みを取り入れたコードを限界まで簡略化した最終形態です。
従ってラムダ式を理解するためにはデリゲートの理解が必要不可欠です。
まずはデリゲートについて解説し、そこからどのように変形したらラムダ式になるのかを順序立てて説明していきます。
デリゲートとは
デリゲートを使用すると関数を代入できる変数を定義することができます。
デリゲート自体は関数を代入できる変数を定義する際に用いる型のことを指します。
- 関数を変数に代入できると何がいいか?
関数をパラメーターとして参照できるため、デリゲートはコールバック関数を定義するのに最適(これがメインの目的)
パラメーター:関数の引数に使用できるということ
コールバック関数:ある関数を呼び出す時に、引数に指定する別の関数のこと
- 関数の引数に別の関数を使用できると何がいいか?
関数を実行するタイミングで毎回コールバック関数が呼び出されるので、呼び出し時の条件によって「コールバック関数の返り値」=「関数の引数」を変更することができる
デリゲートを実装してみる
ベースとなるコード
今回の実装のベースとなるコードはこちら
public class Deletest : MonoBehaviour
{
void Start()
{
Debug.Log(Sum(1,2)); //コンソールに表示
}
//整数型の2つの引数(a,b)を基に、文字型の返り値を返すメソッド
public string Sum(int a, int b)
{
int answer = a + b;
return ("足し算の結果:" + answer.ToString()); //ToString()で整数型の変数を文字型に変換
}
}
実行結果
整数型(int型)の2つの引数(a,b)を基に、文字型(string型)の返り値を返すメソッドを定義し、コンソールに表示しています。
今回は2つの引数の足し算を行うメソッドSum
を定義しました。
この「整数型の2つの引数を基に、文字型の返り値を返すメソッド」をデリゲートで表現していきます。
デリゲートの導入
public class Deletest : MonoBehaviour
{
//デリゲートに代入する関数の型を定義する
//「整数型の2つの引数を基に、文字型の返り値を返すメソッド」を入れる変数の型 Deleを宣言
+ public delegate string Dele(int x, int y);
void Start()
{
//デリゲート Dele型の変数 method を宣言
//関数Sumの引数の個数・型、返り値の型の3つ(シグネチャという)が定義した型Deleと一致することを確認し代入
+ Dele method = Sum; //Dele method = new Dele(Sum); でも宣言可能
//定義したデリゲートの使用
+ Debug.Log(method(2,3));
}
//整数型の2つの引数(a,b)を基に、文字型の返り値を返すメソッド
public string Sum(int a, int b)
{
int answer = a + b;
return ("足し算の結果:" + answer.ToString()); //ToString()で整数型の変数を文字型に変換
}
}
実行結果
デリケートで定義した変数method
を用いて、代入されている関数Sum
を呼び出して実行することができました!
・・・ここで1つの疑問が浮かびます。デリゲート使わなくてもシンプルに関数Sum
使った方がコード短くて済むじゃん、、、と。
ここで冒頭の内容を思い返してみましょう
関数をパラメーターとして参照できるため、デリゲートはコールバック関数を定義するのに最適(これがメインの目的)
そう、デリケートの真価はコールバック関数を定義するときに発揮します。
ここまで使用したコードは私の頭の整理も兼ねて極力シンプルになるように組んだのでコールバック関数を実装できていません。
ここからコードを丸々作り変えて、デリゲートを用いたコールバック関数を導入したコードを組んでいきたいと思います。
デリゲートを用いたコールバック関数の導入
public class DeleCallBack : MonoBehaviour
{
//デリゲートに代入する関数の型を定義する
//「整数型の2つの引数を基に、整数型の返り値を返すメソッド」を入れる変数の型 Deleを宣言
public delegate int Dele(int x, int y);
int ans;
void Start()
{
//デリゲート Dele型の変数 method を宣言
//関数SumIntの引数の数・型、返り値の型の3つが定義した型Deleと一致することを確認し代入
Dele method = SumInt;
//デリゲートを用いたコールバック関数を導入した関数の実行
Debug.Log(Output(5, method));
}
//整数型の2つの引数(a,b)を基に、整数型の返り値を返すメソッド
public int SumInt(int a, int b)
{
return a + b;
}
//Dele型の変数 method をコールバックする関数OutPut
//SumInt関数を使用して奇数を1+3+5+...とa回足し算する関数
public string Output(int a, Dele deleFunc)
{
for(int i = 0; i < a ; i +=1)
{
//定義したデリゲートの使用
ans += deleFunc(i, i+1);// 1+3+5+7+...
}
return ("奇数を1+3+5+...と" + a.ToString() + "回足した結果:" + ans.ToString());
}
実行結果
①デリケートで定義した変数method
に関数SumInt
を代入し、
②関数OutPut
で変数method
を引数に取ることで
関数SumInt
をコールバックして実行することができました!
具体的に行った計算は1+3+5+7+9=25 ですね。
ちなみに
Output(5, method)
の部分をOutput(5, method(2,3))
とmethod
の引数まで記述するとCS1503エラーが起きます。
なぜかというとOutPut
関数の第2引数にはDele型を使用するように宣言していたのに、method
の引数まで記述してしまうと返り値のint型が渡されてしまうからですね。
私は見事に引っ掛かりました。
定義済みデリゲート (Func,Action)
C#ではデリゲートをより簡単に導入するための仕組みが用意されています。
これまではデリゲートにより定義する型(先ほどまで使用していたDele
に当たる部分)は自分で用意してましたが、実はC#が予め用意しているFunc,Action
という定義済みのデリゲート型を代わりに使うことで簡略化することができます。
先に具体例を示します。
+using System; //Func,Actionを使用するために必要
public class FuncCallBack : MonoBehaviour
{
- //デリゲートに代入する関数の型を定義する
- //「整数型の2つの引数を基に、整数型の返り値を返すメソッド」を入れる変数の型 Deleを宣言
- public delegate int Dele(int x, int y); //変更前
+ //「整数型の2つの引数を基に、整数型の返り値を返すメソッド」を入れる変数methodの宣言
+ public Func<int, int, int> method;
int ans;
void Start()
{
- //デリゲート Dele型の変数 method を宣言
- //関数SumIntの引数の数・型、返り値の型の3つが定義した型Deleと一致することを確認し代入
- Dele method = SumInt;
+ //関数SumIntの引数の数・型、返り値の型の3つが宣言した変数methodと一致することを確認し代入
+ method = SumInt;
//定義したデリゲートの使用
Debug.Log(Output(5, method));
}
//整数型の2つの引数(a,b)を基に、整数型の返り値を返すメソッド
public int SumInt(int a, int b)
{
return a + b;
}
+ //第2引数の型を変更する
public string Output(int a, Func<int, int, int> deleFunc)
{
for(int i = 0; i < a ; i +=1)
{
ans += deleFunc(i, i+1);// 1+3+5+7+...
}
return ("奇数を1+3+5+...と" + a.ToString() + "回足した結果:" + ans.ToString());
}
}
定義済みのデリゲート型Func
を使用することで自分で型を定義する手間が省けましたね。
考えてみれば、同じシグネチャの関数ってそう多くないので、自分でわざわざ型を定義したところで作る変数は結局1,2個だけとかざらにありそうですし、シグネチャが同じだとしても対象の型を探すの面倒だから新しく作っちゃえ!とかもありそうですもんね。理に適っています。
FuncとActionの定義方法について整理します。
- Func< 引数1の型, 引数2の型, ... , 返り値の型>
返り値がある関数に使用する - Action< 引数1の型, 引数2の型, ... >
返り値のない関数(void
)に使用する
匿名メソッド
匿名メソッドを活用するとFunc,Action
とは別のアプローチでコードを簡略化することができます。
匿名メソッドを活用するとデリゲート型の定義の省略に加えて、代入されるメソッドを予め定義する必要がなくなります。
先に具体例を示します。
+using System; //Func,Actionを使用するために必要
public class FuncCallBack : MonoBehaviour
{
- ***********基本構文*******************************************************
- //デリゲートに代入する関数の型を定義する
- //「整数型の2つの引数を基に、整数型の返り値を返すメソッド」を入れる変数の型 Deleを宣言
- public delegate int Dele(int x, int y); //変更前
- **************************************************************************
- ***********Func活用ver****************************************************
- //「整数型の2つの引数を基に、整数型の返り値を返すメソッド」を入れる変数methodの宣言
- public Func<int, int, int> method;
- **************************************************************************
+ ***********匿名メソッド活用ver*********************************************
+ public Func<int,int,int> method = delegate(int a, int b)
+ {
+ return a + b;
+ };
+ **************************************************************************
int ans;
void Start()
{
- ***********基本構文*******************************************************
- //デリゲート Dele型の変数 method を宣言
- //関数SumIntの引数の数・型、返り値の型の3つが定義した型Deleと一致することを確認し代入
- Dele method = SumInt;
- **************************************************************************
- ***********Func活用ver****************************************************
- //関数SumIntの引数の数・型、返り値の型の3つが宣言した変数methodと一致することを確認し代入
- method = SumInt;
- **************************************************************************
//定義したデリゲートの使用
Debug.Log(Output(5, method));
}
- //整数型の2つの引数(a,b)を基に、整数型の返り値を返すメソッド
- public int SumInt(int a, int b)
- {
- return a + b;
- }
public string Output(int a, Func<int, int, int> deleFunc)
{
for(int i = 0; i < a ; i +=1)
{
ans += deleFunc(i, i+1);// 1+3+5+7+...
}
return ("奇数を1+3+5+...と" + a.ToString() + "回足した結果:" + ans.ToString());
}
}
コードが一気にスッキリしましたね。
今までは予め定義が必要だった、デリゲートに代入される関数SumInt
部分の情報がデリゲート定義時の右辺に移ったイメージです。
構文は以下の通り。最後のセミコロン忘れがち。
delegate (引数 a) {引数 aを使った処理};
ラムダ式
ラムダ式はデリゲートを用いたコードを限界まで簡略化した最終形態です。
匿名メソッド構文との比較がわかりやすいです。
匿名メソッド:delegate (引数 a) {引数 aを使った処理};
↓
ラムダ式 :(引数 a) => {引数 aを使った処理};
using System;
public class lambdaTest : MonoBehaviour
{
public Func<int, int, int> method = (int a, int b)=>
{
return a + b;
};
int ans;
void Start()
{
Debug.Log(Output(5, method));
}
//整数型の2つの引数(a,b)を基に、整数型の返り値を返すメソッド
public string Output(int a, Func<int, int, int> deleFunc)
{
for(int i = 0; i < a ; i +=1)
{
ans += deleFunc(i, i+1);// 1+3+5+7+...
}
return ("奇数を1+3+5+...と" + a.ToString() + "回足した結果:" + ans.ToString());
}
}
ラムダ式の省略形
- 引数が一つの場合は()不要
int x => { return x * x }; - 引数の型推論ができる場合は型の記載不要
x => { return x * x }; - {}内の処理が一行で済む場合は{}とreturnが不要
x => x * x; - 引数がない場合は()内を空にすればいい
() => Debug.Log("引数なし");
まとめ
今回はラムダ式の仕組みについてまとめてみました。
ラムダ式の原型であるデリゲートまで遡って調べることでどういう構造で作られているのかをより深く理解することができました。
最後に、ツイッターやってますのでもしよかったらフォローお願いします。
参考文献
↑おすすめ