C#
.NET
Unity3D
Unity

StartCoroutineなどで、引数に渡すメソッド名をうっかり変えそびれるのを防ぐ方法

More than 3 years have passed since last update.


はじめに

これらのメソッドの共通点は何でしょうか?

それは、「メソッドの名前を引数にとる」という点です。

さて、ちょっと前に、【Unity】SendMessage怖い、専用アトリビュート作ったらどうだろう?【結局微妙】という投稿をしました。

SendMessageというメソッドは他のGameObjectや他のコンポーネントのメソッドを呼び出すためのメソッドです。このメソッドは、呼び出したいメソッド名を引数に渡します。このSendMessageには気をつけないといけないことがあります。SendMessageで呼び出すメソッドの名前が変更されたのに、SendMessageに渡す引数名を変更し忘れてしまうと、実行時エラーになってしまうということです。

先の投稿では、これの紹介とそれに対する一応の対処を紹介しました。

さてSendMessage以外にも、同じようにメソッド名を引数にとるメソッドは上記のようにいくつかあります。

これらのメソッドの呼びだしも、「メソッド名を変更した時に、引数のメソッド名も変更し忘れる」という問題が発生してしまうのでしょうか?実はこれらのメソッドに関しては、SendMessageの時は使えなかった方法でそれを防ぐことが可能です。


StartCoroutineメソッドの説明

簡単にUnityのMonoBehaviorクラスのStartCoroutineメソッドを説明します。

1秒をおきに"logging"というデバックログを表示します。

任意のMonoBehaviourのサブクラスにおいて、

private IEnumerator LoopLoggingRoutine ()

{
while (true){
yield return new WaitForSeconds (1.0F);
Debug.Log ("logging");
}
}

というメソッドを定義して,Startの中で,

StartCoroutine RoopLoggingRoutine ());

もしくは,

StartCoroutine ("RoopLoggingRoutine");

とすれば良いです。

前者はIEnumerator型を引数にとるオーバーロード、後者は呼び出したいメソッドの文字列を引数にとるオーバーロードです。


メソッド名の文字列が引数であることの問題点

ちょっと話は変わって。

静的型付け言語の良さの一つは,実行する前にコンパイルエラーで型の矛盾点やおかしい点を見つけられることだと思います。例えば,定義していないメソッドを呼び出そうとしたり,クラス名が間違っていた場合,実行前におかしいということに気づけます。EclipseやInteliJ,MonoDevelopなどのIDEもコンパイルが通らないコードを書いたら,「おい,そこおかしいぞ」って教えてくれます。

メソッド名を変更する,そんな状況を想像してください。

普通だったらIDEのリファクタリング機能を使って,変更したメソッドを呼び出している箇所も一気に変更すると思います。

仮にそうしなくても古いメソッド名のままになっている箇所は,コンパイルエラーになるので,それをIDEが教えてくれます。

しかし,StartCoroutineのようにメソッド名を文字列で引数で渡している場合注意が必要です。

前節StartCoroutineの説明で書いたコードをよく見てください。

RoopLoggingRoutineとなっています。これは正しくは、LoopLoggingRoutineでしょう。(正:Loop、誤:Roop)

RoopLoggingRoutineというメソッドをLoopLoggingRoutineに変更するとしましょう。ここで大切なのは変更する際、StartCoroutine ("RoopLoggingRoutine")という箇所も変更しないといけません。

この箇所はリファクタリング機能では変更されません。自分で変更しなくてはいけません。もし,この箇所の変更を忘れた場合,IDEは何も警告をしてくれませんし,コンパイルも通ります。

このまま変更を忘れてまま,実行すると次のようなエラーが出てしまいます。

Coroutine 'RoopLoggingRoutine' couldn't be started!

UnityEngine.MonoBehaviour:StartCoroutine(String)

さて実は、StartCoroutineなど、呼び出すメソッドがそのクラスで呼び出せるメソッドであれば、コンパイル時にエラーに気づく方法があります。


対処法

ここではSystem.Func<T>クラスを使います。

StopCoroutine ("LoopLoggingRoutine");

string methodName = ((Func<IEnumerator>) LoopLoggingRoutine).Method.Name;

