新人エンジニアが初めてC#のデリゲートを使った時の話

こんにちは。弥生の木下です。この記事は Misoca+弥生 Advent Calendar 2019 15日目の記事です。

私は2019年に入社した新人エンジニアです。研修中は主にJavaを使って開発を行っており、現在のチームに配属されてから初めてC#を使い始めました。

今回は、C#を学習していく中で行き詰った「デリゲート」について、どこの理解が困難だったか、それをどのようにして理解したかをまとめてみようと思います。

デリゲートって何に使うの?何が便利なの?

私はデリゲートを学習した際に、文献で見たサンプルコードは次のようなものでした。

using System;

delegate void SampleDelegate();

class sample
{
    static void SayHello()
    {
        Console.WriteLine("こんにちは");
    }

    public static void Main()
    {
        //デリゲートの生成
        SampleDelegate sd = new SampleDelegate(SayHello);
        //デリゲートを通してSayHello関数を呼び出す
        sd();
    }
}

このコードを見て、私は「これ、デリゲートを使う意味ある?」「何がしたいの?」といったことを思いました。今見ても、この例ではデリゲートの良さはわからないなと思います(笑)

この時点で、デリゲートは「関数を何かのオブジェクトにする」というところまではわかっていました。しかし、それを何に使うのか、何のために行っているのかが全く理解できませんでした。

トレーナーからヒントをもらった

文献のソースコードを見ても、書き方はわかったもののやはり目的がわからなく、何が便利なのかさっぱりだったので、後日トレーナーに相談しました。すると、次のようなコードを考えてみようと、例を提示してくれました。

using System;
using System.Collections.Generic;

class Person
{
    public int _id;
    public string _name;
    public string _address;
}

class Program
{
    static void Main(string[] args)
    {
        List<Person> personList = new List<Person>();
        //personListをソートする関数を作る(ID順、名前順、住所順)
    }
}

これを見て私が一番最初に書いたコードは次のように、ID、名前、住所それぞれでソートする関数を別々に作成してしまいました。

//IDでのソート
public static List<Person> SortById(List<Person> personList)
{
    for (int i = 0; i < personList.Count - 1; i++)
    {
        Person min = personList[i];
        int k = i;

        for (int j = i + 1; j < personList.Count; j++)
        {
            //比較
            if (personList[j]._id < min._id)
            {
                min = personList[j];
                k = j;
            }
        }
        Person temp = personList[i];
        personList[i] = personList[k];
        personList[k] = temp;
    }
    return personList;
}

//Nameでのソート
public static List<Person> SortByName(List<Person> personList)
{
    for (int i = 0; i < personList.Count - 1; i++)
    {
        Person min = personList[i];
        int k = i;

        for (int j = i + 1; j < personList.Count; j++)
        {
            //比較
            if (string.Compare(personList[j]._name, min._name, true) < 0)
            {
                min = personList[j];
                k = j;
            }
        }
        Person temp = personList[i];
        personList[i] = personList[k];
        personList[k] = temp;
    }
    return personList;
}

//Addressでのソート
public static List<Person> SortByAddress(List<Person> personList)
{
    for (int i = 0; i < personList.Count - 1; i++)
    {
        Person min = personList[i];
        int k = i;

        for (int j = i + 1; j < personList.Count; j++)
        {
            //比較
            if (string.Compare(personList[j]._address, min._address, true) < 0)
            {
                min = personList[j];
                k = j;
            }
        }
        Person temp = personList[i];
        personList[i] = personList[k];
        personList[k] = temp;
    }
    return personList;
}

これをトレーナーに見せたところ、「この3つの関数は共通化できるね。」と言われました。つまり、1つ「ソートをする」という関数を作って、何でソートするかをメインメソッドで指定するような関数を作れば、ソートの処理を1つの関数で共通化できるのです。

「共通化」を目標にコードを書いてみた

3つのソート関数で、「比較する対象と比較の方法」以外は全く同じ処理を書いていることには気が付いたので、比較の部分のみをメソッドに切り出すところまではできました。

デリゲートを使わなかったら詰んだ

今までの私の知識で、共通化したソート関数は次のようにif文で分岐したコードになりました。

//指定した条件でのソート
public static List<Person> Sort(List<Person> personList, string sortSign)
{
    for (int i = 0; i < personList.Count - 1; i++)
    {
        Person min = personList[i];
        int k = i;
        bool isLower = false;

        for (int j = i + 1; j < personList.Count; j++)
        {
            //sortSignの値によって比較方法を変える
            if (sortSign == "id")
            {
                if (CompareId(personList[j], min))
                {
                    min = personList[j];
                    k = j;
                }
            }
            else if (sortSign == "name")
            {
                if (CompareName(personList[j], min))
                {
                    min = personList[j];
                    k = j;
                }
            }
            else if (sortSign == "address")
            {
                if (CompareAddress(personList[j], min))
                {
                    min = personList[j];
                    k = j;
                }

            }
        }
        Person temp = personList[i];
        personList[i] = personList[k];
        personList[k] = temp;
    }
    return personList;
}

//比較する関数の定義
public static bool CompareId(Person person1, Person person2)
{
    return person1._id < person2._id;
}

public static bool CompareName(Person person1, Person person2)
{
    return string.Compare(person1._name, person2._name, true) < 0;
}

public static bool CompareAddress(Person person1, Person person2)
{
    return string.Compare(person1._address, person2._address, true) < 0;
}

この書き方はまず条件分岐が多く、長くて見にくい。そして最大の問題は、もし新たな順序のソートを追加する場合、さらに条件分岐を追記していかなくてはなりません。さらに関数内のソースコードが長くなります。今までの知識だと、ここで「詰み」なのです。

デリゲートを使ったら書けた

そこで、「デリゲートを使って書いてみよう。」というトレーナーのヒントから、コードを書いてみました。

using System;
using System.Collections.Generic;

delegate bool MyDelegate(Person person1, Person person2);

class sample
{
    //指定した条件でのソート
    public static List<Person> Sort(List<Person> personList, MyDelegate md)
    {
        for (int i = 0; i < personList.Count - 1; i++)
        {
            Person min = personList[i];
            int k = i;

            for (int j = i + 1; j < personList.Count; j++)
            {
                if (md(personList[j], min))
                {
                    min = personList[j];
                    k = j;
                }
            }
            Person temp = personList[i];
            personList[i] = personList[k];
            personList[k] = temp;
        }
        return personList;
    }
}

これなら、もし新たな順序のソートを追加する場合、デリゲートに格納する比較関数を変えるだけで対応でき、ソート関数の中身は一切いじらずに対応できます。

デリゲートを使うことによって、うまくソート関数を共通化することができました!

ここまでわかってようやくデリゲートを使う意味、その価値がわかりました。

まとめ

私がデリゲートとは何なのか、何のために使うものなのかを理解できたきっかけは、「共通化」という言葉でした。もっとも、デリゲートの目的の一つが共通化なので当然ですね(笑)

今後ソースコードを書く上で、「共通化」は非常に大切であることを実感することができました。今後もより良いソースコードを書けるよう、継続的に学習を重ねていきたいと思っています!

最後に

Misoca+弥生 Advent Calendar 2019 の16日目は、higo_yayoiさんです。 お楽しみに!