C#のConditionalAttributeの条件をandやorしたい

こんにちは。技術部平山です。

今回は図もサンプルもない地味な小ネタです。C#の言語機能の話なのでUnityに限りませんが、 Unity屋にわかりやすいサンプルになっています。

なお、同じことに関する記事があるので英語で良い方はそちらをご覧ください。

結論

#if !(UNITY_EDITOR || UNITY_STANDALONE)
[Conditional("YOUR_PROJECT_NAME_NEVER_DEFINED_SYMBOL")]
#endif
void DebugLog(){ ... }

みたいな書き方をすると、複数のシンボル(ここではUNITY_EDITORとUNITY_STANDALONE) のandやorでConditionalを使えるよ、というお話です。

これ以降は、こうしたい背景と、これで何故動くのか、のお話といたします。

何の話か

using System.Diagnostics;
[Conditional("DEBUG")]
void DebugLog(){ ...(中身)... }

みたいなのって、皆さん結構やりますよね?Conditional属性。 機種固有機能や、デバグ機能を、呼び出し側をいじらずに消せて便利です。

もしConditional属性がなかったら、この関数を呼ぶ所の数だけ、

#if DEBUG
   DebugLog("BUGだー!!!");
#endif

という具合に#ifでくくる羽目になるわけです。 まあ、もしなかったらなかったで、たぶん、元のDebugLog()を、

void DebugLog()
{
#if DEBUG
     ...(中身)...
#endif
}

という具合にして、DEBUGが定義されてない時に中身を空にしちゃえば、 同じように呼び出し側は放っておけるので、 実はそれでもいいんじゃないかという気はするのですが、 もしかしたら最適化の面で違いがあるのかもしれません。 関数呼出そのものが消えるに留まらず、引数、ここではBUGだー!!! という文字列を用意する負荷も削ってくれるとうれしいですからね(確認はしてませんが)。 もしそうであれば、C/C++でマクロを使うのと同じことができてうれしいわけです。

ANDとORをしたい

さて、便利なConditional属性ですが、 複数のシンボルを組み合わせたいことがあります。

「UNITY_EDITORか、UNITY_STANDALONEが定義されていたら」 というのはよくある例ではないでしょうか。 エディタ、あるいは開発用のPCビルドであれば、 開発専用の機能がONになる、という感じです。 普通に、

#if UNITY_EDITOR || UNITY_STANDALONE
void DebugLog(){ ... }
#endif

と関数を消してしまうことはできますけど、 それだと呼び出し側もいじらないといけなくなります。 それは面倒なので、 関数定義側でどうにか複数シンボルを||や&&でつなぎたくなるわけです。

かといって、こんなことはできません。

#if UNITY_EDITOR || UNITY_STANDALONE
#define USE_DEBUG_LOG
#endif

[Conditional("USE_DEBUG_LOG")]
void DebugLog(){ ... }

動きそうな気がするんですが、ダメです。 なぜなら、Conditional属性というのは、 「条件を満たす時だけ関数が存在する」ではなく、 「条件を満たす時だけ関数を呼ぶ」を意味するからです。

つまり、判定をしているのは呼ぶ側であり、 呼び出し側のコードに問題のシンボルがあるかないかで決まります。

上の例だと、USE_DEBUG_LOGが呼び出し側のコードにも存在していないとダメで、 違うファイルからの呼び出しでは 結果UNITY_EDITORがあろうが、UNITY_STANDALONEがあろうが、 DebugLogは呼ばれてしまいます。「製品でログが出てる!!!」 という事故につながるわけです。

そこで、冒頭の書き方が出てきます。

#if !(UNITY_EDITOR || UNITY_STANDALONE)
[Conditional("YOUR_PROJECT_NAME_NEVER_DEFINED_SYMBOL")]
#endif
void DebugLog(){ ... }

私はこれを初めて見た時に一瞬では意図が理解できなかったのですが、 場合分けしてみるとわかります。

  • UNITY_EDITORもUNITY_STANDALONEなければ、Conditionalが現れる。
    • これらのシンボルは他のソースコードでも見てるので条件が成立し、関数が呼ばれなくなる。
  • UNITY_EDITORかUNITY_STANDALONEがあれば、Conditionalが消える。
    • この関数は常に呼ばれる。

条件を!で反転しないといけないので少しわかりにくいのですが、 ともかくも「UNITY_EDITORかUNITY_STANDALONEがある時だけDebugLogが呼ばれる」 が実現できます。

YOUR_PROJECT_NAME_NEVER_DEFINED_SYMBOL は「絶対に定義されないシンボル」なら何でもかまいません。 私はYOUR_PROJECT_NAMEはプロジェクト名にしていますが、 まあ怖がりすぎかもしれませんね。NEVER_DEFINED_SYMBOL くらいでも十分かもしれません。絶対に、

#define X

と書かない自信があれば、X一文字でもいいわけです。 あまりおすすめしませんけどね。