StartCoroutine (methodName);

// 以下でも可
string methodName = new Func<IEnumerator>(LoopLoggingRoutine).Method.Name;
StartCoroutine (methodName);

と変更します。

こう記述しておけばメソッド名を変えた際に上記の箇所も変更されます。もしリファクタリング機能を用いずに,変えた場合もコンパイル時にエラーになったり,MonoDevelopが警告してくれるので,実行時のエラーで焦ることもないです。

using System;も必要です。


なにを使っている?

string methodName = ((Func<IEnumerator>) LoopLoggingRoutine).Method.Name;

上の例は明示的なメソッドグループ変換を用いて、Func型のデリゲートを作成して、そのメソッドの名前を参照しています。

参考 : ECMA-334: 13.6 Method group conversions

string methodName = new Func<IEnumerator>(LoopLoggingRoutine).Method.Name;

上の例はメソッドグループを引数に取るデリゲート生成式でデリゲートを作成しています。そしてそのデリゲートのメソッドの名前を参照しています。

参考 : ECMA-334: 14.5.10.3 Delegate creation expressions


まとめ

メソッド名を引数にとるメソッドがいくつかあります。

引数に渡すメソッド名の文字列を、対象のメソッド名が変わったのに、変え忘れてしまうということがあります。

メソッドグループ変換もしくはデリゲート生成式を使いメソッド名の文字列をつくることで、うっかり変え忘れるということを防ぐことができます。


補足

この記事の内容は、投稿者 @RyotaMurohoshi のはてなブログの過去の投稿、StopCoroutineみたいに引数にメソッド名を文字列で渡すのが嫌。の表現を変えて、「なにを使っている?」の節を追加したものです。


参考


【追記】StartCoroutineは、IEnumeratorを引数にとるのもあるよね?

 StartCoroutineメソッドには、IEnumeratorを引数にとるオーバーロードがあります。この投稿で紹介した、stringを引数にとるものとIEnumeratorを引数にとるものであれば、IEnumeratorを引数にとるほうを使った方がいいと思います。うっかりタイポしたら、コンパイルエラーで気づけますからね。実は私もStartCoroutineは、stringでなく、IEnumeratorを引数の方を原則使っています。

 じゃあ、StartCoroutineのstringを引数にとるオーバーロードって使いどころがないかというとそうではありません。使わないといけない状況や、そちらの方を使った方がいいかもしれない状況があります。

 StartCoroutineで始めたコルーチンを止めたくなることがあります。その場合、StopCoroutineメソッドを使って、コルーチンを止めることが可能です。

 Unity4.5以前は、StopCoroutineメソッドは、string型を引数にとるオーバーロードしかありませんでした。そしてStopCoroutineで止めることができるコルーチンは、string型の引数のオーバーロードのStartCoroutineで始めたコルーチンだけでした。そのため、StopCoroutineでコルーチンを止めたい場合、Unity4.5以前はstring型を引数にとるStartCoroutineメソッドのオーバーロードを使う必要がありました。

 つづいてUnity4.5以降の場合。Unity4.5で引数にIEnumerator型の引数をとるStopCoroutineのオーバーロードが追加されました。これにより、IEnumerator型を引数にとるStartCoroutineで初めたコルーチンも、このIEnumerator型の引数をとるStopCoroutineを使って途中で止めることができるようになりました。しかしこれはこれで便利なのですが、IEnumerator型のインスタンスを、クラス内のフィールドなどに保持しておく必要ができてしまいました。設計上、変数に持たせたくないという場合もあるかもしれません。その場合、stringを引数にとるStartCoroutine・StopCoroutineを使った方がいいかもしれません。

 このように、コルーチンをStopCorotuineで止める必要があるのであれば、stringの方を使わなくてはいけないor使った方がいいかもしれませんね。

 ちなみに、StopAllCoroutinesメソッドを使えば、string引数のStartCoroutineで始めたコルーチンも、IEnumerator引数のStartCoroutineで始めたコルーチンも全て止まります。ですが、止めたいコルーチン以外も止めてしまうので、注意が必要ですね。

@snaka さん

コメントありがとうございます。