多分わかりやすいC#のDelegateとラムダ式入門

こんにちは、サイオステクノロジー技術部 武井です。

何番煎じくらいになるのかはわかりませんが、C#のDelegateとラムダ式について、書こうかと思います。Delegateやラムダ式の概念ってわかりにくいですよね。私が理解に至ったプロセスを本記事に残したいと思います。ほとんど、私の備忘録に近いものがありますが、誰かの役に立てればと、、、。

Delegateの概要

delegateとはひとことで言ってしまえば、

関数を代入できる変数

でございます。

早速コードを説明したいのですが、その前にまずDelegate型の変数を宣言する方法をご説明します。先程、Delegateは関数を代入する変数とお話しました。というとことは、関数には「戻りと」と「引数」が必要です。まずこれを以下のような形式で宣言します。

delegate 戻り値の型 デリゲート型名(引数リスト);

例えば、標準出力にHelloと表示するだけのような、戻り値なし、引数無しのSayHelloというデリゲート型名を宣言するには以下のように記述します。

delegate void SayHello();

さらに、具体例を挙げますと、例えば、名前を引数に入れると、「Hello!XXXさん」という関数を考えるとします。これを先程のように宣言しますと以下のようになります。

delegate string SayHello(string name);

Delegateの説明はここまでにして、実際のコードで以下のことを実現したいと思います。

hogeという文字列を標準出力に表示する関数をHogeという変数に代入して実行する

それが以下のコードになります。

class Program
{
    // まず、delegate型の変数を宣言します。
    // hogeという文字列を標準出力に表示するので、戻り値もなし(void)で、
    // Delegateの引数もありません
    public delegate void Delegate();  // (1)

    static void Main(string[] args)
    {   
        // hogeという変数を(1)で定義したDelegate型で定義します。
        // そして、このhogeに、(2)で定義したHogeという関数を代入します。
        // これがDelegateです。
        Delegate hoge = Hoge;

        // 先程の変数を以下のように実行します。
        // 変数を実行するというのも変ですが、この変数にはHogeという
        // 関数が代入されているので、Hogeという関数が実行されます。
        hoge();

        Console.ReadLine();
    }   

    static void Hoge() { // (2) 
        Console.WriteLine("hoge");
    }   
}

以上が、Delegateの概要でした。

引数あり戻り値ありのDelegate

引数と戻り値のあるDelegateについても考えたいと思います。以下の要件を実現するコードを考えてみたいと思います。

関数の引数に指定した文字列の長さを戻り値として返す

これを実現するコードは以下になります。

class Program
{
    // 先ほどと同様Delegate型の変数を宣言します。関数の引数には、
    // 長さをカウントしたい対象の文字列、戻り値には文字列の長さを返しますので、
    // 引数はstring、戻り値はint型にします。なので、定義は以下のようになります。
    public delegate int Delegate(string s); // (1)

    static void Main(string[] args)
    {   
        // (1)で定義したDelegate型の変数countStringを定義します。
        // そして、変数countStirngに、(2)で定義した関数countStringを代入します。
        Delegate countString = CountString;

        // 変数countStringを実行すると、関数CountStringが実行されて、
        // hogeの文字列の長さが表示されます。
        Console.WriteLine(countString("hoge"));

        Console.ReadLine();
    }   

    static int CountString(string s) // (2)
    {   
        return s.Length;
    } 
}

引数なし戻り値なしのDelegateとそんなに大差ないですね。

Delegateとコールバック関数

では、どんなときにDelegteって使うのでしょうか?その主な利用例の一つとして、コールバック関数があります。コールバック関数とは、何かの処理が終わったあとに、呼び出したい処理のことで、非同期処理の終了通知なんかに使われることがあります。

ここでは、「Hello!!」という文字列を標準出力に表示したあとに、何からの処理を行うコールバック関数をDelegateとして定義して、外側から与えてみることを考えます。下記の例は、「Hello!!」と標準出力に出力する関数のコールバック関数として「How are you?」を標準出力に出力する関数を定義したいと思います。要するに下記のプログラムを実行すると「Hello!!How are you?」と標準出力に出力されるわけです。

class Program
{
    // コールバック関数のDelegate型を定義します。
    // ここでは引数なし戻り値なしのDelegate型を定義しています。
    public delegate void Callback(); // (1)

    static void Main(string[] args)
    {
        // (1)で定義したDelegate型の変数sayHowAreYouを定義します。
        // そこに、(2)で定義したSayHowAreYou関数を入れます。
        Callback sayHowAreYou = SayHowAreYou;

        // コールバック関数を代入したDelegate型の変数を
        // SaySomethingAfterHello関数((3)で定義)の引数にして実行します。
        SaySomethingAfterHello(sayHowAreYou);

        Console.ReadLine();
    }

    // コールバック関数です。(3)で定義したSaySomethingAfterHello関数の
    // 最後に実施します。
    static void SayHowAreYou() // (2)
    {
        Console.WriteLine("How are you?");
    }

