Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Объектно-ориентированное программирование.-6

.pdf
Скачиваний:
6
Добавлен:
05.02.2023
Размер:
4.5 Mб
Скачать

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

csc.exe /main:<имя класса> ...

компилятора C#, можно задавать класс, метод Main которого должен быть задействован.

Также можно выбрать этот класс в опциях проекта в среде Visual Studio 2008 (меню «Проект» → «Свойства…», закладка «Приложение», выпадающий список «Автоматически запускаемый объект»).

281

§ 4.5. Свойства. Индексаторы

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

4.5.1. Определение и использование свойств

Всегда поощряется создание классов, которые не только скрывают реализацию своих методов, но и запрещают любой прямой доступ к полям класса. Обеспечить корректную работу с полем можно, предоставляя методы доступа (accessor methods), выполняющие работу по получению и установке значений этих полей, так чтобы они действовали согласно правилам конкретной предметной области. Такая конструкция называется свойством класса.

Например, в языке C++ это можно сделать так:

class MyClass

{

private: int F;

public:

int GetF(void) const { /* стратегия чтения */ } void SetF(int f) { /* стратегия записи */ }

};

В расширениях языка C++, появившихся в библиотеке Borland VCL, предусмотрены специальные языковые конструкции для описания свойств:

class MyClass

{

private:

int FF;

int __fastcall GetF(void) { /* стратегия чтения */ } void __fastcall SetF(int f) { /* стратегия записи */ }

public:

__property int F = {read = GetF, write = SetF};

282

};

Так гораздо удобнее – со свойством можно работать как с обычным полем, не задумываясь о том, что при доступе к нему вызываются какие-то скрытые методы. Эту идею заимствовали и разработчики языка C#, хотя несколько изменили синтаксис.

Свойство в C# состоит из объявления поля и методов доступа, применяемых для изменения значения поля. Эти методы доступа называются получатель (getter) и установщик (setter). Методы-получатели используются для получения значения поля, а установщики – для его изменения. Общий синтаксис таков:

<свойство> :: [<атрибуты>] [<модификаторы>] <тип> <имя свойства> "{" <методы доступа> "}"

<имя свойства> :: <идентификатор> <имя свойства> :: <тип интерфейса>.<идентификатор>

<методы доступа> :: <получатель> [<установщик>] <методы доступа> :: <установщик> [<получатель>]

<получатель> :: [<атрибуты>] [<модификатор метода>] get <тело метода> <установщик> :: [<атрибуты>] [<модификатор метода>] set <тело метода>

<модификатор метода> :: internal <модификатор метода> :: protected <модификатор метода> :: protected internal <модификатор метода> :: internal protected <модификатор метода> :: private

<тело метода> :: <блок> <тело метода> :: ;

Как видно, у получателя и установщика могут быть разные уровни доступа. Тело у свойства может отсутствовать, если оно абстрактное, внешнее или автоматически реализуемое. Наследование и перегрузка свойств обсуждаются в § 4.7.

Пример: Samples\4.5\4_5_1_props.

4.5.1.1. Методы доступа

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

283

Метод доступа set (установщик) соответствует методу с типом возвращаемого значения void и одним параметром значения, имеющим тип свойства. Неявному параметру установщика всегда присваивается имя value. Операторы return в теле установщика не могут задавать выражение.

В зависимости от наличия или отсутствия получателя и установщика свойства классифицируются следующим образом:

Свойство, содержащее оба метода доступа, называется свойством для чтения и записи.

Свойство, содержащее только получатель, называется свойством только для чтения. При использовании свойства только для чтения в качестве конечного объекта для операции присваивания возникает ошибка времени компиляции.

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

Пример:

public class Person1

{

public enum TSex { Male, Female };

private TSex FSex; private string FName; private int FAge = 0;

public string Name

{

set { FName = value; } get { return FName; }

}

public TSex Sex

{

protected set { FSex = value; } get { return FSex; }

}

public int Age

{

set

{

if (value >= 0)

{

FAge = value;

}

else

{

284

throw new ArgumentOutOfRangeException("Возраст не должен быть отрицательным");

}

}

get

{

return FAge;

}

}

public Person1(TSex sex)

{

FSex = sex;

FName = sex == TSex.Male ? "Иван" : "Маша";

}

}

static int Main()

{

Person1 p1 = new Person1(Person1.TSex.Male);

p1.Name = "Пётр";

p1.Age = -1; // ArgumentOutOfRangeException p1.Age = 7;

p1.Sex = Person1.TSex.Female; // Ошибка return 0;

}

В классе Person объявлены три свойства. Свойство Name не содержит ограничений. Свойство Age генерирует исключительную ситуацию, если попытаться установить отрицательный возраст. Свойство Sex задается только один раз, в конструкторе, а потом извне класса его можно только читать.

Итак, свойства позволяют предоставлять методы доступа для полей и универсальные, интуитивно понятные интерфейсы для клиента. Из-за этого свойства иногда называют «умными полями».

4.5.1.2. Автоматически реализуемые свойства

Если для обоих методов доступа мы не указываем тело, то получаем

автоматически реализуемое свойство. Для него автоматически создается скрытое резервное поле, для которого реализуются методы доступа для чтения и записи.

Поскольку резервное поле недоступно, чтение и запись его значений осуществляются только с помощью методов доступа свойства. Это означает, что автоматически реализуемые свойства только для чтения или только для записи не имеют смысла и не разрешены. Однако возможно задать различные уровни доступа для каждого из методов доступа.

Например, сделаем поле Sex в предыдущем примере автоматически ре-

285

ализуемым:

public class Person2

