Конструкторы

Конструктор предназначен для инициализации объекта. Он вызывается авто­матически при создании объекта класса с помощью операции new. Имя конст­руктора совпадает с именем класса. Ниже перечислены свойства конструкторов:

□ Конструктор не возвращает значение, даже типа void.

□ Класс может иметь несколько конструкторов с разными параметрами для раз­
ных видов инициализации.

□ Если программист не указал ни одного конструктора или какие-то поля не
были инициализированы, полям значимых типов присваивается нуль, полям
ссылочных типов — значение null.

□ Конструктор, вызываемый без параметров, называется конструктором по
умолчанию.

До сих пор мы задавали начальные значения полей класса при описании класса (см., например, листинг 5.1). Это удобно в том случае, когда для всех экземпляров класса начальные значения некоторого поля одинаковы. Если же при создании объектов требуется присваивать полю разные значения, это следует делать в кон­структоре. В листинге 5.6 в класс Demo добавлен конструктор, а поля сделаны закрытыми (ненужные в данный момент элементы опущены). В программе соз­даются два объекта с различными значениями полей.

Листинг 5.6. Класс с конструктором

using System;

namespace ConsoleApplication1

{

class Demo

{

public Demo(int a, double у) // конструктор с параметрами

{

this.a = а; this.у = у;

}

public double Gety() // метод получения поля у

{

return у;

}

int а; double у;

}

class Class1

{

static void Main()

{

Demo a = new Demo(300, 0.002); // вызов конструктора

Console.WriteLine(a.Gety()); // результат: 0,002

Demo b = new Demo(1, 5.71); // вызов конструктора

Console.WriteLine(b.Gety()); // результат: 5,71

}

}

}

Часто бывает удобно задать в классе несколько конструкторов, чтобы обес­печить возможность инициализации объектов разными способами. Следую­щий пример несколько «притянут за уши», но тем не менее иллюстрирует этот тезис:

class Demo

{

public Demo(int a) // конструктор 1

{

this.a = a; this.у = 0.002; }

public Demo(double у) // конструктор 2

{

this.a = 1;

this.у = у;

}

}

Demo x = new Demo(300); // вызов конструктора 1

Demo у = new Demo(5.71); // вызов конструктора 2

}

Все конструкторы должны иметь разные сигнатуры.

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

class Demo

{

public Demo(int a) // конструктор 1

{

this.a = a;

}

public Demo(int a, double у)

: this(a) // вызов конструктора 1

{

this.у = у;

}

}

Конструкция, находящаяся после двоеточия, называется инициализатором, то есть тем кодом, который исполняется до начала выполнения тела конст­руктора.

Как вы помните, все классы в С# имеют общего предка — класс object. Конст­руктор любого класса, если не указан инициализатор, автоматически вызывает конструктор своего предка. Это можно сделать и явным образом с помощью ключевого слова base, обозначающего конструктор базового класса. Таким обра­зом, первый конструктор из предыдущего примера можно записать и так:

public Demo(int a): base() // конструктор 1

{

this.a = а;

}

ПРИМЕЧАНИЕ

Конструктор базового класса вызывается явным образом в тех случаях, когда ему требуется передать параметры.

До сих пор речь шла об «обычных» конструкторах, или конструкторах экземп­ляра. Существует второй тип конструкторов — статические конструкторы, или конструкторы класса. Конструктор экземпляра инициализирует данные экземп­ляра, конструктор класса — данные класса.

Статический конструктор не имеет параметров, его нельзя вызвать явным об­разом. Система сама определяет момент, в который требуется его выполнить. Гарантируется только, что это происходит до создания первого экземпляра объ­екта и до вызова любого статического метода.

Некоторые классы содержат только статические данные, и, следовательно, созда­вать экземпляры таких объектов не имеет смысла. Чтобы подчеркнуть этот факт, в первой версии С# описывали пустой закрытый (private) конструктор. Это предотвращало попытки создания экземпляров класса. В листинге 5.7 приведен пример класса, который служит для группировки величин. Создавать экземпля­ры этого класса запрещено.

Листинг 5.7. Класс со статическим и закрытым конструкторами (для версий ниже 2.0)

using System;

namespace ConsoleApplication1

{

class D

{

private D() { } // закрытый конструктор

static D() // статический конструктор.

{

a = 200;

}

static int a;

static double b = 0.002;

public static void Print()

{

Console.WriteLine("a = " + a);

Console.WriteLine("b = " + b);

}

}

class Class2

{

static void Main()

{

D.Print();

//D d = new D(); // ошибка: создать экземпляр невозможно

}

}

}

ПРИМЕЧАНИЕ

В классе, состоящем только из статических элементов (полей и констант), описы­вать статический конструктор не обязательно, начальные значения полей удобнее задать при их описании.

