演算子は1つ、2つ、または3つのオペランドを受け取り、値を生成します:a + b、!done、x ?? fallback。C#の演算子はC、Java、JavaScriptのものに近いですが、いくつかは初めての人を驚かせる動作をします。特に整数の除算と % の符号です。
算術演算子
+、-、*、/、% はすべての数値型に使えます。結果の型はオペランドに従います。2つの int の値からは int、int と double からは double が生成されます。
出力:
22
12
85
3
2
3.4
3.4
3
3.4
整数の除算はゼロの方向に切り捨てます。 17 / 5 は3で、-17 / 5 は-3です。これは非常によくあるバグの原因です。int percent = done / total * 100; は、done / total の時点ですでに0なので、done < total なら常に0になります。先に掛ける(done * 100 / total)か、double として割ります。
整数や decimal をゼロで割ると DivideByZeroException が投げられます。double をゼロで割っても例外は投げられず、無限大になり、0.0 / 0.0 ならNaNになります。
出力:
True
True
False
DivideByZeroException
剰余演算子と負の数
% は除算の余りを返し、その結果は左のオペランドの符号になります。これは、正の除数に対して常に負でない結果を返すPythonや数学の「mod」とは異なります。
出力:
1
-1
1
2
even
62 min 5 s
Thu
1.5
左辺が負になりえて、結果をインデックスに使うときは、常に Mod ヘルパーを使います。days[(today + offset) % 7] は、合計が負だと IndexOutOfRangeException を投げます。奇数の判定にも同じ落とし穴があります。n % 2 == 1 は負の奇数に対して false になるので、n % 2 != 0 で判定します。
代入、複合代入、インクリメント
= は代入です。複合形の +=、-=、*=、/=、%=、&=、|=、^=、<<=、>>= は演算と代入を組み合わせます。++ と -- は1を足したり引いたりし、前置と後置の形があり、返す値が異なります。
出力:
14
start, step
a=5 b=7 i=7
4
byte の例は1つの癖を示しています。byte + int は int になるので level = level + 10; はコンパイルできませんが、複合代入には変数の型に戻す暗黙のキャストが含まれるので level += 10; はコンパイルできます。そのキャストはオーバーフローすると一周します(250 + 10は4になります)。
比較演算子と論理演算子
比較演算子(==、!=、<、>、<=、>=)は bool を返します。文字列では、== は参照ではなくテキストを比較します。論理演算子は !(否定)、&&(かつ)、||(または)、^(排他的論理和)です。
出力:
True
no active user
with ||:
evaluated A
with |:
evaluated A
evaluated B
True
&& と || は短絡評価します。右のオペランドは、結果を変えうる場合にだけ実行されます。だから user != null && user.IsActive は安全です。ブール値に対する1文字の & と | は常に両辺を評価し、条件でそれが望ましいことはまれです。
ビット演算子とシフト演算子
整数型では、&、|、^、~ は個々のビットに作用し、<< と >> はビットをシフトします。フラグ、マスク、ハッシュ、低レベルのプロトコルに使われます。
出力:
3
True
False
101
1024
125
5
-1
名前の付いたフラグの集合には、整数の定数より [Flags] 属性を付けたenumのほうが明確で、マスクより HasFlag のほうが読みやすくなります。C# 11では、負の数でもゼロで埋める符号なしの右シフト >>> が追加されました。
null演算子
null になりうる値を扱うための演算子が3つあります。
出力:
Guest
0
5
a ?? b は、a がnullでなければ a を、nullなら b を返します。a?.Member は、a がnullのとき例外を投げずにnullを返します。C# 8では、a がnullのときだけ b を a に代入する a ??= b が追加されました。これらについてはnull許容型で詳しく扱います。
演算子の優先順位
式に複数の演算子が混ざっている場合、上の行ほど強く結び付きます。同じ行の演算子は左から右にまとまりますが、代入、??、?: は右から左にまとまります。(オペランド自体は常に左から右に評価されます。)
| 分類 | 演算子 |
|---|---|
| 一次式 | x.y、x?.y、f(x)、a[i]、x++、x--、new、typeof、nameof |
| 単項 | +x、-x、!x、~x、++x、--x、(T)x、await |
| 乗除 | *、/、% |
| 加減 | +、- |
| シフト | <<、>>、>>> |
| 関係と型の判定 | <、>、<=、>=、is、as |
| 等値 | ==、!= |
| ビットごとのAND | & |
| ビットごとのXOR | ^ |
| ビットごとのOR | | |
| 条件付きAND | && |
| 条件付きOR | || |
| null合体 | ?? |
| 条件 | c ? a : b |
| 代入とラムダ | =、+=、-=、??=、=>、その他の複合形 |
知っておくべき結果が2つあります。a + b * c は先に掛け算をします。そして x & mask == 0 は先に比較をする(== は & より強く結び付く)ので、x & (mask == 0) という意味になり、整数ではコンパイルできません。(x & mask) == 0 と書きます。かっこにはコストがなく、意図を明確にします。
演算子のオーバーロード
自分で作った型でも、演算子の意味を定義できます。decimal、DateTime、TimeSpan が + や < に対応しているのはこの仕組みです。
出力:
33.49
True
演算子は public static でなければならず、組で必要なものもあります。< を定義するには > が必要で、== には != が必要です。== を定義すると、それに合わせて Equals と GetHashCode をオーバーライドするまで、コンパイラが警告も出します。演算子のオーバーロードは、数値、ベクトル、お金のように意味が明らかな場合に限って使います。
よくある質問
C#の%は何をしますか?
% は剰余演算子で、17 % 5 は2です。結果は左のオペランドの符号になるので、-7 % 3 はPythonのような2ではなく-1です。インデックスを一周させるときのように常に負でない結果が必要なら、((a % n) + n) % n と書きます。double や decimal にも使え、7.5 % 2 は1.5です。
C#で7 / 2が3になるのはなぜですか?
両方のオペランドが整数のとき、/ は整数の除算を行い、小数部分をゼロの方向に切り捨てます。小数の結果を得るには、一方のオペランドを浮動小数点数かdecimalにします。7 / 2.0 は3.5、(double)a / b は割る前に変換し、7m / 2 は decimal の3.5です。
C#の&&と&の違いは何ですか?
&& は条件付きAND(論理積)で、左辺が false なら右辺は評価されません。ブール値に対する & は常に両辺を評価し、整数に対してはビットごとのANDです。条件には && と || を使います。特に user != null && user.IsActive のようなガードは、& だと NullReferenceException を投げてしまいます。
C#のi++と++iの違いは何ですか?
どちらも i に1を足します。式 i++ は古い値に、++i は新しい値に評価されます。単独の行(i++;)なら違いはありません。違いが出るのは int a = i++; のように結果を使う場合だけなので、多くのスタイルガイドはインクリメントを独立した行に書くよう勧めています。
C#の演算子の優先順位はどうなっていますか?
高いものから順に、一次式(x.y、f(x)、a[i]、x++)、単項(!、-、++x、キャスト)、乗除(* / %)、加減(+ -)、シフト、関係(< > <= >= is as)、等値(== !=)、&、^、|、&&、||、??、条件 ?:、そして最後に代入です。迷ったらかっこを付けます。