Использование делегатов

Спецификация делегата определяет форму функции. Для создания экземпляра делегата необходимо использовать функцию, совпадающую с заданной формой. Делегаты иногда называются «безопасными указателями на функции». Однако в отличие от обычных указателей на функции, делегаты C# могут вызывать более одной функции; при совместном добавлении двух делегатов результатом будет делегат, который вызывает их обоих.

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

using System;

public class Container

{

public delegate int CompareItemsCallback(object obj1, object obj2);

public void Sort(CompareItemsCallback compare)

{

// Это не настоящая сортировка, а упрощенный пример

int x = 0;

int y = 1;

object item1 = arr[x];

object item2 = arr[y];

int order = compare(item1, item2);

}

object[] arr = new object[1]; // Содержимое коллекции

}

public class Employee

{

Employee(string name, int id)

{

this.name = name;

this.id = id;

}

public static int CompareName(object obj1, object obj2)

{

Employee emp1 = (Employee) obj1;

Employee emp2 = (Employee) obj2;

return(String.Compare(emp1.name, emp2.name));

}

public static int CompareId(object obj1, object obj2)

{

Employee emp1 = (Employee) obj1;

Employee emp2 = (Employee) obj2;

if (emp1.id > emp2.id)

return(1);

if (emp1.id < emp2.id)

return(-1);

else

return(0);

}

string name;

int id;

}

class Test

{

public static void Main()

{

Container employees = new Container();

// Создать и добавить записи о работниках

// Создать делегата для сортировки по именам

// и выполнить сортировку

Container.CompareItemsCallback sortByName =

new Container.CompareItemsCallback(Employee.CompareName);

employees.Sort(sortByName);

// Теперь работники отсортированы по именам

}

}

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

Чтобы отсортировать содержимое контейнера, функции сортировки передается делегат, описывающий порядок сортировки, и эта функция выполняет сортировку.

Во всяком случае, выполняла бы — если бы была нормально реализована в нашем примере.

Рефлексия

При определении атрибутов программисту необходим механизм получения их значений. Этот механизм называется рефлексией.

В следующей программе продемонстрирован класс атрибута, его применение к некоторому классу и чтение параметров атрибута посредством рефлексии.

using System;

using System.Reflection;

[AttributeUsage(AttributeTargets.Class)]

public class CodeReviewAttribute: System.Attribute

{

public CodeReviewAttribute(string reviewer, string date)

{

this.reviewer = reviewer;

this.date = date;

}

public string Comment

{

get

{

return(comment);

}

set

{

comment = value;

}

}

public string Date

{

get

{

return(date);

}

}

public string Reviewer

{

get

{

return(reviewer);

}

}

string reviewer;

string date;

string comment;

}

[CodeReview("Eric", "01-12-2000", Comment="Bitchin' Code")]

class Complex

{

}

class Test

{

public static void Main()

{

System.Reflection.MemberInfo info;

info = typeof(Complex);

object[] atts;

atts = info.GetCustomAttributes(typeof(CodeReviewAttribute));

if (atts.GetLength(0) != 0)

{

CodeReviewAttribute att = (CodeReviewAttribute) atts[0];

Console.WriteLine("Reviewer: {0}", att.Reviewer);

Console.WriteLine("Date: {0}", att.Date);

Console.WriteLine("Comment: {0}", att.Comment);

}

}

}

Функция Main() получает объект типа, связанный с типом Complex. Затем она загружает все атрибуты, относящиеся к типу CodeReviewAttribute. Если массив атрибутов не пуст, программа преобразует первый элемент массива к типу CodeReviewAttribute и выводит его значение. Массив может состоять только из одного элемента, поскольку CodeReviewAttribute является атрибутом однократного использования.

В приведенном примере выводится следующий результат:

Reviewer: Eric

Date: 01-12-2000

Comment: Bitchin' Code

Функция GetCustomAttribute() может вызываться и без указания типа, в этом случает она возвращает все пользовательские атрибуты объекта.

Пользовательские атрибуты

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

При создании атрибута необходимо ответить на два главных вопроса: во-первых, для каких элементов программы может устанавливаться ваш атрибут, и, во-вторых, какая информация будет в нем храниться?

Использование атрибута

Включение атрибута AttributeUsage в класс атрибута управляет возможностями использования последнего. Допустимые значения атрибута AttributeUsage определяются в перечислении AttributeTargets.

Значение

Описание

Assembly

Сборка, в которую входит программа

Module

Текущий файл программы

Class

Класс

Struct

Структура

Enum

Перечисление

Constructor

Конструктор

Method

Метод (функция класса)

Property

Свойство

Field

Поле (переменная класса)

Event

Событие

Interface

Интерфейс

Parameter

Параметр функции класса

ReturnValue

Возвращаемое значение функции класса

Delegate

Делегат

All

Любое место

ClassMembers

Класс, структура, перечисление, конструктор, метод, свойство, поле, событие, делегат, интерфейс

При использовании атрибута AttributeUsage можно указать один идентификатор или несколько, объединенных в операции OR.

Атрибут AttributeUsage также определяет кратность использования атрибута (однократно или многократно). Задача решается при помощи именованного параметра AllowMultiple. Это выглядит примерно так:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Event,

AllowMultiple = true)]

Параметры атрибутов

Данные, хранящиеся в атрибуте, делятся на две группы: обязательные и необязательные.

Обязательная информация передается классу атрибута при вызове конструктора. Следовательно, пользователь вынужден указать все обязательные параметры при попытке использования атрибута.

Необязательные данные реализуются в виде именованных параметров. Таким образом, пользователь сам выбирает, какие необязательные параметры будут использоваться.

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

Типы параметров атрибутов

В формате сохранения атрибутов поддерживается лишь некоторое подмножество типов .NET Runtime. Следовательно, параметры атрибутов могут относиться лишь к определенному набору типов. Допускаются следующие типы:

l bool, byte, char, double, float, int, long, short, string;

l object;

l System.Type;

l открытое перечисление (не вложенное в другой тип с уровнем доступа, отличным от public);

l одномерный массив любого из перечисленных типов.

Подробнее об атрибутах

Некоторые атрибуты разрешается устанавливать для данного элемента всего один раз. Другие, называемые атрибутами многократного использования (multi-use attributes), могут устанавливаться несколько раз. Например, с их помощью можно установить для класса несколько различных атрибутов безопасности. В документации к атрибуту должно быть указано, является ли он атрибутом однократного или многократного использования.

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

class Test

{

[ReturnsHResult]

public void Execute() {}

}

Обычно атрибут, находящийся в этой позиции, относится к функции класса, но в данном случае он относится к возвращаемому значению. Как же компилятор различает эти случаи?

Подобная неоднозначность возникает в нескольких ситуациях:

l функция класса/возвращаемое значение;

l событие/свойство или переменная класса;

l делегат/возвращаемое значение;

l свойство/функция доступа/возвращаемое значение функции чтения/параметр функции записи.

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

class Test

{

[return:ReturnsHResult]

public void Execute() {}

}

Строка return: означает, что атрибут относится к возвращаемому значению.

Элемент можно указать даже в том случае, если неоднозначность отсутствует. Используемые при этом идентификаторы перечислены в следующей таблице.

Идентификатор

Описание

Assembly

Атрибут относится к сборке

Module

Атрибут относится к модулю

Type

Атрибут относится к классу или структуре

Method

Атрибут относится к функции класса

Property

Атрибут относится к свойству

Event

Атрибут относится к событию

Field

Атрибут относится к переменной класса

Param

Атрибут относится к параметру

Return

Атрибут относится к возвращаемому значению

Атрибуты, относящиеся к сборке или модулю, должны находиться после всех директив using и до начала программного кода:

using System;

[assembly:CLSCompliant(true)]

class Test

{

Test() {}

}

В этом примере атрибут CLSCompliant устанавливается для всей сборки. Все атрибуты уровня сборки, объявленные в любом из файлов этой сборки, группируются вместе и присоединяются к сборке.

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

 

Сохранение атрибутов

