.NET c#コンパイラ(.NET 4.0)fixed
は、かなり特殊な方法でステートメントをコンパイルします。
これは、私が話していることを示すための短いが完全なプログラムです。
using System;
public static class FixedExample {
public static void Main() {
byte [] nonempty = new byte[1] {42};
byte [] empty = new byte[0];
Good(nonempty);
Bad(nonempty);
try {
Good(empty);
} catch (Exception e){
Console.WriteLine(e.ToString());
/* continue with next example */
}
Console.WriteLine();
try {
Bad(empty);
} catch (Exception e){
Console.WriteLine(e.ToString());
/* continue with next example */
}
}
public static void Good(byte[] buffer) {
unsafe {
fixed (byte * p = &buffer[0]) {
Console.WriteLine(*p);
}
}
}
public static void Bad(byte[] buffer) {
unsafe {
fixed (byte * p = buffer) {
Console.WriteLine(*p);
}
}
}
}
フォローしたい場合は、「csc.exe FixedExample.cs / unsafe /o+」でコンパイルしてください。
メソッドに対して生成されたILは次のGood
とおりです。
良い()
.maxstack 2
.locals init (uint8& pinned V_0)
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: ldelema [mscorlib]System.Byte
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: conv.i
IL_000a: ldind.u1
IL_000b: call void [mscorlib]System.Console::WriteLine(int32)
IL_0010: ldc.i4.0
IL_0011: conv.u
IL_0012: stloc.0
IL_0013: ret
メソッドに対して生成されたILは次のBad
とおりです。
悪い()
.locals init (uint8& pinned V_0, uint8[] V_1)
IL_0000: ldarg.0
IL_0001: dup
IL_0002: stloc.1
IL_0003: brfalse.s IL_000a
IL_0005: ldloc.1
IL_0006: ldlen
IL_0007: conv.i4
IL_0008: brtrue.s IL_000f
IL_000a: ldc.i4.0
IL_000b: conv.u
IL_000c: stloc.0
IL_000d: br.s IL_0017
IL_000f: ldloc.1
IL_0010: ldc.i4.0
IL_0011: ldelema [mscorlib]System.Byte
IL_0016: stloc.0
IL_0017: ldloc.0
IL_0018: conv.i
IL_0019: ldind.u1
IL_001a: call void [mscorlib]System.Console::WriteLine(int32)
IL_001f: ldc.i4.0
IL_0020: conv.u
IL_0021: stloc.0
IL_0022: ret
内容Good
は次のとおりです。
- buffer[0]のアドレスを取得します。
- そのアドレスを間接参照します。
- その逆参照された値でWriteLineを呼び出します。
'Bad`の機能は次のとおりです。
- バッファがnullの場合、GOTO3。
- buffer.Length!= 0の場合、GOTO5。
- 値0をローカルスロット0に格納します。
- GOTO6。
- buffer[0]のアドレスを取得します。
- そのアドレスを尊重します(ローカルスロット0で、現在は0またはバッファになっている可能性があります)。
- その逆参照された値でWriteLineを呼び出します。
buffer
がnullでも空でもない場合、これら2つの関数は同じことを行います。関数呼び出しBad
に到達する前に、いくつかのフープをジャンプすることに注意してください。WriteLine
buffer
がnullの場合、固定ポインタ宣言子( )にGood
aをスローします。一般に、 fixed-statement内の操作は、修正されるオブジェクトの有効性に依存するため、これは管理対象配列を修正するための望ましい動作です。そうでなければ、なぜそのコードはブロック内にあるのでしょうか?null参照が渡されると、ブロックの開始直後に失敗し、関連性のある有益なスタックトレースを提供します。開発者はこれを見て、使用する前に検証する必要があること、またはおそらくロジックが誤ってに割り当てられていることに気付くでしょう。いずれにせよ、明確にブロックに入るNullReferenceException
byte * p = &buffer[0]
fixed
Good
fixed
buffer
null
buffer
fixed
null
マネージドアレイは望ましくありません。
Bad
望ましくない場合でも、このケースの処理方法は異なります。Bad
が逆参照されるまで、実際には例外がスローされないことがわかりますp
。これは、を保持しているのと同じローカルスロットにnullを割り当て、後でブロックステートメントが参照を解除p
したときに例外をスローするという迂回的な方法で行われます。fixed
p
この方法で処理null
すると、C#のオブジェクトモデルの一貫性を保つことができるという利点があります。つまり、fixed
ブロック内では、p
意味的には一種の「管理対象配列へのポインター」として扱われ、nullの場合、逆参照されるまで(または逆参照されない限り)問題を引き起こしません。一貫性はすべて良好ですが、問題は、pが管理対象配列へのポインターではないことです。これは、の最初の要素へのポインタでありbuffer
、このコード()を記述した人は誰でも、Bad
その意味をそのように解釈します。buffer
からのサイズを取得p
できず、呼び出すこともできないp.ToString()
のに、なぜそれをオブジェクトであるかのように扱うのでしょうか。nullの場合、buffer
明らかにコーディングの間違いがあります。Bad
メソッド内ではなく、固定ポインタ宣言子で例外をスローします。
したがって、Good
処理はnull
実際よりも優れているようですBad
。空のバッファはどうですか?
buffer
長さが0の場合、固定ポインタ宣言Good
子でスローIndexOutOfRangeException
します。これは、範囲外の配列アクセスを処理するための完全に合理的な方法のようです。結局のところ、コードは、と同じように扱われる必要があります。これは明らかに。をスローするはずです。&buffer[0]
&(buffer[0])
IndexOutOfRangeException
Bad
このケースの処理方法は異なりますが、これも望ましくありません。の場合と同じように buffer
、null
が逆参照されるまで例外をスローせずbuffer.Length == 0
、その時点でIndexOutOfRangeExceptionではなくNullReferenceExceptionをスローします。 が逆参照されない場合、コードは例外をスローしません。繰り返しになりますが、ここでの考え方は、「管理対象配列へのポインター」の意味を与えることであるように思われます。繰り返しになりますが、このコードを書いている人がそのように考えるとは思いません。このコードは、固定ポインター宣言子をスローして、渡された配列が空ではなく空であることを開発者に通知すると、はるかに役立ちます。Bad
p
p
p
p
IndexOutOfRangeException
null
fixed(byte * p = buffer)
と同じコードにコンパイルされているはずのようfixed (byte * p = &buffer[0])
です。 また、任意の式である可能性がある場合でも、その型()はコンパイル時に認識されているため、のコードは任意の式で機能することに注意してください。buffer
byte[]
Good
編集
実際、の実装は実際に2回Bad
エラーチェックを行うことに注意してください。メソッドの最初で明示的に実行し、次に命令で暗黙的に実行します。buffer[0]
ldelema
Good
したがって、とBad
は意味的に異なる ことがわかります。Bad
は長く、おそらく遅く、コードにバグがある場合は確かに望ましい例外を与えず、場合によっては必要以上に遅く失敗することさえあります。
好奇心旺盛な人のために、仕様(C#4.0)のセクション18.6には、これらの障害の両方のケースで動作が「実装定義」であると記載されています。
固定ポインター初期化子は、次のいずれかになります。
•トークン「&」型T*がfixedステートメントで指定されたポインタ型に暗黙的に変換可能である場合、アンマネージ型Tの移動可能変数(§18.3)への変数参照(§5.3.3)が続きます。この場合、初期化子は指定された変数のアドレスを計算し、変数は固定ステートメントの期間中、固定アドレスに留まることが保証されます。
•タイプT*が、fixedステートメントで指定されたポインター型に暗黙的に変換可能である場合に限り、アンマネージ型Tの要素を持つ配列型の式。この場合、初期化子は配列の最初の要素のアドレスを計算し、配列全体が固定ステートメントの期間中固定アドレスに留まることが保証されます。配列式がnullの場合、または配列の要素がゼロの場合、fixedステートメントの動作は実装によって定義されます。
...その他の場合..。
最後に、MSDNのドキュメントでは、この2つは「同等」であると示唆されています。
//次の2つの割り当ては同等です...
固定(double * p = arr){/ ... /}
固定(double * p =&arr [0]){/ ... /}
2つが「同等」であると想定される場合、前のステートメントに異なるエラー処理セマンティクスを使用するのはなぜですか?
また、で生成されたコードパスの記述に余分な労力Bad
が費やされたようです。のコンパイル済みコードはGood
、すべての障害の場合に正常に機能し、障害のない場合のコードと同じBad
です。のために生成されたより単純なコードを使用するのではなく、なぜ新しいコードパスを実装するのGood
ですか?
なぜこのように実装されているのですか?