• 6
    Like
  • 0
    Comment

三項演算子?:は悪である。異論は認める。1

三項演算子とは何か?

悪である三項演算子(ternary operator)?:というものだけである。それは次のようなものである。

  • 条件演算子(conditional operator)とも言われる。他にもinline if (iif)ternary ifという呼び方がある。
  • 多くのプログラミング言語において?:唯一の三項演算子である。三項演算子?:が存在するプログラミング言語において、他の三項演算子が存在するような言語を私は知らない。「三項演算子」という言葉が参照透過性(referential transparency)2を有することを私は信じている。
  • ?:という二項演算子もあるが、ここでは別物として扱う。3
  • ifを用いている、?だけの演算子である、といった場合はここでは含めていない。

三項演算子は式abcに対して、a ? b : cという形を取る。始めにaを評価してその評価値が、真である時はbを評価してその評価値を返し、偽である時はcを評価してその評価値を返す。何を持って真か偽かは言語によって異なり4、真偽値を返す式以外はaに入れることができない言語5も存在する。静的型付けではbcが返す評価値の型は同じで無ければならない6

一般に代入の右辺に使用することが多い。

C
int x = a ? b : c;

上の文は下のif文を用いた場合と同じである。

C
int x;
if (a)
  x = b;
else
  x = c;

if文は冗長になってしまうため、三項演算子を使った方が良いと多くの人に信じられている。

なぜ、悪か?

理由はただ一つ、わかりにくい、以上だ。

どのようにわかりにくいかを見ていこう。

複雑になるとわかりにくい。

複雑な式で三項演算子を使うと、途端にわかりにくくなる。

C
f(a ? g("x", (x ? 0 : 1) + y): b, c);

それぞれの項がどこになるのかぱっと見わからない。

記号がぶつかるとわかりにくい。

Cでは、リテラルを除けば、?記号を使うのは三項演算子とほぼ使用されていないトライグラフ7のみ、:記号を使うのは三項演算子のみである。そのため三項演算子の記号が他の表現とぶつかることはほぼ無い。しかし、Cを拡張した言語や、さらに発展した言語においては、多くの機能追加に伴い他の意味に使う場合がある。

  • ?: エルビス演算子 GNU拡張C/C++, PHP8
  • ?? null合体演算子 C#, Swift, PHP
  • ?. null条件演算子 C#, Swift
  • :: スコープ解決演算子 C++, PHP, Ruby, C#(名前空間エイリアス修飾子), Java(他とは意味が異なる、演算子の名称を誰か教えてくれ)
  • ?(前値や後置) nullable型宣言 C#, Swift, TypeScript,
  • ?(識別子の一部) Ruby(メソッド名の最後)
  • :(区切り) マップ等におけるキー(名前)の区切り JavaScript, Ruby
  • :(リテラルの一部) Ruby(シンボル)
  • ?(リテラルの一部) Ruby(一文字文字列)9

※ 三項演算子が存在する言語のみあげている。

これらを三項演算子と共に使う場合、とてもわかりにくくなる。

C#
public class Test {
    public bool a { get; set; }
    public static string f() { return "f"; }
    public static string g() { return "g"; }
    public static void Main() {
        var a = new Test();
        a.a = true;
        var b = false;
        var x = a?.a??b?global::Test.f():global::Test.g();
        System.Console.WriteLine(x);
    }
}

また、言語によっては解釈が異なってしまう。

Ruby
def hoge(x = 'no')
  "hoge: #{x}"
end
def hoge?(x = 'no')
  "hogera: #{x}"
end
fuga = 'fuga'
piyo = 'piyo'
puts hoge?fuga:piyo
puts hoge ? fuga : piyo

最後の二行は同じように見えるが、文法上の解釈は異なっているため、出力も異なる。

優先順位を把握していないとわかりにくい。

aが真の時はbと同じか、偽の時はcと同じかを調べたかったとする。

C
bool check = x == a ? b : c;

これはうまくいかない。なぜならほとんどの言語で(x == a) ? b : cと解釈されるからだ。三項演算子は優先順位が低く、それよりも低いのは代入演算子やカンマ演算子ぐらいしか無い。適切な()で囲む必要があるが、余りに多いと、今度は読みにくさが増してしまう。

入れ子はさらにわかりにくい。

C
int x = a0 ? b0 :
        a1 ? b1 :
        a2 ? b2 :
        c;

これはCやJavaScript等の多くの言語では下記と等価である。

C
int x;
if (a0)
  x = b0;
else if (a1)
  x = b1;
else if (a2)
  x = b2;
else
  x = c;

しかし、PHPは三項演算子が左結合であるため下記と等価にである。

PHP
/*
$x = $a0 ? $b0 :
     $a1 ? $b1 :
     $a2 ? $b2 :
     $c;
*/

if ($a0)
  $r0 = $b0;
else 
  $r0 = $a1;
if ($r0)
  $r1 = $b1;
else
  $r1 = $a2;
if ($r1)
  $x = $b2;
else
  $x = $c;

よって()でおかしな組合せを矯正する必要がある。

