Ссылки и указатели. Часть 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# такая сложная, поскольку это часто бывает очень полезно. При написании компилятора мы часто сталкиваемся с ситуацией, когда было бы здорово передать расположение переменной, которая является внутренним указателем массива, сравнить эти переменные и т.д. И нам для этого приходится использовать небезопасный код и фиксировать массив? К счастью нет!
В следующий раз: Как получить безопасный внутренний указатель на массив, который можно более или менее рассматривать как указатель.
Comments
- Anonymous
April 03, 2011
+cite Вы можете выполнять арифметические указатели над указателями... проверяет содержимое переменное на null... -cite ... арифметические операции над ... переменной на null