Небезопасный код, типы указателей и указатели функций

Большая часть написанного кода C# — это проверенный безопасный код. Безопасный код означает, что средства .NET могут убедиться, что код является безопасным. Как правило, безопасный код не обращается непосредственно к памяти с помощью указателей. Он также не выделяет необработанную память. Вместо этого он создает управляемые объекты.

Справочные документы на языке C#, выпущенные последней версией языка C#. Она также содержит начальную документацию по функциям в общедоступных предварительных версиях для предстоящего языкового выпуска.

Документация определяет любую функцию, впервые представленную в последних трех версиях языка или в текущих общедоступных предварительных версиях.

Подсказка

Чтобы узнать, когда функция впервые появилась в C#, ознакомьтесь со статьей по журналу версий языка C#.

C# также поддерживает unsafe контекст, в котором можно написать неподключаемый код. Небезопасный код не обязательно является опасным; это просто код, безопасность которого не может быть проверена средствами .NET. Небезопасный код используется для вызова собственных функций, требующих указателей, и в некоторых случаях для повышения производительности с помощью прямого доступа к памяти, что позволяет избежать проверок границ массива. Небезопасный код также представляет риски безопасности и стабильности. Чтобы скомпилировать код, содержащий unsafe контекст, добавьте параметр компилятора AllowUnsafeBlocks .

C# определяет две модели для того, что подсчитывается как небезопасный код: исходная модель и обновленная модель безопасности памяти, которая находится в предварительной версии в C# 15 и .NET 11. Сведения о том, как отличаются две модели, см. в разделе "Две модели" для небезопасного кода.

Сведения о рекомендациях по небезопасным кодам в C#см. в разделе "Небезопасные рекомендации по коду".

Две модели для небезопасного кода

C# определяет две модели для небезопасного кода. Модель фактически определяет, какие операции требуют контекста unsafe и как unsafe модификатор на член влияет на вызывающих участников.

  • Исходная небезопасная модель: unsafe контекст охватывает существование признаков указателя. Вы объявляете тип указателя, принимаете адрес переменной, разыменовываете указатель, преобразуете stackalloc выражение в указатель или применяете sizeof к произвольному типу только внутри контекста unsafe . (Выражение stackalloc , назначенное или разрешено в безопасном Span<T>ReadOnlySpan<T> коде.) Модификатор unsafe типа, члена или блока устанавливает этот контекст, но не ставит никаких обязательств для вызывающих. В C# 1.0 представлена эта модель, и она остается стандартной.
  • Обновленная модель безопасности памяти: unsafe контекст охватывает операции, которые обращаются к памяти, а среда выполнения не управляет. Существование указателя не является небезопасным; Разыменовка указателя. Модификатор unsafe на члене становится контрактом, который распространяет обязательство аудита безопасности вызывающей стороне. Эта модель находится в предварительной версии в C# 15 и .NET 11.

В следующей таблице сравниваются операции, требующие контекста в каждой unsafe модели.

Операция Исходная модель Обновленная модель
Объявление типа указателя или адрес с помощью & Требует unsafe Разрешено в безопасном коде
Инструкция fixed Требует unsafe Разрешено в безопасном коде
stackalloc Преобразование выражения в указатель Требует unsafe Разрешено в безопасном коде
Оператор sizeof любого неуправляемого типа Требует unsafe Разрешено в безопасном коде
Косвенное использование указателя (*p), доступ к члену (p->m) или доступ к элементам (p[i]) Требует unsafe Требует unsafe
Вызов указателя функции Требует unsafe Требует unsafe
Доступ к элементам в буфере фиксированного размера Требует unsafe Требует unsafe
Вызов элемента, помеченного unsafe Нет требования вызывающего абонента Требует unsafe

Чтобы попробовать обновленную модель, используйте пакет SDK .NET 11 (в предварительной версии) и задайте для компилятора LangVersion значение preview. Расслабление указателя применяется при компиляции с компилятором C# 15 и preview версией языка. Полное применение, включая обязательства вызывающего абонента и согласие на сборку, по-прежнему находится в процессе разработки. Дополнительные сведения см. в обновленной модели безопасности памяти (предварительная версия).

Исходная небезопасная модель

В исходной модели unsafe ключевое слово устанавливает небезопасный контекст для типа, члена или блока, и этот контекст разблокирует функции указателя, описанные в следующих разделах. Модификатор unsafe изменяет только то, что может сделать помеченный код; он не устанавливает требования к вызывающим абонентам. Чтобы скомпилировать любой из этих примеров, задайте параметр компилятора AllowUnsafeBlocks .

