Смекни!
smekni.com

Основы алгоритмизации и программирования 2 (стр. 24 из 32)

Последовательность выполнения конструкторов

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

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

Для того чтобы создать экземпляр производного класса, необходимо создать экземпляр его базового класса В свою очередь, чтобы создать экземпляр этого базового класса, требуется создать экземпляр базового класса этого базового класса – и так далее до System.Object. В результате, какой бы конструктор ни использовался для создания класса, System.Object.Object() всегда будет вызываться первым.

Если используется конструктор класса не по умолчанию, то в таком случае по умолчанию будет использоваться конструктор базового класса, сигнатура которого совпадает с сигнатурой данного конструктора. Если таковой обнаружить не удается, то используется конструктор базового класса по умолчанию (это происходит всегда, за исключением корневого класса System.Object, поскольку у него отсутствуют конструкторы, использующиеся не по умолчанию) Давайте рассмотрим некоторый пример, который поможет проиллюстрировать последовательность событий. Рассмотрим следующую иерархию объектов:

public class MyBaseClass

{

public MyBaseClass ()
public MyBaseClass (int i) { }

{

}

}

public class MyDerivedClass : MyBaseClass

{

public MyDerivedClass()

{

public MyDerivedClass(int i)
public MyDerivedClass(int i, int j) { }

}

{

}

}

Если мы попытаемся создать экземпляр класса MyDerivedClass следующим образом:

MyDerivedClass myObj = new MyDerivedClass () ; то это приведет к такой последовательности событий:

- выполнится конструктор System.Object.Object ();

- выполнится конструктор MyBaseClass.MyBaseClass(); - выполнится конструктор MyDerivedClass.MyDerivedClass ().

Если же мы попытаемся создать экземпляр класса таким образом:

MyDerivedClass myObj = new MyDerivedClass(4); то соответствующая последовательность будет иметь следующий вид:

- выполнится конструктор System.Object.Object ();

- выполнится конструктор MyBaseClass.MyBaseClass(int i); - выполнится конструктор MyDerivedClass.MyDerivedClass (int i).

Наконец, если мы воспользуемся следующим вариантом: MyDerivedClass myObj = new MyDerivedClass(4, 8); то произойдет вот что:

- выполнится конструктор System. Object. Object();

- выполнится конструктор MyBaseClass.MyBaseClass();

- выполнится конструктор MyDerivedClass.MyDerivedClass(int i, int j).

При таком подходе мы получаем возможность поместить код, ответственный за обработку параметра int i, в MyBaseClass(int i), подразумевая, что конструктору MyDerivedClass(int i, int j) достанется меньше работы – ему придется обрабатывать только параметр int j (подобные рассуждения строятся на основе предположения, что параметр int i в обоих случаях имеет один и тот же смысл, что может и не выполняться, хотя на практике при таком способе оформления обычно выполняется ). С# при необходимости позволяет нам задать именно такой тип поведения.

Для этого требуется просто указать конструктор базового класса в определении конструктора нашего производного класса следующим образом:

public class MyDerivedClass : MyBaseClass

{

public MyDerivedClass (int i, int j) : base(i)
{ }

}

Ключевое слово base указывает NET, что в процессе создания экземпляра следует использовать конструктор базового класса с сигнатурой, совпадающей с заданной. В данном случае мы задействуем единственный параметр int, поэтому будет вызван MyBaseCiass(int i) конструктор MyBaseClass () не будет вызываться, т. е. последовательность событий будет такой же, как и в последнем примере,– что, собственно, в данном случае и требовалось.

Существует возможность с помощью этого же ключевого слова задавать литеральные значения для конструкторов базового класса, допустим, применяя конструктор класса MyDerivedClass, использующийся по умолчанию, для вызова конструктора класса MyBaseClass, использующегося не по умолчанию:

public class MyDerivedClass : MyBaseClass { public MyDerivedClass() : base(5)

{ }

}

В этом случае последовательность событий будет иметь такой вид - выполнится конструктор System Object.Object(); - выполнится конструктор MyBaseClass MyBaseClass(int i);

- выполнится конструктор MyDerivedClass MyDerivedClass().

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

public class MyDerivedClass : MyBaseClass

{

public MyDerivedClass() : this (5, 6)

{ }

public MyDerivedClass(int i, int j) : based) { }

}

