25

.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は次のとおりです。

  1. buffer[0]のアドレスを取得します。
  2. そのアドレスを間接参照します。
  3. その逆参照された値でWriteLineを呼び出します。

'Bad`の機能は次のとおりです。

  1. バッファがnullの場合、GOTO3。
  2. buffer.Length!= 0の場合、GOTO5。
  3. 値0をローカルスロット0に格納します。
  4. GOTO6。
  5. buffer[0]のアドレスを取得します。
  6. そのアドレスを尊重します(ローカルスロット0で、現在は0またはバッファになっている可能性があります)。
  7. その逆参照された値でWriteLineを呼び出します。

bufferがnullでも空でもない場合、これら2つの関数は同じことを行います。関数呼び出しBadに到達する前に、いくつかのフープをジャンプすることに注意してください。WriteLine

bufferがnullの場合、固定ポインタ宣言子( )にGoodaをスローします。一般に、 fixed-statement内の操作は、修正されるオブジェクトの有効性に依存するため、これは管理対象配列を修正するための望ましい動作です。そうでなければ、なぜそのコードはブロック内にあるのでしょうか?null参照が渡されると、ブロックの開始直後に失敗し、関連性のある有益なスタックトレースを提供します。開発者はこれを見て、使用する前に検証する必要があること、またはおそらくロジックが誤ってに割り当てられていることに気付くでしょう。いずれにせよ、明確にブロックに入るNullReferenceExceptionbyte * p = &buffer[0]fixedGoodfixedbuffernullbufferfixednullマネージドアレイは望ましくありません。

Bad望ましくない場合でも、このケースの処理方法は異なります。Badが逆参照されるまで、実際には例外がスローされないことがわかりますp。これは、を保持しているのと同じローカルスロットにnullを割り当て、後でブロックステートメントが参照を解除pしたときに例外をスローするという迂回的な方法で行われます。fixedp

この方法で処理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このケースの処理方法は異なりますが、これも望ましくありません。の場合と同じように buffernullが逆参照されるまで例外をスローせずbuffer.Length == 0、その時点でIndexOutOfRangeExceptionではなくNullReferenceExceptionをスローします。 が逆参照されない場合、コードは例外をスローしません。繰り返しになりますが、ここでの考え方は、「管理対象配列へのポインター」の意味を与えることであるように思われます。繰り返しになりますが、このコードを書いている人がそのように考えるとは思いません。このコードは、固定ポインター宣言子をスローして、渡された配列が空ではなく空であることを開発者に通知すると、はるかに役立ちます。BadppppIndexOutOfRangeExceptionnull

fixed(byte * p = buffer)と同じコードにコンパイルされているはずのようfixed (byte * p = &buffer[0])です。 また、任意の式である可能性がある場合でも、その型()はコンパイル時に認識されているため、のコードは任意の式で機能することに注意してください。bufferbyte[]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ですか?

なぜこのように実装されているのですか?

4

2 に答える 2

9

含めたILコードがほぼ行ごとに仕様を実装していることに気付くかもしれません。これには、関連する場合に仕様にリストされている2つの例外ケースを明示的に実装することと、関連しない場合にコードを含めないことが含まれます。したがって、コンパイラがそのように動作する最も単純な理由は、「仕様がそう言っているため」です。

もちろん、それは私たちが尋ねる可能性のあるさらに2つの質問につながります。

  • C#言語グループがこの方法で仕様を作成することを選択したのはなぜですか?
  • コンパイラチームがその特定の実装定義の動作を選択したのはなぜですか?

適切なチームの誰かが現れることを除いて、私たちはこれらの質問のいずれかに完全に答えることを本当に望んでいません。しかし、私たちは彼らの推論に従うことを試みることによって、2番目のものに答えることに突き刺すことができます。

仕様では、固定ポインタ初期化子に配列を提供する場合、次のようになっていることを思い出してください。

配列式がnullの場合、または配列の要素がゼロの場合、fixedステートメントの動作は実装によって定義されます。

この場合、実装は自由に実行することを選択できるため、コンパイラチームが実行するのが最も簡単で、最も安価な合理的な動作であると想定できます。

この場合、コンパイラチームが選択したのは、「コードが何か間違ったことをした時点で例外をスローする」ことでした。コードがfixed-pointer-initializer内にない場合にコードが何をするかを検討し、他に何が起こっているかを考えます。「良い」例では、存在しないオブジェクトのアドレスを取得しようとしています。これは、null/空の配列の最初の要素です。これは実際にできることではないため、例外が発生します。「悪い」例では、パラメータのアドレスをポインタ変数に割り当てるだけです。byte * p = null完全に正当な声明です。WriteLine(*p)エラーが発生するのは、それを試みたときだけです。固定ポインタ初期化子以降この例外の場合、は必要なことを何でも実行できます。最も簡単なことは、割り当ての実行を許可することです。それは無意味です。

明らかに、2つのステートメントは完全に同等ではありません。これは、標準がそれらを異なる方法で処理するという事実によってわかります。

  • &arr[0]は:「トークン「&」後に変数参照が続く」ので、コンパイラはarr[0]のアドレスを計算します。
  • arrは「配列型の式」であるため、コンパイラは配列の最初の要素のアドレスを計算しますが、nullまたは長さ0の配列は、表示されている実装定義の動作を生成することに注意してください。

配列に要素がある限り、この2つは同等の結果を生成します。これは、MSDNドキュメントが伝えようとしているポイントです。明示的に定義されていない動作または実装定義された動作がそのように動作する理由について質問しても、将来的には真実であると信頼できないため、特定の問題を解決するのに実際には役立ちません。(そうは言っても、もちろん、メモリ内のnull値を「修正」することはできないので、思考プロセスが何であるかを知りたいと思います...)

于 2012-08-03T22:14:16.370 に答える
1

したがって、GoodとBadは意味的に異なることがわかります。なんで?

良いのはケース1で、悪いのはケース2だからです。

Goodは「配列型の式」を割り当てません。「トークン「&」」を割り当てます 「その後に変数参照」が続くので、ケース1です。Badは「配列型の式」を割り当ててケース2にします。これが当てはまる場合、MSDNのドキュメントは間違っています。

いずれにせよ、これは、C#コンパイラが2つの異なる(そして2番目の場合は特殊な)コードパターンを作成する理由を説明しています。

なぜケース1はそのような単純なコードを生成するのですか?私はここで推測しています:配列要素のアドレスを取得することは、おそらく-expressionで使用するのと同じ方法でコンパイルされarray[index]ますref。CLRレベルでは、refパラメーターと式は単なるマネージポインターです。式もそうです&array[index]:ピン留めされていないが「内部」であるマネージポインターにコンパイルされます(この用語はマネージC ++に由来すると思います)。GCはそれを自動的に修正します。通常のオブジェクト参照のように動作します。

したがって、ケース1は通常のマネージポインター処理を取得し、ケース2は特別な実装定義(未定義ではない)の動作を取得します。

これはあなたのすべての質問に答えているわけではありませんが、少なくともそれはあなたの観察のいくつかの理由を提供します。エリック・リッパートがインサイダーとして彼の答えを追加してくれることを期待しています。

于 2012-08-03T22:10:55.513 に答える