LoginSignup
58
33

コンマ演算子の使い道の話

Last updated at Posted at 2024-03-09

これは何?

return の後ろにカンマ演算子で区切ったステートメントを記述し、最右のステートメントを返すことに有用性があるかどうか知りたいのです。 という記述を読み、そこに限定せずに コンマ演算子が役に立つパターンについて考えた。

とはいえ、 return 式, 式; が念頭にある。

そもそも

そもそも、コンマ演算子は C 言語に由来する。
と言いたいところだけど、よく知らない。BCPL には無かったようなので、B言語か C言語なのかな。

いずれにせよ、C言語がきっかけで広まったと思う。

可読性の低下に寄与

コンマは演算子以外の用途でも使われるのでコンマ演算子なのかそうじゃないのかを判断するのがめんどくさく、可読性の低下に寄与しやすい。

JavaScript
a=(b,c); // コンマ演算子
a=f(b,c); // 引数区切り
a=g((b,c)); // コンマ演算子
a=typeof(b,c); // コンマ演算子
a=(x,y)=>x; // 引数区切り
a=(x,y)<=x; // コンマ演算子
a=[4,5][0,1]; // 4,5 のコンマは配列の要素区切り。[0,1]のコンマはコンマ演算子
{a:1,b:1} // オブジェクトの要素区切り
{a:1,b;1} // コンマ演算子(a はラベルで、`{`〜`}` はコードブロック)

コンマ演算子が使える言語

Java や C# にはコンマ演算子がない(C# についてはこの記事のための調査で知った)。

Ruby・Python にもない。

じゃあ使える言語はどれなのかというと。

  • C / C++ / Objective-C
  • JavaScript / TypeScript
  • Perl
  • 昔の PHP(昔の言語仕様の文書らしきもの では演算子となっていたけど、今の文書では演算子の中に入っていない)
  • D言語

あたり。他にあるかな。

C++ の場合の注意点

C++ の場合、演算子のオーバーロードでコンマ演算子に別の意味をもたせることが可能となっている。

これが、他の演算子よりもトラブルの原因となりやすい。

a(),b() は、コンマ演算子がオーバーロードされていない場合、 a()b() より前に評価されることが文法上確定するが、コンマ演算子がオーバーロードされている場合、どっちでも良くなり、 b() が先に評価されるかもしれない。

ということで、特段の事情がない限りコンマ演算子のオーバーロードはおすすめしない。

主要な用途

主要な用途は for(初期化節;第二の式;第三の式) の、第三の式での利用ということで間違いない。

ここには式をひとつしか書けない。

ループ 1回で ij を両方インクリメントしたい、みたいなときにコンマ演算子が登場し、 ++i,++j のように、複数の式をひとつの式にまとめるために使う。

このためだけに発明されたんではないかと想像している(想像に過ぎない。何も調べていない)。

Java と C# の場合

JavaScript/C/C++ は for 文の第三の式に書けるものは「式」なんだけど、Java・C# の場合はそうではない。

Java・C# の場合、for 文の最後に書けるものは「式」ではなく「StatementExpression をコンマで区切ったもの」なので、Java・C# の for(;;++i,++j) のコンマはコンマ演算子ではない。

ちなみに StatementExpression は

  • 代入文
  • ++ij-- の類
  • メソッド呼び出し
  • インスタンス生成式
  • await 式(C# のみ)

のいずれかで、条件演算子は含まれない。ということで Java・C# は JavaScript/C/C++ と違って

Java or C#
for(int i=foo() ; i!=0 ; i<0 ? ++i : --i){} // エラー

は文法エラーになる。 上記の計算を書きたかったら下記のように

Java or C#
for(int i=foo() ; i!=0 ; i+=(i<0 ? 1 : -1)){}

すればよい。 i+=式 は代入文なので許される。

go の場合

go の場合、

そもそも i++ は式ではないのでコンマ演算子で繋げられるはずもなく、i++,j++ と書けたとしてもそのコンマは演算子ではないわけだけど Java や C# が行っているような文法による救済がないので

go
for i := 0; i < 10; i++, j-- {}

はエラーになる。
for InitStmt ; Condition ; PostStmt {}
とした場合の PostStmt に書いていいのは

  • i++ とか j-- のたぐい
  • 代入文

のいずれか。

PostStmt の場所で i++j-- の両方の計算を行いたければ

go
for i := 0; i < 10; i, j = i+1, j-1 {}

上記のように ij を二回ずつ書くという苦しみに耐えて多重代入するか (ちなみに i, j += 1, 1 は文法エラー)、

下記のようにそのためだけに匿名関数を定義して即座に呼び出す

go
for i := 0; i < 10; func() { i++; j-- }() {}

のが普通なんだと思う。

for 文の第三の式以外の用途

{ } の省略

C or JavaScript
if (cond){ a=f(); b=a*a }

C or JavaScript
if (cond)a=f(),b=a*a

と書くことができる。この用途だとコンマ演算子式内で変数宣言ができないのであまり使いたいとは思わないし、そもそも { } を省略したいとも思わない。

※ コーディングゴルフという遊びだと必須のノウハウとなる。

この用途での利用は、ゴルファー以外にはおすすめしない。

条件演算子との組み合わせ

条件演算子と組み合わせた下記の例は

C or JavaScript
foo(a).b = bar(p) ? (x=baz(p),qux(x, x+1)) : corge(i);

コンマ演算子がないとわりと書きにくい。
JavaScript なら x のスコープを狭くしたいので lambda 式にするのが好ましいと思うけど、C 言語でコンマ演算子禁止とすると

C
B * pb = &foo(a).b;
if (bar(p)){
  int x = baz(p);
  *pb = qux(x, x+1);
} else {
  *pb = corge(i);
}

のように、pb を導入する必要があってちょっとだるい(foo(a) を二回書くのはもっとだるい)。

それでも私が C で書くなら if 文のパターンのほうが好きだけど、コンマ演算子版の方が見やすいという意見にもあまり反対はしない。そもそも関数に切り出すとも思うけど、ローカル変数をたくさん参照していたらそれもだるいので悩むと思う。

条件演算子を例に書いたけど、たぶん &&|| でも、コンマ演算子があるとスッキリ書けるパターンはある。

アロー関数式

JavaScript の アロー関数式には
引数 => 式引数 => {文} がある。
コンマ演算子を使うと、前者を使うことができる。

まあ {} の省略と似ているけど、return も省略できる。

つまり

JavaScript
let a = x=>{
  const y = x*x+1;
  return y+1/y;
}

JavaScript
let a = x=>(x=x*x+1,x+1/x);

になり、だいぶ短くなる(ことがある)。x を上書きしてしまうのは、式内で変数宣言ができないから。

x を上書きしてしまうので、最後の式が foo(x,y) のたぐいだとだいぶ苦しい(無理ではない)。

return 式, 式

と。
ここまで return 式, 式 につなげようと思考を巡らせながら書いていたんだけれど、なかなか return 式, 式 と書きたい理由が浮かばない。

C or JavaScript
return A, B;

は必ず

C or JavaScript
A;
return B;

と書き換えることが可能(自信ない)なので、 return 式A, 式B; と書きたくなる理由は思いつかない。

なにかあるかなぁ。

まとめ

  • コンマ演算子は for 文の第三の式で使うが、それ以外のおすすめできる使い道はあまりない
  • Java・C# の場合、for(;;i++,j++) のコンマは演算子ではない
  • return 式, 式; と書きたくなる理由は思いつかなかった
58
33
17

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
33