Поделиться через


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.

Выражение с типом указателя нельзя использовать для предоставления значения в member_declarator внутри anonymous_object_creation_expression (§12.8.17.3).

Значение по умолчанию (§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.6).
  • Двоичные + и - операторы могут использоваться для арифметики указателя (§23.6.7).
  • Операторы ==, !=, и <><=>=операторы могут использоваться для сравнения указателей (§23.6.8).
  • Оператор stackalloc может использоваться для выделения памяти из стека вызовов (§23.9).
  • Инструкцию fixed можно использовать для временного исправления переменной, чтобы его адрес можно было получить (§23.7).

23.4 Фиксированные и перемещаемые переменные

Оператор извлечения адреса (§23.6.5) и оператор (fixed§23.7) делят переменные на две категории: фиксированные переменные и перемещаемые переменные.

Фиксированные переменные находятся в местах хранения, не затронутых операцией сборщика мусора. (Примеры фиксированных переменных включают локальные переменные, параметры по значению и переменные, созданные посредством разыменования указателей.) С другой стороны, перемещаемые переменные находятся в хранилищах, которые подвергаются перемещению или удалению сборщиком мусора. (Примеры перемещаемых переменных включают поля в объекты и элементы массивов.)

Оператор & (§23.6.5) позволяет получить адрес фиксированной переменной без ограничений. Однако, поскольку перемещаемая переменная подвергается перемещению или удалению сборщиком мусора, адрес перемещаемой переменной можно получить только с помощью (fixed statement§23.7), и этот адрес остается допустимым только в течение этой fixed инструкции.

Точно говоря, фиксированная переменная является одной из следующих:

Все остальные переменные классифицируются как перемещаемые переменные.

Статическое поле классифицируется как перемещаемая переменная. Кроме того, параметр путем ссылки классифицируется как перемещаемая переменная, даже если аргумент, заданный для параметра, является фиксированной переменной. Наконец, переменная, созданная путем расшифровки указателя, всегда классифицируется как фиксированная переменная.

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, shortushortintuintlongили 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.4) в небезопасном контексте. В массивах указателей разрешены только некоторые преобразования, применяемые к другим типам массивов:

  • Неявное преобразование ссылок (§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.5) из T (типа элемента) в 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 состоит из символа звёздочки (*), за которым следует унарное выражение.`

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 '->' 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 и C++. конечная заметка

23.6.5 Оператор адреса

Выражение "адрес" состоит из амперсанда (&), после которого следует унарное выражение.

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.8.16 и §12.9.6) можно применять к указательным переменным всех типов, кроме void*. Таким образом, для каждого типа T*указателя неявно определены следующие операторы:

T* operator ++(T* x);
T* operator --(T* x);

Операторы создают те же результаты, что x+1 и x-1, соответственно (§23.6.7). Другими словами, для переменной указателя типа 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 типа указателя и выражение 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 является unmanaged_type, и T* неявно преобразуется в тип указателя, указанный в fixed. В этом случае инициализатор вычисляет адрес возвращаемой переменной, и эта переменная гарантированно остается на фиксированном адресе в течение длительности инструкции fixed . Метод GetPinnableReference() может быть использован оператором fixed, если разрешение перегрузки (§12.6.4) создает ровно один член функции, и этот член функции удовлетворяет предыдущим условиям. Метод GetPinnableReference должен возвращать ссылку на адрес, равный нулю, такую как возвращаемая из System.Runtime.CompilerServices.Unsafe.NullRef<T>() когда в System.Runtime.CompilerServices.Unsafe.NullRef<T>() нет данных для закрепления.
  • Simple_name или member_access, ссылающийся на член буфера фиксированного размера перемещаемой переменной, если тип элемента буфера фиксированного размера неявно преобразуется в тип указателя, указанный в инструкцииfixed. В этом случае инициализатор вычисляет указатель на первый элемент буфера фиксированного размера (§23.8.3), а буфер фиксированного размера гарантированно остается в фиксированном адресе в течение длительности инструкции fixed .

Для каждого адреса, вычисленного с помощью fixed_pointer_initializer, оператор fixed гарантирует, что переменная, на которую ссылается адрес, не подлежит перемещению или удалению сборщиком мусора на время выполнения оператора 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 инструкции в приведенном выше примере дают идентичные результаты. Как правило, для экземпляра aa[0] массива указание 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* Значение, созданное путем исправления строкового экземпляра, всегда указывает на строку, завершающуюся нулевым символом. В фиксированной инструкции, которая получает указатель 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 метод с правильной подписью. В инструкции fixedref int, возвращаемый из этого метода при вызове на c, используется для инициализации указателя int*p. конечный пример

Изменение объектов управляемого типа с помощью фиксированных указателей может привести к неопределенному поведению.

Примечание. Например, поскольку строки неизменяемы, это ответственность программиста, чтобы гарантировать, что символы, на которые ссылается указатель на фиксированную строку, не изменяются. конечная заметка

Примечание. Автоматическое добавление нулевого символа в конце строки особенно удобно при вызове внешних API, которые ожидают C-подобные строки. Обратите внимание, что экземпляр строки может содержать нулевые символы. Если такие символы нулевой длины присутствуют, строка будет казаться усеченной при обработке как завершенной нулевым значением char*. конечная заметка

Буферы фиксированного размера 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, byteshortushortintuintlongulongcharfloatdoubleили .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.IE., где this. может быть неявным, если E структурного типа и поиск члена I в этом структурном типе определяет фиксированный размер члена, то E.I вычисляется и классифицируется следующим образом:

  • Если выражение E.I не происходит в небезопасном контексте, возникает ошибка во время компиляции.
  • Если E классифицируется как значение, возникает ошибка во время компиляции.
  • В противном случае, если E это перемещаемая переменная (§23.4), то:
    • Если выражение E.I является fixed_pointer_initializer (§23.7), то результатом выражения будет указатель на первый элемент определенного буферного члена I в E.
    • В противном случае, если выражение E.I является primary_no_array_creation_expression (§12.8.12.1) внутри element_access (§12.8.12) в форме E.I[J], результатом E.I будет указатель, P, на первый элемент член фиксированного размера буфера I в E, и тогда окружающий element_access оценивается как pointer_element_access (§23.6.4) P[J].
    • В противном случае возникает ошибка во время компиляции.
  • 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

См. §12.8.22 для получения общей информации об операторе 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();
    }
}

конечный пример

Конец условно нормативных текстов.