23 Небезопасный код
23.1 Общие
Реализация, которая не поддерживает небезопасный код, требуется для диагностики любого использования синтаксических правил, определенных в этом предложении.
Остальная часть этого предложения, включая все его подклаузы, является условно нормативным.
Примечание. Основной язык C#, как определено в предыдущих предложениях, отличается, в частности, от C и C++ в его пропуске указателей в качестве типа данных. Вместо этого C# предоставляет ссылки и возможность создавать объекты, управляемые сборщиком мусора. Этот дизайн, в сочетании с другими функциями, делает C# гораздо более безопасным языком, чем C или C++. На основном языке C# просто невозможно иметь неинициализированную переменную, указатель "dangling" или выражение, которое индексирует массив за пределами его границ. Таким образом, устранены целые категории ошибок, которые обычно чумят программы C и C++.
Хотя практически каждый тип указателя в C или C++ имеет аналог ссылочного типа в C#, тем не менее, существуют ситуации, когда доступ к типам указателей становится необходимостью. Например, взаимодействие с базовой операционной системой, доступ к устройству, сопоставленное с памятью, или реализация критического алгоритма времени может оказаться невозможным или практическим без доступа к указателям. Для решения этой проблемы C# предоставляет возможность писать небезопасный код.
В небезопасном коде можно объявлять и работать с указателями, выполнять преобразования между указателями и целыми типами, принимать адрес переменных и т. д. В смысле, написание небезопасного кода очень похоже на написание кода C в программе C#.
Небезопасный код фактически является "безопасным" компонентом с точки зрения как разработчиков, так и пользователей. Небезопасный код должен быть четко помечен модификатором
unsafe
, поэтому разработчики не могут случайно использовать небезопасные функции, и подсистема выполнения работает, чтобы гарантировать, что небезопасный код не может быть выполнен в ненадежной среде.конечная заметка
23.2 Небезопасные контексты
Небезопасные функции C# доступны только в небезопасных контекстах. Небезопасный контекст представлен путем включения unsafe
модификатора в объявление типа, члена или локальной функции или использования unsafe_statement:
- Объявление класса, структуры, интерфейса или делегата может включать
unsafe
модификатор, в этом случае весь текстовый экстент объявления этого типа (включая текст класса, структуры или интерфейса) считается небезопасным контекстом.Примечание. Если type_declaration является частичным, это только небезопасный контекст. конечная заметка
- Объявление поля, метода, свойства, события, индексатора, оператора, конструктора экземпляра, метода завершения, статического конструктора или локальной функции может включать
unsafe
модификатор, в этом случае весь текстовый экстент объявления члена считается небезопасным контекстом. - Unsafe_statement позволяет использовать небезопасный контекст в блоке. Весь текстовый экстент связанного блока считается небезопасным контекстом. Локальная функция, объявленная в небезопасном контексте, небезопасна.
Связанные расширения грамматики показаны ниже и в последующих подклаузах.
unsafe_modifier
: 'unsafe'
;
unsafe_statement
: 'unsafe' block
;
Пример. В следующем коде
public unsafe struct Node { public int Value; public Node* Left; public Node* Right; }
unsafe
модификатор, указанный в объявлении структуры, приводит к тому, что весь текстовый экстент объявления структуры становится небезопасным контекстом. Таким образом, можно объявитьLeft
поля дляRight
типа указателя. Приведенный выше пример также может быть написанpublic struct Node { public int Value; public unsafe Node* Left; public unsafe Node* Right; }
Здесь модификаторы в объявлениях полей приводят к тому,
unsafe
что эти объявления считаются небезопасными контекстами.пример конца
Кроме установления небезопасного контекста, что позволяет использовать типы указателей, unsafe
модификатор не влияет на тип или элемент.
Пример. В следующем коде
public class A { public unsafe virtual void F() { char* p; ... } } public class B : A { public override void F() { base.F(); ... } }
Небезопасный модификатор метода
F
A
просто приводит к тому, что текстовая степеньF
становится небезопасным контекстом, в котором можно использовать небезопасные функции языка. В переопределенииF
вB
ней нет необходимости повторно указыватьunsafe
модификатор, если, конечно,F
сам метод не нуждается вB
доступе к небезопасным функциям.Ситуация немного отличается, когда тип указателя является частью сигнатуры метода.
public unsafe class A { public virtual void F(char* p) {...} } public class B: A { public unsafe override void F(char* p) {...} }
Здесь, поскольку
F
подпись включает тип указателя, его можно записать только в небезопасном контексте. Однако небезопасный контекст может быть введен либо путем создания всего класса небезопасным, как и вA
случае, либо путем включенияunsafe
модификатора в объявление метода, как и в случае.B
пример конца
unsafe
Если модификатор используется в объявлении частичного типа (§15.2.7), то только эта конкретная часть считается небезопасным контекстом.
Типы указателей 23.3
В небезопасном контексте тип (§8.1) может быть pointer_type, а также value_type, reference_type или type_parameter. В небезопасном контексте pointer_type также может быть типом элемента массива (§17). Pointer_type также можно использовать в выражении typeof (§12.8.18) вне небезопасного контекста (так как такое использование не является небезопасным).
Pointer_type записывается как unmanaged_type (§8.8) или ключевое словоvoid
, а затем *
маркер:
pointer_type
: value_type ('*')+
| 'void' ('*')+
;
Тип, указанный перед *
типом указателя, называется ссылочной типом типа указателя. Он представляет тип переменной, к которой указывает значение точек типа указателя.
Pointer_type можно использовать только в array_type в небезопасном контексте (§23.2). Non_array_type — это любой тип, который не является array_type.
В отличие от ссылок (значений ссылочных типов), указатели не отслеживаются сборщиком мусора— сборщик мусора не знает указателей и данных, к которым они указывают. По этой причине указатель не может указывать на ссылку или структуру, содержащую ссылки, и ссылочный тип указателя должен быть unmanaged_type. Типы указателей сами являются неуправляемыми типами, поэтому тип указателя может использоваться в качестве ссылочных типов для другого типа указателя.
Интуитивно понятное правило для смешивания указателей и ссылок заключается в том, что ссылки на ссылки (объекты) разрешены содержать указатели, но ссылки указателей не разрешены содержать ссылки.
Пример: в таблице ниже приведены некоторые примеры типов указателей:
Пример Description byte*
Указатель на byte
char*
Указатель на char
int**
Указатель на указатель на указатель int
int*[]
Одномерный массив указателей на int
void*
Указатель на неизвестный тип пример конца
Для данной реализации все типы указателей должны иметь одинаковый размер и представление.
Примечание. В отличие от C и C++, когда несколько указателей объявляются в одном объявлении, в C#
*
записывается вместе с базовым типом, а не как префикс пунктуатором для каждого имени указателя. Например:int* pi, pj; // NOT as int *pi, *pj;
конечная заметка
Значение указателя с типом T*
представляет адрес переменной типа T
. Оператор косвенного указателя *
(§23.6.2) можно использовать для доступа к этой переменной.
Пример. При указании переменной типа
P
выражениеint*
обозначает*P
переменнуюint
, найденную в адресе, содержаемом вP
. пример конца
Как и ссылка на объект, указатель может быть null
. Применение оператора косвенного обращения к указателю с значением приводит к null
выполнению определяемого реализацией поведения (§23.6.2). Указатель со значением null
представлен all-bits-zero.
Тип void*
представляет указатель на неизвестный тип. Поскольку тип ссылки неизвестен, оператор косвенного обращения не может применяться к указателю типа void*
, а также не может выполняться арифметика на таком указателе. Однако указатель типа можно привести к любому другому типу void*
указателя (и наоборот) и по сравнению со значениями других типов указателей (§23.6.8).
Типы указателей — это отдельная категория типов. В отличие от ссылочных типов и типов значений, типы указателей не наследуются и object
преобразования не существуют между типами указателей и object
. В частности, бокс и распаковка (§8.3.13) не поддерживаются для указателей. Однако преобразования разрешены между различными типами указателей и между типами указателей и целочисленными типами. Это описано в разделе "23.5".
Не удается использовать pointer_type в качестве аргумента типа (§8.4), а вывод типа (§12.6.3) завершается сбоем при вызовах универсальных методов, которые выводили бы аргумент типа в качестве типа указателя.
Pointer_type нельзя использовать в качестве типа подтекстов динамической связанной операции (§12.3.3).
Pointer_type нельзя использовать в качестве типа первого параметра в методе расширения (§15.6.10).
Pointer_type можно использовать в качестве типа переменного поля (§15.5.4).
Динамический E*
E
.
Выражение с типом указателя нельзя использовать для предоставления значения в member_declarator в anonymous_object_creation_expression (§12.8.17.7).
Значение по умолчанию (§9.3) для любого типа null
указателя.
Примечание. Хотя указатели могут передаваться в качестве ссылочных параметров, это может привести к неопределенному поведению, так как указатель может быть указан на локальную переменную, которая больше не существует при возврате вызываемого метода, или фиксированный объект, в который он использовался для указания, больше не фиксирован. Например:
class Test { static int value = 20; unsafe static void F(out int* pi1, ref int* pi2) { int i = 10; pi1 = &i; // return address of local variable fixed (int* pj = &value) { // ... pi2 = pj; // return address that will soon not be fixed } } static void Main() { int i = 15; unsafe { int* px1; int* px2 = &i; F(out px1, ref px2); int v1 = *px1; // undefined int v2 = *px2; // undefined } } }
конечная заметка
Метод может возвращать значение определенного типа, и этот тип может быть указателем.
Пример. При указании указателя на последовательную последовательность элементов, число элементов этой последовательности
int
и другоеint
значение, следующий метод возвращает адрес этого значения в этой последовательности, если совпадение происходит; в противном случае возвращаетсяnull
:unsafe static int* Find(int* pi, int size, int value) { for (int i = 0; i < size; ++i) { if (*pi == value) { return pi; } ++pi; } return null; }
пример конца
В небезопасном контексте несколько конструкций доступны для работы с указателями:
- Унарный
*
оператор может использоваться для выполнения косвенного обращения указателя (§23.6.2). - Оператор
->
может использоваться для доступа к члену структуры через указатель (§23.6.3). - Оператор
[]
может использоваться для индексирования указателя (§23.6.4). - Унарный
&
оператор может использоваться для получения адреса переменной (§23.6.5). - Операторы
++
могут использоваться для увеличения и уменьшения указателей (--
). - Двоичные
+
и-
операторы могут использоваться для арифметики указателя (§23.6.7). - Операторы
==
,!=
, и<
>
<=
>=
операторы могут использоваться для сравнения указателей (§23.6.8). - Оператор
stackalloc
может использоваться для выделения памяти из стека вызовов (§23.9). - Инструкцию
fixed
можно использовать для временного исправления переменной, чтобы его адрес можно было получить (§23.7).
23.4 Фиксированные и перемещаемые переменные
Адрес оператора (§23.6.5) и оператор (§23.7) делят переменные на две категории:fixed
и перемещаемые переменные.
Фиксированные переменные находятся в местах хранения, не затронутых операцией сборщика мусора. (Примеры фиксированных переменных включают локальные переменные, параметры значений и переменные, созданные указателями расшифровки.) С другой стороны, перемещаемые переменные находятся в местах хранения, которые подвергаются перемещению или удалению сборщиком мусора. (Примеры перемещаемых переменных включают поля в объекты и элементы массивов.)
Оператор &
(§23.6.5) позволяет получить адрес фиксированной переменной без ограничений. Однако, поскольку перемещаемая переменная подвергается перемещению или удалению сборщиком мусора, адрес перемещаемой переменной можно получить только с помощью (fixed statement
§23.7), и этот адрес остается допустимым только в течение этой fixed
инструкции.
Точно говоря, фиксированная переменная является одной из следующих:
- Переменная, полученная из simple_name (§12.8.4), которая относится к локальной переменной, параметру значения или массиву параметров, если переменная не фиксируется анонимной функцией (§12.19.6.2).
- Переменная, полученная из member_access (§12.8.7
V
- Переменная, полученная от pointer_indirection_expression (§23.6.2) формы, pointer_member_access (§23.6.3) формы
*P
или pointer_element_access (P->I
) формы.P[E]
Все остальные переменные классифицируются как перемещаемые переменные.
Статическое поле классифицируется как перемещаемая переменная. Кроме того, параметр путем ссылки классифицируется как перемещаемая переменная, даже если аргумент, заданный для параметра, является фиксированной переменной. Наконец, переменная, созданная путем расшифровки указателя, всегда классифицируется как фиксированная переменная.
Преобразования указателя 23.5
23.5.1 Общие
В небезопасном контексте набор доступных неявных преобразований (§10.2) расширен, чтобы включить следующие неявные преобразования указателя:
- От любого pointer_type к типу
void*
. -
null
От литерала (§6.4.5.7) до любого pointer_type.
Кроме того, в небезопасном контексте набор доступных явных преобразований (§10.3) расширен для включения следующих явных преобразований указателей:
- От любого pointer_type до любого другого pointer_type.
- От
sbyte
,byte
,short
ushort
int
uint
long
илиulong
до любой pointer_type. - От любого pointer_type до
sbyte
byte
,short
ushort
, , ,int
uint
,long
или .ulong
Наконец, в небезопасном контексте набор стандартных неявных преобразований (§10.4.2) включает следующие преобразования указателя:
- От любого pointer_type к типу
void*
. -
null
От литерала до любого pointer_type.
Преобразования между двумя типами указателей никогда не изменяют фактическое значение указателя. Другими словами, преобразование из одного типа указателя в другой не влияет на базовый адрес, заданный указателем.
Если один тип указателя преобразуется в другой, если результирующий указатель неправильно выравнивается для типа с указанием на тип, поведение не определено, если результат разоменовывается. Как правило, понятие "правильно выровнено" является транзитивным: если указатель на тип A
правильно выравнивается для типа указателя, который, в свою очередь, правильно выравнивается для указателя B
на типC
, то указатель на тип A
правильно выравнивается для указателя типаC
.
Пример. Рассмотрим следующий случай, когда переменная с одним типом обращается через указатель на другой тип:
unsafe static void M() { char c = 'A'; char* pc = &c; void* pv = pc; int* pi = (int*)pv; // pretend a 16-bit char is a 32-bit int int i = *pi; // read 32-bit int; undefined *pi = 123456; // write 32-bit int; undefined }
пример конца
Когда тип указателя преобразуется в указатель byte
, результат указывает на наименьший адрес byte
переменной. Последовательные приращения результата, вплоть до размера переменной, дают указатели на оставшиеся байты этой переменной.
Пример. Следующий метод отображает каждый из восьми байтов в
double
виде шестнадцатеричного значения:class Test { static void Main() { double d = 123.456e23; unsafe { byte* pb = (byte*)&d; for (int i = 0; i < sizeof(double); ++i) { Console.Write($" {*pb++:X2}"); } Console.WriteLine(); } } }
Конечно, выходные данные зависят от эндианности. Одна из возможных возможностей .
" BA FF 51 A2 90 6C 24 45"
пример конца
Сопоставления между указателями и целыми числами определяются реализацией.
Примечание. Однако на 32-разрядных и 64-разрядных архитектурах ЦП с линейным адресным пространством преобразования указателей в целочисленные типы обычно ведут себя точно так же, как преобразования
uint
илиulong
значения соответственно в эти целочисленные типы. конечная заметка
Массивы указателей 23.5.2
Массивы указателей можно создать с помощью array_creation_expression (§12.8.17.5) в небезопасном контексте. В массивах указателей разрешены только некоторые преобразования, применяемые к другим типам массивов:
- Неявное преобразование ссылок (§10.2.8) из любого array_type в
System.Array
интерфейсы, которые он реализует, также применяется к массивам указателей. Однако любая попытка доступа к элементамSystem.Array
массива через или интерфейсы, которые она реализует, может привести к исключению во время выполнения, так как типы указателей не преобразуются вobject
. - Неявные и явные преобразования ссылок (§10.2.8, §10.3.5) из одномерного типа
S[]
массива вSystem.Collections.Generic.IList<T>
и его универсальные базовые интерфейсы никогда не применяются к массивам указателей. - Явное преобразование ссылок (§10.3.5)
System.Array
и интерфейсы, которые он реализует для любых array_type применяется к массивам указателей. - Явные преобразования ссылок (§10.3.5) из
System.Collections.Generic.IList<S>
и его базовых интерфейсов в одномерный типT[]
массива никогда не применяются к массивам указателей, так как типы указателей не могут использоваться в качестве аргументов типа, и нет преобразования типов указателей в типы указателей.
Эти ограничения означают, что расширение foreach
инструкции по массивам, описанным в §9.4.4.17 , нельзя применить к массивам указателей. Вместо этого оператор foreach
формы
foreach (V v in x)
embedded_statement
Где тип x
массива является типом массива формы T[,,...,]
, n — это число измерений минус 1 и T
или V
является типом указателя, развернутым с помощью вложенных циклов следующим образом:
{
T[,,...,] a = x;
for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++)
{
for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++)
{
...
for (int in = a.GetLowerBound(n); in <= a.GetUpperBound(n); in++)
{
V v = (V)a[i0,i1,...,in];
*embedded_statement*
}
}
}
}
Переменные a
, i0
, i1
...
in
не видимы или недоступны x
для embedded_statement или любого другого исходного кода программы. Переменная v
доступна только для чтения в внедренной инструкции. Если не существует явного преобразования (§23.5T
типа элемента) V
в , возникает ошибка и дальнейшие действия не выполняются. Если x
имеет значение null
, System.NullReferenceException
создается во время выполнения.
Примечание. Хотя типы указателей не разрешены в качестве аргументов типа, массивы указателей могут использоваться в качестве аргументов типа. конечная заметка
23.6 Указатели в выражениях
23.6.1 Общие
В небезопасном контексте выражение может привести к результату типа указателя, но за пределами небезопасного контекста это ошибка во время компиляции для выражения типа указателя. В точных терминах вне небезопасного контекста возникает ошибка времени компиляции, если какая-либо simple_name (§12.8.4), member_access (§12.8.7), invocation_expression (§12.8.10) или element_access (§12.8.12) имеет тип указателя.
В небезопасном контексте primary_no_array_creation_expression (§12.8) и unary_expression (§12.9) позволяют создавать дополнительные конструкции, описанные в следующих подклаузах.
Примечание. Приоритет и ассоциативность небезопасных операторов подразумевается грамматикой. конечная заметка
23.6.2 Указатель косвенного
Pointer_indirection_expression состоит из звездочки () и unary_expression*
.
pointer_indirection_expression
: '*' unary_expression
;
Унарный *
оператор обозначает косвенное указатель и используется для получения переменной, к которой указывает указатель. Результат вычисления *P
, где P
является выражение типа T*
указателя, является переменной типа T
. Это ошибка во время компиляции для применения унарного *
оператора к выражению типа или к выражению, которое не относится к типу void*
указателя.
Эффект применения унарного *
оператора к null
указателю с значением определяется реализацией. В частности, эта операция не гарантирует, что эта операция создает System.NullReferenceException
исключение.
Если указателю назначено недопустимое значение, поведение унарного *
оператора не определено.
Примечание. Среди недопустимых значений для расшифровки указателя унарным
*
оператором являются адрес, неправильно выровненный для типа, на который указывает тип (см. пример в §23.5) и адрес переменной после окончания его существования.
В целях определенного анализа назначения переменная, созданная при оценке выражения формы *P
, считается первоначально назначенной (§9.4.2).
Доступ к члену указателя 23.6.3
Pointer_member_access состоит из primary_expression, за которым следует маркер "->
" и идентификатор и необязательный type_argument_list.
pointer_member_access
: primary_expression '->' identifier type_argument_list?
;
В доступе к элементу указателя формы P->I
P
должен быть выражением типа указателя и I
должен указывать на доступный член типа, к которому P
указываются точки.
Доступ к элементу указателя формы P->I
вычисляется точно так же, как (*P).I
. Описание оператора косвенного указателя (*
) см. в разделе §23.6.2. Описание оператора доступа к члену (.
) см. в разделе §12.8.7.
Пример. В следующем коде
struct Point { public int x; public int y; public override string ToString() => $"({x},{y})"; } class Test { static void Main() { Point point; unsafe { Point* p = &point; p->x = 10; p->y = 20; Console.WriteLine(p->ToString()); } } }
->
Оператор используется для доступа к полям и вызова метода структуры с помощью указателя. Так как операцияP->I
точно эквивалентна(*P).I
,Main
метод может быть написан так же хорошо:class Test { static void Main() { Point point; unsafe { Point* p = &point; (*p).x = 10; (*p).y = 20; Console.WriteLine((*p).ToString()); } } }
пример конца
Доступ к элементу указателя 23.6.4
Pointer_element_access состоит из primary_no_array_creation_expression, за которым следует выражение, заключенное в "[
" и "]
".
pointer_element_access
: primary_no_array_creation_expression '[' expression ']'
;
В доступе к элементу указателя формы P[E]
P
должен быть выражением типа указателя, отличного void*
от типа, и E
должно быть выражением, которое может быть неявно преобразовано в int
, uint
long
или ulong
.
Доступ к элементу указателя формы P[E]
вычисляется точно так же, как *(P + E)
. Описание оператора косвенного указателя (*
) см. в разделе §23.6.2. Описание оператора добавления указателя (+
) см. в разделе §23.6.7.
Пример. В следующем коде
class Test { static void Main() { unsafe { char* p = stackalloc char[256]; for (int i = 0; i < 256; i++) { p[i] = (char)i; } } } }
Доступ к элементу указателя используется для инициализации буфера символов в цикле
for
. Так как операцияP[E]
точно эквивалентна*(P + E)
, пример может быть написан так же хорошо:class Test { static void Main() { unsafe { char* p = stackalloc char[256]; for (int i = 0; i < 256; i++) { *(p + i) = (char)i; } } } }
пример конца
Оператор доступа к элементу указателя не проверяет наличие ошибок вне границ и поведение при доступе к элементу вне границ не определено.
Примечание. Это то же самое, что и C++. конечная заметка
23.6.5 Оператор адреса
Addressof_expression состоит из амперсанда (&
), за которым следует unary_expression.
addressof_expression
: '&' unary_expression
;
Учитывая выражениеE
, которое имеет тип T
и классифицируется как фиксированная переменная (§23.4), конструкция &E
вычисляет адрес переменной, заданной .E
Тип результата и T*
классифицируется как значение. Ошибка во время компиляции возникает, если E
не классифицируется как переменная, если E
классифицируется как локальная переменная только для чтения, или если E
обозначает перемещаемую переменную. В последнем случае фиксированная инструкция (§23.7) может использоваться для временного исправления переменной перед получением адреса.
Примечание. Как указано в §12.8.7, за пределами конструктора экземпляра или статического конструктора для структуры или класса, определяющего
readonly
поле, это поле считается значением, а не переменной. Таким образом, его адрес нельзя принять. Аналогичным образом нельзя принять адрес константы. конечная заметка
Оператор &
не требует, чтобы его аргумент был определенно назначен, но после &
операции переменная, к которой применяется оператор, считается определенно назначенной в пути выполнения, в котором происходит операция. Это ответственность программиста, чтобы убедиться, что правильная инициализация переменной на самом деле происходит в этой ситуации.
Пример. В следующем коде
class Test { static void Main() { int i; unsafe { int* p = &i; *p = 123; } Console.WriteLine(i); } }
i
считается определенно назначенным после операции, используемой&i
для инициализацииp
. Назначение, которое*p
фактически инициализируетi
, но включение этой инициализации является ответственностью программиста, и ошибка во время компиляции не будет возникать, если назначение было удалено.пример конца
Примечание. Правила определенного назначения для
&
оператора существуют таким образом, чтобы можно было избежать избыточной инициализации локальных переменных. Например, многие внешние API принимают указатель на структуру, которая заполняется API. Вызовы таких API обычно передают адрес локальной переменной структуры, а без правила требуется избыточное инициализация переменной структуры. конечная заметка
Примечание. Если локальная переменная, параметр значения или массив параметров захватывается анонимной функцией (§12.8.24), то локальная переменная, параметр или массив параметров больше не считается фиксированной переменной (§23.7), но вместо этого считается перемещаемой переменной. Таким образом, это ошибка для любого небезопасного кода, чтобы получить адрес локальной переменной, параметра значения или массив параметров, который был захвачен анонимной функцией. конечная заметка
23.6.6 Указатель и увеличение и уменьшение
В небезопасном контексте операторы и ++
операторы (--
и §12.9.6) можно применять к переменным указателя всех типов, кроме .void*
Таким образом, для каждого типа T*
указателя неявно определены следующие операторы:
T* operator ++(T* x);
T* operator --(T* x);
Операторы создают те же результаты, что x+1
и соответственно (x-1
). Другими словами, для переменной указателя типа T*
++
оператор добавляет sizeof(T)
адрес, содержащийся в переменной, и --
оператор вычитает sizeof(T)
из адреса, содержащегося в переменной.
Если операция увеличения или уменьшения указателя переполнена доменом типа указателя, результат определяется реализацией, но исключения не создаются.
23.6.7 арифметический указатель
В небезопасном контексте +
оператор (§12.10.5-
§12.10.6) можно применять к значениям всех типов указателей, кроме void*
. Таким образом, для каждого типа T*
указателя неявно определены следующие операторы:
T* operator +(T* x, int y);
T* operator +(T* x, uint y);
T* operator +(T* x, long y);
T* operator +(T* x, ulong y);
T* operator +(int x, T* y);
T* operator +(uint x, T* y);
T* operator +(long x, T* y);
T* operator +(ulong x, T* y);
T* operator –(T* x, int y);
T* operator –(T* x, uint y);
T* operator –(T* x, long y);
T* operator –(T* x, ulong y);
long operator –(T* x, T* y);
Учитывая выражение P
типа указателя и выражение T*
типа N
int
, uint
long
или ulong
, выражения P + N
и N + P
вычисляет значение указателя типаT*
, которое приводит к добавлению N * sizeof(T)
в адрес, заданный.P
Аналогичным образом выражение P – N
вычисляет значение указателя типа T*
, которое вычитается N * sizeof(T)
из адреса, заданного P
.
Учитывая два выражения P
и Q
тип указателя T*
, выражение P – Q
вычисляет разницу между адресами, заданными P
, а Q
затем делит это различие на sizeof(T)
. Тип результата всегда long
. В действительности вычисляется P - Q
как ((long)(P) - (long)(Q)) / sizeof(T)
.
Пример:
class Test { static void Main() { unsafe { int* values = stackalloc int[20]; int* p = &values[1]; int* q = &values[15]; Console.WriteLine($"p - q = {p - q}"); Console.WriteLine($"q - p = {q - p}"); } } }
который создает выходные данные:
p - q = -14 q - p = 14
пример конца
Если арифметическая операция указателя переполнена доменом типа указателя, результат усечен в определяемом реализацией способе, но исключения не создаются.
Сравнение указателей 23.6.8
В небезопасном контексте ==
операторы , , !=
<
, >
<=
и >=
операторы (§12.12) можно применять к значениям всех типов указателей. Операторы сравнения указателя:
bool operator ==(void* x, void* y);
bool operator !=(void* x, void* y);
bool operator <(void* x, void* y);
bool operator >(void* x, void* y);
bool operator <=(void* x, void* y);
bool operator >=(void* x, void* y);
Так как неявное преобразование существует из любого типа указателя в void*
тип, операнды любого типа указателя можно сравнить с помощью этих операторов. Операторы сравнения сравнивают адреса, заданные двумя операндами, как если бы они были без знака целых чисел.
23.6.9 Оператор sizeof
Для определенных предопределенных типов (§12.8.19) sizeof
оператор получает константное int
значение. Для всех других типов результат sizeof
оператора определяется реализацией и классифицируется как значение, а не констант.
Порядок упаковки элементов в структуру не определен.
Для выравнивания может быть неназванное заполнение в начале структуры, внутри структуры и в конце структуры. Содержимое битов, используемых в качестве заполнения, не определено.
При применении к операнду с типом структуры результатом является общее количество байтов в переменной этого типа, включая любое заполнение.
23.7 Фиксированная инструкция
В небезопасном контексте рабочий embedded_statement (§13.1) разрешает дополнительную конструкцию, фиксированную инструкцию, которая используется для "исправления" перемещаемой переменной, чтобы его адрес оставался постоянным в течение длительности инструкции.
fixed_statement
: 'fixed' '(' pointer_type fixed_pointer_declarators ')' embedded_statement
;
fixed_pointer_declarators
: fixed_pointer_declarator (',' fixed_pointer_declarator)*
;
fixed_pointer_declarator
: identifier '=' fixed_pointer_initializer
;
fixed_pointer_initializer
: '&' variable_reference
| expression
;
Каждый fixed_pointer_declarator объявляет локальную переменную заданного pointer_type и инициализирует локальную переменную с адресом, вычисляемым соответствующим fixed_pointer_initializer. Локальная переменная, объявленная в фиксированной инструкции, доступна в любом fixed_pointer_initializer, происходящих справа от объявления этой переменной, и в embedded_statement фиксированной инструкции. Локальная переменная, объявленная фиксированной инструкцией, считается доступной только для чтения. Ошибка во время компиляции возникает, если внедренная инструкция пытается изменить эту локальную переменную (с помощью назначения или ++
--
операторов) или передать ее в качестве ссылочного или выходного параметра.
Ошибка в использовании захваченной локальной переменной (§12.19.6.2), параметра значения или массива параметров в fixed_pointer_initializer. Fixed_pointer_initializer может быть одним из следующих вариантов:
- Маркер "
&
", за которым следует variable_reference (§9.5) в перемещаемую переменную (§23.4) неуправляемого типаT
, если тип неявно преобразуется в типT*
указателя, указанный в инструкцииfixed
. В этом случае инициализатор вычисляет адрес указанной переменной, и переменная гарантированно остается на фиксированном адресе в течение фиксированной инструкции. - Выражение array_type с элементами неуправляемого типа
T
, если типT*
неявно преобразуется в тип указателя, заданный в фиксированной инструкции. В этом случае инициализатор вычисляет адрес первого элемента в массиве, а весь массив гарантированно остается на фиксированном адресе в течение длительности инструкцииfixed
. Если выражение массива имеетnull
нулевые элементы, инициализатор вычисляет адрес, равный нулю. - Выражение типа
string
, если тип неявно преобразуется в типchar*
указателя, заданный в инструкцииfixed
. В этом случае инициализатор вычисляет адрес первого символа в строке, и вся строка гарантированно остается на фиксированном адресе в течение длительности инструкцииfixed
. Поведение инструкцииfixed
определяется реализацией, если строковое выражение равноnull
. - Выражение типа, отличного от array_type или если существует доступный метод или метод расширения, соответствующий сигнатуре
string
, гдеref [readonly] T GetPinnableReference()
являетсяT
, иT*
неявно преобразуется в тип указателя, указанный в инструкцииfixed
. В этом случае инициализатор вычисляет адрес возвращаемой переменной, и эта переменная гарантированно остается на фиксированном адресе в течение длительности инструкцииfixed
.GetPinnableReference()
Метод может использоваться операторомfixed
, если разрешение перегрузки (§12.6.4) создает ровно один член функции, и этот элемент функции удовлетворяет предыдущим условиям. МетодGetPinnableReference
должен возвращать ссылку на адрес, равный нулю, например, возвращаемую приSystem.Runtime.CompilerServices.Unsafe.NullRef<T>()
отсутствии данных для закрепления. - Simple_name или member_access, ссылающийся на член буфера фиксированного размера перемещаемой переменной, если тип элемента буфера фиксированного размера неявно преобразуется в тип указателя, указанный в инструкции
fixed
. В этом случае инициализатор вычисляет указатель на первый элемент буфера фиксированного размера (§23.8.3), а буфер фиксированного размера гарантированно остается в фиксированном адресе в течение длительности инструкцииfixed
.
Для каждого адреса, вычисляемого fixed_pointer_initializerfixed
оператором, гарантирует, что переменная, на которую ссылается адрес, не подлежит перемещению или удалению сборщиком мусора в течение длительности инструкции fixed
.
Пример. Если адрес, вычисляемый fixed_pointer_initializer ссылается на поле объекта или элемента экземпляра массива, то фиксированная инструкция гарантирует, что содержащий экземпляр объекта не перемещается или не удаляется во время существования инструкции. пример конца
Программист несет ответственность за обеспечение того, чтобы указатели, созданные фиксированными операторами, не выживали после выполнения этих инструкций.
Пример. Когда указатели, созданные
fixed
операторами, передаются во внешние API, это ответственность программиста за то, чтобы API не сохраняли память этих указателей. пример конца
Фиксированные объекты могут привести к фрагментации кучи (так как они не могут быть перемещены). По этой причине объекты должны быть исправлены только при абсолютной необходимости, а затем только в течение короткого периода времени.
Пример: пример
class Test { static int x; int y; unsafe static void F(int* p) { *p = 1; } static void Main() { Test t = new Test(); int[] a = new int[10]; unsafe { fixed (int* p = &x) F(p); fixed (int* p = &t.y) F(p); fixed (int* p = &a[0]) F(p); fixed (int* p = a) F(p); } } }
демонстрирует несколько способов использования инструкции
fixed
. Первая инструкция исправляет и получает адрес статического поля, второй оператор исправляет и получает адрес поля экземпляра, а третий оператор исправляет и получает адрес элемента массива. В каждом случае было бы ошибкой использовать обычный&
оператор, так как переменные классифицируются как перемещаемые переменные.Третий и четвертый
fixed
операторы в приведенном выше примере дают идентичные результаты. Как правило, для экземпляраa
a[0]
массива указание вfixed
инструкции совпадает с простым указаниемa
.пример конца
В небезопасном контексте элементы массива одномерных массивов хранятся в растущем порядке индекса, начиная с индекса 0
и заканчивая индексом Length – 1
. Для многомерных массивов элементы массива хранятся таким образом, чтобы индексы самого правого измерения были увеличены сначала, а затем следующее левое измерение и т. д. слева.
fixed
В инструкции, которая получает указатель p
на экземпляр a
массива, значения указателя, начиная от p
p + a.Length - 1
представления адресов элементов в массиве. Аналогичным образом переменные, начиная от p[0]
p[a.Length - 1]
представления фактических элементов массива. Учитывая способ хранения массивов, массив любого измерения можно рассматривать, как если бы он был линейным.
Пример:
class Test { static void Main() { int[,,] a = new int[2,3,4]; unsafe { fixed (int* p = a) { for (int i = 0; i < a.Length; ++i) // treat as linear { p[i] = i; } } } for (int i = 0; i < 2; ++i) { for (int j = 0; j < 3; ++j) { for (int k = 0; k < 4; ++k) { Console.Write($"[{i},{j},{k}] = {a[i,j,k],2} "); } Console.WriteLine(); } } } }
который создает выходные данные:
[0,0,0] = 0 [0,0,1] = 1 [0,0,2] = 2 [0,0,3] = 3 [0,1,0] = 4 [0,1,1] = 5 [0,1,2] = 6 [0,1,3] = 7 [0,2,0] = 8 [0,2,1] = 9 [0,2,2] = 10 [0,2,3] = 11 [1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15 [1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19 [1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23
пример конца
Пример. В следующем коде
class Test { unsafe static void Fill(int* p, int count, int value) { for (; count != 0; count--) { *p++ = value; } } static void Main() { int[] a = new int[100]; unsafe { fixed (int* p = a) Fill(p, 100, -1); } } }
fixed
Оператор используется для исправления массива, чтобы его адрес можно было передать методу, который принимает указатель.пример конца
char*
Значение, созданное путем исправления строкового экземпляра, всегда указывает на строку, завершающую значение NULL. В фиксированной инструкции, которая получает указатель p
на экземпляр s
строки, значения указателя, начиная от p
p + s.Length ‑ 1
представления адресов символов в строке, а значение p + s.Length
указателя всегда указывает на пустой символ (символ со значением "\0").
Пример:
class Test { static string name = "xx"; unsafe static void F(char* p) { for (int i = 0; p[i] != '\0'; ++i) { System.Console.WriteLine(p[i]); } } static void Main() { unsafe { fixed (char* p = name) F(p); fixed (char* p = "xx") F(p); } } }
пример конца
Пример. В следующем коде показан fixed_pointer_initializer с выражением типа, отличного от array_type или
string
:public class C { private int _value; public C(int value) => _value = value; public ref int GetPinnableReference() => ref _value; } public class Test { unsafe private static void Main() { C c = new C(10); fixed (int* p = c) { // ... } } }
Тип
C
имеет доступныйGetPinnableReference
метод с правильной подписью. В инструкцииfixed
возвращаемый из этого метода при вызовеref int
используется для инициализации указателяc
int*
.p
пример конца
Изменение объектов управляемого типа с помощью фиксированных указателей может привести к неопределенному поведению.
Примечание. Например, поскольку строки неизменяемы, это ответственность программиста, чтобы гарантировать, что символы, на которые ссылается указатель на фиксированную строку, не изменяются. конечная заметка
Примечание. Автоматическое завершение строк с пустым значением особенно удобно при вызове внешних API, ожидающих строк "C-style". Обратите внимание, что экземпляр строки может содержать пустые символы. Если такие символы null присутствуют, строка будет усечена при обработке как завершаемой
char*
значением NULL. конечная заметка
Буферы фиксированного размера 23.8
23.8.1 Общие
Буферы фиксированного размера используются для объявления встроенных массивов C в виде элементов структур и в основном полезны для взаимодействия с неуправляемыми API.
Объявления буфера фиксированного размера 23.8.2
Буфер фиксированного размера — это элемент, представляющий хранилище для буфера фиксированной длины переменных заданного типа. Объявление буфера фиксированного размера представляет один или несколько буферов фиксированного размера заданного типа элемента.
Примечание. Как и массив, буфер фиксированного размера можно рассматривать как содержащий элементы. Таким образом, тип элемента термина, определенный для массива, также используется с буфером фиксированного размера. конечная заметка
Буферы фиксированного размера разрешены только в объявлениях структур и могут возникать только в небезопасных контекстах (§23.2).
fixed_size_buffer_declaration
: attributes? fixed_size_buffer_modifier* 'fixed' buffer_element_type
fixed_size_buffer_declarators ';'
;
fixed_size_buffer_modifier
: 'new'
| 'public'
| 'internal'
| 'private'
| 'unsafe'
;
buffer_element_type
: type
;
fixed_size_buffer_declarators
: fixed_size_buffer_declarator (',' fixed_size_buffer_declarator)*
;
fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;
Объявление буфера фиксированного размера может включать набор атрибутов (§22), модификатор (§15.3.5), модификаторы специальных возможностей, соответствующие любому из объявленных специальных возможностей для членов структуры (§16.4.3) и new
модификатор (§23.2).unsafe
Атрибуты и модификаторы применяются ко всем элементам, объявленным объявлением буфера фиксированного размера. Это ошибка для одного модификатора несколько раз в объявлении буфера фиксированного размера.
Объявление буфера фиксированного static
размера не допускается включать модификатор.
Тип буферного элемента объявления буфера фиксированного размера указывает тип элемента буферов, представленных объявлением. Тип элемента буфера должен быть одним из предопределенных типов sbyte
, byte
short
ushort
int
uint
long
ulong
char
float
double
или .bool
За типом буферного элемента следует список деклараторов буфера фиксированного размера, каждый из которых представляет новый элемент. Декларатор буфера фиксированного размера состоит из идентификатора, именующего член, за которым следует константное выражение, заключенное в [
маркеры.]
Константное выражение обозначает количество элементов в элементе, введенном этим декларатором буфера фиксированного размера. Тип константного выражения должен быть неявно преобразован в тип int
, и значение должно быть ненулевым положительным целым числом.
Элементы буфера фиксированного размера должны быть последовательно размещены в памяти.
Объявление буфера фиксированного размера, объявляющее несколько буферов фиксированного размера, эквивалентно нескольким объявлениям одного объявления буфера фиксированного размера с одинаковыми атрибутами и типами элементов.
Пример:
unsafe struct A { public fixed int x[5], y[10], z[100]; }
эквивалентно правилу
unsafe struct A { public fixed int x[5]; public fixed int y[10]; public fixed int z[100]; }
пример конца
Буферы фиксированного размера 23.8.3 в выражениях
Поиск элементов (§12.5) элемента буфера фиксированного размера выполняется точно так же, как поиск элемента поля.
Буфер фиксированного размера можно ссылаться в выражении с помощью simple_name (§12.8.4), member_access (§12.8.7) или element_access (§12.8.12).
Если элемент буфера фиксированного размера ссылается на простое имя, эффект совпадает с доступом к члену формы this.I
, где I
является элемент буфера фиксированного размера.
В доступе к члену формы E.I
E.
, где может быть неявным this.
, если E
тип структуры и подстановка I
элемента в этом типе структуры определяет член фиксированного размера, то E.I
вычисляется и классифицируется следующим образом:
- Если выражение
E.I
не происходит в небезопасном контексте, возникает ошибка во время компиляции. - Если
E
классифицируется как значение, возникает ошибка во время компиляции. - В противном случае, если
E
это перемещаемая переменная (§23.4), то:- Если выражение является fixed_pointer_initializer (§23.7
I
E
- В противном случае, если выражение
E.I
является primary_no_array_creation_expression (§12.8.12.1P[J]
- В противном случае возникает ошибка во время компиляции.
- Если выражение является fixed_pointer_initializer (§23.7
-
E
В противном случае ссылается на фиксированную переменную, а результат выражения — указатель на первый элемент элементаI
буфера фиксированного размера вE
. Результатом является типS*
, где S является типомI
элемента и классифицируется как значение.
К последующим элементам буфера фиксированного размера можно получить доступ с помощью операций указателя из первого элемента. В отличие от доступа к массивам, доступ к элементам буфера фиксированного размера является небезопасной операцией и не проверяет диапазон.
Пример. В следующем объявлении и использовании структуры с членом буфера фиксированного размера.
unsafe struct Font { public int size; public fixed char name[32]; } class Test { unsafe static void PutString(string s, char* buffer, int bufSize) { int len = s.Length; if (len > bufSize) { len = bufSize; } for (int i = 0; i < len; i++) { buffer[i] = s[i]; } for (int i = len; i < bufSize; i++) { buffer[i] = (char)0; } } unsafe static void Main() { Font f; f.size = 10; PutString("Times New Roman", f.name, 32); } }
пример конца
23.8.4 Определенное задание
Буферы фиксированного размера не подлежат определенной проверке назначения (§9.4), а члены буфера фиксированного размера игнорируются в целях проверки определенного назначения переменных типа структуры.
Если самая внешняя содержащая переменную структуры элемента буфера фиксированного размера является статической переменной, переменной экземпляра экземпляра класса или элемента массива, элементы буфера фиксированного размера автоматически инициализированы в значения по умолчанию (§9.3). Во всех остальных случаях начальное содержимое буфера фиксированного размера не определено.
Выделение стека 23.9
Общие сведения об операторе см. в разделе stackalloc
. Здесь рассматривается возможность этого оператора привести к указателю.
Когда stackalloc_expression используется в качестве инициализирующего выражения local_variable_declaration (§13.6.2), где local_variable_type является либо типом указателя (§23.3), либо выводится (var
), результатом stackalloc_expression является указатель типа T*
, где T
— это unmanaged_type для stackalloc_expression. В этом случае результатом является указатель на начало выделенного блока.
Пример:
unsafe { // Memory uninitialized int* p1 = stackalloc int[3]; // Memory initialized int* p2 = stackalloc int[3] { -10, -15, -30 }; // Type int is inferred int* p3 = stackalloc[] { 11, 12, 13 }; // Can't infer context, so pointer result assumed var p4 = stackalloc[] { 11, 12, 13 }; // Error; no conversion exists long* p5 = stackalloc[] { 11, 12, 13 }; // Converts 11 and 13, and returns long* long* p6 = stackalloc[] { 11, 12L, 13 }; // Converts all and returns long* long* p7 = stackalloc long[] { 11, 12, 13 }; }
пример конца
В отличие от доступа к массивам или stackalloc
Span<T>
"блокам типа", доступ к элементам stackalloc
"блока указателя" является небезопасной операцией и не проверяет диапазон.
Пример. В следующем коде
class Test { static string IntToString(int value) { if (value == int.MinValue) { return "-2147483648"; } int n = value >= 0 ? value : -value; unsafe { char* buffer = stackalloc char[16]; char* p = buffer + 16; do { *--p = (char)(n % 10 + '0'); n /= 10; } while (n != 0); if (value < 0) { *--p = '-'; } return new string(p, 0, (int)(buffer + 16 - p)); } } static void Main() { Console.WriteLine(IntToString(12345)); Console.WriteLine(IntToString(-999)); } }
stackalloc
Выражение используется в методеIntToString
для выделения буфера из 16 символов в стеке. Буфер автоматически удаляется при возврате метода.Обратите внимание, однако, что
IntToString
можно переписать в безопасном режиме, то есть без использования указателей, как показано ниже.class Test { static string IntToString(int value) { if (value == int.MinValue) { return "-2147483648"; } int n = value >= 0 ? value : -value; Span<char> buffer = stackalloc char[16]; int idx = 16; do { buffer[--idx] = (char)(n % 10 + '0'); n /= 10; } while (n != 0); if (value < 0) { buffer[--idx] = '-'; } return buffer.Slice(idx).ToString(); } }
пример конца
Конец условно нормативных текстов.
ECMA C# draft specification