    // コールバック関数を実施する関数です。Hello!!と標準出力に出力したあとに、
    // この関数の引数に指定されたコールバック関数を実行します。
    static void SaySomethingAfterHello(Callback callback) // (3)
    {
        Console.WriteLine("Hello!!");
        callback();
    }
}

いかがでしょうか?「Hello!!」と標準出力に出力したあとの処理を、Delegate型の変数を使うことによって外部から注入ができています。このDelegate型の変数を違うものにすれば、「Hello!!」と標準出力に出力したあとの処理を簡単に違うものに変更できますね。

ただ、これは例としてはちょっとイマイチなので、もうちょっと実用的というか、実際のプログラムであり得る例を挙げてみたいと思います。

以下のプログラムは、あるURLにHTTPリクエストしたあとに、そのHTTPレスポンスに対して、任意の処理を行うものです。この「任意の処理」をDelegateで外部から注入します。今回はHTTPのスターテスコードを返すこととします。

class Program
{
    // HTTPレスポンスを処理する関数を定義するDegate型を作ります。
    // もちろん引数にはHTTPレスポンスを表すHttpResponseMessageを定義します。
    public delegate void Callback(HttpResponseMessage res); // (1)

    static void Main(string[] args)
    {   
        // (1)で定義したCallback型のDelegateである変数callbackを定義します
        // 変数callbackには、(2)で定義した関数GetStatusCodeを代入します。
        Callback callback = GetStatusCode;

        // (3)で定義した関数を呼び出しています。第1引数に指定したURLアクセスに
        // 対するHTTPレスポンスを、第2引数に指定したコールバック関数で処理します。
        HttpRequest("https://www.yahoo.co.jp/",callback);

        Console.ReadLine();
    }   

    // HTTPレスポンスを処理するためのコールバック関数です。
    // ここでは、HTTPレスポンスのステータスコードを標準出力に出力してます。 
    static void GetStatusCode(HttpResponseMessage res) // (2)
    {   
        Console.WriteLine(res.StatusCode.ToString());
    }   

    // この関数は、第1引数に指定したURLにアクセスした際のHTTPレスポンスに対して、
    // 第2引数で指定したCallback型のDelegateで処理するものです。
    async static void HttpRequest(String url,Callback callback) // (3)
    {   
        using (HttpClient httpsClient = new HttpClient())
        {   
            // HttpClientクラスを使って、第1引数に指定したURLにアクセスして、
            // そのHTTPレスポンスを取得します。
            HttpResponseMessage res = await httpsClient.GetAsync(url);

            // 第2引数で指定したCallback型のDelegateで
            // 先程取得したHTTPレスポンスを処理します。
            callback(res);
        }   
    }   
}

いかがでしょうか?コメント文に概ね解説は書いたので、ご理解頂けたかと思います。こんなふうにしてDelegateを使うと、便利ですね。上記では、HTTPレスポンスに対する処理は、GetStatusCodeで定義した関数をDelegate型の変数callbackに入れていますが、HTTPレスポンスに対して違う処理、例えばContent-Lengthを取得したい場合は、また別の関数を定義して、callback変数に入れ直せばいいだけです。

Action型変数の利用

わざわざDelegate定義するのめんどくさい場合は、Action型の変数を利用することもできます。

class Program
{
    static void Main(string[] args)
    {   
        HttpRequest("https://www.yahoo.co.jp/", GetStatusCode);

        Console.ReadLine();
    }   

    static void GetStatusCode(HttpResponseMessage res)
    {   
        Console.WriteLine(res.StatusCode.ToString());
    }   

    async static void HttpRequest(String url, Action<HttpResponseMessage> callback) // (1)
    {   
        using (HttpClient httpsClient = new HttpClient())
        {   
            HttpResponseMessage res = await httpsClient.GetAsync(url);
            callback(res);
        }   
    }   
}

このソースコードが、先程のDelegateを使ったソースコードと違う点は以下の2つです。

  • Delegate型の変数の定義がなくなっていること
  • そのかわりAction型の変数を定義していること

特に2番目を見てみたいと思います。Delegateを使った場合と、Actionを使った場合で以下のようにソースコードが変わっています。

async static void HttpRequest(String url,Callback callback)
        ↓
async static void HttpRquest(String url, Action<HttpResponseMessage> callback)

Delegateをわざわざ使わなくても、Action型(もしくはFunc型)を使えば、Delegateの代用ができます。引数や戻り値によって、それぞれAction、Funcを使い分ける必要があります。

■引数なし、戻り値なしの場合
Delegateでは以下のように表しますが、、、

delegate void Hoge();

Actionでは以下のように表します。

Action hoge;

■引数あり、戻り値なしの場合
Delegateでは以下のように表しますが、、、

delegate void Hoge(string fuga);

