Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Глава 18 Опасный код, указатели и другие темы Глава 18. Опасный код, указатели и другие темы 485 акое название темы обычно вызывает у программистов удивление. Опасный код зачастую включает использование указателей. Код, отмеченный как “опасный”, и собственно указатели позволяют использовать средства языка C# для создания приложений, которые обычно ассоциируются с применением C++, т.е. приложений, которые отличаются высокой производительностью и претендуют на звание системных. Более того, включение “опасного кода” и указателей дает C# такие возможности, которых не достает языку Java. В этой главе рассматриваются ключевые слова, которые в предыдущих главах не употреблялись. Опасный код C# позволяет программистам писать то, что называется “опасный кодом” (unsafe code). Опасный код — это код, который не плохо написан, а код, который не выполняется под полным управлением системы Common Language Runtime (CLR). Как разъяснялось в главе 1, язык C# обычно используется для создания управляемого кода. Однако можно написать и "неуправляемый" код, который не подчиняется тем же средствам управления и ограничениям, налагаемым на управляемый код. Такой код называется "опасным", поскольку невозможно проконтролировать невыполнение им опасных действий. Таким образом, термин опасный не означает, что коду присуща некорректность. Он просто означает возможность выполнения действий, которые не являются предметом управления системы CLR. Если опасный код способен вызвать проблемы, то зачем, спрашивается, вообще создавать такой код? Дело в том, что управляемый код не допускает использование указателей. Если вы знакомы с языками С или C++, то вам должно быть известно, что указатели — это переменные, которые хранят адреса других объектов. Следовательно, указатели в некотором роде подобны ссылкам в C#. Основное различие между ними заключается в том, что указатель может указывать на что угодно в памяти, а ссылка всегда указывает на объект “своего” типа. Но если указатель может указывать на что угодно, возможно неправильное его использование. Кроме того, работая с указателями, можно легко внести в код ошибку, которую будет трудно отыскать. Вот почему C# не поддерживает указатели при создании управляемого кода. Теме не менее указатели существуют, причем для некоторых типов программ (например, системных утилит) они не просто полезны, они — необходимы, и C# позволяет (что поделаешь) программистам создавать их и использовать. Однако все операции с указателями должны быть отмечены как “опасные”, поскольку они выполняются вне управляемого контекста. Объявление и использование указателей в C# происходит аналогично тому, как это делается в C/C++ (если вы знаете, как использовать указатели в C/C++, можете так же работать с ними и в C#). Но помните: особенность C# — создание управляемого кода. Его способность поддерживать неуправляемый код позволяет применять C#-программы к задачам специальной категории. Но такое C#-программирование уже не попадает под определение стандартного. И в самом деле, чтобы скомпилировать неуправляемый код, необходимо использовать опцию компилятора /unsafe Поскольку указатели составляют сердцевину опасного кода, пожалуй, стоит познакомиться с ними поближе. Т 486 Часть I. Язык C# Основы использования указателей Указатели — это переменные, которые хранят адреса других переменных. Например, если x содержит адрес переменной y , то о переменной x говорят, что она “указывает” на y Поскольку указатель указывает на некоторую переменную, значение этой переменной можно получить или изменить посредством указателя. Операции, выполняемые с помощью указателей, часто называют операциями непрямого доступа. Объявление указателя Переменные-указатели (или переменные типа указатель) должны быть объявлены таковыми. Формат объявления переменной-указателя таков: тип * имя_переменной ; Здесь элемент тип означает базовый тип указателя, причем он не должен быть ссылочным. Следовательно, нельзя объявлять указатель на объект класса. Обратите внимание на расположение оператора “звездочка” ( * ). Он стоит после имени типа. Элемент имя_переменной представляет собой имя переменной-указателя. Рассмотрим пример. Чтобы объявить переменную ip указателем на int -значение, используйте следующую инструкцию: int* ip; Для объявления указателя на float -значение используйте такую инструкцию: float* fp; В общем случае использование символа “звездочка” ( * ) в инструкции объявления после имени типа создает тип указателя. Тип данных, на которые будет указывать указатель, определяется его базовым типом. Следовательно, в предыдущих примерах переменную ip можно использовать для указания на int -значение, а переменную fp — на float -значение. Однако помните: не существует реального средства, которое могло бы помешать указателю указывать на “бог-знает-что”. Вот потому-то указатели потенциально опасны. Если к C# вы пришли от C/C++, то должны понять важное различие между способами объявления указателей в C# и C/C++. При объявлении типа указателя в C/C++ оператор “ * ” не распространяется на весь список переменных, участвующих в объявлении. Поэтому в C/C++ при выполнении инструкции int* p, q; объявляется указатель p на int -значение и int -переменная с именем q . Эта инструкция эквивалентна следующим двум объявлениям: int* p; int q; Однако в C# оператор “ * ” распространяется на все объявление, и поэтому при выполнении инструкции int* p, q; создаются два указателя p и q на int -значения. Таким образом, в C# предыдущая инструкция эквивалентна таким двум объявлениям: int* p; int* q; Это важное различие обязательно следует иметь в виду при переводе С/C++-кода на “рельсы” C#. Глава 18. Опасный код, указатели и другие темы 487 Операторы "*" и "&" С указателями используются два оператора: “ * ” и “ & ”. Оператор “ & ” — унарный. Он возвращает адрес памяти, по которому расположен его операнд, (Вспомните: унарный оператор требует только одного операнда.) Например, при выполнении следующего фрагмента кода int* ip; int num = 10; ip = # в переменную ip помещается адрес переменной num . Этот адрес соответствует области во внутренней памяти компьютера, которая принадлежит переменной num . Выполнение последней инструкции никак не повлияло на значение переменной num . Итак, переменная ip содержит не значение 10 (начальное значение переменной num ), а адрес, по которому оно хранится. Назначение оператора “ & ” можно “перевести” на русский язык как “адрес переменной”, перед которой он стоит. Следовательно, последнюю инструкцию присваивания можно выразить так: “переменная ip получает адрес переменной num ”. Второй оператор работы с указателями ( * ) служит дополнением к первому ( & ). Это также унарный оператор, но он обращается к значению переменной, расположенной по адресу, заданному его операндом. Другими словами, он указывает на значение переменной, адресуемой заданным указателем. Если (продолжая работу с предыдущим фрагментом кода) переменная ip содержит адрес переменной num , то при выполнении инструкции int val = *ip; переменной val будет присвоено значение 10 , являющееся значением переменной num , на которую указывает переменная ip . Назначение оператора “ * ” можно выразить словосочетанием “по адресу”. В данном случае предыдущую инструкцию можно прочитать так: “переменная val получает значение (расположенное) по адресу ip ”. Оператор “ * ” также можно использовать с левой стороны от оператора присваивания. В этом случае он устанавливает значение, адресуемое заданным указателем. Например, при выполнении инструкции *ip = 100; число 100 присваивается переменной, адресуемой указателем ip (в данном случае имеется в виду переменная num ). Таким образом, эту инструкцию можно прочитать так: “по адресу ip помещаем значение 100 ”. Использование ключевого слова unsafe Код, в котором используются указатели, должен быть отмечен как “опасный” с помощью ключевого слова unsafe . Так можно отмечать отдельные инструкции и методы целиком. Например, рассмотрим программу, в методе Main() которой используются указатели, и поэтому весь метод отмечен словом unsafe // Демонстрация использования указателей и // ключевого слова unsafe. using System; class UnsafeCode { // Отмечаем метод Main() как "опасный". unsafe public static void Main() { int count = 99; 488 Часть I. Язык C# int* p; // Создаем указатель на int-значение. p = &count; // Помещаем адрес переменной count //в указатель р. Console.WriteLine( "Начальное значение переменной count равно " + *p); *p = 10; // Присваиваем значение 10 переменной count // через указатель р. Console.WriteLine( "Новое значение переменной count равно " + *p); } } Вот результаты выполнения этой программы: Начальное значение переменной count равно 99 Новое значение переменной count равно 10 Использование модификатора fixed При работе с указателями зачастую используется модификатор fixed . Он предотвращает удаление управляемых переменных системой сбора мусора. Это необходимо в том случае, если, например, указатель ссылается на какое-нибудь поле в объекте класса. Поскольку указатель “ничего не знает” о действиях “сборщика мусора”, то в случае удаления такого объекта этот указатель будет указывать на неверный объект. Формат применения модификатора fixed таков: fixed(type* p = & var ) { // Использование зафиксированного объекта. } Здесь элемент p — указатель, которому присваивается адрес переменной. Объект будет оставаться в текущей области памяти до тех пор, пока не выполнится соответствующий блок кода. Инструкция fixed может включать вместо блока кода единственную инструкцию. Ключевое слово fixed можно использовать только в контексте “опасного кода”. Используя список элементов, разделенных запятыми, можно объявить сразу несколько фиксированных указателей. Рассмотрим пример использования модификатора fixed : // Демонстрация использования модификатора fixed. using System; class Test { public int num; public Test(int i) { num = i; } } class FixedCode { // Отмечаем метод Main() как опасный. unsafe public static void Main() { Test о = new Test(19); fixed(int* p = io.num) { // Используем модификатор // fixed, чтобы поместить // адрес поля o.num в р. Глава 18. Опасный код, указатели и другие темы 489 Console.WriteLine( "Начальное значение поля o.num равно " + *p); *p = 10; // Присваиваем число 10 переменной count // через указатель р. Console.WriteLine( "Новое значение поля o.num равно " + *p); } } } При выполнении этой программы получены такие результаты: Начальное значение поля o.num равно 19 Новое значение поля o.num равно 10 Здесь модификатор fixed защищает объект о от удаления. Поскольку p указывает на поле o.num , то в случае удаления объекта о указатель p некорректно ссылался бы на область памяти. Доступ к членам структур с помощью указателей Указатель может ссылаться на объект структурного типа, если он не содержит ссылочных типов. При доступе к члену структуры посредством указателя необходимо использовать оператор “стрелка” ( -> ), а не оператор “точка” ( ). Рассмотрим, например, следующую структуру: struct MyStruct { public int x; public int y; public int sum() { return x + y; } } Теперь покажем, как получить доступ к ее членам с помощью указателя: MyStruct о = new MyStruct(); MyStruct* p; // Объявляем указатель. p = &о; p->x = 10; p->y = 20; Console.WriteLine("Сумма равна " + p->sum()); Арифметические операции над указателями С указателями можно использовать только четыре арифметических оператора: ++ , -- , + и - . Чтобы лучше понять, что происходит при выполнении арифметических действий с указателями, начнем с примера. Пусть p1 — указатель на int -переменную с текущим значением 2 000 (т.е. p1 содержит адрес 2 000 ). После выполнения выражения p1++; содержимое указателя p1 станет равным 2 004 , а не 2 001 ! Дело в том, что при каждом инкрементировании указатель p1 будет указывать на следующее int -значение. Поскольку в C# int -значения занимают четыре байта, то при инкрементировании p1 его значение увеличивается на 4. Для операции декрементирования справедливо обратное утверждение, т.е. при каждом декрементировании значение p1 будет уменьшаться на 4. Например, после выполнения инструкции 490 Часть I. Язык C# p1--; указатель p будет иметь значение 1 996 , если до этого оно было равно 2 000 Итак, каждый раз, когда указатель инкрементируется, он будет указывать на область памяти, содержащую следующий элемент базового типа этого указателя. А при каждом декрементировании он будет указывать на область памяти, содержащую предыдущий элемент базового типа этого указателя. Арифметические операции над указателями не ограничиваются использованием операторов инкремента и декремента. Со значениями указателей можно выполнять операции сложения и вычитания, используя в качестве второго операнда целочисленные значения. Выражение p1 = p1 + 9; заставляет p1 указывать на девятый элемент базового типа указателя p1 относительно элемента, на который p1 указывал до выполнения этой инструкции. Несмотря на то что складывать указатели нельзя, один указатель можно вычесть из другого (в предположении, что они оба имеют один и тот же базовый тип). Разность покажет количество элементов базового типа, которые разделяют эти два указателя. Помимо сложения (и вычитания) указателя и (из) целочисленного значения, а также вычитания двух указателей, над указателями никакие другие арифметические операции не выполняются. Например, с указателями нельзя складывать float - или double -значения. Чтобы понять, как формируется результат выполнения арифметических операций над указателями, выполним следующую короткую программу. Она выводит реальные физические адреса, которые содержат указатель на int -значение ( ip ) и указатель на float -значение ( fp ). Обратите внимание на каждое изменение адреса (зависящее от базового типа указателя), которое происходит при повторении цикла. // Демонстрируем результаты выполнения арифметических // операций над указателями. using System; class PtrArithDemo { unsafe public static void Main() { int x; int i; double d; int* ip = &i; double* fp = &d; Console.WriteLine("int double\n"); for(x=0; x < 10; x++) { Console.WriteLine((uint) (ip) + " " + (uint) (fp)); ip++; fp++; } } } Ниже показаны возможные результаты выполнения этой программы. Ваши результаты могут отличаться от приведенных, но интервалы между значениями должны быть такими же. Глава 18. Опасный код, указатели и другие темы 491 int double 1243324 1243328 1243328 1243336 1243332 1243344 1243336 1243352 1243340 1243360 1243344 1243368 1243348 1243376 1243352 1243384 1243356 1243392 1243360 1243400 Как подтверждают результаты выполнения этой программы, арифметические операции над указателями выполняются в зависимости от базового типа каждого указателя. Поскольку любое int -значение занимает четыре байта, а double -значение — восемь, то и сами адреса изменяются с учетом этих значений. Сравнение указателей Указатели можно сравнивать, используя операторы отношения == , < и >. Однако для того чтобы результат сравнения указателей поддавался интерпретации, сравниваемые указатели должны быть каким-то образом связаны. Например, если p1 и p2 — указатели, которые указывают на две отдельные и никак не связанные переменные, то любое сравнение p1 и p2 в общем случае не имеет смысла. Но если p1 и p2 указывают на переменные, между которыми существует некоторая связь (как, например, между элементами одного и того же массива), то результат сравнения указателей p1 и p2 может иметь определенный смысл. Рассмотрим пример, в котором сравнение указателей используется для отыскания среднего элемента массива. // Демонстрация возможности сравнения указателей. using System; class PtrCompDemo { unsafe public static void Main() { int[] nums = new int[11]; int x; // Находим средний элемент массива. fixed(int* start = &nums[0]) { fixed(int* end = &nums[nums.Length-1]) { for(x=0; start+x <= end-x; x++); } } Console.WriteLine( "Средний элемент массива имеет номер " + x); } } Вот как выглядят результаты выполнения этой программы: Средний элемент массива имеет номер 6 Эта программа находит средний элемент, первоначально установив указатель start равным адресу первого элемента, а указатель end — адресу последнего элемента массива. Затем, используя возможности выполнения арифметических операций над указателями, мы увеличиваем указатель start на целочисленное значение x , а указатель 492 Часть I. Язык C# end — уменьшаем на то же значение x до тех пор, пока результат суммирования start и x не станет меньше или равным результату вычитания end и x Одно уточнение: указатели start и end должны быть созданы внутри fixed - инструкции, поскольку они указывают на элементы массива, который представляет собой ссылочный тип данных. Не забывайте, что в C# массивы реализованы как объекты и могут быть удалены сборщиком мусора. Указатели и массивы В C# указатели и массивы связаны между собой. Например, имя массива без индекса образует указатель на начало этого массива. Рассмотрим следующую программу: /* Имя массива без индекса образует указатель на начало этого массива. */ using System; class PtrArray { unsafe public static void Main() { int[] nums = new int[10]; fixed(int* p = &nums[0], p2 = nums) { if(p == p2) Console.WriteLine( "Указатели p и p2 содержат один и тот же адрес."); } } } Вот какие результаты получены при выполнении этой программы: Указатели p и p2 содержат один и тот же адрес. Как подтверждают результаты выполнения этой программы, выражение &nums[0] эквивалентно nums Поскольку вторая форма короче, большинство программистов используют именно ее в случае, когда нужен указатель на начало массива. Индексация указателя Указатель, который ссылается на массив, можно индексировать так, как если бы это было имя массива. Этот синтаксис обеспечивает альтернативу арифметическим операциям над указателями, поскольку он более удобен в некоторых ситуациях. Рассмотрим пример, // Индексирование указателя подобно массиву. using System; class PtrIndexDemo { unsafe public static void Main() { int[] nums = new int[10]; // Индексируем указатель. Console.WriteLine( "Индексируем указатель подобно массиву."); Глава 18. Опасный код, указатели и другие темы 493 fixed(int* p = nums) { for(int i=0; i < 10; i++) p[i] = i; // Индексируем указатель подобно массиву. for(int i=0; i < 10; i++) Console.WriteLine("p[{0}]: {1} ", i, p[i]); } // Используем арифметические операции над указателями. Console.WriteLine( "\nИспользуем арифметические операции над указателями."); fixed(int* p = nums) { for(int i=0; i < 10; i++) *(p+i) = i; // Используем арифметические // операции над указателями. for(int i=0; i < 10; i++) Console.WriteLine("*(p+{0}|): {1} ", i, *(p+i)); } } } Вот результаты выполнения этой программы: Индексируем указатель подобно массиву. p[0]: 0 p[1]: 1 p[2]: 2 p[3]: 3 p[4]; 4 p[5]: 5 p[6]: 6 p[7]: 7 p[3]: 8 p[9]: 9 Используем арифметические операции над указателями. *(p+0): 0 *(p+1): 1 *(p+2): 2 *(p+3): 3 *(p+4): 4 *(p+5): 5 *(p+6): 6 *(p+7): 7 *(p+8): 8 *(p+9): 9 Как видно по результатам выполнения этой программы, выражение (в котором участвует указатель) в формате *(ptr + i) можно переписать с использованием синтаксиса, применяемого при индексировании массивов: ptr[i] При индексировании указателя необходимо помнить следующее. Во-первых, при этом нарушение границ массива никак не контролируется. Следовательно, существует возможность получить доступ к “элементу” за концом массива, если на него ссылается указатель. Во-вторых, указатель не имеет свойства Length . Поэтому при использовании указателя невозможно узнать длину массива. 494 Часть I. Язык C# Указатели и строки Несмотря, на то что в C# строки реализованы как объекты, к отдельным их символам можно получить доступ с помощью указателя. Для этого достаточно присвоить char* - указателю адрес начала этой строки, используя fixed -инструкцию: fixed(char* p = str ) { // ... После выполнения такой fixed -инструкции p будет указывать на начало символьного массива, который составляет эту строку. Этот символьный массив заканчивается символом конца строки, т.е. нулевым символом. Этот факт можно использовать для проверки достижения конца массива. В C/C++ символьные строки реализованы в виде символьных массивов, завершающихся нулевым символом. Таким образом, получив char* -указатель на строку, можно обрабатывать строки практически так же, как это делается в C/C++. Рассмотрим программу, которая демонстрирует доступ к строке с помощью char* - указателя: // Использование fixed-инструкций для получения // указателя на начало строки. using System; class Fixedstring { unsafe public static void Main() { string str = "Это простой тест."; // Направляем указатель p на начало строки str. fixed(char* p = str) { // Отображаем содержимое строки str // с помощью указателя р. for(int i=0; p[i] != 0; i++) Console.Write(p[i]); } Console.WriteLine(); } } Вот результаты выполнения этой программы: Это простой тест. Использование многоуровневой непрямой адресации Можно создать указатель, который будет ссылаться на другой указатель, а тот — на конечное значение. Эту ситуацию называют многоуровневой непрямой адресацией (multiple indirection), или использованием указателя на указатель. Идея многоуровневой непрямой адресации схематично проиллюстрирована на рис. 18.1. Как видите, значение обычного указателя (при одноуровневой непрямой адресации) представляет собой адрес переменной, которая содержит некоторое значение. В случае применения указателя на указатель первый содержит адрес второго, а тот указывает на переменную, содержащую определенное значение. При использовании непрямой адресации можно организовать любое желаемое количество уровней, но, как правило, ограничиваются лишь двумя, поскольку увеличение числа уровней часто ведет к возникновению концептуальных ошибок. Глава 18. Опасный код, указатели и другие темы 495 Переменную, которая является указателем на указатель, нужно объявить соответствующим образом. Для этого достаточно поставить дополнительный символ “звездочка” ( * ) после имени типа. Например, следующее объявление сообщает компилятору о том, что q — это указатель на указатель на значение типа int: int** q; Необходимо помнить, что переменная q здесь — не указатель на целочисленное значение, а указатель на указатель на int -значение. Чтобы получить доступ к значению, адресуемому указателем на указатель, необходимо дважды применить оператор “ * ”, как показано в следующем примере: using System; class Multiplelndirect { unsafe public static void Main() { int x; // Переменная содержит значение. int* p; // Переменная содержит указатель //на int-значение. int** q; // Переменная содержит указатель на указатель //на int-значение. x = 10; p = &x; // Помещаем адрес x в р. q = &p; // Помещаем адрес p в q. Console.WriteLine(**q); // Отображаем значение x. } } При выполнении этой программы мы получили бы значение переменной x , т.е. число 10 . Здесь переменная p объявлена как указатель на int -значение, а переменная q — как указатель на указатель на int -значение. И еще: не следует путать многоуровневую непрямую адресацию с такими высокоуровневыми структурами данных, как связные списки, которые используют указатели. Это — две принципиально различные концепции. Рис. 18.1. Одноуровневая и многоуровневая непрямая адресация Массивы указателей Указатели, подобно данным других типов, могут храниться в массивах. Вот, например, как выглядит объявление трехэлементного массива указателей на int -значения: int*[] ptrs = new int*[3]; Адрес Указатель Переменная Одноуровневая непрямая адресация Указатель Переменная Многоуровневая непрямая адресация Указатель Значение Адрес Адрес Значение 496 Часть I. Язык C# Чтобы присвоить адрес int -переменной с именем var третьему элементу этого массива указателей, запишите следующее: ptrs[2] = &var; Чтобы получить значение переменной var , используйте такой синтаксис: *ptrs[2] Ключевые слова смешанного типа В заключение части I рассмотрим определенные в C# ключевые слова, которые еще не были здесь описаны. sizeof Не исключено, что вам понадобится узнать размер (в байтах) одного из C# -типов значений. Для получения этой информации используйте оператор sizeof . Формат его применения таков: sizeof( тип ) Здесь элемент тип — это тип, размер которого мы хотим получить. Оператор sizeof можно использовать только в контексте опасного ( unsafe ) кода. Таким образом, он предназначен в основном для специальных ситуаций, особенно при работе со смешанным (управляемым и неуправляемым) кодом. lock Ключевое слово lock используется при работе с несколькими потоками. В C# программа может содержать два или больше потоков выполнения. В этом случае программы работают в многозадачном режиме. Другими словами, отдельные фрагменты программ выполняются не только независимо один от другого, или, можно сказать, одновременно. Это приводит к возникновению определенной проблемы: а что если два потока попытаются использовать ресурс, который одновременно может использовать только один поток? Чтобы решить эту проблему, можно создать критический раздел кода, который в данный момент может выполняться только одним потоком. Такой подход реализуется с помощью ключевого слова lock . Его формат таков: lock( obj ) { // Критический раздел. } Здесь элемент obj представляет объект, который стремится получить блокировку. Если один поток уже вошел в критический раздел, то второй должен ожидать до тех пор, пока не выполнится первый. Критический раздел может быть выполнен при получении разрешения на установку блокировки. (Подробнее см. главу 21.) readonly В классе можно создать поле, предназначенное только для чтения, если объявить его с помощью ключевого слова readonly , причем readonly -поле можно инициализировать, но после этого изменить его содержимое уже нельзя. Следовательно, использование readonly -полей — удобный способ создать константы (например, обозначающие размерность массивов), которые применяются на протяжении всей про- Глава 18. Опасный код, указатели и другие темы 497 граммы. Предназначенные только для чтения поля могут быть как статическими, так и нестатическими. Рассмотрим пример создания readonly -поля: // Демонстрация использования readonly-поля. using System; class MyClass { public static readonly int SIZE = 10; } class DemoReadOnly { public static void Main() { int[]nums = new int[MyClass.SIZE]; for(int i=0; i Console.Write(i + " "); // MyClass.SIZE = 100; // Ошибка!!! readonly-поле // изменять нельзя! } } Здесь поле MyClass.SIZE инициализируется значением 10 . После этого его можно использовать, но не изменять. Чтобы убедиться в этом, попытайтесь удалить символ комментария, стоящий в начале последней строки, и скомпилировать программу. Вы получите сообщение об ошибке. stackalloc С помощью ключевого слова stackalloc можно использовать память стека. Это можно делать только при инициализации локальных переменных. Распределение памяти стека имеет следующий формат: тип * p - stackalloc тип [ размер ] Здесь p — указатель, который получает адрес области памяти достаточно большого размера, чтобы хранить размер объектов типа тип . Ключевое слово stackalloc можно использовать только в контексте опасного ( unsafe ) кода. Обычно память для объектов выделяется из кучи, которая представляет собой область свободной памяти. Выделение памяти из области стека — своего рода исключение. Переменные, размещенные в области стека, не обрабатываются сборщиком мусора. Но они существуют только при выполнении блока, где были объявлены. По завершении выполнения блока эта память освобождается. Единственное достоинство использования ключевого слова stackalloc — можно не беспокоиться о том, что соответствующая область памяти попадет под “метлу” сборщика мусора. Рассмотрим пример использования ключевого слова stackalloc : // Демонстрация использования ключевого слова stackalloc. using System; class UseStackAlloc { unsafe public static void Main() { int* ptrs = stackalloc int[3]; 498 Часть I. Язык C# ptrs[0] = 1; ptrs[1] = 2; ptrs[2] = 3; for(int i=0; i < 3; i++) Console.WriteLine(ptrs[i]); } } Результаты выполнения этой программы таковы: 1 2 3 Инструкция using Ключевое слово using, применение которого в качестве директивы описано выше, имеет и второй вариант использования, а именно в качестве инструкции using. В этом случае возможны следующие две формы: using( obj ) { // Использование объекта obj } using(type obj = инициализатор) { // Использование объекта obj } Здесь элемент obj представляет объект, используемый внутри блока using . В первой форме этот объект объявляется вне using -инструкиии, а во второй — внутри, При завершении блока для объекта obj вызывается метод Dispose() (определенный в интерфейсе System.IDisposable ). Инструкция using применяется только к объектам, которые реализованы в интерфейсе System.IDisposable Рассмотрим пример использования каждой формы инструкция using : // Демонстрация использования инструкции using. using System; using System.IO; class UsingDemo { public static void Main() { StreamReader sr = new StreamReader("test.txt"); // Используем объект внутри инструкции using. using(sr) { Console.WriteLine(sr.ReadLine() ); sr.Close(); } // Создаем StreamReader-объект внутри инструкции using. using(StreamReader sr2 = new StreamReader("test.txt")) { Console.WriteLine(sr2.ReadLine()); sr2.Close(); } } } Глава 18. Опасный код, указатели и другие темы 499 Класс StreamReader реализует интерфейс IDisposable (через свой базовый класс TextReader ). Следовательно, его можно использовать в using -инструкции. (Описание интерфейса IDisposable см. в главе 24.) Модификаторы const и volatile Модификатор const используется для объявления полей или локальных переменных, которые не должны изменяться. Этим переменным необходимо присвоить начальные значения при объявлении. Таким образом, const -переменная является по сути константой. Например, при выполнении инструкции const int i = 10; создается const -переменная i , которая имеет значение 10 Модификатор volatile сообщает компилятору о том, что значение соответствующего поля может быть изменено в программе неявным образом. Например, поле, содержащее текущее значение системного времени, может обновляться операционной системой автоматически. В этой ситуации содержимое такого поля изменяется без явного выполнения инструкции присваивания. Причина внешнего изменения поля может быть очень важной. Дело в том, что C#-компилятору разрешается оптимизировать определенные выражения автоматически в предположении, что содержимое поля остается неизменным, если оно не находится в левой части инструкции присваивания. Но если некоторые факторы, не относящиеся к текущему коду (например, связанные с обработкой второго потока выполнения), изменят значение этого поля, такое предположение окажется неверным. Использование модификатора volatile позволяет сообщить компилятору о том, что он должен получать значение этого поля при каждом обращении к нему. Полный справочник по Часть II Библиотека C# Часть II посвящена описанию библиотеки C#. Как упоминалось в части I, используемая в C# библиотека классов является, по сути, библиотекой .NET Framework. Таким образом, материал этого раздела применим не только к языку C#, но и в целом к среде .NET Framework. Библиотека C# организована с использованием пространств имен. Для работы с какой- либо ее частью с помощью директивы using импортируется соответствующее пространство имен. Конечно, можно также указывать полное имя элемента, т.е. сопровождать его названием пространства имен, но легче импортировать (так чаще всего и поступают) само пространство имен. |