give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

デリゲート(delegate)ってなんだろう?

C#のデリゲートがよくわからんという同僚さんのために、なんかいいサンプルコードを書きたいと思ったんですが、なかなか難しいですねえ。
分かりやすくて、なおかつ実用的なサンプルが思いつかないです。


おいらの中では「デリゲート = メソッド(処理)をあたかも変数のようにして使いまわせるモノ」みたいな感じの理解です。(かなり適当・・・)


たとえば、スクリプト系の言語だと何でも実行時に解決しちゃうんで、デリゲートなんてものが必要ありません。
JavaScript(というかJScript)で書くとこんな感じです。

main();

function main() {
    var methodContainer = [];
    methodContainer.push(sayHelloInEnglish);
    methodContainer.push(sayHelloInJapanese);
    
    for (var i in methodContainer) {
        var helloMethod = methodContainer[i];
        var name = "John";
        var message = helloMethod(name);
        WScript.Echo(message);
    }
}

function sayHelloInEnglish(name) {
    return "Hello, " + name;
}

function sayHelloInJapanese(name) {
    return "こんにちは、" + name;
}
実行結果:
Hello, John
こんにちは、John


JavaScriptは関数 = オブジェクト(変数)なので、こんな書き方ができちゃいます。

// sayHelloInEnglishは関数
methodContainer.push(sayHelloInEnglish);


「メソッドを変数みたいに扱う」という感覚は、こういうスクリプト系のコードの方が分かりやすいかな〜と個人的には思います。


で、C# 2.0(古い。。。)で書くとこんな感じ。

using System;
using System.Collections.Generic;

public static class Program
{
    private delegate string SayHelloDelegate(string name);

    public static void Main(string[] args)
    {
        IList<SayHelloDelegate> methodContainer = new List<SayHelloDelegate>();
        methodContainer.Add(new SayHelloDelegate(SayHelloInEnglish));
        methodContainer.Add(new SayHelloDelegate(SayHelloInJapanese));

        foreach (SayHelloDelegate helloMethod in methodContainer)
        {
            string name = "John";
            string message = helloMethod(name);
            Console.WriteLine(message);
        }
    }

    private static string SayHelloInEnglish(string name)
    {
        return "Hello, " + name;
    }

    private static string SayHelloInJapanese(string name)
    {
        return "こんにちは、" + name;
    }
}


C#だとコンパイラが色々と事前にチェックしなきゃいけないので、何でもかんでも変数にはできません。
事前に変数化したいメソッドの引数と戻り値(いわゆるシグニチャ)をハッキリさせておかなきゃならない。
なのでJavaScriptでは出てこなかったデリゲートが必要になってきます。


このサンプルコードでは戻り値がstring、引数が1個でstringであることを「SayHelloDelegate」が規定しています。
そして「SayHelloInEnglish」と「SayHelloInJapanese」はそのシグニチャに従っています。

delegate string SayHelloDelegate(string name)

string SayHelloInEnglish(string name)

string SayHelloInJapanese(string name)


さらに、デリゲートが間に入ることでシグニチャが確定するため、コンパイラは以下のコードが実行可能であることを保証できます。

// helloMethodは変数化されたメソッド
// ここでは「SayHelloInEnglish」または「SayHelloInJapanese」のいずれか
string message = helloMethod(name);


ちなみに、無名delegateという機能を使うと「SayHelloInEnglish」や「SayHelloInJapanese」の定義が不要になります。

using System;
using System.Collections.Generic;

public static class Program
{
    private delegate string SayHelloDelegate(string name);

    public static void Main(string[] args)
    {
        IList<SayHelloDelegate> methodContainer = new List<SayHelloDelegate>();
        methodContainer.Add(delegate(string name) { return "Hello, " + name; });
        methodContainer.Add(delegate(string name) { return "こんにちは、" + name; });

        foreach (SayHelloDelegate helloMethod in methodContainer)
        {
            string name = "John";
            string message = helloMethod(name);
            Console.WriteLine(message);
        }
    }
}


少々トリッキーなコードになりますが、その処理を再利用する必要がなかったり、メソッド内のローカル変数を手軽に使い回したいときなんかには結構便利だったりします。



