Компилятор .NET C# (.NET 4.0) компилирует оператор fixed
довольно своеобразным способом.Почему MSFT C# компилирует исправленный «массив в размытие указателя» и «адрес первого элемента» по-другому?
Вот краткая, но полная программа, чтобы показать вам, о чем я говорю.
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/небезопасного/о +", если вы хотите следовать.
Вот сгенерированный IL для метода Good
:
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
:
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
:
- Получить адрес буфера [0].
- Разнообразить этот адрес.
- Вызовите WriteLine с этим разыменованным значением.
Вот что «делает Bad`:
- Если буфер пустой, GOTO 3.
- Если buffer.Length = 0, GOTO 5.
- хранить значение 0 в местный! slot 0,
- GOTO 6.
- Получить адрес буфера [0].
- Относитесь к этому адресу (в локальном слоте 0, который может быть 0 или буфером сейчас).
- Вызовите WriteLine с этим разыменованным значением.
Когда buffer
является не нулевым и непустым, эти две функции выполняют одно и то же. Обратите внимание, что Bad
просто перепрыгивает через несколько обручей, прежде чем перейти к вызову функции WriteLine
.
Когда buffer
имеет нулевое значение, Good
бросает NullReferenceException
в фиксированной указатель описателя (byte * p = &buffer[0]
). Предположительно, это желаемое поведение для фиксации управляемого массива, поскольку в целом любая операция внутри фиксированного оператора будет зависеть от действительности фиксируемого объекта. В противном случае, почему код должен быть внутри блока fixed
? Когда Good
передается нулевая ссылка, он не работает сразу в начале блока fixed
, обеспечивая соответствующую и информативную трассировку стека. Разработчик увидит это и поймет, что он должен подтвердить buffer
перед его использованием, или, возможно, его логику неправильно присвоило null
- buffer
.В любом случае, явно вводить блок fixed
с управляемым массивом null
нежелательно.
Bad
ручка в этом случае по-разному, даже неприемлемо. Вы можете видеть, что Bad
фактически не генерирует исключение, пока p
не будет разыменован. Он делает это по обходному пути , назначая нуль тому же локальному слоту, который содержит p
, а затем бросает исключение, когда оператор блока fixed
разыскивает разницу p
.
Обработка null
таким образом имеет то преимущество, что объектная модель в C# соответствует. То есть внутри блока fixed
p
по-прежнему обрабатывается семантически как своего рода «указатель на управляемый массив», который не будет, когда null, вызывает проблемы до тех пор, пока (или если) не будет разыменован. Согласованность - все хорошо и хорошо, но проблема в том, что 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) говорит, что поведение «Реализация определенных» в обоих случаях отказа:
Фиксированный-указатель-инициализатор может быть один из следующее:
• Токен «&», за которым следует переменная-ссылка (§5.3.3) на подвижную переменную (§18.3) неуправляемого типа T, если тип T * неявно конвертируется в указатель тип, заданный в фиксированном утверждении. В этом случае инициализатор вычисляет адрес данной переменной, и гарантируется, что переменная останется на фиксированном адресе на протяжении фиксированного оператора.
• Выражение типа массива с элементами неуправляемого типа T, если тип T * неявно конвертируется в тип указателя, заданный в фиксированном утверждении. В этом случае инициализатор вычисляет адрес первого элемента в массиве, и весь массив, как гарантируется, останется на фиксированном адресе на протяжении фиксированного оператора. Поведение фиксированного оператора определяется реализацией, если выражение массива равно null или массив имеет нулевые элементы.
... другие случаи ...
Последняя точка зрения, MSDN documentation предполагает, что два "эквивалент":
// Следующие два задания эквивалентны ...
фиксированной (двойной * р = обр) {/ ... /}
фиксированных (двойной * р = & обр [0]) {/ ... /}
Если два должны быть «эквивалент», то почему бы использовать различные обработки ошибок семантику для бывшего заявления ?
Похоже, что дополнительное усилие было записано в виде кодов, созданных в Bad
. Скомпилированный код в Good
отлично работает для всех случаев сбоев и совпадает с кодом в Bad
в случаях отказа. Зачем внедрять новые пути кода, а не просто использовать более простой код, созданный для Good
?
Почему это реализовано таким образом?
@pst Я удалил 'SoWhat' и его объяснение для краткость. Исправлена. –
Вы проверили, что говорит спецификация C# о 'arr [0]'? Я уверен, что 'arr [0]' генерирует исключение, когда 'arr' является нулевым или пустым в любом контексте, независимо от какого-либо' фиксированного' утверждения, которое его окружает. – hvd
@hvd Да, поэтому 'fixed (byte * p = buffer)' следует просто рассматривать как 'fixed (byte * p = & buffer [0])'. Последний обеспечивает более четкую проверку ошибок. Я добавлю это к вопросу. –