PHP
$x = $a0 ? $b0 : (
     $a1 ? $b1 : (
     $a2 ? $b2 : (
     $c)));

どうしても入れ子な()を使わないのであれば、こう書くこともできる。

PHP
$x = !$a0 ? !$a1 ? !$a2 ? c : $b2 : $b1 : $b0;

これがぱっと見でわかるような人間はいない。

条件分岐のまた条件分岐という場合もある。

C
int x = a0 ? (a1 ? b1 : c1)
           : (a2 ? b2 : c2);

ifネストの下記と同じになる。

C
int x;
if (a0) {
  if (a1) {
    x = b1;
  } else {
    x = c1;
} else {
  if (a2) {
    x = b2;
  } else {
    x = c2;
}

サンプルのため、それぞれの式部分は小さく整形されているから見やすいが、実際のコードは他の関数呼び出しや式が入ることになる。if文ではさほど変わらないが、三項演算子については適当な改行が必要になった時点で、見やすさは完全に失われる。

どうすれば良いか?

一番は三項演算子が?:でない言語を使えばいい。例えば、Pythonだ。Pythonは?:ではなくif elseを使う。だから、上のような読みにくいコードにはならない。10

Python
x = b if a else c

次点はifがある言語だ。式であるから代入が分散するというif文の問題を回避できる。例えば、Kotlinだ。

Kotlin

val x = if (a) b else c

言語を変えたくない!

君は実に我が儘だな。

退っ引きならない理由で、より優れた言語に変更できない事は多々あることだ。環境の対応言語が限られている(組込系とか)、利用しなければならないライブラリが一部の言語にしか対応していない(Cバインドさえあればほとんどの言語で対応できるけど)、戦略的にある言語のフレームワークを使用する、前任者が作ったプログラムの保守、上司が許可しなかった11プログラマを募集したらJavaerとPHPerしか来なかった12他の言語を覚えたくない13、等々である。

ここまで読んだ人なら気付いたと思う。単純な代入するだけの文であればそれほどわかりにくくない。そう、複雑になる場合が駄目だと言うことだけで、何がなんでも駄目だというわけでは無い。そこで、単純化するために次の規則を守るようにする。

  1. 途中で改行してはならない。1行が80文字(言語によっては120文字)越えた時点で、各項を変数にするか、if文に変更する。
  2. 今後一生PHPに触れることは無いという誓いを立てなければ、入れ子にしてはならない。
  3. ?:の前後に必ずスペースを入れる。
  4. 代入演算子とカンマ演算子以外の演算子と結合する場合は、必ず()で囲む。()が入れ子になる場合は、事前に変数に入れるか、if文に変更する。
  5. ぱっと見で各項がどこからどこまでなのかがわからない場合は、()で囲むか、事前に変数に入れるか、if文に変更する。

つまり、int x = a ? b : c;というオーソドックスな使い方以外はするなと言うことだ。

なお、事前に各項を変数にする場合は、そのままでは必ず評価されてしまうことに注意しなければならない。その場合はif文の方を使うと良いだろう。他にもnull合体演算子や論理演算子などを使った方が良い場合もある。

まとめ

この文章が言いたいのはただ一つ、「魔術は書くな」と言うことだ。複雑な三項演算子は簡単に魔術的コードになる。君がウィザード級のハッカーであって、他人に読んで貰う可能性が全くないコードであれば、好きに魔術を使ったらいい。しかし、誰かに読んで貰うようなコードに魔術を仕込むような人は、ただのわるいプログラマ LV26である。14


  1. コメント募集中。編集リクでもいいよ。 

  2. ここでの「参照透過性」とはクワインが言及した分析哲学での意味での「参照透過性」である。プログラミング言語理論における「参照透過性」ではない。つまり、「参照透過性」は参照透過性を有さない。 

  3. この二項演算子の?:エルビス演算子(elvis operator)と言われている。エルビス・プレスリーの顔を横倒しにしたように見えるからだ。 

  4. ほとんどの言語で、ifの真偽値判定と同じ基準が用いられる。  

  5. 例えばJavaはbooleanを返す式以外は使用できない。 

  6. union型やany型というのもあり得るが。 

  7. トライグラフの考えに基づけば、¥(\x5c)は??/(\x7e)は??-、と書くべきだったのかも知れない。「円記号でエスケープ」とか言っているの日本人だけである。 

  8. 単に第二項が省略された三項演算子という扱いという話もある。a ?: ca ? a : cの意味になる。 

  9. Rubyひどくね? 

  10. 本当にわかりやすくなっているかの検討はしていないことは秘密だ。 

  11. これを回避するには、高度なプレゼン能力が必要になる。 

  12. COBOLERと.NETerではないVB使いの方々はお祈りさせていただきました。 

  13. 君は実に我が儘だな。 

  14. 白魔術は使っても良いかもしれない。例えば、連続する三項演算子の例は、整形されていれば白魔術と言えなくはない。しかし、世の中にはPHPのような魔法防御力がマイナスの言語も存在するため、白魔術であっても大ダメージを受けてしまう場合がある。レベルを上げて(行数増やして)物理で殴る(ベタで書く)方が良い事もあるのだ。