Прочитав эту главу, вы научитесь:
• инкапсулировать логические поля с помощью свойств;
• управлять доступом к свойствам для чтения из них информации, объявляя методы доступа get;
• управлять доступом к свойствам для записи в них информации, объявляя методы доступа set;
• создавать интерфейсы, объявляющие свойства;
• реализовывать интерфейсы, содержащие свойства, путем использования структур и классов;
• создавать свойства в автоматическом режиме на основе определения полей;
• использовать свойства для инициализации объектов.
В этой главе рассматриваются способы определения и использования свойств для инкапсуляции полей и данных в классе. В предыдущих главах привлекалось внимание к тому, что нужно поля в классе делать закрытыми и предоставлять методы для хранения в полях значений, а также извлечения этих значений из полей. Такой подход гарантирует безопасный и контролируемый доступ к полям, и его можно использовать для инкапсуляции дополнительной логики и правил, применяемых к допустимым значениям. Но синтаксис для доступа к полю таким способом выглядит не вполне естественно. Когда нужно считать или записать значение переменной, обычно используется инструкция присваивания, поэтому вызов метода для получения такого же эффекта в отношении поля (которое по своей сути представляет собой переменную) выглядит неуклюжим. Для устранения этой несуразности и были разработаны свойства.
Сначала давайте вернемся к исходной мотивации использования методов для сокрытия полей.
Рассмотрим следующую структуру, представляющую позицию на экране компьютера в виде пары координат x и y. Предположим, допустимые значения для координаты x находятся в диапазоне от 0 до 1280, а для координаты y — от 0 до 1024:
struct ScreenPosition
{
public int X;
public int Y;
public ScreenPosition(int x, int y)
{
this.X = rangeCheckedX(x);
this.Y = rangeCheckedY(y);
}
private static int rangeCheckedX(int x)
{
if (x < 0 || x > 1280)
{
throw new ArgumentOutOfRangeException("X");
}
return x;
}
private static int rangeCheckedY(int y)
{
if (y < 0 || y > 1024)
{
throw new ArgumentOutOfRangeException("Y");
}
return y;
}
}
Одной из проблем, связанных с этой структурой, является то, что она не следует золотому правилу инкапсуляции, то есть не хранит свои данные в закрытом состоянии. Открытые данные зачастую далеко не лучший вариант, поскольку класс не в состоянии контролировать значения, указываемые приложением. Например, конструктор ScreenPosition проверяет свои параметры, убеждаясь, что они находятся в указанном диапазоне, но такую проверку невозможно выполнить при необработанном доступе к открытому полю. Рано или поздно (вероятнее всего, совсем скоро) ошибка или недопонимание разработчиком принципов использования этого класса в приложении приведут к тому, что либо X, либо Y выйдет за пределы своего диапазона:
ScreenPosition origin = new ScreenPosition(0, 0);
...
int xpos = origin.X;
origin.Y = -100; // вот так!
Стандартный способ решения данной проблемы заключается в том, чтобы сделать эти поля закрытыми и добавить к ним метод доступа и метод изменения, чтобы выполнять чтение и запись значения каждого закрытого поля соответственно. После чего методы изменения смогут проверять диапазон значений для нового поля. Например, следующий код содержит для поля X метод доступа (GetX) и метод изменения (SetX). Обратите внимание на то, как метод SetX проверяет переданный ему параметр:
struct ScreenPosition
{
...
public int GetX()
{
return this.x;
}
public void SetX(int newX)
{
this.x = rangeCheckedX(newX);
}
...
private static int rangeCheckedX(int x) { ... }
private static int rangeCheckedY(int y) { ... }
private int x, y;
}
Теперь в соответствии с нашими требованиями в коде успешно применяются ограничения, соответствующие диапазону допустимых значений. Но за полезные гарантии приходится платить — у ScreenPosition теперь нет естественного синтаксиса, присущего полям, а вместо него используется неудобный синтаксис на основе методов. В следующем примере значение X увеличивается на 10 единиц. Чтобы добиться нужного результата, нужно прочитать значение X, воспользовавшись методом доступа GetX, а затем записать значение X, воспользовавшись методом изменения SetX.
int xpos = origin.GetX();
origin.SetX(xpos + 10);
Сравним это с эквивалентным кодом, используемым при открытом поле X:
origin.X += 10;
Несомненно, в данном случае использование открытых полей синтаксически понятнее, короче и легче, но, к сожалению, это нарушает инкапсуляцию. Используя свойства, можно сочетать все лучшие стороны обоих миров (полей и методов), сохраняя инкапсуляцию и предоставляя синтаксис, который похож на тот, что используется с полями.
Свойство — это гибрид поля и метода: оно выглядит как поле, но работает как метод. Доступ к свойству осуществляется с использованием точно такого же синтаксиса, какой используется для доступа к полю. Но компилятор автоматически переводит синтаксис, похожий на тот, который используется для поля, в вызов методов доступа, которые иногда называются получателями (getters) и установщиками (setters) свойств.
Синтаксис для объявления свойства выглядит следующим образом:
AccessModifier Type PropertyName
{
get
{
// код метода доступа к чтению свойства
}
set
{
// код метода доступа к записи свойства
}
}
В свойстве могут содержаться два блока кода, начинающиеся с ключевых слов get и set. Блок get содержит инструкции, выполняемые при чтении свойства, а блок set содержит инструкции, выполняемые при записи в свойство. Тип свойства определяет тип данных, считываемых из этого свойства или записываемых в него, методами доступа get и set.
В следующем примере кода показана структура ScreenPosition, переписанная с использованием свойств. Изучая код, обратите внимание на следующее:
• записанные символами в нижнем регистре _x и _y являются закрытыми полями;
• записанные символами в верхнем регистре X и Y являются открытыми свойствами;
• все данные, передаваемые установленным методам доступа, должны быть записаны с помощью скрытого встроенного параметра по имени value:
struct ScreenPosition
{
private int _x, _y;
public ScreenPosition(int X, int Y)
{
this._x = rangeCheckedX(X);
this._y = rangeCheckedY(Y);
}
public int X
{
get { return this._x; }
set { this._x = rangeCheckedX(value); }
}
public int Y
{
get { return this._y; }
set { this._y = rangeCheckedY(value); }
}
private static int rangeCheckedX(int x) { ... }
private static int rangeCheckedY(int y) { ... }
}
В данном примере непосредственная реализация каждого свойства осуществляется с использованием закрытого поля, но это лишь один из способов реализации свойства. Требования ограничиваются только тем, что метод доступа get должен возвращать значение заданного типа. Вместо того чтобы просто извлекаться из сохраненных данных, значение может легко вычисляться в динамическом режиме, и в таком случае надобность в наличии физического поля отпадает.
ПРИМЕЧАНИЕ Хотя примеры в данной главе показывают способы определения свойств для структур, они в равной степени применимы и к классам, для которых используется точно такой же синтаксис.
Предупреждение, касающееся имен свойств и полей
В главе 2 «Работа с переменными, операторами и выражениями» в разделе «Правила присваивания имен переменным» рассматривался ряд рекомендаций по присваиванию имен переменным. В частности, там утверждалось, что использование в начале имени идентификатора символа подчеркивания следует избегать. Но, как видите, в структуре ScreenPosition допущено отступление от этой рекомендации, поскольку здесь содержатся поля с именами _x и _y. Для этого исключения из правил есть довольно веская причина. Во врезке «Задание имен и доступность» в главе 7 «Создание классов и объектов и управление ими» утверждалось, что идентификаторы, начинающиеся с буквы в верхнем регистре, чаще всего используются для открытых методов и полей, а идентификаторы, начинающиеся с буквы в нижнем регистре, — для закрытых методов и полей. Если придерживаться обоих правил, это может привести к тому, что имена свойств и закрытых полей будут различаться лишь регистром первой буквы, и во многих организациях такое положение вещей считается вполне допустимым.
Если в вашей организации используется такой же подход, следует иметь в виду один его существенный недостаток. Посмотрите на следующий код, в котором реализуется класс по имени Employee. Поле employeeID является закрытым, а свойство EmployeeID предоставляет открытый доступ к нему:
class Employee
{
private int employeeID;
public int EmployeeID
{
get { return this.EmployeeID; }
set { this.EmployeeID = value; }
}
}
Код без проблем откомпилируется, но в результате при обращении к свойству EmployeeID программа выдаст исключение переполнения стека — StackOverflowException. Дело в том, что методы доступа get и set относятся к свойству (имя которого начинается с большой буквы «E»), а не к закрытому полю (имя которого начинается с маленькойй буквы «e»), что вызывает бесконечный рекурсивный цикл, который в итоге приведет к исчерпанию доступной памяти. Подобную ошибку отследить очень трудно! Поэтому в примерах данной книги для закрытых полей, используемых для предоставления свойствам данных, применяются имена с лидирующим символом подчеркивания, из-за чего их становится намного проще отличить от имен свойств. Для всех остальных закрытых полей будут по-прежнему использоваться идентификаторы в смешанном регистре без начального символа подчеркивания.
В выражении свойство можно использовать в контексте чтения (когда его значение извлекается) и в контексте записи (когда его значение изменяется). В следующем примере показано, как значение читается из свойств X и Y структуры ScreenPosition:
ScreenPosition origin = new ScreenPosition(0, 0);
int xpos = origin.X; // вызывается origin.X.get
int ypos = origin.Y; // вызывается origin.Y.get
Обратите внимание на то, что для обращения к свойствам и полям используется одинаковый синтаксис. Когда свойство используется в контексте чтения, компилятор автоматически преобразует ваш код, похожий на код, использующий поле, в вызов метода get этого свойства. Аналогично этому, если используется контекст записи, компилятор автоматически преобразует ваш код, похожий на код, использующий поле, в вызов метода set этого свойства:
origin.X = 40; // вызывается origin.X.set со значением value, равным 40
origin.Y = 100; // вызывается origin.Y.set со значением value, равным 100
Как упоминалось в предыдущем разделе, присваиваемые значения передаются в метод доступа set с использованием переменной value. Среда исполнения делает это автоматически.
Свойство можно использовать также в контексте чтения/записи. В таком случае используются оба метода доступа, как get, так и set. Например, компилятор автоматически переводит такие инструкции, как следующая, в вызовы методов доступа get и set:
origin.X += 10;
СОВЕТ Статические свойства можно объявлять точно так же, как и статические поля и методы. Обратиться к статическим свойствам можно с использованием имени класса или структуры, а не их экземпляра.
Можно объявить свойство, содержащее только метод доступа get. В таком случае свойство можно будет использовать только в контексте чтения. Например, здесь свойство X структуры ScreenPosition объявлено как свойство только для чтения:
struct ScreenPosition
{
private int _x;
...
public int X
{
get { return this._x; }
}
}
У свойства X нет метода доступа set, поэтому, как показано в следующем примере, любая попытка использования X в контексте записи будет неудачной:
origin.X = 140; // ошибка в ходе компиляции
Аналогичным образом можно объявить свойство, содержащее лишь метод доступа set. В таком случае свойство можно будет использовать только в контексте записи. Например, в следующем фрагменте кода свойство X структуры ScreenPosition объявлено как свойство только для записи:
struct ScreenPosition
{
private int _x;
...
public int X
{
set { this._x = rangeCheckedX(value); }
}
}
В свойстве X не содержится метод доступа get, и как показано в следующем примере, любая попытка использования X для чтения закончится неудачей:
Console.WriteLine(origin.X); // ошибка в ходе компиляции
origin.X = 200; // компиляция проходит успешно
origin.X += 10; // ошибка в ходе компиляции
ПРИМЕЧАНИЕ Свойства, предназначенные только для чтения, хорошо подходят для конфиденциальных данных, таких как пароли. В идеале приложение, принимающее меры безопасности, должно позволить устанавливать пароль, но никогда не должно позволять его считывать. При попытке войти в систему пользователь должен сообщить пароль. Метод, выполняющий регистрацию, может сравнить этот пароль с сохраненным паролем и должен возвращать лишь свидетельство об их совпадении.
Степень доступности свойства можно указать при его объявлении, воспользовавшись ключевыми словами public, private или protected. Но внутри объявления свойства доступность для методов доступа get и set можно переопределить. Например, показанная в следующем примере кода структура ScreenPosition объявляет методы доступа set свойств X и Y закрытыми (private). (Методы доступа get являются открытыми, потому что открытыми являются сами свойства.)
struct ScreenPosition
{
private int _x, _y;
...
public int X
{
get { return this._x; }
private set { this._x = rangeCheckedX(value); }
}
public int Y
{
get { return this._y; }
private set { this._y = rangeCheckedY(value); }
}
...
}
При определении методов доступа с различной степенью доступности следует соблюдать ряд правил.
• При определении методов доступность можно изменить только одному из них. Нет смысла определять свойство открытым только для того, чтобы изменить доступность обоих методов, объявляя их закрытыми.
• Модификатор доступности не должен определять доступность, накладывающую меньшее ограничение, чем то, что наложено на само свойство. Например, если свойство объявлено закрытым (private), метод доступа для чтения не может быть открытым (public). (Вместо этого следовало бы объявить свойство открытым, а метод доступа для записи — закрытым.)
Когда данные считываются и записываются с использованием свойств, эти свойства выглядят, работают и воспринимаются как поля. Но на самом деле они не являются полями, и на них накладываются определенные ограничения.
• Присвоить значение через свойство структуры или класса можно только после инициализации структуры или класса. Следующий пример кода некорректен, потому что переменная location не была инициализирована (с использованием ключевого слова new):
ScreenPosition location;
location.X = 40; // ошибка в ходе компиляции, значение location
// не присваивается
ПРИМЕЧАНИЕ Может быть, это и покажется несущественным, но если бы идентификатор X указывал на поле, а не на свойство, то код был бы вполне работоспособным. Поэтому структуры и классы с самого начала нужно определять с использованием свойств, а не полей, которые позже вы будете превращать в свойства. Код, использующий ваши структуры и классы, после превращения полей в свойства может утратить работоспособность. Данный вопрос будет рассматриваться также в разделе «Создание свойств в автоматическом режиме».
• Свойства нельзя использовать в качестве ref- или out-аргумента метода (хотя поле с доступом для записи в данном качестве использовать можно). В этом есть определенный смысл, поскольку свойство на самом деле указывает не на место в памяти, а на метод доступа, поэтому код следующего примера некорректен:
MyMethod(ref location.X); // ошибка в ходе компиляции
• Свойство должно содержать не более одного метода доступа get и не более одного метода доступа set. Свойство не может содержать другие методы, поля или свойства.
• Методы доступа get и set не могут принимать параметры. Присваиваемые данные передаются методу доступа set автоматически с использованием переменной value.
• Свойства нельзя объявлять с использованием ключевого слова const:
const int X { get { ... } set { ... } } // ошибка в ходе компиляции
Правильное использование свойств
Свойства являются весьма эффективным средством программирования и при правильном использовании позволяют сделать код понятнее и легче в сопровождении. Но они не подменяют собой тщательно выверенную объектно-ориентированную конструкцию, основное внимание в которой уделяется поведению объектов, а не их свойствам. Сам по себе доступ к закрытым полям посредством обычных методов или свойств не превращает ваш код в удачно спроектированную программу. К примеру, на банковском счете хранится баланс, показывающий объем доступных средств. Это может побудить вас к созданию свойства по имени Balance, принадлежащего классу BankAccount:
class BankAccount
{
private decimal _balance;
...
public decimal Balance
{
get { return this._balance; }
set { this._balance = value; }
}
}
Подобное проектное решение нельзя признать удачным, поскольку в нем не отражается функциональная возможность, необходимая при снятии средств со счета или его пополнении. (Если вам известен банк, позволяющий изменять баланс напрямую, без физического помещения денег на счет, пожалуйста, сообщите мне об этом!) Старайтесь в процессе программирования выразить в решении суть решаемой задачи, не заблудившись в большом объеме низкоуровневого синтаксиса. Как показано в следующем примере, лучше вместо написания метода доступа по установке значения свойства предусмотреть наличие в классе BankAccount методов Deposit и Withdraw:
class BankAccount
{
private decimal _balance;
...
public decimal Balance { get { return this._balance; } }
public void Deposit(decimal amount) { ... }
public bool Withdraw(decimal amount) { ... }
}
Интерфейсы рассматривались в главе 13 «Создание интерфейсов и определение абстрактных классов». В интерфейсах могут объявляться как свойства, так и методы. Для этого нужно указать ключевое слово get или set или оба этих слова, но вместо тела методов доступа get или set следует поставить точку с запятой:
interface IScreenPosition
{
int X { get; set; }
int Y { get; set; }
}
Любой класс или структура, реализующие этот интерфейс, должны реализовывать также свойства X и Y с методами доступа get и set.
struct ScreenPosition : IScreenPosition
{
...
public int X
{
get { ... }
set { ... }
}
public int Y
{
get { ... }
set { ... }
}
...
}
Если свойства интерфейса реализуются в классе, можно объявить реализации свойств виртуальными, что позволит производным классам переопределять эти реализации:
class ScreenPosition : IScreenPosition
{
...
public virtual int X
{
get { ... }
set { ... }
}
public virtual int Y
{
get { ... }
set { ... }
}
...
}
ПРИМЕЧАНИЕ В данном примере показан класс. Следует напомнить, что использовать ключевое слово virtual при создании структур нельзя, потому что структуры не поддерживают наследование.
Можно также выбрать вариант реализации свойства путем использования синтаксиса явного определения интерфейса, рассмотренного в главе 13. Явная реализация свойства не должна иметь модификаторов public и virtual и не может быть переопределена:
struct ScreenPosition : IScreenPosition
{
...
int IScreenPosition.X
{
get { ... }
set { ... }
}
int IScreenPosition.Y
{
get { ... }
set { ... }
}
...
}
Изучая главу 13, вы научились создавать приложение для рисования фигур, с помощью которого пользователь может помещать на холст в окне круги и квадраты. В упражнениях, приводимых в этой главе, функциональность классов Circle и Square выносится в абстрактный класс по имени DrawingShape. Этот класс предоставляет методы SetLocation и SetColor, которые используются приложением для указания позиции и цвета фигуры на экране. В следующем упражнении класс DrawingShape будет изменен, чтобы он мог представлять местоположение и цвет фигуры в виде свойств.
Откройте в среде Visual Studio 2015 проект Drawing, который находится в папке \Microsoft Press\VCSBS\Chapter 15\Drawing Using Properties вашей папки документов. Выведите в окно редактора файл DrawingShape.cs. В нем содержится почти такой же класс DrawingShape, какой был показан в главе 13, за исключением того, что в нем учтены рекомендации, рассмотренные ранее в данной главе, и поле size переименовано в _size, а поля locX и locY — в _x и _y:
abstract class DrawingShape
{
protected int _size;
protected int _x = 0, _y = 0;
...
}
Откройте в окне редактора файл IDraw.cs проекта Drawing. Интерфейс в этом файле дает следующее определение метода SetLocation:
interface IDraw
{
void SetLocation(int xCoord, int yCoord);
...
}
Данный метод предназначен для установки для полей _x и _y объекта типа DrawingShape переданных ему значений. Этот метод может быть заменен двумя свойствами. Удалите метод SetLocation и поставьте на его место определение двух свойств с именами X и Y, выделенное здесь жирным шрифтом:
interface IDraw
{
int X { get; set; }
int Y { get; set; }
...
}
Удалите в классе DrawingShape метод SetLocation и поставьте вместо него следующие реализации свойств X и Y:
public int X
{
get { return this._x; }
set { this._x = value; }
}
public int Y
{
get { return this._y; }
set { this._y = value; }
}
Выведите в окно редактора файл DrawingPad.xaml.cs и найдите в нем метод drawingCanvas_Tapped. Этот метод запускается при касании экрана пальцем или щелчке левой кнопкой мыши. Он рисует на экране квадрат в точке касания или в точке нахождения указателя мыши при щелчке. Найдите инструкцию, вызывающую метод SetLocation для установки позиции квадрата на экране. Она находится в блоке инструкции if и выделена в следующем фрагменте кода жирным шрифтом:
if (mySquare is IDraw)
{
IDraw drawSquare = mySquare;
drawSquare.SetLocation((int)mouseLocation.X, (int)mouseLocation.Y);
drawSquare.Draw(drawingCanvas);
}
Замените инструкцию кодом, устанавливающим свойства X и Y объекта Square, выделенным здесь жирным шрифтом:
if (mySquare is IDraw)
{
IDraw drawSquare = mySquare;
drawSquare.X = (int)mouseLocation.X;
drawSquare.Y = (int)mouseLocation.Y;
drawSquare.Draw(drawingCanvas);
}
Найдите метод drawingCanvas_RightTapped. Он запускается при касании экрана пальцем и удержании пальца в точке касания или при щелчке правой кнопкой мыши. Он рисует на экране круг в точке касания и удержания пальца или в точке нахождения указателя мыши при щелчке.
Замените в методе инструкцию, вызывающую метод SetLocation объекта типа Circle, на установку значений для свойств X и Y, выделенную в следующем примере кода жирным шрифтом:
if (myCircle is IDraw)
{
IDraw drawCircle = myCircle;
drawCircle.X = (int)mouseLocation.X;
drawCircle.Y = (int)mouseLocation.Y;
drawCircle.Draw(drawingCanvas);
}
Откройте в окне редактора файл IColor.cs. Интерфейс в этом файле дает следующее определение метода SetColor:
interface IColor
{
void SetColor(Color color);
}
Удалите этот метод и поставьте на его место определение свойства по имени Color:
interface IColor
{
Color Color { set; }
}
Это предназначенное только для записи свойство предоставляет метод доступа set, но не предоставляет метод доступа get. Свойство определяется именно таким образом, потому что цвет еще не хранится в классе DrawingShape и указывается только при прорисовке каждой фигуры, то есть вы не можете сделать запрос к фигуре, чтобы определить, какого она цвета.
ПРИМЕЧАНИЕ По устоявшейся практике свойству дается такое же имя, что и типу (в данном случае это Color).
Вернитесь в окне редактора к классу DrawingShape. Замените в нем метод SetColor показанным здесь свойством Color:
public Color Color
{
set
{
if (this.shape != null)
{
SolidColorBrush brush = new SolidColorBrush(value);
this.shape.Fill = brush;
}
}
}
ПРИМЕЧАНИЕ Код для метода доступа set практически такой же, как и для исходного метода SetColor, за исключением того, что инструкции, создающей объект SolidColorBrush, передается параметр value.
Вернитесь в окне редактора к файлу DrawingPad.xaml.cs. Измените в методе drawingCanvas_Tapped код инструкции, устанавливающей цвет Square-объекта, чтобы он соответствовал коду, выделенному здесь жирным шрифтом:
if (mySquare is IColor)
{
IColor colorSquare = mySquare;
colorSquare.Color = Colors.BlueViolet;
}
Измените по аналогии с этим инструкцию, устанавливающую цвет Circle-объекта в методе drawingCanvas_RightTapped:
if (myCircle is IColor)
{
IColor colorCircle = myCircle;
colorCircle.Color = Colors.HotPink;
}
Щелкните в меню Отладка на пункте Начать отладку, чтобы инициировать сборку и запуск проекта.
Убедитесь в том, что приложение работает точно так же, как и раньше. При касании экрана или щелчке левой кнопкой мыши на холсте приложение должно рисовать квадрат, а при касании и удержании пальца или щелчке правой кнопкой мыши — окружность. При работе приложения должно получаться следующее изображение (рис. 15.1).
Рис. 15.1
Вернитесь в среду Visual Studio 2015 и остановите отладку.
Как уже упоминалось, основная задача свойств заключается в том, чтобы скрыть реализацию полей от внешнего мира. Хорошо, когда свойства действительно выполняют некую полезную работу, но если методы доступа get и set просто заключат в свой блок кода операции, которые всего лишь считывают значение поля или присваивают этому полю значение, ценность такого подхода может оказаться сомнительной. Но даже в таких ситуациях есть как минимум две веские причины, по которым следует определять свойства, а не выставлять данные в качестве открытых полей.
• Совместимость с приложениями. Поля и свойства выставляются путем использования в сборках различных метаданных. Если при разработке класса принимается решение об использовании открытых полей, то любые приложения, использующие этот класс, будут ссылаться на эти элементы как на поля. Хотя для чтения и записи полей в C# используется точно такой же синтаксис, как и для чтения и записи свойств, скомпилированный код различается, просто компилятор C# скрывает от вас эти различия. Если позже прийти к решению, что нужно заменить поля свойствами (возможно, в связи с изменением каких-то бизнес-требований и появлением необходимости в реализации дополнительной логики при присваивании значений), существующее приложение не сможет воспользоваться обновленной версией класса без перекомпиляции. Если приложение развернуто на большом количестве устройств в масштабе всей организации, возникнут затруднения. Существуют, конечно, обходные пути, но лучше все же в первую очередь побеспокоиться о том, чтобы не попасть в подобную ситуацию.
• Совместимость с интерфейсами. При реализации интерфейса, в котором элемент определен как свойство, нужно создать свойство, соответствующее спецификации в интерфейсе, даже если свойство просто считывает данные из закрытого поля и записывает их в него. Вы не можете реализовать свойство просто путем выставления открытого поля с таким же именем.
Разработчики языка C# понимали, что программисты — люди занятые и их не стоит заставлять попусту тратить время на написание не нужного им кода. По этой причине компилятор C# способен создавать код для свойств автоматически:
class Circle
{
public int Radius{ get; set; }
...
}
В этом примере в классе Circle содержится свойство по имени Radius. Вы указали лишь тип этого свойства, не указав, как оно работает, поскольку методы get и set оставлены пустыми. Компилятор C# преобразует это определение в закрытое поле и в исходную реализацию, имеющую следующий вид:
class Circle
{
private int _radius;
public int Radius{
get
{
return this._radius;
}
set
{
this._radius = value;
}
}
...
}
Таким образом, приложив минимум усилий, вы можете реализовать простое свойство, создав код автоматически, и если позже понадобится включить дополнительную логику, это можно будет сделать, не ломая существующих приложений.
ПРИМЕЧАНИЕ Синтаксис для определения автоматически создаваемого свойства практически идентичен синтаксису для определения свойства в интерфейсе. Исключение состоит в том, что в автоматически создаваемом свойстве можно указывать такие модификаторы доступа, как private, public или protected.
Автоматическое создание свойства, предназначенного только для чтения, можно инициировать, не указывая в объявлении метода доступа set:
class Circle
{
public DateTime CircleCreatedDate { get; }
...
}
Этот прием пригодится, если понадобится создать неизменяемое свойство, значение которого устанавливается при конструировании объекта и впоследствии не может быть изменено. Например, вам может понадобиться установить дату создания объекта или имя пользователя, создавшего объект, или, возможно, вам потребуется задать для объекта значение уникального идентификатора. Существуют объекты, значения которых желательно устанавливать только один раз, а затем не позволять их изменять. Исходя из этого C# позволяет вам инициализировать автоматически создаваемое свойство, предназначенное только для чтения, одним из двух способов. Свойство можно инициализировать из конструктора:
class Circle
{
public Circle()
{
CircleCreatedDate = DateTime.Now;
}
public DateTime CircleCreatedDate { get; }
...
}
Или же его можно инициализировать при объявлении:
class Circle
{
public DateTime CircleCreatedDate { get; } = DateTime.Now;
...
}
Следует иметь в виду, что при инициализации свойства таким образом наряду с установкой его значения в конструкторе значение, предоставленное в конструкторе, перепишет значение, указанное в инициализаторе свойства, поэтому следует использовать один из этих подходов, но не оба сразу!
ПРИМЕЧАНИЕ Автоматически создавать свойства только для записи невозможно. При попытке автоматического создания свойства без метода доступа get будет выдана ошибка в ходе компиляции.
В главе 7 вы научились для инициализации объектов определять конструкторы. У объекта может быть несколько конструкторов, которые можно определять с различными параметрами для инициализации различных элементов объекта. Например, можно определить класс, моделирующий треугольник:
public class Triangle
{
private int side1Length;
private int side2Length;
private int side3Length;
// пассивный конструктор — исходные значения для всех сторон
public Triangle()
{
this.side1Length = this.side2Length = this.side3Length = 10;
}
// указание длины для side1Length и исходные значения для других сторон
public Triangle(int length1)
{
this.side1Length = length1;
this.side2Length = this.side3Length = 10;
}
// Указание длины для side1Length и side2Length,
// исходное значение для side3Length
public Triangle(int length1, int length2)
{
this.side1Length = length1;
this.side2Length = length2;
this.side3Length = 10;
}
// Указание длины для всех сторон
public Triangle(int length1, int length2, int length3)
{
this.side1Length = length1;
this.side2Length = length2;
this.side3Length = length3;
}
}
В зависимости от количества полей, содержащихся в классе, и различных комбинаций, которые вам хотелось бы разрешить использовать при инициализации полей, вы можете прийти к написанию множества конструкторов. Если у многих полей один и тот же тип, образуется потенциальная проблема: у вас может отсутствовать возможность написания уникального конструктора для всех комбинаций полей. Например, в рассмотренном ранее классе Triangle вы не можете просто добавить конструктор, инициализирующий только поля side1Length и side3Length, поскольку у него не может быть уникальной сигнатуры — он бы получал два int-параметра, и точно такую же сигнатуру уже имеет конструктор, инициализирующий side1Length и side2Length. Одно из возможных решений при создании Triangle-объекта заключается в определении конструктора, принимающего необязательные параметры, и в указании значений для параметров в виде поименованных аргументов. Но более удачным и понятным решением будет инициализация закрытых полей для установки исходных значений и выставление их в виде свойств:
public class Triangle
{
private int side1Length = 10;
private int side2Length = 10;
private int side3Length = 10;
public int Side1Length
{
set { this.side1Length = value; }
}
public int Side2Length
{
set { this.side2Length = value; }
}
public int Side3Length
{
set { this.side3Length = value; }
}
}
При создании экземпляра класса его можно инициализировать указанием имен и значений для любых открытых свойств, имеющих методы доступа. Например, можно создать Triangle-объекты и инициализировать любую комбинацию трех сторон:
Triangle tri1 = new Triangle { Side3Length = 15 };
Triangle tri2 = new Triangle { Side1Length = 15, Side3Length = 20 };
Triangle tri3 = new Triangle { Side2Length = 12, Side3Length = 17 };
Triangle tri4 = new Triangle { Side1Length = 9, Side2Length = 12,
Side3Length = 15 };
Этот синтаксис известен под названием инициализатора объекта. При подобном вызове инициализатора объекта компилятор C# создает код, вызывающий пассивный конструктор, а затем вызывает метод доступа set каждого названного свойства с целью инициализации этого свойства указанным значением. Инициализаторы объектов можно указывать и в сочетании с активными конструкторами. Например, если класс Triangle предоставляет конструктор, получающий один строковый параметр, который описывает тип треугольника, то можно вызвать этот конструктор и наряду с этим инициализировать другие свойства:
Triangle tri5 = new Triangle("Equilateral triangle") { Side1Length = 3,
Side2Length = 3,
Side3Length = 3 };
Важно помнить, что первым запускается конструктор, а после этого устанавливаются свойства. Понимание этой последовательности необходимо в том случае, когда конструктор устанавливает для полей в объекте конкретные значения, а указываемые вами свойства эти значения изменяют.
В следующем упражнении будут показано, что инициализаторы объектов могут использоваться также с автоматически создаваемыми свойствами, не предназначенными только для чтения. В этом упражнении будет определен класс для моделирования правильных многоугольников, имеющих автоматически создаваемые свойства для предоставления доступа к информации о количестве сторон многоугольника и об их длине.
ПРИМЕЧАНИЕ Инициализировать таким образом автоматически создаваемые свойства, предназначенные только для чтения, невозможно, для этого придется воспользоваться одной из технологий, рассмотренных в предыдущем разделе.
Откройте в среде Visual Studio 2015 проект AutomaticProperties, который находится в папке \Microsoft Press\VCSBS\Chapter 15\AutomaticProperties вашей папки документов. В проекте AutomaticProperties содержится файл Program.cs, в котором находится определение класса Program с методами Main и doWork, уже встречавшимися в предыдущих упражнениях.
Щелкните правой кнопкой мыши в обозревателе решений на проекте AutomaticProperties, выберите пункт Добавить, а затем щелкните на пункте Класс, чтобы открылось диалоговое окно Добавить новый элемент – AutomaticProperties. Наберите в поле Имя строку Polygon.cs и щелкните на кнопке Добавить. Будет создан и добавлен в проект файл Polygon.cs, содержащий класс Polygon, код которого появится в окне редактора.
Добавьте к классу Polygon автоматически создаваемые свойства NumSides и SideLength, выделенные здесь жирным шрифтом:
class Polygon
{
public int NumSides { get; set; }
public double SideLength { get; set; }
}
Добавьте к классу Polygon следующий пассивный конструктор, выделенный жирным шрифтом:
class Polygon
{
...
public Polygon()
{
this.NumSides = 4;
this.SideLength = 10.0;
}
}
Этот конструктор инициализирует исходными значениями поля NumSides и SideLength. В этом упражнении исходным многоугольником является квадрат со сторонами длиной 10 единиц.
Выведите в окно редактора файл Program.cs. Добавьте к методу doWork инструкции, выделенные здесь жирным шрифтом, заменив ими комментарий // TODO::
static void doWork()
{
Polygon square = new Polygon();
Polygon triangle = new Polygon { NumSides = 3 };
Polygon pentagon = new Polygon { SideLength = 15.5, NumSides = 5 };
}
Эти инструкции создают Polygon-объекты. Переменная square инициализируется с помощью пассивного конструктора. Переменные triangle и pentagon также инициализируются с использованием пассивного конструктора, а затем этот код изменяет значения свойств, предоставляемых классом Polygon. В случае с переменной triangle для свойства NumSides устанавливается значение 3, но у свойства SideLength остается его исходное значение 10.0. Для переменной pentagon код изменяет значения свойств SideLength и NumSides.
Добавьте к концу метода doWork следующий код, выделенный жирным шрифтом:
static void doWork()
{
...
Console.WriteLine($"Square: number of sides is {square.NumSides}, length of
each side is {square.SideLength}");
Console.WriteLine($"Triangle: number of sides is {triangle.NumSides},
length of each side is {triangle.SideLength}");
Console.WriteLine($"Pentagon: number of sides is {pentagon.NumSides},
length of each side is {pentagon.SideLength}");
}
Содержащиеся в нем инструкции выводят на экран значения свойств NumSides и SideLength для каждого объекта Polygon.
Щелкните в меню Отладка на пункте Запуск без отладки. Убедитесь в том, что выполняются сборка и запуск программы и она выводит в окне консоли показанные на рис. 15.2 сообщения.
Рис. 15.2
Нажмите клавишу Ввод, чтобы закрыть приложение и вернуться в среду Visual Studio 2015.
В этой главе было показано, как создаются и используются свойства, предоставляющие управляемый доступ к данным, находящимся в объекте. Вы также увидели, как свойства создаются в автоматическом режиме и как они используются для инициализации объектов.
Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 16 «Использование индексаторов».
Если сейчас хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Увидев диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.
Чтобы | Сделайте следующее |
Объявить для структуры или класса свойство, доступное для чтения и записи | Объявите тип свойства, его имя, а также методы доступа get и set, например: struct ScreenPosition { ... public int X { get { ... } set { ... } } ... } |
Объявить для структуры или класса свойство, доступное только для чтения | Объявите свойство, указав для него только метод доступа get, например: struct ScreenPosition { ... public int X { get { ... } } ... } |
Объявить для структуры или класса свойство, доступное только для записи | Объявите свойство, указав для него только метод доступа set, например: struct ScreenPosition { ... public int X { set { ... } } ... } |
Объявить свойство в интерфейсе | Объявите свойство, используя только ключевое слово get или set или же оба этих слова, например: interface IScreenPosition { int X { get; set; } // тела нет int Y { get; set; } // тела нет } |
Реализовать свойство интерфейса в структуре или классе | Объявите в структуре или в классе, реализующем интерфейс, свойство и реализуйте его методы доступа, например: struct ScreenPosition : IScreenPosition { public int X { get { ... } set { ... } }
public int Y { get { ... } set { ... } } } |
Определить автоматически создаваемое свойство | Определите свойство в содержащем его классе или в структуре с пустыми методами доступа get и set, например: class Polygon { public int NumSides { get; set; } } Если определяется свойство, доступное только для чтения, его следует инициализировать либо в конструкторе объекта, либо сразу же при его определении, например: class Circle { public DateTime CircleCreatedDate { get; } = DateTime.Now; ... } |
Использовать свойства для инициализации объекта | Укажите свойства и их значения в виде списка, заключенного в фигурные скобки, и поместите этот список в конструктор объекта, например: Triangle tri3 = new Triangle { Side2Length = 12, Side3Length = 17 }; |