Ненадежный код, типы указателей и указатели функций
Большая часть написанного кода C# — «проверяемый безопасно код». Проверяемый безопасно код означает, что средства .NET позволяют проверить, является ли код надежным. Как правило, в безопасном коде не используется прямой доступ к памяти с помощью указателей. В нем также не используется выделение блоков памяти в «сыром» виде. Вместо этого в нем создаются управляемые объекты.
C# поддерживает контекст unsafe , в котором можно писать непроверяемый код. В контексте unsafe в коде можно использовать указатели, выделять и освобождать блоки памяти, а также обращаться к методам с помощью указателей функций. Небезопасный код в C# не обязательно является опасным, просто его безопасность нельзя проверить.
Небезопасный код имеет следующие свойства:
- Методы, типы и блоки кода можно определить как небезопасные.
- В некоторых случаях небезопасный код может увеличить скорость работы приложения, если не проверяются границы массивов.
- Небезопасный код необходим при вызове встроенных стандартных функций, требующих указателей.
- Использование небезопасного кода создает риски для стабильности и безопасности.
- Код, содержащий небезопасные блоки, должен компилироваться с параметром компилятора AllowUnsafeBlocks.
типы указателей
В небезопасном контексте тип может быть не только типом значения или ссылочным типом, но и типом указателя. Объявления типа указателя выполняется одним из следующих способов:
Тип, указанный до * в типе указателя, называется ссылочным типом. Ссылочным типом может быть только неуправляемый тип.
Типы указателей не наследуются от объекта, а типы указателей не преобразуются в object . Кроме того, упаковка-преобразование и распаковка-преобразование не поддерживают указатели. Однако можно выполнять преобразования между различными типами указателей, а также между типами указателей и целочисленными типами.
При объявлении нескольких указателей в одном объявлении знак ( * ) указывается только с базовым типом. Он не используется в качестве префикса для каждого имени указателя. Пример:
Указатель не может указывать на ссылку или на структуру, содержащую ссылки, поскольку ссылка на объект может быть подвергнута сбору мусора, даже если на нее указывает указатель. Сборщик мусора не отслеживает наличие указателей любых типов, указывающих на объекты.
Значением переменной-указателя типа MyType* является адрес переменной типа MyType . Ниже приведены примеры объявлений типов указателей.
- int* p : p — указатель на целое число.
- int** p : p — указатель на указатель на целое число.
- int*[] p : p — одномерный массив указателей на целые числа.
- char* p : p — указатель на тип char.
- void* p : p — указатель на неизвестный тип.
Оператор косвенного обращения указателя * можно использовать для доступа к содержимому, на которое указывает переменная-указатель. В качестве примера рассмотрим следующее объявление:
Выражение *myVariable обозначает переменную int , находящуюся по адресу, содержащемуся в myVariable .
Несколько примеров указателей можно найти в статьях, посвященных оператору fixed . В следующем примере используются ключевое слово unsafe и оператор fixed , а также демонстрируется способ инкрементирования внутреннего указателя. Этот код можно вставить в функцию Main консольного приложения для его запуска. Эти примеры должны быть скомпилированы с заданным параметром AllowUnsafeBlocks.
Для указателя типа void* использовать оператор косвенного обращения нельзя. Однако можно использовать приведение для преобразования указателя типа void в любой другой тип и наоборот.
Указатель может иметь значение null . При применении оператора косвенного обращения к указателю со значением null результат зависит от конкретной реализации.
При передаче указателей между методами может возникнуть неопределенное поведение. Рекомендуется использовать метод, возвращающий указатель в локальную переменную с помощью параметра in , out или ref либо в виде результата функции. Если указатель был задан в фиксированном блоке, переменная, на которую он указывает, больше не может быть фиксированной.
В следующей таблице перечислены операторы, которые можно использовать для указателей в небезопасном контексте.
Оператор Использовать * Косвенное обращение к указателю. -> Доступ к члену структуры через указатель. [] Индексирование указателя. & Получение адреса переменной. ++ и -- Увеличение и уменьшение указателей. + и - Арифметические действия с указателем. == , != , < , > , <= и >= Сравнение указателей. stackalloc Выделение памяти в стеке. Инструкция fixed Временная фиксация переменной, чтобы можно было найти ее адрес.
Дополнительные сведения об операторах, связанных с указателем, см. в разделе Операторы, связанные с указателем.
Любой тип указателя можно неявно преобразовать в тип void* . Любому типу указателя может быть присвоено значение null . Любой тип указателя можно явно преобразовать в любой другой тип указателя с помощью выражения приведения. Можно также преобразовать любой целочисленный тип в тип указателя или преобразовать любой тип указателя в целочисленный тип. Для этих преобразований требуется явным образом использовать приведение.
Например, в следующем примере тип int* преобразуется в тип byte* . Обратите внимание, что указатель указывает на наименьший адресуемый байт переменной. При последовательном увеличении результата до размера int (4 байта) можно отобразить оставшиеся байты переменной.
Буферы фиксированного размера
В языке C# для создания буфера с массивом фиксированного размера в структуре данных можно использовать оператор fixed. Буферы фиксированного размера полезны при написании методов, взаимодействующих с источниками данных, созданными на других языках или платформах. Фиксированный массив может принимать любые атрибуты или модификаторы, допустимые для обычных членов структуры. Единственным ограничением является то, что массив должен иметь тип bool , byte , char , short , int , long , sbyte , ushort , uint , ulong , float или double .
В безопасном коде структура C#, содержащая массив, не содержит элементы массива. Вместо этого в ней присутствуют ссылки на элементы. Вы можете внедрить массив фиксированного размера в структуру, если он используется в блоке небезопасного кода.
Размер следующего объекта struct не зависит от количества элементов в массиве, поскольку pathName представляет собой ссылку:
struct может содержать внедренный массив в небезопасном коде. В приведенном ниже примере массив fixedBuffer имеет фиксированный размер. Для установки указателя на первый элемент используется оператор fixed . С помощью этого указателя осуществляется доступ к элементам массива. Оператор fixed закрепляет поле экземпляра fixedBuffer в определенном месте в памяти.
Размер массива из 128 элементов char составляет 256 байт. В буферах типа char фиксированного размера на один символ всегда приходится по 2 байта независимо от кодировки. Этот размер массива одинаков, даже если буферы char упакованы в методы API или структуры с помощью CharSet = CharSet.Auto или CharSet = CharSet.Ansi . Для получения дополнительной информации см. CharSet.
В предыдущем примере демонстрируется доступ к fixed полям без закрепления, который доступен начиная с C# 7,3.
Еще одним распространенным массивом фиксированного размера является массив bool. Элементы в массиве bool всегда имеют размер в 1 байт. Массивы bool не подходят для создания битовых массивов или буферов.
Буферы фиксированного размера компилируются с помощью класса System.Runtime.CompilerServices.UnsafeValueTypeAttribute, что указывает среде CLR, что тип содержит неуправляемый массив, который может привести к переполнению. Кроме того, в среде CLR для памяти, выделенной с помощью функции stackalloc, автоматически включаются функции обнаружения переполнения буфера. В предыдущем примере показано существование буфера фиксированного размера в unsafe struct .
Созданный компилятором код C# для Buffer помечен с помощью атрибутов, как показано далее.
Буферы фиксированного размера отличаются от обычных массивов указанными ниже особенностями.
- Могут использоваться только в unsafe контексте.
- Могут быть только полями экземпляров структур.
- Всегда являются векторами или одномерными массивами.
- Объявление должно включать длину, например fixed char id[8] . Вы не можете использовать fixed char id[] .
Практическое руководство. Использование указателей для копирования массива байтов
В следующем примере указатели используются для копирования байт из одного массива в другой.
В этом примере применяется ключевое слово unsafe, которое позволяет использовать указатели в методе Copy . Оператор fixed используется для объявления указателей исходного и конечного массивов. Оператор fixed закрепляет расположение исходного и конечного массивов в памяти, чтобы они не перемещались при сборке мусора. Закрепление этих блоков памяти отменяется, когда завершается обработка блока fixed . Поскольку метод Copy в этом примере использует ключевое слово unsafe , он должен быть скомпилирован с параметром компилятора AllowUnsafeBlocks.
В этом примере доступ к элементам обоих массивов выполняется с помощью индексов, а не второго неуправляемого указателя. Объявление указателей pSource и pTarget закрепляет массивы. Эта возможность доступна начиная с C# 7.3.
Указатели функций
В C# имеются типы delegate , позволяющие определить безопасные объекты указателя функции. При вызове делегата создается экземпляр типа, производного от System.Delegate, и вызывается его виртуальный метод Invoke . В этом виртуальном вызове используется инструкция IL callvirt . В критически важных с точки зрения производительности путях к коду использование инструкции IL calli является более эффективным.
Указатель функции можно определить, используя синтаксический элемент delegate* . Вместо того, чтобы создавать экземпляр объекта delegate и вызывать метод Invoke , компилятор вызывает такую функцию, используя инструкцию calli . В следующем коде объявляются два метода, которые используют delegate или delegate* для объединения двух объектов одного типа. В первом методе используется тип делегата System.Func<T1,T2,TResult>. Во втором методе используется объявление delegate* с теми же параметрами и типом возвращаемого значения:
В следующем коде показано, как объявить статическую локальную функцию и вызвать метод UnsafeCombine , используя указатель на эту локальную функцию:
В приведенном выше коде иллюстрируется ряд правил работы с функциями, доступ к которым осуществляется по указателю:
- Указатели функций могут быть объявлены только в контексте unsafe .
- Методы, принимающие в качестве параметра значение типа delegate* (или возвращающие значение типа delegate* ), могут вызываться только в контексте unsafe .
- Оператор & для получения адреса функции допускается только для функций static . (Это правило применяется как к функциям-членам, так и к локальным функциям).
Синтаксис имеет сходства с объявлением типов delegate и использованием указателей. Суффикс * в служебном слове delegate указывает на то, что данное объявление является указателем функции. Знак & при назначении группы методов указателю функции указывает, что операция использует адрес метода.
Для delegate* можно указать соглашение о вызовах, используя ключевые слова managed и unmanaged . Кроме того, соглашение о вызовах можно указать для указателей на функции unmanaged . Примеры для всех этих случаев можно увидеть в объявлениях ниже. В первом объявлении используется соглашение о вызовах managed , которое используется по умолчанию. В следующих четырех используется unmanaged соглашение о вызовах. В каждом из них указано одно из соглашений о вызовах из стандарта ECMA 335: Cdecl , Stdcall , Fastcall или Thiscall . В последнем объявлении используется unmanaged соглашение о вызовах, что дает возможность среде CLR выбрать соглашение о вызовах по умолчанию для платформы. Среда CLR выберет соглашение о вызовах во время выполнения кода.
Дополнительные сведения об указателях функций см. в предложении Указатель функции для C# 9.0.