Компилятор .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
:
- Получить адрес буфера[0].
- Разыменуйте этот адрес.
- Вызовите WriteLine с этим разыменованным значением.
Вот что делает Bad:
- Если буфер пуст, ПЕРЕЙТИ к 3.
- Если buffer.Length != 0, ПЕРЕЙТИ К 5.
- Сохраните значение 0 в локальном слоте 0,
- ПЕРЕЙТИ К 6.
- Получить адрес буфера[0].
- Уважайте этот адрес (в локальном слоте 0, который сейчас может быть 0 или буфером).
- Вызовите WriteLine с этим разыменованным значением.
Когда buffer
и ненулевое, и непустое значение, эти две функции делают одно и то же. Обратите внимание, что Bad
просто перепрыгивает через несколько обручей, прежде чем перейти к вызову функции WriteLine
.
Когда buffer
равно нулю, Good
создает NullReferenceException
в деклараторе фиксированного указателя (byte * p = &buffer[0]
). Предположительно, это желаемое поведение для исправления управляемого массива, потому что в целом любая операция внутри fixed-statement будет зависеть от достоверности фиксируемого объекта. Иначе зачем бы этот код находился внутри блока 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) говорится, что поведение определяется реализацией в обоих этих случаях сбоя:
Инициализатор с фиксированным указателем может быть одним из следующих:
• Маркер «&», за которым следует ссылка-переменная (§5.3.3) на подвижную переменную (§18.3) неуправляемого типа T, при условии, что тип T* неявно преобразуется в тип указателя, заданный в фиксированном операторе. В этом случае инициализатор вычисляет адрес данной переменной, и переменная гарантированно останется по фиксированному адресу на время действия фиксированного оператора.
• Выражение типа массива с элементами неуправляемого типа T при условии, что тип T* можно неявно преобразовать в тип указателя, заданный в фиксированном операторе. В этом случае инициализатор вычисляет адрес первого элемента в массиве, и весь массив гарантированно остается по фиксированному адресу на время действия фиксированного оператора. Поведение фиксированного оператора определяется реализацией, если выражение массива имеет значение null или если массив не содержит элементов.
...другие случаи...
И последний пункт: документация MSDN предлагает что они эквивалентны:
// Следующие два присваивания эквивалентны...
фиксированный (double* p = arr) { /.../ }
фиксированный (двойной* p = &arr[0]) { /.../ }
Если предполагается, что они эквивалентны, то зачем использовать другую семантику обработки ошибок для первого оператора?
Также кажется, что были приложены дополнительные усилия для написания путей кода, сгенерированных в Bad
. Скомпилированный код в Good
отлично работает для всех случаев сбоя и совпадает с кодом в Bad
в случаях без сбоев. Зачем реализовывать новые пути кода, а не просто использовать более простой код, сгенерированный для Good
?
Почему это реализовано именно так?
SoWhat
и его объяснение для краткости. Фиксированный. - person Michael Graczyk   schedule 04.08.2012arr[0]
? Я почти уверен, чтоarr[0]
выдает исключение, когдаarr
является нулевым или пустым в любом контексте, независимо от любого оператораfixed
, который его окружает. - person   schedule 04.08.2012fixed(byte *p = buffer)
следует рассматривать какfixed(byte *p = &buffer[0])
. Последний обеспечивает более четкую проверку ошибок. Я добавлю это к вопросу. - person Michael Graczyk   schedule 04.08.2012fixed
поведения не существует. Кроме этого: в C поведение undefined для ссылки на элемент массива, который не существует, поэтому&arr[0]
не определено, когдаarr
равноNULL
. Я не думаю, что стандарт C или C++ в настоящее время допускает массивы 0-длины (кажется, C++0x при использованииnew[]
); GCC работает как расширение, но я понятия не имею, что там за поведение. - person Michael Edenfield   schedule 04.08.2012&arr[0]
действительно недействителен в C, когдаarr
имеет значение null, но это потому, что&arr[0]
является сокращением от&*(arr+0)
, а арифметика недействительна для нулевых указателей.&*arr
, с другой стороны, действителен, даже еслиarr
имеет значение null, как и&arr[1]
, когдаarr
является массивом длины 1. - person   schedule 04.08.2012*(arr + 1)
иarr[1]
различаются: еслиarr
не имеет элемента в[1]
, то первый является допустимым указателем, находящимся за концом массива, а второй не определен. Я больше не делаю достаточно C, чтобы отслеживать :) - person Michael Edenfield   schedule 04.08.2012