В версию 2.0 введена возможность описывать статический класс, то есть класс с модификатором static. Экземпляры такого класса создавать запрещено, и кроме того, от него запрещено наследовать. Все элементы такого класса должны явным образом объявляться с модификатором static (константы и вложенные типы классифицируются как статические элементы автоматически). Конструктор эк­земпляра для статического класса задавать, естественно, запрещается.

В листинге 5.8 приведен пример статического класса.

Листинг 5.8. Статический класс (начиная с версии 2.0)

using System;

namespace ConsoleApplication1

{

static class D

{

static int a = 200; static double b = 0.002;

public static void Print()

{

Console.WriteLine("a = " + a);

Console.WriteLine("b = " + b);

}

}

class Class1

{

static void Main()

{

D.Print();

}

}

}

В качестве «сквозного» примера, на котором будет демонстрироваться работа с различными элементами класса, создадим класс, моделирующий персонаж компьютерной игры. Для этого требуется задать его свойства (например, коли­чество щупальцев, силу или наличие гранатомета) и поведение. Естественно, пример (листинг 5.9) является схематичным, поскольку приводится лишь для демонстрации синтаксиса.

Листинг 5.9. Класс Monster

using System;

namespace ConsoleApplication1

{

class Monster

{

public Monster()

{

this.name = "Noname";

this.health = 100;

this.ammo = 100;

}

public Monster(string name)

: this()

{

this.name = name;

}

public Monster(int health, int ammo, string name)

{

this.name = name;

this.health = health;

this.ammo = ammo;

}

public int GetName()

{

return name;

}

public int GetHealth()

{

return health;

}

public int GetAmmo()

{

return ammo;

}

public void Passport()

{

Console.WriteLine("Monster {0} \t health = {1} ammo = {2}",

name, health, ammo);

}

string name; // закрытые поля

int health, ammo;

}

class Class1

{

static void Main()

{

Monster X = new Monster();

X.Passport();

Monster Vasia = new Monster("Vasia");

Vasia.Passport();

Monster Masha = new Monster(200, 200, "Masha");

Masha.Passport();

}

}

}

Результат работы программы:

Monster Noname health = 100 ammo = 100

Monster Vasia health = 100 ammo = 100

Monster Masha health = 200 ammo = 200

В классе три закрытых поля (name, health и ammo), четыре метода (GetName, GetHealth, GetAmmo и Passport) и три конструктора, позволяющие задать при создании объек­та ни одного, один или три параметра.

Свойства

Свойства служат для организации доступа к полям класса. Как правило, свойст­во связано с закрытым полем класса и определяет методы его получения и уста­новки. Синтаксис свойства: [ атрибуты ] [ спецификаторы ] тип имя_свойства

{

[ get код_доступа ]

[ set код_доступа ]

}

Значения спецификаторов для свойств и методов аналогичны. Чаще всего свой­ства объявляются как открытые (со спецификатором public), поскольку они вхо­дят в интерфейс объекта.

Код доступа представляет собой блоки операторов, которые выполняются при получении (get) или установке (set) свойства. Может отсутствовать либо часть get, либо set, но не обе одновременно.

Если отсутствует часть set, свойство доступно только для чтения (read-only), если отсутствует часть get, свойство доступно только для записи (write-only).

В версии С# 2.0 введена удобная возможность задавать разные уровни доступа для частей get и set. Например, во многих классах возникает потребность обеспе­чить неограниченный доступ для чтения и ограниченный — для записи.

Спецификаторы доступа для отдельной части должны задавать либо такой же, либо более ограниченный доступ, чем спецификатор доступа для свойства в целом. На­пример, если свойство описано как publiс, его части могут иметь любой специфика­тор доступа, а если свойство имеет доступ protected internal, его части могут объяв­ляться как internal, protected или private. Синтаксис свойства в версии 2.0 имеет вид

[ атрибуты ] [ спецификаторы ] тип имя_свойства

{

[ [ атрибуты ] [ спецификаторы ] get код_доступа ]

[ [ атрибуты ] [ спецификаторы ] set код_доступа ]

}

Пример описания свойств:

public class Button: Control

{

private string caption; // закрытое поле, с которым связано свойство

public string Caption // свойство

{

get // способ получения свойства

{

return caption;

}

set // способ установки свойства

{

if (caption!= value)

caption = value;

}

}

}

Метод записи обычно содержит действия по проверке допустимости устанавли­ваемого значения, метод чтения может содержать, например, поддержку счетчи­ка обращений к полю.

В программе свойство выглядит как поле класса, например:

Button ok = new Button();

ok.Caption = "OK"; // вызывается метод установки свойства

string s = ok.Caption; // вызывается метод получения свойства

При обращении к свойству автоматически вызываются указанные в нем методы чтения и установки.

Синтаксически чтение и запись свойства выглядят почти как методы. Метод get должен содержать оператор return, возвращающий выражение, для типа которо­го должно существовать неявное преобразование к типу свойства. В методе set используется параметр со стандартным именем value, который содержит уста­навливаемое значение.