Типы указателей

В небезопасном контексте тип может быть типом указателя, а также типом значения или ссылочным типом. Объявление типа указателя принимает одну из следующих форм:

type* identifier;
void* identifier; //allowed but not recommended

Тип, указанный перед * типом указателя, является ссылочной.

Типы указателей не наследуются от объекта, и преобразования между типами указателей и objectне существуют. Кроме того, упаковка и распаковывание не поддерживают указатели. Однако можно преобразовать между различными типами указателей и между типами указателей и целочисленными типами.

При объявлении нескольких указателей в одном объявлении напишите звездочку (*) вместе только с базовым типом. Он не используется в качестве префикса для каждого имени указателя. Например:

int* p1, p2, p3;   // Ok
int *p1, *p2, *p3;   // Invalid in C#

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

Значение переменной указателя типа MyType* — адрес переменной типа MyType. Ниже приведены примеры объявлений типов указателя:

  • int* p: p — это указатель на целое число.
  • int** p: p — это указатель на целое число.
  • int*[] p: p — это одномерный массив указателей на целые числа.
  • char* p: p — это указатель на символ.
  • void* p: p — это указатель на неизвестный тип.

Оператор косвенного обращения указателя * можно использовать для доступа к содержимому в расположении, на которое указывает переменная указателя. Например, рассмотрим следующее объявление:

int* myVariable;

Выражение *myVariable обозначает переменную int, найденную по адресу, указанному в myVariable.

Существует несколько примеров указателей в статьях по инструкции fixed. В следующем примере используется ключевое слово unsafe и оператор fixed и показано, как увеличить внутренний указатель. Этот код можно вставить в основную функцию консольного приложения, чтобы запустить ее. Эти примеры необходимо скомпилировать с опцией компилятора AllowUnsafeBlocks.

// Normal pointer to an object.
int[] a = [10, 20, 30, 40, 50];
// Must be in unsafe code to use interior pointers.
unsafe
{
    // Must pin object on heap so that it doesn't move while using interior pointers.
    fixed (int* p = &a[0])
    {
        // p is pinned as well as object, so create another pointer to show incrementing it.
        int* p2 = p;
        Console.WriteLine(*p2);
        // Incrementing p2 bumps the pointer by four bytes due to its type ...
        p2 += 1;
        Console.WriteLine(*p2);
        p2 += 1;
        Console.WriteLine(*p2);
        Console.WriteLine("--------");
        Console.WriteLine(*p);
        // Dereferencing p and incrementing changes the value of a[0] ...
        *p += 1;
        Console.WriteLine(*p);
        *p += 1;
        Console.WriteLine(*p);
    }
}

Console.WriteLine("--------");
Console.WriteLine(a[0]);

/*
Output:
10
20
30
--------
10
11
12
--------
12
*/

Не удается применить оператор косвенного обращения к указателю типа void*. Однако можно использовать приведение для преобразования указателя void в любой другой тип указателя, и наоборот.

Указатель может быть null. Применение оператора косвенного обращения к указателю NULL вызывает поведение, определенное реализацией.

Передача указателей между методами может привести к неопределенному поведению. Рассмотрим метод, который возвращает указатель на локальную переменную через in, outили параметр ref или в качестве результата функции. Если указатель был задан в фиксированном блоке, переменная, к которой она указывает, больше не будет исправлена.

В следующей таблице перечислены операторы и выражения, которые могут работать с указателями в небезопасном контексте.

Оператор/Утверждение Использование
* Выполняет непрямление указателя.
-> Обращается к члену структуры с помощью указателя.
[] Индексирует указатель.
& Получает адрес переменной.
++ и -- Приращения и уменьшения указателей.
+ и - Выполняет арифметику указателя.
==, !=, <, >, <=и >= Сравнивает указатели.
stackalloc Выделяет память на стеке.
заявление fixed Временно исправляет переменную, чтобы его адрес можно было найти.

Дополнительные сведения о связанных с указателем операторах см. в .

Любой тип указателя может быть неявно преобразован в тип void*. Любому типу указателя может быть присвоено значение null. Вы можете явно преобразовать любой тип указателя в любой другой тип указателя с помощью выражения приведения. Вы также можете преобразовать любой целочисленный тип в тип указателя или любой тип указателя в целочисленный тип. Для этих преобразований требуется явное приведение.