おいらがデリゲートを定義したり、使ったりする時のイメージはこんな感じです。
でもこんな説明でデリゲートへの理解が少しでも深まりますかねえ?
自分でもあまり自信がありませんが、何かの参考になれば幸いです。

補足: コマンドパターンで書き換えた場合

ちなみに、このロジックをあえてコマンドパターンで書き換えるとこんな感じになります。

using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main(string[] args)
    {
        IList<SayHelloCommand> methodContainer = new List<SayHelloCommand>();
        methodContainer.Add(new SayHelloInEnglishCommand());
        methodContainer.Add(new SayHelloInJapaneseCommand());

        foreach (SayHelloCommand helloMethod in methodContainer)
        {
            string name = "John";
            string message = helloMethod.SayHello(name);
            Console.WriteLine(message);
        }
    }

    private abstract class SayHelloCommand
    {
        public abstract string SayHello(string name);
    }

    private class SayHelloInEnglishCommand : SayHelloCommand
    {
        public override string SayHello(string name)
        {
            return "Hello, " + name;
        }
    }

    private class SayHelloInJapaneseCommand : SayHelloCommand
    {
        public override string SayHello(string name)
        {
            return "こんにちは、" + name;
        }
    }
}


この場合、抽象クラスではなくインターフェースを使って書き換えることもできます。(だから何ということはないですが。。。)

using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main(string[] args)
    {
        IList<ISayHelloCommand> methodContainer = new List<ISayHelloCommand>();
        methodContainer.Add(new SayHelloInEnglishCommand());
        methodContainer.Add(new SayHelloInJapaneseCommand());

        foreach (ISayHelloCommand helloMethod in methodContainer)
        {
            string name = "John";
            string message = helloMethod.SayHello(name);
            Console.WriteLine(message);
        }
    }

    private interface ISayHelloCommand
    {
        string SayHello(string name);
    }

    private class SayHelloInEnglishCommand : ISayHelloCommand
    {
        public string SayHello(string name)
        {
            return "Hello, " + name;
        }
    }

    private class SayHelloInJapaneseCommand : ISayHelloCommand
    {
        public string SayHello(string name)
        {
            return "こんにちは、" + name;
        }
    }
}


「デリゲートは分からないけど、コマンドパターンなら分かる!」という人にはもしかしたら参考になるかもです。

補足2: JavaScriptのコードをアレンジしてみる

前述した通り、JavaScriptでは「関数 = オブジェクト(変数)」です。
実はこんなコードを書いても動いちゃいます。

var sayHelloInEnglish = function(name) {
    return "Hello, " + name;
};

var sayHelloInJapanese = function(name) {
    return "こんにちは、" + name;
};

var main = function() {
    var methodContainer = [];
    methodContainer.push(sayHelloInEnglish);
    methodContainer.push(sayHelloInJapanese);
    
    for (var i in methodContainer) {
        var helloMethod = methodContainer[i];
        var name = "John";
        var message = helloMethod(name);
        WScript.Echo(message);
    }
};

main();


注目すべきは以下の部分です。
このコードだと関数が変数として扱われているということが明示的につかめるんじゃないでしょうか。

var sayHelloInEnglish = function(name)

var sayHelloInJapanese = function(name)


また、C#の無名delegateと同じようなコードを書くこともできます。

var main = function() {
    var methodContainer = [];
    methodContainer.push(function(name) { return "Hello, " + name; });
    methodContainer.push(function(name) { return "こんにちは、" + name; });
    
    for (var i in methodContainer) {
        var helloMethod = methodContainer[i];
        var name = "John";
        var message = helloMethod(name);
        WScript.Echo(message);
    }
};

main();


JavaScriptはJavaScriptで、興味深い仕様がたくさんあります。
詳しく知りたい方は以下の書籍がオススメです。

JavaScript 第5版

JavaScript 第5版

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

補足3: マルチキャストデリゲートを使ってみる

マルチキャストデリゲートという機能を使うと、こんな感じに書き換えられます。

using System;
using System.Collections.Generic;

