はじめに
C#でこんな書き方すると後置インクリメントは思っても無い動きをするので何故だろうと思い動作検証する事にしました。
対象コード
後置インクリメント対象の変数を代入先変数に指定するとどうなるか
x = x++;
普通は後置インクリメント対象変数と代入先変数は別物にするのでこういう書き方しないですが、
なんとなくこれでも変数Xはインクリメントされそうな気がしますよね。
TL;DR
以下のコードを実行して実行結果を見てみましょう
int x = 0;
Console.WriteLine($"初期化後のXは{x}です。");
x = x++;
Console.WriteLine($"'X = X++'後のXは{x}です。");
実行結果は以下のようになります。
何と、Xはインクリメントされずに『0』のままという結果になります。
なぜこうなるのか
マイクロソフトのドキュメントにはこのように書かれています。
後置インクリメント(
x++
)または後置デクリメントx--
演算のランタイム処理は、以下のステップからなる
- xが変数に分類された場合
- xが評価されて変数が生成される。
- xの値が保存される。
- 保存されたxの値は、選択された演算子のオペランド型に変換され、この値を引数として演算子が呼び出される。
- 演算子から返された値はXの型に変換され、xを先に評価した場所に保存される。
- 保存されたxの値が演算の結果となる。
今回の例で重要なのは太字の部分になります。
xがまず保存され、その保存された値が代入先の変数に返される。
よって、0が保存されて、その値は代入先に返されるのでxは0のままと言う事になります。
結論
後置インクリメント対象の変数を代入先変数に指定するな!!
この書き方はダメな書き方なので気を付けてください。
修正案
その1
x++;
まぁ、これで良いですよね。
その2
x += 1;
その3
x = ++x;
前置インクリメントはちゃんと動作します
動作検証
今回どうやって動作検証するかですが、 コンパイル結果のIL(.NETの中間言語:Intermediate Language)を見てみる事にします。
SharpLabというサイトを使えば自分で書いたコードをIL簡単にすることができるので今回はそのサイトを利用します。
検証コード
以下のコードをILにしてみます
using System;
public class C {
public void M() {
int x = 0;
x = x++;
}
}
生成されたILの説明
.method public hidebysig
instance void M () cil managed
{
.maxstack 3
.locals init (
[0] int32 x
)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: dup
IL_0005: ldc.i4.1
IL_0006: add
IL_0007: stloc.0
IL_0008: stloc.0
IL_0009: ret
}
IL_####:は説明では不要なので無視します。
右側がIL命令になるのでそちらを見ていきます。
IL命令 | 説明 |
---|---|
ldc.i4.0 | 整数値0をint32型としてスタックに入れる |
ldc.i4.1 | 整数値1をint32型としてスタックに入れる |
stloc.0 | スタックから値を取り出し、インデックス0の変数に格納する |
ldloc.0 | インデックス0の変数をスタックに読み込みする |
dup | スタックの一番上の値をコピーし、そのコピーをスタックに入れる |
add | スタックから2つ値を取り出して加算し、結果をスタックに入れる |
ret | 現在のメソッドから戻り、呼び出し先のスタックから呼び出し元のスタックに戻り値 (存在する場合) を入れる |
nop | スタックの遷移動作は定義されていないので、今回は無視して大丈夫 |
MSのドキュメントの説明通りに保存した値で最後上書きしてしまうため、最終的にx(インデックス[0])は0という結果になります。
参考サイト
- 後置インクリメントの動作説明
- コードをILに変換してくれるサイト
- SharpLabの使い方
- ILの説明
- IL命令の説明