ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2005 И ПЛАТФОРМА .NET 2.0. 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 позволяет строить операторы, закрепляющие ссылочную переменную в памяти, чтобы ее адрес оставался постоянным на время выполнения оператора. Для гарантии безопасности обязательно фиксируйте ссылки при взаимодействии со ссылочными типами из небезопасного контекста программного кода.
Ключевое слово sizeof
В заключение обсуждения вопросов, связанных с небезопасным контекстом в C#, рассмотрим ключевое слово sizeof. Как и в C(++), ключевое слово C# sizeof используется для того, чтобы выяснить размер в байтах типа, характеризуемого значениями (но не ссылочного типа), и это ключевое слово может использоваться только в рамках небезопасного контекста. Очевидно, что указанная возможность может оказаться полезной при взаимодействии с неуправляемыми API, созданными на базе C. Использовать ее очень просто.
unsafe {
Console.WriteLine("Длина short равна {0}.", sizeof(short));
Console.WriteLine("Длина int равна {0}.", sizeof(int));
Console.WriteLine("Длина long равна {0}.", sizeof(long));
}
Поскольку sizeof может оценить число байтов для любого элемента, производного от System.ValueType, можно получать размеры пользовательских структур. Допустим, мы определили следующую структуру.
struct MyValueType {
public short s;
public int i;
public long l;
}
Тогда ее размеры можно выяснить так.
unsafe {
Console.WriteLine("Длина short равна {0}.", sizeof(short));
Console.WriteLine("Длина int равна {0}.", sizeof(int));
Console.WriteLine("Длина long равна {0}.", sizeof(long));
Console.WriteLine("Длина MyValueType равна {0}."/ sizeof(MyValueType));
}
Исходный код. Проект UnsafeCode размещен в подкаталоге, соответствующем главе 9.
Директивы препроцессора C#
Подобно многим другим языкам из семейства C, в C# поддерживаются различные символы, позволяющие влиять на процесс компиляции. Перед рассмотрением директив препроцессора C# согласуем соответствующую терминологию. Термин "директива препроцессора C#" не вполне точен. Фактически этот термин используется только для согласованности с языками программирования C и C++. В C# нет отдельного шага препроцессора. Директивы препроцессора в C# являются составной частью процесса лексического анализа компилятора.
Так или иначе, синтаксис директив препроцессора C# очень похож на синтаксис соответствующих директив остальных членов семейства C в том, что эти директивы всегда имеют префикс, обозначенный знаком "диез" (#). В табл. 9.4 описаны некоторые из наиболее часто используемых директив (подробности можно найти в документации .NET Framework 2.0 SDK).
Таблица 9.4. Типичные директивы препроцессора C#
Директивы Описание #region, #endregion Используются для обозначения разделов стягиваемого исходного кода #define, #undef Используются для определения и отмены определения символов условной компиляции #if, #elif, #else, #endif Используются для условного пропуска разделов исходного кода (на основе указанных символов компиляции)Разделы программного кода
Возможно, одной из самых полезных директив препроцессора являются #region и #endregion. Используя эти признаки, вы указываете блок программного кода, который можно скрыть от просмотра и идентифицировать информирующим текстовым маркером. Использование разделов программного кода может упростить обслуживание больших файлов *.cs. Можно, например, создать один раздел для конструкторов типа, другой – для свойств и т.д.
class Car {
private string petName;
private int currSp;
#region Constructors
public Car() {…}
public Car Car(int currSp, string petName) {…}
#endregion
#region Properties
public int Speed {…}
public string Name {…}
#endregion
}
При помещений указателя мыши на маркер свернутого раздела вы получите снимок программного кода, спрятанного за соответствующим названием (рис. 9.5).
Рис. 9.5. Разделы программного кода за работой
Условная компиляция
Другой пакет директив препроцессора (#if, #elif, #else, #endif) позволяет выполнить компиляцию блока программного кода по условию, базируясь на предварительно заданных символах. Классическим вариантом использования этих директив является идентификация блока программного кода, который компилируется только при отладке (а не при окончательной компоновке).