JavaScript の方から来ました! というような C# コードを書いたところ、
static string Path => _path ?? (_path = "フィールド初期値にできない文字列");
👇 の提案を受けました。
static string Path => _path ??= "フィールド初期値にできない文字列";
戻り値として扱われる場合もホントにこれで大丈夫なの?? と気になったので調べてみたところ、??
と ??=
では生成される IL 中間コードに違いが出ることが分かりました!
代入演算子の戻り値
=
演算子には戻り値1があります。
代入式の結果は、左辺のオペランドに割り当てられる値です。
https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/assignment-operator
a = b = c
a = (b = c)
// イメージとして
var ret = (b = c);
a = ret;
??=
は?
ところが ??=
演算子については、公式サイトを確認してもどのような結果が帰ってくるのか言及されていません。
null 合体演算子
??
では、それが null ではない場合、その左側のオペランドの値が返されます。それ以外の場合は、右側のオペランドが評価され、その結果が返されます。??
演算子では、左側のオペランドが null 値以外に評価された場合は、その右側のオペランドは評価されません。null 合体代入演算子
??=
は、左側のオペランドが null と評価された場合にのみ、右側のオペランドの値を左側のオペランドに代入します。??=
演算子では、左側のオペランドが null 値以外に評価された場合は、その右側のオペランドは評価されません。
https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/null-coalescing-operator
評価しないってことはそこには何があるの? 未定義動作ってコト?? って感じですが、以下の記載が同じページ内にあるため結果自体は存在するようです。
(というよりはヌル判定時に使ったものがそのまま、という感じでしょうか?)
※ 冒頭で示したワンライナーも動作に問題はなかった
a ?? b ?? c
d ??= e ??= f
// 次のように評価されます。
a ?? (b ?? c)
d ??= (e ??= f)
そもそもどう展開される?
a ?? b
は単独で使う演算子ではないため気になりませんが、a ??= b
は
if (a == null)
a = b;
と展開される、して欲しい印象があります。
しかし、結果があるということは、
if (a == null)
a = b;
else
a; // ←?
冒頭の例であれば問題はないですが、それ以外の場面でも常に無駄があるのか? と考えてしまいますね。
IL 中間コードの確認
ということで実際に何が起きているのか、以下のコードから生成される中間コードを確認してみます。
テスト用ソースコード
#nullable enable
class Main
{
static object _obj = null;
static object GetObj() => _obj;
static object GetObjAssign() => (_obj = new object());
static object GetObjQQ() => _obj ?? (_obj = new object());
static object GetObjQQAssign() => _obj ??= new object();
static string _str = null;
static string GetStr() => _str;
static string GetStrAssign() => (_str = "STRING");
static string GetStrQQ() => _str ?? (_str = "STRING");
static string GetStrQQAssign() => _str ??= "STRING";
static int? _value = null;
static int? GetValue() => _value;
static int? GetValueAssign => (_value = 310);
static int? GetValueQQ() => _value ?? (_value = 310);
static int? GetValueQQAssign() => _value ??= 310;
// ??= は int を返せる
static int GetValueQQAssignInt() => _value ??= 310;
static object otherObj = null;
static string otherStr = null;
static int? otherValue = null;
static object GetObjQQandQQ() => _obj ?? otherObj ?? (_obj = new object());
static string GetStrQQandQQ() => _str ?? otherStr ?? (_str = "STRING");
static int? GetValueQQandQQ() => _value ?? otherValue ?? (_value = 310);
static object GetObjQQandQQAssign() => _obj ?? (otherObj ??= (_obj = new object()));
static string GetStrQQandQQAssign() => _str ?? (otherStr ??= (_str = "STRING"));
static int? GetValueQQandQQAssign() => _value ?? (otherValue ??= (_value = 310));
static void IfStatements()
{
if (_obj == null)
_obj = new object();
if (_str == null)
_str = "STRING";
if (_value == null)
_value = 310;
}
static void QQAssignments()
{
_obj ??= new object();
_str ??= "STRING";
_value ??= 310;
}
static void NoReturns()
{
object obj = new object();
object objNull = null;
string str = "STRING";
string strNull = null;
int? value = 0;
int? valueNull = null;
obj ??= new object();
objNull ??= new object();
str ??= "STRING";
strNull ??= "STRING";
value ??= 310;
valueNull ??= 310;
System.Console.Write(obj);
System.Console.Write(objNull);
System.Console.Write(str);
System.Console.Write(strNull);
System.Console.Write(value);
System.Console.Write(valueNull);
}
}
#nullable restore
👉 https://sharplab.io/
Rider/ReSharperならエディター上で確認できるらしい
結果
調査した結果、ヌルチェックして代入というシチュエーションや Null 許容型で面白い結果が得られました。
一方で冒頭のワンライナーのような、元から null を代入できる型を使用する場合はどちらも同じ IL 中間コードが得られるという結果でした。
??=
の意味的なところ、評価されなかったら何がそこにあるの? が気になって調べてみましたが、結果、この演算子は使える場面では積極的に使ったほうが良いモノだということが分かりました。
※ 秒間数十億回の参照型ヌルチェックが発生している場合はパフォーマンスの爆上げも期待できます。
それでは細かく見ていきましょう。
Null 合体代入演算子
if (a == null) a = b
と a ??= b
では生成される中間コードが結構違います。
if
文では静的フィールドの読み込み、null 準備、比較判定、なんか色々とやっていますが、??=
はフィールドをスタックに読み込み即判定を行っています。
流石後発の演算子といった感じでしょうか。汎用的に使われる if
文と違って null チェックに特化しているという点も大きいのでしょう。
if (a is null)
なら? と思ったものの==
と変わらず。is
はオーバーロードできるオペレーターでは無いが、特に特殊な処理は行われていないようだ。
参照型変数が0じゃないならヌルじゃない、って C++ っぽいことを C# で実現するには??
??=
演算子を使うしかない??
if (a == null) a = b
//object
IL_0000: nop
IL_0001: ldsfld object Main::_obj
IL_0006: ldnull
IL_0007: ceq
IL_0009: stloc.0
// sequence point: hidden
IL_000a: ldloc.0
IL_000b: brfalse.s IL_0017
IL_000d: newobj instance void [System.Private.CoreLib]System.Object::.ctor()
IL_0012: stsfld object Main::_obj
//string
IL_0017: ldsfld string Main::_str
IL_001c: ldnull
IL_001d: ceq
IL_001f: stloc.1
// sequence point: hidden
IL_0020: ldloc.1
IL_0021: brfalse.s IL_002d
IL_0023: ldstr "STRING"
IL_0028: stsfld string Main::_str
//int?
IL_002d: ldsflda valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0032: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<int32>::get_HasValue()
IL_0037: ldc.i4.0
IL_0038: ceq
IL_003a: stloc.2
// sequence point: hidden
IL_003b: ldloc.2
IL_003c: brfalse.s IL_004a
IL_003e: ldc.i4.s 310
IL_0040: newobj instance void valuetype [System.Private.CoreLib]System.Nullable`1<int32>::.ctor(!0)
IL_0045: stsfld valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_004a: ret
a ??= b
//object
IL_0000: nop
IL_0001: ldsfld object Main::_obj
IL_0006: brtrue.s IL_0012
IL_0008: newobj instance void [System.Private.CoreLib]System.Object::.ctor()
IL_000d: stsfld object Main::_obj
//string
IL_0012: ldsfld string Main::_str
IL_0017: brtrue.s IL_0023
IL_0019: ldstr "STRING"
IL_001e: stsfld string Main::_str
//int?
IL_0023: ldsflda valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0028: call instance !0 valuetype [System.Private.CoreLib]System.Nullable`1<int32>::GetValueOrDefault()
IL_002d: stloc.0
IL_002e: ldsflda valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0033: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<int32>::get_HasValue()
IL_0038: brtrue.s IL_004a
IL_003a: ldc.i4.s 310
IL_003c: stloc.0
IL_003d: ldloc.0
IL_003e: newobj instance void valuetype [System.Private.CoreLib]System.Nullable`1<int32>::.ctor(!0)
IL_0043: stsfld valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0048: br.s IL_004a
IL_004a: ret
null 許容型は使いたくなくなりますね。JSONパーサー用に作られたって感じ??
ワンライナーの場合
冒頭で示したワンライナーのような場面での結果も見てみましょう。
参照型
参照型では ??
と ??=
で完全に同じ中間コードでした。ただ、先の a ??= b
と違い dup
が増えていますね。(要らない感がありますが)メソッドの戻り値とする為でしょうか。
IL中間コード
// object
IL_0000: ldsfld object Main::_obj
IL_0005: dup
IL_0006: brtrue.s IL_0014
IL_0008: pop
IL_0009: newobj instance void [System.Private.CoreLib]System.Object::.ctor()
IL_000e: dup
IL_000f: stsfld object Main::_obj
IL_0014: ret
// string
IL_0000: ldsfld string Main::_str
IL_0005: dup
IL_0006: brtrue.s IL_0014
IL_0008: pop
IL_0009: ldstr "STRING"
IL_000e: dup
IL_000f: stsfld string Main::_str
IL_0014: ret
Null 許容型 int?
対して Null 許容型 int?
の場合は違いが出ます。
a ?? (a = b)
IL_0000: ldsfld valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0005: stloc.0
IL_0006: ldloca.s 0
IL_0008: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<int32>::get_HasValue()
IL_000d: brtrue.s IL_001e
IL_000f: ldc.i4.s 310
IL_0011: newobj instance void valuetype [System.Private.CoreLib]System.Nullable`1<int32>::.ctor(!0)
IL_0016: dup
IL_0017: stsfld valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_001c: br.s IL_001f
IL_001e: ldloc.0
IL_001f: ret
a ??= b
IL_0000: ldsflda valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0005: call instance !0 valuetype [System.Private.CoreLib]System.Nullable`1<int32>::GetValueOrDefault()
IL_000a: stloc.0
IL_000b: ldsflda valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0010: call instance bool valuetype [System.Private.CoreLib]System.Nullable`1<int32>::get_HasValue()
IL_0015: brtrue.s IL_0028
IL_0017: ldc.i4.s 310
IL_0019: stloc.0
IL_001a: ldloc.0
IL_001b: newobj instance void valuetype [System.Private.CoreLib]System.Nullable`1<int32>::.ctor(!0)
IL_0020: stsfld valuetype [System.Private.CoreLib]System.Nullable`1<int32> Main::_value
IL_0025: ldloc.0
IL_0026: br.s IL_0029
IL_0028: ldloc.0
// int ではなく int? を返す場合は以下が追加
IL_0029: newobj instance void valuetype [System.Private.CoreLib]System.Nullable`1<int32>::.ctor(!0)
IL_002e: ret
??
の方がシンプル。なるほどですね!
付録
C# / .NET で使われている中間言語は元々 MSIL Microsoft Intermediate Language
と呼ばれていたものが標準化され、CIL Common Intermediate Language
となりました。Mono Cecil もココからですね。
似た略語が近しい場所で使われていて、それが CLI Common Language Infrastructure
です。CIL CIL 言ってるかと思ったら CLI CIL だった。なんてことにならないようにしたいですね。
CIL コマンド
nop
Do nothing (No operation) (0x00)
ldnull
Push a null reference on the stack (0x14)
ceq
Push 1 (of type int32) if value1 equals value2, else push 0 (0xFE 0x01)
pop
Pop value from the stack (0x26)
dup
Duplicate the value on the top of the stack (0x25)
ldsfld
Push the value of field on the stack (0x7E <T>)
ldsflda
Push the address of the static field, field, on the stack (0x7F <T>)
stloc.0
Pop a value from stack into local variable 0 (0x0A)
ldloc.0
Load local variable 0 onto stack (0x06)
ldloca.s
Load address of local variable with index indx, short form (0x12 <uint8>)
stsfld
Replace the value of field with val (0x80 <T>)
br.s
Branch to target, short form (0x2B <int8>)
brtrue.s
Branch to target if value is non-zero (true), short form (0x2D <int8>)
brfalse.s
Branch to target if value is zero (false), short form (0x2C <int8>)
call
Call method indicated on the stack with arguments (0x28 <T>)
newobj
Allocate an uninitialized object or value type and call ctor (0x73 <T>)
ret
Return from method, possibly with a value (0x2A)
--
如何だったでしょうか。最近まとめサイトの「調べてみましたが、分かりませんでした!」構文が好きです。
以上です。お疲れ様でした。
-
なのか? ↩