{

public enum TSex { Male, Female };

private string FName; private int FAge = 0;

public string Name

{

set { FName = value; } get { return FName; }

}

public TSex Sex

{

protected set; get;

}

public int Age

{

set

{

if (value >= 0)

{

FAge = value;

}

else

{

throw new ArgumentOutOfRangeException("Возраст не должен быть отрицательным");

}

}

get

{

return FAge;

}

}

public Person2(TSex sex)

{

Sex = sex;

FName = sex == TSex.Male ? "Иван" : "Маша";

}

}

static int Main()

{

Person2 p2 = new Person2(Person2.TSex.Female);

p2.Age = 12; return 0;

}

286

4.5.1.3. Доступность

На использование модификаторов методов доступа налагаются следующие ограничения:

Модификатор метода доступа не может использоваться в интерфейсе или явной реализации члена интерфейса;

Для свойства или индексатора, не имеющего модификатора override, модификатор метода доступа может использоваться только в том случае, если свойство или индексатор содержит оба метода доступа, и применяется только к одному из них;

Для свойства или индексатора, содержащего модификатор override, метод доступа должен соответствовать используемому модификатору метода доступа переопределяемого метода доступа;

Модификатор метода доступа должен объявлять более строгий уровень доступа, чем уровень доступа самого свойства или индексатора.

После выбора конкретного свойства или индексатора допустимость их использования определяется на основании областей доступности задействованных методов доступа:

Для использования в качестве значения (r-value) должен существовать

ибыть доступным получатель.

Для использования в качестве конечного объекта операции простого присваивания должен существовать и быть доступным установщик.

Для использования в качестве конечного объекта операции составного присваивания или в качестве конечного объекта операторов инкремента и декремента должны существовать и быть доступны оба метода доступа.

Как уже было сказано в пункте 4.2.2, для свойства P типа T зарезервированы сигнатуры «T get_P()» и «void set_P(T value)» (независимо от типа свойства). Методы с такими сигнатурами для доступа к свойству создаются компилятором автоматически.

Пример:

public class Test

{

int x = 0, y = 0, z = 0;

public int X { get { return x; } } public int Y { set { y = value; } }

public int Z { get { return z; } set { z = value; } }

}

public class Props

287

{

int X1 { get; set; } // Ошибка void get_X1() { }

int X2 { get; set; } // ОК void get_X2(int i) { }

}

static int Main()

{

Test test = new Test(); int val;

test.X = val; // Ошибка test.X++; // Ошибка

val = test.X; // ОК test.Y = val; // ОК

val = test.Y; // Ошибка test.Y++; // Ошибка

val = test.Z; // ОК test.Z = val; // ОК test.Z++; // ОК return 0;

}

4.5.2. Индексаторы

Прейдем к рассмотрению индексаторов – особой возможности C#, позволяющей программно обращаться с объектами так, как если бы они были массивами. Индексаторы примерно соответствуют возможности перегрузки операции индексации в языке C++:

class MyClass

{

public:

int &operator [] (int idx) { /* стратегия доступа */ }

}

В этом примере оператор индексации возвращает ссылку, чтобы его можно было использовать и для чтения, и для записи. В расширениях языка C++, введенных в библиотеке Borland VCL, появилась возможность делать индексируемые свойства. Причем тип и количество индексов можно варьировать (тогда как в операторе индексации это всегда одно целое число):

class MyClass

{

private:

int __fastcall GetF(String idx) { /* стратегия чтения */ }

void __fastcall SetF(String idx, int f) { /* стратегия записи */ } public:

__property int F[String] = {read = GetF, write = SetF};

};

288

В данном примере в качестве индекса выступает строка. По этому же пути пошли разработчики языка C#.

Синтаксис описания индексаторов совпадает с таковым для свойств, за исключением имени:

<имя свойства> :: this "[" <список формальных параметров > "]" <имя свойства> :: <тип интерфейса>.this "[" <список формальных

параметров > "]"

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

<доступ к индексатору> :: <экземпляр класса> "[" <список фактических параметров > "]"

Например, добавим в разработанный нами ранее класс Person информацию о весе субъекта на каждом году жизни:

public class Person3

{

public enum TSex { Male, Female };

private string FName; private int FAge = 0; double[] FWeights;

public string Name

{

set { FName = value; } get { return FName; }

}

public TSex Sex

{

protected set; get;

}

public int Age

{

set

{

if (value >= 0)

{

FAge = value;

FWeights = new double[FAge + 1];

}

else

{

throw new ArgumentOutOfRangeException("Возраст не должен быть отрицательным");

}

}

get

289

{

return FAge;

}

}

public double this[int year]

{

get { return FWeights[year]; } set { FWeights[year] = value; }

}

public Person3(TSex sex)

{

Sex = sex;

FName = sex == TSex.Male ? "Иван" : "Маша";

FWeights = new double[1] { 3.5 };

}

}

static int Main()

{

Person3 p3 = new Person3(Person3.TSex.Male);

p3.Name = "Саша"; p3.Age = 3;

p3[0] = 3.2; p3[1] = 5.0; p3[2] = 7.4; p3[3] = 9.8;

for(int i = 0; i <= p3.Age; i++)

{

Console.WriteLine("Когда {0} был(а) в возрасте {1} лет, его(её) вес составлял {2} кг.", p3.Name, i, p3[i]);

}

return 0;

}

Как уже было сказано в пункте 4.2.2, для индексатора типа T со списком формальных параметров L зарезервированы сигнатуры «T get_Item(L)» и «void set_Item(L, T value)». Пример:

class Indexers

{

int this[int i] { get { return 0; } } // Ошибка int this[int i, int j] { get { return 0; } } // ОК int get_Item(int x) { return 0; }

}

Пример: Samples\4.5\4_5_2_index.

290