Книга: ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2005 И ПЛАТФОРМА .NET 2.0. 3-е издание
Работа с типами указателя
Разделы на этой странице:
Работа с типами указателя
Из главы 8 вы узнали, что платформа .NET определяет две главные категории данных: типы, характеризуемые значениями, и типы, характеризуемые ссылками (ссылочные типы). Однако, справедливости ради., следует сказать, что имеется и третья категория: это типы указателя. Для работы с типами указателя предлагаются специальные операции и ключевые слова, с помощью которых можно "обойти" схему управления памятью CLR и "взять управление в свои руки" (табл. 9.3).
Таблица 9.3. Операции и ключевые слова C# для работы с указателями
Операция или ключевое слово | Описание |
---|---|
* | Используется для создания переменной указателя (т.е. переменной, представляющей непосредственно адресуемую точку в памяти). Как и в C(++), тот же знак используется для операции разыменования указателя (т.е, для операции, которая возвратит значение, размещенное по адресу, указанному операндом) |
& | Используется для получения адреса переменной в памяти |
–› | Используется для доступа к полям типа, представленным указателем (небезопасная версия операции, обозначаемой в C# точкой) |
[] | Операция [] (в небезопасном контексте) позволяет индексировать элемент, на который указывает переменная указателя. (Обратите внимание на аналогию между переменной указателя и операцией [] в C(++).) |
++, -- | В небезопасном контексте к типам указателя могут применяться операции приращения и отрицательного приращения |
+, - | В небезопасном контексте к типам указателя могут применяться операции сложения и вычитания |
==, !=, <, >, <=, >= | В небезопасном контексте к типам указателя могут применяться операции сравнения и проверки на тождественность |
stackalloc | В небезопасном контексте можно использовать ключевое слово stackalloc, чтобы размещать массивы C# в стеке |
fixed | В небезопасном контексте можно использовать ключевое слово fixed, временно фиксирующее переменную с тем, чтобы можно было найти ее адрес |
Перед рассмотрением деталей позвольте заметить, что необходимость в использовании типов указателя возникает очень редко, если она возникает вообще. Хотя C# и позволяет "спуститься" на уровень манипуляций с указателями, следует понимать, что среда выполнения .NET не имеет никакого представления о ваших намерениях. Поэтому если вы ошибетесь в направлении указателя, то за последствия будете отвечать сами. Если учитывать это, то когда же на самом деле возникает необходимость использования типов указателя? Есть две стандартные ситуации.
• Вы стремитесь оптимизировать работу определенных частей своего приложения путем непосредственной манипуляции памятью вне пределов управления CLR.
• Вы хотите использовать методы C-библиотеки *.dll или COM-сервера, требующие ввода указателей в виде параметров.
Если вы решите использовать указанную возможность языка C#, необходимо информировать csc.exe об этих намерениях, указав разрешение для проекта поддерживать "небезопасный программный код". Чтобы сделать это с командной строки компилятора C# (csc.exe), просто укажите в качестве аргумента флаг /unsafe. В Visual Studio 2005 вы должны перейти на страницу свойств проекта и активизировать опцию Allow Unsafe Code (Разрешать использование небезопасного программного кода) на вкладке Build (рис. 9.4).
Рис. 9.4. Разрешение небезопасного программного кода Visual Studio 2005
Ключевое слово unsafe
В следующих примерах предполагается, что вы имеете опыт работы с указателями в C(++). Если это не так, не слишком отчаивайтесь. Подчеркнем еще раз, что создание небезопасного программного кода не является типичной задачей в большинстве приложений .NET. Если вы хотите работать с указателями в C#, то должны специально объявить блок программного кода "небезопасным", используя для этого ключевое слово unsafe (как вы можете догадаться сами, весь программный код, не обозначенный ключевым unsafe, автоматически считается "безопасным").
unsafe {
// Операторы для работы с указателями.
}
Кроме объявления контекста небезопасного программного кода, вы можете строить "небезопасные" структуры, классы, члены типов и параметры. Вот несколько примеров, на которые следует обратить внимание.
// Вся эта структура является 'небезопасной'
// и может использоваться только в небезопасном контексте.
public unsafe struct Node {
public int Value;
public Node* Left;
public Node* Right;
}
// Эта структура является безопасной, но члены Node* – нет.
// Строго говоря, получить доступ к 'Value' извне небезопасного
// контекста можно, а к 'Left' и 'Right' - нет.
public struct Node {
public int Value;
// К этим элементам можно получить доступ только
// в небезопасном контексте!
public unsafe Node* Left;
public unsafe Node* Right;
}
Методы (как статические, так и уровня экземпляра) тоже можно обозначать, как небезопасные. Предположим, например, вы знаете, что некоторый статический метод использует логику указателей. Чтобы гарантировать вызов данного метода только в небезопасном контексте, можно определить метод так, как показано ниже.
unsafe public static void SomeUnsafeCode() {
// Операторы для работы о указателями.
}
В такой конфигурации требуется, чтобы вызывающая сторона обращалась к SomeUnsafeCode() так.
static void Main(string[] args) {
unsafe {
SomeUnsafeCode();
}
}
Если же не обязательно, чтобы вызывающая сторона делала вызов в небезопасном контексте, то можно не указывать ключевое слово unsafe в методе SomeUnsafeCode() и записать следующее:
public static void SomeUnsafeCode() {
unsafe {
// Операторы для работы с указателями.
}
}
что должно упростить вызов:
static void Main(string[] args) {
SomeUnsafeCode();
}
Работа с операциями * и &
После создания небезопасного контекста вы можете строить указатели на типы с помощью операции * и получать адреса заданных указателей с помощью операции &. В C# операция * применяется только к соответствующему типу, а не как префикс ко всем именам переменных указателя. Например, в следующем фрагменте программного кода объявляются две переменные типа int* (указатель на целое).
// Нет! В C# это некорректно!
int *pi, *pj;
// Да! Это в C# правильно.
int* pi, pj;
Рассмотрим следующий пример.
unsafe {
int myInt;
// Определения указателя типа int
// и присваивание ему адреса myInt.
int* ptrToMyInt = &myInt;
// Присваивание значения myInt
// с помощью разыменования указателя.
*ptrToMyInt = 123;
// Печать статистики.
Console.WriteLine("Значение myInt {0}", myInt);
Console.WriteLine("Адрес myInt {0:X}", (int)&ptrToMyInt);
}
Небезопасная (и безопасная) функция Swap
Конечно, объявление указателей на локальные переменные с помощью просто-то присваивания им значений (как в предыдущем примере) никогда не требуется. Чтобы привести более полезный пример небезопасного программного кода, предположим, что вы хотите создать функцию обмена, используя арифметику указателей.
unsafe public static void UnsafeSwap(int* i, int* j) {
int temp = *i;
*i = *j;
*j = temp;
}
Очень похоже на C, не так ли? Однако с учетом знаний, полученных из главы 3, вы должны знать, что можно записать следующую безопасную версию алгоритма обмена, используя ключевое слово C# ref.
public static void SafeSwap(ref int i, ref int j)
int temp = i;
i = j;
j = temp;
}
Функциональные возможности каждого из этих методов идентичны, и это еще раз подтверждает, что работа напрямую с указателями в C# требуется редко. Ниже показана логика вызова,
static void Main(string[] args) {
Console.WriteLine(*** Вызов метода с небезопасным кодом ***");
// Значения для обмена.
int i = 10, i = 20;
// 'Безопасный' обмен значениями.
Console.WriteLine("n***** Безопасный обмен *****");
Cоnsоle.WriteLine("Значения до обмена: i = {0}, j = {1}", i, j);
SafeSwap(ref 1, ref j);
Console.WriteLine("Значения после обмена: i = {0}, j = {l}", i, j);
// 'Небезопасный' обмен значениями.
Console.WriteLine("n***** Небезопасный обмен *****");
Console.WriteLine("Значения до обмена: i = {0}, j = {1}", i, j);
unsafe { UnsafeSwap(&i, &j); }
Console.WriteLine("Значения после обмена: i = {0}, j = {1}", i, j);
Console.ReadLine();
}
Доступ к полям через указатели (операция -›)
Теперь предположим, что у нас определена структура Point и мы хотим объявить указатель на тип Point. Как и в C(++), для вызова методов или получения доступа к полям типа указателя необходимо использовать операцию доступа к полю указателя (-›). Как уже упоминалось в табл. 9.3, это небезопасная версия стандартной (безопасной) операции, обозначаемой точкой (.). Фактически, используя операцию разыменования указателя (*). можно снять косвенность указателя, чтобы (снова) вернуться к применению нотации, обозначаемой точкой. Рассмотрите следующий программный код.
struct Point {
public int x;
public int y;
public override string ToString() { return string.Format ("({0}, {1})", x, y); }
}
static void Main(string[] args) {
// Доступ к членам через указатели.
unsafe {
Point point;
Point* p =&point;
p-›x = 100;
p-›y = 200;
Console.WriteLine(p-›ToString());
}
// Доступ к членам через разыменование указателей.
unsafe {
Point point;
Point* p =&point;
(*p).x = 100;
(*p).y = 200;
Console.WriteLine((*p).ToString());
}
}
Ключевое слово stackalloc
В небезопасном контексте может понадобиться объявление локальной переменной, размещаемой непосредственно в памяти стека вызовов (и таким образом не подлежащей "утилизации" при сборке мусора .NET). Чтобы сделать такое объявление, в C# предлагается ключевое слово stackalloc являющееся C#-эквивалентом функции alloca из библиотеки времени выполнения C. Вот простой пример.
unsafe {
char* p = stackalloc char[256];
for (int k = 0; k ‹ 256; k++) p[k] = (char)k;
}
Фиксация типа с помощью ключевого слова fixed
Как показывает предыдущий пример, задачу размещения элемента в памяти в рамках небезопасного контекста можно упростить с помощью ключевого слова stackalloc. В силу самой природы этой операции соответствующая память очищается, как только происходит возврат из метода размещения (поскольку память выбирается из стека). Но рассмотрим более сложный пример. В процессе обсуждения операции -› вы создали характеризуемый значением тип Point. Подобно всем типам, характеризуемым значениями, выделенная для него память в стеке освобождается сразу же после исчезновения контекста выполнения. Теперь, для примера, предположим что тип Point был определен как ссылочный тип.
class Point { //‹= Теперь это класс!
public int x;
public int у;
public override string ToString() { return string.Format("({0}, {1})", x, y); }
}
Вы хорошо знаете о том, что если вызывающая сторона объявляет переменную типа Point, для нее выделяется динамическая память, являющаяся объектом для сборки мусора. Тогда возникает вопрос: что произойдет, если небезопасный контекст попытается взаимодействовать с соответствующим объектом (или любым другим объектом в динамической памяти)? Поскольку сборка мусора может начаться в любой момент, представьте себе всю болезненность доступа к членам Point, когда идет очистка динамической памяти. Теоретически возможно, что небезопасный контекст будет пытаться взаимодействовать с членом, который больше не доступен или занимает новое положение в динамической памяти, "пережив" очередную генерацию чистки (и это, очевидно, является проблемой).
Чтобы блокировать переменную ссылочного типа в памяти из небезопасного контекста, в C# предлагается ключевое слово fixed. Оператор fixed устанавливает указатель на управляемый тип и "закрепляет" переменную на время выполнения оператора. Без ключевого слова fixed в применении указателей на управляемые переменные было бы мало смысла, поскольку в результате сборки мусора. такие переменные могут перемещаться непредсказуемым образом. (На самом деле компилятор C# вообще не позволит установить указатель на управляемую переменную, если в операторе не используется ключевое слово fixed.)
Так что, если вы создадите тип Point (сейчас переопределенный, как класс) и захотите взаимодействовать с его членами, то должны записать следующий программный код (иначе возникнет ошибка компиляции).
unsafe public static void Main() {
point pt = new Point();
pt.x = 5;
pt.y = 6;
// Фиксация pt, чтобы не допустить перемещения
// или удаления при сборке мусора.
fixed (int* p =&pt.x) {
// Переменная int* используется здесь.
}
// Теперь pt не зафиксирована и может быть убрана
// сборщиком мусора.
Console.WriteLine("Значение Point: {0}", pt);
}
В сущности, ключевое слово fixed позволяет строить операторы, закрепляющие ссылочную переменную в памяти, чтобы ее адрес оставался постоянным на время выполнения оператора. Для гарантии безопасности обязательно фиксируйте ссылки при взаимодействии со ссылочными типами из небезопасного контекста программного кода.
- Ключевые слова C#, предназначенные для более сложных конструкций
- Практическая работа 53. Запуск Access. Работа с объектами базы данных
- Лекция 15. Работа с базами данных
- Работа с ресурсами локальной сети
- Эффективная работа с временными файлами сортировки
- Ускоренная работа с индексами
- HR-брендинг: Работа с поколением Y, новые инструменты для коммуникации, развитие корпоративной культуры и еще 9 эффектив...
- Безопасная работа с внешними таблицами
- Работа со строками
- Использование CAST() с типами дата
- 9.2. Работа прокси-сервера
- 3. Заработок для фотографов: заработать на фото – сайты фотобанков