В следующем примере int* преобразуется в byte*. Обратите внимание, что указатель указывает на наименьший байт переменной. После последовательного увеличения результата до размера int (4 байта) можно отобразить оставшиеся байты переменной.

int number = 1024;

unsafe
{
    // Convert to byte:
    byte* p = (byte*)&number;

    System.Console.Write("The 4 bytes of the integer:");

    // Display the 4 bytes of the int variable:
    for (int i = 0 ; i < sizeof(int) ; ++i)
    {
        System.Console.Write(" {0:X2}", *p);
        // Increment the pointer:
        p++;
    }
    System.Console.WriteLine();
    System.Console.WriteLine($"The value of the integer: {number}");

    /* Output:
        The 4 bytes of the integer: 00 04 00 00
        The value of the integer: 1024
    */
}

Буферы фиксированного размера

Массивы являются ссылочными типами, поэтому в безопасном коде поле структуры, которое является массивом, хранит только ссылку на элементы массива, а не сами элементы. Размер следующих struct элементов не зависит от количества элементов в массиве, так как pathName это ссылка:

public struct PathArray
{
    public char[] pathName;
    private int reserved;
}

Чтобы сохранить содержимое массива внутри самой структуры, используйте fixed ключевое слово для объявления буфера фиксированного размера. Ключевое fixed слово требует контекста unsafe . Буферы фиксированного размера полезны при написании методов, взаимодействующих с источниками данных из других языков или платформ. Буфер фиксированного размера может принимать любые атрибуты или модификаторы, разрешенные для обычных элементов структуры. Единственное ограничение заключается в том, что тип массива должен быть bool, intcharlongsbyteshortbyteuintulongushortfloatили :double

private fixed char name[30];

В следующем примере массив fixedBuffer имеет фиксированный размер. Оператор используетсяfixed для получения указателя на первый элемент, а затем доступа к элементам массива с помощью этого указателя. Оператор fixed закрепляет поле экземпляра fixedBuffer в определенном расположении в памяти:

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

internal unsafe class Example
{
    public Buffer buffer = default;
}

private static void AccessEmbeddedArray()
{
    var example = new Example();

    unsafe
    {
        // Pin the buffer to a fixed location in memory.
        fixed (char* charPtr = example.buffer.fixedBuffer)
        {
            *charPtr = 'A';
        }
        // Access safely through the index:
        char c = example.buffer.fixedBuffer[0];
        Console.WriteLine(c);

        // Modify through the index:
        example.buffer.fixedBuffer[0] = 'B';
        Console.WriteLine(example.buffer.fixedBuffer[0]);
    }
}

Размер массива char из 128 элементов составляет 256 байт. Фиксированный размер буферов char всегда принимает 2 байта на символ независимо от кодировки. Этот размер массива остается одинаковым даже при маршалировании буферов char в методы или структуры API с CharSet = CharSet.Auto или CharSet = CharSet.Ansi. Дополнительные сведения см. в CharSet.

В предыдущем примере показано, как осуществить обращение к полям fixed без закрепления. Другим общим массивом фиксированного размера является массив логических значений . Элементы в массиве bool всегда равны 1 байтам. bool массивы не подходят для создания битовых массивов или буферов.

Буферы фиксированного размера компилируются с помощью System.Runtime.CompilerServices.UnsafeValueTypeAttribute, который указывает общей языковой среде выполнения (CLR), что тип содержит неуправляемый массив, способный к потенциальному переполнению. Память, выделенная с помощью stackalloc , также автоматически включает функции обнаружения переполнения буфера в среде CLR. В предыдущем примере показано, как буфер фиксированного размера может существовать в объекте unsafe struct.

internal unsafe struct Buffer
{
    public fixed char fixedBuffer[128];
}

С# код, сгенерированный компилятором для Buffer, имеет следующую атрибуцию:

internal struct Buffer
{
    [StructLayout(LayoutKind.Sequential, Size = 256)]
    [CompilerGenerated]
    [UnsafeValueType]
    public struct <fixedBuffer>e__FixedBuffer
    {
        public char FixedElementField;
    }

