Перед рассмотрением деталей позвольте заметить, что необходимость в использовании типов указателя возникает очень редко, если она возникает вообще. Хотя 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];