Существует несколько причин, по которым схема сохранения атрибутов работает не так, как было описано выше. В первую очередь это связано с быстродействием. Чтобы компилятор мог реально создать объект атрибута, в это время должна работать среда .NET Runtime, поэтому при каждой компиляции пришлось бы запускать среду, а компилятор должен был бы работать как управляемый исполняемый файл (managed executable).

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

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

Применение атрибутов

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

А можно воспользоваться атрибутами, в результате чего станут возможными оба способа получения информации.

Для этого вам понадобится класс атрибута. Класс атрибута определяет имя атрибута, способ его создания и способ хранения информации. Определение классов атрибутов более подробно рассматривается в разделе «Пользовательские атрибуты».

Класс атрибута выглядит примерно так:

using System;

[AttributeUsage(AttributeTargets.Class)]

public class CodeReviewAttribute: System.Attribute

{

public CodeReviewAttribute(string reviewer, string date)

{

this.reviewer = reviewer;

this.date = date;

}

public string Comment

{

get

{

return(comment);

}

set

{

comment = value;

}

}

public string Date

{

get

{

return(date);

}

}

public string Reviewer

{

get

{

return(reviewer);

}

}

string reviewer;

string date;

string comment;

}

[CodeReview("Eric", "01-12-2000", Comment="Bitchin' Code")]

class Complex

{

}

Атрибут AttributeUsage перед именем класса указывает, что данный атрибут может устанавливаться только для классов. При установке атрибута для некоторого элемента программы компилятор проверяет, возможно ли использование атрибута в данном контексте.

Согласно правилам выбора имен, имя класса атрибута завершается строкой Attribute. Это позволяет легко определить, какие классы являются классами атрибутов, а какие представляют собой обычные классы. Все классы атрибутов должны быть производными от System.Attribute.

У нашего класса имеется открытое свойство Comment и один конструктор, которому в качестве параметров передается имя рецензента и дата.

Когда компилятор находит атрибут, установленный для класса Complex, он сначала ищет класс, производный от Attribute, с именем CodeReview. Не обнаружив такого класса, он начинает искать класс CodeReviewAttribute. На этот раз поиск заканчивается успешно.

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

Затем начинается поиск конструктора, который бы соответствовал параметрам, указанным при установке атрибута. Если такой конструктор будет найден, компилятор создает экземпляр объекта вызовом конструктора с заданными параметрами.

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

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

Во всяком случае, так должно происходить на логическом уровне. О том, что же происходит в действительности, рассказано в разделе «Сохранение атрибутов».

Атрибуты

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

public int Test;

Приведенный пример относится к декларативной информации; ее основными достоинствами является лаконичность выражения и то, что вся обработка технических деталей выполняется компилятором.

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

В среде .NET Runtime поддерживаются атрибуты (attributes), которые представляют собой «примечания» к элементам исходного текста программы (классам, членам классов, параметрам и т. д.). При помощи атрибутов можно изменить поведение программы во время выполнения, передать сведения об объекте для проведения транзакций или описать его организационную структуру. Информация атрибутов хранится вместе с метаданными элемента. Ее можно легко прочитать во время выполнения программы, этот процесс называется рефлексией (reflection).

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

using System.Diagnostics;

class Test

{

[Conditional("DEBUG")]

public void Validate()

{

}

}

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

Преобразования

Перечисляемые типы преобразуются к своему базовому типу и обратно с применением явного преобразования:

enum Values

{

A = 1,

B = 5,

C = 3,

D = 42

}

class Test

{

public static void Main()

{

Values v = (Values) 2;

int ival = (int) v;

}

}

У этого правила существует единственное исключение: константа 0 преобразуется к перечисляемому типу без явного указания типа. Это позволяет использовать конструкции следующего вида:

public void DoSomething(BitValues bv)

{

if (bv == 0)

{

}

}

Если бы не это исключение, команду if пришлось бы записывать в следующем виде:

if (bv == (BitValues) 0)

Хотя в данном примере это приемлемо, на практике, если перечисление находится на низком уровне иерархии типов, конструкция получается довольно громоздкой:

if (bv == (CornSoft.PlotLibrary.Drawing.LineStyle.BitValues) 0)

Работа с битовыми флагами перечисления

Перечисления также используются при работе с битовыми флагами; в этом случае каждый бит определяется при помощи двоичной маски. Ниже показано типичное определение:

[Flags]

enum BitValues

{

NoBits = 0,

Bit1 = 0x00000001,

Bit2 = 0x00000002,

Bit3 = 0x00000004,

Bit4 = 0x00000008,

Bit5 = 0x00000010,

AllBits = 0xFFFFFFFF

}

class Test

{

public static void Member(BitValues value)

{

// Какие-то действия

}

public static void Main()

{

Member(BitValues.Bit1 | BitValues.Bit2);

}

}

Перед определением перечисления указан атрибут [Flags], поскольку в программах просмотра и конструирования объектов для флаговых перечислений может быть предусмотрен специальный интерфейс. В таких перечислениях пользователю следует предоставить возможность объединения битов операцией OR, тогда как для прочих перечислений эта операция не имеет смысла.

Функция Main() объединяет два бита операцией OR и передает полученное значение функции класса.

Инициализация

По умолчанию первой перечисляемой величине присваивается 0, а последующие величины инициализируются с единичным приращением. Рядом с каждой перечисляемой величиной можно указать конкретное значение:

enum Values

{

A = 1,

B = 5,

C = 3,

D = 42

}

Также разрешается использовать вычисляемые выражения — при условии, что они зависят только от других величин, определенных ранее в перечислении:

enum Values

{

A = 1,

B = 2,

C = A + B,

D = A * C + 33

}

Объявление перечислений без нулевых членов может вызвать проблемы, поскольку 0 используется для инициализации первой перечисляемой величины по умолчанию:

enum Values

{

A = 1,

B = 2,

C = A + B,

D = A * C + 33

}

class Test

{

public static void Member(Values value)

{

// Какие-то действия

}

public static void Main()

{

Values value = 0;

Member(value);

}

}

В составе перечисления всегда объявляйте величину с нулевым значением.

Базовые типы перечислений

У каждого перечисления имеется базовый тип, который определяет объем памяти, выделяемой для перечисляемых величин. Для перечислений допускаются базовые типы byte, sbyte, short, ushort, int, uint, long и ulong. Если базовый тип не задан, по умолчанию выбирается тип int. Базовый тип указывается после имени перечисления:

enum SmallEnum : byte

{

A,

B,

C,

D

}

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

Перечисление для выбора стиля линий

Рассмотрим графический класс для рисования линий. В этом примере перечисление используется для объявления стиля рисуемой линии:

using System;

public class Draw

{

public enum LineStyle

{

Solid,

Dotted,

DotDash,

}

public void DrawLine(int x1, int y1,

int x2, int y2, LineStyle lineStyle)

{

switch (lineStyle)

{

case LineStyle.Solid:

// Нарисовать сплошную линию

break;

case LineStyle.Dotted:

// Нарисовать пунктирную линию

break;

case LineStyle.DotDash:

// Нарисовать штрих-пунктирную линию

break;

default:

throw(new ArgumentException("Invalid line style"));

}

}

}

class Test

{

public static void Main()

{

Draw draw = new Draw();

draw.DrawLine(0, 0, 10, 10, Draw.LineStyle.Solid);

draw.DrawLine(5, 6, 23, 3, (Draw.LineStyle) 35);

}

}

Перечисление LineStyle определяет фиксированный набор стилей линии; при вызове функций оно задает тип рисуемой линии.

Хотя перечисления предотвращают случайное использование значений, не входящих в заданный набор, значения перечисляемого типа не ограничиваются идентификаторами, указанными при объявлении перечисления. Второй вызов DrawLine() также допустим, поэтому переданное функции значение все равно проверяется на принадлежность набору разрешенных значений. При передаче недопустимой величины класс Draw инициирует исключение «недопустимый аргумент».

Индексаторы и команда foreach

