Ссылки и указатели. Часть 1

Написание кода в языке C# заключается всего лишь в манипулировании значениями. Значение может быть значимого типа (value type), такими как integer или decimal или ссылками на экземпляр ссылочного типа, такими как строки или исключения. Значения, с которыми вы работаете, всегда имеют хранилище, связанное со значением; эти хранилища называются «переменными». Обычно в программе на языке C# вы манипулируете значениями путем описания того, какая переменная вам нужна.

В C# существует три базовых операции, которые можно выполнять с переменными:

  • Чтение значения из переменной
  • Запись значения в переменную
  • Создания синонима переменной

Первые две операции очень простые. Последняя операция осуществляется с помощью ключевых слов “ref” и/или “out”:

void M(ref int x)
{
x = 123;
}
...
int y = 456;
M(ref y);

“ref y” означает – «сделать переменную x синонимом переменной y». (Я бы хотел, чтобы изначально разработчики языка C# выбрали бы ключевое слово “alias” или другое слово, менее сбивающее с толку, чем ref. Поскольку многие программисты на языке C# путают “ref” в “ref int x” со ссылочными типами. Но мы сейчас изменить это уже не можем.) В то время, как внутри метода M, переменная x – это всего лишь еще одно имя переменной y, мы получаем два имени для одного и того же хранилища.

Существует и четвертый тип операции, которую можно выполнять над переменной в языке C#, но она не очень распространена, поскольку требует небезопасного кода. Вы можете получить адрес зафиксированной (fixed) переменной и сохранить этот адрес в указателе.

unsafe void M(int* x)
{
*x = 123;
}
...
int y = 456;
M(&y);

Назначение указателей заключается в манипулировании самой переменной, как данными, а не в манипулировании значением этой переменой. Если x – это указатель, тогда *x – это связанная переменная.

Указатели, конечно, очень похожи на ссылки и, на самом деле, ссылки реализованы в виде особого типа указателя. Однако вы можете делать что-то с указателями, что не можете делать со ссылками. Например, следующий код не выполняет ничего полезного:

int Difference(ref double x, ref double y)
{
return y - x;
}
...
double[] array = whatever;
difference = Difference(ref array[5], ref array[15]);

Этот код не корректен; он вычисляет разницу двух чисел с плавающей точкой и пытается преобразовать этот результат к целочисленному значению. Однако с указателями вы можете определить, насколько далеко в памяти располагаются эти переменные:

unsafe int Difference(double* x, double* y)
{
return y - x;
}
...
double[] array = whatever;
fixed(double* p1 = &array[5])
fixed(double* p2 = &array[15])
difference = Difference(p1, p2); // Расстояние в 10 double

Вы можете выполнять арифметические указатели над указателями, но не можете делать этого со ссылками, поскольку в языке C# нет возможности сказать: «я хочу манипулировать самим хранилищем, а не его содержимым». Однако указатели представляют собой само хранилище; разыменовывание указателя с помощью * дает доступ к переменной, что позволяет получить или установить значение данных, находящихся в хранилище.

Аналогично, вы можете проверять указатели на null, но вы не можете проверять равенство ссылок на null; проверка ref на null всего лишь проверяет содержимое переменное на null; такого понятия, как пустая ссылка (“null ref”) – не существует.

Еще, вы можете рассматривать указатели, как массивы; вы не можете делать этого со ссылками:

unsafe double N(double* x)
{
  return x[10];
}
...
double[] array = whatever;
fixed(double* p1 = &array[5])
  q = N(p1); // возвращает array[15];

Все это, конечно же, очень опасно. Мы заставляем помечать ваш код ключевым словом “unsafe” не просто так; делать нечто подобное – небезопасно. Прямое использование указателей отключает систему безопасности и вы берете ответственность на себя за гарантию того, что все операции над указателями являются разумными. Например, предположим, мы передаем внутренние указатели двух разных массивов в метод Difference. Что произойдет? Разумного результата не будет; нет никакого смысла пытаться получить количество элементов между двумя разными массивами. Этот вопрос имеет смысл только для одного массива. Предположим, в предыдущем коде мы передали адрес array[5] для массива из 7 элементов. Что произойдет при попытке получить пятнадцатый элемент? Управляемая система безопасности выключена, так что вы не получите исключение о выходе за пределы массива с указателями, вы просто получите мусор или завалите приложение.

Более того, обратите внимание, что массив должен быть «зафиксирован» (fixed), до получения внутреннего указателя на него. Фиксация массива говорит сборщику мусора: «кто-то сохранил внутренний указатель на эту штуку; не перемещай его в процессе упаковки кучи, пока он не будет отпущен (unfixed)». Это приводит к множеству проблем. Во-первых, это может нарушить возможность сборщика мусора эффективно управлять памятью, поскольку теперь существует область памяти, которую нельзя перемещать. И, во-вторых, вы снова ответственны за безопасное выполнение некоторых операций; если вы сохраните указатель, а затем разыменуете после завершения оператора fixed, нет никакой гарантии, что массив еще будет в том же самом месте! Вы можете разыменовать его уже после перемещения.

Немного жаль, что работа с внутренними указателями массивов в C# такая сложная, поскольку это часто бывает очень полезно. При написании компилятора мы часто сталкиваемся с ситуацией, когда было бы здорово передать расположение переменной, которая является внутренним указателем массива, сравнить эти переменные и т.д. И нам для этого приходится использовать небезопасный код и фиксировать массив? К счастью нет!

В следующий раз: Как получить безопасный внутренний указатель на массив, который можно более или менее рассматривать как указатель.

Оригинал статьи