Я уже описал основные поддерживаемые С# типы, способы их объявления и использования в классах и приложениях. А теперь мы нарушим порядок изложения, при котором каждая глава посвящена описанию какой-либо одной из важных функций языка, — в этой главе вы узнаете о свойствах, массивах и индексаторах, так как у этих функций языка много общего. Они позволяют разработчику классов на С# расширять возможности базовых структур классов (полей и методов), чтобы члены классов предоставляли более понятный и естественный интерфейс.
Всегда поощряется создание классов, которые не только скрывают реализацию своих методов, но и запрещают членам любой прямой доступ к полям класса. Обеспечить корректную работу с полем можно, предоставляя методы-аксессоры (accessor methods), выполняющие работу по получению и установке значений этих полей, так чтобы они действовали согласно правилам конкретной предметной области.
Допустим, у вас есть класс "Адрес" с полями для почтового индекса и города. Когда клиент модифицирует поле индекса Address.ZipCode, вам нужно сверить введенный код с БД и автоматически установить значение поля Address. City в зависимости от этого почтового индекса. Если бы у клиента был прямой доступ к открытому члену (public) Address.ZipCode, выполнить обе эти задачи было бы сложно, поскольку для непосредственного изменения открытого члена метод не требуется. Поэтому вместо того, чтобы предоставить доступ к полю Address.ZipCode, лучше определить поля Address.ZipCode и Address.City как protected и предоставить методы-аксессоры для получения и установки значения поля Address.Zip-Code. Таким образом, вы можете добавить код, выполняющий дополнительную работу при изменении поля.
Этот пример с почтовым индексом можно запрограммировать на С# следующим образом. Заметьте: поле ZipCode определено как protected и поэтому недоступно клиенту, а методы-аксессоры GetZipCode и SetZipCode определены как public.
Методы-аксессоры используются программистами на нескольких объектно-ориентированных языках, в том числе на C++ и Java. Однако С# предоставляет еще более мощный механизм — свойства с такими же возможностями, как методы-аксессоры, но гораздо более элегантные на стороне клиента. Свойства позволяют написать клиент, способный обращаться к полям класса, как если бы они были открытыми, даже не зная, существуют ли методы-аксессоры.
Свойство в С# состоит из объявления поля и методов-аксессоров, применяемых для изменения значения поля. Эти методы-аксессоры называются получатель (getter) и установщик (setter). Методы-получатели используются для получения значения поля, а установщики — для его изменения. Вот наш пример, переписанный с применением свойств С#:
Я создал поле Address.zipCode и свойство Address.ZipCode. Поначалу это может ввести в заблуждение: можно подумать, что Address.ZipCode — это поле, да еще и определенное дважды. Но это не поле, а свойство, представляющее собой универсальное средство определения аксессоров для членов класса, что позволяет использовать более интуитивно понятный синтаксис вида объект, поле. Если бы я опустил в этом примере поле Address.zipCode и изменил бы оператор в установщике с zipCode = value на ZipCode = value, метод-установщик вызывался бы в итоге бесконечно. Заметьте также, что установщик не принимает аргументов. Передаваемое значение автоматически помещается в переменную value, доступную внутри метода-установщика (вскоре с помощью MSIL вы увидите, как происходит это маленькое чудо).
А теперь, написав свойство Address.ZipCode, рассмотрим изменения, которые необходимо сделать для клиентского кода:
Как видите, способ обращения клиента к полям интуитивно понятен: не требуется больше никаких догадок или поисков в документации (и в исходном коде), чтобы узнать, является ли это поле открытым, и, если нет, — выяснять имя метода-аксессора.
Итак, как же компилятор позволяет вызывать метод с помощью стандартного синтаксиса объект.поле! И откуда берется переменная value"! Чтобы ответить на эти вопросы, взглянем на MSIL-код, сгенерированный компилятором. Сначала рассмотрим метод-получатель свойства. В следующем примере определен такой метод-получатель:
Взглянув на MSIL, получившийся из этого метода, вы увидите, что компилятор создал метод-аксессор getJZipCode, как показано ниже:
Вы можете сообщить имя метода-аксессора, поскольку компилятор добавляет к имени префикс get_ (в случае метода-получателя) или set_
(в случае метода-установщика). В результате следующий код разрешается как вызов get__ZipCode:
Однако в этом случае код не будет скомпилирован, так как явно вызывать внутренний метод MSIL недопустимо.
Ответ на наш вопрос — как компилятор позволяет использовать стандартный синтаксис объект.поле для вызова метода? — в том, что при разборе синтаксиса свойства на С# компилятор на самом деле генерирует для нас соответствующие методы-получатели и установщики, поэтому в случае свойства Address.ZipCode компилятор генерирует MSIL, содержащий методы get_ZipCode и setJZipCode.
А теперь посмотрим на сгенерированный метод-установщик. В классе Address вы видели следующее:
В этом коде не объявлена переменная value, но мы все же можем использовать ее для хранения значения, переданного вызывающим кодом, и для установки защищенного поля zipCode. Генерируя MSIL для метода-установщика, компилятор С# вводит эту переменную как аргумент метода set_ZipCode. В сгенерированном MSIL этот метод принимает как аргумент строковую переменную:
Даже если вы не найдете этого метода в исходном коде на С#, при установке свойства ZipCode [например, так: addr.ZipCode(" 12345")}, оно разрешается в MSIL-вызов метода Address ::set_ZipCode(" 12345"). Как и в случае метода getJZipCode, попытка прямого вызова этого метода на С# приводит к ошибке.
В нашем примере свойство Address.ZipCode считается доступным для чтения и записи, так как определены оба метода: установщик и получатель. Конечно, иногда может потребоваться лишить клиент возможности устанавливать значение данного поля. В этом случае вы можете сделать это поле неизменяемым, опустив метод-установщик. Чтобы проиллюстрировать неизменяемые свойства, предотвратим установку поля Address.city клиентом, оставив Address.ZipCode как единственную ветвь кода, задачей которого является изменение значение поля:
class Address {
protected string city;
public string City {
get
{
return city;
} }
protected string zipCode;
public string ZipCode {
get
{
return zipCode;
}
set
{
// Сверить значение с базой данных.
zipCode = value;
// обновить город с помощью проверенного zipCode. } } }
У свойств, как и методов, могут быть указаны модификаторы virtual, override или abstract, о которых я рассказывал в главе 6. Это позволяет производным классам наследовать и подменять свойства подобно любому другому члену, унаследованному от базового класса. Главная проблема в том, что вы можете задавать эти модификаторы только на уровне свойства. Иначе говоря, когда у вас есть оба метода — получатель и установщик, при подмене одного из них нужно подменять и второй.
Я уже обсудил следующие причины, которые делают полезными свойства:
- они предоставляют клиентам более высокий уровень абстракции;
- они обеспечивают универсальные средства доступа к членам класса с использованием синтаксиса объект.поле;
- они позволяют классу гарантировать, что может быть выполнена любая дополнительная обработка при изменении или обращении к некоторому полю.
Третий пункт связан с еще одним полезным способом применения свойств — реализации отложенной инициализации (lazy initialization). При этой методике оптимизации некоторые члены класса не инициализируются, пока не потребуются.
Отложенная инициализация дает преимущества, если у вас есть класс с членами, на которые редко ссылаются и на инициализацию которых уходит много времени и ресурсов. Примерами этого могут служить ситуации, когда требуется считывание данных из БД или через перегруженную сеть. Поскольку вам известно, что на эти члены ссылаются редко, а их инициализация требует больших ресурсов, их инициализацию можно отложить до вызова их методов-получателей. Чтобы проиллюстрировать этот момент, допустим, что у вас есть приложение управления запасами, которое представители по продажам запускают на своих портативных компьютерах для размещения заказов клиентов, и время от времени используют его для проверки наличия товара. Используя свойства, вы можете разрешить создание экземпляров соответствующих классов, так чтобы при этом не считывались записи из БД, как показано в приведенном ниже коде. Когда представитель захочет узнать о количестве товара на складе, метод-получатель обратится к удаленной БД.
class Sku {
protected double onHand;
public string OnHand {
get
{
// Считать из центральной базы данных и установить
// значение onHand.
return onHand; } } }
Итак, свойства позволяют предоставлять методы-аксессоры для полей и универсальные, интуитивно понятные интерфейсы для клиента. Из-за этого свойства иногда называют "умными полями". А теперь рассмотрим способы определения и использования массивов на С#. Вы также узнаете, как свойства используются с массивами в виде индексаторов (indexers).
До сих пор мои примеры иллюстрировали способы определения конечного, предопределенного числа переменных. Однако во многих реальных приложениях точное число нужных объектов неизвестно до периода выполнения. Так, если вы разрабатываете редактор и хотите отслеживать число элементов управления, добавляемых к диалоговому окну, точное количество элементов управления, которое будет показано редактором, неизвестно до периода выполнения. Но для хранения и отслеживания совокупности динамически выделяемых объектов, в данном случае — элементов управления редактора — вы можете использовать массив. В С# массивы являются объектами, производными от базового класса System.Array. Поэтому, хотя синтаксис определения массива аналогичен C++ или Java, реально вы создаете при этом экземпляр класса .NET. Это значит, что члены каждого объявленного массива унаследованы от Sys-tem.Array. В этом разделе я расскажу, как объявлять массивы и создавать их экземпляры, как работать с массивами разных типов, и опишу циклическую обработку элементов массива. Я также коснусь нескольких распространенных свойств и методов класса System.Array.
Для объявления массива на С# нужно поместить пустые квадратные скобки между именем типа и переменной, например, так:
int[] numbers;
Этот синтаксис отличен от C++, в котором квадратные скобки идут после имени переменной. Поскольку массивы основаны на классах, многие из правил объявления классов применяются и к массивам. Например, при объявлении массива на самом деле вы не создаете его. Так же, как и в случае класса, вы должны создать экземпляр массива, и только после этого он будет существовать в том смысле, что для его элементов будет выделена память. Вот как объявить массив и одновременно создать его экземпляр:
// Этот код объявляет одномерный массив
// из 6 элементов и создает его экземпляр.
int[] numbers = new int[6]; -
Однако, объявляя массив как член класса, вам нужно разбить определение массива и создание его экземпляра на два четко обозначенных этапа, поскольку вы не можете создавать экземпляр объекта до периода выполнения:
class YourClass {
int[] numbers;
void SomelnitMethodO <
numbers = new int[6];
}
>
Вот простой пример объявления одномерного массива как члена класса. При этом в конструкторе создается и заполняется экземпляр массива, после чего все элементы массива выводит цикл
using System;
class SingleOimArrayApp {
protected int[] numbers;
SingleDimArrayApp() {
numbers = new int[6];
for (int 1 = 0; i < 6; i++)
{
numbers[i] = i * i; } }
protected void PrintArrayO {
for (int 1=0; i < numbers.Length; 1++) {
Console.WriteLine("numbers[{0}]={1}", i, numbers[i]); } }
public static void Main() <
SingleDimArrayApp app = new SingleDimArrayAppO;
app. PrintArrayO; } }
При запуске этого примера будет получена выходная информация:
numbers[0]=0
nurabers[1]=1
numbers[2]=4
nuinbers[3]=9
nurabers[4]=16
numbers[5]=25
В этом примере метод SingleDimArray.PrintArray определяет число элементов массива с помощью свойства Length класса System.Array. Это не совсем наглядный пример, так как мы используем всего лишь одномерный массив, а свойство Length на самом деле возвращает число всех элементов по всем измерениям массива. Так, в случае двумерного массива 5 на 4 свойство Length вернет 20. Ниже я рассмотрю многомерные массивы и способы определения верхней границы конкретного измерения массива.
Кроме одномерных, С# поддерживает объявление многомерных массивов, где каждое измерение отделяется запятой. Здесь я объявил трехмерный массив двойных слов:
doublet,>1 numbers;
Чтобы быстро определить число измерений массива, объявленного на С#, подсчитайте число запятых и к сумме прибавьте единицу.
В следующем примере я объявил двумерный массив объемов продаж, представляющих объемы продаж по месяцам в этом году и суммы за аналогичный период времени прошлого года. Обратите особое внимание на синтаксис создания экземпляра массива (в конструкторе MultiDimAirayApp).
using System;
class MultiDimArrayApp ,--~~~~~ {
protected int currentMonth;
protected doublet,] sales;
MultiDimArrayAppO {
currentMonth=10;
sales = new double[2, currentMonth];
for (int i = 0; i < sales.GetLength(O); i++)
{
for (int j=0; j < 10; j++) {
sales[i,j] = (i * 100) + j; } } >
protected void PrintSalesO <
for (int i = 0; i < sales.GetLength(O); i++)
{
for (int j=0; j < sales.GetLength(l); j++) {
Console.WriteLine("[{0}][{1}]={2}", i, j, sales[i,J]); } } }
public static void Main() {
MultiDimArrayApp app = new MultiDimArrayAppO;
app.PrintSalesO;
} }
Запустив MultiDimArrayApp, вы получите такую информацию:
[0][0]=0
[0][1]=1
[0][2]=2
[0][3]=3
[0][4]=4
[0][5]=5
[0][6]=6
[0][7]=7
[0][8]=8
С0][9]=9
[1][0]=100
[1][1]=101
[1][2]=102
[1][3]=103
[1][4]=104
[1][5]=105
[1][6]=106
[1][7]=107
[1][8]=108
[1][9]=109
Помните: свойство Length, как я говорил при рассмотрении примера одномерного массива, возвращает суммарное число элементов массива, поэтому в данном примере это свойство вернет 20. Для определения длины или верхней границы каждого измерения массива в методе MultiDimArray.PrintSales я использовал метод Array. GetLength. Далее я смог задействовать каждое конкретное значение в методе PrintSales.
Теперь, увидев, что динамическая обработка одно- или многомерного массива большой сложности не представляет, вас может заинтересовать способ программного определения числа измерений массива. Число измерений массива называется рангом, а его значение позволяет получить свойство Array.Rank. Вот как это сделать для нескольких массивов:
using System;
class RankArrayApp <
int[] singleD;
int[,] doubleD;
int[,,] tripleD;
protected RankArrayAppO {
singleD = new Int[6];
doubleD = new int[6,7];
tripleD = new int[6,7,8]; ,,-—-}
protected void PrintRanksQ {
Console.WriteLine("PaHr singleD = {0}", singleD.Rank);
Console.WriteLine("PaHr doubleD = {0}", doubleD.Rank);
Console.WriteLineC'Painr tripleD = {0}", tripleD.Rank); }
public static void Main() {
RankArrayApp app = new RankArrayAppO; app.PrintRanksO; } }
Как и ожидалось, приложение RankArrayApp выдало:
Ранг singleD = 1
Ранг doubleD = 2
Ранг tripleD = 3
Последнее, что мы рассмотрим в связи с массивами, — невыровненные массивы (jagged array). Невыровненный массив — это просто массив массивов. Вот пример определения массива, состоящего из массивов целочисленных значений:
int[][] jaggedArray;
Невыровненные массивы можно применять при разработке редактора. При этом вы можете хранить каждый объект, представляющий созданный пользователем элемент управления, в массиве. Допустим, у вас есть массив кнопок и массив полей со списком (чтобы этот пример был небольшим и легко управляемым). У вас могут быть три кнопки и три поля со списком в соответствующих массивах. Объявив невыровненный массив, можно создать для этих массивов "родительский" массив, что позволит вам при необходимости легко выполнять циклическую программную обработку элементов управления:
using System;
class Control {
virtual public void SayHiO
{
Console.WriteLine("Ba3oebiu класс для элементов управления");
} }
class Button : Control {
override public void SayHiO
{
Console.WriteLine("Кнопка");
} }
class Combo ; Control <
override public void SayHiO ?
{ . . ,
Console.WriteLine("Элемент со списком");
} }
class JaggedArrayApp
{
public static void Main() {
Control[][] controls; controls = new Control[2][];
controls[0] = new Control[3];
for (int 1=0; i < controls[0].Length; i++)
<
oontrols[0][i] = new ButtonO;
}
controls[1] = new Control[2];
for (Int i = 0; i < controls[1].Length; i++)
{
controls[1][i] = new ComboO; }
for (int i = 0; i < controls.Length;i++) {
for (int j=0;j< controls[i].Length;J++) <
Control control = controls[i][j]; control. SayHiO; } }
string str = Console.ReadLineO; } }
Как видите, я определил базовый класс (Control), два производных (Button и Combo) и объявил массив массивов, содержащих объекты Controls. Таким образом, я могу хранить в массивах значения определенных типов и благодаря полиморфизму быть уверенным, что, когда наступит время извлечь объект из массива (с помощью объектов, приведенных к базовому классу), все будет работать так, как я задумал.
Теперь вы знаете, как объявлять массивы и создавать их экземпляры, как работать с массивами разных типов и циклически обрабатывать элементы массивов. Вы также узнали, как для массивов использовать наиболее популярные свойства и методы, определенные в классе System.Array. Теперь перейдем к рассмотрению индексаторов — особой возможности С#, позволяющей программно обращаться с объектами так, как если бы они были массивами.
Но зачем это нужно? Как и в случае большинства функций языка программирования, польза от индексаторов в том, что писать приложения становится более интуитивно понятно. В разделе "Свойства как "умные поля"" вы узнали, как свойства в С# дают вам возможность ссылаться на поля класса с использованием стандартного синтаксиса класс.поле. Такие поля в конечном счете приводятся к методам-получателям и установщикам. Это абстрагирование освобождает программиста, пишущего клиент класса, от необходимости определения наличия у поля методов-получателей и установщиков и от необходимости знать их точный формат. Аналогично индексаторы позволяют клиенту класса индексировать объект, как если бы объект был массивом.
Рассмотрим пример. У вас есть класс "окно со списком", куда пользователь класса может вставлять строки. Если вы хорошо знакомы с Win32 SDK, то знаете, что для того, чтобы вставить строку в окно со списком, нужно послать ему сообщение LB^ADDSTRING или LBJNSERTSTRING. Когда этот механизм появился в конце 80-х годов, мы думали, что были настоящими объектно-ориентированными программистами. Разве после всего этого мы не посылали сообщения объекту, как нас учили все эти модные книги по объектно-ориентированному анализу и проектированию? Но с началом распространения таких объектно-ориентированных языков и языков на основе объектов, как C++ и Object Pascal, мы узнали, что объекты позволяют создавать более интуитивно понятные интерфейсы программирования для решения подобных задач. При использовании C++ и MFC (Microsoft Foundation Classes) нам доступна вся структура классов, позволяющая обращаться с окнами (такими, как окно со списком), как с объектами, классы которых содержат члены-функции, которые в основном являются тонкими оболочками для отсылки и получения сообщений от элементов управления Microsoft Windows. В случае класса CListBox (т. е. оболочки MFC для элемента управления Windows "окно со списком") для решения задач, выполнявшихся прежде путем отсылки сообщений LB_ADDSTRING и LBJNSERTSTRING, нам даны члены-функции AddString и InsertString.
Приняв это во внимание и стремясь облегчить создание лучшего и наиболее интуитивно понятного языка, команда разработчиков С# задалась вопросом: "А почему бы не предоставить возможность обработки объекта, который по своей сути является массивом, как массива?" Разве окно со списком — это не просто массив строк с дополнительной функциональностью вывода и сортировки? Вот из этой идеи и родилась концепция индексаторов.
Свойства иногда называются "умными полями", а индексаторы — "умными массивами", а значит, стоит использовать для них один синтаксис. Действительно, определение индексатора во многом напоминает определение свойств, кроме двух крупных отличий. Во-первых, индексатор принимает аргумент индекс. Во-вторых, поскольку сам класс применяется как массив, в качестве имени индексатора используется ключевое слово this. Вскоре вы увидите более полный пример, а сейчас взгляните на такой пример индексатора:
class MyClass
{
public object this [int idx]
{
get
{
// Возврат нужных данных.
} set
{
// Установка нужных данных.
} }
}
Это лишь часть примера, иллюстрирующего синтаксис индексаторов, так как внутренняя реализация способа определения данных, их получения и установки к индексаторам не относится. Имейте в виду, что независимо от внутреннего способа хранения ваших данных (т. е. в виде массива, набора и т. д.) индексаторы — всего лишь средства, позволяющие программисту создавать экземпляр класса для написания, например, такого кода:
MyClass els = new MyClassO;
cls[0] = someObject;
Console.WriteLine("{0}", cls[0]);
Что именно вы делаете в пределах индексатора — ваше личное дело, пока клиент класса получает при обращении к объекту как к массиву ожидаемые результаты.
Когда же применение индексаторов наиболее оправданно? Начну с уже приведенного примера окна со списком. С концептуальной точки зрения, такое окно представляет собой просто список, или массив строк, подлежащих выводу. В следующем примере я объявил класс с именем MyListBox, содержащий индексатор для установки и получения строк через объект ArrayList (класс Array List является классом .NET Framework, который используется для хранения совокупности объектов).
using System;
using System.Collections;
class MyListBox {
protected ArrayList data = new ArrayListQ;
public object this[int idx] {
get {
if (idx > -1 && idx < data.Count) {
return (data[idxj); }
else {
// Здесь возможно возникновение исключения, return null; } }
set {
if (idx > -1 && idx < data.Count) {
datafidx] = value; >
else if (idx == data.Count) {
data.Add(value);
}
else
{
// Здесь возможно возникновение исключения.
} > } }
class IndexerslApp
{
public static void Main()
{
MyListBox Ibx = new MyListBoxQ;
lbx[0] = "foo";
lbx[1] = "bar";
lbx[2] = "baz";
Console.WriteLine("{0} {1} {2}",
lbx[0], lbx[1], lbx[2]);
} >
В этом примере я реализовал проверку на ошибки, возникающие при выходе индекса за границы. Формально это не связано с индексаторами, поскольку индексаторы связаны лишь со способом использования объекта как массива клиентом класса, и никак — с внутренним представлением данных. Однако при изучении функций нового языка это помогает понять не только их синтаксис, но и принципы практического использования. Итак, в обоих методах индексатора (получателе и установщике) я проверял передаваемое значение индекса с помощью данных, хранимых членом класса ArrayList. Лично я выбрал бы генерацию исключений в тех случаях, когда переданное значение индекса не может быть использовано. Но это дело вкуса — ваша обработка ошибок может отличаться. Важно дать знать клиенту о возникновении ошибки, когда передается неверный индекс.
Индексаторы — пример того, как добавленная к языку командой разработчиков небольшая, но мощная функция помогает повысить производительность наших усилий по разработке. Однако, как и у любой функции любого языка, у индексаторов своя область применения. Их нужно использовать только там, где понятно, что с данным объектом можно обращаться, как с массивом. Рассмотрим приложение по подготовке счетов. В нем, конечно, нужен класс Invoice, определяющий член-массив объектов InvoiceDetail. В таком случае пользователю будет совершенно понятно применение следующего синтаксиса при обращении к подробностям счетов:
InvoiceDetail detail = invoice[2]; // Возвращает 3-ю строку
// с подробностями счета.
Однако этого не скажешь о попытке превращения всех членов InvoiceDetail в массив, доступ к которому будет осуществляться через индексатор. Как видите, первая строка гораздо понятнее, чем вторая:
TermCode terms = invoice.Terms; // Аксессор свойства для члена Terms. TermCode terms = invoice[3]; // Тут есть над чем задуматься.
Истина в том, что не стоит делать что-то лишь потому, что это возможно. Или конкретнее: задумывайтесь над тем, как реализация любой новой функции повлияет на клиенты вашего класса, и принимайте исходя из этого решение о том, облегчит ли реализованная функция использование вашего класса, или нет.
Подведем итоги
Свойства в С# состоят из объявления полей и методов-аксессоров. Свойства обеспечивают интеллектуальный доступ к полям класса, что освобождает программиста, занятого написанием клиента класса, от необходимости выяснять, был ли создан для поля класса метод-аксессор (и если был, то как). На С# массивы объявляются путем размещения пустых квадратных скобок между именем типа и переменной — в этом плане синтаксис несколько отличается от принятого в C++. Массивы С# могут быть одномерными, многомерными или невыровненными. В С# с объектами можно обращаться как с массивами, применяя индексаторы. Индексаторы позволяют программистам легко работать с множеством объектов одного типа.