public static class Program
{
    private delegate void SayHelloDelegate(string name);

    public static void Main(string[] args)
    {
        SayHelloDelegate methodContainer = null;
        methodContainer += new SayHelloDelegate(SayHelloInEnglish);
        methodContainer += new SayHelloDelegate(SayHelloInJapanese);
        string name = "John";
        methodContainer(name);
    }

    private static void SayHelloInEnglish(string name)
    {
        Console.WriteLine("Hello, " + name);
    }

    private static void SayHelloInJapanese(string name)
    {
        Console.WriteLine("こんにちは、" + name);
    }
}


さらに無名delegateを使うとこんな風になります。

using System;
using System.Collections.Generic;

public static class Program
{
    private delegate void SayHelloDelegate(string name);

    public static void Main(string[] args)
    {
        SayHelloDelegate methodContainer = null;
        methodContainer += delegate(string name) { Console.WriteLine("Hello, " + name); };
        methodContainer += delegate(string name) { Console.WriteLine("こんにちは、" + name); };
        methodContainer("John");
    }
}


さらにC# 3.0では・・・と続くはずなのですが、手軽に確認できる環境がないため、ここで切り上げます。 m(_ _)m
=> 補足5にて書き換えてみました。

補足4: なんとなくRubyで書いてみる

Rubyは全くの初心者ですが、見よう見まねで書いてみました。
単なるRubyへの憧れです、はい。

def say_hello_in_english(name)
  "Hello, " + name
end

def say_hello_in_japanese(name)
  "こんにちは、" + name
end

method_container = []
method_container.push method :say_hello_in_english
method_container.push method :say_hello_in_japanese

method_container.each do |hello_method| 
  name = "John"
  message = hello_method.call name
  puts message
end


無名delegateっぽく書くとこんな感じ?

method_container = []
method_container.push lambda{|name| "hello, " + name}
method_container.push lambda{|name| "こんにちは、" + name}

method_container.each do |hello_method| 
  name = "John"
  message = hello_method.call name
  puts message
end

補足5: C#3.0で書き換えてみる

今度はC# 3.0で書き換えてみました。
ラムダ式ってのを使うとdelegateの宣言がいらなくなくなるみたいですね〜。
ループ処理もRubyみたいな書き方ができるので、ソースがさらに簡潔になります。
会社の環境もさっさと最新の開発環境に移行してもらいたいのですが。う〜ん。。。

using System;
using System.Collections.Generic;

public static class Program
{
    public static void Main(string[] args)
    {
        var methodContainer = new List<Func<string, string>>();
        methodContainer.Add((name) => "Hello, " + name);
        methodContainer.Add((name) => "こんにちは、" + name);

        methodContainer.ForEach((helloMethod) =>
            {
                var name = "John";
                var message = helloMethod(name);
                Console.WriteLine(message);
            });
    }
}


元のロジックを意識しつつ、とことん短く書いてやろうと思うとこんな感じ?
かなりトリッキーで一体何をやりたいのかわからないかもしれませんが。。。 (^ ^;;

using System;
using System.Collections.Generic;

public static class Program
{
   public static void Main(string[] args)
   {
       new List<Func<string, string>>
       {
           ((name) => "Hello, " + name),
           ((name) => "こんにちは、" + name)
       }
       .ForEach((helloMethod) =>
       {
           var name = "John";
           var message = helloMethod(name);
           Console.WriteLine(message);
       });
   }
}

補足6: F#で書き換えてみる

調子に乗ってF#版まで作ってみました。
全くもって関数型脳は習得できていないので、たぶん全然関数型言語らしくないんじゃないかと思います・・・。
あまり参考になさらぬよう。(- -;;

let printMessage helloFunc =
  let name = "John"
  let message = helloFunc name
  printfn "%s" message

List.iter printMessage [(fun name -> "Hello, " + name); (fun name -> "こんにちは、" + name)]


可読性を無視したらこうなりました。ただの変態コードですね。。。

List.iter 
  (fun helloFunc -> printfn "%s" (helloFunc "John"))
  [(fun name -> "Hello, " + name); (fun name -> "こんにちは、" + name)]