    [FixedBuffer(typeof(char), 128)]
    public <fixedBuffer>e__FixedBuffer fixedBuffer;
}

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

  • Их можно использовать только в контексте unsafe .
  • Они могут быть только полями экземпляров структур.
  • Они всегда векторы или одномерные массивы.
  • Объявление должно содержать длину, например fixed char id[8]. Нельзя использовать fixed char id[].

Указатели функций

C# предоставляет типы delegate для определения безопасных объектов указателя функций. Вызов делегата включает создание экземпляра типа, производного от System.Delegate, и вызов виртуального метода к его Invoke методу. Этот виртуальный вызов использует инструкцию callvirt IL. В критически важных участках кода с точки зрения производительности использование инструкции IL calli более эффективно.

Указатель функции можно определить с помощью синтаксиса delegate* . Компилятор вызывает функцию с помощью calli инструкции, а не создания экземпляра delegate объекта и вызова Invoke. Следующий код объявляет два метода, которые используют delegate или delegate* для объединения двух объектов одного типа. Первый метод использует тип делегата System.Func<T1,T2,TResult>. Второй метод использует объявление delegate* с теми же параметрами и типом возвращаемого значения:

public static T Combine<T>(Func<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

public static unsafe T UnsafeCombine<T>(delegate*<T, T, T> combinator, T left, T right) => 
    combinator(left, right);

В следующем коде показано, как объявить статическую локальную функцию и вызвать UnsafeCombine метод с помощью указателя на эту локальную функцию:

int product = 0;
unsafe
{
    static int localMultiply(int x, int y) => x * y;
    product = UnsafeCombine(&localMultiply, 3, 4);
}

Приведенный выше код иллюстрирует несколько правил для функции, доступной через указатель функции.

  • В контексте unsafe можно объявлять только указатели функций.
  • Методы, которые принимают delegate* (или возвращают) в контекстеdelegate*, можно вызывать unsafeтолько методы.
  • Оператор & для получения адреса функции разрешен только для static функций. Это правило применяется как к функциям-членам, так и к локальным функциям.

Синтаксис имеет параллели с объявлением типов delegate и использованием указателей. Суффикс * на delegate указывает, что объявление является указателем функции . & при назначении группы методов указателю функции указывает, что операция принимает адрес метода.

С помощью ключевых слов и delegate*ключевых слов можно указать соглашение managed о вызовахunmanaged. Кроме того, для указателей функций unmanaged можно указать соглашение о вызовах. В следующих объявлениях показаны примеры каждого из них. В первой декларации используется соглашение о вызовах managed, которое является стандартным. Следующие четыре используют конвенцию вызова unmanaged. Каждый определяет одно из соглашений о вызовах ECMA 335: Cdecl, Stdcall, Fastcallили Thiscall. Последнее объявление использует соглашение о вызовах unmanaged, которое указывает среде CLR выбрать соглашение о вызовах по умолчанию для данной платформы. Общая среда выполнения (CLR) выбирает соглашение о вызовах во время выполнения.

public static unsafe T ManagedCombine<T>(delegate* managed<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T CDeclCombine<T>(delegate* unmanaged[Cdecl]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T StdcallCombine<T>(delegate* unmanaged[Stdcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T FastcallCombine<T>(delegate* unmanaged[Fastcall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T ThiscallCombine<T>(delegate* unmanaged[Thiscall]<T, T, T> combinator, T left, T right) =>
    combinator(left, right);
public static unsafe T UnmanagedCombine<T>(delegate* unmanaged<T, T, T> combinator, T left, T right) =>
    combinator(left, right);

Дополнительные сведения о указателях функций см. в разделе указателей функций спецификации языка C#.

Пример. Использование указателей для копирования массива байтов

В следующем примере указатели используются для копирования байтов из одного массива в другой.

В этом примере используется ключевое unsafe слово, которое позволяет использовать указатели в методе Copy . Инструкция fixed объявляет указатели на исходные и целевые массивы. Инструкция fixedзакрепляет расположение исходных и целевых массивов в памяти, чтобы сборка мусора не перемещала массивы. Блок fixed закрепляет блоки памяти для массивов в области блока. Copy Так как метод в этом примере использует ключевое unsafe слово, его необходимо скомпилировать с помощью параметра компилятора AllowUnsafeBlocks.

Этот пример обращается к элементам обоих массивов с помощью индексов, а не второго неуправляемого указателя. Объявление указателей pSource и pTarget закрепляет массивы.

static unsafe void Copy(byte[] source, int sourceOffset, byte[] target,
    int targetOffset, int count)
{
    // If either array is not instantiated, you cannot complete the copy.
    if ((source == null) || (target == null))
    {
        throw new System.ArgumentException("source or target is null");
    }

    // If either offset, or the number of bytes to copy, is negative, you
    // cannot complete the copy.
    if ((sourceOffset < 0) || (targetOffset < 0) || (count < 0))
    {
        throw new System.ArgumentException("offset or bytes to copy is negative");
    }

    // If the number of bytes from the offset to the end of the array is
    // less than the number of bytes you want to copy, you cannot complete
    // the copy.
    if ((source.Length - sourceOffset < count) ||
        (target.Length - targetOffset < count))
    {
        throw new System.ArgumentException("offset to end of array is less than bytes to be copied");
    }

    // The following fixed statement pins the location of the source and
    // target objects in memory so that they will not be moved by garbage
    // collection.
    fixed (byte* pSource = source, pTarget = target)
    {
        // Copy the specified number of bytes from source to target.
        for (int i = 0; i < count; i++)
        {
            pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
}

static void UnsafeCopyArrays()
{
    // Create two arrays of the same length.
    int length = 100;
    byte[] byteArray1 = new byte[length];
    byte[] byteArray2 = new byte[length];

    // Fill byteArray1 with 0 - 99.
    for (int i = 0; i < length; ++i)
    {
        byteArray1[i] = (byte)i;
    }

    // Display the first 10 elements in byteArray1.
    System.Console.WriteLine("The first 10 elements of the original are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray1[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of byteArray1 to byteArray2.
    Copy(byteArray1, 0, byteArray2, 0, length);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");

    // Copy the contents of the last 10 elements of byteArray1 to the
    // beginning of byteArray2.
    // The offset specifies where the copying begins in the source array.
    int offset = length - 10;
    Copy(byteArray1, offset, byteArray2, 0, length - offset);

    // Display the first 10 elements in the copy, byteArray2.
    System.Console.WriteLine("The first 10 elements of the copy are:");
    for (int i = 0; i < 10; ++i)
    {
        System.Console.Write(byteArray2[i] + " ");
    }
    System.Console.WriteLine("\n");
    /* Output:
        The first 10 elements of the original are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        0 1 2 3 4 5 6 7 8 9

        The first 10 elements of the copy are:
        90 91 92 93 94 95 96 97 98 99
    */
}

Обновленная модель безопасности памяти (предварительная версия)

Important

Обновленная модель безопасности памяти — это предварительная версия функции в C# 15 и .NET 11. Она продолжает развиваться на основе отзывов во время предварительных выпусков. Чтобы попробовать модель, используйте пакет SDK .NET 11 (предварительная версия) и установите для компилятора LangVersion значение preview. Компилятор в .NET 11 Preview 5 реализует расслабление указателя, но пока не применяет обязательства вызывающего абонента, согласие сборки или ключевое safe слово. Полный дизайн см. в спецификации функции безопасности памяти.

Обновленная модель разделяет две вещи, которые исходная модель рассматривается как одна: существование кода указателя и распространение обязательств по безопасности вызывающим абонентам. Маркировка члена unsafe больше не позволяет указателям в своем теле; он делает вызывающий член небезопасным, поэтому каждый вызывающий объект должен либо распространять это обязательство, либо выполнять его за проверенной, безопасно вызываемой границы. Для поддержки этого разделения модель также сужает небезопасный контекст: существование указателя не является небезопасным, только операции, обращаюющиеся к памяти среды выполнения, не управляют. Сужение позволяет хранить, передавать и возвращать указатели в безопасном коде, а также unsafe помечает операции и члены, которые могут на самом деле нарушать безопасность памяти.

Небезопасные члены вызывающего абонента

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

Модификатор unsafe подписи члена больше не устанавливает небезопасный контекст для текста. Две роли разделяются:

  • Модификатор unsafe подписи распространяет обязательство вызывающим.
  • Внутренний unsafe блок определяет операции, которые обращаются к неуправляемой памяти.

В приведенной ниже предварительной версии макет ReadInt32 является вызывающим небезопасным. Сигнатура содержит модификатор, а внутренний unsafeunsafe блок упаковывает деиференс:

// Preview: illustrates the updated model, which the current compiler doesn't fully enforce yet.
public static unsafe int ReadInt32(byte* source)
{
    unsafe
    {
        return *(int*)source;
    }
}

Вызывающий объект упаковывает вызов в свой собственный unsafe блок:

// Preview
unsafe
{
    int value = ReadInt32(buffer);
}

Обновленная модель также ужесточает несколько связанных правил:

  • Модификатор unsafe создает ошибку в объявлении типа, статического конструктора и средства завершения, так как модификатор не имеет вызывающего объекта для информирования.
  • Делегаты не могут быть unsafe, так как делегат имеет типообразную форму.
  • Тип, конструктор которого без параметров unsafe не соответствует ограничению new() .

Операции, требующие небезопасного контекста

Для операций, обращаюющихся к указанной памяти, требуется unsafe контекст:

  • Косвенное использование указателя (*p), доступ к члену указателя (p->member) и доступ к элементу указателя (p[i]).
  • Вызов указателя функции.
  • Доступ к элементам в буфере фиксированного размера.

Следующий пример закрепляет массив без контекста unsafe , но разыменовывает указатель внутри одного:

public static int ReadValue(int[] numbers)
{
    fixed (int* first = numbers)
    {
        // Dereferencing a pointer accesses unmanaged memory, so it still
        // requires an unsafe context.
        unsafe
        {
            return *first;
        }
    }
}

Расслабленные операции

Операции, которые не обращаются к указанной памяти, больше не требуют контекста unsafe :

  • Объявление типа указателя и получение адреса переменной с & помощью оператора.
  • Оператор fixed , закрепляющий переменную.
  • Преобразование stackalloc выражения в указатель.
  • Оператор, sizeof применяемый к любому неуправляемому типу.

В следующем примере создаются и закрепляется указатели без контекста unsafe :

public static void CreatePointer()
{
    int value = 42;
    // Creating a pointer doesn't require an unsafe context.
    int* pointer = &value;
    int** pointerToPointer = &pointer;
}
public static void PinArray(int[] numbers)
{
    // The fixed statement no longer requires an unsafe context.
    fixed (int* first = numbers)
    {
        int* current = first;
    }
}

Эти расслабления применяются всякий раз, когда вы компилируете версию preview языка, независимо от того, выбирает ли сборка обновленные правила безопасности памяти.

Выполнение небезопасных обязательств абонента

Участник, вызывающий небезопасную операцию, имеет два варианта: распространение обязательства или его освобождение.

  • Распространение: помечайте свой собственный член unsafe. Обязательство передается вашим абонентам. Используйте распространение, если вы не можете полностью проверить обязательство самостоятельно.
  • Выгрузка: оставьте подпись члена в безопасности. Проверьте обязательство внутри элемента, как правило, с охранниками среды выполнения, а затем выполните небезопасную операцию в внутреннем unsafe блоке. Элемент, содержащий внутренний unsafe блок, но не помечает собственную подпись unsafe , является небезопасной границей: он превращает небезопасный код в безопасную вызываемую поверхность.

Следующая предварительная версия макета проверяет входные данные с помощью охранника, закрепляет управляемый массив и считывает указатель. Вызывающие люди не нуждаются в контексте unsafe , так как метод выполняет обязательство:

// Preview
public static int SumBytes(byte[] source)
{
    ArgumentNullException.ThrowIfNull(source);

    fixed (byte* first = source)
    {
        unsafe
        {
            // SAFETY: the null check and source.Length bound every read to the pinned array.
            int total = 0;
            for (int i = 0; i < source.Length; i++)
            {
                total += first[i];
            }

            return total;
        }
    }
}

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

Документация по безопасности

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

  • Блок /// <safety> документации над подписью указывает формальный контракт: условия, которые должен удовлетворять вызывающий объект. Анализатор может пометить небезопасный участник, который отсутствует.
  • Комментарий // SAFETY: внутри unsafe блок-записи, почему операция звучит на этом месте, для разработчиков и аудиторов, которые читали тело.

В следующем предварительном макете показаны оба стиля в методе вызывающего небезопасного ReadByte метода:

// Preview
/// <summary>Reads a single byte from unmanaged memory.</summary>
/// <safety>
/// The sum of <paramref name="ptr"/> and <paramref name="offset"/> must address a byte
/// the caller is permitted to read.
/// </safety>
public static unsafe byte ReadByte(IntPtr ptr, int offset)
{
    byte* address = (byte*)ptr;
    unsafe
    {
        // SAFETY: relies on the caller obligation stated in the <safety> block.
        return address[offset];
    }
}

Блок /// <safety> сообщает контракту. Контракт принадлежит в документации, где каждый вызывающий и рецензент видят его.

Небезопасные поля

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

Самым четким случаем является поле, которое содержит собственный указатель. Указатель не объявляет, сколько байтов он обращается к ним как это System.Span<T> делает, поэтому содержащий тип сохраняет эти сведения:

// Preview
public class NativeBuffer
{
    /// <safety>
    /// Null, or points to a buffer of Length bytes.
    /// </safety>
    private unsafe byte* _pointer;

    public int Length { get; }

    public byte ReadAt(int index)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Length);
        unsafe
        {
            // SAFETY: the bounds checks confine the read to the buffer that _pointer addresses.
            return _pointer[index];
        }
    }
}

readonly unsafe Поле связывает контракт со встроенным охранником: unsafe называет инвариантный и readonly предотвращает запись, которая может нарушить его после строительства. Маркировка свойства или события unsafe не делает его резервное поле вызывающим поле небезопасным. В структуре [StructLayout(LayoutKind.Explicit)]помечает каждое поле safe или unsafe.

Безопасное ключевое слово

Обновленная модель добавляет safe контекстное ключевое слово, указывающее, что объявление является звуком, в котором компилятор требует явного выбора.

Член вызывает машинный extern код, поэтому компилятор не может классифицировать его безопасность. В обновленной модели помечается каждое extern объявление, включая частичный LibraryImport метод, safe либо unsafe:

// Preview
[LibraryImport("libc")]
internal static safe partial int getpid();

[LibraryImport("libc", StringMarshalling = StringMarshalling.Utf8)]
internal static unsafe partial nint strlen(byte* str);

getpid не принимает параметров и возвращает примитив, поэтому автор подтверждает, что вызов является безопасным, и вызывающие используют его без церемонии. strlen принимает необработанный указатель на то, что машинный код разыменовывается, поэтому объявление является unsafe и распространяет обязательство вызывающим. Опущение обоих модификаторов является ошибкой, которая заставляет вас принимать решение о безопасности. Поле структуры с явным макетом использует то же правило.

Поведение при выборе и перекрестной сборке

Обновленная модель имеет два независимых коммутатора уровня проекта:

  • Новое свойство согласия включает обновленные правила. При отключении свойства применяются исходные правила. Когда он включен, unsafe член распространяется на вызывающих абонентов, а компилятор записывает выбор в сборку с атрибутом MemorySafetyRulesAttribute .
  • Существующее свойство AllowUnsafeBlocks включает каждый внешний вид ключевого unsafe слова, включая внутренние блоки на сайтах вызовов. По умолчанию он имеет falseзначение , поэтому проект по умолчанию не может вызывать небезопасный API.

Два свойства объединяются следующим образом:

Свойство opt-in AllowUnsafeBlocks Result
Вкл Off (по умолчанию) Самая безопасная конфигурация. Проект использует обновленную модель и не разрешает небезопасный код.
Вкл Вкл Проект использует обновленную модель и разрешает небезопасный код.
Выключено Выключено Исходная модель применяется, и проект не может использовать типы указателей.
Выключено Вкл Исходная модель применяется, и проект может использовать типы указателей.

Если одна сборка применяет обновленные правила к другой, зависит от того, какая сторона выбирает:

  • Обновленный вызывающий объект модели, обновленный вызывающий объект модели: маркеры вызываемого абонента unsafe перемещают метаданные. Вызывающий объект упаковывает каждый вызов вызывающей небезопасной член в блоке unsafe .
  • Обновленный вызывающий объект модели, вызывающий исходную модель: режим совместимости обрабатывает любой член вызываемого объекта с типом указателя в своей подписи как вызывающий небезопасный, поэтому сайт вызова нуждается в заключающем unsafe блоке. Этот режим позволяет api на основе указателя автоматически потерять свое unsafe требование.
  • Вызывающий объект исходной модели, обновленный вызывающий объект модели: исходные правила указателя по-прежнему применяются. Вызывающий небезопасный член, не имеющий типа указателя в своей сигнатуре, становится вызываемым из безопасного кода, так как вызывающий объект исходной модели не может считывать новые маркеры.

Спецификация языка C#

Дополнительные сведения см. в разделе небезопасный код главе спецификации языкаC#.

Сведения о проектировании обновленной модели безопасности памяти см. в спецификации функции безопасности памяти.

См. также