При переборе содержимого объектов, интерпретируемых как массивы, часто бывает удобно использовать команду foreach. Для использования foreach и аналогичных конструкций в других языках .NET объект должен реализовать интерфейс IEnumerable. Этот интерфейс состоит из единственной функции GetEnumerator(), возвращающей ссылку на интерфейс IEnumerator. Функции интерфейса IEnumerator используются при выполнении перебора.

Интерфейс IEnumerator можно реализовать непосредственно в классе контейнера или закрытом классе. Второй вариант является предпочтительным, поскольку он упрощает класс контейнера.

using System;

using System.Collections;

class DataValue

{

public DataValue(string name, object data)

{

this.name = name;

this.data = data;

}

public string Name

{

get

{

return(name);

}

set

{

name = value;

}

}

public object Data

{

get

{

return(data);

}

set

{

data = value;

}

}

string name;

object data;

}

class DataRow: IEnumerable

{

class DataRowEnumerator: IEnumerator

{

public DataRowEnumerator(DataRow dataRow)

{

this.dataRow = dataRow;

index = -1;

}

public bool MoveNext()

{

index++;

if (index >= dataRow.row.Count)

return(false);

else

return(true);

}

public void Reset()

{

index = -1;

}

public object Current

{

get

{

return(dataRow.row[index]);

}

}

DataRow dataRow;

int index;

}

public DataRow()

{

row = new ArrayList();

}

public void Load()

{

/* Загрузка данных */

row.Add(new DataValue("Id", 5551212));

row.Add(new DataValue("Name", "Fred"));

row.Add(new DataValue("Salary", 2355.23m));

}

public object this[int column]

{

get

{

return(row[column - 1]);

}

set

{

row[column - 1] = value;

}

}

int FindColumn(string name)

{

for (int index = 0; index < row.Count; index++)

{

DataValue dataValue = (DataValue) row[index];

if (dataValue.Name == name)

return(index);

}

return(-1);

}

public object this[string name]

{

get

{

return this[FindColumn(name)];

}

set

{

this[FindColumn(name)] = value;

}

}

public IEnumerator GetEnumerator()

{

return((IEnumerator) new DataRowEnumerator(this));

}

ArrayList row;

}

class Test

{

public static void Main()

{

DataRow row = new DataRow();

row.Load();

foreach (DataValue dataValue in row)

{

Console.WriteLine("{0}: {1}",

dataValue.Name, dataValue.Data);

}

}

}

Компилятор преобразует цикл foreach в функции Main() к следующему виду:

IEnumerator enumerator = row.GetEnumerator();

while (enumerator.GetNext())

{

DataValue dataValue =

(DataValue) enumerator.Current;

Console.WriteLine("{0}: {1}",

dataValue.Name, dataValue.Data)

}

Целочисленная индексация

В классе, имитирующем отдельную запись базы данных (совокупность пар «имя/значение»), индексатор может использоваться для обращения к полю записи по номеру:

using System;

using System.Collections;

class DataValue

{

public DataValue(string name, object data)

{

this.name = name;

this.data = data;

}

public string Name

{

get

{

return(name);

}

set

{

name = value;

}

}

public object Data

{

get

{

return(data);

}

set

{

data = value;

}

}

string name;

object data;

}

class DataRow

{

public DataRow()

{

row = new ArrayList();

}

public void Load()

{

/* Загрузка данных */

row.Add(new DataValue("Id", 5551212));

row.Add(new DataValue("Name", "Fred"));

row.Add(new DataValue("Salary", 2355.23m));

}

public object this[int column]

{

get

{

return(row[column - 1]);

}

set

{

row[column - 1] = value;

}

}

ArrayList row;

}

class Test

{

public static void Main()

{

DataRow row = new DataRow();

row.Load();

DataValue val = (DataValue) row[0];

Console.WriteLine("Column 0: {0}", val.Data);

val.Data = 12; // Присвоить значение полю ID

}

}

Класс DataRow содержит функции для загрузки записи, функции для сохранения данных и функцию-индексатор, обеспечивающую доступ к данным. В реальном классе функция Load() должна загружать данные из базы.

Функция-индексатор записывается так же, как свойство, за исключением того, что ей передается дополнительный параметр — индекс. Индексатор объявляется с именем this, поскольку он не имеет собственного имени.

Класс может иметь несколько индексаторов. Например, в классе DataRow было бы удобно выполнять индексацию не только по номеру, но и по имени поля:

using System;

using System.Collections;

class DataValue

{

public DataValue(string name, object data)

{

this.name = name;

this.data = data;

}

public string Name

{

get

{

return(name);

}

set

{

name = value;

}

}

public object Data

{

get

{

return(data);

}

set

{

data = value;

}

}

string name;

object data;

}

class DataRow

{

public DataRow()

{

row = new ArrayList();

}

public void Load()

{

/* Загрузка данных */

row.Add(new DataValue("Id", 5551212));

row.Add(new DataValue("Name", "Fred"));

row.Add(new DataValue("Salary", 2355.23m));

}

public object this[int column]

{

get

{

return(row[column - 1]);

}

set

{

row[column - 1] = value;

}

}

int FindColumn(string name)

{

for (int index = 0; index < row.Count; index++)

{

DataValue dataValue = (DataValue) row[index];

if (dataValue.Name == name)

return(index);

}

return(-1);

}

public object this[string name]

{

get

{

return this[FindColumn(name)];

}

set

{

this[FindColumn(name)] = value;

}

}

ArrayList row;

}

class Test

{

public static void Main()

{

DataRow row = new DataRow();

row.Load();

DataValue val = (DataValue) row["Id"];

Console.WriteLine("Id: {0}", val.Data);

Console.WriteLine("Salary: {0}",

((DataValue) row["Salary"]).Data);

((DataValue)row["Name"]).Data = "Barney"; // Присвоить значение

// полю name

Console.WriteLine("Name: {0}", ((DataValue) row["Name"]).Data);

}

}

Строковый индексатор при помощи функции FindColumn() находит индекс, соответствующий заданному имени, после чего поручает свою работу индексатору для типа int.

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

Эффективность свойств

Давайте вернемся к первому примеру этой главы и попробуем оценить его эффективность:

class Test

{

private string name;

public string Name

{

get

{

return name;

}

set

{

name = value;

}

}

}

На первый взгляд такое решение выглядит неэффективным, поскольку вместо простого обращения к переменной класса в нем вызывается функция. Впрочем, ничто не мешает среде выполнения подставить (inline) функции доступа в код класса, как любые другие простые функции, поэтому выбор свойств вместо простых полей часто[1] не влияет на быстродействие программы. Пересмотр реализации без изменения интерфейса — воистину неоценимая возможность, поэтому свойства обычно являются более предпочтительным вариантом, нежели открытые переменные класса.

У свойств имеется лишь один второстепенный недостаток — они поддерживаются не всеми языками .NET, поэтому в других языках может возникнуть необходимость в непосредственном вызове функций доступа. По сравнению с использованием переменных класса этот вариант чуть более сложен.


[1] Версия .NET Runtime для Win32 выполняет подстановку тривиальных функций доступа, хотя в большинстве других платформ такая возможность не поддерживается.

Статические свойства

C# также позволяет определять статические свойства, принадлежащие классу в целом, а не его отдельным экземплярам. Статические свойства, как и статические функции класса, не могут объявляться с модификаторами virtual, abstract и override.

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

class Color

{

public Color(int red, int green, int blue)

{

this.red = red;

this.green = green;

this.blue = blue;

}

int red;

int green;

int blue;

public static Color Red

{

get

{

return(new Color(255, 0, 0));

}

}

public static Color Green

{

get

{

return(new Color(0, 255, 0));

}

}

public static Color Blue

{

get

{

return(new Color(0, 0, 255));

}

}

}

class Test

{

static void Main()

{

Color background = Color.Red;

}

}

Когда пользователю потребуется объект для одного из стандартных цветов, функция чтения создает экземпляр с правильным набором цветовых составляющих и возвращает его.

Свойства

Несколько месяцев назад при написании программы на C++ у автора возникла ситуация, при которой одна из переменных класса (Filename) могла быть производной от другой переменной (Name). Было принято решение воспользоваться идиомой (или архитектурным шаблоном) свойства и написана для производной переменной функция getFileName(). Это привело к необходимости просмотра всего программного кода и замены всех ссылок на эту переменную вызовами getFilename(). Проект был довольно большим, поэтому на это потребовалось немало времени.

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

В C# свойства реализованы на уровне синтаксиса языка. Для пользователя свойства выглядят как переменные класса, однако чтение и изменение их значений выполняется функциями класса. Таким образом, пользовательская модель (переменная класса) отделяется от модели реализации (функция класса); пользователь изолируется от конкретных технических решений, что обеспечивает большую гибкость при выборе архитектуры и сопровождении класса.

В .NET Runtime свойства реализуются при помощи небольшого объема дополнительных метаданных, связывающих функции класса с именами свойств. Благодаря этому в одних языках свойства выглядят как свойства, а в других — как обычные функции класса.

Свойства широко используются в библиотеке .NET Base Class Library; более того, открытые поля в ней практически не встречаются.

Функции доступа

При объявлении свойства вы указываете один или два блока программного кода, называемых функциями доступа (accessors). Функции доступа читают и/или задают новое значение свойства. Рассмотрим простой пример.

class Test

{

private string name;

public string Name

{

get

{

return name;

}

set

{

name = value;

}

}

}

В этом классе объявлено свойство Name, для которого определена как функция чтения (getter), так и функция записи (setter). Функция чтения просто возвращает значение закрытой переменной класса, а функция записи обновляет переменную при помощи специального параметра value. При каждом вызове функции записи переменная value содержит присваиваемую величину. Тип value совпадает с типом свойства.

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

Свойства и наследование

Свойства, как и функции класса, могут объявляться с модификаторами virtual, override и abstract. Модификаторы включаются в объявление свойства и распространяются на обе функции доступа.

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

Применение свойств

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

Предположим, производитель автомашин хочет получить отчет с текущей информацией о состоянии производства.

using System;

class Auto

{

public Auto(int id, string name)

{

this.id = id;

this.name = name;

}

// Запрос для определения числа произведенных машин

public int ProductionCount

{

get

{

if (productionCount == -1)

{

// Получить количество машин из базы данных.

}

return(productionCount);

}

}

public int SalesCount

{

get

{

if (salesCount == -1)

{

// Обратиться за данными к торговым представителям

}

return(salesCount);

}

}

string name;

int id;

int productionCount = -1;

int salesCount = -1;

}

Свойства ProductionCount и SalesCount инициализируются значением –1. Длительные операции по получению данных откладываются до момента, когда в этих данных действительно возникнет необходимость.

Побочные эффекты при модификации свойств

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

using System;

using System.Collections;

class Basket

{

internal void UpdateTotal()

{

total = 0;

foreach (BasketItem item in items)

{

total += item.Total;

}

}

ArrayList items = new ArrayList();

Decimal total;

}

class BasketItem

{

BasketItem(Basket basket)

{

this.basket = basket;

}

public int Quantity

{

get

{

return(quantity);

}

set

{

quantity = value;

basket.UpdateTotal();

}

}

public Decimal Price

{

get

{

return(price);

}

set

{

price = value;

basket.UpdateTotal();

}

}

public Decimal Total

{

get

{

// При покупке 10 и более единиц — скидка 10 %

if (quantity >= 10)

return(quantity * price * 0.90m);

else

return(quantity * price);

}

}

int quantity; // Количество единиц товара

Decimal price; // Цена за единицу

Basket basket; // Обратная ссылка на корзину

}

В этом примере класс Basket содержит массив объектов BasketItem. При изменении цены или количества единиц товара класс Basket оповещается об этом, перебирает все содержимое корзины и обновляет общую стоимость заказа.

Нетривиальный анализ строковой информации

Применение регулярных выражений для улучшения возможностей функции Split() не дает представления об их настоящей силе. В следующем примере регулярные выражения применяются для анализа файла журнала IIS. Файл журнала выглядит примерно так:

#Software: Microsoft Internet Information Server 4.0

#Version: 1.0

#Date: 1999-12-31 00:01:22

#Fields: time c-ip cs-method cs-uri-stem sc-status

00:01:31 157.56.214.169 GET /Default.htm 304

00:02:55 157.56.214.169 GET /docs/project/overview.htm 200

Следующая программа преобразует файл журнала в более удобный формат.

// Файл: logparse.cs

// Команда компиляции:

// csc logparse.cs /r:system.net.dll /r:system.text.regularexpressions.dll

using System;

using System.Net;

using System.IO;

using System.Text.RegularExpressions;

using System.Collections;

class Test

{

public static void Main(string[] args)

{

if (args.Length == 0) //Должен быть указан анализируемый файл

{

Console.WriteLine("No log file specified.");

}

else

ParseLogFile(args[0]);

}

public static void ParseLogFile(string filename)

{

if (!System.IO.File.FileExists(filename))

{

Console.WriteLine ("The file specified does not exist.");

}

else

{

FileStream f = new FileStream(filename, FileMode.Open);

StreamReader stream = new StreamReader(f);

string line;

line = stream.ReadLine(); // Строка заголовка

line = stream.ReadLine(); // Строка версии

line = stream.ReadLine(); // Строка даты

Regex regexDate= new Regex(@"\:\s(?<date>[^\s]+)\s");

Match match = regexDate.Match(line);

string date = "";

if (match.Length != 0)

date = match.Group("date").ToString();

line = stream.ReadLine(); // Строка Fields

Regex regexLine =

new Regex( // Последовательность цифр или :

@"(?<time>(\d|\:)+)\s" +

// Последовательность цифр или .

@"(?<ip>(\d|\.)+)\s" +

// Любая комбинация непробельных символов

@"(?<method>\S+)\s" +

// Любая комбинация непробельных символов

@"(?<uri>\S+)\s" +

// Последовательность цифр

@"(?<status>\d+)");

// Последовательно читать строки файла

// Сгенерировать описание для каждой строки

while ((line = stream.ReadLine()) != null)

{

//Console.WriteLine(line);

match = regexLine.Match(line);

if (match.Length != 0)

{

Console.WriteLine("date: {0} {1}", date,

match.Group("time"));

Console.WriteLine("IP Address: {0}",

match.Group("ip"));

Console.WriteLine("Method: {0}",

match.Group("method"));

Console.WriteLine("Status: {0}",

match.Group("status"));

Console.WriteLine("URI: {0}\n",

match.Group("uri"));

}

}

f.Close();

}

}

}

Общая структура программы выглядит знакомо. В этом примере использованы два регулярных выражения. Строка даты и регулярное выражение для ее анализа выглядят так:

#Date: 1999-12-31 00:01:22

\:\s(?<date>[^\s]+)\s

В программе регулярные выражения обычно записываются в виде строк-литералов, поскольку синтаксис регулярных выражений допускает использование служебного префикса \. Чтобы регулярное выражение было проще читать, его разбивают на отдельные элементы. Например, следующая запись совпадает в строке с двоеточием (:):

\:

Обратная косая черта (\) необходима из-за того, что в регулярных выражениях двоеточие обладает самостоятельным смыслом. Метасимвол \s соответствует одному пробельному символу (табуляции или пробелу).

В следующей части выражение ?<date> задает имя переменной, которой будут присвоены совпавшие символы для последующего извлечения:

(?<date>[^\s]+)

Выражение [^\s] называется символьной группой, а символ ^ означает «ни один из следующих символов». Таким образом, эта группа совпадает с любым непробельным символом. Наконец, символ + совпадает с одним или несколькими экземплярами предыдущей спецификации (то есть одним или несколькими непробельными символами). В приведенном примере эта часть выражения совпадает с последовательностью 1999-12-31.

Чтобы поиск давал более точный результат, можно воспользоваться метасимволом \d, соответствующим одной цифре. В этом случае все выражение принимает вид:

\:\s(?<date>\d\d\d\d-\d\d-\d\d)\s

Итак, мы разобрались с простым регулярным выражением. Для анализа строк файла журнала применяется более сложное регулярное выражение. Поскольку строки журнала имеют единый формат, в данном примере можно было воспользоваться функцией Split(), но тогда пример стал бы недостаточно показательным. Регулярное выражение состоит из следующих частей:

(?<time>(\d|\:)+)\s // последовательность цифр или : (время)

(?<ip>(\d|\.)+)\s // последовательность цифр или . (IP-адрес)

(?<method>\S+)\s // любые непробельные символы (метод HTTP)

(?<uri>\S+)\s // любые непробельные символы (URL)

(?<status>\d+) // последовательность цифр (статус)

Регулярные выражения

Если функций поиска, поддерживаемых классом String, окажется недостаточно, в пространство имен System.Text входит класс Regex, предназначенный для работы с регулярными выражениями. Регулярные выражения обладают чрезвычайно мощными возможностями поиска и/или замены текста.

В этом разделе приводятся примеры использования регулярных выражений, однако их подробное описание выходит за рамки этой книги. Существует несколько книг, посвященных регулярным выражениям. Эта тема также рассматривается в большинстве книг по языку Perl[1].

В классе регулярных выражений использована довольно интересная методика, обеспечивающая максимальное быстродействие. Вместо того чтобы заново интерпретировать регулярное выражение при каждой операции поиска, класс генерирует короткую программу, которая осуществляет поиск по регулярному выражению. Затем эта программа выполняется в нужный момент[2].

Пример с функцией Split() можно изменить так, чтобы позиции разбиения определялись регулярным выражением вместо отдельных символов-разделителей. При этом из приведенного выше результата исчезает «пустое» слово.

// Файл: regex.cs

// Команда компиляции:

// csc /r:system.text.regularexpressions.dll regex.cs

using System;

using System.Text.RegularExpressions;

class Test

{

public static void Main()

{

string s = "Oh, I hadn't thought of that";

Regex regex = new Regex(@"( |, )");

char[] separators = new char[] {' ', ','};

foreach (string sub in regex.Split(s))

{

Console.WriteLine("Word: {0}", sub);

}

}

}

Программа выводит следующий результат:

Word: Oh

Word: I

Word: hadn't

Word: thought

Word: of

Word: that

Приведенное регулярное выражение разделяет строку либо по пробелам, либо по комбинациям «запятая + пробел».


[1] Например, книга «Perl: библиотека программиста» (Т. Кристиансен, Н. Торкингтон), выпущенная издательством «Питер» — Примеч. перев.

[2] Программа пишется на промежуточном языке .NET — том самом языке, который генерируется C# в результате компиляции.

Класс StringBuilder

Хотя функция String.Format() позволяет генерировать строки в зависимости от значений других строк, это не самый эффективный способ построения строк. В среде времени выполнения существует класс StringBuilder, упрощающий решение этой задачи.

Класс StringBuilder поддерживает следующие свойства и функции:

Свойство

Описание

Capacity

Читает или задает текущую вместимость (количество символов, которые могут храниться в StringBuilder)

[]

Индексатор StringBuilder предназначен для чтения или записи символа в заданной позиции

Length

Читает или задает длину строки

MaxCapacity

Читает максимальную вместимость StringBuilder

Функция

Описание

Append()

Присоединяет строковое представление объекта

AppendFormat()

Присоединяет строковое представление объекта с использованием заданной форматной строки

EnsureCapacity()

Обеспечивает наличие в StringBuilder свободного места для хранения заданного количества символов

Insert()

Вставляет строковое представление заданного объекта в заданную позицию

Remove()

Удаляет заданные символы

Replace()

Заменяет все экземпляры заданного символа в строке новым символом

В следующем примере показано, как использовать класс StringBuilder для построения одной строки из нескольких отдельных строк:

using System;

using System.Text;

class Test

{

public static void Main()

{

string s = "I will not buy this record, it is scratched";

char[] separators = new char[] {' ', ','};

StringBuilder sb = new StringBuilder();

int number = 1;

foreach (string sub in s.Split(separators))

{

sb.AppendFormat("{0}: {1} ", number++, sub);

}

Console.WriteLine("{0}", sb);

}

}

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

1: I 2: will 3: not 4: buy 5: this 6: record 7: 8: it 9: is 10: scratched

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

Преобразование объектов в строки

Переопределение функции object.ToString() во встроенных типах обеспечивает простейший способ преобразования величин в их строковые эквиваленты. Результат вызова ToString() представляет собой стандартное строковое представление величины; другое представление можно получить при помощи функции String.Format(). За дополнительной информацией обращайтесь к разделу главы 30, посвященному форматированию.

Пример

Функция Split() разбивает строку на несколько подстрок по разделителям:

using System;

class Test

{

public static void Main()

{

string s = "Oh, I hadn't thought of that";

char[] separators = new char[] {' ', ','};

foreach (string sub in s.Split(separators))

{

Console.WriteLine("Word: {0}", sub);

}

}

}

Программа выводит следующий результат:

Word: Oh

Word:

Word: I

Word: hadn't

Word: thought

Word: of

Word: that

В символьном массиве separators хранятся символы, по которым разделяется строка. Функция Split() возвращает массив строк, а команда foreach перебирает элементы этого массива и выводит их.

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

Строки

Все строки в C# представляют собой экземпляры типа System.String среды Common Language Runtime, поэтому для работы со строками существует много встроенных операций. Например, в классе String определена функция-индексатор, используемая для перебора символов строки:

using System;

class Test

{

public static void Main()

{

string s = "Test String";

for (int index = 0; index < s.Length; index++)

Console.WriteLine("Char: {0}", s[index]);

}

}

Операции

Класс String является примером неизменяемого (immutable) типа — это означает, что символы, содержащиеся в строке, не могут быть изменены пользователем «на месте». Все операции, выполняемые с классом String, возвращают новую, модифицированную версию строки (вместо модификации того экземпляра, для которого был вызван метод).

Класс String поддерживает следующие функции сравнения и поиска:

Функция

Описание

Compare()

Сравнивает две строки

CompareOrdinal()

Сравнивает два фрагмента строки

CompareTo()

Сравнивает текущий экземпляр с другим экземпляром

EndsWith()

Проверяет, завершается ли строка заданной подстрокой

StartsWith()

Проверяет, начинается ли строка с заданной подстроки

IndexOf()

Возвращает индекс первого вхождения заданной подстроки

LastIndexOf()

Возвращает индекс последнего вхождения заданной подстроки

Класс String() поддерживает следующие функции модификации строк:

Функция

Описание

Concat()

Выполняет конкатенацию двух и более строк или объектов. Если при вызове функции передаются объекты, для них вызывается функция ToString()

CopyTo()

Копирует заданное количество символов из строки в массив, начиная с заданной позиции

Insert()

Возвращает новую строку, полученную в результате вставки подстроки в заданную позицию

Join()

Объединяет массив строк в одну строку, вставляя разделитель между элементами массива

PadLeft()

Выравнивает строку по левому краю поля

PadRight()

Выравнивает строку по правому краю поля

Remove()

Удаляет символы из строки

Replace()

Заменяет все экземпляры символа в строке другим символом

Split()

Создает массив строк посредством разбиения строки по разделителю (в качестве которого выбирается один из символов)

Substring()

Извлекает из строки подстроку

ToLower()

Возвращает версию строки, преобразованную к нижнему регистру

ToUpper()

Возвращает версию строки, преобразованную к верхнему регистру

Trim()

Удаляет пробельные символы (пробелы, табуляции и пр.) из строки

TrimEnd()

Удаляет последовательность символов в конце строки

TrimStart()

Удаляет последовательность символов в начале строки

Тип System.Array

Поскольку массивы C# основаны на типе .NET Runtime System.Array, с ними можно выполнять некоторые операции, не поддерживаемые для традиционных массивов.

Сортировка и поиск

В типе System.Array реализованы встроенные средства сортировки и поиска. Функция Sort() сортирует содержимое массива, а функции IndexOf(), LastIndexOf() и BinarySearch() выполняют поиск элементов в массиве.

Эти функции предназначены для работы со встроенными типами. О том, как организовать их поддержку для пользовательских классов или структур, рассказано в главе 27.

Инверсия

Функция Reverse() переставляет элементы массива в обратном порядке:

using System;

class Test

{

public static void Main()

{

int[] arr = {5, 6, 7};

Array.Reverse(arr);

foreach (int value in arr)

{

Console.WriteLine("Value: {0}", value);

}

}

}

Программа выводит следующий результат:

7

6

5

Преобразования массивов

Преобразования между массивами зависят от их размерности и возможности преобразования между типами элементов.

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

К явным преобразованиям предъявляются аналогичные требования, за исключением того, что элементы S должны явно преобразовываться к типу элементов T:

using System;

class Test

{

public static void PrintArray(object[] arr)

{

foreach (object obj in arr)

Console.WriteLine("Word: {0}", obj);

}

public static void Main()

{

string s = "I will not buy this record, it is scratched.";

char[] separators = {' '};

string[] words = s.Split(separators);

PrintArray(words);

}

}

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

Массивы

Массивы в C# представляют собой ссылочные объекты. Память для них выделяется в пространстве кучи, а не в стеке. Способ хранения элементов массива определяется их типом. Если элемент относится к ссылочному типу (например, string), в массиве сохраняются ссылки на них. Если элемент относится к структурному типу (например, число или тип struct), то элементы сохраняются непосредственно в массиве. Другими словами, массив структурных типов не содержит упакованных экземпляров.

Синтаксис объявления массивов выглядит так:

<тип>[] <идентификатор>;

Изначально ссылка на массив равна null. Объект массива создается оператором new:

int[] store = new int[50];

string[] names = new string[50];

Только что созданный массив заполняется значениями по умолчанию, соответствующими типу хранящихся в нем элементов. Так, каждый элемент массива store представляет собой число типа int, равное 0. В массиве names каждый элемент относится к типу string и равен null.

Инициализация массива

Массивы можно инициализировать в момент создания. При инициализации конструкцию new int[x] указывать не обязательно — компилятор автоматически вычислит размер массива по количеству элементов в списке инициализации:

int[] store = {0, 1, 2, 3, 10, 12};

Приведенная строка эквивалентна следующей:

int[] store = new int[6] {0, 1, 2, 3, 10, 12};

Многомерные и ступенчатые массивы

Для индексации элементов в нескольких направлениях используются многомерные и ступенчатые (jagged) массивы.

Многомерные массивы

Многомерные массивы объявляются с несколькими измерениями:

int[,] matrix = new int[4,2];

matrix[0, 0] = 5;

matrix[3, 1] = 10;

Массив matrix является двумерным. В первом измерении его размер равен 4, а во втором — 2. Инициализация этого массива может быть выполнена следующей командой:

int[,] matrix = {{1, 1}, {2, 2}, {3, 5}, {4, 5}};

Многомерные массивы иногда называют «прямоугольными массивами», поскольку их элементы можно представить в виде прямоугольной таблицы (если размерность массива равна 2). При выделении памяти для массива matrix в куче выделяется один блок, в котором хранятся все элементы массива (рис. 16.1).

<16-01>

Рис. 16.1. Хранение данных в многомерном массиве

Ступенчатые массивы

Ступенчатый массив представляет собой «массив массивов». Он называется ступенчатым, поскольку количество элементов по каждому измерению не является постоянной величиной. Рассмотрим пример:

int[][] matrix = new int[3][];

matrix[0] = new int[10];

matrix[1] = new int[11];

matrix[2] = new int[2];

matrix[0][3] = 4;

matrix[1][1] = 8;

matrix[2][0] = 5;

Одномерный массив matrix состоит из 3 элементов, каждый из которых представляет собой целочисленный массив. Первый элемент-массив содержит 10 чисел типа int, второй — 11 чисел, и третий — всего 2 числа.

Поскольку каждый элемент ступенчатого массива также является массивом, при инициализации каждому элементу массива верхнего уровня присваивается null. Таким образом, каждый элемент также необходимо инициализировать массивом. По этой причине не существует обобщенного синтаксиса инициализации ступенчатых массивов. Впрочем, в «псевдо-двумерном» случае приведенный выше фрагмент можно записать следующим образом:

int[][] matrix = {new int[5], new int[4], new int[2]};

matrix[0][3] = 4;

matrix[1][1] = 8;

matrix[2][0] = 5;

 

Массивы ссылочных типов

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

class Employee

{

public void LoadFromDatabase(int employeeID)

{

// Загрузка данных из базы

}

}

class Test

{

public static void Main()

{

Employee[] emps = new Employee[3];

emps[0].LoadFromDatabase(15);

emps[1].LoadFromDatabase(35);

emps[2].LoadFromDatabase(255);

}

}

При вызове LoadFromDatabase() инициируется исключение, поскольку значения элементов массива еще не были заданы и поэтому остаются равными null.

Этот класс можно привести к следующему виду:

class Employee

{

public static Employee LoadFromDatabase(int employeeID)

{

Employee emp = new Employee();

// Загрузка данных из базы

return(emp);

}

}

class Test

{

public static void Main()

{

Employee[] emps = new Employee[3];

emps[0] = Employee.LoadFromDatabase(15);

emps[1] = Employee.LoadFromDatabase(35);

emps[2] = Employee.LoadFromDatabase(255);

}

}

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

Инициализация массивов не выполняется из соображений быстродействия. Если бы компилятор инициализировал массивы, ему пришлось бы выполнять одни и те же действия для каждого элемента массива. Если бы эта инициализация оказалась ненужной, то все затраты по выделению памяти оказались бы напрасными.

Преобразование к интерфейсу, который может быть реализован объектом

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

using System;

interface IDebugDump

{

string DumpObject();

}

class Simple

{

public Simple(int value)

{

this.value = value;

}

public override string ToString()

{

return(value.ToString());

}

int value;

}

class Complicated: IDebugDump

{

public Complicated(string name)

{

this.name = name;

}

public override string ToString()

{

return(name);

}

string IDebugDump.DumpObject()

{

return(String.Format(

"{0}\nLatency: {0}\nRequests: {1}\nFailures: {0}\n",

new object[] {name, latency, requestCount, failedCount} ));

}

string name;

int latency = 0;

int requestCount = 0;

int failedCount = 0;

}

class Test

{

public static void DoConsoleDump(params object[] arr)

{

foreach (object o in arr)

{

IDebugDump dumper = o as IDebugDump;

if (dumper != null)

Console.WriteLine("{0}", dumper.DumpObject());

else

Console.WriteLine("{0}", o);

}

}

public static void Main()

{

Simple s = new Simple(13);

Complicated c = new Complicated("Tracking Test");

DoConsoleDump(s, c);

}

}

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

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

Использованный в примере оператор as возвращает ссылку на интерфейс, если он реализуется объектом, и null в противном случае.

Преобразование из одного типа интерфейса к другому

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

Преобразования структурных типов

Для структур определено лишь одно встроенное преобразование — неявное преобразование структуры в реализуемый ею интерфейс. Экземпляр структуры упаковывается в ссылочный тип, после чего преобразуется в ссылку на соответствующий интерфейс. Явное преобразование интерфейса в структуру недопустимо.

Преобразование к базовому классу объекта

Ссылка на объект может быть неявно преобразована к ссылке на базовый класс этого объекта. Обратите внимание: сам объект при этом не преобразуется к типу базового класса; только ссылка на него превращается в ссылку на тип базового класса. Эту ситуацию иллюстрирует следующий пример:

using System;

public class Base

{

public virtual void WhoAmI()

{

Console.WriteLine("Base");

}

}

public class Derived: Base

{

public override void WhoAmI()

{

Console.WriteLine("Derived");

}

}

public class Test

{

public static void Main()

{

Derived d = new Derived();

Base b = d;

b.WhoAmI();

Derived d2 = (Derived) b;

object o = d;

Derived d3 = (Derived) o;

}

}

Программа выдает следующий результат:

Derived

Сначала создается новый экземпляр класса Derived, и ссылка на этот объект сохраняется в переменной d. Затем ссылка d преобразуется в ссылку на базовый тип Base. Тем не менее, обе ссылки по-прежнему относятся к объекту типа Derived. В этом нетрудно убедиться, поскольку при вызове виртуальной функции WhoAmI() вызывается версия класса Derived. Ссылку b на тип Base можно преобразовать в ссылку на тип Derived, или же преобразовать ссылку на Derived в ссылку на object и обратно.

Преобразование к базовому типу относится к категории неявных преобразований, поскольку, как говорилось в главе 1, производный класс всегда уточняет базовый класс. Другими словами, Derived является частным случаем Base.

Преобразование базового класса в производный возможно не всегда. Поскольку класс Derived является производным от Base, любая ссылка на Base может оказаться ссылкой на объект Derived, приведенной к типу Base. В такой ситуации можно попробовать произвести обратное преобразование. Во время выполнения программа определяет фактический тип объекта, к которому относится ссылка на Base (b в предыдущем примере), и проверяет, действительно ли это тип Derived. Если это не так, инициируется исключение.

Поскольку object является всеобщим базовым типом, любую ссылку на класс можно преобразовать в ссылку на object, а ссылку на object можно попытаться преобразовать в ссылку на любой класс.

Проверяемые преобразования

В других ситуациях бывает полезно узнать, успешно ли завершилось преобразование. Для этого преобразование выполняется в контексте checked:

using System;

class Test

{

public static void Main()

{

checked

{

uint value1 = 312;

byte value2 = (byte) value1;

Console.WriteLine("Value: {0}", value2);

}

}

}

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

Команда checked создает блок, в котором проверяются выполняемые преобразования. Наличие или отсутствие проверки преобразования определяется на стадии компиляции. Проверка не распространяется на код функций, вызванных из блока checked.

Проверка преобразований несколько замедляет работу программы и потому обычно не используется в коммерческих версиях программ. Тем не менее, в процессе разработки программы бывает полезно включить проверку для всех явных числовых преобразований. У компилятора C# существует ключ /checked, генерирующий код проверки для всех явных числовых преобразований. Этот ключ можно применять на стадии разработки программы, однако в коммерческих версиях он обычно не используется для повышения быстродействия программы.

Если потеря данных предусмотрена в работе программы, запуск компилятора с ключом /checked может вызвать проблемы. В этом случае следует воспользоваться командой unchecked; она означает, что преобразования в блоке проверяться не должны.

Иногда бывает удобно разрешить или запретить проверку в одной команде. В таких случаях оператор checked или unchecked ставится в начале выражения:

using System;

class Test

{

public static void Main()

{

uint value1 = 312;

byte value2;

value2 = unchecked((byte) value1); // Никогда не проверяется

value2 = (byte) value1; // Проверяется при вызове

// с ключом /checked

value2 = checked((byte) value1); // Всегда проверяется

}

}

В данном примере первое преобразование не проверяется никогда, второе преобразование проверяется при вызове компилятора с ключом /checked, а третье преобразование проверяется всегда.

Явные преобразования числовых типов

Явные преобразования выполняются в обратном направлении по сравнению с неявными. Преобразование short в long производится неявно; следовательно, преобразование long в short относится к категории явных.

Определение можно сформулировать и иначе — величина, полученная в результате явного числового преобразования, может отличаться от оригинала:

using System;

class Test

{

public static void Main()

{

uint value1 = 312;

byte value2 = (byte) value1;

Console.WriteLine("Value2: {0}", value2);

}

}

Приведенная программа дает следующий результат:

56

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

Преобразования и идентификация функций класса

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

using System;

class Conv

{

public static void Process(sbyte value)

{

Console.WriteLine("sbyte {0}", value);

}

public static void Process(short value)

{

Console.WriteLine("short {0}", value);

}

public static void Process(int value)

{

Console.WriteLine("int {0}", value);

}

}

class Test

{

public static void Main()

{

int value1 = 2;

sbyte value2 = 1;

Conv.Process(value1);

Conv.Process(value2);

}

}

Программа выводит следующий результат:

int 2

sbyte 1

При первом вызове Process() компилятор находит совпадение с параметром int лишь в одной из функций — той, которая получает параметр типа int.

Однако при втором вызове компилятору приходится выбирать одну из трех функций, получающих параметры типа sbyte, short и int. Чтобы выбрать одну из версий, компилятор сначала пытается выполнить точное сопоставление типа. В данном примере он находит функцию с параметром типа sbyte, поэтому будет вызвана именно эта версия. При отсутствии этой функции была бы выбрана функция с параметром short, потому что тип short может быть неявно преобразован в int. Другими словами, тип short расположен «ближе» к sbyte в иерархии преобразований, поэтому предпочтение отдается именно ему.

Это правило подходит для многих случаев, но в следующем примере оно не поможет:

using System;

class Conv

{

public static void Process(short value)

{

Console.WriteLine("short {0}", value);

}

public static void Process(ushort value)

{

Console.WriteLine("ushort {0}", value);

}

}

class Test

{

public static void Main()

{

byte value = 3;

Conv.Process(value);

}

}

На этот раз описанное правило не позволяет компилятору отдать предпочтение одной функции перед другой, поскольку между ushort и short не существует неявных преобразований.

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

Числовые типы преобразование

 

К числовым типам со знаком и без применяются расширяющие неявные преобразования. Иерархия преобразований показана на рис. 15.1. Если от исходного типа можно по стрелкам перейти к результирующему типу, значит, существует неявное преобразование от источника к результату. Например, существуют неявные преобразования от sbyte к short, от byte к decimal и от ushort к long.

Учтите, что изображенный на рисунке путь от исходного типа к результирующему не означает, что преобразование выполняется именно в такой последовательности. Он всего лишь говорит о том, что такое преобразование возможно. Другими словами, byte преобразуется в long за одну операцию, без промежуточных преобразований в ushort и uint.

class Test

{

public static void Main()

{

// Все эти преобразования выполняются неявно

sbyte v = 55;

short v2 = v;

int v3 = v2;

long v4 = v3;

// Явное преобразование к "меньшим" типам

v3 = (int) v4;

v2 = (short) v3;

v = (sbyte) v2;

}

}

Операторы типа

Операторы типа работают не с данными объекта, а с его типом.

typeof

Оператор typeof возвращает тип объекта в виде экземпляра класса System.Type. При использовании этого оператора можно обойтись без создания экземпляра объекта только для того, чтобы получить объект type. Если экземпляр уже существует, для получения объекта type можно вызвать функцию GetType() для экземпляра.

После получения объекта type к нему можно обращаться с запросами для получения информации о типе, используя механизм рефлексии. За дополнительной информацией обращайтесь к разделу «Подробнее о рефлексии» главы 31.

is

Оператор is проверяет, возможно ли преобразование ссылки на объект к определенному типу интерфейса. Чаще всего при помощи этого оператора узнают, поддерживает ли объект некоторый интерфейс:

using System;

interface IAnnoy

{

void PokeSister(string name);

}

class Brother: IAnnoy

{

public void PokeSister(string name)

{

Console.WriteLine("Poking {0}", name);

}

}

class BabyBrother

{

}

class Test

{

public static void AnnoyHer(string sister, params object[] annoyers)

{

foreach (object o in annoyers)

{

if (o is IAnnoy)

{

IAnnoy annoyer = (IAnnoy) o;

annoyer.PokeSister(sister);

}

}

}

public static void Main()

{

Test.AnnoyHer("Jane", new Brother(), new BabyBrother());

}

}

Программа выдает следующий результат:

Poking: Jane

В этом примере интерфейс IAnnoy реализуется классом Brother, но не реализуется классом BabyBrother. Функция AnnoyHer() перебирает все переданные ей объекты, проверяет, поддерживает ли объект интерфейс IAnnoy, и если поддерживает — вызывает для него функцию PokeSister().

as

Оператор as очень похож на оператор is, но он не просто позволяет узнать, можно ли преобразовать объект к некоторому типу или интерфейсу, но и выполняет явное преобразование к этому типу или интерфейсу. Если преобразование объекта невозможно, оператор возвращает null. Оператор as превосходит оператор is по эффективности, поскольку оператору as достаточно проверить тип объекта всего один раз, а в примере с оператором is тип приходится проверять дважды — при использовании оператора и при преобразовании.

В приведенном примере фрагмент:

if (o is IAnnoy)

{

IAnnoy annoyer = (IAnnoy) o;

annoyer.PokeSister(sister);

}

можно заменить следующим:

IAnnoy annoyer = o as IAnnoy;

if (Annoyer != null)

annoyer.PokeSister(sister);

Операторы присваивания

Операторы присваивания задают новое значение переменной. Присваивание делится на две категории: простое и сложное.

Простое присваивание

Оператор простого присваивания в C# имеет вид одиночного знака равенства (=). Чтобы присваивание успешно сработало, правая часть оператора должна относиться к типу, который может быть неявно преобразован к типу переменной в левой части.

Сложное присваивание

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

+= -= *= /= %= &= = ^= <<= >>=

Оператор сложного присваивания x<операция>=y вычисляется точно так же, как команда x = x <операция>y, с двумя исключениями:

l значение x вычисляется всего один раз, и результат используется как при выполнении операции, так и при присваивании;

l если x содержит вызов функции или ссылку на массив, эта операция выполняется всего один раз.

При обычных правилах преобразования, если x и y относятся к типу short, следующая команда привела бы к ошибке компиляции, поскольку сложение выполняется со значениями типа int, а результат типа int нельзя неявно преобразовать в short:

x = x + 3;

Однако в данном случае, поскольку тип short может быть неявно преобразован в int, и вы можете написать

x = 3;

Такая операция допустима.

Операторы отношения, логические и поразрядные операторы

Операторы отношения используются для сравнения двух величин, а поразрядные операторы — для выполнения поразрядных операций между величинами.

Логическое отрицание

Оператор ! возвращает отрицание логической (булевой) величины.

Операторы отношения

В C# определяются следующие операторы отношения:

Операция

Описание

a == b

Возвращает истину, если a равно b

a != b

Возвращает истину, если a не равно b

a < b

Возвращает истину, если a меньше b

a <= b

Возвращает истину, если a меньше или равно b

a > b

Возвращает истину, если a больше b

a >= b

Возвращает истину, если a больше или равно b

Эти операторы возвращают результат типа bool.

При сравнении двух объектов ссылочного типа компилятор сначала ищет операторы отношения, определенные для этих объектов. Если он не находит подходящего оператора, то для операторов отношения == и != вызывается соответствующий оператор отношения класса object. Этот оператор сравнивает, относятся ли два операнда к одному объекту; значения их переменных при этом не сравниваются.

Для типа string операторы отношения перегружаются так, чтобы == и != сравнивали не ссылки, а содержимое строк.

Логические и поразрядные операторы

В C# определяются следующие логические и поразрядные операторы:

Оператор

Описание

&

Поразрядная операция AND с двумя операндами

|

Поразрядная операция OR с двумя операндами

^

Поразрядная операция XOR (исключающее OR) с двумя операндами

&&

Логическая операция AND с двумя операндами

||

Логическая операция OR с двумя операндами

Операторы &, | и ^ обычно используются с целыми типами данных, хотя они также могут применяться и к типу bool.

Операторы && и || отличаются от своих односимвольных версий тем, что они выполняют ускоренные вычисления. В выражении

a && b

b вычисляется лишь в том случае, если a равно true. В выражении

a || b

b вычисляется лишь в том случае, если a равно false.

Оператор проверки

Оператор проверки (?:), также называемый тернарным оператором, выбирает одно из двух выражений на основании логического условия.

int value = (x < 10) ? 15 : 5

В этом примере сначала вычисляется значение управляющего выражения (x < 10). Если оно равно true, результат оператора равен значению первого выражения, следующего за знаком вопроса (15 в нашем примере). Если управляющее выражение равно false, результат оператора равен значению выражения, следующего за двоеточием (5).

Математические операторы

В следующих разделах приведена краткая сводка математических операций, выполняемых в C#. Вещественные типы подчиняются очень жестким правилам[1]; за более полными сведениями обращайтесь к спецификации CLR. Выполнение в проверяемом контексте математических выражений для невещественных типов может привести к инициированию исключений.

Унарный плюс

Результат операции, реализуемой оператором унарного плюса, просто равен значению операнда.

Унарный минус

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

Сложение

В C# знак + используется как для сложения чисел, так и для конкатенации строк.

Числовое сложение

Значения двух операндов суммируются. Если выражение вычисляется в проверяемом контексте и сумма нарушает границы типа результата, инициируется исключение OverflowException. Числовое сложение продемонстрировано в следующем примере:

using System;

class Test

{

public static void Main()

{

byte val1 = 200;

byte val2 = 201;

byte sum = (byte) (val1 + val2); // Исключения нет

checked

{

byte sum2 = (byte) (val1 + val2); // Исключение

}

}

}

Конкатенация строк

Операция конкатенации может выполняться с двумя строками или между строкой и операндом типа object[2]. Если какой-либо из операндов равен null, вместо него подставляется пустая строка.

Операнды, не относящиеся к типу string, автоматически преобразуются в строку вызовом виртуального метода ToString.

Вычитание

Второй операнд вычитается из первого операнда. Если выражение вычисляется в проверяемом контексте и разность нарушает границы типа результата, инициируется исключение OverflowException.

Умножение

Оператор (*) вычисляет произведение двух операндов. Если выражение вычисляется в проверяемом контексте и произведение нарушает границы типа результата, инициируется исключение OverflowException.

Деление

Первый операнд делится на второй операнд. Если второй операнд равен нулю, инициируется исключение DivideByZero.

Вычисление остатка

Результат x % y вычисляется по формуле x - (x / y) * y. Если значение y равно нулю, инициируется исключение DivideByZero.

Сдвиг

При сдвиге влево (<<) старшие биты отбрасываются, а младшие биты заполняются нулями.

При сдвиге вправо (>>) в типах uint и ulong младшие биты отбрасываются, а старшие биты заполняются нулями.

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

Инкремент и декремент

Оператор ++ увеличивает значение переменной на 1, а оператор -- уменьшает значение переменной на 1[3].

Операторы ++ и -- могут использоваться в префиксной форме (когда значение переменной модифицируется до ее чтения) или в постфиксной форме (когда значение переменной возвращается до ее модификации).

Например:

int k = 5;

int value = k++; // value = 5

value = --k; // value = 5

value = ++k; // value = 6


[1] Они соответствуют стандарту IEEE 754.

[2] То есть операндом произвольного типа, поскольку любой тип может быть преобразован к object.

[3] В программном коде unsafe при использовании инкремента и декремента по отношению к указателям приращение равно размеру объекта, на который ссылается указатель.

Приоритет операторов

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

int value = 1 + 2 * 3; // 1 + (2 * 3) = 7

value = (1 + 2) * 3; // (1 + 2) * 3 = 9

В C# все бинарные операторы являются лево-ассоциативными; это означает, что операции выполняются слева направо. Исключением являются операторы присваивания и оператор проверки (?:), выполняемые справа налево.

В следующей таблице перечислены все операторы в порядке убывания приоритета.

Категория

Операторы

Первичные операторы

(x) x.y f(x) a[x] x++ x-- new typeof sizeof checked unchecked

Унарные операторы

+ - ! ~ ++x --x (T)x

Операторы умножения и деления

* / %

Операторы сложения и вычитания

+ -

Операторы сдвига

<< >>

Операторы отношения

< > <= >= is

Операторы равенства

== !=

Поразрядный оператор AND

&

Поразрядный оператор XOR

^

Поразрядный оператор OR

|

Логический оператор AND

&&

Логический оператор OR

||

Оператор проверки

?:

Оператор присваивания

= *= /= %= += -= <<= >>= &= ^= |=