Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
Console.WriteLine("Address of myInt {0:X}", (int)&ptrToMyInt);
// адрес myInt
}
В результате запуска этого метода из блока unsafe вы получите такой вывод:
**** Print Value And Address ****
Value of myInt 123
Address of myInt 90F7E698
Небезопасная (и безопасная) функция обмена
Разумеется, объявлять указатели на локальные переменные, чтобы просто присваивать им значения (как в предыдущем примере), никогда не понадобится и к тому же неудобно. В качестве более практичного примера небезопасного кода предположим, что необходимо построить функцию обмена с использованием арифметики указателей:
unsafe static void UnsafeSwap(int* i, int* j)
{
int temp = *i;
*i = *j;
*j = temp;
}
Очень похоже на язык С, не так ли? Тем не менее, учитывая предшествующую работу, вы должны знать, что можно было бы написать безопасную версию алгоритма обмена с применением ключевого слова ref языка С#:
static void SafeSwap(ref int i, ref int j)
{
int temp = i;
i = j;
j = temp;
}
Функциональность обеих версий метода идентична, доказывая тем самым, что прямые манипуляции указателями в C# не являются обязательными. Ниже показана логика вызова, использующая безопасные операторы верхнего уровня, но с небезопасным контекстом:
Console.WriteLine("***** Calling method with unsafe code *****");
// Значения, подлежащие обмену.
int i = 10, j = 20;
// "Безопасный" обмен значений местами.
Console.WriteLine("n***** Safe swap *****");
Console.WriteLine("Values before safe swap: i = {0}, j = {1}", i, j);
SafeSwap(ref i, ref j);
Console.WriteLine("Values after safe swap: i = {0}, j = {1}", i, j);
// "Небезопасный" обмен значений местами.
Console.WriteLine("n***** Unsafe swap *****");
Console.WriteLine("Values before unsafe swap: i = {0}, j = {1}", i, j);
unsafe { UnsafeSwap(&i, &j); }
Console.WriteLine("Values after unsafe swap: i = {0}, j = {1}", i, j);
Console.ReadLine();
Доступ к полям через указатели (операция ->)
Теперь предположим, что определена простая безопасная структура Point:
struct Point
{
public int x;
public int y;
public override string ToString() => $"({x}, {y})";
}
В случае объявления указателя на тип Point для доступа к открытым членам структуры понадобится применять операцию доступа к полям (имеющую вид ->). Как упоминалось в табл. 11.2, она представляет собой небезопасную версию стандартной (безопасной) операции точки (.). В сущности, используя операцию обращения к указателю (*), можно разыменовывать указатель для применения операции точки. Взгляните на следующий небезопасный метод:
static unsafe void UsePointerToPoint()
{
// Доступ к членам через указатель.
Point;
Point* p = &point;
p->x = 100;
p->y = 200;
Console.WriteLine(p->ToString());
// Доступ к членам через разыменованный указатель.
Point point2;
Point* p2 = &point2;
(*p2).x = 100;
(*p2).y = 200;
Console.WriteLine((*p2).ToString());
}
Ключевое слово stackalloc
В небезопасном контексте может возникнуть необходимость в объявлении локальной переменной, для которой память выделяется непосредственно в стеке вызовов (и потому она не обрабатывается сборщиком мусора .NET Core). Для этого в языке C# предусмотрено ключевое слово stackalloc, которое является эквивалентом функции _аllоса библиотеки времени выполнения С. Вот простой пример:
static unsafe string UnsafeStackAlloc()
{
char* p = stackalloc char[52];
for (int k = 0; k < 52; k++)
{
p[k] = (char)(k + 65)k;
}
return new string(p);
}
Закрепление типа посредством ключевого слова fixed
В предыдущем примере вы видели, что выделение фрагмента памяти внутри небезопасного контекста может делаться с помощью ключевого слова stackalloc. Из-за природы операции stackalloc выделенная память очищается, как только выделяющий ее метод возвращает управление (т.к. память распределена в стеке). Однако рассмотрим более сложный пример. Во время исследования операции -> создавался тип значения по имени Point. Как и все типы значений, выделяемая его экземплярам память исчезает из стека по окончании выполнения. Предположим, что тип Point взамен определен как ссылочный:
class PointRef // <= Renamed and retyped.
{
public int x;
public int y;
public override string ToString() => $"({x}, {y})";
}
Как вам известно, если в вызывающем коде объявляется переменная типа Point, то память для нее выделяется в куче, поддерживающей сборку мусора. И тут возникает животрепещущий вопрос: а что если небезопасный контекст пожелает взаимодействовать с этим объектом (или любым другим объектом из кучи)? Учитывая, что сборка мусора может произойти в любое время, вы только вообразите, какие проблемы возникнут при обращении к членам Point именно в тот момент, когда происходит реорганизация кучи! Теоретически может случиться так, что небезопасный контекст попытается взаимодействовать с членом, который больше не доступен или был перемещен в другое место кучи после ее очистки с учетом поколений (что является очевидной проблемой).
Для фиксации переменной ссылочного типа в памяти из небезопасного контекста язык C# предлагает ключевое слово fixed. Оператор fixed устанавливает указатель на управляемый тип и "закрепляет" такую переменную на время выполнения кода. Без fixed от указателей на управляемые переменные было бы мало толку, поскольку сборщик мусора может перемещать переменные в памяти непредсказуемым образом. (На самом деле компилятор C# даже не позволит установить указатель на управляемую переменную, если оператор fixed отсутствует.)
Таким образом, если вы создали объект Point и хотите взаимодействовать с его членами, тогда должны написать следующий код (либо иначе получить ошибку на этапе компиляции):
unsafe static void UseAndPinPoint()
{
PointRef pt = new PointRef
{
x = 5,
y = 6
};
// Закрепить указатель pt на месте, чтобы он не мог