Это приведет к такой последовательности событий:

- выполнится конструктор System Object.Object();

- выполнится конструктор MyBaseClass.MyBaseClass(int i);

- выполнится конструктор MyDerivedClass.MyDerivedClass (int i, int j) - выполнится конструктор MyDerivedClass.MyDerivedClass ().

Единственным ограничением в данном случае является задание только одного конструктора, использующего ключевые слова base или this. На самом деле, как было продемонстрировано в предыдущем примере, это не такое уж серьезное ограничение, поскольку все равно остается возможность конструирования чрезвычайно изощренных последовательностей выполняемых действий.

Мы увидим этот способ в действии немного позже

6.3.Типы структур

В предыдущей главе мы обращали внимание на то, что структуры и классы очень похожи, хотя структуры представляют собой значимые типы, а классы – ссылочные типы. Что же это может означать на самом деле? Самый простой способ разобраться в этом – рассмотреть некоторый пример.

ПРАКТИКУМ: сравнение классов и структур

1. Создайте новое консольное приложение.

2. Измените код следующим образом:

namespace C {

class MyClass

{

public int val;

}

struct myStruct { public int val; } /// <summary>

/// Summary description for Classl.

/// </summary>

class Classl

{

static void Main(string[] args) {

MyClass objectA = new MyClass ();MyClass objectB = objectA; objectA. val = 10; objectB.val = 20;

myStruct structA = new myStruct(); myStruct structB = structA; structA.val = 30; structB.val = 40;

Console.WriteLine("objectA.val = {0}", objectA.val); Console.WriteLine("objects.val = {0}", objectB.val); Console.WriteLine("structA.val = {0}", structA.val);

Console.WriteLine("structB.val = {0}", structB.val);

Console.ReadLine(); }

}

}

3. Запустите приложение: Как это работает

В этом приложении содержатся два определения типов, одно из которых описывает структуру с именем myStruct, обладающую единственным общим полем типа int с именем val, а второе описывает класс с именем MyClass, который содержит аналогичное поле (мы будем рассматривать члены классов, такие как поля, в следующей главе, а пока вам достаточно знать, что синтаксис в данном случае используется точно такой же). Далее мы выполняем одинаковые операции над экземплярами обоих типов:

- объявляем переменную данного типа;

- создаем новый экземпляр переменной данного типа; - объявляем вторую переменную данного типа;

- присваиваем первую переменную второй переменной;

- присваиваем значение полю val первого экземпляра переменной;

- присваиваем значение полю val второго экземпляра переменной; - выводим значение поля val обеих переменных.

Хотя мы выполняем над переменными разных типов одни и те же операции, окончательные результаты оказываются различными. При выводе значений поля val обнаруживается, что обе переменные типа object обладают одинаковым значением, в то время как переменные типа структуры имеют различающиеся значения. Итак, что же произошло?

Объекты – это ссылочные типы. Когда мы присваиваем объект некоторой переменной, мы на самом деле присваиваем этой переменной указатель, который ссылается на этот объект. С точки зрения программы указатель – это некий адрес в памяти. В данном случае это адрес, по которому располагается объект. Когда мы присваиваем ссылку на первый объект второй переменной типа Myciass, то мы, по сути, копируем этот адрес. Это означает, что обе переменных содержат ссылки на один и тот же объект.

Структуры – это значимые типы. В переменных не хранится никаких ссылок на структуру, вместо этого в них хранится сама структура. Поэтому, когда одна структура присваивается другой переменной типа mystruct, происходит копирование всей информации из одной структуры в другую. Это поведение идентично тому, которое мы наблюдали ранее для переменных простых типов, таких как тип int. Поэтому в качестве конечного результата мы получаем две переменные, содержащие разные структуры.

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

6. 4.Неглубокое и глубокое копирование

Копирование объектов из одной переменной в другую по значению, а не по ссылке (т. е. копирование тем же способом, который используется при копировании структур) иногда оказывается делом весьма сложным. Поскольку конкретный объект может содержать большое количество ссылок на другие объекты – например, в виде своих полей, этот процесс может потребовать выполнения очень большого количества действий. Простое копирование каждого члена одного объекта в другой может не сработать, потому что некоторые из этих членов сами по себе могут иметь ссылочные типы.