Actionでは以下のように表します。

Action<string> hoge;

■引数なし、戻り値ありの場合
Delegateでは以下のように表しますが、、、

delegate int Hoge();

Funcでは以下のように表します。

Func<int> hoge;

■引数あり、戻り値ありの場合
Delegateでは以下のように表しますが、、、

delegate int Hoge(string fuga);

Funcでは以下のように表します。

Func<int,string> hoge;

それでもいまいち見にくい、、、

delegateをActionに置き換えて、ちょっとだけソースコードが見やすくなりました。でも、まだまだ見にくいと思いませんか?

例えば、以下のソースコードを例に挙げます(先程のソースコードと同じですが、、、)。

class Program
{
    static void Main(string[] args)
    {   
        HttpRequest("https://www.yahoo.co.jp/", GetStatusCode); // (1)

        Console.ReadLine();
    }   

    static void GetStatusCode(HttpResponseMessage res) // (2)
    {   
        Console.WriteLine(res.StatusCode.ToString());
    }   

    async static void HttpRequest(String url, Action&amp;lt;HttpResponseMessage&amp;gt; callback) // (3)
    {   
        using (HttpClient httpsClient = new HttpClient())
        {   
            HttpResponseMessage res = await httpsClient.GetAsync(url);
            callback(res);
        }   
    }   
}

この場合、ソースコードを見る順番は、以下のようになります。

  1. Mainの関数の(1)に書いてあるHttpRequest関数を見る。
  2. (3)に行ってHttpRequest関数の定義を見る。
  3. また(1)に戻って、今度は、HttpRequest関数の第2引数であるGetStatusCode関数の処理((2)の関数)を見る。

あっち行ったりこっち行ったりめんどくさいです。特に3がめんどくさいですね。

そこでラムダ式の登場です。

ラムダ式

ラムダ式を使うと、先程のコードをもっと見やすくすることができます。まず、手っ取り早く、ラムダ式を使って、先程のコードを書き換えてしまいます。

class Program
{
    static void Main(string[] args)
    {   

        HttpRquest("https://www.yahoo.co.jp/", (res) =&amp;gt; {
            Console.WriteLine(res.StatusCode.ToString());
        }); 

        Console.ReadLine();
    }   

    async static void HttpRquest(String url, Action&amp;lt;HttpResponseMessage&amp;gt; callback)
    {   

        using (HttpClient httpsClient = new HttpClient())
        {   
            HttpResponseMessage res = await httpsClient.GetAsync(url);
            callback(res);
        }   
    }   
}

ラムダ式を使わない場合のソースコードと、ラムダ式を使った場合の変更点は以下のとおりです。

  • GetStatusCode関数がなくなっていること
  • そのかわりHttpRquestの第2引数にラムダ式が使われていること

2番目に着目したいと思います。

HttpRequest("https://www.yahoo.co.jp/", GetStatusCode);
           ↓
HttpRquest("https://www.yahoo.co.jp/", (res) => {
    Console.WriteLine(res.StatusCode.ToString());
});

GetStatusCode関数がなくなったので、HttpRequest関数の第2引数にラムダ式を指定しています。

ラムダ式とは無名関数とも言われます。今回みたいに、HTTPレスポンスを取得するためだけの処理のためにわざわざGetStatusCode関数を定義するのもめんどくさいです。

なので、そのかわりに、わざわざ関数名を定義しないで、その関数の引数と、処理や戻り値を名無しの関数で定義してしまえというのが無名関数つまりラムダ式になります。

ラムダ式の書式は以下になります。

(引数) => {
    処理の内容
}

例えば以下のような関数があったとします。

int Hoge(string s) {
    return s.Length;
}

これをラムダ式で表すと、以下のようになります。

(s) => {
    return s.Length;
}

なので、以下のようなGetStatusCode関数は、、、

static void GetStatusCode(HttpResponseMessage res)
{   
    Console.WriteLine(res.StatusCode.ToString());
} 

以下のようなラムダ式で置き換えることができます。

(res) => {
    Console.WriteLine(res.StatusCode.ToString());
}

もちろんHttpRquestの第2引数に書くことのできるラムダ式はどんな形式のものでも書けるわけではありません。それは、HttpRquest関数の第2引数のところのActionで以下のように定義されています。

async static void HttpRquest(String url, Action<HttpResponseMessage> callback)
{    
    using (HttpClient httpsClient = new HttpClient())
    {   
        HttpResponseMessage res = await httpsClient.GetAsync(url);
        callback(res);
    }   
} 

Action<HttpResponseMessage>と定義されているので、引数にはHttpResponseMessage型のもの、戻り値はなしというラムダ式しか定義ができません。

まとめ

いかがでしたでしょうか?Delegateから始まり、Action、そしてラムダ式を使うことによって、どんどんコードを省略可していきました。Delegateやラムダ式の存在意義がご理解頂ければ幸いです。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

23人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です