新人エンジニアが初めて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さんです。 お楽しみに!