Console. WriteLine(123.ToString() + 456.ToString());
Проверяемые и непроверяемые операции для элементарных типов
В CLR есть IL-команды, позволяющие компилятору по-разному реагировать на переполнение. Так, суммирование двух чисел выполняет IL-команда add, не реагирующая на переполнение, и команда add. ovf, которая при переполнении генерирует исключение System. OverflowException. Кроме того, в CLR есть аналогичные IL-команды для вычитания (sub/sub. ovf), умножения (mul/mul. ovf) и преобразования данных (conv/conv. ovf).
По умолчанию проверка переполнения отключена. Чтобы включить управление процессом обработки переполнения на этапе компиляции, добавьте в командную строку компилятора переключатель /checked+. Он сообщает компилятору, что для выполнения сложения, вычитания, умножения и преобразования должны быть сгенерированы IL-команды с проверкой переполнения. Когда оно возникает, CLR генерирует исключение OverflowException. Необходимо предусмотреть корректную обработку этого исключения.
Существует механизм гибкого управления проверкой в виде операторов checked и unchecked:
Byte b = 100;
b = checked((Byte)(b+200)); //Генерируется OverflowException
Можно включить проверяемые или непроверяемые выражения внутрь блока:
checked
{
Byte b = 100;
b = (Byte) (b + 200);
}
Установка режима контроля переполнения не влияет на работу метода, вызываемого внутри оператора или инструкции checked, так как действие оператора checked распространяется только на выбор IL-команд сложения, вычитания, умножения и преобразования сложения:
checked
{
SomeMethod(400);
//Возникновение OverflowException в SomeMethod зависит от наличия в нем проверяемых инструкций.
}
Используя checked и uncheked, необходимо учитывать следующее:
- Включите в блок checked ту часть кода, в которой при переполнении должно генерироваться исключение. Включите в блок uncheked ту часть кода, в которой переполнение не должно вызывать исключения Для кода, где нет операторов и блоков checked и unchecked, делается предположение: если на этапе разработки возникнет переполнение, желательно сгенерировать исключение, в то время как в окончательном коде проверка переполнения нежелательна.
При отладке кода установите переключатель компилятора /checked+. Обнаружив исключение, вы можете исправить ошибку. При окончательной сборке приложения установите переключатель /checked-, что ускорит выполнение приложения, а исключения генерироваться не будут.
Передача параметров по ссылке
По умолчанию CLR предполагает, что все параметры методов передаются по значению. Кроме этого, параметры можно передавать по ссылке. В C# это делается с помощью ключевых слов out и ref. Разница между этими ключевыми словами касается метода инициализации объекта, на который указывает переданная ссылка. Если параметр метода помечен ключевым словом out, то вызывающий код может не инициализировать его, пока не будет вызван этот метод. В этом случае вызванный метод не может читать значение параметра и должен записать его, прежде чем вернуть управление (это выходной параметр). Если параметр метода помечен ключевым словом ref, то вызывающий код должен инициализировать его перед вызовом этого метода, а вызванный метод может как читать, так и записывать значение параметра.
class App
{
static void Main( )
{
int x;
SetVal( out x );
AddVal( ref x );
}
static void SetVal( out int v )
{
v = 10;
}
static void AddVal( ref int v)
{
v += 10;
}
}
Обратим особое внимание, что ключевые слова out и ref также необходимо указывать и при вызове метода. Это сделано для того, чтобы по вызову метода было легко определить, какие параметры передаются по ссылке, а какие – по значению.
В случае ссылочных типов вызывающий код выделяет память для указателя на передаваемый объект, а вызванный код манипулирует этим указателем. В силу этих особенностей использование ключевых слов out и ref со ссылочными типами полезно лишь когда метод возвращает ссылку на объект.
Переменное число параметров
Метод, принимающий переменное число параметров, объявляют так:
int sum(params int [] values)
{
int s = o;
for (int i = 0, i < values. Length, i++)
s += values[ i ];
return s;
}
Обнаружив вызов метода, компилятор C# проверяет все методы с заданным именем, у которых ни один из параметров не помечен атрибутом params. Если метод, способный принять вызов, есть, компилятор генерирует вызывающий его код, иначе компилятор ищет методы с атрибутом params и проверяет, могут ли они принять вызов.
Только последний параметр метода может быть помечен ключевым словом params. Этот параметр должен указывать одномерный массив любого типа. В последнем параметре метода допустимо передавать null или ссылку на массив, состоящий из 0 элементов.
Ссылочные и размерные типы
CLR поддерживает две разновидности типов: ссылочные (reference types) и размерные (value types). Чаще всего приходится иметь дело со ссылочными типами. Память для них всегда выделяется из управляемой кучи, а оператор new возвращает адрес в памяти, где размещается сам объект. Необходимо иметь в виду следующее:
- память для ссылочных типов всегда выделяется из управляемой кучи; каждый объект, размещаемый в куче, имеет некоторые дополнительные члены, подлежащие инициализации; размещение объекта в управляемой куче со временем инициирует сборку мусора.
Экземпляры размерных типов обычно размещаются в стеке потока (они могут быть встроены и в объект ссылочного типа). Если тип называется классом (class), речь идет о ссылочном типе. Размерные типы – это структуры (structure) и перечисления (enumeration).
Все структуры являются производными непосредственно от типа System. ValueType, который в свою очередь является производным от типа System. Object. По умолчанию все размерные типы должны быть производными от System. ValueType. Все перечисления являются производными от типа System. Enum, производного от System. ValueType.
При определении собственного размерного типа нельзя выбрать базовый тип, однако размерный тип может реализовать один или несколько интерфейсов.
Иногда для повышения эффективности кода можно использовать вместо ссылочного типа размерный. Это справедливо для типа, удовлетворяющего следующим условиям:
- тип ведет себя подобно элементарному типу; типу не нужен любой другой тип в качестве базового; тип не будет иметь производных от него типов; экземпляры этого типа редко передаются методам в качестве параметров; экземпляры этого типа редко возвращаются методами;
Приведем далее отличия между ссылочными и размерными типами:
- Объекты размерного типа существуют в двух формах: неупакованной (unboxed) и упакованной (boxed). Ссылочные типы бывают только в упакованной форме. Размерные типы являются производными от System. ValueType. Он переопределяет метод Equals, который возвращает true, если значения полей в обоих объектах совпадают. Также переопределен метод GetHashCode, который создает значение хэш-кода с помощью алгоритма, учитывающего значения полей экземпляра объекта. Определяя собственные размерные типы значений, необходимо переопределить и написать свою реализацию методов Equals и GetHashCode. Поскольку в объявлении нового размерного или ссылочного типа нельзя указывать размерный тип в качестве базового класса, то вводить в размерный тип новые виртуальные методы нельзя. Переменные ссылочного типа содержат адреса объектов в куче. Когда переменная ссылочного типа создается, ей по умолчанию присваивается null, т. е. в этот момент она не указывает на действительный объект. Когда переменной размерного типа присваивается другая переменная размерного типа, выполняется копирование всех ее полей. Когда переменной ссылочного типа присваивается переменная ссылочного типа, копируется только ее адрес. Несколько переменных ссылочного типа могут ссылаться на один объект в куче, благодаря чему, работая с одной переменной, можно изменить объект, на который ссылается другая переменная. Каждая переменная размерного типа имеет собственную копию данных «объекта», и операции с одной переменной размерного типа не повлияют на другую переменную. Так как неупакованные размерные типы не размещаются в куче, память, отведенная для них, освобождается сразу при возвращении из метода, в котором описан экземпляр этого типа. Это значит, что экземпляр размерного типа не получает уведомления (через метод Finalize), когда его память освобождается.
Пример:
struct SomeVal { public int x; }
SomeVal v = new SomeVal( ); //размещается на стеке, поле x инициализируется нулем
SomeVal v; // поле x не инициализируется
class A
{
public int x;
A next; // для размерных типов так делать нельзя
}
A a = new A();
a. next = new A();
a. next. next = new A();
a = null; // сборщик мусора позаботится об удалении объекта
Упаковка и распаковка размерных типов
Переменной типа Object можно присвоить любой объект размерного типа, при этом происходит его упаковка. Чтобы восстановить из упакованного объекта первоначальное значение, его необходимо распаковать, совершив явное приведение типов.
Упаковка
Рассмотрим следующий код:
struct Point
{
public int x, y;
}
ArrayList a = new ArrayList();
a. Add(new Point(2,3)); //создается на стеке и упаковывается.
В качестве параметра Add нужен Object, т. е. ссылка (или указатель) на объект в управляемой куче. Чтобы код работал, нужно преобразовать размерный тип Point в объект из управляемой кучи и получить на него ссылку.
Преобразование размерного типа в ссылочный позволяет выполнить упаковка (boxing). При упаковке экземпляра размерного типа происходит следующее:
1. В управляемой куче выделяется память. Ее объем определяется длиной размерного типа и некоторыми накладными расходами, позволяющими этому размерному типу стать настоящим объектом. Этими накладными расходами являются указатель на таблицу виртуальных методов и индекс SyncBlockIndex.
2. Поля размерного типа копируются в память, выделенную только что в куче.
3. Возвращается адрес объекта. Этот адрес является ссылкой на объект; размерный тип превратился в ссылочный.
Компилятор C# автоматически создает IL-код, необходимый для упаковки экземпляра размерного типа.
Распаковка
Рассмотрим следующий код:
Point p = new Point( );
Object o = p; // упаковка
p = (Point)o; // распаковка
Все поля, содержащиеся в упакованном объекте Point, должны быть скопированы в переменную p размерного типа, находящуюся в стеке потока. CLR выполняет эту процедуру в два этапа. Сначала извлекается адрес полей Point из упакованного объекта Point. Этот процесс называется распаковкой (unboxing). Затем значения полей копируются из кучи в экземпляр размерного типа, находящийся в стеке.
При распаковке ссылочного типа может произойти следующее:
1. Если ссылка равна null, генерируется исключение NullReferenceException.
2. Если ссылка указывает на объект, не являющийся упакованным значением требуемого размерного типа, генерируется исключение InvalidCastException.
3. Возвращается указатель на размерный тип, находящийся внутри объекта. Этот тип ничего не знает об обычных издержках, связанных с истинным объектом (об указателе на таблицу методов и SyncBlockIndex). В этом указателе содержится адрес неупакованной части упакованного объекта.
Если вам нужна ссылка на экземпляр размерного типа, этот тип должен быть упакован. Обычно это случается, когда вы хотите передать размерный тип методу, требующему ссылочный тип.
Неупакованные размерные типы «легче», чем ссылочные, поскольку:
- память в управляемой куче им не выделяется; у них нет дополнительных членов, присущих каждому объекту в куче: указателя на таблицу методов и SyncBlockIndex
Еще один пример, демонстрирующий упаковку и распаковку:
Point p = new Point(2, 3);
Console. WriteLine("Координаты точки" + p);
В этом коде одна операция упаковки. Дело в том, что вызывается метод WriteLine, в который нужно передать объект String. Чтобы создать String, компилятор формирует код, в котором вызывается статический метод Concat объекта String: static string concat(object o1, object o2). Первым параметром является строка «Координаты точки» в виде ссылки на объект String, в качестве второго параметра o2 передается p. Но p – неупакованное значение, а o2 – это Object, поэтому p нужно упаковать, а его адрес передать в качестве o2. Этот код можно улучшить:
Point p = new Point(2,3);
Console. WriteLine("Координаты точки" + p. ToString);
Для неупакованного размерного типа p теперь вызывается метод ToString, возвращающий String. Строковые объекты являются ссылочными типами и могут легко передаваться в метод Concat без упаковки.
Массивы
Базовым для всех массивов является тип System. Array, производный от System. Objeсt. Массивы всегда относятся к ссылочному типу и размещаются в управляемой куче, а переменная в приложении содержит не сам массив, а ссылку на него.
Помимо одномерных и многомерных массивов, CLR поддерживает также вложенные (jagged) массивы.
Создание массива:
int[] a = new int[10];
или
int[] a = {1,2,3,4,5};
int[,] a2 = new int[3,4];
или
int[][] a2 = { {1,2,3,4}, {5,6,7,8}, {9,0,1,2} };
int[][] aa2 = new int[5] [ ];
aa2[0] = new int[5];
aa2[1] = new int[3];
Некоторые методы типа System. Array:
- Rank – возвращает размерность массива; GetLength – возвращает число элементов в указанном измерении массива; Length – возвращает общее число элементов в массиве GetLowerBound - возвращает нижнюю границу для указанного измерения. GetUpperBound – возвращает верхнюю границу для указанного измерения; GetEnumerator – возвращает IEnumerator для массива. Позволяет использовать оператор foreach. Для многомерного массива этот итератор позволяет перечислять все элементы массива, начиная перебор с самого правого измерения; Sort – (статический метод) сортирует ээлементы в одном или нескольких массивах. Тип элемента массива должен иметь реализацию интерфейса IComparer или передавать объект, тип которого имеет реализацию интерфейса IComparer; IndexOf – (статический метод) возвращает индекс первого вхождения значения в одномерном массиве или в его части; LasIndexOf – (статический метод) возвращает индекс последнего вхождения значения в одномерном массиве или в его части; Clone – создает новый массив, являющийся ограниченной (shallow) копией исходного масива; CopyTo – копирует элементы из одного массива в другой; Copy – копирует раздел одного массива в другой массив, выполняя приведение типа поэлементно; Clear – (статический метод) устанавливает для диапазона элементов массива значение 0 или null; CreateInstance – (статический метод) создает экземпляр массива. Позволяет динамически определять массивы любого типа, размерности и длины.
CLR выполняет проверку выхода за границы массива, и, при необходимости, генерирует исключение System. IndexOutOfRangeException. Чтобы отключить проверку индексов, необходимо откомпилировать код с переключателем /unsafe:
csc. exe /unsafe MyCod. cs
Проектное задание
Создать сборку dll, реализующую основные операции с одномерными и двумерными массивами: поиск, вставку, удаление, сортировку по критерию, заполнение по заданному алгоритму, трансформацию по заданному алгоритму, копирование в другой массив элементов, удовлетворяющих определенному критерию.
Тест рубежного контроля
1. Можно ли в C# в разных файлах проекта использовать одинаковые имена переменных?
a. Да
b. Только в разных пространствах имен
c. Нет
2. Что возвратит результат выражения 1 is Object?
a. Произойдет ошибка компиляции
b. True
c. False
3. Как правильно описать заголовок функции Swap, меняющей местами значения двух целых переменных?
a. void Swap(int a, int b);
b. void Swap(ref int a, ref int b);
c. void Swap(out int a, out int b);
Модуль 3. Классы
Комплексная цель
Изучить основные механизмы работы с классами: вызов конструкторов, клонирование и сравнение, перегрузку операций, свойства, индексные свойства, наследование и полиморфизм.
Содержание
Конструкторы
Конструкторы – это методы, позволяющие корректно инициализировать новый экземпляр типа. CLR требует определения в каждом классе (ссылочном типе) хотя бы одного конструктора. При создании экземпляра объекта ссылочного типа для него выделяется память, инициализируются его служебные поля (указатель на таблицу методов и SyncBlockIndex), после чего вызывается конструктор экземпляра, устанавливающий исходное состояние нового объекта. При создании объекта ссылочного типа выделяемая для него память всегда обнуляется до вызова конструктора экземпляра типа. По умолчанию компилятор C# определяет для ссылочных типов открытый конструктор без параметров. Тип может определять несколько конструкторов, при этом сигнатуры конструкторов обязательно должны отличаться.
Например:
class Some
{
int x;
string s;
public Some()
{
x = 5;
s = "Hello";
}
public Some (string s)
{
this.s = s;
}
}
Следует обратить внимание, что для обращения к полю s используется запись this.s, где this представляет собой ссылку на сам объект.
Для вызова конструктора используется следующий синтаксис:
Some sm = new Some();
Можно также инициализировать поля экземпляра при их объявлянии:
class A
{
int x = 5;
}
Компилятор транслирует этот синтаксис в метод-конструктор, выполняющий инициализацию.
В отличие от ссылочного, для размерного типа конструктор по умолчанию не создается и не вызывается автоматически. Более того, C# не позволяет определять для размерного типа конструкторы без параметров. Кроме того, любой конструктор, определенный для размерного типа, должен инициализировать все поля этого типа.
Конструкторы типов
CLR поддерживает конструкторы типов, которые применяются для установки первоначального состояния типа. У типа может быть один и только один конструктор. У конструктора типа никогда не бывает параметров. Он исполняется при первом обращении к типу.
class SomeRef
{
static int i = 5, j;
static SomeRef()
{
i = 6;
j = 7;
}
}
Здесь компилятор генерирует единственный метод-конструктор типа, который сначала инициализирует поле i значением 5, затем – значением 6. Т. е. при генерации IL-кода конструктора типа компилятор сначала генерирует код, инициализирующий статические поля, затем обрабатывает явный код, содержащийся внутри метода-конструктора типа. Конструктор типа всегда должен быть закрытым (C# делает его закрытым автоматически).
Если конструктор типа генерирует необрабатываемое исключение, CLR считает такой тип непригодным. При попытке обращения к любому полю или методу такого типа возникает исключение System. TypeInitializationException.
Код конструктора типа может обращаться только к статическим полям типа.
Клонирование объектов
Разрешение на клонирование экземпляра устанавливается на уровне класса. Если класс позволяет клонировать свои экземпляры, в нем должен быть реализован интерфейс ICloneable:
public interface ICloneable
{
Object Clone();
}
Если типу необходимо ограниченное копирование (т. е. копирование значений полей объекта, но не тех данных, на которые указывают поля), нужно включить в метод Clone вызов защищенного метода MemberwiseClone для System. Object:
class MyType: ICloneable
{
public Object Clone()
{
return MemberwiseClone( );
}
}
Внутри MemberwiseClone выделяется память для создаваемого объекта. Тип нового объекта соответствует типу объекта, на который указывает ссылка this. Затем MemberwiseClone последовательно просматривает все экземплярные поля (включая и те, что достались по наследству от базовых типов) и копирует их содержимое из оригинала в новый объект.
Сравнение объектов на равенство
У типа System. Object есть виртуальный метод Equals, который возвращает true для двух объектов, имеющих в некотором смысле одинаковое значение. Кроме того, есть статический метод Equals с двумя параметрами – сравниваемыми объектами. Смысл сравнения на равенство определяет программист: например, два списка равны, если совпадают их длины и данные в узлах, но не поля связи.
Реализация метода Equals для System. Object имеет вид:
class Object
{
public virtual bool Equals(Object obj)
{
if (this == obj) return true;
return false;
}
public static bool Equals(Object o1, Object o2)
{
if (Object. ReferenceEquals(o1,o2))
return true;
if (o1!= null && o1.Equals(o2))
return true;
return false;
}
}
Реализация Equals для ссылочного типа
Пусть в классе определены ссылочное поле Ref и размерное поле Val. Тогда сравнение на равенство заключается вначале в проверке второго параметра на null, затем в проверке совпадения типов сравниваемых объектов и, наконец. в проверке совпадения их полей. Если хотя бы одна проверка не проходит, то возвращается false. Если все проверки прошли, возвращается true. Ниже приведен код класса, реализующего метод Equals.
class MyRef: Base
{
Ref r;
Val v;
public override bool Equals (Object obj)
{
// если Base переопределяет Equals
// то, добавить if (!base. Equals(obj)) return false;
if (obj == null)
return false;
if (this. GetType() != obj. GetType())
return false;
MyRef m = (MyRef)obj;
if (!Object. Equals(r, m. r))
return false;
if (!v. Equals(m. v))
return false;
return true;
}
public static bool operator==(MyRef m1, MyRef m2)
{
return Object. Equals(m1, m2);
}
public static bool operator!=(MyRef m1, MyRef m2)
{
return !(m1 == m2);
}
}
Если в одном или нескольких базовых классах переопределен метод Equals для Object, то к данному коду необходимо добавить сравнение полей также и базовым классом – данный код заключен в комментарий. Отметим также, что ссылочные поля сравниваются с помощью статического метода Equals (поскольку обе ссылки могут быть нулевыми), а размерные – при помощи экземплярного.
Реализация Equals для размерного типа
Для размерного типа сравнение на равенство проще. Принято составлять вспомогательный метод Equals – с параметром, тип которого совпадает с типом класса. Это позволяет воспользоваться им при реализации виртуального метода Equals с параметром типа Object. Ниже приведен код структуры, реализующего метод Equals.
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 4 |


