W.I.S. Laboratory
menu-bar

LL言語


LL言語の+演算子と比較演算子の挙動が言語ごとに違う

多くのLL言語には、コンパイル型言語と同じように四則演算子や比較演算子が用意されていて、変数やリテラルの比較ができるようになっている。
しかし実際にコードを書いていくにつれ、プログラマーが意識していないところで期待している型同士ではない比較演算が行われるケースが頻発してくる。
また四則演算子のうち「+」演算子は、多くのLL言語において数値の加算のほか文字列結合子としても使われている。
そのため、プログラマーが数値同士の加算を期待してるところで文字列結合になってしまったり、その逆になったりとなかなか一筋縄にいかないことがある。
このときの挙動が言語によって異なり、複数の言語を行ったり来たりしていると徐々に訳が解らなくなってきたので調べてみた。

大抵、「数値型同士」「文字列型同士」「数値型と文字列型」「文字列型と数値型」に分かれた時の比較や加算・結合であることが多いので、この4つに絞って整理してみた。
ただし文字列型はすべて "123" など、数字以外の文字を含まないものとしている。(言語によっては同じ演算子でも、数字のみの文字列と数字以外の文字を含む文字列で挙動が異なるものがあり、すべての組み合わせを調べるのは困難だと判断した。それによく起こるミスは大抵「数値型だと思っていたのに実際は文字列型だった」というパターンが多いと思う)

LL言語にもいろいろあるが、ここでは Perl、PHP、JavaScript、VBS、Python、Ruby、Groovy、SmileBasic、HSP3、PowerShell をあげてみた。

この中で最も間違いが起こりにくいのはPerlだ。
Perlでは演算や比較をする時、どのような型で行うのかをその都度プログラマーが決めればよいという仕様になっている。
Perlには「数値比較演算子」と「文字列比較演算子」、「数値加算子」と「文字列結合子」がそれぞれ用意されていて、これのおかげで「プログラマーが期待していたのとは異なる型同士での演算や比較が行われてしまう」ということが理論上起こらない。
Perlの変数には「型情報」が含まれておらず、「この変数は今数値型だ」とか「ここで文字列型になった」などということは起こり得ない。
Perlでは「$a = 15;」と「$a = "15";」で起こることはまったく同等であり、変数$aに格納されるのはどちらもスカラー型の「15」であって、数値型でも文字列型でもないのだ。

Perl以外のLL言語は大抵「原則として暗黙キャストが行われる動的型付け言語」なので、変数に「型情報」が含まれていて、それが代入や演算のたびにコロコロと変化する。
型付けが「強い」か「弱い」かという差はあれど、静的型付け言語とは違って、一度変数の型を決めたらプログラムが走り終わるまで変わらないという保証は無い。
おまけにPerlのような文字列型専用の比較演算子や結合子が用意されておらず、いわゆる数値用の演算子だけで、数値型にも文字列型にも対応するような実装になっていることがほとんどなので、比較時や演算時の挙動は当然ながら言語ごとの実装依存となる。
そのためいろんな言語を行ったり来たりしながら開発しているとバグの予感しかしなくなってくるのだ。
適当に調べてみたが、ざっと下のような感じだった。

Perl

  • == != < > <= >= : 両辺を数値として比較
  • eq ne : 両辺を文字列として比較
  • lt gt le ge : 両辺の文字コードを比較
  • + : 加算
  • . : 文字列結合

PHP

  • === !== : 型を含めて完全比較
  • == != : 型変換した上で比較
  • < > <= >= : 両辺を数値として比較
  • strcmp(a, b) : 文字列または文字コードで比較(a - b の差分が返るので、同値の場合は 0 となる)
  • + : 両辺を数値として加算
  • . : 両辺を文字列として結合

JavaScript

  • === !== : 型を含めて完全比較
  • == != : 型変換した上で比較
  • < > <= >= : 両辺共文字列型なら文字コードで比較、型が異なる場合は数値として比較
  • + : 両辺数値型なら加算、両辺文字列型なら結合、型が異なる場合は文字列として結合

VBS

  • = <> : 型変換した上で比較
  • < > <= >= : 両辺共文字列型なら文字コードで比較、型が異なる場合は数値として比較
  • + : 両辺共数値型なら加算、両辺文字列型なら結合、型が異なる場合は数値として加算
  • & : 両辺を文字列として結合

Groovy

  • == != : 型を含めて完全比較
  • < > <= >= : 両辺が同じ型ならその型で比較、型が異なる場合はエラー
  • + : 両辺数値型なら加算、両辺文字列型なら結合、型が異なる場合は文字列として結合

HSP3

  • == != < > <= >= : 左辺の型に合わせて比較、ただし文字列での [< > <= >=] 比較はエラー
  • + : 左辺が数値型なら加算、左辺が文字列型なら文字列として結合

PowerShell

  • -eq -ne -lt -gt -le -ge : 左辺の型に合わせて比較、文字列での [-lt -gt -le -ge] は文字コードでの比較
  • + : 左辺が数値型なら加算、左辺が文字列型なら文字列として結合

Python / Ruby / SmileBasic

  • == != : 型を含めて完全比較
  • < > <= >= : 両辺が同じ型ならその型で比較、型が異なる場合はエラー
  • + : 両辺数値型なら加算、両辺文字列型なら結合、型が異なる場合はエラー

まさに言語ごとでバラバラだが、なんとなくバックエンド系よりもフロントエンド系のほうが暗黙キャストが多い気がする。
比較や加算(結合)時に暗黙キャストが行われる言語では、常に両辺の型と挙動についてピリピリしている必要があるが、それが行われない言語でも安心できるものではない。
両辺が数値型だと思っていても、いつの間にか両辺とも文字列型になっていた、というケースだってあるだろうし。
PHPは不等号だと強制的に数値型比較になるので、文字コードで比較したいときはstrcmp関数を使うことになるが、文字列結合子がある点も含めてPerlの影響を強く受けているようで、演算子による挙動がはっきりと分かれている印象を受ける。

PythonとRuby、SmileBasicは「比較や演算(結合)は同型同士のみ許可する」という設計思想がしっかりと見えるし、HSP3とPowerShellは「どんなときでも左辺の型に合わせる」という潔さと統一感がある。
VBSもとりあえず「型が違うときは数値型」と覚えておけばいいし、何より文字列結合子がある。
Groovyは比較時と加算(結合)時で挙動が違うが、異型比較はエラーになってくれるので「異型加算は文字列型」とだけ覚えておけばなんとかなる。
問題はJavaScriptだ。
型が異なるとき、比較は数値型へ、加算(結合)は文字列型へキャストされるので、コードを書く間ずっと意識していないとうっかりミスをしてしまいそうになるし、事実それが頻発する。
MicrosoftがTypeScriptを作った理由が見えた。


[ 戻る ]
saluteweb