Вообще говоря, свойство может и не связываться с полем. Фактически, оно опи­сывает один или два метода, которые осуществляют некоторые действия над данными того же типа, что и свойство. В отличие от открытых полей, свойства обеспечивают разделение между внутренним состоянием объекта и его интер­фейсом и, таким образом, упрощают внесение изменений в класс.

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

class A

{

private static ComplexObject x; // закрытое поле

public static ComplexObject X // свойство

{

get

{

if (x == null)

x = new ComplexObject(); // создание объекта при 1-м обращении

return x;

}

}

}

Добавим в класс Monster, описанный в листинге 5.9, свойства, позволяющие рабо­тать с закрытыми полями этого класса. Свойство Name сделаем доступным только для чтения, поскольку имя объекта задается в конструкторе и его изменение не предусмотрено1, в свойствах Health и Ammo введем проверку на положительность устанавливаемой величины. Код класса несколько разрастется, зато упростится его использование.

Листинг 5.10. Класс Monster со свойствами

using System;

namespace ConsoleApplication1

{

class Monster

{

public Monster()

{

this.health = 100;

this.ammo = 100;

this.name = "Noname";

}

public Monster(string name)

: this()

{

this.name = name;

}

public Monster(int health, int ammo, string name)

{

this.health = health;

this.ammo = ammo;

this.name = name;

}

public int Health

// свойство Health связано с полем health

{

get

{

return health;

}

set

{

if (value > 0) health = value;

else

health = 0;

}

}

public int Ammo // свойство Ammo связано с полем ammo

{

get

{

return ammo;

}

set

{

if (value > 0) ammo = value;

else ammo = 0;

}

}

public string Name // свойство Name связано с полем name

{

get

{

return name;

}

}

public void Passport()

{

Console.WriteLine("Monster {0} \t health = {1} ammo = {2}", name, health, ammo);

}

string name; // закрытые поля

int health, ammo;

}

class Class1

{

static void Main()

{

Monster Masha = new Monster(200, 200, "Masha");

Masha.Passport();

--Masha.Health; // использование cвойств

Masha.Ammo += 100; // использование свойств

Masha.Passport();

}

}

}

Результат работы программы:

Monster Masha health = 200 ammo = 200

Monster Masha health - 199 ammo = 300

Рекомендации по программированию

При создании класса, то есть нового типа данных, следует хорошо продумать его интерфейс — средства работы с классом, доступные использующим его програм­мистам. Интерфейс хорошо спроектированного класса интуитивно ясен, непро­тиворечив и обозрим. Как правило, он не должен включать поля данных. Поля предпочтительнее делать закрытыми (private). Это дает возможность впо­следствии изменить реализацию класса без изменений в его интерфейсе, а также регулировать доступ к полям класса с помощью набора предоставляемых поль­зователю свойств и методов. Важно помнить, что поля класса вводятся только для того, чтобы реализовать характеристики класса, представленные в его интер­фейсе с помощью свойств и методов.

Не нужно расширять интерфейс класса без необходимости, «на всякий слу­чай», поскольку увеличение количества методов затрудняет понимание класса пользователем1. В идеале интерфейс должен быть полным, то есть предоставлять возможность выполнять любые разумные действия с классом, и одновременно минимально необходимым — без дублирования и пересечения возможностей ме­тодов.

Методы определяют поведение класса. Каждый метод класса должен решать только одну задачу (не надо объединять два коротких независимых фрагмента кода в один метод). Размер метода может варьироваться в широких пределах, все зависит от того, какие функции он выполняет. Желательно, чтобы тело метода помещалось на 1-2 экрана: одинаково сложно разбираться в программе, содер­жащей несколько необъятных функций, и в россыпи из сотен единиц по не­сколько строк каждая.

Если метод реализует сложные действия, следует разбить его на последователь­ность шагов и каждый шаг оформить в виде вспомогательной функции (метода со спецификатором private). Если некоторые действия встречаются в коде хотя бы дважды, их также нужно оформить в виде отдельной функции.

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

Предпочтительнее, чтобы каждая функция вычисляла ровно один результат, одна­ко это не всегда оправдано. Если величина вычисляется внутри функции и воз­вращается из нее через список параметров, необходимо использовать перед соот­ветствующим параметром ключевое слово out. Если параметр значимого типа может изменить свою величину внутри функции, его предваряют ключевым сло­вом ref. Величины ссылочного типа всегда передаются по адресу и, следователь­но, могут изменить внутри функции свое значение.

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

Поля, характеризующие класс в целом, то есть имеющие одно и то же значение для всех экземпляров, следует описывать как статические. Все литералы, связан­ные с классом (числовые и строковые константы), описываются как поля-кон­станты с именами, отражающими их смысл.

При написании кода методов следует руководствоваться правилами, приведен­ными в аналогичном разделе главы 4 (см. с. 95).


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: