Компилятор.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
:
Вот что делает Bad `:
Когда buffer
и не -null, и не -пусто, эти две функции делают одно и то же. Обратите внимание, что Bad
просто перескакивает через несколько обручей, прежде чем перейти к вызову функции WriteLine
.
Когда buffer
равно нулю, Good
выдает NullReferenceException
в декларатор фиксированного -указателя (byte * p = &buffer[0]
).. Предположительно, это желаемое поведение для исправления управляемого массива, потому что в целом любая операция внутри фиксированного -оператора будет зависеть от достоверности фиксируемого объекта. Иначе зачем этот код находиться внутри блока fixed
? Когда Good
передается нулевая ссылка, происходит сбой сразу же в начале блока fixed
, обеспечивая релевантную и информативную трассировку стека. Разработчик увидит это и поймет, что он должен проверить buffer
перед его использованием, или, возможно, его логика неправильно назначила null
на buffer
. В любом случае, явный вход в блок fixed
с управляемым массивом null
нежелателен.
Bad
обрабатывает этот случай по-разному, даже нежелательно. Вы можете видеть, что Bad
на самом деле не генерирует исключение, пока p
не будет разыменован. Он делает это окольным путем: присваивает null тому же локальному слоту, который содержит p
, а затем выдает исключение, когда операторы блока fixed
разыменовывают p
.
Преимущество такой обработки null
заключается в сохранении согласованности объектной модели в C #. То есть внутри блока fixed
p
по-прежнему семантически обрабатывается как своего рода «указатель на управляемый массив», который при нулевом значении не будет вызывать проблем до (или до тех пор, пока )не будет разыменован. Согласованность — это хорошо, но проблема в том, что p не является указателем на управляемый массив .Это указатель на первый элемент buffer
, и любой, кто написал этот код (Bad
), будет интерпретировать его семантическое значение как таковое. Вы не можете получить размер buffer
из p
и не можете вызвать p.ToString()
, так зачем обращаться с ним, как с объектом? В тех случаях, когда buffer
имеет значение null, явно имеет место ошибка кодирования, и я считаю, что было бы гораздо полезнее, если бы Bad
выбрасывал исключение в декларатор фиксированного -указателя , а не внутри метода.
Таким образом, кажется, что Good
обрабатывает null
лучше, чем Bad
. Что делать с пустыми буферами?
Когда buffer
имеет длину 0, Good
выдает IndexOutOfRangeException
в фиксированный -декларатор указателя . Это кажется вполне разумным способом обработки доступа к массиву за пределами границ. В конце концов, код &buffer[0]
следует обрабатывать так же, как &(buffer[0])
, который, очевидно, должен выдавать IndexOutOfRangeException
.
Bad
обрабатывает этот случай по-разному, и снова нежелательно. Точно так же, как если бы buffer
было null
, когда buffer.Length == 0
, Bad
не выдает исключение до тех пор, пока p
не будет разыменовано, и в это время выбрасывает NullReferenceException, а не IndexOutOfRangeException! Если p
никогда не разыменовывается, код даже не выдает исключение. Опять же, кажется, что идея здесь состоит в том, чтобы дать p
семантическое значение «указатель на управляемый массив». Опять же, я не думаю, что кто-либо, пишущий этот код, будет думать о p
таким образом. Код был бы намного полезнее, если бы он бросал IndexOutOfRangeException
в декларатор фиксированного -указателя , тем самым уведомляя разработчика о том, что переданный массив пуст, а не null
.
Похоже, fixed(byte * p = buffer)
должен был быть скомпилирован в тот же код, что и fixed (byte * p = &buffer[0])
. Также обратите внимание, что хотя buffer
могло быть любым произвольным выражением, его тип(byte[]
)известно во время компиляции, поэтому код в Good
будет работать для любого произвольного выражения.
Изменить
Фактически,обратите внимание, что реализация Bad
фактически выполняет проверку ошибок наbuffer[0]
дважды . Он делает это явно в начале метода, а затем снова неявно в инструкции ldelema
.
Итак, мы видим, что Good
и Bad
семантически различны. Bad
длиннее, возможно, медленнее и, конечно же, не дает нам желаемых исключений, когда у нас есть ошибки в нашем коде, и даже в некоторых случаях дает сбой намного позже, чем должен.
Для тех, кому любопытно, в разделе 18.6 спецификации (C #4.0 )говорится, что поведение «определяется реализацией -» в обоих этих случаях отказа :
A fixed-pointer-initializer can be one of the following:
• The token “&” followed by a variable-reference (§5.3.3) to a moveable variable (§18.3) of an unmanaged type T, provided the type T* is implicitly convertible to the pointer type given in the fixed statement. In this case, the initializer computes the address of the given variable, and the variable is guaranteed to remain at a fixed address for the duration of the fixed statement.
• An expression of an array-type with elements of an unmanaged type T, provided the type T* is implicitly convertible to the pointer type given in the fixed statement. In this case, the initializer computes the address of the first element in the array, and the entire array is guaranteed to remain at a fixed address for the duration of the fixed statement. The behavior of the fixed statement is implementation-defined if the array expression is null or if the array has zero elements.
... other cases...
. Наконец, документация MSDN предполагает, что эти два понятия «эквивалентны» :
// The following two assignments are equivalent...
fixed (double* p = arr) { /.../ }
fixed (double* p = &arr[0]) { /.../ }
. Если предполагается, что они «эквивалентны», то зачем использовать различную семантику обработки ошибок для первого оператора?
Также кажется, что дополнительные усилия были приложены к написанию путей кода, сгенерированных в Bad
. Скомпилированный код в Good
отлично работает для всех случаев отказа и аналогичен коду в Bad
в случаях, отличных от -. Зачем реализовывать новые пути кода, а не просто использовать более простой код, сгенерированный для Good
?
Почему это реализовано именно так?