Книга: ЯЗЫК ПРОГРАММИРОВАНИЯ С# 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 позволяет строить операторы, закрепляющие ссылочную переменную в памяти, чтобы ее адрес оставался постоянным на время выполнения оператора. Для гарантии безопасности обязательно фиксируйте ссылки при взаимодействии со ссылочными типами из небезопасного контекста программного кода.

Оглавление книги


Генерация: 1.813. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз