Занятие 2. Библиотека классов

advertisement
1
Введение
При компьютерном моделировании физических процессов в научных или
образовательных целях большую помощь могут оказать навыки программирования в
современных средах с использованием современных языков и библиотек базовых классов.
В настоящее время на смену прежней библиотеке функций и структур Win32, созданной в
начале 90-ых годов и составляющей основу OS Windows, пришла новая библиотека .NET
(читается «дот-нет»; можно перевести как «сетевое расширение»).
Библиотека .NET создана в начале 2000-ых годов и является библиотекой классов. В
настоящее время библиотека .NET составляет основу операционной системы нового
поколения. Настоящий курс знакомит с библиотекой .NET и языком программирования
C# (читается «си-шарп»; можно перевести как «на пол тона выше си», или «си-диез»),
лежащим в ее основе.
Неформальное введение в C#. Интерфейс IIntegrator
При численной симуляции физических процессов часто встречается ситуация, в которой
математическая модель предполагает решение задачи с начальными условиями, или
задачи Коши. Поэтому, в качестве первого примера, рассмотрим, как может выглядеть
класс интегрирования системы дифференциальных уравнений на языке C#.
Формулировка задачи Коши выглядит довольно просто.
Дана система N обыкновенных дифференциальных уравнений 1-ого порядка вида
dyi/dt = fi (t, y1, y2,…, yN)
с N неизвестными функциями yi(t).
Даны начальные условия – значения неизвестных функций yi0 в некоторой точке t = t0.
Необходимо определить значения функций yi в любой точке t.
Правые части дифференциальных уравнений определены задачей. В каждой точке с
координатами t, y1, y2,…, yN функции fi образуют вектор относительно линейных
преобразований переменных dyi. Эти вектора fi (t, y1, y2,…, yN), заданные в каждой точке,
образуют, как говорят, векторное поле.
При составлении алгоритма необходимо предусмотреть, чтобы класс интегратора имел
доступ к следующим данным
 Число уравнений N;
 Метод, позволяющий вычислять векторное поле fi (t, y1, y2,…, yN) в каждой точке;
 Начальные условия;
 Погрешность, с которой проводится интегрирование;
 Метод, реализующий алгоритм интегрирования до заданной точки;
 Наконец, метод, который предоставит пользователю возможность отладки и
управления кодом в ходе процесса интегрирования. Это так называемая «функция
обратного вызова» (callback function), или «обработчик события».
Вполне возможно, что для реализации конкретного алгоритма понадобятся
дополнительные методы и свойства, но перечисленное является, пожалуй, необходимым
минимумом.
Язык C# позволяет уже на этом, столь общем этапе программирования задачи, написать
классы, которые позволят ограничить рамки задачи и кода, требуемого для ее решения. На
основании сформулированных требований можно составить так называемый «интерфейс»
- совершенно абстрактный класс, состоящий лишь из заголовков методов, которые
должны быть реализованы в любом классе, решающем поставленную задачу.
Назовем наш интерфейс IIntegrator. В языке C# все имена интерфейсов принято
начинать с буквы I. Синтаксис описания интерфейса выглядит следующим образом
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
2
public interface IIntegrator
{
// Здесь описываются члены интерфейса
}
Служебные слова в дальнейшем выделяются полужирным шрифтом. Описание
интерфейса IIntegrator вместе с некоторыми другими вспомогательными классами
будет нами в дальнейшем помещено в отдельную библиотеку классов. Служебный термин
public является одним из так называемых модификаторов доступа. Он означает, что
описание интерфейса будет доступно любому приложению, ссылающемуся на ту
библиотеку, в которой это описание размещается.
Описание так называемых членов интерфейса должно размещаться внутри фигурных
скобок, которые ограничивают описание интерфейса от остальных классов, описываемых
в том же модуле (библиотеке). Любой комментарий к коду, который не должен влиять на
его трансляцию, может быть помещен после двойного знака // (double slash) и до конца
строки.
В интерфейсе IIntegrator должно быть реализовано свойство – число уравнений N. Этот
факт мы опишем в качестве одного из членов интерфейса – так называемого свойства
(property)
ushort N { get;}
Так в C# выглядит описание абстрактного (не реализованного) свойства.
Все члены интерфейсов имеют доступ public, и этот модификатор нигде в описании
членов специально не указывается.
Свойства в C# - это пара методов доступа set и get. Методы доступа устанавливают
(set) и возвращают (get) объект того типа, который указан в описании свойства. Метод
установки set имеет неявный параметр, обозначаемый служебным словом value.
Параметр value является объектом типа свойства. Он содержит в себе значение, которое
передается свойству оператором присваивания, если имя свойства находится в правой
части оператора. В языке C# оператор присваивания имеет вид простого знака равенства
=. Поэтому запись N = 5, могла бы означать в коде C#, что некоторому свойству с именем
присваивается значение 5.
Сразу отметим, что в нашем случае свойства N такое присваивание оказалось бы
невозможным. В описании свойства N отсутствует метод set. А это означает, что
интерфейсом IIntegrator предполагается реализация свойства N только по чтению.
Метод get свойства возвращает объект типа свойства, то есть «работает» в операторе
присваивания, где свойство находится в правой части. Например, curN = N, где curN
предположительно есть некоторое текущее (current) значение свойства N.
Тип ushort описанного свойства N означает, что свойство N должно возвращать любое
целое неотрицательное число, меньшее 65536.
Приведенное описание свойства числа уравнений N в форме «свойства только для чтения»
предполагает, что объекты классов, основанных на интерфейсе IIntegrator (говорят,
«классы, наследующие интерфейс»), уже при своем конструировании будут иметь
заданное число уравнений, для решения которых они создаются. Поэтому необходимости
в изменении значения числа уравнений на протяжении жизненного цикла таких объектов
не будет возникать.
То же замечание будет относиться и к следующему свойству интерфейса IIntegrator
TEquations Equations { get;}
Предполагается, что свойство Equations будет возвращать ссылку на метод, который
готовит локальный вектор уравнений. Тип этого метода имеет имя TEquations и должен
быть описан где-либо вне скобок, содержащих члены интерфейса IIntegrator.
Описание типа TEquations имеет вид
public delegate void TEquations
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
3
(
double t,
//независимая переменная
double [ ] y,
//массив текущих значений неизвестных функций
double [ ] localVector //массив правых частей уравнений
);
Уже знакомый нам модификатор доступа public указывает на то, что тип TEquations
доступен всем приложениям, ссылающимся на готовящуюся библиотеку.
Служебное слово delegate означает, что тип TEquations принадлежит к специальным
классам (делегатам), полями которых являются ссылки на методы заданной сигнатуры.
Термин сигнатура означает совокупность параметров метода, их типы и тип
возвращаемого методом результата. При описании класса делегата сигнатура
указывается в самом описании. Так, в данном случае, полями делегата типа могут быть
ссылки на любой метод, который ничего не возвращает. На это указывает термин void.
Метод, на который ссылается делегат, должен иметь три параметра. Первый параметр t
должен быть типа double, а два других y и localVector должны быть одномерными
массивами объектов типа double произвольной длины. На это указывают скобки [ ].
Объекты типа double хранят положительные и отрицательные числа с плавающей
запятой, модуль которых лежит в интервале [5.0×10−324; 1.7×10308].
Из комментария ясен смысл этих параметров. Отметим, что параметр типа double
передается методу по значению. Другими словами передаются поле структуры,
описанной в библиотеке .NET под именем Double, содержащее значение параметра t и
длиной 8 байт. В то же время параметры типа массива double [ ] передаются в метод по
ссылке, то есть передается адрес области памяти, в которой расположены элементы
массива. Длина адреса в системе с 32-битовыми адресами 4 байта.
Итак, свойства N и Equations описывают систему дифференциальных уравнений.
Предполагается, что эти свойства будут возвращать определенные значения после
создания объекта реализующего (наследующего) интерфейс IIntegrator. При этом
свойство N будет возвращать именно значение, содержимое 2-байтового поля структуры
UInt16 библиотеки .NET (ushort - псевдоним этого типа), а свойство Equations – 4-ех
байтовую ссылку на экземпляр класса делегата типа TEquations.
Интерфейс IIntegrator может предложить для реализации свойства, которые
используются для задания начальных условий интегрирования и чтения неизвестных
функций при текущем значении аргумента после, или в процессе интегрирования
уравнений. Все эти свойства можно объединить в одно свойство, снабженное индексом.
Такое свойство называется индексатором. По правилам C# описание индексатора как
члена интерфейса имеет вид
double this [ushort index] { get; set;}
Служебное слово this необходимо при описании
индексатора. Оно означает, что имя
свойства будет совпадать с именем объекта, свойством которого является. Конкретное
значение параметра передается в качестве дополнительного параметра методу при
установке значения интегратора, и методу при получении этого значения. Пусть,
например, описан объект типа IIntegrator (или класса-наследника от IIntegrator) с
именем integrator. Тогда оператор присваивания
integrator[2] = -0.5;
вызовет метод set индексатора this, передав ему в качестве параметра index значение 2,
а значение -0.5 в качестве параметра value. Так можно установить начальное значение
независимой переменной, сопоставив ей индекс 0, и начальные значения неизвестных
функций, сопоставив им индексы от 1 до N.
Обратно, если необходимо копировать значение, скажем, третьей независимой функции в
некую переменную y3 типа double, можно применить оператор присваивания вида
y3 = integrator[3];
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
4
В общем случае тип параметра index индексатора (в нашем случае ushort), как и тип
возвращаемого объекта (в нашем примере double) могут быть любыми. Параметр index
является не индексом элемента массива, а параметром методов set и get индексатора.
Интегрирование уравнений всегда производится с ограниченной погрешностью. Поэтому
интерфейс IIntegrator предлагает для реализации член типа
double Tolerance { get; set;}
Класс-наследник должен реализовать оба метода доступа set и get свойства Tolerance
(погрешность).
Следующее поле
CallBackEventHandler CallBack { get; set;}
позволит пользователю вмешиваться в процесс интегрирования в точках, которые будут
выделены классом, наследующим интерфейс IIntegrator.
Дело в том, что метод, реализующий алгоритм интегрирования дифференциальных
уравнений, может требовать выполнения довольно большого цикла вычислений.
Представьте себе, что хотя бы одна из неизвестных функций yi(t) имеет период
существенного изменения значительно меньший интервала между начальной и конечной
точкой интегрирования. Тогда, для поддержки необходимой погрешности, цикл
вычислений будет состоять из большого количества шагов. Возможно, пользователю
будет необходимо выводить текущую информацию где-либо внутри цикла
интегрирования, или даже на каждом его шаге. В этом случае класс, реализующий метод
интегрирования, может предусмотреть вызов пользователем так называемой функции
обратного вызова (callback function). Поставив вызов этой функции в некоторые точки цикла
вычислений (например, в конце каждого шага цикла), можно обеспечить пользователя
доступом к этому событию (например, окончания шага цикла). Свойство CallBack
устанавливает и возвращает ссылку на функцию обратного вызова. Эта функция является
объектом класса-делегата, названного здесь CallBackEventHandler – обработчик
события обратного вызова.
Описание класса CallBackEventHandler следует поместить вне скобок с членами
интерфейса IIntegrator.
public delegate void
CallBackEventHandler (Object Sender, CallBackEventArgs e);
Как и в случае класса-делегата TEquations в этом описании указана сигнатура методов,
которые могут быть делегированы типом CallBackEventHandler. Согласно описанию у
метода (или методов), который может делегировать пользователь в качестве
обработчика рассматриваемого события, должно быть два параметра типа Object и
CallBackEventArgs и этот метод не должен возвращать какой-либо объект (void). Такой
набор параметров является стандартным в модели обработки событий платформы .NET,
хотя и не обязательным.
Тип Object является библиотечным классом – предком всех других классов библиотеки
.NET. Поэтому на место параметра Sender (посыльный) может подставляться объект
любого типа. Посыльным является объект, порождающий событие, с которым связан
вызов метода-делегата, или обработчика события. Таким объектом может быть
integrator, описанный выше. Этот объект вызывает обработчик внутри своего цикла и
передает ему в качестве параметра Sender ссылку на себя (this). Ниже мы увидим этот
фрагмент кода.
Второй параметр обработчика, названный e (от слова event – событие), имеет тип
CallBackEventArgs (аргументы события обратного вызова).
Описание класса CallBackEventArgs, которое мы разместили в той же библиотеке, что и
весь предыдущий код, имеет вид
public class CallBackEventArgs : EventArgs
{
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
5
public bool Stop;
}
Это пример описания класса, который имеет предка. Имя класса-предка указывается
после имени класса-наследника через двоеточие.
Здесь предком является класс EventArgs, взятый нами из библиотеки .NET. Объекты
класса EventArgs и его наследников являются стандартными параметрами обработчиков
событий библиотеки .NET. Эти классы содержат данные о событии. Класс-предок пуст в
том смысле, что его объекты не имеют полей – абстрактное событие не имеет данных. Но
классы-наследники могут содержать поля, переносящие данные о событии.
В нашем случае наследник CallBackEventArgs имеет одно поле Stop типа bool. Поле
Stop может принимать два значения true и false. По умолчанию после создания объекта
класса CallBackEventArgs значение поля Stop равно false. Но пользователь, написав
собственный код обработчика, может изменить это значение на true. В реализации
интерфейса IIntegrator значение true может служить сигналом к прерыванию цикла
интегрирования. В этом случае пользователь сможет управлять результатом события,
меняя поле stop параметра e метода, делегированного в качестве обработчика событий.
На этом моменте мы еще раз остановимся при обсуждении кода реализации интерфейса.
Здесь отметим, что в сигнатуре метода, который может быть делегирован в качестве
обработчика событий делегату CallBackEventHandler, типом второго параметра может
быть не только класс CallBackEventArgs, но и его предок – класс EventArgs. Это
допускается синтаксисом и называется свойством контравариантности делегата. Правда
в этом случае пользователь лишит себя возможности использовать поле stop, которое
отсутствует у класса EventArgs.
Возвратимся к описанию членов интерфейса IIntegrator. Последним его членом должен
быть метод, который берет на себя работу по численному интегрированию системы
дифференциальных уравнений с заданными начальными условиями. Заголовок этого
метода мы написали в виде
bool IntegrateTo (double tEnd);
Метод IntegrateTo проводит интегрирование до точки tEnd и возвращает true или false
в зависимости от того, остановлено ли его выполнение обработчиком (false), или метод
завершен естественным путем (true) и найдены значения неизвестных функций в точке
tEnd.
Посмотрите прокомментированный код описанных классов на файле IIntegrator.txt.
В описании кода присутствует строка
using System;
Это ссылка на то, что в файле будут использованы классы из пространства имен System.
Это пространство имен одной из стандартных библиотек .NET. В частности, в
пространстве имен System описан использованный нами класс EventArgs.
Пространства имен используются при написании кода для того, чтобы логически
разделить участки кода. Например, отделить имена классов одной библиотеки от имен
классов другой. В частности, весь написанный нами код помещен в пространство имен
namespace Integrators. Это имя, которое мы дали создаваемой нами библиотеки
интеграторов. Для внешнего пользователя имя интерфейса IIntegrator выглядит как
Integrators.IIntegrator. Оператор точка соединяет в полное имя идентификатора
класса IIntegrator и имя пространства имен Integrators. Если пользователь в какомлибо файле кода будет ссылаться на классы нашей библиотеки, то в начале файла он
сможет набрать строку
using Integrators;
А затем использовать краткие имена наших классов IIntegrator, CallBackEventHandler
и т. д.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
6
Наконец, обратим внимание на особенности комментирования кода, приведенного в
файле. Кроме обычного комментария, выделенного двойным знаком slash //, в коде, как мы
видим, присутствует комментарий, выделенный тройным знаком slash и имеющий
дополнительные идентификаторы, окруженные угловыми скобками.
Например,
///
///
///
///
<summary>
Класс аргументов обработчика события, наступающего внутри
цикла интегрирования (например, после завершения оптимального шага)
</summary>
Этот комментарий используется средой программирования для поддержки справочной
системы библиотеки создаваемых классов. Другими словами, давая такой комментарий,
мы создаем основу для справочной системы своего кода.
Идентификаторы, окруженные угловыми скобками, называются «тэгами». Эти тэги
являются составной частью универсального языка общения, названного XML (eXtensible
Markup Language – «расширяемый язык разметок»). Язык XML широко используются
средой для обмена данными между ее отдельными компонентами. В отличие от
известного языка HTML, построенного на той же идее разметок-тэгов, язык XML
использует любые тэги, заданные его схемой. Кроме того, тэги языка XML используется
не для указания формата изображаемых объектов, а для определения содержательной
части - данных. Так в нашем примере «открывающий» <summary> и «закрывающий»
</summary> тэги говорят о том, что между ними находится краткая справка о классе,
описанном ниже. Везде в коде при ссылке на класс, обладающий таким комментарием,
среда будет выводить справку на экран.
Описание любых класса, их полей, свойств, методов может сопровождаться справкой типа
<summary>. В дополнение к этому тэг <remarks> позволяет вносить примечания к тому
или иному члену кода, как в примере
///
///
///
///
///
///
<summary>
Хранит флаг прерывания интегрирования
</summary>
<remarks>
Служит для прерывания интегрирования, если установлено в true.
</remarks>
Еще один пример
///
///
///
///
///
///
///
///
///
///
///
<summary>
Интегрирование от текущей точки до tEnd
</summary>
<param name="tEnd">
Значение независимой переменной,
в котором интегрирование должно быть прервано
</param>
<returns>
Обычное завершение - возвращает true,
завершение, прерванное обработчиком внутреннего события - false
</returns>
показывает, как описание метода может сопровождаться справкой о его параметрах (тэг
<param…) и возвращаемом значении (тэг <returns>).
Реализация интегратора абстрактным классом
Рассмотрим первый этап реализации интерфейса IIntegrator, спроектировав пока что
абстрактный класс TIntegrator, наследующий этот интерфейс.
Описание класса синтаксически похоже на описание интерфейса. Класс, как и интерфейс,
имеет заголовок с именем и модификатором доступа, и все члены класса, как и члены
интерфейса, заключены в фигурные скобки
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
7
public abstract class TIntegrator : IIntegrator
{
}
В заголовке класса обязательно указывается служебное слово class. В данном случае
указан дополнительный модификатор abstract, который означает, что класс TIntegrator
будет содержать не реализованные методы. Вспомним, что у интерфейса все методы не
реализованы. Абстрактный класс, как и интерфейс, не может создавать объекты. Но, в
отличие от интерфейса, абстрактный класс может иметь поля и реализовывать любое
количество методов и свойств. В заголовке класса TIntegrator указано так же, что он
наследует от интерфейса IIntegrator. Это означает, что класс TIntegrator берет на себя
обязательство реализовать все методы и свойства, описанные в интерфейсе IIntegrator.
Другими словами, если наш класс TIntegrator не реализует (хотя бы в абстрактной
форме) какой-либо из членов интерфейса IIntegrator, транслятор не пропустит такой
код.
Рассмотрим подробнее члены класса TIntegrator.
В начале опишем ряд постоянных полей, которые потребуются для реализации некоторых
методов класса TIntegrator и его наследников.
public const double minTolerance = 1e-15;
public const double maxTolerance = 0.01;
public const double defTolerance = 1e-6;
protected internal const double epsilon = 1e-16;
Постоянные поля имеют модификатор const. Их значения
определяются на стадии
конструирования класса как типа в том смысле, что эти значения фиксированы для всех
объектов класса и его наследников, если имеют достаточный уровень доступа.
Первые два поля minTolerance, maxTolerance ограничивают допустимые значения,
которые может иметь погрешность интегрирования дифференциальных уравнений. Поле
defTolerance определяет погрешность по умолчанию, то есть значение погрешности,
которое будет приниматься в любом объекте интегратора, если это значение не задано
явно.
Эти три постоянных поля имеют максимально высокий уровень доступа public. Они
видны не только методам класса TIntegrator и его наследников, но и всем приложениям,
использующим библиотеку, в которой описан класс TIntegrator. В любой части кода
можно написать, в частности, выражение
double d = TIntegrator.maxTolerance;
Выполнение этого кода поместит значение постоянного поля maxTolerance в объект d.
Однако запись вида
TIntegrator.maxTolerance = 0.1;
приведет к ошибке компиляции, так как значение постоянного поля не может быть
изменено нигде и никем.
Постоянное поле epsilon имеет смысл минимальной по сравнению с единицей величины,
которая используется в некоторых методах класса TIntegrator и его наследников. Поле
epsilon имеет уровень доступа protected internal. Уровень доступа protected
internal позволяет использовать поле epsilon только в самом классе TIntegrator и его
наследниках, описанных в той же библиотеке, что класс TIntegrator.
Далее будем описывать переменные поля и свойства класса TIntegrator.
Следующим кодом класс TIntegrator реализует два свойства N и Equations интерфейса
IIntegrator.
protected internal ushort n;
public ushort N { get { return n; } }
protected internal TEquations equations;
public TEquations Equations { get { return equations; } }
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
8
Как мы видим, поля n и equations, хранящие текущие значения числа уравнений и
ссылку на метод, возвращающий уравнений, имеют модификатор доступа protected
internal, более низкий, чем свойства. Причем свойства N и Equations позволяют лишь
читать значения этих полей, но не менять их. Такая практика программирования
препятствует без нужды рисковать значениями важных полей в период эксплуатации
объектов.
Метод доступа get в том и другом свойстве содержит характерный оператор return.
Оператор return возвращает значение метода-функции, в данном случае метода get, и
прерывает выполнение метода.
Далее можно описать поля
protected internal double [ ] currY;
protected internal double currt;
Они предназначены для хранения текущего (current) вектора неизвестных функций currY и
текущего значения независимой переменной currt. Поле currY описано как ссылка на
массив объектов типа double. На это указывают квадратные скобки. Длина массива не
указана. Она устанавливается после задания числа уравнений и будет равна числу
уравнений n. Нумерация элементов массива начинается индексом 0, поэтому фактически
поле currY будет содержаться ссылка на массив с элементами currY[0],…, currY[n - 1].
Опишем еще два поля и соответствующие им свойства
protected internal double stepSize;
public double StepSize { get { return stepSize; } }
protected internal ulong equationsCallNmb;
public ulong EquationsCallNmb { get { return equationsCallNmb; } }
Поле stepSize хранит текущий шаг интегрирования. Оно доступно только наследникам
класса TIntegrator, описанным в той же библиотеке классов (модификатор доступа
protected internal говорит об этом). Его свойство StepSize доступно всем (имеет
модификатор доступа public). Но свойство StepSize лишь возвращает значение поля
stepSize, и не может его изменить.
Поле equationsCallNmb хранит показания счетчика вызова уравнений, который может
потребоваться пользователю для оценки эффективности того или иного метода
интегрирования. Тип ulong определяет объекты длинных целых чисел без знака,
занимающих 8 байт памяти. Пределы значений таких чисел от 0 до
18,446,744,073,709,551,615. Доступное всем свойство EquationsCallNmb возвращает
пользователю значение поля equationsCallNmb.
Теперь опишем метод, возвращающий интегратор в состояние начала интегрирования
protected internal virtual void Reset ()
{
stepSize = Double.NaN; equationsCallNmb = 0;
}
Модификатор virtual указывает на то, что метод Reset
является виртуальным.
Виртуальные методы могут быть перекрыты (изменены) в классах-наследниках.
Служебное слово void (пустой тип) перед именем метода Reset означает, что метод после
своего выполнения не возвращает объект какого-либо типа. Такой тип метода в C#
аналогичен процедуре в Паскале. Обратите внимание и на то, что хотя метод Reset не
имеет параметров, синтаксис языка C# требует написания круглых скобок после имени
метода.
В теле метода находится два простых оператора присваивания. Первый оператор
присваивает полю stepSize значение Double.NaN. NaN это постоянное поле класса Double
(служебное слово double является псевдонимом класса Double), которое указывает на
особое значение Not a Number объекта типа double. Фактически это неопределенное
значение вещественного числа типа double, или представление числа «не имеющего
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
9
значения». До начала интегрирования размер шага stepSize не определен, так как шаг
зависит от начальных условий и самих дифференциальных уравнений.
Таким образом, метод Reset делает размер шага stepSize не определенным и обнуляет
значение счетчика обращений к уравнениям equationsCallNmb.
Теперь реализуем индексатор this, описанный в интерфейсе IIntegrator.
public double this [ushort index]
{
get
{
return index == 0 ? currt : currY [index - 1];
}
set
{
if ( index == 0 ) currt = value;
else currY [index - 1] = value;
// Любое внешнее изменение полей currt или currY
// предполагает, что задаются начальные условия.
// Это должно приводить к вызову метода Reset,
// "стирающего" оптимальный шаг интегрирования
if ( !Double.IsNaN (stepSize) ) Reset ();
}
}
Выражение
index == 0 ? currt : currY [index - 1]
из метода get называется условным. В нем находится оператор сравнения ==, означающий
«равен ли объект index нулю»? Оператор сравнения возвращает логическое значение true
или false. Если это true, то условное выражение возвращает то, что следует за знаком
вопроса ? (в данном случае currt). В противном случае условное выражение возвращает
то, что следует за знаком двоеточия : (в данном случае элемент массива currY [index 1]). Таким алгоритмом индексатор класса возвращает при нулевом индексе текущее
значение независимой переменной currt, а при других значениях текущее значение
соответствующей неизвестной функции.
На практике такое описание индексатора приведет к следующему эффекту. Пусть описан
объект integrator класса TIntegrator (вернее, наследника класса TIntegrator, так как
сам класс TIntegrator абстрактный и не может создавать объектов). Тогда использование
индексатора может иметь вид оператора
double d = integrator[5];
Это означает, что объекту d будет передано значение, возвращаемое методом get
индексатора, отвечающее значению 5 параметра index. Согласно приведенному выше
описанию индексатора, это должен быть 4-ый элемент массива неизвестных функций
currY.
Но что произойдет, если у интегратора задано всего 4 уравнения и массив currY имеет
границы от 0 до 3? Произойдет останов по ошибке. Возникнет, как говорят,
исключительная ситуация, связанная с тем, что совершена попытка выбрать элемент
массива вне зоны его границ.
Как вариант, можно изменить содержание метода get. Усилим условие, накладываемое на
значение параметра index, подставив во вторую часть условного выражения еще одно
условное выражение вида
index<=n ? currY [index - 1]: Double.NaN
Полный оператор метода get теперь будет иметь вид
return index == 0 ? currt : index<=n ? currY [index - 1]: Double.NaN;
Теперь значение, которое будет возвращать индексатор при любом индексе,
превышающем число уравнений, будет неопределенным (Double.NaN). Исключительная
ситуация не возникнет.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
10
Перейдем к методу set индексатора this.
Метод set содержит условные операторы типа if…else и типа if. В операторе if условие
формулируется в круглых скобках. В данном случае в первом операторе if это условие
равенства нулю индекса index == 0. Во втором операторе if условие !Double.IsNaN
(stepSize) выполняется, если поле stepSize является определенным, то есть не равно
NaN. Метод IsNaN класса Double возвращает логическое значение true или false в
зависимости от того, имеет ли параметр этого метода значение NaN или нет. Унарный
оператор отрицания ! превращает true в false и наоборот.
При выполнении условия index == 0 метод set передает полю текущего значения
независимой переменной currt значение своего параметра value. В противном случае,
когда индекс не равен нулю, выполняется оператор, следующий за словом else. Здесь
значение параметра value метода set передается соответствующему элементу массива
текущих значений неизвестных функций currY [index - 1].
В пользовательском классе метод set индексатора будет вызываться в операторе вида (это
пример!)
integrator[5] = 0.1;
Вызов этого оператора фактически сводится к вызову метода set индексатора с
параметром value, равным 0.1, и параметром index, равным 5.
Если индексы массива неизвестных функций currY ограничены, как и в предыдущем
примере, значениями 0…3 (система содержит четыре уравнения), то вызов метода set
приводит к возникновению исключительной ситуации.
Для того, чтобы избежать исключительной ситуации, можно усилить условие,
дополнительно потребовав, чтобы значение индекса не превышало число уравнений n.
Это усложнит условный оператор if…else, придав ему вид
if ( index == 0 ) currt = value;
else if ( index <= n ) currY [index - 1] = value;
Здесь, как видно, при условии index > n метод set вообще не выполняет никаких
действий. Поэтому в новом варианте кода оператор integrator[5] = 0.1 не выполнит
никаких действий, если число уравнений меньше пяти.
Тем и хороши свойства, что они могут оберегать поля от неразумного использования.
Заметим, правда, что в описанных реализациях методов set и get нельзя с уверенностью
сказать, какой из вариантов кода предпочтительнее. Если пользователь получит
сообщение об исключительной ситуации, то он разберется, поймет ошибку и исправит ее.
Во втором варианте код выполнится без помех и пользователь будет введен в
заблуждение, считая, что его код не содержит ошибочных операторов.
В последнем операторе if метода set вызывается метод Reset, если поле stepSize имеет
осмысленное значение (не NaN). Отличие stepSize от NaN фактически означает, что метод
Reset еще не вызывался. Тем самым метод Reset вызывается лишь один раз при попытке
изменить значение независимой переменной, либо одной из неизвестных функций. При
любом таком изменении интегратор предполагает, что пользователь этим изменением
задает новые начальные условия интегрирования.
Продолжим рассмотрение других членов класса TIntegrator.
protected internal double tolerance;
public virtual double Tolerance
{
get { return tolerance; }
set
{
tolerance =
value > maxTolerance ? maxTolerance :
value < minTolerance ? minTolerance :
value;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
11
}
}
Так описывается поле tolerance, хранящее текущую погрешность интегрирования, и
соответствующее ему свойство Tolerance.
Свойство Tolerance скрытого поля tolerance доступно всем. Оно реализует абстрактное
описание свойства Tolerance интерфейса IIntegrator. Модификатор virtual в описании
свойства означает, что оно является виртуальным и его методы set и get могут быть
перекрыты, изменены в классах – наследниках класса TIntegrator.
Обратим внимание на содержание метода set свойства Tolerance. Если пользователь
задает погрешность, выходящую за границы, определенные постоянными полями
maxTolerance и minTolerance, то поле tolerance принимает значение той границы, за
которую выходит значение параметра value.
Возможен другой вариант кода, предусматривающий в этом случае возбуждение
исключительной ситуации. Тогда пользователь предупреждается о нарушении границ
погрешности. В этом варианте тело метода set может выглядеть следующим образом
if ( value > maxTolerance || value < minTolerance )
{
throw (new ApplicationException (
"Погрешность выходит за допустимые границы."));
}
tolerance = value;
Теперь выполняется полная проверка значения параметра value,
с которым пользователь
передает предполагаемую погрешность. Если это значение больше максимально
допустимого maxTolerance или (оператор || означает «или» в C#) меньше минимально
допустимого minTolerance, то стандартный оператор throw создает («вбрасывает»)
исключительную ситуацию. Параметром оператора throw является объект стандартного
класса ApplicationException. Этот объект создается вызовом оператора new и
конструктора класса ApplicationException с параметром-строкой
"Погрешность
выходит за допустимые границы.". Эта строка будет выведена как комментарий, или
сообщение об ошибке, отвечающей возникшей исключительной ситуации. После
оператора throw выполнение метода set прерывается, и программа ищет обработчик
исключительной ситуации, готовый принять созданный объект с его сообщением.
Обработчик устанавливается пользователем в расчете на возможную исключительную
ситуацию. С технологией обработки исключительных ситуаций мы познакомимся
несколько позже.
Опишем еще одно поле и еще один метод класса TIntegrator
protected internal double [ ] startLocalVector;
protected internal virtual void Initialize ()
{
// Создается экземпляр объекта currY и
// резервируется память под массив текущих значений неизвестных функций
currY = new double [n];
// Элементы массива currY делаются неопределенными (NaN, Not a Number)
for ( ushort i = 0; i < n; i++ ) currY [i] = Double.NaN;
// Независимая переменная делается неопределенной NaN
currt = Double.NaN;
// Создается экземпляр объекта startLocalVector и
// резервируется память под массив правых частей уравнений
startLocalVector = new double [n];
// Погрешность принимает значение «по умолчанию»
Tolerance = defTolerance;
}
Поле-массив startLocalVector используется в некоторых методах класса TIntegrator
и
его наследников. Этот массив хранит начальное значение вектора правой части
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
12
дифференциальных уравнений, необходимый для оценки начального шага
интегрирования.
В виртуальном методе Initialize с ограниченным доступом объявленные выше поля
currY, currt, startLocalVector, и tolerance принимают начальные значения.
Предполагается, что к моменту вызова метода Initialize число уравнений n
установлено. Поля-ссылки на массивы currY и startLocalVector инициализируются с
помощью оператора new и конструктора double [n]. Оператор выделяет память под
хранение необходимого числа элементов массива и передает ссылку на эту память
указанному полю. Конструктор сообщает всю необходимую информацию о создаваемом
объекте оператору new.
Обратите внимание на оператор цикла for (;;). Вслед за ним находится один или
несколько операторов, объединенных фигурными скобками. В нашем коде это один
оператор присваивания currY [i] = Double.NaN. Этот оператор называют «оператором,
которым управляет цикл», или, короче, «телом цикла».
Синтаксис языка C# требует, чтобы оператор цикла типа for (;;) имел в круглых
скобках три выражения, разделенных точкой с запятой. В первом выражении параметр
цикла принимает начальное значение и может быть описан (как в нашем примере). Второе
выражение является условием выполнения тела цикла (в нашем случае условие
выполнения тела i < n). Наконец, в третьем выражении параметр цикла изменяется. В
нашем операторе изменение параметра определяется выражением i++. В языке C#
операция i++ означает «добавление к i единицы и сохранение нового значения в объекте
i».
Оператор for (;;) работает следующим образом:
 сразу после установки начального значения проверяется условие выполнения тела.
Если оно соблюдается, то тело выполняется, если нет, то тело цикла не
выполняется, а совершается «выход из цикла», то есть переход к оператору,
следующему за оператором цикла вместе с его телом.
 После выполнения тела цикла управление передается разделу, изменяющему
параметр цикла. Параметр цикла меняется.
 Затем вновь проверяется условие выполнения тела цикла, и т.д.
В общем случае любой из трех разделов может быть пустым, а первый и третий разделы
могут представлять собой списки выражений, перечисленных через запятую и
инициализирующих и изменяющих параметры цикла.
Продолжим описание членов класса TIntegrator
protected internal sbyte direction;
public sbyte Direction { get { return direction; } }
protected internal CallBackEventHandler callBack;
public virtual CallBackEventHandler CallBack
{ get { return callBack; } set { callBack = value; } }
Поле direction хранит направление изменения независимой переменной (плюс один при
росте и минус один при уменьшении). Соответствующее свойство Direction доступно
всем. Оно возвращает значение поля direction. Тип sbyte означает, что объект этого
типа хранит целое число со знаком объемом в один байт. Пределы значений объектов
типа sbyte [-128; 127].
Поле callBack хранит ссылку на делегата типа CallBackEventHandler, передающего от
пользователя обработчики, или функции обратного вызова. Об этом шла речь в описании
интерфейса IIntegrator. Свойство CallBack реализует свойство интерфейса IIntegrator
с тем же именем и является, как мы видим, виртуальным. Наследник может изменить
содержание его методов set и get.
Далее разместим и прокомментируем описание конструкторов объектов класса
TIntegrator
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
13
protected internal TIntegrator (
ushort N,
TEquations Equations,
CallBackEventHandler CallBack
)
{
if ( N == 0 )
throw (new ApplicationException ("\nЧисло уравнений равно нулю!?"));
n = N;
if ( Equations == null )
throw (new ApplicationException ("\nУравнения не заданы!!!"));
equations = Equations;
Initialize ();
this.CallBack = CallBack;
}
protected internal TIntegrator (ushort N, //Число уравнений
TEquations Equations//уравнения
): this (N, Equations, null) { }
В языке C# конструкторы имеют те же имена, что и имя класса. По виду это обычные
методы с заголовками, содержащими модификатор доступа, имя и список параметров в
круглых скобках. В данном случае модификатор доступа описанных конструкторов
ограничен наследниками, описанными в той же библиотеке (protected internal), так
как класс TIntegrator абстрактный и не может создавать объекты. Ему не нужны
доступные всем конструкторы. Важное отличие описания конструктора от обычного
метода в том, что конструктор не возвращает никакой тип, даже void. Описание
обычного метода должно указывать на тип, возвращаемого объекта.
В классе TIntegrator описаны два конструктора. Но число параметров у них разное.
Такой способ описания нескольких методов с одинаковыми именами, но разной
сигнатурой, называется перегрузкой (overloading).
У первого конструктора три параметра N, Equations и CallBack. Они определяют
соответственно число уравнений, делегат уравнений и делегат функции обратного вызова.
После проверки значений этих параметров условными операторами конструктор
присваивает их соответствующим полям. Если число уравнений равно нулю, или
уравнения не заданы (это означает, что ссылка Equations равна null - это ссылка в
«никуда»), то возбуждаются исключительные ситуации. В строке сообщения при создании
объекта класса исключительной ситуации ApplicationException присутствует символ
\n. Это пример так называемой escape-последовательности, используемой для вывода
специальных символов. Начинается escape-последовательность с символа \ (back slash). За
этим следуют другие буквы из некоторого ограниченного набора. Так символ n означает,
что перед выводом следующей далее строки в окно необходимо перейти на новую строку
(new line).
Далее вызывается метод Initialize, рассмотренный выше. Поле callBack
инициализируется через свое виртуальное свойство CallBack, а не напрямую.
Второй конструктор имеет только два параметра. С его помощью можно создавать объект,
которому не нужна ссылка на функцию обратного вызова CallBack. Второй конструктор
оператором this (N, Equations, null), отделенным двоеточием, просто вызывает
первый конструктор, подставляя в качестве третьего параметра значение null –
обработчик CallBack отсутствует. Тело второго конструктора пустое, но скобки {} писать
необходимо.
Рассмотрим следующий метод класса TIntegrator
protected internal virtual bool DoCallBack ()
{
CallBackEventArgs e = new CallBackEventArgs ();
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
14
callBack (this, e);
return e.Stop;
}
Виртуальный метод DoCallBack возвращает объект типа bool. Так обозначается
логический тип, принимающий два значения true и false. Метод DoCallBack
используется классом TIntegrator для вызова обработчика, или функции обратного
вызова callBack, заданной пользователем.
В теле метода DoCallBack создается экземпляр e класса CallBackEventArgs, который
должен содержать состояние события, обрабатываемого пользователем. Фактически у
объекта e есть единственное поле Stop логического типа. Поле Stop может быть изменено
пользователем, после того как будет вызван метод callBack.
При вызове метода callBack первым параметром является this. Это ссылка на объект
класса, в котором произошло событие. Таковым является класс TIntegrator или его
наследник. Метод DoCallBack должен вызываться классом TIntegrator в точке события.
После вызова обработчика callBack в методе DoCallBack стоит оператор возврата
return, который прекращает выполнение метода DoCallBack и одновременно возвращает
значение поля e.Stop как результат функции DoCallBack. Таким образом, если метод
DoCallBack возвращает true, то цикл интегрирования должен прервать свое выполнение,
а если false, то цикл интегрирования должен продолжиться.
Далее в классе находятся описания трех методов, реализующих поиск оптимального
начального шага интегрирования. Они содержат алгоритм такого поиска, в детали
которого мы не будем вдаваться. Их полный код приведен в тексте файла с описанием
класса TIntegrator. Прокомментируем лишь заголовки и некоторые детали
использования этих методов
protected internal virtual void FirstEstimate (ref double df, ref double dy)
{
…
}
protected internal virtual void SecondEstimate
(ref double der2, double [ ] tempLocalVector)
{
…
}
double StartStepSize (double MaxStepSize)
{
double df = 0.0, dy = 0.0, initStep;
FirstEstimate (ref df, ref dy);
…
double der2 = 0.0, der12, h1;
SecondEstimate (ref der2, tempLocalVector);
…
}
Методы FirstEstimate и SecondEstimate являются виртуальными. Они обеспечивают
алгоритмы первичной и вторичной оценки (estimation) шага интегрирования и вызываются
внутри метода StartStepSize.
Обратим внимание на модификатор ref, использованный при описании параметров
методов FirstEstimate и SecondEstimate. Модификатор ref означает, что вызов объекта
(в данном случае типа double) производится по ссылке (reference). По правилу языка C#
фактический параметр, который подставляется в метод при его вызове на место
параметра, вызываемого по ссылке, должен так же иметь модификатор ref. Именно это
указано в операторах тела метода StartStepSize, вызывающего методы FirstEstimate и
SecondEstimate. При этом фактический параметр с модификатором ref должен к
моменту обращения иметь определенное значение. В данном случае параметры df и dy
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
15
внутри метода StartStepSize принимают нулевые значения перед вызовом метода
FirstEstimate. Так же нулевое значение принимает параметр der2 перед вызовом
SecondEstimate. Значения параметров по ссылке могут быть изменены вызываемым
методом. Другими словами значения df и dy могут отличаться от нуля после вызова
метода FirstEstimate. Так же вызов метода SecondEstimate может изменить значение
объекта der2.
Метод StartStepSize возвращает объект типа double. Это будет найденный им
оптимальный шаг интегрирования. Метод StartStepSize имеет один параметр
MaxStepSize, который используется при определении оптимального шага. Это параметр
по значению и, в отличие от параметров по ссылке, значение MaxStepSize метод
StartStepSize изменить не может.
В описании метода StartStepSize отсутствует модификатор доступа. Это означает, что
модификатором является private. Если модификатор доступа не указывается при
описании какого-либо члена класса (описание «по умолчанию»), то им является
модификатор private. Это правило языка. Член класса с модификатором private
доступен только членам этого же класса, и никому более.
Прокомментируем два оставшихся метода класса
public bool IntegrateTo (double tEnd)
{
for ( ushort i = 0; i < n + 1; i++ )
if ( Double.IsNaN (this [i]) )
throw (new ApplicationException
("\nНе заданы начальные условия интегрирования!"));
if ( tEnd - currt == 0 )
throw (new ApplicationException ("\nШаг интегрирования равен нулю?!"));
direction = (sbyte)(tEnd - currt > 0 ? 1 : -1);
try
{
Equations (currt, currY, startLocalVector);
equationsCallNmb++;
}
catch ( Exception e )
{
throw (new ApplicationException
(e.Message + "\nОшибка при первом обращении к уравнениям!"));
}
if ( Double.IsNaN (stepSize) )
{
stepSize = StartStepSize (Math.Abs (tEnd - currt));
if ( Double.IsNaN (stepSize) || Double.IsInfinity (stepSize) )
{
throw (new ApplicationException
("\nОшибка при вычислении начального шага интегрирования!"));
}
}
else stepSize = Math.Abs (stepSize) * Direction;
return BasicLoop (tEnd);
}
protected internal abstract bool BasicLoop (double tEnd);
Метод IntegrateTo реализует абстрактный метод интерфейса IIntegrator с тем же
именем. У метода IntegrateTo есть один параметр по значению tEnd, в который
подставляется значение независимой переменной, до которого следует интегрировать
систему уравнений. Метод IntegrateTo возвращает объект типа bool, равный true, если
метод завершен «естественным путем», то есть конечная точка интегрирования tEnd
достигнута. В самом конце тела метода IntegrateTo стоит оператор return BasicLoop
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
16
(tEnd), вызывающий метод BasicLoop и возвращающий то значение, которое возвращает
BasicLoop.
Описание метода BasicLoop содержит модификатор abstract. Поэтому метод BasicLoop
не имеет тела и, собственно поэтому, класс TIntegrator является абстрактным. Метод
BasicLoop должен быть реализован в наследниках класса TIntegrator. Там он будет
содержать конкретный алгоритм интегрирования системы дифференциальных уравнений.
Рассмотрим последовательно тело метода IntegrateTo.
При создании объекта класса TIntegrator и его наследников вызывается конструктор. В
свою очередь конструктор вызывает метод Initialize, который придает неопределенные
значения (NaN) независимой переменной currt и неизвестным функциям currY. Эти
значения сохраняются в полях объекта до тех пор, пока их не изменит обращение к
индексатору this. Своим методом set индексатор изменит значения currt и currY на те
числа, которые будут введены в качестве начальных условий. Предполагается, что только
после этого объект будет вызывать метод интегрирования IntegrateTo.
Первые операторы метода IntegrateTo проверяют, заданы ли начальные условия. Для
этого просто проверяется, определены ли поля currt и currY. При этом вызывается метод
IsNaN класса Double, параметром которому передается индексатор this в цикле при всех
допустимых значениях индекса от 0 до n. Если хотя бы одно из проверяемых значений не
определено, метод IsNaN возвратит true. Это, как указано в коде, приведет к
возбуждению исключительной ситуации типа ApplicationException с соответствующим
сообщением и цикл прервет свое выполнение, как, впрочем, и весь метод IntegrateTo.
Код передаст созданный объект исключительной ситуации первому попавшемуся
обработчику.
Если начальные условия заданы, то прерывание не наступает. Далее начальное значение
независимой переменной currt сравнивается с ее конечным значением tEnd –
параметром метода IntegrateTo. При совпадении этих значений так же создается объект
исключительной ситуации с сообщением "\nШаг интегрирования равен нулю?!" и
выполнение метода IntegrateTo прерывается.
Следующий оператор
direction = (sbyte)(tEnd - currt > 0 ? 1 : -1);
устанавливает значение поля direction - направление
изменения независимой
переменной при интегрировании. Обратите внимание на преобразование типов целых
переменных. Объекты 1 или -1 имеют тип int. Но поле direction имеет тип sbyte. Для
перехода от объектов типа int к объектам типа sbyte необходимо писать явный оператор
преобразования типов (sbyte), как указано в коде.
Рассмотрим подробнее следующий оператор вида
try
{
Equations (currt, currY, startLocalVector);
equationsCallNmb++;
}
catch ( Exception e )
{
throw (new ApplicationException
(e.Message + "\nОшибка при первом обращении к уравнениям!"));
}
Это пример оператора, в котором производится обработка исключительной ситуации.
Блок, выделенный служебным словом try (пытайся) содержит код, который должен быть
выполнен. В данном случае здесь вызывается метод Equations, вычисляющий вектор
правой части дифференциальных уравнений startLocalVector. в точке currt, currY.
Затем значение счетчика обращения к уравнениям equationsCallNmb увеличивается на
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
17
единицу. Если при выполнении этих операций возникнет исключительная ситуация
(скорее всего она может возникнуть только внутри метода Equations, заданном
пользователем), то выполнение дальнейших операторов будет прервано и управление
будет немедленно передано в блок catch.
В блоке catch происходит обработка исключительной ситуации. Объект
исключительной ситуации попадает в блок catch через параметр e типа Exception. Это
стандартный класс из библиотеки .NET, который является предком всех классов,
порождающих объекты исключительных ситуаций. В частности, уже встречавшийся ранее
класс ApplicationException является наследником Exception. Каждый объект класса
Exception и его наследников содержит поле Message, несущее сообщение об
исключительной ситуации. В блок catch объект прибывает с конкретным значением поля
Message. В нашем коде, при обработке исключительной ситуации внутри блока catch мы
создаем новый объект исключительной ситуации типа ApplicationException. В качестве
сообщения мы переправляем новому объекту сообщение e.Message о том, что произошло
в методе Equations, и добавляем свое сообщение "\nОшибка при первом обращении к
уравнениям!".
Следующий условный оператор в методе IntegrateTo
if ( Double.IsNaN (stepSize) )
проверяет, имеет ли текущий шаг интегрирования stepSize осмысленное значение.
Значение NaN поля stepSize означает, что шаг еще не определен. Тогда вызывается метод
StartStepSize
stepSize = StartStepSize (Math.Abs (tEnd - currt));
Он вычисляет оптимальный начальный шаг интегрирования. Параметром метода
StartStepSize является максимальный шаг – длина всего интервала интегрирования от
начальной точки currt до конечной tEnd.
В особой ситуации метод может возвратить неопределенное StartStepSize значение
шага. Полученный шаг может иметь значение NaN или быть бесконечным. Если это
происходит, то следующий условный оператор возбуждает исключительную ситуацию,
чтобы прервать выполнение метода IntegrateTo.
if ( Double.IsNaN (stepSize) || Double.IsInfinity (stepSize) )
{
throw (new ApplicationException
("\nОшибка при вычислении начального шага интегрирования!"));
}
Здесь метод IsInfinity класса Double проверяет, имеет ли аргумент (шаг stepSize)
бесконечное значение.
Последний оператор метода IntegrateTo вызывает метод BasicLoop, который должен
содержать алгоритм интегрирования системы дифференциальных уравнений. В классе
TIntegrator метод BasicLoop описан как абстрактный и не имеет тела
protected internal abstract bool BasicLoop (double tEnd);
Это последний член класса TIntegrator. На этом описание
класса TIntegrator
завершается. Можете посмотреть весь текст модуля с описанием класса TIntegrator.
Обратите внимание на структуру комментариев и операторы, из которых состоят тела не
рассмотренных методов FirstEstimate, SecondEstimate и StartStepSize. В теле этих
методов встречаются обращение к стандартным математическим функциям,
сосредоточенным в классе Math библиотеки .NET. Это такие функции, как абсолютное
значение Abs, квадратный корень Sqrt, степень числа Pow, минимальное Min и
максимальное Max из двух чисел. Алгоритм вычисления начального оптимального шага
взят из работы E. Hairer, S.P. Norsett and G. Wanner, Solving ordinary differential equations I,
nonstiff problems, 2nd edition, Springer Series in Computational Mathematics, Springer-Verlag
(1993).
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
18
http://scitation.aip.org/getabs/servlet/GetabsServlet?prog=normal&id=SIREAD00003200000300
0485000001&idtype=cvips&gifs=yes.
Класс RK4
В качестве следующего примера рассмотри класс интегратора, построенный на основе
алгоритма Рунге-Кутта 4-ого порядка и позволяющий создавать объекты. Назовем новый
класс RK4. Наследуем RK4 от абстрактного класса TIntegrator.
В начале приведем общий список членов класса RK4 с комментариями
В заголовке класса RK4 через двоеточие указано, что он наследует от класса TIntegrator.
Следовательно, все члены класса TIntegrator являются так же членами его наследника
RK4. В описании класса RK4 указываются лишь новые члены и те методы и свойства,
которые класс RK4 перекрывает.
public class RK4 : TIntegrator
{
/*Класс RK4 добавляет новые поля, описанные без модификатора доступа и,
следовательно, имеющие доступ private - только для членов класса RK4. Это одно поле
lowestAcc типа double и пять рабочих массивов
tempY,
tempLocalVector,
oneStepLocalVector,
firstHalfStepLocalVector,
secondHalfStepLocalVector, элементами которых являются также объекты типа double.
Свойство lowestAcc хранит пороговое значение погрешности. Это свойство используется
в алгоритме выбора оптимального шага. Во время интегрирования шаг может оказаться
излишне мелким. Таким, что погрешность станет ниже порогового значения. При этом
условии алгоритм увеличит шаг для оптимизации счета. Поля-массивы используются для
организации алгоритма счета в методах класса.*/
double lowestAcc;
double [ ] tempY, tempLocalVector, oneStepLocalVector,
firstHalfStepLocalVector, secondHalfStepLocalVector;
/*Ниже описано виртуальное свойство погрешности Tolerance. В описании свойства
Tolerance присутствует модификатор доступа override. Этим указывается, что
виртуальное свойство Tolerance перекрыто в классе RK4. Внутри методов set и get
свойства Tolerance есть оператор, вызывающий методы set и get предка. Это
base.Tolerance. Как видно метод get свойства Tolerance просто вызывает метод get
предка. В перекрытом методе set проводится дополнительная установка значения
пороговой погрешности lowestAcc. */
public override double Tolerance
{
get { return base.Tolerance; }
set
{
base.Tolerance = value;
// Пороговая погрешность
lowestAcc = 0.025 * tolerance;
}
}
/*Дальше описывается метод Initialize, который так же перекрывает виртуальный
метод Initialize класса TIntegrator. Код метода Initialize рассмотрим несколько
ниже.*/
protected internal override void Initialize ()
{
//…
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
19
/*Конструкторы класса-предка должны быть обязательно перекрыты в наследниках,
даже, если они не вносят никакого дополнительного кода. Это правило языка. Обратите
внимание, что конструкторы класса RK4 описаны с модификатором public. Класс RK4 не
является абстрактным – он не содержит не реализованных свойств и методов. Поэтому его
конструкторы должны быть доступны.
В описании конструкторов RK4 следует обратить внимание на операторы вида base (N,
Equations), отделенные двоеточием от имени конструктора. Это вызов конструкторов
предка. Тела новых конструкторов RK4 в данном случае пусты, но фигурные скобки
писать необходимо. */
public RK4 (ushort N, TEquations Equations) : base (N, Equations) { }
public RK4 (ushort N, TEquations Equations,
CallBackEventHandler CallBack)
: base (N, Equations, CallBack) { }
/*Класс вводит новый метод DoStep, который реализует формулу Рунге-Кутта 4-ого
порядка. Тело метода DoStep будет прокомментировано ниже. Здесь лишь отметим, что
этот метод доступен только классам-наследникам из той же библиотеки (модификатор
доступа protected internal) и он является виртуальным.*/
protected internal virtual void DoStep (double tStart, double currStep,
double [ ] yStart, double [ ] yEnd)
{
// Метод Рунге-Кутта 4-ого порядка
}
// Класс реализует метод BasicLoop, содержащий цикл интегрирования.
protected internal override bool BasicLoop (double tEnd)
{
//…
}
}
Метод Initialize, реализованный классом RK4, добавляет к унаследованной версии
инициализацию рабочих массивов-полей.
protected internal override void Initialize ()
{
base.Initialize ();
try
{
oneStepLocalVector = new double [n];
firstHalfStepLocalVector = new double [n];
secondHalfStepLocalVector = new double [n];
tempY = new double [n];
tempLocalVector = new double [n];
}
catch ( Exception e )
{
throw (new ApplicationException
(e.Message + "\nПрерывание в методе Initialize."));
}
}
Первый оператор метода base.Initialize (); вызывает унаследованную версию метода
Initialize. Обратите внимание на блоки try и catch, предназначенные для обнаружения
и обработки исключительной ситуации в том случае, если рабочие массивы займут
слишком много памяти.
Метод DoStep реализует формулу Рунге-Кутта 4-ого порядка (см. Г. Корн, Т. Корн,
Справочник по математике, стр. 702, Таблица 20.8-1, формула c), которая имеет вид
yEnd = yStart + (k1/2 + k2+ k3+ k4/2) / 3, где
k1/2 = f (tStart, yStart)*h/2;
k2 = f (tStart + h/2, yStart + k1/2)*h;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
20
k3 = f (tStart + h/2, yStart + k2/2)*h;
k4/2 = f (tStart + h, yStart + k3)*h/2;
Здесь функция f является вектором правой части уравнений yi = fi (t, y1,…,yN). Ссылка на
этот вектор возвращается методом equations в третьем параметре (tempLocalVetor в
методе DoStep). Независимая переменная tStart, вектор неизвестных функций yStart, шаг h,
обозначенный currStep, и возвращаемый вектор yEnd являются параметрами метода
DoStep. Сам метод DoStep вычисляет вектор yEnd, последовательно применяя приведенные
формулы.
protected internal virtual void DoStep (double tStart, double currStep,
double [ ] yStart, double [ ] yEnd)
{
// Метод Рунге-Кутта 4-ого порядка
// Реализуется формула yEnd=yStart+(k1/2+k2+k3+k4/2)/3
double HalfStep = 0.5 * currStep;
try
{
// tempLocalVector=f(tStart,yStart);
// k1/2 =tempLocalVector*currStep/2
equations (tStart, yStart, tempLocalVector);
equationsCallNmb++;
for ( ushort count = 0; count < n; count++ )
{
// k1/2
yEnd [count] = HalfStep * tempLocalVector [count];
// yStart + k1/2
tempY [count] = yStart [count] + yEnd [count];
};
// tempLocalVector=f(tStart+currStep/2,yStart+k1/2);
// k2=tempLocalVector*currStep
equations (tStart + HalfStep, tempY, tempLocalVector);
equationsCallNmb++;
for ( ushort count = 0; count < n; count++ )
{
// yS+k2/2
tempY [count] = yStart [count] + HalfStep * tempLocalVector [count];
// k1/2+k2
yEnd [count] = yEnd [count] + currStep * tempLocalVector [count];
};
// tempLocalVector=f(tStart+currStep/2,yStart+k2/2);
// k3=tempLocalVector*currStep
equations (tStart + HalfStep, tempY, tempLocalVector);
equationsCallNmb++;
for ( ushort count = 0; count < n; count++ )
{
//yStart+k3
tempY [count] = yStart [count] + currStep * tempLocalVector [count];
//k1/2+k2+k3
yEnd [count] = yEnd [count] + currStep * tempLocalVector [count];
};
// tempLocalVector=f(tStart+currStep,yStart+k3);
// k4/2=tempLocalVector*currStep/2
equations (tStart + currStep, tempY, tempLocalVector);
equationsCallNmb++;
}
catch ( Exception e )
{
throw (new ApplicationException
(e.Message + "\nОшибка при обращении к уравнениям!"));
}
//Результирующие значения функций после данного шага
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
21
for ( ushort count = 0; count < n; count++ )
{
// yEnd = yStart+(k1/2+k2+k3+k4/2)/3
yEnd [count] = yStart [count] + 0.333333333333333333 *
(yEnd [count] + HalfStep * tempLocalVector [count]);
if ( Double.IsNaN (yEnd [count]) || Double.IsInfinity (yEnd [count]) )
throw (new ApplicationException
("\nКомпонента NaN или Infinity в методе DoStep!"));
};
}
Комментарии в тексте расшифровывают эту формулу и алгоритм ее реализации.
Обратите внимание на то, что практически все тело метода DoStep помещено в блок
try…catch. Если при выполнении метода возникает исключительная ситуация (это, скорее
всего, происходит во время одного из обращений к методу equations), обработчик блока
catch возбуждает новую исключительную ситуацию, добавляя к сообщению e.Message о
возникшей ситуации свое сообщение.
Теперь рассмотрим подробнее реализацию метода BasicLoop.
protected internal override bool BasicLoop (double tEnd)
{
double currStep, currHalfStep, Divergence, maxDivergence;
bool isGoodAcc, isLastStep, isStepTooSmall;
try
{
// Текущий шаг currStep выбирается равным оптимальному.
currStep = stepSize;
// Перед входом в цикл вызывается обработчик
if ( callBack != null && DoCallBack () ) return false;
// Цикл прохождения всего полного шага интегрирования
do
{
// Если текущий шаг составляет более 80% всего интервала,
// он выбирается равным интервалу tEnd-FCurrt.
// Флаг isLastStep регулирует выход из основного цикла.
if ( (currt + 1.25 * currStep - tEnd) * direction >= 0 )
{
// Шаг является последним
currStep = tEnd - currt; isLastStep = true;
}
else isLastStep = false; //Это не последний шаг
// Флаг isGoodAcc регулирует возможность увеличения шага,
// если погрешность излишне мала
isGoodAcc = true;
//Допустим рост шага
//Делается один текущий шаг
DoStep (currt, currStep, currY, oneStepLocalVector);
//Цикл выбора шага, удовлетворяющего требуемой точности.
do
{
//Делаются две половины шага
currHalfStep = 0.5 * currStep;
DoStep (currt, currHalfStep, currY, firstHalfStepLocalVector);
DoStep (currt + currHalfStep, currHalfStep,
firstHalfStepLocalVector, secondHalfStepLocalVector);
// Вычисляется макс. относительное расхождение
// в результатах одного шага и двух половинок
maxDivergence = 0.0;
for ( ushort count = 0; count < n; count++ )
{
if ( Double.IsInfinity (1.0 / oneStepLocalVector [count]) &&
Double.IsInfinity (1.0 / secondHalfStepLocalVector [count]) )
continue;
if ( oneStepLocalVector [count] != secondHalfStepLocalVector [count] )
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
22
{
// Абсолютное расхождение
Divergence = Math.Abs (oneStepLocalVector [count] secondHalfStepLocalVector [count]);
// Относительное расхождение вычисляется
// при достаточно большом абсолютном
if ( Divergence > epsilon )
Divergence =
2.0 * Divergence /
(Math.Abs (oneStepLocalVector [count]) +
Math.Abs (secondHalfStepLocalVector [count]));
if ( Double.IsInfinity (Divergence) || Double.IsNaN (Divergence) )
throw (new ApplicationException
("\nПроблема при вычислении maxDiv"));
// Корректируется значение максимального расхождения
if ( Divergence > maxDivergence ) maxDivergence = Divergence;
}
}
//Делаются выводы по сравнению с требуемой точностью
if ( maxDivergence > tolerance )
{
isGoodAcc = false;
// Рост шага запрещен
currStep = currHalfStep; // Шаг уменьшается вдвое
stepSize = currHalfStep;
isLastStep = false;
firstHalfStepLocalVector.CopyTo (oneStepLocalVector, 0);
}
isStepTooSmall = currt == 0.0 && Math.Abs (currStep) <= epsilon
|| Math.Abs (currStep) <= Math.Abs (currt) * epsilon;
}
// Процесс проверки и уменьшения шага продолжается
// до достижения требуемой точности
while
( maxDivergence > tolerance && !isStepTooSmall );
if ( isStepTooSmall )
{ throw (new ApplicationException ("\nШаг слишком мал")); }
// Точность достигнута. Устанавливаются скорректированные значения
// функций в конце сделанного шага
for ( ushort count = 0; count < n; count++ )
currY [count] =
secondHalfStepLocalVector [count] + 0.0666666666666666667 *
(secondHalfStepLocalVector [count] oneStepLocalVector [count]);
//Если шаг не последний
if ( !isLastStep )
{
currt += currStep; //Изменение переменной
//Вызывается обработчик
if ( callBack != null && DoCallBack () ) return false;
if ( maxDivergence < lowestAcc && isGoodAcc )
{
//Слишком мелкий шаг удваивается
currStep += currStep;
if ( Math.Abs (currStep) > Math.Abs (tEnd - currt) )
currStep = tEnd - currt;
stepSize = currStep;
}
}
//Прерывание цикла, если сделан последний шаг
else break;
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
23
while ( true );
//Текущее значение переменной совпадает с конечным
currt += currStep; //tEnd
//Вызывается обработчик
return !(callBack != null && DoCallBack ());
}
catch (Exception e)
{
throw (new ApplicationException
(e.Message+ "\nBasicLoop прерван"));
}
}
В начале метод BasicLoop копирует поле текущего оптимального шага stepSize в
локальную переменную currStep. Значение поля stepSize либо было инициализировано
унаследованным методом IntegrateTo, либо сохранилось от предыдущего вызова того же
метода.
Затем вызывается обработчик callBack, который может быть задан пользователем класса.
В условном операторе логическая формула callBack != null && DoCallBack ()
возвращает true лишь в том случае, если обработчик callBack задан (ссылка на него не
равна null – ссылке в «никуда») и метод DoCallBack, вызывающий обработчик,
возвращает значение true. В этом случае выполнение метода BasicLoop прерывается, а
сам метод возвращает false. Если обработчик callBack не определен (тогда ссылка на
него имеет значение null), либо он определен, но DoCallBack возвращает после своего
выполнения значение false, выполнение метода BasicLoop продолжается.
Далее в методе BasicLoop находится оператор цикла do…while(true). Он охватывает
большую часть операторов метода BasicLoop. Такой оператор (условие while(true)
всегда выполняется) является бесконечным циклом, и выход из него производится
стандартной командой break, прерывающей выполнение любого цикла в C#. Внутри
оператора do…while происходят оценки, позволяющие провести интегрирование
уравнений от значения currt независимой переменной до tEnd - параметра метода
BasicLoop.
В начале цикла длина всего интервала интегрирования |tEnd - currt| сравнивается с
величиной текущего шага |currStep|. Если шаг превышает 80% длины интервала, то он
выбирается равным всему интервалу и флаг isLastStep, указывающий на то, что это
последний шаг, устанавливается в true. В противном случае флагу isLastStep
присваивается значение false.
Следующий оператор устанавливает локальный флаг (объект типа bool) isGoodAcc,
регулирующий возможность увеличения шага, в true.
Затем, с помощью метода DoStep вычисляется результат оптимального шага currStep по
формуле Рунге-Кутта. Этот результат возвращается в виде вектора приближенного
решения oneStepLocalVector в точке currt + currStep.
Следующим оператором метода BasicLoop является новый цикл do…while, который
«настраивает» шаг currStep в соответствии с допустимой погрешностью.
Для этого с помощью метода DoStep вычисляется результат шага той же длины currStep,
но выполненного двумя половинными шагами. Результирующие вектора запоминаются в
массивах
firstHalfStepLocalVector
и
secondHalfStepLocalVector.
Между
компонентами вектора secondHalfStepLocalVector и компонентами вектора
oneStepLocalVector, полученного одним полным шагом, устанавливается максимальное
относительное расхождение maxDivergence. Если полученное расхождение превышает
допустимую погрешность tolerance, то текущий шаг currStep (как и поле оптимального
шага stepSize) уменьшается вдвое, флаги «хорошего приближения» isGoodAcc и
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
24
«последнего шага» isLastStep «опускаются» (принимают значение false), а компоненты
вектора последней точки искомой функции oneStepLocalVector принимают значения
компонент вектора, полученного после половинного шага, firstHalfStepLocalVector.
При вычислении расхождений Divergence и maxDivergence проводится ряд оценок,
позволяющих возбудить исключительную ситуацию, или обойти ее при некоторых
значениях переменных. В частности стандартный оператор continue позволяет в
определенной ситуации обойти все проверки, попав сразу на следующий шаг цикла.
Так как уменьшение шага не может продолжаться до бесконечности, устанавливается
значение флага isStepTooSmall в зависимости от значения текущего шага.
Условием окончания цикла уменьшения шага является либо условие достижения
требуемой точности maxDivergence < tolerance, либо недопустимо малое значение шага
(флаг isStepTooSmall поднят).
Если шаг слишком мал, то возбуждается исключительная ситуация с необходимым
сообщением.
После выбора шага, удовлетворяющего погрешности, компонентам вектора неизвестной
функции в конечной точке придаются скорректированные ошибкой значения.
Далее стоит условный оператор, в результате которого цикл, а за ним и сам метод
BasicLoop будет завершен, если сделанный шаг последний на заданном интервале. Если
это не так, то выполняется ряд операторов, в которых
 устанавливается новое значение поля независимой переменной currt,
 вызывается обработчик callBack (если он задан)
 и при условии, что флаг isGoodAcc поднят (т.е. рост шага допустим) и
максимальное расхождение maxDivergence меньше нижней границы погрешности
lowestAcc, текущий шаг currStep удваивается и передает свое значение полю
оптимального шага stepSize. При этом если удвоенный шаг выводит точку за
границу интервала tEnd, значение currStep корректируется.
Далее цикл возвращается к началу, имея новые значения currt, соответствующий вектор
currY и, возможно, новое значение текущего шага currStep и поля оптимального шага
stepSize.
После завершения цикла поле независимой переменной currt принимает конечное
значение tEnd, делая последний шаг, и вызывается обработчик callBack.
Занятие 1. .NET – C# - MS Visual Studio
Частью платформы .NET является мощная библиотека классов. Библиотека написана на
языке C#, который, пожалуй, лучше всего приспособлен для программирования на
платформе .NET. Использование среды MS Visual Studio значительно облегчает создание
приложений на языке C#.
1.1 Введение
Особенности программирования на платформе .NET
Конечной целью программиста на платформе .NET (произносится дот-нет) является
создание и компиляция кода программы в форме одного или нескольких исполняемых
модулей (module). Исполняемым модулем является файл типа .dll или .exe, содержащий
исполняемый код. Модуль типа .exe отличается от .dll тем, что содержит точку входа в
программу.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
25
Файлы исполняемых модулей не являются окончательным исполняемым кодом,
доступным процессору, но представляют собой перевод кода программиста на так
называемый «промежуточный язык» (MSIL – Microsoft Intermediate Language). Достоинством
такого подхода является кросс-платформенность - код с промежуточного языка может
быть транслирован в любой локальный код, понятный конкретному процессору. Для
этого, естественно, на компьютере должна быть установлена платформа .NET.
Трансляция промежуточного кода в локальный код осуществляется после активизации
приложения (run time). В начале транслируется минимально необходимая часть кода.
Дальнейшая трансляция регулируется потребностью. Но любая часть кода транслируется
только один раз.
Программист пишет код на одном из языков, для которого имеется компилятор,
переводящий программу в промежуточный язык. Основным языком такого типа является
C# (произносится си-шарп).
На уровне программиста будущий исполняемый модуль типа .dll или .exe представляет
собой один или несколько текстовых файлов – компилируемых модулей (compiled units).
Термины компилируемый модуль и исполняемый модуль относятся к разным объектам, и
их следует различать. Несколько компилируемых модулей, обычно формирующих
приложение, собираются в процессе компиляции; к ним добавляются сведения,
описывающие отличительные свойства исполняемого модуля, или сборки (assembly).
Сведения о сборке, образующие так называемый манифест сборки, включают в себя имя,
версию, ссылки на другие исполняемые модули, описание типов, доступных внешним
модулям и самой среде (self-describing), и т.п.
В каждой не пустой сборке должен быть, по крайней мере, один компилируемый модуль,
содержащий описания классов – носителей программного кода и данных.
Пространство имен (namespace)
Пространство имен служит для изоляции имен одной части кода от другой. (В Delphi
такую изоляцию обеспечивают модули, unit.) Имена из одного пространства имен не
вступают в конфликт с такими же именами из другого пространства имен. Так, имя name в
пространстве имен ns1 не конфликтует с таким же именем name в пространстве имен ns2,
так как их полные имена ns1.name и ns2.name разные.
 Весь текст компилируемых модулей, формирующих сборку, может принадлежать
одному или нескольким пространствам имен (namespace).
 Одно пространство имен может охватывать несколько компилируемых модулей.
 Один компилируемый модуль может содержать несколько пространств имен.
Класс с именем class1, описанный внутри пространства имен с именем name1,
имеет для внешних пространств имен имя name1.class1.
 Пространство имен может содержать вложенное пространство имен. Имя
вложенного пространства имен для внешнего пользователя представляет собой так
же последовательность простых имен, соединенных оператором точка . (dot).
Осмысленное (не пустое) пространство имен состоит из описания отдельных классов –
типов данных, содержащих в себе как сами данные, так и методы, ими управляющие
(encapsulation).
Классы имеют имена. Пространства имен изолируют имена классов. Весь программный
код сосредоточен в методах классов. Код состоит из операторов. Это операторы
арифметических и логических действий, операторы присвоения, цикла, условные
операторы, операторы, вызывающие другие методы, операторы, создающие объекты
других классов и т.д.
В пространство имен может входить описание одного или нескольких из перечисленных
ниже типов классов.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
26




(собственно класс) и struct (структура) - два типа классов, содержащих
описания методов и их реализацию. Объекты классов class являются объектами
по ссылке - содержат в себе ссылку на ту область памяти, где собственно
содержатся все данные (поля) класса. Объекты структур являются объектами по
значению - непосредственно содержат все данные, входящие в состав структуры.
interface - тип класса, все методы которого не реализованы, но объявлены. Это
так называемые интерфейсы. Предполагается, что для каждого интерфейса
существует хотя бы один class или struct, реализующий его методы. Класс типа
class или struct, реализующий некоторый интерфейс, является наследником
этого интерфейса.
enum - тип структур, объекты которых содержат элементы конечного счетного
множества имен-обозначений целых чисел. Например, некий класс Жених может
содержать качества-имена красивый, молодой, богатый и, вообще говоря, их
произвольные сочетания. Под именами скрываются целые числа, например,
красивый - 0, молодой - 2, богатый - 4. Такие структуры называются
нумераторы, или перечислимые типы (enum).
delegate - тип класса, полями которого являются ссылки на методы заданного
типа. Тип метода определяется числом параметров, типами параметров и типом
возвращаемого объекта. Это так называемая сигнатура метода ( signature). Объекты
делегатов хранят и передают (делегируют) ссылки на методы с определенной
сигнатурой одного (делегирующего) класса другому (делегируемому). Обычно
делегаты используют для создания методов-обработчиков событий. Событие,
наступающее в делегируемом классе, обрабатывается с участием метода
делегирующего класса.
class
Структура классов
Типы class и struct состоят из членов класса (class member). К членам класса относятся
 поля (field). Поля хранят данные, отличающие один экземпляр объекта данного
класса от другого; интерфейсы не имеют полей, полями нумераторов являются
числа, стоящие под именами нумератора, а полями делегата являются ссылки на
методы и классы, эти методы делегирующие;
 методы (method), объединяющие операторы кода и отличающиеся друг от друга
именами и/или сигнатурой;
 свойства (property) – поименованная пара методов доступа (accessors) set
(установить) и get (возвратить) (или, по крайней мере, один из них), обычно
используемых для доступа к значениям (value) полей класса;
 индексаторы (indexer) – поименованная пара методов доступа (accessors) set и get
(или, по крайней мере, один из них), устанавливающие и возвращающие
некоторую величину (value), ассоциирующую значение объекта с элементом
индексированного множества (массива);
 события (event) – специальное свойство типа делегата с парой методов доступа
(accessors) add (добавить) и remove (исключить), позволяющих удлинять или
укорачивать цепочку методов (обработчиков), делегируемых для обработки
события. Делегируемые методы выполняются в определенной точке кода и при
выполнении определенного условия, т.е. при определенном событии. Например,
при опросе очереди событий от мышки обнаруживается, что клавиша мышки была
нажата – click-событие. Если цепочка делегатов click-события не пуста, все
делегаты из цепочки вызываются при наступлении события.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
27

константы (const) – неизменные поля данных, имеющие одинаковые значения
для всех экземпляров класса; интерфейсы не имеют констант;
 операторы (operator) – это операторы из числа стандартных операторов
арифметических и логических действий, операторов сравнения и т.п., действия
которых переопределяются в отношении объектов описываемого класса;
 конструкторы (constructor) – специальные методы, носящие имя класса и
вызываемые при создании экземпляра класса – объекта; интерфейсы не имеют
конструкторов;
 деструктор (destructor) – специальный метод (может быть только один на класс и
только в классах типа class), код которого выполняется непосредственно перед
тем, как экземпляр класса перестает быть доступным («возвращается в кучу»).
Кроме того, в структуру типов class и struct могут входить описания вложенных типов
class, struct, delegate, enum, interface, но не пространств имен (namespace).
Членами класса типа interface могут быть только свойства, методы, события и
индексаторы.
Описание одного типа class, struct или interface может охватывать несколько
компилируемых модулей (частичное, или partial описание), но в любом случае должно
принадлежать одной сборке (assembly).
Библиотека классов (Class Library)
Платформа .NET содержит библиотеку классов. Классы библиотеки имеют свои
собственные имена, но доступ к ним осуществляется через расширенные (или, полные)
имена. Например, базовый класс, наследниками которого являются все остальные классы
библиотеки, имеет собственное имя Object. Полное его имя System.Object, так как класс
Object принадлежит пространству имен System. Еще один пример. Класс Stream
(поток) принадлежит пространству имен System.IO (Input-Output) и имеет полное имя
System.IO.Stream. Пространство имен IO является вложенным в пространство имен
System.
Физически библиотека классов находится в исполняемых модулях (сборках, или assemblies)
типа .dll (библиотека классов). К этим модулям относятся mscorlib.dll, System.dll, System.Data.dll
и т.д. Названия пространств имен не обязательно совпадают с именами модулей. Так
классы System.Object и System.IO.Stream физически находятся в модуле mscorlib.dll. А,
например, класс DataSet принадлежит пространству имен System.Data и находится в
модуле System.Data.dll.
Среда и настройки
Прежде всего, следует установить на компьютере среду программирования (IDE – Integrated
Development Environment), содержащую средства, необходимые для программирования на
языке C#. Хотя тексты предлагаемых ниже примеров можно набирать и в обычном
текстовом редакторе (например, в Блокноте), но транслятор с языка C# в любом случае
необходим. Удобнее с самого начала работать в готовой среде. В дальнейшем мы
предполагаем, что у читателя установлена среда MS Visual Studio 2005.
Все команды, которые мы будем использовать (их число невелико), находятся в
различных меню среды. Ссылки на эти команды мы будем писать в форме, к примеру,
File.New.Project…. Здесь File имя меню, New – команда из меню File, Project… - команда
подменю команды New. Многоточие в конце имени команды означает, что ее выполнение
приводит к появлению диалогового окна.
Среда имеет кнопки быстрого вызова многих команд меню, расположенные
непосредственно под строкой меню. Кнопки сгруппированы в отдельные панели – Toolbars.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
28
Присутствие на экране панелей и кнопок регулируется настройками среды командой
Tools.Customize…. В появившемся окне на странице Toolbars регулируется наличие на экране
отдельных панелей инструментов, а на странице Commands - отдельных кнопок на панели.
Для добавления к Toolbars кнопки с командой, например, Open Project обратитесь к странице
Commands. В категории команд File (список категорий находится слева) найдите в правом
списке команду Open Project. Нажав левую кнопку мышки, «захватите» эту команду и
перенесите ее в какое-либо место стандартной панели. Удалить кнопку с панели можно
обратным маневром – «захват» и перенос в окно настроек.
На той же странице Commands имеется кнопка Keyboard…. Она позволяет перейти к окну
настроек сочетаний клавиш для быстрого вызова отдельных команд меню. По желанию
можно выбрать любое сочетание клавиш для вызова любой команды меню.
1.2 Solution и Project
Так называются два логических контейнера, которые заполняются файлами с кодом
программы. Solution – самый большой, внешний контейнер. Project – внутренний контейнер.
Контейнер Solution может заполняться любым количеством контейнеров Project. Любой
контейнер Project должен быть частью какого-либо контейнера Solution. На конечном этапе
создания программы файлы контейнера Project компилируются в один исполняемый
модуль (.dll или .exe). Объединение различных проектов в общий контейнер Solution несет
чисто логическую нагрузку.
Создадим пустой контейнер типа Solution. Для этого
 Вызовем команду File.New.Project….
 В левой панели Project Types: появившегося окна New Project выберем узел Other Project
Types и в нем команду Visual Studio Solutions.
 На правой панели Templates: (шаблоны) выберем шаблон Blank Solution.
 Согласившись с именем Solution1 по умолчанию и с местом размещения каталога с
тем же именем (по умолчанию среда организует каталог Solution1 в каталоге My
Documents\Visual Studio 2005\Projects текущего профиля-пользователя),
щелкнем OK.
В каталоге Solution1 среда формирует два файла с тем же именем и расширениями sln
(solution) и suo (solution user options). В файл с расширением sln среда помещает информацию о
том, из каких проектов состоит Solution1 и где эти проекты размещены. В файле suo
сохраняется информация о пользовательских опциях, связанных с Solution1.
Добавим в Solution1 пустой проект. Для этого
 Вызовем окно View.Solution Explorer (если оно автоматически не появилось на экране).
 Щелкнем правой кнопкой над верхним узлом дерева Solution ‘Solution1’(0 projects).
 В появившемся контекстном меню вызовем команду Add.New Project….
 В правой панели Templates: появившегося окна Add New Project выберем Empty Project.
 В строке Name появится имя по умолчанию Project1. Оставим его без изменения.
Щелкнем OK.
В результате наших действий в каталоге Solution1 появится новый каталог Project1. Этот
каталог должен содержать внутренние каталоги bin и obj, созданные средой для
размещения в них скомпилированных модулей, и файл с именем проекта Project1 и
расширением csproj, который используется средой разработки и содержит информацию,
специфическую для проекта. В нашу задачу не входит полный анализ содержимого
вспомогательных файлов, поддерживаемых средой в ходе работы над проектом. Отметим
лишь, что не следует уничтожать или перемещать какие-либо из образованных средой
каталогов и файлов в процессе работы над проектом.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
29
При необходимости можно удалить проект из solution. Для этого в окне Solution Explorer
следует над именем проекта щелкнуть правой кнопкой мышки, вызвав контекстное меню,
а в нем команду Remove. Это удалит проект из solution, но оставит его файлы не тронутыми.
Проект
В древесной структуре окна Solution Explorer имеется узел Project1. Щелкнув по этому узлу
правой кнопкой, вызовем контекстное меню. Командой Add.New Item… откроем окно с
заголовком Add New Item - Project1. На панели Templates: выберем шаблон Code File. Согласимся
с именем CodeFile1.cs, предложенным средой по умолчанию, и нажмем кнопку Add.
В результате наших действий в древесной структуре окна Solution Explorer появится новый
узел – ссылка на файл CodeFile1.cs и откроется пустое окно с тем же именем. Расширение cs
(от CSharp) добавляется ко всем текстовым файлам, содержащим код, написанный на языке
C#. Если требуется удалить кодовый файл из проекта, следует в окне Solution Explorer
открыть контекстное меню над именем удаляемого файла и выбрать команду Delete.
Наберем в окне CodeFile1.cs простой код – описание пока пустого класса с именем _1.
class _1
{
}
Сохраним новую редакцию файла командой File.Save CodeFile1.cs.
Обратите внимание на то, что слева от текста появились вертикальные скобки с квадратом
и знаком минус в верхнем углу. Такие скобки ограничивают сворачиваемую область
текста (collapsible region). Использование такого рода областей существенно упрощает обзор
структуры кода и его логическое описание (блок-схема). Щелчком мышки по квадратикуметке поименованная область сворачивается, оставляя лишь первую строку текста. Знак
минус заменяется знаком плюс, новый щелчок по которому разворачивает текст.
IntelliSense
Обратите внимание на действия среды при наборе текста. Сразу после набора первого
символа слова class среда предлагает список возможных слов, подходящих по контексту.
Можно выбрать в этом списке требуемое слово class и, нажав Enter, внести его в текст
кода. Этот инструмент, именуемый IntelliSense, значительно облегчает набор кода.
Служебные слова (keywords)
Слово class является служебным словом (keyword) транслятора C#. Служебные слова
выделяются средой. Везде в тексте служебные слова будут выделены полужирным
шрифтом.
Идентификаторы
Имя класса _1 относится к словам-идентификаторам.
Идентификатор может быть любым сочетанием букв, цифр и символа подчеркивания.
Первым символом может быть либо буква, либо символ подчеркивания. Большие и малые
буквы различаются компилятором. Поэтому идентификаторы aB, Ab, ab и AB – это
четыре разных идентификатора.
Замечания.
1. Ключевое слово компилятора может служить идентификатором, если его
предварить символом @. Например, @class может использоваться в качестве
идентификатора.
2. Место любого допустимого символа в идентификаторе может занимать так
называемая escape-последовательность – последовательность некоторых
символов, следующих за символом \ (backslash). Записью одного символа в
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
30
идентификаторе может быть сочетание \u (от Unicode) и двухбайтового целого
числа в шестнадцатеричной системе счисления. Это число является двухбайтовым
унифицированным кодом символа. Например, выражение \u0041\u0042 означает
AB, а сочетание \u042f\u044f есть escape-последовательности двух букв
кириллицы Яя.
Использование унифицированного кода позволяет использовать буквы кириллицы в
написании идентификаторов и в явном виде.
1.3 Метод Main
Строка
class _1
является простейшим заголовком описания класса. За заголовком класса следует описание
членов класса (class members). Описание членов класса ограничено фигурными скобками.
Это блок членов класса.
Сейчас класс _1 пуст.
Попытка скомпилировать проект с пустым классом командой Build.Build Solution закончится
неудачей. Появится сообщение об ошибке. У проекта Project1 отсутствует некий
статический метод Main, служащий точкой входа проекта.
Добавим внутрь блока членов класса _1 (между фигурными скобками) описание метода
Main следующего вида
static void Main()
{
}
На этот раз компиляция пройдет успешно.
Первым (и пока единственным) членом класса _1 является метод класса с именем Main.
Метод Main является входной точкой программы. С него начинается выполнение
программы и на нем выполнение программы заканчивается. Хотя бы один класс
программы должен иметь метод Main. Измените в методе Main букву M на m. Опять
проведите компиляцию. Результат будет отрицательный. Важно, чтобы имя метода было
именно Main, а не main. Уберите слово static в описании метода Main. Проведите
компиляцию. Она вновь будет ошибочной.
Правила следующие
1. Метод Main должен иметь модификатор static.
2. Метод Main может ничего не возвращать (иметь тип void), или возвращать
значение типа int (целое). В последнем случае внутри метода должен быть
оператор возврата вида return expr, где expr – выражение целого типа.
3. Метод Main может не иметь параметров вовсе или иметь один параметр типа
массива строк string[].
Сделайте несколько редакций, набрав допустимые описания метода Main (и стирая
каждый раз предыдущую версию описания)
1 вариант кода
static int Main()
{
return 1;
}
2 вариант кода
static void Main(string[] args)
{
}
3 вариант кода
static int Main(string[] args)
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
31
{
return 0;
}
Значение типа int, которое возвращает метод Main в двух версиях, может использоваться
другой программой, выполняющей свой код после окончания работы программы с
методом Main.
В любом классе приложения можно описать метод с именем Main и сигнатурой, отличной
от четырех описанных вариантов. Это будет обычный метод, который нельзя будет
использовать в качестве входной точки приложения.
1.4 Модификатор static
Пусть имя класса _1. Пусть член класса имеет имя Member. Наличие модификатора static
перед описанием Member указывает на то, что член класса Member может быть использован
вне класса только в форме _1.Member. Говорят, что член класса типа static
принадлежит самому типу этого класса, но не его экземплярам. Вспомним, что каждому
классу отвечает область памяти с таблицей виртуальных методов. В этом смысле
идентификатор _1 в нашем случае является ссылкой на ту область памяти, где эта таблица
помещена. Статический метод класса это функция, неявным параметром которой является
ссылка на область VMT. Так же, обычный метод (метод экземпляра, нестатический метод)
является функцией, неявным параметром которой является ссылка на область, где
расположены поля экземпляра. Кроме VMT в области данных класса как типа могут
размещаться статические поля, и они будут управляться статическими методами.
1.5 Метод класса и его тип
Это член класса, являющийся функцией с операторами исполняемого кода. Будучи
функцией, метод класса, может возвращать значение любого типа и иметь любое число
параметров любого типа. Если метод не возвращает значение какого-либо типа, то его тип
указывается ключевым словом void.
Параметры метода
За указанием типа метода и его имени следует список параметров метода в круглых
скобках. Даже если метод не имеет параметров, круглые скобки все-таки пишутся.
Каждый параметр должен быть обозначен идентификатором, перед которым следует
указать тип параметра. Так одна из версий метода Main имеет один параметр с именем
args типа string[] - массив строк.
1.6 Работающий проект
Оставим первоначальную редакцию метода
static void Main()
{
}
Активизируем проект командой Debug.Start Debugging. В результате получим мелькнувшее
на мгновение черное окно и возврат в среду.
Ничего удивительного. Метод Main пуст. Чтобы зафиксировать черное окно добавим
внутрь блока метода Main (между фигурными скобками) строку
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
32
System.Console.ReadLine();
и вновь стартуем проект.
Теперь черное окно осталось на экране. Войдя в это окно и нажав Enter, завершим проект.
Замечание.
В некоторых версиях среда может предоставить свое черное окно Console. Следует войти в
окно Console и нажать Enter или завершить проект командой Debug.Stop Debugging.
Еще об IntelliSense
Обратите внимание на действия инструмента IntelliSense при наборе последней строки.
 Сразу же после набора первой буквы S IntelliSense показывает список возможных
имен, начинающихся с буквы S. В списке слева от каждого имени дается значок.
Имени System отвечает значок из фигурных скобок вида {}. Так помечаются
пространства имен.
 После выделения в списке какого-либо элемента (мышкой, либо клавишами
курсора) появляется окно с кратким описанием выбранного элемента.
 После появления точки в выражении System. список IntelliSense содержит все
пространства имен, вложенные в System, и все имеющиеся в System классы.
 Далее, после набора класса Console, список IntelliSense содержит все статические
члены класса Console.
У IntelliSense есть «память». Если начать писать новый оператор с буквы S (поставив точку
с запятой после System.Console.ReadLine();), то IntelliSense сразу выделит элемент
System, который недавно использовался.
1.7 Перегруженные методы (overloaded)
Изменим код так, чтобы получить какой-либо текст в черном окне. Для этого обратимся к
другому методу класса System.Console. А именно, добавим перед уже имеющейся
строкой System.Console.ReadLine(); внутри метода Main строку вида
System.Console.WriteLine("Hello C#!");
Обратите внимание, что после набора открывающей скобки метода WriteLine IntelliSense
дает информацию о том, что метод с именем WriteLine имеет множество реализаций.
Возможные реализации отличаются количеством и типом параметров метода
(сигнатурой). С версиями метода WriteLine можно познакомиться, листая список
клавишами курсора или мышкой. Методы, имеющие одно имя и описанные в форме
нескольких версий, отличающихся сигнатурой, называются перегруженными (overloaded).
В списке есть реализация метода WriteLine с параметром типа string, которой мы и
воспользовались.
Активизируем проект и убедимся, что в черном окне появилась требуемая строка.
1.8 Константы типа string
Была выбрана реализация метода WriteLine с параметром типа string. В качестве
фактического параметра подставлена постоянная типа string "Hello C#!". Заметим, что
любая постоянная типа string заключается в двойные кавычки. Внутри кавычек можно
писать как любые символы, так и их escape-последовательности. Например, вместо
"Hello C#!" можно набрать строку "\u0048ello C#!”. Программа выведет на экран ту
же строку. Проверьте.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
33
Внутрь строки можно вставлять escape-последовательности другого формата. Например,
\n означает перенос строки. Проверьте результат выполнения программы, набрав строку
"Hello\nC#!". Другой символ \t означает табуляцию. В общем случае при формировании
escape-последовательности за символом \ (backslash) могут стоять только определенные
символы. Среди них " (двойные кавычки), \(backslash), a (звуковой сигнал). Полный
список можно найти в справочной системе.
Строка может предваряться символом @. В этом случае все символы, находящиеся внутри
кавычек, будут восприниматься дословно (verbatim). Проверьте вывод текста
@”Hello\nC#!”. Такой способ записи строки часто используется для маршрутных строк
типа @”C:\Мои документы\Visual Studio 2005\”, где в качестве содержательного
символа строки используется служебный символ escape-последовательности \ (backslash).
Если внутри дословно записываемой строки необходимо поместить двойные кавычки, их
надо предварить двойными кавычками. Например, @”””Hello”” – said C#”.
1.9 Директива using
В теле метода Main дважды использовался класс из пространства имен System. Если
указание на пространство имен System опустить, то компилятор не сможет найти класс
System.Console и выдаст ошибку. Попробуйте сделать это.
Однако использование длинного расширенного имени System.Console можно избежать,
введя в начале файла (перед заголовком класса) строку вида
using System;
В этом случае можно заменить расширенное имя класса System.Console более коротким
Console. Текст файла CodeFile1 т.о. будет иметь вид
using System;
class _1
{
static void Main()
{
Console.WriteLine("Hello C#!");
Console.ReadLine();
}
}
Строка using System; указывает на то, что проект будет использовать стандартные
классы из библиотеки System. Директива using в начале компилируемого модуля
используется для сокращенной записи имен в тексте модуля.
Существует еще один аспект использования директивы using. Вслед за строкой
using System;
или, даже, вместо нее наберите строку
using C=System.Console;
После этого везде в тексте вместо идентификатора Console поставьте символ C. Так что
новая версия кода будет иметь вид
using C=System.Console;
class _1
{
static void Main()
{
C.WriteLine("Hello C#!");
C.ReadLine();
}
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
34
Вновь активизируйте программу командой Debug.Start Debugging. Результат будет прежним –
программа работает. Таким образом, директиву using можно использовать для
объявления псевдонимов (alias) классов.
Замечание.
После любого редактирования текста файла справа от его имени на закладке страницы
появляется знак * (asterisk). Если новую редакцию сохранить командой File.Save CodeFile1.cs,
asterisk исчезает. При больших объемах редактирования регулярно сохраняйте новую
редакцию файла. Тогда при сбое в работе компьютера редакция не будет утеряна. Заметим
также, что при компиляции среда автоматически сохраняет последние редакции файлов,
участвующих в компиляции.
1.10 Параметры по значению и по ссылке
Параметры по значению
Добавьте внутрь блока членов класса _1 (между фигурными скобками, но вне метода
Main) описание метода UseValue с одним параметром целого типа
static void UseValue(int i)
{
i = i + 1;
// Изменение значения параметра i
C.WriteLine(i);// Распечатка значения параметра i
}
Внутри метода Main поместите операторы
int j = 0;
// описание и инициализация локальной переменной j
UseValue(j);
// вызов метода UseValue с фактическим параметром j
C.WriteLine(j);
// распечатка значения j
Дайте команду Debug.Start Debugging и посмотрите результат. В окне должны быть 1 и 0.
Значение j, переданное методу UseValue в качестве фактического параметра, было
помещено в стек метода UseValue в качестве параметра-значения i. Значение копии i
было изменено и распечатано внутри метода UseValue. При этом значение оригинала j
осталось без изменений (ноль).
Параметры по ссылке
В описании параметра метода могут присутствовать служебные слова (keywords) ref и out.
Служебное слово ref (от reference - ссылка) означает, что фактический параметр,
используемый методом при его вызове, рассматривается как ссылка на значение,
хранящееся вне метода. Изменяя значение фактического параметра, метод тем самым
меняет значение переменной, на которую этот параметр ссылается.
Для примера опишите еще один метод класса _1 (до или после метода UseValue, но вне
метода Main)
static void UseRef(ref int i)
{
i = i + 1;
C.WriteLine(i); // Распечатается 1
}
Добавьте внутрь метода Main операторы
UseRef(ref j);
C.WriteLine(j);// Также распечатается 1
Результат должен отличаться – две единицы. Теперь значения j и i будут совпадать,
поскольку это значения одного и того же объекта j, на который ссылается параметр i
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
35
метода UseRef при его вызове. Копирования значения j в стек метода UseRef не
происходит, а параметр i является фактически псевдонимом того же объекта, что и j.
Обратите внимание, что служебное слово ref должно использоваться как при описании
метода, так и при его вызове. Заметим, что в Delphi слову ref отвечает символ var.
Другое служебное слово out так же, как и ref, определяет параметр по ссылке. Отличие
out от ref в том, что фактический параметр, описанный как out, может вовсе не иметь
никакого значения до вызова метода, а описанный как ref должен иметь какое-либо
значение перед вызовом метода. В предыдущем примере фактический параметр j имеет
значение ноль в момент вызова метода UseRef.
Наберите новый метод UseOut в классе _1
static void UseOut(out int i)
{
i = 0;
C.WriteLine(i);
}
Наберите в методе Main код
int k;
UseOut(out k);
C.WriteLine(k);
Посмотрите результат; распечатаются два нуля – значения i и k. Они совпадают, так как
переменная i является псевдонимом того же объекта, что и k, как и в случае параметра
типа ref.
Перед вызовом метода UseOut значение объекта k не определено. Если попытаться
распечатать значение k перед вызовом метода UseOut, возникнет ошибка компиляции
(попробуйте). Сравните с предыдущими примерами.
1.11 Два метода Main
Добавим новый файл для записи кода к проекту Project1. Для этого, как и раньше:
 В окне Solution Explorer щелкнем правой кнопкой узел проекта Project1, вызвав
контекстное меню.
 Командой Add.New Item… откроем окно с заголовком Add New Item - Project1.
 На панели Templates: выберем шаблон Code File.
 Согласимся с именем CodeFile2.cs, предложенным средой по умолчанию, и нажмем
кнопку Add.
В новом файле наберем код
class _2
{
static void Main(string[] args)
{
}
}
Сохраним новую редакцию файла CodeFile2.cs.
Теперь проект Project1 состоит из двух компилируемых
модулей. Попытка компиляции
новой версии проекта приведет к ошибке. Среда укажет на существование двух методов
Main в двух классах без указания на то, какой из этих методов является точкой входа в
программу.
Чтобы уточнить какой, можно
 убрать один из двух методов Main,
 либо указать компилятору, какой из двух методов Main следует использовать.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
36
Сделаем последнюю операцию. Войдем в окно со свойствами проекта командой
Project.Project1 Properties…. На закладке Application найдем элемент Startup Object. Откроем
выпадающий список значений этого элемента (по умолчанию значение не установлено).
Выберем из двух предложенных значений класс _2. Скомпилируем проект еще раз.
Компиляция должна пройти успешно.
1.12 Командная строка (Command Line)
Командной строкой (command line) является последовательность символов, которая
набирается при активизации приложения командой run (Выполнить...) меню кнопки
«Пуск». Командная строка включает в себя полное имя файла приложения (в нашем
случае это Project1.exe с указанием маршрута из корневого каталога) и аргументы,
состоящие из любых строк, разделенных пробелами. Параметр args типа string[]
(массив строк) метода Main класса _2 содержит эти аргументы.
Аргументы командной строки можно задавать непосредственно в среде. Для этого
выполните команду Project.Project1 Properties…. На закладке Debug открывшегося окна
найдите элемент Command Line Arguments. Значение этого элемента - текст аргументов
командной строки и, тем самым, фактическое значение параметра args метода Main.
Наберите, к примеру, 1 2 3 (с пробелами!). В метод Main класса _2 вставьте текст так, что
метод Main примет вид
static void Main(string[] args)
{
for (int i = 0; i < args.Length; i++)
System.Console.Write(args[i] + " ");
System.Console.ReadLine();
}
Результат в черном окне 1 2 3
Обратите внимание, что в результирующей строке после символов 1 2 3 символ
положения каретки | не перенесен на новую строку. Это объясняется тем, что для вывода
на экран результата (элементы массива args[i]) используется метод Write класса
Console, а не WriteLine.
1.13 Операторы цикла
Оператор цикла for
Оператор System.Console.Write(args[i]+" ") вызывается на каждом шаге цикла
for (int i=0;i<args.Length;i++).
Счетчиком цикла (counter) является локальная переменная i типа int, которая меняется в
пределах от нуля до значения args.Length-1. Свойство Length массива args возвращает
длину массива – число элементов массива. Оператор ++ увеличивает значение i на
единицу на каждом шаге цикла. Оператор i++ равносилен оператору i=i+1 или i+=1.
Структура оператора цикла for выглядит следующим образом
for (<инициализаторы>;<логическое выражение, проверяющее условие конца цикла>;<итераторы>)
<выполняемый оператор>.
Любая из трех секций оператора for, стоящих в скобках, может быть пустой. Например,
оператор for (;;) образует бесконечный цикл.
При действии оператора for
 в начале работает секция инициализаторов,
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
37

затем вычисляется логическое выражение, определяющее условие окончания
цикла.
 Если условие выполняется, то управление передается выполняемому оператору,
 затем выполняются итераторы, и вновь проверяется условие.
 Если условие не выполняется, то управление передается внешнему оператору,
следующему за оператором цикла.
Таким образом, оператор, стоящий в цикле for, может ни разу не выполниться. Замените
в операторе цикла условие i<args.Length на i<0. Проверьте результат.
Инициализаторы и итераторы могут иметь вид списков.
Например, инициализаторы int i=0,j=100 и итераторы i++,--j.
Испытайте разные сочетания элементов оператора for. Если программа выйдет на ошибку
в ходе выполнения, вернитесь к редактированию командой Debug.Stop Debugging.
Операторы цикла while и do…while
В языке C# есть еще два оператора цикла. Это
 оператор «с предусловием» while <условие> <выполняемый оператор>
 и оператор «с постусловием» do <выполняемый оператор> while <условие>.
Заменим цикл с оператором for циклом с оператором while так, что тело метода Main
будет иметь вид
int i = 0;
while (i < args.Length) System.Console.Write(args[i++] + " ");
System.Console.ReadLine();
Результат выполнения программы будет прежним.
Операторы цикла for и while в начале проверяют условие, а затем выполняют
требуемые действия.
Оператор цикла «с постусловием» do…while в начале обязательно выполняет действие, а
затем проверяет условие выхода из цикла.
Испытайте оператор do…while в нашем коде
int i = 0;
do
System.Console.Write(args[i++] + " ");
while (i < args.Length);
System.Console.ReadLine();
1.14 Операторы инкремент ++ и декремент -Отметим использование в наших последних циклах оператора инкремента i++ для
получения индекса элемента массива.
 Величина i++ имеет значение i до изменения. Затем действует оператор ++,
увеличивающий значение i на 1, как если бы стоял оператор i=i+1.
 Выражение ++i в начале увеличивает значение i на 1, а затем возвращает
результат.
Замена i++ на ++i в приведенном коде существенно изменит его логику, что приведет к
ошибке времени выполнения типа Index was outside the bounds of the array (индекс вышел за границы
массива). Проверьте.
Заметим, что в операторе цикла for замена i++ на ++i вполне безвредна. Аналогично
операторам инкремента ++ действует оператор декремента --, вычитающий единицу.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
38
Вопросы для самоконтроля
1. Чем отличаются исполняемые модули .exe и .dll?
2. Компилируемый модуль vs исполняемый модуль. Сборка.
3. Пространство имен vs компилируемый модуль.
4. Типы классов в C#.
5. Структура классов в C#.
6. Solution vs project.
7. Идентификаторы в C#.
8. Метод Main.
9. Модификатор static.
10. Метод класса.
11. Перегруженный (overloaded) метод.
12. Константы типа string и escape-последовательности.
13. Директива using.
14. Параметры по значению vs параметры по ссылке.
15. Командная строка.
16. Оператор цикла for.
17. Операторы цикла while и do…while.
18. Операторы инкремент ++ и декремент --.
Занятие 2. Библиотека классов
Добавим в контейнер Solution1 новый проект.
Но прежде заменим имя готового проекта Project1 именем MainExe. Для этого в окне Solution
Explorer щелкнем правой кнопкой над узлом Project1 и в контекстном меню выберем
команду Rename. Введем новое имя MainExe. Сохраним все файлы командой File.Save All.
Замечание.
Изменения имен проектов следует проводить командами среды, а не файловой системы.
Изменим также имя сборки (Assembly). Командой Project.MainExe Properties…откроем окно со
свойствами проекта. Найдем элемент Assembly Name и наберем новое имя MainExe. Вновь
сохраним все файлы проекта командой File.Save All.
Теперь откроем новый проект. Для этого в окне Solution Explorer щелкнем правой кнопкой
над верхним узлом дерева Solution‘Solution1’(1 project). В появившемся контекстном меню
вызовем команду Add.NewProject…. В появившемся окне на панели Templates: выберем Empty
Project и, набрав Lib в качестве значения Name, щелкнем кнопку Add. В дереве окна Solution
Explorer должен появиться узел Lib. Сохраним все файлы.
Теперь добавим к проекту Lib файл кода. Щелкнув правой кнопкой над узлом Lib, вызовем
командой Add.New Item… окно с шаблонами. Выберем шаблон Code File, а в строке Name
исправим предложенное средой имя файла. Пусть это будет LibCode.cs. Нажмем Add и
сохраним все файлы.
2.1 Создание библиотеки классов
Нашей первой целью является создание проекта, результатом которого будет библиотека
– исполняемый модуль с расширением .dll.
Для этого вызовем командой Project.Lib Properties… окно со свойствами проекта Lib. Найдем
элемент Output Type, в котором по умолчанию должно стоять значение Console Application.
Открыв список допустимых значений, выберем Class Library. Теперь результирующим
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
39
исполняемым модулем проекта Lib будет не exe-файл (как было по умолчанию), а файл с
расширением .dll.
В пустом окне файла Lib.cs наберем текст
class _1
{
void Method()
{
System.Console.WriteLine("Lib");
}
}
Имя Method является, конечно, произвольным идентификатором.
Скомпилируем Solution1 командой Build.Build Solution. В каталоге Solution1 должен
образоваться каталог Lib, а внутри него, в каталогах obj и bin, после компиляции должен
появиться файл Lib.dll.
Попытаемся использовать класс новой библиотеки в проекте MainExe. Для этого
необходимо, чтобы, как минимум, сама библиотека Lib.dll была доступна проекту MainExe.
С этой целью откроем окно Solution Explorer и щелкнем правой кнопкой над узлом References
(ссылки) проекта MainExe. В появившемся меню выберем команду Add Reference…. Откроется
окно с тем же именем. В нем на странице с закладкой Projects должна быть строка с именем
Lib. Надо выделить эту строку и нажать OK. Убедитесь, что после этого узел References
проекта MainExe в окне Solution Explorer содержит ссылку на библиотеку Lib.
Проведите рекомпиляцию Solution1 командой Build.Rebuild Solution. В результате копия файла
Lib.dll должна оказаться среди файлов каталога Project1\bin\Debug проекта MainExe.
Предоставив проекту MainExe доступ к библиотеке классов Lib, можно попытаться
использовать библиотечный класс _1 в классах MainExe.
Войдем в окно файла CodeFile2 проекта MainExe. Уберем все операторы в методе Main за
исключением последнего оператора. Так что код файла CodeFile2 примет вид
class _2
{
static void Main(string[] args)
{
System.Console.ReadLine();
}
}
Попытаемся первым оператором в методе Main вызвать ссылку на класс _1 из библиотеки
Lib.
Первая трудность, с которой мы столкнемся, это явный конфликт имен. В модуле CodeFile1
проекта MainExe уже существует класс с тем же именем _1, что и библиотечный. Из-за него
не виден библиотечный класс _1. Чтобы разрешить этот конфликт можно
1. переименовать один из классов _1 – либо в библиотеке Lib, либо в проекте MainExe.
2. расширить имя одного или обоих классов.
Первый способ не самый лучший. Обычно имена классов несут смысловую нагрузку,
которую программист не захочет терять.
2.2 Конфликт имен и пространство имен
Воспользуемся вторым способом.
Конфликт имен наступил из-за того, что имена классов как библиотеки Lib, так и проекта
MainExe принадлежат одному и тому же пространству имен. Это корневое пространство
имен с псевдонимом global. Изолировать пространства имен можно, поместив код
библиотечного класса в файле LibCode в отдельное пространство имен с именем, например,
LibClasses так, что новый код будет иметь вид
namespace LibClasses
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
40
{
class _1
{
void Method()
{
System.Console.WriteLine("Lib");
}
}
}
Теперь общее имя LibClasses изолирует имена классов, описанных внутри блока {}
пространства имен LibClasses. Библиотечный класс _1 получает расширенное имя
LibClasses._1, и это позволяет разрешить конфликт имен.
Заметим, что пространство имен System принадлежит корневому пространству имен,
описываемому псевдонимом в форме служебного слова global. Убедитесь в этом,
расширив в предыдущем коде имя System именем global::System. Обратите внимание на
оператор доступа «двойное двоеточие» ::, заменяющий в этом специальном случае
оператор «точка» . (dot).
После редакции сохраним все файлы, проведем рекомпиляцию Solution1 командой
Build.Rebuild Solution и вновь перейдем в окно CodeFile2 проекта MainExe.
Сделаем еще одну попытку сослаться на библиотечный класс, применив на этот раз
расширенное имя LibClasses._1.
Начнем набирать это имя сразу после открывающей скобки { метода Main класса _2.
После набора первой буквы L IntelliSense должен сразу подсказать полное имя LibClasses.
Но после полного набора LibClasses. (с точкой) ожидаемая подсказка с именем класса _1
не появится! Разберемся в этом.
2.3 Модификаторы доступа к классам
Строка
class _1
является простейшим заголовком описания класса.
Одни классы могут быть доступны только коду сборки (Assembly), частью которой
является компилируемый модуль. Тогда в заголовке класса указывается модификатор
доступа internal, либо не указывается никакой модификатор доступа. В этом последнем
случае, как говорят, «по умолчанию», модификатором доступа также будет internal.
Класс с неограниченной областью доступа следует предварять модификатором public.
Класс _1 в нашей библиотеке Lib не имеет модификатора доступа и, следовательно, имеет
модификатор internal и может быть доступен только коду сборки Lib.
Замечание.
Если сборка состоит из нескольких компилируемых модулей, то класс с модификатором
internal доступен коду в любом из модулей. Так класс _1 проекта MainExe, описанный в
компилируемом модуле CodeFile1 с модификатором internal, доступен коду в модуле
CodeFile2, так как компилируемый модуль CodeFile2 принадлежит тому же проекту MainExe
(т.е., является частью одной сборки), что и модуль CodeFile1.
Изменим модификатор доступа к классу LibClasses._1, добавив к его описанию в модуле
LibCode модификатор public. Новый код файла LibCode теперь
namespace LibClasses
{
public class _1
{
void Method()
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
41
{
System.Console.WriteLine("Lib");
}
}
}
2.4 Экземпляр класса (instance)
Возвратимся в модуль CodeFile2 проекта MainExe, очистим метод Main класса _2 от следов
предыдущей неудачной редакции и вновь скомпилируем Solution1 командой Build.Rebuild
Solution.
Новая попытка набора расширенного имени класса LibClasses._1 должна завершиться
успешно! Наберем новую редакцию модуля CodeFile2 в виде
class _2
{
static void Main(string[] args)
{
LibClasses._1 inst_1;
System.Console.ReadLine();
}
}
Оператор
LibClasses._1 inst_1;
есть оператор описания ссылки на экземпляр (instance) класса LibClasses._1 с именем
inst_1. Словосочетание «ссылка на экземпляр» часто будем замещать кратким термином
«экземпляр».
У класса LibClasses._1 есть только один метод Method(). Он не имеет модификатора
static, поэтому может вызываться только экземплярами класса. Попытаемся вызвать
этот метод с помощью экземпляра inst_1. Для этого в следующей строке после описания
экземпляра наберем имя inst_1. (с точкой). В списке IntelliSense нет имени Method!
2.5 Модификаторы доступа к членам класса
Дело в том, что у членов класса, в частности, у метода Method() есть также модификаторы
доступа. Их разнообразие больше, чем у классов, описанных в пространстве имен. По
умолчанию модификатором члена класса является private. Член класса, описанный с
модификатором private, доступен лишь другим членам этого же класса и больше
никому. При необходимости можно использовать модификатор internal. Тогда доступ
члена класса будет распространяться на всю сборку, но не далее. Если модификатор
доступа члена класса public и при этом сам класс имеет тот же уровень доступа, то член
класса доступен всем. Это именно то, что нам нужно.
Сделаем новую редакцию библиотечного файла в виде
namespace LibClasses
{
public class _1
{
public void Method()
{
System.Console.WriteLine("Lib");
}
}
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
42
и вновь рекомпилируем Solution1 командой Build.Rebuild Solution.
Вернемся к файлу CodeFile2. Новая попытка набора inst_1. в строке после описания
экземпляра должна завершиться успешно. Метод Method должен появиться в списке
IntelliSense! Но…
2.6 Создание экземпляра
Добавление строки
inst_1.Method();
и новая компиляция приводят к ошибке компиляции «использование не присвоенной
(unassigned) переменной inst_1».
Дело в том, что, описав ссылку на экземпляр inst_1 класса LibClasses._1, мы не создали
сам экземпляр в памяти компьютера. Переменная inst_1 содержит пока что лишь ссылку
в «никуда» (null). Для создания экземпляра следует добавить оператор вида
inst_1 = new LibClasses._1();
или заменить имеющееся описание экземпляра
LibClasses._1 inst_1;
другим
LibClasses._1 inst_1 = new LibClasses._1();
Так что весь код файла CodeFile2 из проекта MainExe примет вид
class _2
{
static void Main(string[] args)
{
LibClasses._1 inst_1 = new LibClasses._1();
inst_1.Method();
System.Console.ReadLine();
}
}
Скомпилируем Solution1 и дадим команду Debug.Start Debugging. В черном окне должна
появиться надпись Lib.
Метод LibClasses._1(), который мы использовали в последней редакции, называется
конструктором экземпляра класса. Конструктор экземпляра в языке C# имеет то же
имя, что сам класс, и по умолчанию не имеет параметров.
Оператор new
выделяет память для хранения экземпляра класса и возвращает ссылку на созданный
экземпляр. Память выделяется в локальной куче (heap) и должна быть освобождена. Кто и
когда это делает?
Автоматическая сборка мусора (garbage collection)
Обычно, при динамическом использовании памяти от программиста требовалось следить
за тем, чтобы занятая память вовремя освобождалась. В данном случае следовало бы
освободить память от объекта inst_1 перед окончанием метода Main. Но при
программировании в среде .NET освобождение памяти от объектов происходит
автоматически!
2.7 Поля класса (field)
До сих пор единственные члены класса, которые мы использовали, были методы –
статические методы Main и метод экземпляра Method().
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
43
Другими членами класса являются поля. Полями класса являются экземпляры объектов,
описанных внутри класса, но вне его методов. Например, экземпляр inst_1 класса
LibClasses._1 описан внутри класса _2, но внутри его метода Main. Поэтому inst_1 не
является полем класса _2, а лишь локальной переменной метода Main.
Можно перенести строку LibClasses._1 inst_1 = new LibClasses._1();
с описанием экземпляра inst_1 наружу метода Main. Так, что новая редакция кода файла
CodeFile2 примет вид
class _2
{
LibClasses._1 inst_1 = new LibClasses._1();
static void Main(string[] args)
{
inst_1.Method();
System.Console.ReadLine();
}
}
В этом случае inst_1 становится полем класса _2.
Но…, новая редакция кода содержит ошибку, которую сразу же обнаружит компилятор.
2.8 static vs. нестатический
Дело в том, что поле inst_1 является нестатическим полем класса _2. Оно примет какоелибо осмысленное значение лишь после создания экземпляра самого класса _2. Метод
Main является статическим и его вызов не требует наличия экземпляра класса _2.
Использование внутри статического метода нестатических членов класса (полей или
методов) недопустимо и поэтому вызывает ошибку при компиляции.
В данном коде достаточно объявить поле inst_1 статическим, добавив в его описание
модификатор static, чтобы сделать код осмысленным.
Новая редакция кода
class _2
{
static LibClasses._1 inst_1 = new LibClasses._1();
static void Main(string[] args)
{
inst_1.Method();
System.Console.ReadLine();
}
}
должна быть скомпилирована без ошибок.
2.9 Описание конструктора
Программист может описать свой собственный конструктор экземпляра класса, определив
его как функцию с параметрами.
Войдем в окно библиотечного файла LibCode. Добавим поле s класса string в класс _1 и
конструктор с параметром типа string так, что новая редакция кода файла LibCode примет
вид
namespace LibClasses
{
public class _1
{
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
44
string s; // описание нестатического поля s
//описание конструктора с одним параметром
public _1(string s)
{
this.s = s;
}
public void Method()
{
System.Console.WriteLine(s);
}
}
}
Двойной знак // (double slash) ограничивает область комментария от области кода в конце
строки. Комментарий никак не воспринимается компилятором и пишется исключительно
для пояснений кода.
Отметим правила описания конструктора экземпляра.
 Имя конструктора должно совпадать с именем класса.
 Конструктор не должен возвращать какой-либо тип, даже void.
 Обычно предполагается создавать экземпляры класса вне самого класса. Поэтому
следует в описание конструктора добавить модификатор доступа, отличный от
private (вспомним, что для членов класса модификатор private принимается по
умолчанию).
Описание поля s не имеет модификатора доступа и, следовательно, имеет доступ private.
Поле s доступно лишь методам класса – в частности конструктору класса и методу
Method(), где и используется. Поле s является нестатическим, поэтому имеет смысл лишь
как поле экземпляра, но не как поле класса.
this
В теле конструктора поле s записано как this.s, чтобы отличить от параметра
конструктора, так же обозначенного s. Служебное слово this является ссылкой на
текущий экземпляр (в Delphi экземпляр обозначается self). Например, при создании
экземпляра inst_1, которым мы сейчас займемся, в this будет находиться значение
inst_1.
Компиляция Solution1 при использовании новой редакции кода библиотечного класса
LibClasses._1 приведет к ошибке в коде класса _2 проекта MainExe.
Дело в том, что, описав конструктор класса LibClasses._1 с параметром, мы
автоматически отказались от существования конструктора без параметров,
предлагаемого компилятором по умолчанию. Теперь в файле CodeFile2 в строке с
описанием экземпляра inst_1 класса LibClasses._1 следует в качестве параметра
конструктора подставить какую-нибудь строку. Сделаем это и активизируем новую
редакцию кода проекта MainExe и библиотеки Lib.
class _2
{
static LibClasses._1 inst_1 = new LibClasses._1("Anything as string");
static void Main(string[] args)
{
inst_1.Method();
System.Console.ReadLine();
}
}
Результат должен быть ожидаемым. Строка
Anything as string
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
45
в черном окне.
Перегруженный конструктор (overloaded)
Можно описать произвольное число конструкторов одного класса с разным числом и
разными типами параметров. Такое описание называется перегрузкой (overloading).
Например, в библиотечный класс LibClasses._1 можно добавить конструктор без
параметров
public _1()
{
}
и воспользоваться этим конструктором (убрав из предыдущего кода ссылку на строку
"Anything as string") при создании экземпляра inst_1 в классе _2 проекта MainExe.
Проверьте работу новой версии кода. Результатом должно быть пустое черное окно.
При создании экземпляра конструктор обнуляет все поля. Поэтому в поле s, которое
Method() должен вывести на экран, ничего нет (пустая строка).
2.10 Отладка проекта (debugging)
Будем считать, что код файла CodeFile2 имеет тот вид, в котором мы его оставили
class _2
{
static LibClasses._1 inst_1 = new LibClasses._1();
static void Main(string[] args)
{
inst_1.Method();
System.Console.ReadLine();
}
}
Пошаговое выполнение кода (Step Into, Step Over, Step Out, Run To
Cursor)
Выберем команду Debug.Step Into. Указатель отладчика попадает на открывающую скобку
блока { метода Main класса _2, а затем, после повторной команды Debug.Step Into, на первый
оператор этого метода
inst_1.Method();
который будет выделенным цветом.
Подведем курсор к элементу inst_1. Среда даст комментарий вида
+|inst_1|{LibClasses._1}
Это означает, что inst_1 есть экземпляр класса LibClasses._1. Поставьте курсор на знак
+ и получите дополнительный комментарий. Это комментарий о полях экземпляра inst_1
s|null
так как у класса LibClasses._1 есть только одно нестатическое поле s и к моменту
комментария его значение null.
Новый вызов команды Debug.Step Into переведет указатель отладчика внутрь метода
LibClasses._1.Method(). Имя команды Step Into говорит о том, что в процессе пошаговой
отладки проводится вход в каждый из встреченных методов.
Другая команда отладчика Debug.Step Over дает методу выполниться и передает управление
оператору после выхода из метода. Чтобы проверить ее действие можно, находясь внутри
метода Method(), вызвать в начале команду Debug.Step Out. Оказавшись вновь снаружи,
дайте команду Debug.Step Over. Указатель отладчика должен передвинуться на строку
System.Console.ReadLine();
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
46
Если в любой момент пошаговой отладки выбрать команду Debug.Continue, то выполнение
кода продолжится в обычном, не пошаговом режиме. Прервать пошаговую отладку или
выполнение кода можно командой Debug.Stop Debugging. Возобновить выполнение кода с
начала – командой Debug.Restart.
Ранее указывалось, что до входа в метод Main класса _2 должен быть выполнен оператор
static LibClasses._1 inst_1 = new LibClasses._1();
Но отладчик на нем не задерживается.
Чтобы все-таки войти в этот оператор, можно использовать еще одну команду отладчика
Run To Cursor (из контекстного меню), установив предварительно каретку в область текста
static LibClasses._1 inst_1 = new LibClasses._1();
Помещая указатель мышки на текст inst_1, можно узнать,
данной точке значение null.
что экземпляр inst_1 имеет в
Точки прерывания (breakpoints) и другие возможности отладчика
Процесс отладки можно запрограммировать, используя так называемые точки прерывания
(breakpoints). Любая строка кода может быть помечена как точка прерывания. Для этого
достаточно щелкнуть на левом поле строки. Строка будет выделена цветом и слева
появится кружок – символ точки прерывания. Теперь, после запуска кода отладчик
остановит выполнение в этой точке. Число точек прерывания может быть любым.
Попробуйте установить несколько точек прерывания в имеющемся проекте и проверьте
их действие. Убирается точка прерывания командой Delete Breakpoint из контекстного меню
на символе точки или «кликом» по самой точке.
Преимущество точек прерывания по сравнению с командой Run To Cursor в том, что
 можно программировать условия, при которых в данной точке следует
приостановить выполнение кода;
 можно указать, через какое число проходов через данную точку выполнение
должно прерваться;
 можно активизировать конкретные действия (например, появление сообщения) при
достижении точки прерывания.
В процессе отладки из меню Debug.Windows можно открывать окна, расширяющие
возможности отладки. Например,
 можно следить за текущими значениями переменных кода и при необходимости
редактировать значения локальных переменных (окно Locals).
 в окнах Watch дополнительно можно формулировать простые выражения из
локальных переменных, оценивая их результат.
 если возникает необходимость формулировать дополнительные участки кода с
требованием непосредственного выполнения, следует открыть Immediate Window.
Вопросы для самоконтроля
1.
2.
3.
4.
5.
6.
7.
Модификаторы доступа к классам и членам класса.
Описание экземпляра класса и его создание.
Оператор new. Автоматическая сборка мусора.
Поля класса. Статический vs нестатический.
Правила описания конструктора экземпляра. Перегруженные конструкторы.
Что такое this?
Отладка проекта (команды Step Into, Step Over, Step Out, Run To Cursor,
точки прерывания).
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
47
Занятие 3. Классы и объекты в C#
Эффективное программирование, особенно в C#, оказывается затруднительным без
понимания этих терминов.
Термин объект в самом общем понимании есть область памяти со своим адресом,
конкретным объемом и форматом.
Есть два типа данных, с которыми оперирует процессор – адрес объекта и содержимое
объекта. Адрес в зависимости от процессора является 32 или 64-битовым (4-ех или 8-ми
байтовым) целым числом. Адрес можно считать внутренним представлением ссылки на
объект (reference), а содержимое – значением (value) объекта. Содержимое объекта в
соответствии с его форматом состоит из его полей (field). Поля находятся в памяти,
начиная с адреса объекта в том объеме и формате, в котором они представлены в описании
типа объекта.
Тип класса является объектом в том смысле, что он имеет адрес и занимает память.
Полями объекта «тип класса» является таблица виртуальных методов и его статические
поля. Примерами объекта «тип класса» является LibClasses._1 в библиотеке Lib и
классы _1 и _2 в проекте MainExe.
3.1 Статический конструктор и статический класс
Статический конструктор инициализирует тип класса. Внутренне ссылка на тип класса
есть адрес той области памяти, где хранятся статические поля класса (включая VMT
типа).
Статический конструктор вызывается средой .NET
 либо при первом обращении к статическому полю,
 либо перед созданием экземпляра класса,
и никогда не вызывается явно. Больше одного раза статический конструктор не
вызывается.
Непосредственно перед вызовом статического конструктора статическим полям
присваиваются начальные значения, если последние заданы. Например, в классе _2
проекта MainExe есть статическое поле inst_1, которое в последней редакции
инициализируется конкретным значением.
static LibClasses._1 inst_1 = new LibClasses._1();
Это происходит непосредственно перед вызовом статического конструктора класса _2.
Чтобы убедиться в этом добавьте в блок с членами класса _2 описание статического
конструктора в виде
static _2()
{
}
После чего поставьте каретку в область текста строки
static LibClasses._1 inst_1 = new LibClasses._1();
и выберите команду Run To Cursor из контекстного меню. Далее двигайтесь
Debug.Step Into.
командой
Необходимости в описании статического конструктора обычно не возникает – работает
конструктор по умолчанию. У статического конструктора (его еще называют
конструктором класса)
 не должно быть параметров,
 он не должен возвращать какой-либо тип,
 его имя должно совпадать с именем класса,
 у статического конструктора должен быть модификатор static,
 не должно быть модификаторов доступа (типа public)
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
48
Статический класс
В языке C# можно описать статический класс, который содержит только статические
члены. Статический класс не может создавать экземпляры и имеет модификатор static в
описании.
3.2 Нестатический класс и его экземпляры
Нестатический класс может создавать экземпляры. Создает экземпляр класса оператор
new с последующим вызовом конструктора экземпляра. Оператор new размещает
экземпляр в памяти компьютера и придает ссылке на экземпляр осмысленное (не равное
null) значение. Конструктор экземпляра при этом помещает в специальное (не
описываемое программистом) поле экземпляра адрес VMT, т.е. адрес таблицы
виртуальных методов в объекте «тип класса», экземпляр которого создается. Строка кода
static LibClasses._1 inst_1 = new LibClasses._1();
является тому примером. Экземпляр inst_1 класса LibClasses._1
new с участием конструктора LibClasses._1().
создается оператором
Содержимым (значением) экземпляра класса являются нестатические поля.
Нестатические поля могут иметь инициализаторы. Например, поле s класса
LibClasses._1 может быть описано в форме
string s="a string";
Код выполняет инициализацию нестатических полей непосредственно перед вызовом
конструктора экземпляра. Проверьте, используя команды отладчика.
Неинициализированным нестатическим полям класса конструктор экземпляра
присваивает значения по умолчанию (default). Это 0, "", false или null в зависимости от
типа поля.
3.3 Ссылочный тип (reference type) и тип-значение (value type)
Все типы классов разделены на две категории в зависимости от того, какую информацию
содержат в себе экземпляры (переменные) данного типа
 ссылку
или
 значение.
Будем называть кратко классы первой категории «ссылочный тип» (reference type) , а второй
– «тип-значение» (value type).
Переменная экземпляра ссылочного типа содержит ссылку (адрес) области памяти, где
расположены нестатические поля класса. Содержимое ссылочного типа размещается в
динамической области памяти – «куче» (heap) по адресу, хранящемуся в ссылкепеременной. Наиболее распространенное использование термина «объект» относится
именно к экземпляру класса ссылочного типа.
Примерами классов ссылочного типа, которые нам уже встречались, являются
пользовательские классы, описываемые служебным словом class, и встроенный класс
string. Служебное слово string является псевдонимом библиотечного класса
System.String. К встроенным ссылочным типам относятся также interface – класс с
нереализованными методами, delegate – класс, содержащий ссылки на методы, и
встроенный класс object (псевдоним библиотечного класса System.Object).
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
49
Переменная экземпляра типа-значения размещается в стеке, либо непосредственно в
области кода программы (inline). Содержимым этой переменной являются сами
нестатические поля класса.
Существенным отличием двух типов классов является то, что при присвоении одной
переменной (экземпляра) класса ссылочного типа другой копируется только ссылка, но не
содержимое экземпляра. При той же операции присвоения для типов-значений копируется
все содержимое – все поля экземпляра. Другими словами присвоение одной переменной
типа-значения другой приводит к неявному созданию нового экземпляра
соответствующего класса и копированию в него полей исходного экземпляра.
Объекты классов типов-значений обычно требуют для хранения сравнительно небольшие
объемы памяти.
Это
 классы, описываемые служебным словом struct, - пользовательские структуры;
 встроенные представители типа struct:
целые типы
однобайтовые: целые со знаком sbyte и без знака byte - псевдонимы
классов System.SByte и System.Byte;
2-байтовые: символы char, целые со знаком short и без знака ushort псевдонимы классов System.Char, System.Int16 и System.UInt16;
4-байтовые: со знаком int и без знака uint - псевдонимы классов
System.Int32 и System.UInt32;
8-байтовые: со знаком long и без знака ulong - псевдонимы классов
System.Int64 и System.UInt64;
типы с плавающей запятой (floating-point type)
4-байтовый float – псевдоним класса System.Single;
8-байтовый double – псевдоним класса System.Double:
16-байтовый тип decimal (псевдоним класса System.Decimal), используемый в
финансовых расчетах;
логический тип bool – псевдоним класса System.Boolean;
 и тип enum – перечислимый тип (нумератор), элементы которого имеют в качестве
базовых классов любой целый тип за исключением char.
Экземпляры типов-значений можно перевести в экземпляр ссылочного типа (так
называемый boxing - упаковка) и вернуть назад в тип-значение (unboxing - распаковка).
Пусть, например, описан экземпляр типа-значения char ch=’a’. Упаковка состоит в
описании экземпляра ссылочного типа и размещении в нем экземпляра типа-значения
object och=ch. При этой операции в динамической памяти (куче, или heap) создается
копия символа ’a’ и ссылка на эту копию помещается в переменную och. Обратная
операция распаковки сводится к описанию экземпляра объекта ch1 типа char и
размещению в нем содержимого ссылки och с помощью оператора преобразования типа
(char)
char ch1=(char)och.
3.4 Указатели (pointers)
Отдельной категорией типов в C# являются указатели (pointer). Это адреса объектов типов-значений. Только
объекты типов-значений и только те, у которых нет полей ссылочного типа (так называемые
«неуправляемые типы» - unmanaged types), могут быть поставлены под указатели. Указатели не являются
классами. Блок кода или метод, в котором применяются указатели, должен предваряться служебным словом
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
50
unsafe (небезопасный). При компиляции проекта с небезопасным кодом в свойствах проекта на странице
Build должен быть поднят флажок Allow unsafe code.
Наберите для примера код с использованием указателя так, чтобы метод Main класса _2 проекта MainExe
имел вид
int i = 256;
unsafe
{
byte* pb = (byte*)&i;
System.Console.WriteLine(*pb);
}
System.Console.ReadLine();
Выполните эту версию проекта MainExe.
В строке
byte* pb = (byte*)&i;
 описан указатель pb на тип byte;
 указатель pb инициализирован значением адреса & переменной целого типа i;
 так как адрес &i относится к типу int* - «указатель на целое», то оператором (byte*)
проводится его преобразование в «указатель на байт».
Результатом должен быть байт *pb , «стоящий под указателем» pb. В данном случае это число 0, так как
младший (первый) байт целого числа 256 равен нулю, а старший – единице.
В отличие от ссылочного типа указатель существует сам по себе как число, независящее от объекта, на
который он «указывает». Поэтому указатели можно складывать как целые числа. Например, указатель pb++
будет содержать адрес на единицу больший pb.
В вышеприведенном примере кода вместо WriteLine(*pb)распечатайте *++pb, затем pb[0] и pb[1].
Указатель ++pb равносилен указателю pb+1 и несет адрес второго байта числа i. Выражения pb[0]и
pb[1] равносильны *pb и *(pb+1). Значение *++pb и pb[1]при i=256 должно быть 1. Замените 256
на 512. Посмотрите результат.
Если объекту отвечает некоторый указатель и этот объект оказывается уничтоженным автоматической
«сборкой мусора», то адрес указателя теряет смысл. Чтобы избежать такой ситуации в C# применяется
оператор fixed, «замыкающий» доступ «сборщика мусора» к объекту в блоке кода, в котором определен и
действует указатель на этот объект.
Другой способ избежать сборки мусора при работе с указателями позволяет зарезервировать область стека
достаточного объема для хранения данных под указателем. Оператор, резервирующий n байт памяти под
указатель buffer, имеет вид byte* buffer = stackalloc byte[n];. Используется служебное
слово stackalloc. Такое резервирование возможно лишь внутри блока unsafe. Например, в методе
static unsafe void DoSmth(int n)
{
byte* buffer = stackalloc byte[n];
//Код, работающий с буфером buffer
}
Область buffer будет освобождена (возвращена в «кучу») только по окончанию метода DoSmth.
3.5 Наследование (inheritance)
Вернемся к проекту MainExe, к описанию класса _2. В методе Main добавьте строку
LibClasses._1.
В этой точке IntelliSense покажет список, состоящий из двух статических методов Equals
ReferenceEquals. Другими словами, класс LibClasses._1 содержит два статических
метода с указанными именами. Но откуда эти методы? Ни один из них явно не
описывался в классе LibClasses._1!
Замените добавленную строку другой
string.
На этот раз IntelliSense покажет более длинный список статических членов класса
System.String. Среди них есть те же два метода Equals и ReferenceEquals, что и у
класса LibClasses._1.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
и
51
Попробуйте еще строку
double.
Теперь будет отображен список статических членов класса System.Double и в нем
встретятся те же два метода Equals и ReferenceEquals.
Одним из важнейших качеств объектно-ориентированного подхода в программировании
является способность классов наследовать код других классов. Класс-наследник (derived
class) может наследовать любые члены класса-предка (base class) – методы, поля и т.д. Это
означает, что класс-наследник использует наследуемые члены класса-предка, как свои
собственные.
В языке C# каждый класс может иметь не более одного класса-предка. Но у класса-предка
может быть свой предок и т.д. В основе всех классов, используемых в .NET, лежит один
общий предок - класс System.Object. Прямым предком описанного нами класса
LibClasses._1 является именно класс System.Object. Псевдонимом класса
System.Object является служебное слово object.
Наследование и вызов конструктора
Еще раз рассмотрим класс _2 и действие в нем оператора
static LibClasses._1 inst_1 = new LibClasses._1();
Установите каретку в область текста этого оператора. Дайте команду отладчика Run To
Cursor (например, из контекстного меню). После того, как отладчик выделит строку с этим
оператором, поместите указатель мышки на имя _1 конструктора класса LibClasses._1,
стоящего после оператора new. Тогда получите информацию вида
+LibClasses._1|LibClasses._1
Указав на +, получите еще один комментарий
base {object}|object
Смысл этого комментария в том, что базовым классом (или, предком) класса
LibClasses._1 является класс object и его конструктор с тем же именем object
вызывается каждый раз при вызове конструктора LibClasses._1.
Чтобы убедиться в этом лишний раз, добавьте в пространстве имен LibClasses
библиотечного файла LibCode новый класс _1Der с описанием
public class _1Der : _1
{
}
Это будет класс _1Der - наследник
класса _1. Класс _1Der пуст. Но, по умолчанию, имеет
конструктор с тем же именем и без параметров. Чтобы увидеть это, добавьте в классе _2
после строки
static LibClasses._1 inst_1 = new LibClasses._1();
строку
static LibClasses._1Der inst_1Der = new LibClasses._1Der();
Так будет описан и создан экземпляр класса _1Der, наследующего класс _1.
Установите каретку в область текста этого оператора. Дайте, как и раньше, команду
отладчика Run To Cursor (из контекстного меню). После того, как отладчик выделит строку с
этим оператором, поместите указатель мышки на имя _1Der конструктора класса
LibClasses._1Der, стоящего после оператора new. Тогда получите информацию вида
+LibClasses._1Der|LibClasses._1Der
Указав на +, получите комментарий о базовом классе
+base {LibClasses._1}|LibClasses._1
Вновь указав на +, получите новый комментарий
base {object}|object
Теперь убедитесь, что конструктор базового класса вызывается всегда, когда вызывается
конструктор наследника. С этой целью конструктор базового класса _1 (файл библиотеки
LibCode) перепишите так, чтобы он имел вид
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
52
public _1()
{
System.Console.WriteLine("Base constructor");
}
После этого активизируйте проект. В черном окне дважды должна появиться строка Base
constructor.
Это связано с тем, что конструктор класса _1 вызывается дважды – строкой кода
static LibClasses._1 inst_1 = new LibClasses._1();
в классе _2, и следующей за ней строкой кода
static LibClasses._1Der inst_1Der = new LibClasses._1Der();
Последняя строка создает экземпляр inst_1Der класса-наследника _1Der. Но при этом
конструктор класса _1Der вызывает неявно конструктор своего предка, или базового
класса _1. Проверьте ход выполнения кода с помощью команд отладчика Run To Cursor и
Step Into.
3.6 Методы Equals и ReferenceEquals
Класс System.Object имеет два статических метода Equals и ReferenceEquals,которые
наследуются всеми классами .NET. Оба этих метода содержат два параметра типа object
(псевдоним класса System.Object) и возвращают логическое значение true или false
(типа bool) в зависимости от того, равны ли экземпляры объектов-параметров (метод
Equals) и равны ли ссылки на эти экземпляры (метод ReferenceEquals).
Сотрите либо закомментируйте в конструкторе класса _1 строку
System.Console.WriteLine("Base constructor");
Опишите внутри метода Main класса _2 новый экземпляр inst_1a класса LibClasses._1 и
сравните два имеющихся экземпляра inst_1 и inst_1a так, чтобы новый код класса _2
имел вид
class _2
{
static LibClasses._1 inst_1 = new LibClasses._1();
static void Main(string[] args)
{
LibClasses._1 inst_1a = new LibClasses._1();
System.Console.WriteLine(
LibClasses._1.ReferenceEquals(inst_1, inst_1a));
System.Console.ReadLine();
}
}
Результатом выполнения кода будет False.Этот результат вполне понятен. Ссылки на два
различных экземпляра класса LibClasses._1 не совпадают.
Замените метод ReferenceEquals методом Equals. Результат будет прежний. Вот это уже
не понятно! Ведь поля s экземпляров inst_1 и inst_1a одинаковы?!
Объяснение в том, что по умолчанию метод Equals класса System.Object, как и всех
ссылочных типов, сравнивает только ссылки, как и ReferenceEquals. Однако наследники
класса System.Object могут изменять поведение метода Equals.
В качестве упражнения воспользуемся этой возможностью, изменив поведение метода
Equals при сравнении экземпляров нашего класса LibClasses._1.
3.7 Виртуальные методы (virtual)
Наберите inst_1. (с точкой). В этом случае IntelliSense отображает список доступных
нестатических членов экземпляра класса LibClasses._1. Среди них лишь один метод
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
53
Method описан нами явно в классе. Остальные методы Equals, GetHashCode, GetType
ToString унаследованы от класса System.Object:



и
метод GetHashCode возвращает целое число типа int, «нумерующее» некоторым,
вообще говоря, произвольным образом, экземпляры класса данного типа,
метод GetType возвращает экземпляр объекта типа System.Reflection.Type,
«отражающего» (отсюда reflection) в своих методах и свойствах все характеристики
того типа класса, экземпляром которого является вызывающий объект,
метод ToString дает «читабельное» описание экземпляра объекта – имя класса для
ссылочных типов (кроме string) и значение для типов-значений. Проверьте
действие этого метода, набрав в методе Main класса _2 дополнительную строку
System.Console.WriteLine(inst_1.ToString());
Результатом должна быть строка LibClasses._1. То же и
ToString другим экземпляром inst_1a.ToString().
Рассмотрим более внимательно нестатический метод Equals.
при вызове метода
По названию он совпадает с уже рассмотренным статическим методом, но имеет только
один параметр типа object, а не два, как его статический тезка. Чтобы проверить работу
нестатической версии Equals, внесите изменение в код класса _2
class _2
{
static LibClasses._1 inst_1 = new LibClasses._1();
static void Main(string[] args)
{
LibClasses._1 inst_1a = new LibClasses._1();
System.Console.WriteLine(inst_1.Equals(inst_1a));
System.Console.ReadLine();
}
}
Здесь для сравнения экземпляров inst_1 и inst_1a класса LibClasses._1 вместо
статического метода Equals вызывается его нестатический тезка. Он также сравнивает
экземпляры inst_1 и inst_1a. Результат, как и в предыдущем варианте кода, будет False.
Опять же, по умолчанию, нестатический вариант метода Equals класса System.Object
сравнивает объекты по ссылке.
Изменим содержание класса LibClasses._1 в модуле LibCode, добавив в него описание
нового нестатического метода Equals
public override bool Equals(object obj)
{
return s == ((_1)obj).s;
}
Комментарии к коду
 Служебное слово override означает, что метод Equals является виртуальным
(virtual) и уже описан в методе-предке. При первом описании виртуального
метода (в данном случае, в описании нестатической версии метода Equals в классе
System.Object) его описание содержит служебное слово virtual. Во всех
наследниках, перекрывающих описание виртуального метода, термин virtual
заменяется override.
 Оператор return уже встречался нам в описании некоторых версий метода Main.
Тело нашей версии метода Equals включает служебное слово return. Любой
метод, возвращающий объект какого-либо типа, отличного от void, (в нашем
случае это тип bool) обязательно должен содержать оператор return. Выражение,
следующее за оператором return должно быть того типа, который указан в
описании метода. Оператор return прекращает исполнение метода, даже если этот
оператор не является последним в теле метода. В методе типа void оператор
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
54
может быть опущен. Но если return присутствует в методе типа void, то
дальше не должно следовать какое-либо выражение и в этой точке прекращается
выполнение метода.
 Логическое выражение. Выражение s == ((_1)obj).s возвращает значение типа
bool и является логическим выражением. В данном случае в нем сравниваются две
строки – поле s текущего экземпляра объекта и поле s объекта-параметра obj.
Результатом будет true при совпадении строк и false при их отличии.
 Предполагается, что при вызове метода Equals значением объекта-параметра obj
будет экземпляр класса LibClasses._1 или его наследника. Объект-параметр obj
имеет тип object. У класса object нет поля s. Поэтому перед вызовом поля s
проводится приведение типа объекта obj к объекту типа LibClasses._1.
После проведенной редакции проверим результат выполнения кода. Он будет True.
Переопределение (overriding) виртуального метода Equals принесло свои плоды. Более
того, если, возвращаясь к коду класса _2, заменить вызов нестатического метода
inst_1.Equals(inst_1a) вызовом статического метода LibClasses._1.Equals(inst_1,
inst_1a), результат будет также True. Это говорит о том, что внутри статического метода
Equals класса System.Object, который мы не переопределяли (!), вызывается его
нестатический тезка, нами переопределенный. В этом могущество виртуальных методов.
Переопределив виртуальный нестатический метод Equals, мы изменили результат работы
кода, который не редактировали и который нами даже не контролируется – кода
статического метода Equals!
Недостатки кода.
Заметим, что описанный нами метод Equals класса LibClasses._1, имеет как минимум
два недостатка.
1. Если обратиться к методу Equals с параметром null, то есть испытать код
inst_1.Equals(null), то возникнет ошибка времени выполнения (проверьте). В
методе Equals предполагается, что параметр obj имеет осмысленное значение, не
равное null. Если это не так, метод Equals, очевидно, должен возвращать false.
Ведь вызывающий объект, раз он существует, не может иметь ссылку null.
2. Если обратиться к методу Equals, подставив в качестве параметра ссылку на
объект, не являющийся экземпляром класса LibClasses._1 или его наследника, то
опять возникнет ошибка времени выполнения. Ведь использование оператора
приведения типа (_1)obj в коде метода Equals предполагает, что параметр obj
приводится к типу LibClasses._1. Если это не так, то неизбежна ошибка. Чтобы
убедиться в этом, опишите перед обращением к методу Equals строку с новым
объектом класса, отличного от LibClasses._1, например, int i = 0; и проверьте
выполнение метода Equals в форме inst_1.Equals(i).
Для исправления кода содержание метода Equals следует переписать в виде
return
return obj!=null && GetType()==obj.GetType() && s == ((_1)obj).s;
Сделайте это и вновь проверьте работу кода в двух предыдущих ситуациях.
Комментарии к новой версии кода
 Оператор obj!=null возвращает true, если объекты не равны, и false в противном
случае.
 Служебные символы && (двойной амперсанд) описывают операцию логического
умножения «и» (and) – возвращает true, только если оба операнда равны true. При
этом если логическое выражение obj!=null вернет значение false, дальнейшая
проверка будет приостановлена. То же, если следующее логическое выражение
GetType()==obj.GetType() вернет false. Благодаря этому свойству оператор &&
относится к так называемым шунтирующим (short-circuit) операторам. Другая версия
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
55


оператора логического умножения & (одиночный амперсанд) таким свойством не
обладает и требует вычисления всех операндов вне зависимости от их результатов.
Если в нашем выражении заменить шунтирующий оператор && на обычный &, то
код будет выходить на прежние ошибки времени выполнения при обращениях
inst_1.Equals(null) и inst_1.Equals(i). Испытайте и подумайте, почему это
так.
Кроме логической операции «и» с операторами & и &&, в C# реализованы еще две
бинарные (с двумя операндами) логические операции:
«или» (логическое сложение, или or) с операторами | и || возвращает true, если хотя
бы один из операндов true
операция симметрической разности (xor – исключающее «или», или eXclusive OR) с
оператором ^ возвращает true, если только один из операндов true.
В качестве унарного (действующего на один операнд) оператора отрицания
используется восклицательный знак !.
Операторы &, | и ^ могут применяться к целым операндам. Тогда они действуют
побитовым образом – оперируют над отдельными парами битов, из которых
состоят целые числа-операнды в бинарном представлении. Унарный оператор ~
проводит «побитовое дополнение» целого операнда (возвращает число, которое в
сумме с операндом возвращало бы все биты, равными единице).
 Логическое выражение GetType()==obj.GetType() возвращает true только, если
тип класса вызывающего объекта и тип класса объекта-параметра obj совпадают.
Можно было бы изменить условие, накладываемое на типы объектов, потребовав
лишь, чтобы параметр obj был объектом класса LibClasses._1 или его
наследника. Тогда выражение obj!=null && GetType()==obj.GetType()
достаточно было бы заменить выражением obj is _1. Это выражение возвращает
true, если параметр obj не равен null и если его тип «сводится» к типу
LibClasses._1
(является экземпляром этого типа или его наследника).
Попробуйте эту версию кода в разных вариантах вызова метода Equals. Однако
надо понимать, что при использовании последнего варианта кода метода Equals (с
оператором is) равными окажутся объекты разных типов (хоть и имеющие общего
предка LibClasses._1) с одинаковыми полями s. Поэтому первоначальный
вариант кода
return obj!=null && GetType()==obj.GetType() && s == ((_1)obj).s;
предпочтительней.
3.8 Наследование (синтаксис)
Наследование расширяет число типов, к которым относятся объекты класса-наследника эти объекты имеют тип самого класса-наследника и всех его предков.
Когда один класс наследует другой, класс-наследник приобретает все поля и методы,
которыми обладает его предок.
Класс-наследник может добавить свои поля и методы и может заменить
унаследованные поля и изменить поведение унаследованных методов, если они имеют
достаточный уровень доступа. Существует два способа сделать это.
Один способ «прячет» унаследованное поле или метод. Для этого описывается свое поле с
тем же именем и того же типа или свой метод с тем же именем и сигнатурой и в описание
добавляется служебное слово new.
Например, в классе-предке Base описано поле aField типа type и метод aMethod
public type aField;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
56
public void aMethod();
В классе-наследнике Der можно
public new type aField;
public new void aMethod();
описать
Тогда при вызове объектом класса-наследника метода aMethod() будет вызываться его
новая версия, а в качестве поля aField будет использоваться свое поле, не
пересекающееся с полем aField предка.
При этом способе переопределенные поля и методы предка будут доступны из объекта
DerInst класса-наследника Der путем преобразования типов. То есть, операторы ((Base)
DerInst).aField, ((Base)DerInst).aMethod() будут обращаться к полю aField класса
Base и, соответственно, вызывать метод aMethod() также класса Base.
Другой способ – описать метод, который может быть изменен у наследника, как
виртуальный. Так что, в классе-предке это выглядит как
public virtual void aMethod();
а в классе-наследнике
public override void aMethod();
Поле не может быть виртуальным.
Во втором способе (виртуальный метод) при преобразовании типов, описанном выше,
вызываться будет метод класса-наследника. Это существенно, если виртуальный метод
вызывается внутри кода, написанного для класса-предка. Так как реальная ссылка в
момент выполнения кода будет указывать на экземпляр объекта класса-наследника, то
выполняться будет именно версия класса-наследника.
При описании виртуального метода в классе-наследнике (метод с модификатором
override) часто бывает необходимо вызвать унаследованную версию виртуального
метода. Для этого используется служебное слово base. Так реализация метода aMethod()
класса-наследника может иметь вид
public override void aMethod();
{
base.aMethod(); //Вызов метода класса-предка
// …
}
Если дальнейшее перекрытие виртуального метода aMethod()
в последующих
наследниках не желательно, то в его заголовок следует добавить модификатор sealed
public sealed override void aMethod();//Метод больше не будет перекрываться
Если же, наоборот, метод, описанный как виртуальный, должен быть заменен новой
версией виртуального же метода, описание должно иметь вид
public new virtual void aMethod();//Образуется новый виртуальный метод
3.9 Перегрузка операторов
Проверьте, что последняя редакция кода класса LibClasses._1 в модуле LibCode имеет
вид
namespace LibClasses
{
public class _1
{
string s="a string";
public override bool Equals(object obj)
{
return obj!=null && GetType()==obj.GetType() && s == ((_1)obj).s;
}
public _1(string s)
{
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
57
this.s = s;
}
public _1()
{
}
public void Method()
{
System.Console.WriteLine(s);
}
}
}
Вернемся вновь к редакции класса _2 в модуле CodeFile2 сборки MainExe.
Пусть новая редакция кода класса _2 имеет вид
class _2
{
static LibClasses._1 inst_1 = new LibClasses._1();
static void Main(string[] args)
{
LibClasses._1 inst_1a = new LibClasses._1();
System.Console.WriteLine(inst_1.Equals(inst_1a));
System.Console.WriteLine(inst_1a==inst_1);
System.Console.ReadLine();
}
}
Здесь важна новая строка кода
System.Console.WriteLine(inst_1a==inst_1);
Оператор inst_1a==inst_1 вернет false, так как сравнению подвергаются ссылки.
Новый метод Equals при сравнении тех же объектов возвращает true. На лицо явное
несоответствие – оператор равенства возвращает результат, отличный от метода Equals!
То же несоответствие возникает и в отношении оператора неравенства inst_1a!=inst_1
(проверьте).
Язык C# позволяет перегружать некоторые стандартные операторы, в частности
операторы сравнения == и !=.
Вернемся к коду класса LibClasses._1 в модуле LibCode и добавим к нему описания
перегруженных операторов == и !=, характерные для класса LibClasses._1 и его
наследников.
public static bool operator ==(_1 obj1, _1 obj2)
{
return Equals(obj1,obj2);
}
public static bool operator !=(_1 obj1, _1 obj2)
{
return !Equals(obj1,obj2);
}
Любое описание операторов должно содержать модификаторы public static и
использовать служебное слово operator. Проверьте работу новой версии кода.
3.10 Методы GetHashCode и ToString
Вернемся к коду класса _2 сборки MainExe и добавим внутрь метода Main строку
System.Console.WriteLine(inst_1.GetHashCode() == inst_1a.GetHashCode());
Метод GetHashCode возвращает целое число, «нумерующее» экземпляр класса.
Естественно считать, что метод GetHashCode, вызванный двумя одинаковыми
экземплярами должен возвращать одно и то же целое число. В данном случае это не так.
Добавленная строка вернет значение false.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
58
Метод GetHashCode является виртуальным, как и метод Equals, и его следует
переопределить вместе с методом Equals и операторами == и !=.
В описание класса LibClasses._1 добавьте код
public override int GetHashCode()
{
return s.GetHashCode();
}
Испытайте новую версию класса LibClasses._1.
Здесь использовалось то, что экземпляры класса LibClasses._1 сравниваются по
значению поля s типа System.String. В свою очередь, тип System.String уже имеет
переопределенный метод GetHashCode, который возвращает одинаковые числа для
одинаковых значений строк.
Метод ToString
Виртуальный метод ToString класса уже использовался нами выше. В классе
LibClasses._1 его естественно было бы переопределить, индивидуализируя каждый
экземпляр этого класса значением поля-строки s.
Добавочный код к классу LibClasses._1 наберите в виде
public override string ToString()
{
return s;
}
При этом в методе Main класса _2 сборки MainExe
с целью тестирования добавьте две
строки кода
System.Console.WriteLine(inst_1.ToString());
System.Console.WriteLine(inst_1a.ToString());
Работа проекта в этой редакции должна возвращать в окно две одинаковые строки a
string – значения поля s обоих экземпляров.
3.11 Модификатор protected
Следует отметить, что класс LibClasses._1 унаследовал от System.Object не только
уже перечисленные статические методы Equals и ReferenceEquals и нестатические
методы Equals, GetHashCode, GetType и ToString. Все перечисленные методы имеют
модификатор public, поэтому доступны из любого кода. Но есть еще, по крайней мере,
один метод, наследуемый от класса System.Object и который виден лишь внутри методов
класса LibClasses._1.
Чтобы убедиться в этом, наберите внутри, например, метода Method класса LibClasses._1
ссылку на экземпляр this. (с точкой). Как обычно, IntelliSense предложит список, в
который, кроме поля s и уже известных нам методов, входит метод MemberwiseClone. Это
нестатический (но не виртуальный) метод, не имеющий параметров и возвращающий
объект, являющийся копией вызывающего объекта-оригинала. Фактически метод
MemberwiseClone создает новый экземпляр объекта того же класса, что и оригинал,
переписывая в него все нестатические поля объекта-оригинала дословно. Если среди
полей объекта-оригинала существуют ссылки на объекты других классов, то эти ссылки
также дословно переписываются. Такая копия называется «мелкой» (shallow copy).
Допустимо копирование, при котором копируются и те объекты, на которые ссылаются
поля оригинала. При этом в образованной копии поля-ссылки содержат уже новые ссылки
- на копии объектов. Это «глубокое копирование» (deep copy).
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
59
Для нас сейчас важно то, что метод MemberwiseClone доступен только изнутри методов
класса LibClasses._1 - наследника класса System.Object, потому что он не имеет
модификатора public. В то же время метод MemberwiseClone не является private
методом класса System.Object. В противном случае он не был бы доступен какому-либо
коду вне класса System.Object. Дело в том, что метод MemberwiseClone имеет
модификатор доступа protected.
Член класса с модификатором protected доступен только внутри класса, либо внутри
его наследников. Существует более сильная версия – модификатор protected internal.
Член класса с модификатором protected internal доступен только наследникам,
описанным внутри той же сборки, что и класс-предок.
Для проверки работы метода MemberwiseClone наберите новую версию метода Method
класса LibClasses._1
public void Method()
{
_1 i_1 = (_1)MemberwiseClone();//Создание копии текущего экземпляра
System.Console.WriteLine(i_1.Equals(this));//Сравнение копии с оригиналом
}
Обратите внимание на необходимость в данном коде оператора преобразования типа
(_1). Ведь метод MemberwiseClone возвращает переменную типа object, а описанный
экземпляр i_1 относится к типу _1.
Измените также код метода Main класса _2, добавив вызов метода inst_1a.Method.
static void Main(string[] args)
{
LibClasses._1 inst_1a = new LibClasses._1();
inst_1a.Method();
System.Console.ReadLine();
}
Результатом работы последней версии кода будет True в
черном окне.
3.12 Деструктор
Класс позволяет любому наследнику иметь специальный нестатический метод, код
которого работает непосредственно перед уничтожением объекта - возвратом в кучу
выделенной под объект памяти. Это так называемый деструктор.
Описание деструктора, например, класса LibClasses._1 должно выглядеть следующим
образом
~_1()
{
}
и не должно содержать ни модификаторов доступа, ни параметров. Деструктор не может
быть переопределен, перегружен или унаследован.
Деструктор используется для освобождения ресурсов (открытые ссылки на сайты
Интернета, файлы, окна и т.п.), которые, возможно, потребляет экземпляр класса.
Описывать пустой деструктор не рекомендуется. Деструктор никогда не вызывается явно
из кода приложения. К нему обращается система в тот заранее не определяемый момент
времени, когда требуется очистить память от неиспользуемого более экземпляра
(вспомните - автоматическая сборка мусора).
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
60
3.13 Справочная система
Справку обо всех стандартных пространствах имен (библиотек), классах, их методах и
свойствах, о среде, языках и т.д. можно получить из справочной системы MSDN (Microsoft
Software Development Network). Есть несколько способов сделать это:
 Краткая справка об объекте, который используется в коде программы, может быть
получена путем простого наведения курсора на этот объект. Попробуйте,
например, навести курсор на метод System.Consol.WriteLine. Среда откроет
небольшое окно подсказки с краткой справкой о методе.
 Выше уже указывалось, как работать с IntelliSense. Наберите, например, object с
точкой. Это приведет к появлению окна со статическими методами этого класса –
знакомыми нам методами Equals и ReferenceEquals. Выделение в списке любого
метода позволяет получить краткую справку о нем.
 Есть команда View.Other Windows.Object Browser…, которая открывает окно с описанием
всех пространств имен, классов и их членов. В окне Object Browser также можно
получить краткую информацию обо всех элементах библиотеки.
 Подробную справку можно получить, используя команды Dynamic Help, Contents…,
Index…, Search… меню Help.
o Команда Help.Dynamic Help обеспечивает поиск разделов справочной системы
по термину, на котором в данный момент находится символ каретки в окне
редактора. Эта команда вызывает список разделов, которые содержат
определение термина.
o Команда Help.Contents… открывает окно с полным содержанием MSDN.
o Команда Help.Index… возвращает окно, в котором раздел может быть найден
по термину через предметный указатель.
o Наконец, команда Help.Search… возвращает полный список разделов, в
которых встречается заданный термин.
Для практики в использовании справочной системы найдите через команду Help.Index…
описание класса Object и познакомьтесь подробнее с его методами, отработав
приведенные в их описании примеры.
3.14 Директивы препроцессора
Добавьте к имеющемуся контейнеру Solution1 новый проект. Для этого откройте окно
Solution Explorer (команда View.Solution Explorer) и, щелкнув правой кнопкой над заголовком
Solution1 (2 projects), дайте команду Add.New Project…. В открывшемся окне Add New Project
выберите шаблон Console Application и дайте ему имя TestProject, щелкнув затем по кнопке OK.
Сохраните файл проекта под именем TestProject командой File.Save Program As….
Среда должна создать новый проект с заготовленным содержанием вида
using System;
using System.Collections.Generic;
using System.Text;
namespace TestProject
{
class Program
{
static void Main(string[] args)
{
}
}
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
61
Нам часто придется ссылаться на класс System.Console, поэтому добавьте, как это
делалось раньше, к списку директив using строку
using C=System.Console;
В дальнейшем C будет служить
кратким псевдонимом (alias) класса System.Console.
Добавим в текст еще две строки так, что первые строки кода примут вид
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
using C=System.Console;
#endregion
Директивы препроцессора #region и #endregion ограничивают поименованную
сворачиваемую область текста, именуя ее Using directives. В данном случае
поименованная область объединяет строки с директивами using, но в общем случае
именовать можно любую сворачиваемую область текста. Имя Using directives,
конечно, произвольно. Сверните полученный участок кода и посмотрите результат.
Существует ограниченное число директив препроцессора. Все они начинаются со знака #
(«шарп», или «хэш»). Наиболее распространенными директивами являются
 #define – определение какого-либо символа,
 #undef – отказ от определения символа,
 #if – начало области кода, который транслируется при наличии определенного
(или определенных) символов,
 #endif – конец этой области
 #else и #elif играют роль, подобную #if.
Для примера добавьте к шаблону код, тестирующий работу перечисленных директив так,
что полный текст новой версии файла TestProject примет вид
#define Version1
#define Version2
//#undef Version1
//#undef Version2
#region Using directives
using System;
using System.Collections.Generic;
using System.Text;
using C=System.Console;
#endregion
namespace TestProject
{
class Program
{
static void Main(string[] args)
{
#if (Version1 && !Version2)
C.WriteLine("Version1");
#elif (!Version1 && Version2)
C.WriteLine("Version2");
#elif (Version1 && Version2)
C.WriteLine("Version1 and Version2");
#else
C.WriteLine("Version0");
#endif
C.ReadLine();
}
}
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
62
Попытка активизировать приложение командой Debug.Start Debugging приведет к работе
метода Main проекта MainExe – по умолчанию это был стартовый проект. Чтобы сделать
стартовым проект TestProject, войдите в окно Solution Explorer, щелкните правой кнопкой над
именем проекта TestProject и выберите команду Set as StartUp Project. Шрифт имени проекта
должен стать полужирным.
После компиляции среда выделит цветом или шрифтом компилируемые операторы,
отличив их от не компилируемой части кода.
Заметьте, что директивы препроцессора #define и #undef можно использовать только в
начале компилируемого модуля.
Успешное выполнение кода должно завершиться появлением строки
Version 1 and Version2
Уберите комментаторские символы // (double slash) в строке
//#undef Version1
и вновь испытайте код. Проделайте аналогичные тесты в других сочетаниях.
Вопросы для самоконтроля
1. Тип класса как объект. Статический конструктор. Статический класс.
2. Нестатический класс и его экземпляр как объект.
3. Ссылочный тип vs тип-значение.
4. Перечислите стандартные типы-значения.
5. Наследование и вызов конструктора.
6. Статические методы Equals и ReferenceEquals.
7. Нестатические методы класса object.
8. Описание виртуальных методов.
9. Оператор return.
10. Логические выражения и логические операции.
11. Преобразование (приведение) типов.
12. Оператор is vs метод GetType.
13. Опишите способы new и virtual, override переопределния членов класса при
наследовании.
14. Перегрузка операторов.
15. Использование модификатора protected.
16. Деструктор.
17. Как использовать справочную систему VS?
18. Директивы препроцессора.
Занятие 4. Строка (класс System.String)
Добавьте к имеющемуся контейнеру Solution1 новый проект. Для этого откройте окно
Solution Explorer (команда View.Solution Explorer) и, щелкнув правой кнопкой над заголовком
Solution1 (3 projects), дайте команду Add.New Project…. В открывшемся окне Add New Project
выберите шаблон Console Application и дайте ему имя StringTestProject, щелкнув затем по
кнопке OK.
Сохраните файл проекта под именем StringTestProject командой File.Save Program As….
В целях удобства замените имя класса Program, предложенное средой, именем StringTest и
сохраните проект.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
63
Чтобы сделать стартовым проект StringTestProject, войдите в окно Solution Explorer, щелкните
правой кнопкой над именем проекта StringTestProject и выберите команду Set as StartUp Project
из меню Project. Шрифт имени проекта в окне Solution Explorer должен стать полужирным.
Добавьте, как это делалось раньше, к списку директив using внутри поименованной
области строку
using C=System.Console;
4.1 Инициализация экземпляра типа String
Строка есть массив символов.
Наберите в методе Main строку
string s = "abc";
Так выглядит код описания и инициализации экземпляра класса типа string.
После этого наберите s. (с точкой). Инструмент IntelliSense покажет список всех
нестатических свойств и методов экземпляров строк, доступных пользователю.
4.2 Индексатор и свойство строки
Свойство
public char this[int index]{get;}
отсутствует в списке нестатических свойств. Оно является примером индексатора (indexer)
и возвращает символ (тип char), находящийся на index+1-ом месте в вызывающем
экземпляре строки. С помощью индексатора экземпляр строки (как и любого класса с
индексатором) представляется массивом.
Наберите в методе Main класса StringTest код, иллюстрирующий работу индексатора
string s = "abc";
C.WriteLine(s[2]);
C.ReadLine();
Результатом должен быть символ c. Обратите внимание, что
 индексация символов в строке начинается с нуля (zero based)!
 индексатор строки позволяет использовать только метод get (возвратить).
Простейшая реализация метода get может иметь вид get {return
item[index];}. Предполагаемое поле item (элемент) класса string может хранить
символы, из которых состоит строка. Индексатор (или свойство), у которого
доступен только метод get, называют «только для чтения» (read-only). Поэтому
оператор вида s[1] = 'x'; компилятором будет отвергнут. Попробуйте.
 Метод set (установить) в данном объявлении индексатора отсутствует. Но это не
значит, что его нет на самом деле. Язык C# допускает описание одного из методов
доступа (accessors) set или get со своим модификатором доступа из числа private,
protected,
internal.
При этом оба модификатора доступа должны
присутствовать. Уровень доступа метода доступа должен быть ниже уровня
доступа самого свойства или индексатора. В данном случае метод set мог бы
существовать и иметь один из уровней доступа private, protected, internal –
ниже уровня public самого индексатора. Например, метод set мог бы иметь вид
private set {item[index]=value;}. Служебное слово value - единственный
(неявный) параметр метода доступа set, переносящий значение, которое
устанавливается свойством или индексатором.
Длина строки возвращается свойством «только для чтения» Length (длина)
public int Length{get;}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
64
Простейшая реализация метода get может иметь вид get {return length;}. Поле length
хранит текущее значение числа элементов строки. Общепринятым является обозначать
поля и свойства одинаковыми идентификаторами - у поля первая буква строчная
(lowercase letter), а у свойства – прописная (uppercase letter). Поля обычно имеют доступ
private, а свойства – public.
Скрывать поля – обычная политика объектно-ориентированного подхода. Свойства
регулируют доступ к полям. Если бы у свойства Length был метод доступа set, то он бы
мог иметь вид private set {length=value;}.
Испытайте свойство Length кодом C.WriteLine(s.Length);
4.3 Свойства и индексаторы (синтаксис)

Свойства обеспечивают класс возможностью открытой записи и чтения полей,
скрывая реализацию проверки входящего значения и получения запрашиваемого
значения.
 Метод get с помощью оператора return обеспечивает чтение (возврат) значения
свойства, а метод set его запись (установку). Эти методы могут иметь различный
уровень доступа.
 Служебное слово value является неявным параметром метода set, передающим
записываемое значение свойства.
 Свойство, не имеющее метода set, называется свойством «только для чтения» (read
only).
Свойство с точки зрения пользователя выглядит как поле, а с точки зрения
программиста, как один или два метода из числа get и set.
Описание свойства F, соответствующего полю f типа type, может выглядеть следующим
образом
type f;
//…
public type F
{
set
{
//…
f = value;
}
get
{
//…
return f;
}
}
В отличие от поля свойство нельзя использовать в качестве параметра, передаваемого по
ссылке (с модификаторами ref и out) при вызове какого-либо метода. К примеру, метод,
описанный как void aMethod(ref type a), нельзя вызвать оператором aMethod (ref F),
хотя можно, если позволяет доступ, aMethod (ref f).
Один (но только один) из методов set или get может иметь свой модификатор доступа,
более строгий, чем модификатор доступа самого свойства. При этом оба метода должны
присутствовать в описании свойства. К примеру, метод set свойства F мог бы иметь
модификатор доступа internal, protected internal, protected или private.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
65
Свойство можно перекрыть в наследнике. Для этого используется оператор new. К
примеру, если свойство F уже было описано в классе-предке, но должно быть перекрыто в
наследнике, должно иметь заголовок
public new type F
Свойство (но не каждый из методов set или get в отдельности) может быть виртуальным.
В этом случае в описании свойства используется модификатор virtual, а в классахнаследниках модификатор override.
Свойство может быть абстрактным. Тогда его описание имеет форму
public abstract type F
{
set;
get;
}
Индексаторы
Индексаторы имеют форму свойств-массивов. Они позволяют индексировать объекты.
Для пользователя индексированный объект представляется массивом.
Описание индексатора имеет вид
public int this[int index]
{
// get и set методы
}
Методы get и set имеют один дополнительный неявный параметр index.
Тип параметра index не ограничен типом int. Параметр index может иметь,
тип string.
например,
Число индексов не ограничено. Массив может быть многомерным.
Индексатор может быть перегруженным.
4.4 Конструкторы строк
Простейший способ сконструировать строку из символов – присвоить экземпляру s типа
string значение постоянной строки "abc"
string s = "abc";
При этом неявно срабатывает оператор new, выделяющий память под строку s, и
конструктор string, инициализирующий s значением "abc".
Конструктор экземпляров строк имеет несколько перегруженных версий, в каждой из
которых есть хотя бы один параметр, ссылающийся на символы, формирующие экземпляр
строки. Версия конструктора строки без параметра отсутствует.
Например, наберите в методе Main класса StringTest код с явным вызовом конструктора
строки
char[] chars = new char[3] { 'a', 'b', 'c' };
string s = new string(chars);
C.WriteLine(s);
C.ReadLine();
Здесь вызвана версия конструктора string(chars) с одним параметром chars
типа
массива символов. В предыдущей строке кода
 Выражение char[] chars описывает массив символов.
 Оператор new char[3] вызывает конструктор массива из 3-ех символов.
 Список символов { 'a', 'b', 'c' } в фигурных скобках последовательно
инициализирует элементы сконструированного массива chars так, как если бы
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
66
трижды

применялся оператор присвоения chars[0]=’a’; chars[1]=’b’;
chars[2]=’c’;.
Возможен сокращенный вариант описания и инициализации массива
char[] chars = { 'a', 'b', 'c' };
Результат тот же. Компилятор восстанавливает (infer) полный код по числу постоянных в
списке.
Попытка сконструировать строку, суммируя символы, т.е. оператором s='a'+'b'+'c'
будет отвергнута компилятором «из-за несовместимости объектов целого типа и строк»
(char относится к целому типу). Проверьте.
Можно усложнить выражение, набрав s=('a'+'b'+'c').ToString(). В результате вместо
строки "abc" получится строка "294", выражающая целое число, равное сумме кодов
символов 'a'(97), 'b'(98) и 'c'(99). Опять проверьте.
Складывать надо не символы, но строки
s=”a”+”b”+”c”;
Проверьте.
4.7 Поле Empty
Наберите код, указав лишь имя класса, string. (с точкой). Список, возвращаемый
IntelliSense, будет содержать статические поля и методы класса String.
Единственное поле класса String, доступное внешним классам, это статическое поле
Empty
public static readonly string Empty;
Поле Empty возвращает пустую (empty) строку.
string s=string.Empty;
Написать
равносильно оператору
string s=””;
4.8 Модификатор доступа readonly
Служебное слово readonly требует, чтобы значение поля инициализировалось либо
непосредственно в описании, либо внутри конструктора (для статического поля это
может быть только статический конструктор, а для нестатического – конструктор
экземпляра), но нигде более. При отсутствии инициализации поле с доступом readonly,
как и все неинициализированные поля класса, принимает при создании экземпляра
значение по умолчанию - null для ссылочного типа и ноль или false для типа-значения.
4.9 Статические методы класса System.String
Метод IsNullOrEmpty возвращает True, если его строка-параметр пустая или ссылка на
нее null. Испытайте.
Внутренний пул строк
Два статических метода Equals и ReferenceEquals унаследованы классом String от
своего непосредственного предка System.Object.
Наберите в методе Main класса StringTest код
string s1 = "ab", s2 = "ab";
C.WriteLine(string.ReferenceEquals(s1,s2));
C.ReadLine();
Его выполнение возвратит True в черное окно.
Почему? Ведь описаны два различных экземпляра типа string, хотя и совпадающие по
значениям! Почему они совпадают по ссылкам, как будто это один экземпляр?
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
67
Ответ в том, что среда в целях экономии памяти помещает все «дословно определяемые»
в коде экземпляры класса string в выделенную область памяти – внутренний пул (intern
pool). Если значения постоянных строк совпадают, то они не дублируются и ссылка на
совпадающие строки оказывается одной и той же. Статические методы IsInterned и
Intern класса String работают с этим пулом.
«Не изменяемость» строк и класс StringBuilder
Добавьте строку кода
s2 = s2 + string.Empty;
явно не меняющего значения s2. Убедитесь, что ссылки на s1 и s2 теперь не совпадают.
Убедитесь также, что результат сравнения s1==s2 или string.Equals(s1,s2) возвращает
True. Следовательно, любое изменение значения строки ведет фактически к созданию
нового экземпляра объекта типа string. В этом смысле экземпляры типа string являются
не изменяемыми (immutable) – любая попытка изменить экземпляр строки приводит к
созданию нового экземпляра.
В пространстве имен System.Text существует специальный класс StringBuilder,
который позволяет создавать и поддерживать изменяемые (mutable) экземпляры строк,
меняя их с помощью таких методов как Append (добавить), Insert (вставить), Remove
(убрать), Replace (заменить).
Копирование и объединение строк
Статический метод Copy класса string создает точную копию экземпляра строки с другой
ссылкой. Проверьте его работу, скажем, оператором
C.WriteLine(string.ReferenceEquals(s1, string.Copy(s1)));
Перегруженные версии метода Concat (объединить в цепочку) формируют
строку из
отдельных фрагментов – отдельных строк и строчных представлений других объектов.
Действие такое же, как у оператора сложения. Испытайте, например, версию «3 строки»
C.WriteLine(string.Concat("Hello", " C#", "!"));
или еще одну, «4 объекта» (целого типа)
C.WriteLine(string.Concat(2,0,0,5));
Две версии метода Join образуют строку из
массива (или указанного фрагмента массива)
других строк, вставляя между элементами массива строку-сепаратор. Например, код
C.WriteLine(string.Join("!!!",new string[]{"Okay","All right","Deal?"}));
должен возвратить строку
Okay!!!All right!!!Deal?
Вторым параметром метода Join является массив строк string[] любого размера – тот
же тип, как параметр метода Main.
Оператор new string[]{"Okay","All right","Deal?"}
 создает экземпляр массива из трех строк (вызывает соответствующий
конструктор) и
 заполняет его (инициализирует) строками (элементами массива), перечисляемыми
через запятую в фигурных скобках.
 В
этом
контексте
сократить
оператор
new
string[]{"Okay","All
right","Deal?"} до {"Okay","All right","Deal?"} уже нельзя, как при
описании массива. Ведь в данном случае созданный экземпляр является
одновременно параметром метода.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
68
4.10 Форматирование строк
Каждый класс наследует от System.Object и, при необходимости, переопределяет
виртуальный метод ToString, обеспечивая текстовое представление своих объектов.
Объекты классов ссылочного типа за исключением String возвращают имя своего типа.
Классы типов-значений обычно определяют несколько версий метода ToString,
обеспечивая разную текстовую форму (format) представления своих объектов. Текстовое
представление объектов типов-значений называют в данном контексте форматированием
(formatting).
Процесс, обратный форматированию, состоит в лексическом анализе (parsing) текста
строки с целью воссоздания объекта.
Методы композиционного форматирования
Несколько версий статического метода Format класса String формируют строку из
заданных объектов путем композиционного форматирования. Основным параметром
метода Format является комбинированная строка. В ней кроме чисто текстовой,
комментирующей части находятся так называемые индексированные заполнители (indexed
placeholders). Например, код
int i=13;string s = string.Format("i={0}", i);
C.WriteLine(s);
C.ReadLine();
вернет строку i=13. Здесь комбинированная строка содержит комментарий i= и
индексированный заполнитель {0}. Индексированный заполнитель указывает на то,
объект (в данном случае типа int), стоящий первым (отсюда значение 0 индекса) в
что
последующем списке параметров, должен быть форматирован в строку и эта строка
должна быть поставлена на место заполнителя {0}.
Испытайте этот код для объектов разных типов. Замените, например, первую строку кода
строкой вида
object o = new object();string s = string.Format("o={0}", o);
или
string s = string.Format("Now {0}", DateTime.Now);
Полезный комментарий. У класса System.DateTime типа-значения (struct) есть
статическое свойство Now. Это свойство возвращает объект также типа System.DateTime.
Объект, возвращаемый свойством Now, содержит настоящий момент времени.
Испытайте теперь код с другой версией метода Format, где вторым параметром является
массив объектов
int i1 = 1, i2 = 2;
C.WriteLine(string.Format("i1={0};i2={1}",new object[2] {i1, i2}));
Результат i1=1;i2=2
Замените заполнитель {1} заполнителем {0}. Убедитесь, что результат стал i1=1;i2=1.
В общем случае индексированные заполнители могут иметь вид
{индекс [,выравнивание][:строка формата]}
Здесь в квадратных скобках указаны не обязательные параметры индексированного
заполнителя, которые не использовались в предыдущих примерах:
 Параметр «выравнивание» (alignment) должен быть целым числом со знаком или без
знака. Значение выравнивания есть требуемое число символов в текстовом
представлении выводимого объекта. Если реальное число символов строки больше,
то значение выравнивания игнорируется. Если параметр «выравнивание»
положительный, то выводимая строка выравнивается по правому полю, если
отрицательный – по левому.
Чтобы уяснить роль параметра «выравнивание», наберите строку кода
C.WriteLine(string.Format("i1=|{0,10}|;i2=|{1,-5}|",new object[2] {i1, i2}));
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
69
Результат i1=|
1|;i2=|2
|
 Параметр «строка формата» содержит условные символы, указывающие на
форму текстового выражения объекта.
Добавьте строки формата c и p1 в предыдущую строку кода так, чтобы она имела вид
C.WriteLine(string.Format("i1=|{0,10:c}|;i2=|{1,-5:p1}|",i1,i2));
(одна из версий метода Format, использованная здесь, допускает в качестве параметров
два объекта, а не массив объектов, как в предыдущем примере).
Результатом должна быть строка текста
i1=|
1,00р.|;i2=|200,0%|
Здесь р. – сокращение от «рубль», так как строка формата c указывает, что речь идет о
валюте (currency). Строка формата p1 представляет запись целого числа в процентах после
умножения на 100.
Еще пример
C.WriteLine(string.Format("Today {0:M}", DateTime.Today));
C.WriteLine(string.Format("Now {0:F}", DateTime.Now));
Результатом первой строки должна быть текущая дата, а второй - дата и время. Строками
формата являются буквы M и F (от month - месяц и full - полный формат даты и времени).
Строка формата зависит от типа объекта. В частности, для численных типов объекта
строка формата имеет вид Axx. Здесь A – так называемый спецификатор формата.
Например, если A это символ D или d, то целый тип форматируется в свое обычное
цифровое (digital) представление. Необязательное целое число xx в интервале 0..99, может
следовать за спецификатором формата. Оно определяет требуемое число значащих цифр.
Например, d5 указывает, что выводимым параметром должно быть целое число с пятью
значащими цифрами. Для вывода вещественных чисел спецификаторами формата
являются символы F или f (float) и e или E (exponent).
Испытайте разные строки формата, набрав, например, строки кода
C.WriteLine("Число пи={0}",Math.PI);
C.WriteLine("Число пи={0:g}",Math.PI);
и, заменяя, затем, спецификатор g другими строками
формата, скажем f10, F2, e15, E0.
Обратите внимание в частности на то, что спецификатор формата g (general) используется
по умолчанию, то есть в том случае, когда строка формата опущена. То же и в формате
объектов типа DateTime.
Из последнего примера видно, что комбинированную строку и список форматируемых
объектов можно использовать не только в качестве параметров метода Format, но и
метода WriteLine класса Console. Класс StringBuilder также имеет несколько версий
метода форматирования AppendFormat.
4.11 Метод с нефиксированным числом параметров
В примерах с применением метода string.Format использовались различные версии
этого метода. В одной из них метод имеет два параметра – строка форматирования и
массив объектов. Обратите внимание, что в описании этой версии второй параметр –
массив объектов, описан в форме params object[]args.
Служебное слово params позволяет пометить параметры метода, число которых не
фиксировано. Тип этих параметров должен быть один и тот же. Если метод имеет
несколько параметров, то параметры params могут быть только последними в списке.
Формальным параметром, допускающим служебное слово params, должен быть
одномерный массив.
Наберите в классе StringTest метод
static void UseParams(params char[] Params)
{
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
70
foreach (char ch in Params)
C.Write(ch);
}
В методе Main наберите код
UseParams( 'H','e','l','l','o',' ','C','#','!');
Посмотрите результат.
Если опустить params в описании метода UseParams, то фактическим параметром метода
UseParams может быть только массив символов.
Тогда вызов UseParams в методе Main код должен иметь вид
UseParams(new char[] { 'H', 'e', 'l', 'l', 'o', ' ', 'C', '#', '!' });
Отсюда следует, что метод string.Format можно применять, например, в форме
int i1 = 1, i2 = 2, i3 = 3, i4 = 4;
C.WriteLine(string.Format("i1={0};i2={1};i3={2};i4={3}", i1, i2, i3, i4));
Версии с 4-мя объектами у метода string.Format нет. Проверьте этот код.
Одна из версий метода Format позволяет использовать пользовательский (custom) способ
форматирования объектов – например, целых чисел в бинарной форме, либо комплексных
чисел.
4.12 Класс Math
Это закрытый для наследования (sealed) класс – прямой наследник класса Object. Класс
Math предлагает только статические поля (поле PI – число «пи» и поле E – число «е»,
основание натуральных логарифмов) и методы, часто используемые в математических
расчетах:
 Несколько версий методов Sign, Abs, Min и Max, возвращающие для всех числовых
типов, соответственно, знак, абсолютную величину числа и минимальное и
максимальное из двух предложенных чисел.
 Алгебраические функции:
o Экспонента Exp, степень Pow и квадратный корень Sqrt числа;
o тригонометрические функции Sin, Cos, Tan;
o обратные тригонометрические функции ASin, ACos, ATan, ATan2 (функция
ATan2 возвращает арктангенс отношения двух параметров – y и x-координат
точки);
o логарифмические функции Log (2 версии - натуральный логарифм и
логарифм с произвольным основанием), Log10 (десятичный логарифм);
o гиперболические функции Sinh, Cosh, Tanh.
Все параметры и возвращаемые объекты этих функций - 8-ми байтовые числа с
плавающей запятой типа double.
 Метод BigMul возвращает произведение двух 4-байтовых (Int32) целых чисел в
форме целого числа 8-байтового типа Int64.
 Методы округления чисел
o По две версии методов Ceiling и Floor (для аргументов типа double и
decimal) возвращают, соответственно, ближайшие сверху и снизу целые
числа (говорят, округление к «положительной» и, соответственно,
«отрицательной бесконечности»). Тип возвращаемого числа совпадает с
типом параметра (double или decimal).
o Восемь версий метода Round округляют числа типа double и decimal до
ближайшего числа без учета и с учетом точности округления (до целого или
до вещественного числа определенной точности), учитывая при этом способ
округления полу целых чисел (от нуля или до ближайшего четного числа).
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
71
o Две версии метода Truncate для аргументов типа double и decimal
действуют аналогично Ceiling и Floor, но округляют число в сторону нуля.
Методы округления возвращают числа типа, заданного аргументом (double и
decimal соответственно). Испытайте методы округления на примерах.
 Методы получения остатка от деления
o Метод IEEERemainder возвращает остаток типа double от деления двух
чисел того же типа.
o Две версии метода DivRem (для типов int и long) возвращают целую часть
результата деления двух целых чисел и, в одном из параметров, целый
остаток от их деления.
Представляет интерес описание метода
public static int DivRem(int a, int b, out int result);
Два первых параметра описывают делимое a и делитель b, а последний result –
остаток от деления. Параметр result имеет модификатор out («параметр выхода»),
указывающий на то, что
1. result - параметр по ссылке, то есть в result возвращается значение,
полученное внутри DivRem.
2. result не обязан иметь какое-либо первоначальное значение.
Заметим, что в C# есть специальная бинарная операция % – остаток от деления
нацело двух чисел целых или вещественных типов.
Вопросы для самоконтроля
Как описывается индексатор класса string?
Структура описания свойств и индексаторов.
Как описываются свойства «только для чтения»?
Как описывается массив символов?
Что означает модификатор readonly?
Что означает «не изменяемость» строк?
Опишите способ форматирования с помощью комбинированной строки и
структуру индексированного заполнителя.
8. Как описывается метод с нефиксированным числом параметров?
9. Опишите члены стандартного класса Math. Что означает модификатор sealed?
1.
2.
3.
4.
5.
6.
7.
Занятие 5. Интерфейсы. Полиморфные классы и методы
Заголовок описания класса System.String имеет вид
public sealed class String : IComparable, ICloneable, IConvertible,
IComparable<string>, IEnumerable<string>, IEnumerable, IEquatable<string>;
 Служебное слово sealed («запечатанный») означает (как и в случае класса Math),
что класс System.String не может иметь наследников.

После двоеточия находится список имен, начинающихся буквой I. Это интерфейсы
(interface) – классы с нереализованными методами. В общем случае правила C#
требуют, чтобы в описании класса class или struct после имени типа и двоеточия
указывалось имя одного класса-предка и имена тех интерфейсов, методы которых
класс обязывается реализовать. Если имя класса-предка не указывается, то
предком является класс System.Object. Отсюда заключаем, что класс
System.String является прямым наследником класса Object и реализует методы
интерфейсов, перечисленных в списке (говорят также «наследует интерфейсы»).
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
72
5.1 IComparable (сравнимый)
Первый в списке интерфейс System.IComparable предлагает к реализации один метод
CompareTo. Описание интерфейса System.IComparable имеет вид
public interface IComparable
{
int CompareTo(object obj);
}
Члены интерфейса не должны иметь модификаторов доступа. Не явно все члены
интерфейса имеют доступ public. Члены интерфейса также не могут быть static,
virtual, override или abstract.
Метод CompareTo реализован классом String с заголовком
public int CompareTo(object obj);
Методы интерфейсов, реализуемые классом, должны иметь модификатор доступа public
и совпадать по числу и типу параметров и возвращаемому типу (сигнатура).
Проверьте работу метода String.CompareTo, набрав в методе Main класса StringTest код
object o = null;
C.WriteLine("b".CompareTo(o));
Результатом должна быть единица – в сравнении с null любая строка (и, вообще, любой
объект) возвращает положительную единицу – результат сравнения есть «больше чем»
(greater than).
Испытайте также значения объекта o, равные "b", "a", "c", убедившись, что в первом
случае результатом будет 0 – строки равны (equal), затем 1 ("b" «больше» "a") и, наконец,
-1 ("b" «меньше» "c"). Метод сортирует строки по алфавиту.
Возможны и другие способы сортировки строк – по кодам символов, из которых состоит
строка, с учетом или без учета различий строчных и прописных букв, в зависимости от
порядка выделенных номеров символов (подстрок) и т.д. Для этого в классе String
существует несколько версий статических методов Compare и CompareOrdinal.
5.2 Работа с исключительными ситуациями (exceptions)
Замените последнюю версию строки описания объекта o строкой
object o = new object();
и вновь активизируйте приложение командой Debug.Start Debugging.
Возникает ошибка времени выполнения (runtime error) типа “Object must be of type String”,
вызванная тем, что фактическим параметром метода CompareTo оказался объект, «не
сводимый к строке». В этом случае метод CompareTo создает (throw) экземпляр класса
исключительной ситуации (сокращенно, ИС) типа System.ArgumentException –
наследника корневого класса System.Exception. Среда прерывает выполнение кода,
сообщая, что эта исключительная ситуация осталась без реакции со стороны кода
(unhandled).
Язык C# позволяет реагировать на появление исключительных ситуаций. Для
иллюстрации отредактируйте код метода Main так, чтобы он имел вид
string s = "b";
object o = new object();
try
{
C.WriteLine(s.CompareTo(o));
}
catch (Exception e)
{
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
73
C.WriteLine(e);
}
C.ReadLine();



Внутрь «блока попытки» try { } помещен вызов метода CompareTo, который
может создать (throw) исключительную ситуацию (этот факт указывается в
описании метода в справочной системе).
При возникновении исключительной ситуации управление передается вверх по
стеку в ближайший «блок обнаружения» catch (Exception e){}. В блок catch
программист помещает операторы, реагирующие на обнаруженное исключение.
При этом экземпляр созданного объекта (в данном случае объекта класса
ArgumentException) передается переменной e того же типа или типа предка (в
данном случае типа Exception). При перемещении вверх по стеку, информация о
маршруте перемещения накапливается в свойстве Message объекта e.
Оператор C.WriteLine(e) (сокращенная запись от WriteLine(e.Message);
попробуйте) печатает сообщение, перенесенное объектом e
System.ArgumentException: Object must be of type String.
at System.String.CompareTo(Object value)
at StringTestProject.StringTest.Main(String[] args) in
далее следует конкретный маршрут к исполняемому модулю StringTest и номер
строки кода.
Можно написать код, который должен выполняться после потенциального источника ИС
вне зависимости от того, возникла ИС или нет. Для этого после блока catch следует
открыть еще один блок, озаглавив его служебным словом finally. Наберите в
предыдущем примере сразу после блока catch (перед C.ReadLine();) код
finally { C.WriteLine("Always done"); }
Проверьте его работу при возникновении ИС и, когда ошибка не возникает (к примеру,
замените строку с объявлением объекта o строкой object o = "c";).
Источником исключительной ситуации являлся в данном случае метод CompareTo, и
созданный им объект типа System.ArgumentException принял сообщение (Message) Object
must be of type String, напечатанное в первой строке текста.
Если добавить в «блок попытки» try перед строкой C.WriteLine(s.CompareTo(o));
оператор вида
if (!(o is string))
throw new ArgumentException("Object is not of type String");
то до входа в метод CompareTo дело не дойдет (проверьте).
Примерно такой оператор находится внутри метода CompareTo, и именно так создается
экземпляр объекта исключительной ситуации нужного типа.
Оператор if, использованный в последнее строке, относится к условным операторам
языка C#. Вслед за служебным словом if в круглых скобках располагается выражение,
возвращающее логическое значение true или false. Далее находится оператор (или блок
операторов {}), выполняющийся по значению true.
Класс String, реализовав интерфейс IComparable, делает свои объекты сравнимыми по
определенным характеристикам.
5.3 ICloneable (клонируемый)
Интерфейс System.ICloneable имеет вид
public interface ICloneable
{
object Clone();
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
74
и предлагает для реализации единственный метод Clone, дополняющий рассмотренный
выше метод Object.MemberwiseClone.
Класс String реализует метод
public object Clone()
таким образом, что возвращается ссылка на тот же экземпляр класса String. Проверьте
этот факт, набрав внутри метода Main рабочего проекта код
string s = "a",sClone;
sClone = (string)s.Clone();
C.WriteLine(string.ReferenceEquals(s, sClone));
C.ReadLine();
Результатом должно быть значение True. Следовательно,
экземпляры s и sClone
совпадают полностью.
Обратите внимание на преобразование типов: s.Clone возвращает объект типа object и
его следует явным вызовом оператора () преобразования типа, в данном
случае(string), преобразовать к типу string.
Класс String, реализовав интерфейс ICloneable, позволяет клонировать свои объекты.
5.4 IConvertible (преобразуемый)
Интерфейс System.IConvertible предлагает для реализации методы, преобразующие
вызываемый объект в объект другого типа, поддерживаемого .NET. Это типы Boolean,
Byte, Char, DateTime, Decimal, Double, Int16, Int32, Int64, SByte, Single,
String, UInt16, UInt32 и UInt64. Описание интерфейса имеет вид
public interface IConvertible
{
TypeCode GetTypeCode();
object ToType(Type conversionType, IFormatProvider provider);
bool ToBoolean(IFormatProvider provider);
//Далее - методы, подобные ToBoolean, для остальных типов .NET
}
 Тип TypeCode, возвращаемый методом GetTypeCode, есть нумератор enum,
перечисляющий стандартные типы .NET
public enum TypeCode {Boolean, Byte, Char, DateTime, DBNull, Decimal,
Double, Empty, Int16, Int32, Int64, Object, SByte, Single, String,
UInt16, UInt32, UInt64}
здесь DBNull – отсутствие данных в поле базы данных, Empty – ссылка null.

Метод ToType имеет первым параметром объект conversionType типа Type, в
который следует перевести вызывающий объект. Тип Type – класс из пространства
имен System.Reflection. Методы и свойства класса Type описывают («отражают»
– reflect) общие характеристики типов, используемых в .NET. Второй параметр
provider метода ToType имеет тип IFormatProvider - интерфейс из пространства
имен System;
 интерфейс IFormatProvider имеет единственный метод GetFormat(Type
formatType), возвращающий объект, который отвечает за форматирование данных
типа formatType.
Метод GetTypeCode реализован классом String и доступен для вызова любым
экземпляром класса String. Проверьте это с помощью кода
string s = "10:59:07,May 1,2005";
C.WriteLine(s.GetTypeCode());
Результатом будет имя типа вызывающего объекта String.
Наберите еще одну строку
C.WriteLine(((IConvertible)s).ToDateTime((IFormatProvider)(new
System.Globalization.DateTimeFormatInfo())));
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
75
В черное окно будет выведена строка даты-времени
01.05.2005 10:59:07
5.5 Явная реализация методов интерфейса
Методы ToType, ToDateTime и т.д. интерфейса IConvertible реализованы в классе
String с заголовками особой формы, явно указывающей на их принадлежность к
интерфейсу IConvertible
object IConvertible.ToType(Type conversionType, IFormatProvider provider);
DateTime IConvertible.ToDateTime (IFormatProvider provider);
Явное описание методов интерфейса в реализующем классе не должно сопровождаться
модификаторами доступа типа public и т.п.
По правилам C# - методы интерфейса, описанные в реализующем классе с явным
указанием типа интерфейса можно вызвать только экземпляром того же типа
интерфейса (в данном случае интерфейса IConvertible). Поэтому для вызова метода
ToDateTime понадобился фрагмент кода ((IConvertible)s).ToDateTime, где явно
указано преобразование ссылки на объект s типа string в ссылку на объект типа
интерфейса IConvertible. Другой способ приведения типов может быть реализован с
помощью оператора as (как). Выражение (IConvertible)s можно заменить выражением
s as IConvertible, что означает дословно «рассматривай ссылку на объект s, как ссылку
на объект типа IConvertible». Проверьте.
Параметром метода ToDateTime является объект типа IFormatProvider. Такого типа
объект может быть получен только из класса, реализующего интерфейс IFormatProvider.
В данном примере этим классом является System.Globalization.DateTimeFormatInfo.
Код (IFormatProvider)(new System.Globalization.DateTimeFormatInfo()) создает
экземпляр класса System.Globalization.DateTimeFormatInfo и преобразует его тип в
IFormatProvider.
Класс String, реализовав интерфейс IConvertible, делает свои объекты преобразуемыми
в объекты других типов.
На практике для преобразования строк к значениям переменных конкретных типов
рекомендуется использовать статические методы класса System.Convert.
Действительно, гораздо проще выглядит строка кода
C.WriteLine(Convert.ToDateTime(s));
Проверьте ее работу.
5.6 Интерфейсы (синтаксис)
Интерфейсы подобны абстрактным классам.
Интерфейсы не могут иметь экземпляров.
Интерфейсы могут быть членами пространства имен или классов.
Членами интерфейса могут быть только свойства, индексаторы, методы и события
(event). Интерфейс не может иметь полей. Члены интерфейса автоматически имеют
доступ public и модификатор доступа в их описании не указывается. Интерфейс не
содержит реализации своих методов.
Интерфейс может наследовать один или более интерфейсов.
Класс может наследовать один или более интерфейсов. В этом случае любой не
абстрактный класс должен реализовать все члены интерфейсов. Все члены класса,
реализующие члены интерфейса, должны иметь то же имя, ту же сигнатуру,
модификатор доступа public и должны быть нестатическими.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
76
Члены интерфейса могут быть реализованы классом явно. Члены интерфейса,
реализованные явно, могут быть доступны только экземпляру интерфейса, но не
реализующего класса. При этом тип объекта реализующего класса преобразуется в тип
интерфейса.
5.7 IEnumerable (перечислимый)
Интерфейс System.Collections.IEnumerable
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
предлагает для реализации метод GetEnumerator, возвращающий объект типа
IEnumerator (нумератор).
В свою очередь интерфейс System.Collections.IEnumerator имеет вид
public interface IEnumerator
{
object Current {get;};
bool MoveNext();
void Reset();
}
 Current (текущий элемент) - предлагаемое для реализации свойство типа object.
Интерфейс IEnumerator требует лишь реализации метода get (возвратить)
свойства Current - свойство должно позволять читать значение поля current
реализующего объекта, но не обязано в это поле писать. Простейшая реализация
метода get может иметь вид get {return current;}.
 MoveNext - метод перемещения к следующему элементу; возвращает false, если
следующего элемента нет;
 Reset – метод перемещения к началу; должен устанавливать значение current
перед первым элементом.
Класс String реализует метод GetEnumerator, используя в качестве «реализатора»
интерфейса IEnumerator класс System.CharEnumerator
public CharEnumerator GetEnumerator();
Испытайте код
string s = "abc";
CharEnumerator ce = s.GetEnumerator();
ce.Reset();
while (ce.MoveNext()) {C.WriteLine(ce.Current);}
Убедитесь, что результатом будут символы, из которых состоит строка s.
5.8 Цикл foreach,in
Класс CharEnumerator позволяет перечислять символы в строке. Испытайте его работу
кодом
string s = "Hello C#!";
foreach (char ch in s) C.WriteLine(ch);
C.ReadLine();
Наличие нумератора у класса String позволяет ему пользоваться оператором итераций
foreach,in. Цикл foreach,in действует как обычный оператор цикла, но перечисляет все
элементы объекта в соответствии с правилом, которое задает ему класс-нумератор,
реализующий интерфейс IEnumerator.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
77
Реализовав интерфейс IEnumerable, класс String превращает свои объекты в
перечислимый набор символов.
Примечание
В C# возможно написание специальных участков кода – итераторов (iterators). Итераторы позволяют сделать
объекты описываемого класса перечислимыми и использовать для них цикл foreach,in. Написание
итератора позволяет обойтись без реализации классом интерфейса IEnumerable. Ниже будет приведен
пример создания и использования итератора.
5.9 Полиморфные интерфейсы
Кроме уже перечисленных интерфейсов класс String реализует также методы
полиморфных (generic) интерфейсов IComparable<T>, IEquatable<T> и IEnumerable<T>
для T типа String.
Интерфейс IComparable<T> (полиморфно сравнимый)
является полиморфной (generic) версией интерфейса System.IComparable
public interface IComparable<T>
{
int CompareTo(T other);
}
Интерфейс System.IComparable<T> предлагает для реализации знакомый нам метод
CompareTo для произвольного типа объектов T. Произвольность типа T, являющегося
параметром интерфейса, делает описание интерфейса полиморфным (generic), или «годным
для множества типов».
Класс String реализует указанный метод для своего типа string
public int CompareTo(string other);
Попробуйте код
string s="Hi!";
C.WriteLine(s.CompareTo("Hi!"));
Реализация интерфейса System.IComparable<T>
делает класс String элементом
множества классов со сравнимыми объектами.
Интерфейс IEquatable<T> (полиморфно приравниваемый)
предлагает для реализации полиморфную версию знакомого нам метода Equals
public interface IEquatable<T>
{
bool Equals(T other);
}
Класс String реализует этот метод для
public bool Equals(string other);
своего типа string
Попробуйте код
string s="Hi!";
C.WriteLine(s.Equals("Hi!"));
Реализация интерфейса System.IEquatable<T>
делает класс String элементом
множества классов с приравниваемыми объектами.
Интерфейс IEnumerable<T> (полиморфно перечислимый)
Описание интерфейса
public interface IEnumerable<T>: IEnumerable
{
IEnumerator<T> GetEnumerator();
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
78
Наберите код
string s = "Hello C#!";
IEnumerator<char> es = ((IEnumerable<char>)s).GetEnumerator();
while (es.MoveNext()) { C.Write(es.Current + "\x20"); }
C.ReadLine();
Он демонстрирует явную реализацию метода GetEnumerator интерфейса
IEnumerable<char> классом String.
5.10 Полиморфные классы



Кроме классов типа interface полиморфными могут быть классы типа class,
struct и delegate, а также методы.
Описание полиморфного типа должно быть оформлено так, как это указано выше в
примере интерфейса IComparable<T>. Число параметров-типов может быть любым
<T1,T2,…>.
Описание полиморфного класса может содержать ограничения (constraints) на
параметры-типы. Для этого используется служебное слово where (где). Например,
описание вида
public class AClass<T> where T:struct {}
означает, что тип параметров T ограничен типами-значениями.
Вместо служебного слова struct, как в данном примере, могут




стоять следующие
выражения
 class – только ссылочные типы;
 BaseClass – только класс с именем BaseClass или его наследники;
 InterfaceName - только интерфейс InterfaceName и классы, его
реализующие;
 new() – только классы, имеющие конструктор без параметров.
Ограничения могут образовывать список - перечисляться через запятую.
Ограничение new() должно находиться в конце списка ограничений.
При отсутствии списка ограничений тип T считается не связанным условиями
(unbounded).
Наследование полиморфных классов от обычных допустимо без ограничений.
Наследование обычного класса от полиморфного допустимо лишь, если
полиморфный класс-предок явно определяет тип своего параметра-типа.
Наследование полиморфного класса от другого полиморфного, но с бóльшим
числом параметров-типов, допустимо лишь, если полиморфный класс-предок явно
определяет типы своих параметров, когда их несколько и они не входят в число
параметров наследника. Источник этих ограничений в том, что в C# конструктор
класса-наследника автоматически вызывает конструктор класса-предка. Если
параметр-тип у наследника полиморфного класса не определен, вызов
конструктора класса-предка не имеет смысла.
Создать экземпляр полиморфного класса можно только при явном указании
параметра-типа.
Например, наберите в пространстве имен StringTestProject текущего проекта
(вне или внутри тела класса StringTest, но вне тела метода Main) описание двух
пустых классов
class AClass<T> where T : struct {}
struct AStruct {}
Далее, в методе Main класса StringTest того же файла наберите
экземпляров полиморфного класса AClass<T> в форме
AClass<int> AClassIntInstance = new AClass<int>();
описания двух
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
79
AClass<AStruct> AClassAStrctInstance = new AClass<AStruct>();
Компиляция должна пройти без ошибок.
Но, если в том же методе Main добавить описание вида
AClass<string> AClassStringInstance = new AClass<string>();
компилятор обозначит ошибку – ограничение struct, накладываемое на тип T
полиморфного класса AClass<T>, не позволяет использовать в качестве параметра T
тип string, относящийся к ссылочным типам. Уберите не верно составленную
строку кода.
5.11 Полиморфные методы
Наберите пример полиморфного метода Method<V> так, что код класса StringTest
проекта StringTestProject примет вид
static void Method<V>(){}
static void Main(string[] args)
{
C.ReadLine();
}
Компиляция должна пройти успешно (прежний метод Main надо, конечно, убрать).
Примечание.
Модификатор static в описании метода Method<V>, конечно, не обязателен. Он добавлен лишь для
вызова метода Method<V> в теле статического метода Main. Нестатические методы класса – это методы
экземпляров и их бессмысленно вызывать внутри статических методов, где экземпляры не определены.
 Метод Method<V> может иметь параметры типа V. Например,
static void Method<V>(V v)

Полиморфный метод может возвращать объект полиморфного типа V. Например,
иметь заголовок
static V Method<V>()
Правда, при таком заголовке метод не может быть пустым. В нем должен быть, по
крайней мере, оператор return, возвращающий значение типа V. Если записать
метод в виде
static V Method<V>()
{
V instV = default(V);
return instV;
}
то ошибок компиляции не будет. Проверьте.
 Специальное служебное слово default определяет значение по умолчанию объекта
типа V. Если в своей реализации тип V будет ссылочным типом, то значением
объекта instV будет null, если типом-значением, то метод Method<V> вернет
значение 0 или false.
Проверьте это, набрав строку в методе Main
C.WriteLine(Method<int>());
В черном окне должен быть напечатан 0.
Чтобы проверить работу метода Method<V> для ссылочного типа (например, типа
string) наберите строку
C.WriteLine(object.Equals(null,Method<string>()));
Результатом должно быть значение True.

На типы полиморфного метода могут быть наложены ограничения (constraints) точно
так же, как это делается при описании полиморфного класса.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
80
Посмотрите списки, возвращаемые IntelliSense, при обращении к строке как к объекту
каждого из наследуемых интерфейсов. Для этого наберите, например,
((ICloneable)s).(с точкой). То же для других интерфейсов. Это поучительно.
Вопросы для самоконтроля
1. Описание класса, наследующего интерфейсы. Что означает наследование
интерфейсов?
2. Описание членов интерфейса в самом интерфейсе и в реализующем его классе.
3. Как создается и обрабатывается исключительная ситуация в C#?
4. Общие правила описания интерфейсов и реализации их членов.
5. Опишите структуру оператора итераций foreach, in. В каком случае такой
оператор используется?
6. Как описываются полиморфные классы?
7. Как описываются полиморфные методы, и что означает служебное слово default?
Занятие 6. Массив (класс System.Array)
Класс System.Array поддерживает методы для работы с упорядоченными коллекциями
однотипных объектов и служит базовым классом для описания массивов объектов всех
типов. Класс Array непосредственный наследник класса Object.
Описание класса имеет вид
public abstract class Array: ICloneable, IList, ICollection, IEnumerable;
Примечание. Класс Array не является полиморфным, но все его классы-наследники (массивы объектов
конкретного типа T) реализуют свойства и методы полиморфных версий интерфейсов IList<T>,
ICollection<T>, IEnumerable<T> этого типа.
6.1 Абстрактные классы
Конструкторы классов с модификатором abstract не могут создавать объекты. В этом
смысле у абстрактного класса, как и у интерфейса, не может быть собственных объектов.
Но, в отличие от интерфейса, абстрактный класс может иметь конструктор (обычно
protected) и абстрактный класс может содержать любые члены обычного класса, в том
числе и реализованные методы интерфейсов. Конструкторы не абстрактных наследников
абстрактного класса могут создавать объекты, вызывая, при этом, доступные
конструкторы и методы своего абстрактного предка.
Добавьте к имеющемуся контейнеру Solution1 новый проект. Для этого откройте окно
Solution Explorer (команда View.Solution Explorer) и, щелкнув правой кнопкой над заголовком
Solution1 (4 projects), дайте команду Add.New Project…. В открывшемся окне Add New Project
выберите шаблон Console Application и дайте ему имя ArrayTestProject, щелкнув затем по
кнопке OK.
Сохраните файл проекта под именем ArrayTestProject командой File.Save Program As….
В целях удобства замените имя класса Program, предложенное средой, именем ArrayTest и
сохраните проект.
Чтобы сделать стартовым проект ArrayTestProject, войдите в окно Solution Explorer, щелкните
правой кнопкой над именем проекта ArrayTestProject и выберите команду Set as StartUp Project.
Добавьте, как это делалось раньше, к списку директив using внутри поименованной
области строку
using C=System.Console;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
81
6.2 Одномерный массив
Обычный способ создания экземпляра одномерного массива заданного типа уже не раз
применялся в прежних примерах. Например, чтобы создать экземпляр chars массива типа
char[2] символьных объектов типа char, достаточно набрать код
char[] chars = new char[2];
После этой операции все элементы массива chars[0] и chars[1] будут иметь целые
значения 0. Нулю будут равны коды символов, или численные представления символов
массива. Если набрать код
C.WriteLine(chars[0]);
то ошибки не будет, но и результатом будет символ, не представленный каким-либо
текстовым изображением. Однако код
C.WriteLine((int)chars[0]);
напечатает 0.
Примеры инициализации также уже встречались выше. Еще один пример
decimal[] dcmls = new decimal[2] { -100, 1.5M };
C.WriteLine("d1={0:C5};d2={1:C}",dcmls[0],dcmls[1]);
Спецификатор формата C (currency) представляет значение числа в
местной валюте. Можно
сократить описание и инициализацию массива
decimal[] dcmls = { -100, 1.5M };
Суффикс M (от money, т.к. тип decimal
применяется в основном в финансовых расчетах)
требуется для преобразования типа константы 1.5 (по умолчанию double) в тип decimal.
Размер массива (число элементов) возвращается свойством Length, а размерность –
свойством Rank. Строка кода
C.WriteLine("Length={0};Rank={1}",dcmls.Length,dcmls.Rank);
возвратит обе эти характеристики описанного массива dcmls: Length=2;Rank=1.
Массив может состоять из объектов типа object. В этом смысле - из любых объектов.
Например, опишите массив
object[] objects = new object[7]{1,'a',new object(),"abc",true,null,
new object[2]{DateTime.Now,-1.5e45}};
который содержит разные типы объектов: как ссылочные object, string, null, массив
object[], так и типы-значения int, char, bool. Последний элемент массива сам является
массивом object[] из двух объектов типов double и DateTime.
6.3 Условный оператор if…else
Наберите код, распечатывающий этот массив
foreach (object o in objects)
if (o != null)
C.Write(o.ToString() + " ");
else C.Write("null\x20");
Здесь
 применяется условный оператор типа if…else так, что только объекты, не равные
null (ссылка «в никуда»), используют метод ToString для своего текстового
представления;
 между двойными кавычками дается пробел;
 escape-последовательность \x20
- другой способ распечатать пробел;
шестнадцатеричный (отсюда x - hexadecimal) код пробела равен 20 (32 в десятичной
системе счисления).
Усложните код внутри цикла foreach
if (o != null)
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
82
if (o.GetType() == typeof(object[]))
{
C.WriteLine();
for (int i = 0; i < ((Array)o).Length; i++)
C.Write(((object[])o)[i].ToString() + " ");
}
else
C.Write(o.ToString() + " ");
else C.Write("null\x20");
Это позволит распечатать отдельной строкой содержание массива-элемента object[2].
Обратите внимание
 на порядок выполнения вложенных операторов if…else. Он отмечен отступами.
 На использование метода GetType и оператора typeof. Оператор typeof, как и
метод GetType, возвращает объект типа Type - библиотечный класс ссылочного
типа, содержащий в своих методах и свойствах полное описание типа. Метод
GetType возвращает объект типа Type для объекта, который вызывает GetType
(в данном примере - для объекта o), а оператор typeof – для имени типа, явно
указанного в качестве аргумента оператора (в данном примере - для типа массива
object[]).
 На преобразования типов. Преобразование (Array)o позволяет использовать
свойство Length объектов типа Array и его наследников (у объекта o нет свойства
Length). В то же время, чтобы применить к объекту o оператор индексации [],
следует преобразовать тип объекта o к типу object[]. Преобразование к типу
Array приведет к ошибке. Проверьте, заменив object[] на Array в этой части
кода.
Индекс массива может иметь любой целочисленный тип – это типы целых чисел int,
ulong и т.п. и символьный тип char.
Проверьте работу кода и узнайте символьный код буквы «Я»
object[] objects = new object['Я'];
C.WriteLine(objects.Length);
Можно описать очень большой массив
sbyte[] veryBigOne = new sbyte[100000000L];
Суффикс L означает, что целочисленная постоянная имеет целый восьми байтовый тип:
знаковый long, если она меньше 9223372036854775807, и без знака ulong в противном
случае.
6.4 Многомерный прямоугольный массив
Пример кода
object[,] objRect=new object[3,2]{{1,'a'},{DateTime.Now,"abc"},{true,-1e45}};
C.WriteLine("\t\tobjRect\n\t\tRank={0};LongLength={1};Size={2}x{3}",
objRect.Rank,objRect.LongLength,objRect.GetLength(0),objRect.GetLength(1));
описывает и инициализирует двумерный прямоугольный массив размера 3x2 из элементов
типа object. Посмотрите результат.
 Полное число элементов во всех размерностях возвращается свойством
LongLength.
 Метод GetLength(i) возвращает длину i–ого измерения.
 Измерения, как и индексы элементов, нумеруются от нуля (zero-based).
Распечатайте элементы массива, набрав дополнительный код
for (int i = 0; i < objRect.GetLength(0); i++)
{
for (int j = 0; j < objRect.GetLength(1); j++)
C.Write("[{0},{1}]=|{2,-20}|", i, j, objRect [i, j].ToString());
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
83
C.WriteLine();
}
6.5 «Зубчатый» массив (jagged array)
В прямоугольном массиве длины всех строк одинаковые. В зубчатом массиве они могут
быть разные, потому что зубчатый массив – это массив массивов. Выше уже был пример,
в котором элементом массива был другой массив. Наберите код
object[][] jgdObjects = new object[3][]
{
new object [1]{DateTime.Now},
new object [3]{'a',1,"abc"},
new object [2]{true,-1.5e45 }
};
C.WriteLine("\t\t jgdObjects \n\t\tRank={0};Length={1}",
jgdObjects.Rank, jgdObjects.Length);
и проверьте его работу.
Для краткости, в этом описании можно опустить оператор new object[3][], но нельзя
опускать операторы, стоящие внутри фигурных скобок.
Распечатайте элементы зубчатого массива jgdObjects кодом
for (int i = 0; i < jgdObjects.Length; i++)
{
C.Write("Length={0}|", jgdObjects [i].Length);
for (int j = 0; j < jgdObjects [i].Length; j++)
C.Write("[{0},{1}]=|{2,-20}|", i, j, jgdObjects [i][j].ToString());
C.WriteLine();
}
Зубчатые массивы могут быть
 многомерными прямоугольными массивами одномерного массива object[,][]
 одномерными массивами многомерных прямоугольных массивов object[][,].
Испытайте несколько примеров на собственное усмотрение с разными встроенными
типами элементов int, double и т.п.
6.6 Наследование ICloneable и IEnumerable<T> классами массивов
Реализация рассмотренных выше интерфейсов ICloneable и IEnumerable<T> делает
объекты классов массивов (наследников Array) клонируемыми и нумеруемыми. Наберите
код, иллюстрирующий работу метода Clone из интерфейса ICloneable
char[] chars = new char[3] { 'a', 'b', 'c' }, clone;
clone=(char[])(((ICloneable)chars).Clone());
foreach (char ch in clone) C.Write(ch+"\x20");
Можно не преобразовывать тип chars в тип ICloneable, так как класс Array реализует
метод Clone интерфейса ICloneable как метод своих объектов. Испытайте это, заменив
((ICloneable)chars) просто chars.
Другой код иллюстрирует работу метода полиморфного интерфейса IEnumerable<T> в
реализации класса char[]
char[] chars = new char[3] { 'a', 'b', 'c' };
IEnumerator<char> eChars = ((IEnumerable<char>)chars).GetEnumerator();
while (eChars.MoveNext()) { C.Write(eChars.Current+"\x20"); }
В этом коде уже нельзя отказаться от преобразования объекта chars к типу
IEnumerable<char>, так как объект eChars типа IEnumerator<char> создается методом
GetEnumerator интерфейса IEnumerable<char>, а не класса Array. Хотя, конечно, именно
класс массивов объектов типа T реализует метод IEnumerable<T>.GetEnumerator().
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
84
Реализация методов интерфейсов ICollection<T> и IList<T> добавляет объектам
массивов типа T новые свойства.
6.7 Интерфейс ICollection<T>
Описание
public interface ICollection<T>: IEnumerable<T>, IEnumerable
требует от класса, наследующего интерфейс ICollection<T>
1. реализации методов GetEnumerator полиморфной и не полиморфной
интерфейса IEnumerable (пример использования обсуждался выше);
версий
2. реализации двух собственных свойств:
 Свойство int Count {get;} возвращает своим методом get число элементов в
коллекции. Наберите код с использованием описанного выше массива chars,
C.WriteLine(((ICollection<char>)chars).Count);
Результат – число 3. Откажитесь в этом операторе от преобразования к типу
ICollection<char> и проверьте просто chars.Count. Компилятор укажет на то,
что класс Array не содержит свойства Count. Следовательно, это свойство
реализовано как свойство интерфейса ICollection<char> (то есть, явно).
 Метод get свойства bool IsReadOnly {get;} возвращает, закрыта ли коллекция
для изменений (read-only). В реализации класса Array свойство IsReadOnly всегда
возвращает false, т.е. массив открыт для изменений его членов. Проверьте кодом
C.WriteLine(chars.IsReadOnly);
3. реализации пяти собственных методов:
 Метод void CopyTo(Array array,int index); позволяет копировать элементы
коллекции в массив array, начиная с индекса index. Под типом Array здесь
имеется в виду любой массив – наследник класса Array, так как сам класс Array не
может иметь экземпляров, будучи абстрактным. Классы массивов реализуют
предложенную интерфейсом ICollection<T> версию метода CopyTo. Проверьте
это кодом
char[] chars = new char[3] { 'a', 'b', 'c' }, copy = new char[5];
chars.CopyTo(copy, 2);
C.WriteLine("copy={0} {1} {2}", copy[2], copy[3], copy[4]);
Кроме того, класс Array добавляет свою версию CopyTo, в которой второй
параметр index имеет тип long, а не int. (Версия .NET 2.0 расширяет свою
библиотеку на 8-ми байтовые адреса).
 Вызов объектом класса массива методов Add и Remove интерфейса ICollection<T>
приводит к исключительной ситуации типа «класс не поддерживает метод». Хотя
это не значит, что методы Add и Remove не реализованы классами массивов.
Проверьте строками кода
((ICollection<char>)chars).Add('d');
((ICollection<char>)chars).Remove('a');
Дело в том, что методы Add (добавить) и Remove
(убрать) предназначены для
изменения числа элементов коллекции, а у объектов-массивов, наследующих от
Array, есть свойство bool IsFixedSize {get;} (фиксированный размер), всегда
возвращающее true. Проверьте с помощью кода

C.WriteLine(chars.IsFixedSize);
Метод bool Contains(T item) возвращает true, если вызывающий его
объектов типа T содержит элемент item. Проверьте работу метода Contains
C.WriteLine(((ICollection<char>)chars).Contains('a'));
C.WriteLine(((ICollection<char>)chars).Contains('d'));
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
массив
кодом
85
Метод Contains реализован как метод интерфейса ICollection<T>, а не классамассива, поэтому преобразование к типу ICollection<char> необходимо.
 Метод void Clear() (очисть). Вызов этого метода в отношении массива приводит
к исключительной ситуации. Метод Clear реализован как метод интерфейса
ICollection<T>, а не класса-массива, поэтому преобразование типа необходимо.
У не полиморфной версии интерфейса ICollection есть два свойства, реализованные
классом Array:
 Свойство bool IsSyncronized {get;} возвращает, синхронизован ли (thread safe)
доступ к элементам коллекции в «нитях» (thread; другие варианты перевода этого
же термина, «потоки», «подпроцессы»), где эти элементы используются. В
реализации классом Array свойство IsSyncronized всегда возвращает false.
Проверьте кодом
C.WriteLine(chars.IsSynchronized);
6.8 Потоки, или нити (thread), или подпроцессы
Поток операций (процесс), который пропускается через процессор при выполнении
приложения, в целях экономии времени делится средой или самим программистом на
элементарные «нити» (threads). Таким способом время простоя процессора при выполнении
кода, осуществляющего медленную операцию передачи информации при связи с внешним
устройством (сеть, диск, экран, принтер и т.д.), может быть использовано для проведения
других операций. Этот параллелизм (асинхронность) в выполнении кода может быть
реальным, если параллельно работает нескольких процессоров, или нет - для
однопроцессорной машины. Однако на уровне программиста следует учитывать
возможную асинхронность в выполнении операций над одним и тем же объектом,
блокируя в случае необходимости доступ к полям объекта из подпроцессов, выполняемых
асинхронно.
 Свойство object SyncRoot {get;} интерфейса ICollection возвращает объект,
отвечающий за синхронизацию операций над элементами коллекции в разных
потоках. Код
C.WriteLine(chars.SyncRoot.ToString());
должен возвратить класс System.Char[].
Объект типа ICollection (экземпляр класса, реализующего интерфейс ICollection)
обеспечивает синхронизацию с помощью оператора lock («замкни») как, к примеру, в
коде
ICollection collObj=//например, конструктор класса, реализующего ICollection
lock (collObj.SyncRoot)
{
//Операции с элементами collObj в этой части кода безопасны (thread safe)
}
Доступ к элементам коллекции collObj закрывается для других нитей на время
выполнения предлагаемых операций с этими элементами.
6.9 Интерфейс IList<T> (полиморфный список)
Описание интерфейса
public interface IList<T>: ICollection<T>, IEnumerable<T>, IEnumerable
говорит о том, что класс, реализующий полиморфный интерфейс IList<T>, должен также
реализовать методы интерфейсов ICollection<T>, IEnumerable<T>, IEnumerable. В
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
86
частности, объектам типа IList<T> должны быть доступны методы и свойства,
объявленные интерфейсом ICollection<T>.
Сам по себе интерфейс IList<T> требует от класса-наследника реализации следующих
свойств и методов.
Индексатор object this [int index] {get; set;} интерфейса IList<T>
устанавливает с помощью метода set и возвращает методом get index–ый объект списка
(начиная с нуля). В реализации классов массивов этот индексатор фактически определяет
оператор индексирования [].
В общем случае индекс индексатора может быть любого типа (например, типа string,
или даже вещественного числа) в отличие от индекса массива, который может быть
только целочисленным.
Метод int IndexOf(object value) возвращает индекс объекта value в списке.
Наберите код
C.WriteLine(((IList<char>)chars).IndexOf('b'));
Программа должна напечатать единицу для выше описанного массива chars.
Метод void RemoveAt(int index) убирает из списка объект, занимающий позицию
index, а void Insert(int index, object value) вставляет объект value в позицию
index списка.
Вызов методов RemoveAt и Insert объектом класса массива приводит к исключительной
ситуации из-за фиксированного размера массива. Проверьте.
Не полиморфная версия интерфейса IList, также наследуемая классом Array, содержит
свойства IsFixedSize, IsReadOnly, Item и методы Add, Clear, Contains, IndexOf,
Insert, Remove, RemoveAt, смысл и действие которых был рассмотрен выше.
6.10 Нестатические свойства и методы класса Array
Наберите код chars. (с точкой). Список, предложенный IntelliSense, содержит
нестатические свойства и методы класса char[]. Большая часть из них знакома по
предыдущим примерам. Существование других
C.WriteLine(chars.GetLowerBound(0));
C.WriteLine(chars.GetUpperBound(0));
C.WriteLine(chars.GetValue(1));
chars.SetValue('d',1);
C.WriteLine(chars[1]);
//
//
//
//
нижняя граница индекса (всегда 0)?!
верхняя (всегда GetLength(0)-1)?!
То же, что вызвать chars[1]
То же,что набрать chars[1]=’d’
кажется излишним. Более глубокое знакомство с классом Array показывает, что это не
так.
Наберите код Array. (с точкой). Список, предложенный IntelliSense, содержит статические
методы класса Array. Среди них есть знакомые нам методы Equals и ReferenceEquals,
унаследованные от прямого предка – класса Object, но есть и методы, реализованные
классом Array.
6.11 Полиморфное конструирование массивов
Класс Array абстрактный, а у абстрактного типа не может быть объектов по определению.
Но у класса Array есть несколько версий статического метода CreateInstance, который
создает объекты классов-наследников класса Array. Элементами массивов, созданными
таким образом, являются объекты конкретного типа. Для указания типа у всех версий
метода CreateInstance есть параметр типа Type.
Наберите пример кода с использованием простейшей версии этого метода
C.WriteLine(Array.CreateInstance(typeof(int), 3).ToString());
C.ReadLine();
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
87
Результатом будет строка System.Int32[]– тип массива объектов типа int .
Оператор typeof создает экземпляр класса Type. Испытайте несколько разных типов
элементов массива, меняя int на object и т.д.
Применение метода CreateInstance обеспечивает полиморфизм при создании экземпляра
массива. Тип массива оказывается в этом контексте переменным объектом, значение
которого может меняться в процессе выполнения программы (run-time).
К примеру, наберите и проверьте работу кода
Type aType;
aType = null;
C.Write("Enter a number 1 or 2\x20");
switch (C.ReadLine())
{
case "1": aType = typeof(int); break;
case "2": aType = typeof(DateTime); break;
default: C.WriteLine("Wrong input!"); break;
}
C.WriteLine(Array.CreateInstance(aType, 3).ToString());
C.ReadLine();
Оператор C.ReadLine() в операторе switch вводит с консоли строку.
значения ("1" или "2") создается разный тип массива.
В зависимости от ее
6.12 оператор switch
«переключает» выполнение кода на одну из меток case («в случае») или default («во
всех других случаях») в зависимости от значения выражения, стоящего в круглых скобках
после служебного слова switch. Это выражение должно возвращать объект целого типа,
либо строку. В нашем случае метод Console.ReadLine() возвращает строку. Управление
 передается оператору, стоящему после метки case с совпадающим значением, либо
 при отсутствии метки case с совпадающим значением
o на метку default, если она присутствует (метка default не обязательна),
o оператору, следующему за блоком {} оператора switch, при отсутствии
default.
Обязательный оператор прерывания break, завершающий каждый «случай», передает
управление оператору, следующему за блоком {} оператора switch. В качестве оператора
прерывания может стоять также оператор return, завершающий выполнение текущего
метода.
6.13 Пример обработки исключительной ситуации
В рассмотренном примере метод ReadLine класса Console возвращает строку, введенную
с консоли. Если это 1, то типом aType элементов массива будет object; при вводе 2 - тип
DateTime; во всех остальных случаях объект aType остается null.
Проверьте работу кода в нескольких вариантах:
 в начале введите число 1, затем 2; посмотрите результаты;
 затем введите любую другую строку. Произойдет останов по ошибке.
Дело в том, что если параметр типа aType равен null, метод CreateInstance создает
объект исключительной ситуации (ИС) типа ArgumentNullException. Эту ИС наш код не
обработал – отсюда останов по ошибке.
Измените код, поставив вызов WriteLine «под» try…catch, чтобы обработать ИС
try {C.WriteLine(Array.CreateInstance(aType, 3).ToString());}
catch (ArgumentNullException e){C.WriteLine(e);}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
88
Повторите попытку со строкой, отличной от 1 и 2. Программа должна «нормально»
завершиться с выдачей исчерпывающей информации об ошибке.
Информация эта находится в параметре e типа ArgumentNullException блока «перехвата»
catch. Параметр e передается блоку catch методом CreateInstance, который создал
объект типа ArgumentNullException.
6.14 Условное выражение ?:
Можно избежать выхода на ИС. Наберите в качестве параметра метода WriteLine
условное выражение так, что строка кода с методом WriteLine примет вид
C.WriteLine(aType==null?"null":Array.CreateInstance(aType, 3).ToString());
Перед знаком ? находится выражение типа bool.

Если выражение возвращает true, то результирующим выражением (в данном
случае типа string) будет то, что находится между знаками ? и :. В данном случае
это строка "null".
 В случае false результатом будет выражение, стоящее после знака :. В данном
случае это строка Array.CreateInstance(aType, 3).ToString().
Проверьте последнюю версию кода.
6.15 Создание массивов с ненулевой базой
Еще одна версия кода
do
{
C.Write("Enter a number 1 or 2\x20");
aType = null;
switch (C.ReadLine())
{
case "1": aType = typeof(int); break;
case "2": aType = typeof(DateTime); break;
default: C.WriteLine("Wrong input!"); break;
}
}
while (aType == null);
Array anArray = Array.CreateInstance(aType, 3);
позволяет добиться от пользователя ввода нужной строки. Интерфейс ввода «замыкается»
циклом do…while, пока aType не получит осмысленного значения. Необходимость в
проверке значения объекта aType отпадает, поэтому можно без опасений создать
экземпляр anArray класса Array из элементов типа aType.
Проверьте работу кода, используя метод GetValue. (Оператор индексации []
использовать здесь нельзя - проверьте).
for (int i=0;i<anArray.Length;i++) C.WriteLine(anArray.GetValue(i));
В окне должны быть нули в случае 1 и даты Рождества Христова в случае 2.
Одна из версий метода CreateInstance позволяет создавать массив с индексом,
отсчитываемым от произвольного, ненулевого значения. Остальные версии предназначены
для создания многомерных массивов с обычной zero-based индексацией.
Замените две последние строки предыдущего кода кодом
int[] lengths={3},lowerbounds={5};
Array anArray = Array.CreateInstance(aType, lengths, lowerbounds);
for (int i = anArray.GetLowerBound(0); i <= anArray.GetUpperBound(0); i++)
C.Write("anArray[{0}]={1};\x20",i,anArray.GetValue(i));
Результатом должен быть массив длиной 3 с отсчетом индекса от 5.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
89
Обратите внимание, что использованная версия метода CreateInstance требует кроме
параметра типа aType еще два параметра:
 параметр lengths – массив типа int[] числа элементов для каждого измерения (в
нашем случае одного измерения это одномерный массив со значением элемента 3);
 параметр lowerbounds – массив типа int[] нижних значений индекса для каждого
измерения (в нашем случае одного измерения это одномерный массив со
значением элемента 5).
6.16 Методы инициализации, очистки и копирования массивов
Инициализация
Усовершенствуйте код внутри метода Main:
 все операторы (кроме описания Type aType;) поместите внутрь «бесконечного
цикла» do…while (true)
 добавьте «случай» case "exit": return; в переключатель switch для
завершения метода Main (и приложения в целом) вводом строки exit.
Type aType;
do
{
do
{
C.Write("Enter a number 1 or 2 or exit to exit\x20");
aType = null;
switch (C.ReadLine())
{
case "1": aType = typeof(int); break;
case "2": aType = typeof(DateTime); break;
case "exit": return;
default: C.WriteLine("Wrong input!"); break;
}
}
while (aType == null);
int[] lengths={3},lowerbounds={5};
Array anArray = Array.CreateInstance(aType, lengths, lowerbounds);
for (int i = anArray.GetLowerBound(0); i <= anArray.GetUpperBound(0); i++)
C.Write("anArray[{0}]={1};\x20",i,anArray.GetValue(i)); C.WriteLine();
}
while (true);
Это позволит «изучать» разные «случаи», не прерывая приложения. Испытайте код.
Для инициализации объекта anArray можно использовать несколько версий метода
SetValue. Добавьте после строки с описанием массива anArray код
int k = 1;
foreach (object o in anArray)
{
int index=Array.IndexOf(anArray, o);
anArray.SetValue(aType == typeof(int) ? (object)(k *= 3)
: (object)DateTime.Now,index);
C.WriteLine(anArray.GetValue(index));
}
инициализирующий значения элементов массива, и проверьте его работу.
Обратите внимание в этом коде
 на полиморфное (типа object) перечисление объектов массива с помощью
оператора foreach,in;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
90




на использование статического метода IndexOf класса Array. Существует
несколько версий (в том числе и полиморфных) метода IndexOf, возвращающих
индекс первого найденного элемента массива во всем массиве или в его заданном
сегменте. Аналогичные версии метода LastIndexOf возвращают индекс последнего
найденного элемента.
На преобразование первого параметра метода SetValue к типу object;
на использование оператора комбинированного присвоения k*=3. Это краткая
форма записи оператора k = k * 3. Такие комбинации присвоения возможны с
любыми арифметическими и логическими операциями += -= *= /= %= &= |= ^=.
На то, что оператор присвоения, кроме своей прямой функции - устанавливать
значение объекта в левой части, возвращает присвоенное значение.
Очистка
Очистить участок массива можно статическим методом
public static void Clear(Array array, int index, int length);
Он позволяет обнулить length элементов массива array, начиная с индекса index.
Наберите пример
int[,] @is = new int[3, 2] { {1, 2 }, {3, 4 }, {5, 6 } };
Array.Clear(@is, 3, 2);
foreach (int i in @is)
C.Write(i.ToString() + "\t");
Слово is является служебным в C#, поэтому для использования is в качестве
идентификатора добавляется префикс @. Посмотрите результат. Обратите внимание на то,
что очищаются 2 элемента, начиная с индекса 3 (с первоначальными значениями 4 и 5),
как если бы двумерный массив был вытянут в одномерный массив по строкам.
Копирование
Несколько версий метода Copy и метод ConstrainedCopy позволяют копировать участок
массива в заданное место другого массива. Методы Copy обеспечивают автоматическое
преобразование типов копируемых элементов. Метод ConstrainedCopy требует
совпадения типов элементов копируемых массивов, но при возникновении ИС
восстанавливает первоначальную версию массива, изменявшегося при копировании.
К прежнему примеру с массивом @is добавьте код с использованием одной из версий
метода Copy, в которой 2 элемента из массива @is, начиная с индекса 4, копируются в
массив objects, начиная с индекса 5.
object[,] objects = new object[3,6];
Array.Copy(@is, 4, objects, 5, 2);
for (int i=0;i<objects.GetLength(0);i++)
for (int j=0;j<objects.GetLength(1);j++)
{
if (objects[i,j] == null) continue;
C.Write("objects[{0},{1}]={2}\t", i,j, objects[i,j].ToString());
}
Проанализируйте результат.
6.17 Операторы continue и break
Обратите внимание, что отсутствие осмысленной инициализации элементов массива
objects приводит к тому, что лишь два его элемента [0,5] и [1,0] обладают
конкретными значениями 0 и 6, полученными из массива @is. Оператор continue языка
C# в данном случае допускает к печати лишь элементы массива objects с осмысленными
значениями. Все операторы, следующие за continue в блоке любого цикла, опускаются.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
91
Другой оператор break прерывает выполнение цикла. В предыдущем копировании можно
прервать цикл по достижении i=1, j=0, добавив внутри цикла по j код
if (i == 1 && j == 0) break;
Однако прерывается лишь внутренний цикл по j, в котором стоит оператор break.
Цикл по i продолжается. Проверьте, добавив внутрь цикла по i (но вне цикла по j)
оператор C.WriteLine("i={0}",i);. Конечно, можно прервать и выполнение внешнего
цикла по i. Для этого надо зафиксировать условие прерывания i == 1 && j == 0 в
какой-нибудь локальной переменной типа bool. Опишите, например, вне циклов
переменную bool isDone = false;
Условие внутри цикла по j измените к виду
if (isDone = i == 1 && j == 0) break;
Сразу после внутреннего цикла по j, но внутри
if (isDone) break;
цикла по i, добавьте оператор
Этого будет достаточно. Проверьте.
6.18 Метод сортировки элементов массива
Метод Sort имеет много версий.
Наберите в методе Main пример кода
char[] chars = new char[3] { 'b', 'c', 'a' };
Array.Sort(chars); C.WriteLine();
foreach (char ch in chars) C.Write("{0}\t", ch);
Убедитесь, что символы расположились в «правильном» порядке.
Есть полиморфные и обычные версии метода Sort. Есть версии, позволяющие
сортировать отдельные сегменты массива и сортировать пару массивов «ключ-значение»
(key-value) по массиву ключа. Одни версии метода Sort используют стандартную
реализацию классами-элементами массива метода CompareTo интерфейса IComparable.
Другие версии Sort в качестве одного из параметров используют интерфейс
System.Collections.IComparer с его методом Compare(object x,object y),
позволяющим сравнивать пару объектов x и y.
Предположим, что необходимо отсортировать числа типа double по их модулям.
Наберите в пространстве имен ArrayTestProject (вне или внутри описания класса
ArrayTest) код нового класса aComparer, реализующего интерфейс IComparer нужным
образом
class aComparer : System.Collections.IComparer
{
public int Compare(object x, object y)
{
return x.GetType() == typeof(double)?
Math.Abs((double)x).CompareTo(Math.Abs((double)y)):
((IComparable)x).CompareTo(y);
}
}
Обратите внимание на то, как внутри метода Compare используется метод CompareTo
интерфейса IComparable.
Внутри метода Main класса ArrayTest наберите код, использующий метод Sort и класс
aComparer.
double[] doubles = new double[4] {-3, 0.7, 0,-0.5 };
Array.Sort(doubles, new aComparer());
foreach (double d in doubles) C.Write("{0}\t", d);
Посмотрите результат. Сравните со стандартной сортировкой, убрав второй параметр в
методе Sort в этом коде.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
92
Вопросы для самоконтроля
1. Абстрактный класс vs интерфейс.
2. Описание и инициализация одномерного массива.
3. Размер и размерность массива.
4. Синтаксис условного оператора if…else.
5. Оператор typeof vs метод GetType.
6. Тип индекса массива.
7. Описание и инициализация многомерного прямоугольного массива.
8. Описание и инициализация «зубчатого» массива.
9. Что означает наследование интерфейсом другого интерфейса?
10. Синтаксис условного оператора switch.
11. Синтаксис условного выражения.
12. Синтаксис и действия комбинированных операторов присваивания.
13. Действие операторов continue и break внутри цикла.
Занятие 7. Классы делегатов (delegate)
Одна из полиморфных версий метода Sort имеет описание
public static void Sort<T>(T[] array, Comparison<T> comparison);
Эта версия сортирует массив по данным делегата System.Comparison<T> с описанием
public sealed delegate int Comparison<T>(T x, T y);
Служебное слово delegate определяет в C# специальный класс - наследник абстрактного
класса System.Delegate. Каждый делегат определяет тип методов (их сигнатуру),
делегирование которых он способен взять на себя. Описание делегата состоит в
описании типа метода. При этом имя метода, приведенное в описании, становится
именем делегата как типа класса.
В случае Comparison делегат указывает, что он может делегировать методы со следующей
сигнатурой: это полиморфные методы с параметром-типом T, возвращающие целое число
типа int и имеющие два параметра по значению типа T.
7.1 Описание и инициализация делегатов
Опишите делегат, делегирующий более простые методы. Например, согласно описанию
delegate void aDlgtType();
делегаты типа aDlgtType делегируют
методы, не имеющие параметров и не
возвращающие каких-либо объектов (void).
Наберите эту строку кода вне или внутри класса ArrayTest (но, вне метода Main!).
Конструктор объекта любого делегата имеет один параметр - ссылку на метод,
делегируемый классом данному объекту-делегату.
Выглядеть это может следующим образом. Внутри класса ArrayTest (но, вне метода
Main!) наберите код с описанием метода ToDo и поля aDlgt описанного выше типа
aDlgtType. Поле aDlgt инициализируется методом ToDo с помощью конструктора своего
класса aDlgtType.
static void ToDo()
{
C.WriteLine("ToDo's called");
}
static aDlgtType aDlgt = new aDlgtType(ToDo);
Объект aDlgt описан как поле класса ArrayTest. Он
при вызове конструктора new aDlgtType(ToDo).
инициализируется значением ToDo
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
93
Возможен сокращенный вариант инициализации объекта делегата без явного указания
вызова конструктора:
static aDlgtType aDlgt = ToDo;
Транслятор C# восстанавливает (infer) пропущенную часть кода.
Метод ToDo является методом класса ArrayTest. Поэтому класс ArrayTest является
данном случае делегирующим классом. Он делегирует свой метод ToDo какому-то
в
другому, делегируемому классу.
Делегируемый класс вызывает метод, полученный им от делегата того же типа.
Наберите код, описывающий пример делегируемого класса aClass
class aClass
{
internal aDlgtType onDoSmth;
internal void DoSmth()
{
//Выполняются некоторые действия
if (onDoSmth != null) onDoSmth();
}
}
Описание класса aClass следует разместить там, откуда «виден» тип делегата aDlgtType:
внутри класса ArrayTest, если aDlgtType описан внутри, и вне ArrayTest в противном
случае. (Описания поля aDlgt и метода ToDo, конечно, находятся внутри класса
ArrayTest).
Поле onDoSmth класса aClass имеет тип делегата aDlgtType и модификатор internal,
обеспечивающий доступ внешним объектам (но находящимся внутри сборки!). Метод
DoSmth класса aClass выполняет какие-то действия и при этом вызывает объект делегата
onDoSmth, если этот делегат существует (!= null).
Теперь наберите код внутри метода Main класса ArrayTest
//Создается экземпляр (объект) anInst делегируемого класса aClass
aClass anInst=new aClass();
//Полю onDoSmth объекта anInst присваивается поле aDlgt делегирующего класса
anInst.onDoSmth = aDlgt;
//Вызывается метод делегируемого класса, внутри которого вызывается делегат
anInst.DoSmth();
Этот код приводит к общению делегирующего класса ArrayTest и делегируемого класса
aClass посредством объектов-делегатов aDlgt и onDoSmth типа aDlgtType.
Проверьте работу кода.
Сокращенный вариант общения позволяет обойтись явной записью тела делегируемого
метода при присвоении его полю делегируемого класса (так называемый, «анонимный»
(anonymous) метод). А именно, вместо оператора присвоения
anInst.onDoSmth = aDlgt;
можно набрать
anInst.onDoSmth = delegate {C.WriteLine("ToDo's called");};
Тогда необходимость в описании поля aDlgt делегирующего класса ArrayTest и самого
делегируемого метода ToDo пропадает. Тело метода ToDo явно передается полю onDoSmth
объекта anInst делегируемого класса aClass. Проверьте новую версию кода.
Объекты типа делегат могут запоминать любое количество методов того типа, который
определен типом делегата. В нашем примере это тип aDlgtType, возвращающий void и
не имеющий параметров. Методы ставятся в очередь друг за другом, образуя цепочку
последовательно выполняемых методов. Для примера, наберите в классе ArrayTest еще
один делегируемый метод
static void ToDoMore()
{
C.WriteLine("ToDoMore's called");
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
94
Добавьте к уже имеющемуся коду в методе Main новую строку
anInst.onDoSmth += ToDoMore;
Посмотрите результат.
Методы делегата «прицепляются» оператором +=, дающим сокращение записи от
выражения (попробуйте)
anInst.onDoSmth = anInst.onDoSmth + ToDoMore;
а «отцепляются» оператором -=. Естественно, всегда остается
цепочку сразу, присвоив делегату null. Попробуйте.
возможность убрать всю
Анонимность делегируемого метода возможна и в процессе формирования цепочки.
Замените в последней версии кода вызов метода ToDoMore кодом
delegate { C.WriteLine("ToDoMore's called");} и испытайте результат.
7.2 Контравариантность и ковариантность
Обычно делегируемый метод должен иметь в точности ту сигнатуру (возвращаемый тип и
типы и число параметров), которая указана в описании делегата. Однако, возможны
расширения.
Хотя число параметров делегируемого метода должно совпадать с делегатом, но типы
параметров делегируемого метода могут быть классами-предками классов
соответствующих параметров в описании делегата. Например, пусть описаны два
класса
class aBase
{
}
class aDer : aBase
{
}
Если тип делегата описан как
delegate void dt(aDer id);
то делегируемым методом может быть метод
static void m(aBase ib)
{
}
где класс aBase является предком класса aDer.
Такой метод называется
контравариантным.
Проверьте. Наберите внутри класса ArrayTest весь этот код, а затем в методе Main строку
dt idt = new dt(m);
Компиляция должна пройти без ошибок. Но, если переставить в описаниях dt и m классы
aBase и aDer, компиляция не состоится.
Тип объекта, возвращаемого делегируемым методом, может быть наследником класса
объекта, возвращаемого типом делегата. В этом случае делегируемый метод называется
ковариантным. Например, опишите тип делегата и метода в виде
delegate aBase dt();
static aDer m()
{
return null;
}
Описание классов aBase и aDer, а также код
dt idt = new dt(m);
в методе Main оставьте без изменений. Проведите
компиляцию кода. Она должна пройти
без ошибок.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
95
7.3 Полиморфные делегаты
Вернемся к рассмотрению метода Array.Sort и к его полиморфной версии, ссылающейся
на полиморфного делегата типа Comparison<T>.
Опишите полиморфный метод AbsComparison<T> в классе ArrayTest, и перенесите в
него код из метода Compare, составленного раньше для класса aComparer
static int AbsComparison<T>(T x, T y)
{
return x.GetType() == typeof(double) ?
Math.Abs((double)(object)x).CompareTo(Math.Abs((double)(object)y)) :
((IComparable)x).CompareTo(y);
}
Единственное отличие от кода метода Compare состоит в двойном преобразовании типов
(double)(object) параметров x и y. Это необходимо в виду полной неопределенности их
типа T. Без такого преобразования типов компилятор не пропустит код – проверьте.
Метод AbsComparison<T> будет делегироваться в качестве параметра метода Sort. В
методе Main класса ArrayTest наберите код, вызывающий полиморфную версию метода
Sort<T> в случае T, равном double, для уже использовавшегося массива doubles и
делегата типа Comparison<T>.
double[] doubles = new double[4] { -3, 0.7, 0, -0.5 };
Array.Sort<double>(doubles, new Comparison<double>(AbsComparison<double>));
foreach (double d in doubles) C.Write("{0}\t", d);
Проверьте его работу.
Это полная версия кода, формирующего объект типа делегата Comparison<double>,
делегирующий метод AbsComparison<double>. Код можно упростить
 можно не указывать тип <double> при вызове метода AbsComparison;
 можно опустить вызов конструктора new Comparison<double>, а прямо обратиться
с делегируемым методом
Array.Sort<double>(doubles, AbsComparison);

можно описать и использовать не полиморфную версию метода в виде
static int AbsComparison(double x,double y)
{
return Math.Abs(x).CompareTo(Math.Abs(y));
}

можно, наконец, дать анонимный код делегируемого метода в его конкретном для
типа double варианте
Array.Sort<double>(doubles, delegate (double x,double y)
{ return Math.Abs(x).CompareTo(Math.Abs(y));});
и тем самым вовсе не описывать метод AbsComparison<T>.
Проверьте все варианты.
7.4 Методы поиска элементов массива
Несколько версий статического метода BinarySearch класса Array ищут в заданном
отсортированном по возрастанию одномерном массиве (или в его заданном сегменте)
заданный объект, пользуясь бинарным алгоритмом поиска. Все версии BinarySearch
возвращают индекс найденного объекта или отрицательное целое число, если поиск
оказался безуспешным. Бинарное дополнение отрицательного результата (целое, в
котором нули заменены единицами и наоборот, что соответствует операции отрицания ~ в
C#) дает индекс первого объекта превышающего искомый. Если массив не отсортирован,
ИС не возникает, но результат может быть не верным.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
96
Наберите код, в котором используются две версии метода Sort, прежний массив doubles
и класс aComparer, упорядочивающий по модулю, также использованный в предыдущем
примере
Array.Sort(doubles, new aComparer());// Сортировка по модулям
foreach (double d in doubles) C.Write("{0}\t", d);
C.WriteLine();C.WriteLine(~Array.BinarySearch(doubles,-0.6,new aComparer()));
Array.Sort(doubles);
// Сортировка по умолчанию(по значениям)
foreach (double d in doubles) C.Write("{0}\t", d);
C.WriteLine(); C.WriteLine(Array.BinarySearch(doubles, -3.0));
Проанализируйте результат. Обратите внимание, что если в последней строке задать
значение искомого элемента в виде -3, а не -3.0, метод BinarySearch возбудит ИС из-за
несовпадения типов (постоянная -3 имеет тип int, а не double, как требуется).
Проверьте.
Полиморфный метод
public static bool Exists<T> ( T[] array, Predicate<T> match);
возвращает true или false в зависимости от наличия среди элементов массива array
таких, которые удовлетворяют условию, формируемому делегатом match типа
public delegate bool Predicate<T> (T obj);
Проверьте работу метода Exists<T> с помощью строки кода
C.WriteLine(Array.Exists<double>(doubles,delegate (double d){return d<0;}));
варьируя условие, формируемое предикатом.
Метод TrueForAll<T> проверяет, все ли элементы массива удовлетворяют условию
предиката. Замените Exists на TrueForAll в последней строке кода, чтобы увидеть
работу метода.
В отличие от Exists<T> метод Find<T> возвращает первый найденный элемент,
удовлетворяющий условию предиката. При отсутствии искомого элемента метод Find<T>
возвращает значение по умолчанию типа T (0, false или null в зависимости от типа T).
Замените в последней строке кода TrueForAll на Find. Проверьте результат.
Аналогично работают методы поиска с говорящими названиями FindAll<T>,
FindLast<T>, FindIndex<T> и FindLastIndex<T>. Наберите, к примеру, код
double[] ds=Array.FindAll<double>(doubles,delegate(double d){return d<0;});
foreach (double d in ds) C.Write("{0}\t", d);
Он иллюстрирует работу метода FindAll<T>.
7.5 Методы преобразования массивов
Обращение (revers)
Две версии статического метода Reverse класса Array позволяют переставлять элементы
всего одномерного массива или его части, обращая их последовательность. Наберите код
char[] chars = new char[4] { 'a', 'b', 'c', 'd' };
Array.Reverse(chars, 1, 2);
foreach (char ch in chars) C.Write("{0}\t", ch);
Убедитесь, что 2-ой и 3-ий элементы обратили свое расположение.
Изменение длины массива
Полиморфный метод
public static void Resize<T>(ref T[] array, int newSize);
меняет длину одномерного массива array элементов типа T на значение newSize,
уничтожая прежний массив array и копируя в новый массив элементы прежнего.
Испытайте код
Array.Resize<double>(ref doubles, 5);
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
97
foreach (double d in doubles) C.Write("{0}\t", d);
предлагая разные значения второго параметра – размера нового массива.
Обратите внимание, что массив doubles передается методу Resize по ссылке (reference).
Преобразование типов элементов массива
Полиморфный метод
public static TOutput[] ConvertAll<TInput, TOutput >( TInput [] array,
Converter< TInput, TOutput > converter);
преобразует тип TInput всех элементов массива array в тип TOutput.
При этом используется делегат converter типа
public delegate TOutput Converter< TInput, TOutput >( TInput input);
преобразующий каждый элемент input массива из типа TInput в тип TOutput.
Наберите код
int[] ints = Array.ConvertAll<double, int>
(doubles, delegate(double input) { return (int)Math.Ceiling(input); });
foreach (double d in doubles) C.Write("{0}\t", d); C.WriteLine();
foreach (int i in ints) C.Write("{0}\t", i);
и посмотрите результат его работы.
Произвольные действия над всеми элементами массива
Полиморфный метод
public static void ForEach<T> (T[] array, Action<T> action);
для каждого элемента массива array выполняет действие, определяемое делегатом action
типа
public delegate void Action<T> (T obj);
для каждого элемента obj массива.
Наберите код
Array.ForEach<double>(doubles, delegate(double d) { C.Write("{0}\t", d); });
и посмотрите результат работы метода ForEach<T> при данном действии. Тот же
результат получится, если операторы делегата action выполнить в цикле foreach,in для
всех элементов массива array.
Преобразование к списку «только для чтения»
Все массивы имеют неизменное свойство IsReadOnly, возвращающее всегда false.
Попытаемся написать код, делающий элементы массива доступными только для чтения.
Наберите код класса ReadOnlyArray вне или внутри класса ArrayTest.
class ReadOnlyArray
{
double[] doubles = new double[4] { -3, 0.7, 0, -0.5 };
public double this[int index] { get { return doubles[index]; } }
}
В классе ReadOnlyArray описан массив doubles с доступом private (по умолчанию) и
индексатор this с неограниченным доступом public. Индексатор имеет только метод
get, поэтому с его помощью можно читать отдельные элементы массива doubles, но не
менять их. Проверьте это кодом в методе Main
ReadOnlyArray roa = new ReadOnlyArray();
roa[0]=10; // Эта строка будет отвергнута компилятором:
// индексатор – только для чтения
Однако каждый элемент массива doubles можно прочесть из памяти. Попробуйте
оператор
C.WriteLine(roa[0]);
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
98
Казалось бы, задача решена – элементы массива doubles можно считывать, но нельзя
менять. Но в этой версии недоступны свойства самого массива doubles, например его
длина Length.
Можно ввести свойство «только для чтения» Doubles с неограниченным доступом,
набрав в классе ReadOnlyArray дополнительный код
public double[] Doubles { get { return doubles; } }
Но это был бы ошибочный ход.
Дело в том, что массив относится к ссылочным типам. Получив через свойство Doubles
доступ к ссылке на массив doubles, пользователь получает неограниченный доступ к
самим элементам массива doubles. Проверьте это. Добавьте свойство Doubles к классу
ReadOnlyArray и код
roa.Doubles[0]=10; C.WriteLine(roa[0]);//Элемент 0 изменился
к методу Main.
Чтобы решить задачу, следует воспользоваться методом
public static IList<T> AsReadOnly<T> ( T[] array);
который обеспечивает доступ к одномерному массиву array объектов типа T через объект
интерфейса IList<T>. Метод AsReadOnly устанавливает свойство IsReadOnly «объектаоболочки» IList<T> в true – элементы списка можно только читать, но не изменять.
Если массив передается пользователю только через оболочку в виде такого объекта, то
доступ к элементам массива оказывается ограниченным чтением (read-only).
Проверьте это. Для этого добавьте свойство типа IList<double> к классу ReadOnlyArray
public IList<double> roDbls
{ get { return Array.AsReadOnly<double>(doubles); } }
и код, использующий это свойство, к методу Main.
for (int i = 0; i < roa.roDbls.Count; i++)
C.Write("Item[{0}]={1}\t", i, roa.roDbls[i]);
try { roa.roDbls[0] = 1; }
catch (Exception e) { C.WriteLine("\n"+e); };
(Предыдущие коды – описание индексатора this и свойства Doubles, а также операторы,
использующие эти свойства в методе Main, можно убрать, чтобы не отвлекаться).
Проанализируйте код и его результат. Задача решена. Метод AsReadOnly<T> возвращает в
данном случае объект интерфейса IList<double>, в который «завернут» (wrap) массив
doubles. Свойство Length массива доступно через свойство Count объекта типа
IList<double>.
Вопросы для самоконтроля
1.
2.
3.
4.
5.
6.
Описание и инициализация делегатов.
Делегирующий класс vs делегируемый класс.
Анонимное делегирование.
Как делегировать несколько методов?
Контравариантность делегируемого метода.
Ковариантность делегируемого метода.
Занятие 8. Коллекции, списки, очереди, словари
Это классы библиотеки .NET, которые обеспечивают хранение и выборку различных
типов данных. Большая часть этих классов реализуют интерфейсы ICollection,
IComparer, IEnumerable, IList, IDictionary и IDictionaryEnumerator и их
полиморфные эквиваленты.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
99
Близкие по смыслу элементы данных удобнее объединять в коллекции, вместо того, чтобы
писать отдельный код для манипуляций с каждым из элементов отдельно. Коллекции
позволяют добавлять и устранять как отдельные элементы, так и их множество.
Некоторые коллекции могут сортироваться по разным признакам и многие из них
являются индексированными, как массивы. Память, занимаемая коллекцией, управляется
автоматически и при необходимости расширяется. Синхронизация обеспечивает
независимость доступа к коллекциям из разных подпроцессов (нитей).
8.1 Класс Collection<T>
реализует методы и свойства всех рассмотренных выше интерфейсов ICollection, IList
и IEnumerable, включая их полиморфные аналоги, и не добавляет своих собственных
членов (во всяком случае, с доступом public).
Войдите в окно Solution Explorer; оттуда командами Add и New Project… в окно Add New Project;
выберите шаблон Console Application, дайте имя новому проекту CllctnTestProject и нажмите
кнопку OK. После этого, в окне Solution Explorer щелкните правой кнопкой над именем
образовавшегося файла Program.cs и командой Rename из контекстного меню измените его
имя на CllctnTest.cs. Одновременно должно измениться имя шаблона класса,
предоставленного средой в этом файле. Наконец, в окне Solution Explorer щелкните правой
кнопкой над именем проекта CllctnTestProject и выберите команду Set as StartUp Project.
В дальнейшем мы будем работать, в основном, в методе Main класса CllctnTest.
Добавьте к списку директив using в начале файла CllctnTest.cs строки
using System.Collections.ObjectModel;
using C=System.Console;
В пространстве имен System.Collections.ObjectModel находится интересующий нас
класс Collection<T>, а вторая строка сокращает запись вызовов методов класса Console.
Внутри метода Main наберите код
C.WriteLine("\t\tCollection");
Collection<object> co = new Collection<object>();
C.WriteLine("IsReadOnly={0};IsFixedSize={1}",
((ICollection<object>)co).IsReadOnly,
((System.Collections.IList)co).IsFixedSize);
C.ReadLine();
и проверьте его работу.
В отличие от класса Array, свойства IsReadOnly и IsFixedSize класса Collection<T>
всегда возвращают false. Существует отдельный класс ReadOnlyCollection, свойство
IsReadOnly которого возвращает true.
Если набрать часть кода Collection<object>. (с точкой), то список, воспроизведенный
IntelliSense, покажет только два статических метода Equals и ReferenceEquals,
унаследованных классом Collection<T> от своего прямого предка Object. В списке,
появляющемся после набора объекта co. (с точкой), находятся знакомые нам:
 свойство Count и методы Add, Clear, Contains, CopyTo и Remove интерфейса
ICollection<T>;
 методы IndexOf, Insert и RemoveAt интерфейса IList<T>;
 метод GetEnumerator интерфейса IEnumerable<T>
 и нестатические методы Equals, GetHashCode, GetType и ToString, наследуемые от
класса Object.
Кроме того, класс Collection<T> реализует оба метода set и get индексатора
интерфейса IList<T>.
Добавьте код, иллюстрирующий работу некоторых из перечисленных методов и свойств
//Добавить в коллекцию “co” объект с max 4-байтовым целым
co.Add(int.MaxValue);
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
100
//Распечатать последний элемент коллекции “co”
C.WriteLine("После Add:co[{0}]={1}\nПосле замены:co[{0}]={2}",
co.Count - 1, co[co.Count - 1],
/*Заменить первый элемент коллекции “co” на объект NaN
(Not-a-Number – отсутствие числового значения) типа double
и распечатать его: */ co[co.Count - 1]=double.NaN);
//Добавить в “co” объекты с минимальными значениями типов decimal и double
co.Add(decimal.MinValue); co.Add(double.MinValue);
//Вставить перед объектом с индексом 2 объект с отрицательной ∞ типа double
co.Insert(2, double.NegativeInfinity);
C.WriteLine("\tПосле Add и Insert");
foreach (object o in co)//Распечатать все объекты коллекции “co”
C.WriteLine("{0}\t", o.ToString());
//Убрать из коллекции “co” ортицательное максимальное число типа decimal
co.Remove(decimal.Negate(decimal.MaxValue));
C.WriteLine("\tПосле Remove");
foreach (object o in co) //Распечатать все объекты коллекции “co”
C.WriteLine("{0}\t", o.ToString());
Обратите внимание также на интересные статические свойства классов double, decimal
int, использованные в этом примере.
Класс Collection<T> имеет две версии конструктора: использованный в примере
конструктор без параметров, и конструктор с параметром типа IList<T> - коллекция
и
заполняется объектами из списка-параметра.
8.2 Список List<T>
есть сочетание классов Collection<T> и Array. У полиморфного списка
 есть методы и свойства класса Collection<T>, рассмотренные выше;
 есть свойство Capacity, которое возвращает и устанавливает предельное число
объектов в списке;
 есть методы, подобные методам класса Array:
o методы поиска Exists, BinarySearch, Find, FindLast, FindAll, FindIndex,
FindLastIndex;
o ряд других методов ForEach, ConvertAll, AsReadOnly, IndexOf,
LastIndexOf, Revers, Sort, TrueForAll;
 есть методы, специфические для класса List<T>: InsertRange, RemoveRange,
GetRange, RemoveAll, TrimExcess, ToArray.
Класс List<T> имеет три версии конструктора – без параметров, с параметром типа
IEnumerable<T> и с параметром, устанавливающим Capacity. Объект типа List<T>,
созданный первым и третьим типом конструктора, имеет пустой список, но в первом
случае Capacity имеет значение 4 по умолчанию.
Наберите код в продолжение рассмотренного выше кода для коллекции co.
List<object> lo = new List<object>(co);
C.WriteLine("\n\t\tList\nIsReadOnly={0};"+
"IsFixedSize={1};Capacity={2};Count={3}",
((ICollection<object>)lo).IsReadOnly,
((System.Collections.IList)lo).IsFixedSize,lo.Capacity,lo.Count);
Здесь используется вторая версия конструктора с параметром типа IEnumerable<T>. Этот
интерфейс является одним из предков класса Collection<T>, поэтому объект co типа
Collection<T> является одновременно объектом типа-предка IEnumerable<T>.
Результат показывает, что списки, как и коллекции, имеют свойства IsReadOnly и
IsFixedSize, установленные в false, и что значения Capacity и Count совпадают при
данной инициализации объекта.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
101
Наберите код, иллюстрирующий работу некоторых методов объекта типа List<object>
lo.AddRange(co);
// Добавляется коллекция в конец списка
//Распечатка списка
C.WriteLine("\tПосле AddRange");
foreach (object o in lo) C.WriteLine("{0}\t", o.ToString());
C.WriteLine("Capacity={0};Count={1}", lo.Capacity,lo.Count);
lo.InsertRange(1, co);// Вставляется коллекция перед объектом с индексом 1
C.WriteLine("\tПосле InsertRange");
for (int i = 0; i < lo.Count;i++)
C.WriteLine("{0}\t", lo[i].ToString());
C.WriteLine("Capacity={0};Count={1}", lo.Capacity, lo.Count);
lo.TrimExcess();
// Значение Capacity уменьшается до значения Count
C.WriteLine("После TrimExcess:Capacity={0};Count={1}",lo.Capacity,lo.Count);
object[] os = lo.ToArray();
// Список преобразуется в массив
for (int i = 0; i < os.Length; i++)// Массив печатается
C.WriteLine("{0}\t", os[i].ToString());
Проанализируйте результат работы кода.
В пространстве имен System.Collections существует не полиморфный аналог класса
List<T> – класс ArrayList.
8.3 Очередь Queue<T>
описывает методы и свойства работы с очередями типа FIFO (first in, first out – первый вошел,
первый вышел). Очередь Queue<T> подобна трубе – объекты типа T ее заполняющие
добавляются с одной стороны и извлекаются с другой. Класс Queue<T> реализует
интерфейсы ICollection, IEnumerable, но не интерфейс IList. Поэтому очереди не
индексированы.
Как и List<T>, класс Queue<T> имеет три версии конструкторов. Свойство допустимого
числа элементов Capacity класса Queue<T> непосредственно не доступно пользователю,
но может задаваться одной из версий конструктора и уменьшаться методом TrimExcess.
Значение Capacity по умолчанию равно 8. Специфическими для Queue<T> являются
методы Enqueue, Dequeue и Peek.
В продолжение кода, имеющегося в методе Main, наберите код, иллюстрирующий работу
с объектом qo класса Queue<T>
Queue<object> qo = new Queue<object>(lo);// Очередь “qo” создана из списка lo
C.WriteLine("\n\tQueue\nCount={0};First in queue={1}", qo.Count,
qo.Peek());
// Читает первый объект в очереди
qo.Enqueue(DateTime.MaxValue);//В конец очереди добавляется max время
C.WriteLine("\tПосле Enqueue");
foreach (object o in qo) C.WriteLine("{0}\t", o.ToString());//Распечатка
//Dequeue освобождает и возвращает объект из начала очереди
C.WriteLine("Dequeued:{0}",qo.Dequeue());
C.WriteLine("\tПосле Dequeue");
foreach (object o in qo) C.WriteLine("{0}\t", o.ToString());
Посмотрите результат. Существует не полиморфный аналог - класс Queue.
8.4 Очередь Stack<T>
реализует те же интерфейсы, что класс Queue<T>, имеет те же три версии конструктора, а
аналогами методов Enqueue и Dequeue в нем являются методы Push и Pop. Класс Stack<T>
- это очередь, работающая по принципу FILO (first in, last out – первый вошел, последний
вышел).
Наберите код, работающий с объектом so класса Stack<object>
Stack<object> so=new Stack<object>(qo);//Создание стека “so” из очереди “qo”
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
102
C.WriteLine("\n\tStack\nCount={0};First in stack={1}",so.Count, so.Peek());
so.Push(char.MaxValue);//В стек «заталкивается» max символ как 1-ый объект
C.WriteLine("\tПосле Push");
foreach (object o in so)
C.WriteLine("{0}\t", o.ToString());//Распечатка стека
//Из стека «выталкивается» первый объект
C.WriteLine("\nPopped:{0}", ((int)(char)so.Pop()).ToString());
C.WriteLine("\tПосле Pop");
foreach (object o in so) C.WriteLine("{0}\t", o.ToString());
Просмотрите результат.
Классы Queue<T> и Stack<T> не имеют индексаторов, но их элементы являются
перечислимыми:
 Во-первых, каждый из них реализует интерфейс IEnumerable как в обычной, так и
в полиморфной версии. Это явные реализации.
 Во-вторых, метод GetEnumerator классов List<T>, Queue<T> и Stack<T>
возвращает объект вложенной (nested) в каждый из классов структуры Enumerator.
Наберите внутри или вне класса CllctnTest новый класс IndexedStack<T> - наследник
класса Stack<T>. Класс IndexedStack<T> имеет индексатор от вершины стека, доступный
для чтения.
class IndexedStack<T> : Stack<T>
{
// Конструкторы наследника должны обязательно перекрывать все кон-ры предка
// Служ.слово base вызывает конструктор предка
internal IndexedStack():base(){}
internal IndexedStack(int capacity):base (capacity){ }
internal IndexedStack(IEnumerable<T> collection):base (collection) { }
internal T this[int index]// Описание индексатора
{
get
{
if (Count <= 0)
// Создается объект ИС при попытке прочесть элемент из пустого стека
throw new MemberAccessException("Stack "+ToString()+" is empty");
if (index < 0 || index >= Count)
// Создается объект ИС при попытке прочесть элемент вне допустимого интервала
throw new IndexOutOfRangeException(String.Format(
"index={0} is out of range:[0;{1}]", index, Count - 1));
int i = Count;
// Инициализируется счетчик
Enumerator enm = GetEnumerator();// Создается нумератор
// Цикл просмотра всех объектов до нужного индекса
while (enm.MoveNext() && index < --i) ;
return enm.Current;
}
}
}
Вслед за распечаткой стека в предыдущем примере в метод Main класса CllctnTest
поместите код, тестирующий новый класс IndexedStack<T>
// Создается индексированный экземпляр ins,в который помещается очередь so
IndexedStack<object> ins = new IndexedStack<object>(so);
// Объекты экземпляра ins распечатываются
C.WriteLine("\tIndexedStack");
for (int i = 0; i < ins.Count; i++)
C.WriteLine("ins[{0}]={1}\t", i, ins[i]);
// Попытка обратиться к объекту вне интервала
try { object o = ins[ins.Count];}
catch (IndexOutOfRangeException e)
{
C.WriteLine("\tПосле попытки обратиться вне интервала");
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
103
C.WriteLine("\n" + e);
// Создается пустой индексированный экземпляр ins0
IndexedStack<object> ins0 = new IndexedStack<object>();
// Попытка обратиться к объекту из пустого стека
try { object o = ins0[0];}
catch (MemberAccessException eM)
{
C.WriteLine("\tПосле попытки обратиться из пустого стека");
C.WriteLine("\n" + eM);
}
}
Проанализируйте код.
Попробуйте вариант кода с реализацией метода GetEnumerator интерфейса
IEnumerable<T>, заменив в описании класса IndexedStack<T> код
Enumerator enm = GetEnumerator();
кодом
IEnumerator<T> enm = (IEnumerator < T >) GetEnumerator();
Напишите код, индексирующий стек от дна, а не от вершины.
У стека есть метод ToArray, позволяющий копировать стек в новый массив –
естественный индексируемый объект. Наберите после кода предыдущего примера код,
использующий этот метод
object[] aso=so.ToArray();
for (int i = 0; i < aso.Length; i++)
C.WriteLine("aso[{0}]={1}\t", i, aso[i].ToString());
Сравните результаты. Существует не полиморфная версия – класс Stack.
8.5 Специализированные коллекции
Кроме описанных классов коллекций и списков в библиотеке .net существует множество
специализированных классов, объединяющих элементы в списки и коллекции.
Рассмотрим для примера три из них.
Коллекция строк StringCollection
специализируется на хранении строк и отличается от класса Collection<string>
наличием дополнительного метода AddRange, который позволяет добавлять массив строк в
конец коллекции строк.
Добавьте к списку директив using в начале файла CllctnTest.cs строку
using System.Collections.Specialized;
Это пространство имен, где описан класс StringCollection.
В методе Main класса CllctnTest наберите следующий код
//Создается экземпляр "sc" коллекции строк
StringCollection sc = new StringCollection();
//Добавляются три строки в коллекцию "sc"
sc.AddRange(new string[3] { "белый", "черный", "серый" });
//Распечатывается вся коллекция "sc"
foreach (string s in sc) C.Write("{0}\t", s);
C.ReadLine();
Посмотрите результат.
Битовый массив BitArray
из пространства имен System.Collections организует работу с произвольным
количеством бит, находящихся в двух возможных состояниях false или true.
Конструкторы класса BitArray позволяют инициализировать новый экземпляр класса
шестью различными способами – копированием уже имеющегося битового массива,
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
104
заданием числа битов в массиве и их значений, заданием массива значений логического
типа, массива байт и массива 4-байтовых целых чисел. Попробуйте три варианта, набрав в
методе Main класса CllctnTest код
//Экземпляр "babool" класса BitArray формируется из массива типа bool
BitArray babool = new BitArray(new bool[3] { true, false, true });
C.WriteLine("\tbabool");
foreach (object o in babool) C.WriteLine("\t{0}", o);
//Экземпляр "bacopy" класса BitArray формируется как копия "babool"
BitArray bacopy = new BitArray(babool);
C.WriteLine("\n\tbacopy");
for (int i = 0; i < bacopy.Length;i++) C.WriteLine("\t{0}", bacopy[i]);
//Экземпляр "babyte" класса BitArray формируется из массива типа byte
BitArray babyte = new BitArray(new byte[2] { 1, 255 });
C.WriteLine("\n\t\tbabyte"); int j = 0;
foreach (object o in babyte) C.WriteLine("\t{0}\t{1}",j++, o);
C.ReadLine();
Посмотрите результат.
Методы логического сложения Or, умножения And, отрицания Not и симметрической
разности Xor позволяют проводить перечисленные действия над элементами битовых
массивов. Наберите, к примеру, код
BitArray ba1 = new BitArray(new byte[1] { 1 });
BitArray ba1clone = (BitArray)ba1.Clone();
BitArray Notba1 = new BitArray(ba1clone.Not());
ba1clone = (BitArray)ba1.Clone();
BitArray ba2 = new BitArray(new Byte[1] { 2 });
BitArray ba1Orba2 = new BitArray(ba1clone.Or(ba2));
ba1clone = (BitArray)ba1.Clone();
BitArray ba1Andba2 = new BitArray(ba1clone.And(ba2));
ba1clone = (BitArray)ba1.Clone();
BitArray ba1Xorba2 = new BitArray(ba1clone.Xor(ba2));
C.WriteLine("\ti\t1\tNot1\t2\t1Or2\t1And2\t1Xor2");
for (int i = 0; i < ba1.Length; i++)
{
C.WriteLine("\t{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}",
i, ba1[i], Notba1[i], ba2[i], ba1Orba2[i], ba1Andba2[i], ba1Xorba2[i]);
}
C.ReadLine();
Подумайте над кодом и результатом. Поменяйте значения 1 и 2, которыми
инициализируются массивы ba1 и ba2, и вновь посмотрите результат.
Связанный список LinkedList<T>
из пространства имен System.Collections.Generic представляет коллекцию связанных
объектов типа T. Каждый элемент списка является объектом класса LinkedListNode<T> –
узла списка. У класса LinkedListNode<T> есть три свойства. Это значение Value объекта
типа <T>, а также ссылки на предыдущий Previous и последующий Next элементы
списка. Последние два свойства и делают список связанным с двух сторон - каждый его
элемент не просто имеет определенное значение, но указывает как на предыдущий, так и
на последующий элемент.
Кроме свойства Count объекты связанного списка LinkedList<T> имеют свойства First и
Last, которые возвращают первый и последний объекты типа LinkedListNode<T> в
списке. Методы AddFirst, AddAfter, AddBefore и AddLast добавляют в список объекты
типа LinkedListNode<T>, а методы RemoveFirst, Remove и RemoveLast убирают
соответствующие объекты из списка. Методы Find и FindLast возвращают в списке
первый и последний объекты типа LinkedListNode<T>, имеющие в качестве Value
заданное значение.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
105
Для иллюстрации свойств и методов работы объектов типа LinkedList<T> наберите
следующий код
//Создается пустой экземпляр связанного списка строк
LinkedList<string> ll = new LinkedList<string>();
ll.AddFirst("string1");
//добавляется первая строка
ll.AddAfter(ll.First, "string2");
//добавляется строка после первой
//копия последней строки списка вставляется перед первой
ll.AddBefore(ll.First, ll.Last.Value);
//перед последней строкой добавляется новая
ll.AddBefore(ll.Last, "string3");
foreach (string s in ll) C.WriteLine(s);//распечатываются все строки списка
//объект lln узла списка инициализируется первым найденным узлом,
//свойство Value которого равно "string2"
LinkedListNode<string> lln = ll.Find("string2");
//Распечатывается строка,содержащаяся в следующем узле
C.WriteLine("Строка,следующая за первой string2");
C.WriteLine(lln.Next.Value);
//узлу lln присваивается последний узел со строкой string2
lln = ll.FindLast("string2");
//Распечатывается строка,содержащаяся в предыдущем узле
C.WriteLine("Строка,предшествующая последней string2");
C.WriteLine(lln.Previous.Value);
C.ReadLine();
Посмотрите и проанализируйте результат.
8.6 Словари
Словари - это коллекции пар «ключ-значение», или «имя-значение».
Наиболее простым классом, реализующим словарь, является
Класс NameValueCollection
из пространства имен System.Collection.Specialized.
Класс NameValueCollection содержит коллекцию пар имя-значение, где имя и значение
являются объектами типа string, причем значение может представлять собой список
строк, разделенных запятыми.
От своего предка - абстрактного класса NameObjectCollectionBase класс
NameValueCollection унаследовал множество конструкторов и два свойства Count и
Keys. Свойство Count возвращает число элементов в коллекции, а Keys коллекцию
ключей-имен в форме объекта класса NameObjectCollectionBase.KeysCollection.
(Расширенное имя указывает на то, что класс KeysCollection описан внутри класса
NameObjectCollectionBase). Элементы класса KeysCollection доступны через указание
целочисленного индекса как элементы массива, либо с помощью метода Get, параметром
которого также является индекс.
Класс NameValueCollection имеет свое свойство AllKeys, возвращающее все ключиимена в форме массива строк. Кроме того, элементы класса NameValueCollection
доступны через целочисленный индекс и могут быть изменены или получены через
индекс в форме строки ключа-имени.
Методы Add, Set, Remove и Clear позволяют добавлять элементы к коллекции или
расширять значения имеющегося элемента (Add), устанавливать новое значение для
данного ключа (Set), а также удалять отдельные (Remove) или все (Clear) элементы
коллекции. Метод GetKey возвращает ключ по данному индексу, а методы Get и
GetValues – значения по данному индексу или ключу-имени. Метод HasKeys определяет,
есть ли в коллекции ключи, не имеющие значение null.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
106
Наберите, к примеру, код
//Создается пустой экземпляр nvc коллекции имя-значение
NameValueCollection nvc = new NameValueCollection();
nvc.Add(null, "nothing");
//В коллекцию добавляется пара ключ-значение
C.WriteLine("\tCount={0};HasKeys={1}",nvc.Count,nvc.HasKeys());
nvc.Add("name1", "value1"); //В коллекцию добавляется новая пара
nvc.Add("name1", "value1.1");//Значение пары с именем "name1" расширяется
//Вся коллекция распечатывается
C.WriteLine("\tИмя\tЗначение");
for (int i=0;i<nvc.Count;i++)
C.WriteLine("\t{0}\t{1}",nvc.GetKey(i),nvc[i]);
C.ReadLine();
Посмотрите результат. Добавьте в этот пример новые операторы, использующие
обращения к вышеперечисленным методам и свойствам коллекции nvc.
Класс Dictionary<TKey,TValue>
из пространства имен System.Collections.Generic является наиболее общей
коллекцией пар ключ-значение, где типы ключа TKey и значения TValue произвольные.
Класс Dictionary<TKey,TValue> наследует интерфейс IDictionary<TKey,TValue> и
реализует его
 индексатор, определяющий доступ к элементам коллекции по значению ключаиндекса,
 свойства Keys и Values, возвращающие интерфейсы коллекций ключей и
значений,
 традиционные методы Add, Remove, изменяющие коллекцию,
 метод, пытающийся получить значение по данному ключу TryGetValue
 и метод, определяющий наличие данного ключа ContainsKey.
Наберите пример кода
Dictionary<double, string> d = new Dictionary<double, string>();
d.Add(0.1, "одна десятая");
string s;
C.WriteLine(d.TryGetValue(-0.1,out s));
C.WriteLine(d.ContainsKey(0.1));
C.WriteLine(d[0.1]);
try//Попытка добавить элемент с тем же ключом оказывается неудачной
{
d.Add(0.1, "xxx");
}
catch (Exception e)
{
C.WriteLine(e);
}
d[0.1] = null;
//Попытка изменить элемент с данным ключом завершается успешно
C.WriteLine("d[0.1]="+(d[0.1]==null?"null":d[0.1]));
C.ReadLine();
Посмотрите результат.
Не полиморфным аналогом класса Dictionary<TKey,TValue> является Hashtable.
Существуют также классы, представляющие собой гибриды списка и словаря, и
отличающиеся от Dictionary<TKey,TValue> тем, что коллекция пар ключ-значение
сортируется по значению ключа. Это SortedList<TKey,TValue>, его не полиморфный
аналог и полиморфный класс SortedDictionary<TKey,TValue>. Класс
SortedDictionary<TKey,TValue> быстрее вставляет и удаляет элементы, чем
SortedList<TKey,TValue>, но требует больше памяти для их хранения и не может
возвращать индекс элемента по ключу.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
107
Элементами полиморфных коллекций Dictionary<TKey,TValue>,
SortedList<TKey,TValue> и SortedDictionary<TKey,TValue> являются объектыструктуры KeyValuePair, содержащие два поля Key и Value. Их не полиморфные
аналоги состоят из структур DictionaryEntry, содержащих те же поля.
Вопросы для самоконтроля
1.
2.
3.
4.
5.
6.
Сравните классы Collection<T> и List<T>.
Особенности класса Queue<T>.
Сравните очереди Queue<T> и Stack<T>.
Общая характеристика класса BitArray.
Из каких объектов состоит связанный список LinkedList<T>?
Дайте общую характеристику словарям.
Занятие 9. Структуры (struct)
Структуры относятся к типам-значениям. Объекты структур не велики по объему и
обычно не имеют большого числа методов. К структурам относятся все встроенные
«простые» типы – целые числа, числа с плавающей запятой, логические переменные,
перечислимые объекты и объекты ряда других стандартных типов библиотеки .NET. При
передаче в качестве параметров-значений метода копируются все поля структур.
Структуры используются в тех случаях, когда не предполагается использование
механизма наследования.
9.1 Происхождение, описание и инициализация структур
Абстрактный класс
public abstract class ValueType
из пространства имен System является прямым наследником класса Object. Класс
ValueType не добавляет ни одного общедоступного метода или свойства, хотя
перекрывает виртуальные методы Equals, GetHashCode и ToString класса Object так,
чтобы они отражали значения (value) вызывающих их объектов. Например, метод Equals
класса ValueType сравнивает значения объектов, а не ссылки на них.
Добавьте к Solution1 проект с именем ValueProject, его кодовый файл переименуйте в ValueTest
(параллельно среда должна переименовать шаблон класса внутри этого файла), пометьте
ValueProject как StartUp Project и, наконец, добавьте директиву using в начало файла
using C=System.Console;
– все по аналогии с предыдущими проектами.
Далее будем работать в основном в классе ValueTest проекта ValueProject.
Наберите код, описывающий внутри класса ValueTest структуру Record (можно описать
вне класса, но в том же пространстве имен)
struct Record
{
internal string name;
internal byte age;
}
В методе Main наберите код
Record r = new Record();
C.WriteLine(r.GetType().BaseType.ToString());//BaseType–свойство класса Type
C.ReadLine();
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
108
Результат показывает, что типом непосредственного предка (BaseType) классов, для
которых C# использует служебное слово struct, является ValueType. Структуры в
отличие от обычных классов не могут иметь наследников и каких-либо предков кроме
интерфейсов. Все выглядит так, как если бы описанная в примере структура имела
заголовок
//Язык C# не позволяет явно наследовать от ValueType!
public sealed class Record: ValueType;
с учетом следующих замечаний:
 структуры не могут быть абстрактными, иметь абстрактные члены и виртуальные
методы. В структурах можно лишь перекрывать (override) виртуальные методы,
наследуемые от класса ValueType (это, знакомые нам по классу Object методы
Equals, GetHashCode и ToString).
 Модификаторы доступа protected и protected internal не имеют смысла для
членов структуры.
В структурах в отличие от обычных классов:
 можно описывать только конструкторы с параметрами. Конструктор без
параметров наследован структурами от ValueType и не может быть изменен;
 нельзя инициализировать нестатические поля (статические можно).
Нельзя было бы в нашем примере набрать
internal byte age=17;
но можно
static internal byte age=17;
Попробуйте.
Как и в любом классе, в структурах можно описывать постоянные поля (служебное
слово const)
internal const string fstr = "Name\x20{0};\tAge\x20{1}";
Добавьте это поле к структуре Record.
 нельзя описывать деструктор;
 можно без предварительного вызова конструктора инициализировать доступные
поля структуры прямым обращением к ним.
Например, вместо строки
Record r=new Record();
в коде примера наберите
Record r;
r.age = 0; r.name = string.Empty;
Результат будет тем же.
Конечно, доступные поля можно не только обнулять, но и присваивать им любые
значения их типа; вызов конструктора, как и у любого класса, обнуляет все
нестатические поля структуры.
Статическая и нестатическая версии метода Equals в структурах сравнивают поля
структур одного типа, а не ссылки на сами структуры. Равенство ссылок проверяет
статический метод ReferenceEquals.
Наберите в методе Main код
Record r1 = new Record(), r2 = new Record();
C.WriteLine("r1=r2? {0}\tr1ref=r2ref? {1}",
r1.Equals(r2),Record.ReferenceEquals(r1,r2));
Посмотрите результат. Измените произвольно значение хотя бы одного из полей
структуры r1 или r2 и вновь посмотрите результат.
Оператор присвоения копирует все нестатические поля структуры. То же происходит при
вызове метода со структурой в качестве фактического параметра. Наберите код
Record r1, r2;
r1.name = "Unknown"; r1.age = 0xff;//Шестнадцатеричное 255, или byte.Max
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
109
r2 = r1;
C.WriteLine(Record.fstr, r2.name, r2.age);
и посмотрите результат.
Обратите внимание на то, что поле fstr с модификатором const является статическим
по самому смыслу постоянной, т.е. является в данном случае полем класса Record, а не его
экземпляров r1, r2.
9.2 Простые встроенные типы структур. Целочисленные
типы
Простые типы делятся на числовые (numeric) типы и логический (bool) тип. В свою
очередь числовые типы делятся на целочисленные (integral) типы, типы с плавающей
запятой (floating-point-type) и десятичный (decimal) тип.
Целочисленные типы
Это
1.знаковые целые
 8-битовые (псевдоним sbyte, интервал значений [-128;127])
public struct SByte:IComparable, IFormattable, IConvertible,
IComparable<sbyte>, IEquatable<sbyte>
 16-битовые (псевдоним short, [-32768; 32767])
public struct Int16:IComparable, IFormattable,
IConvertible, IComparable<short>, IEquatable<shot>
 32-битовые (псевдоним int, [-2147483648; 2147483647])
public struct Int32:IComparable, IFormattable,
IConvertible, IComparable<int>, IEquatable<int>
 64-битовые
(псевдоним long, [-9223372036854775808;9223372036854775807])
public struct Int64:IComparable, IFormattable,
IConvertible, IComparable<long>, IEquatable<long>
2. целые без знака
public struct Byte:IComparable, IFormattable, IConvertible,
IComparable<byte>, IEquatable<byte>
public struct UInt16:IComparable, IFormattable,
IConvertible, IComparable<ushort>, IEquatable<ushot>
public struct UInt32:IComparable, IFormattable,
IConvertible, IComparable<uint>, IEquatable<uint>
public struct UInt64:IComparable, IFormattable,
IConvertible, IComparable<ulong>, IEquatable<ulong>
с интервалами значений (в шестнадцатеричном представлении) [0;0xFF],
[0;0xFFFF], [0;0xFFFFFFFF] и [0;0xFFFFFFFFFFFFFFFF], соответственно.
3. 2-байтовые символы (псевдоним char, интервал значений [0;0xFFFF])
public struct Char :
IComparable, IConvertible, IComparable<char>, IEquatable<char>
Все числовые типы могут быть преобразованы в символьное представление (строку) и
обратно. Для этого существуют методы ToString и Parse.
Метод ToString имеет несколько версий. Одна из них реализует единственный
одноименный метод интерфейса IFormattable
string ToString(string format, IFormatProvider formatProvider);
где format – форматирующая строка, а formatProvider – уже встречавшийся
объект интерфейса IFormatProvider.
ранее
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
110
Объекты интерфейса IFormatProvider в случае числовых объектов обычно
представляются объектами класса NumberFormatInfo или CultureInfo из пространства
имен System.Globalization. В том же пространстве имен находится нумератор (enum)
NumberStyles, используемый для указания стиля представления числа при обратном
преобразовании из строки в число методами Parse и TryParse.
Чтобы использовать эти объекты, добавьте в список директив using строку
using System.Globalization;
В методе Main наберите код
int i = -0xFdE;
// Целое число в 16-ричном формате
sbyte sb = -1;
NumberFormatInfo nfi = new NumberFormatInfo();
nfi.NegativeSign = "минус";
// Формат отрицательного знака
C.WriteLine("i={0}\tsb={1}",
//Печать числа i в формате,где отрицательный знак обозначается словом «минус»
i.ToString(nfi),
sb.ToString("X"));
// Печать числа sb в 16-ричном формате
//Возвращает число,учитывающее формат строки по настройкам объекта nfi
C.WriteLine(short.Parse("минус1513", nfi));
ulong ul;
C.WriteLine("{0}\tul={1}",
//В параметр ul возвращается результат преобразования строки в число с учетом
//стиля 16-ричного представления (NumberStyles.HexNumber).
ulong.TryParse("aDfCC0e", NumberStyles.HexNumber,null,out ul),ul);
Проанализируйте код и его результат.
Методы Parse и TryParse являются статическими. Наберите byte. или long. (с точкой).
Появится список статических свойств и методов, которые встречались в предыдущих
примерах.
Если набрать в предыдущем коде i., или sb., или, наконец, ul. (все с точкой), то
появится один и тот же список доступных нестатических методов, наследованных от
Object и реализующих методы интерфейсов, наследуемых целочисленными (и, вообще,
числовыми) структурами. Все эти методы встречались в предыдущих примерах. У всех
простых типов практически один и тот же список нестатических методов.
Наберите char. и убедитесь в том, что список статических членов структуры char гораздо
шире. Все дополнительные статические методы типа char можно разделить на две
категории – методы преобразования и методы распознавания. Простейшие из методов
преобразования ToUpper и ToLower преобразуют символ-букву в прописную и,
соответственно, в строчную букву. Методы IsPunctuation и IsControl – примеры
методов распознавания: является ли символ знаком пунктуации (точка, запятая и т.п.) и
является ли символ управляющим (перенос строки и т.п.).
В методе Main наберите код
char chя = 'я', chSemicolon = ';', chCtrl = '\u000d';
C.WriteLine(char.IsPunctuation(chSemicolon));
C.WriteLine(char.IsControl(chCtrl));
//Преобразование символа с учетом текущей культуры
C.WriteLine(char.ToUpper(chя,CultureInfo.CurrentCulture));
Посмотрите результат.
Символьное выражение целочисленных типов (исключая char) представляет собой набор
десятичных или 16-ричных цифр. В последнем случае следует писать префикс 0x. Кроме
того, в конце числа возможен суффикс из числа букв u, U, l, L, и пар ul, uL, Ul, UL.
Суффиксы u и U означают, что число относится к типу uint или ulong в зависимости от
значения. Например, 12u – число типа uint, а 0x100000000U (8 нулей) – типа ulong.
Суффиксы l и L – число типа long или ulong. Пара символов – типа ulong.
К целочисленным типам применимы обычные арифметические бинарные операции –
сложение, вычитание, умножение, деление. Результатом будет целое число, даже если
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
111
операндами являются символы (char). Распечатайте ряд результатов арифметических
бинарных операций над объектами разных целочисленных типов. Например,
C.WriteLine('a' * 'b'); C.WriteLine('Я' / '\a');
Есть ряд особенностей. Сумма объектов числовых типов sbyte, byte, short, ushort,
возвращает тип int.
Например, не верный код (проверьте)
byte b1=20,b2=21,b=b1+b2;
Верная версия
byte b1=20,b2=21,b=(byte)(b1+b2);
Правила преобразования числовых типов позволяют неявно преобразовать тип с меньшей
длиной в тип с большей длиной (например, ushort в long), но обратное преобразование
должно указываться явно.
Неявное преобразование в тип char вообще отсутствует.
Например, наберите код
char ch = (char)0xa;
C.WriteLine(char.Equals(ch, '\n'));
и посмотрите результат.
Побайтовые операции
 унарная ~ (замена единиц на нули и наоборот) и
 бинарные & (побитовое «и»), | (побитовое «или»), ^ (побитовая симметрическая
разность xor)
Эти операции применимы ко всем целочисленным типам. Проверьте это на примерах типа
C.WriteLine('Я' ^ 'я'); C.WriteLine(0xedf | '\xCD');
К целочисленным типам применимы также бинарные операции сдвига >>c - сдвиг вправо
и <<c - сдвиг влево на c разрядов, причем c должно возвращать объект типа int.
Наберите пример
C.WriteLine(1L << '\a'*'\n');
C.WriteLine('\x1' << '\a' * '\n');
и проанализируйте результат.
Выход значения целого типа за интервал определения (например, >MaxValue) может
приводить к различным последствиям. Наберите код
byte i = byte.MaxValue, j;
j = (byte)(i + 1);
C.WriteLine(j);
Активизация кода приведет либо к останову по переполнению, либо распечатанное
значение j будет равно нулю в зависимости от установки флажка Check for arithmetic
overflow/underflow в опциях компилятора. Этот флажок находится в окне Advanced Build Settings
(команда Project.ValueProject properties…, панель Build, кнопка Advanced). Если приложение
вышло на переполнение, снимите флажок – распечатается значение j=0. Наоборот,
установка флажка приведет к контролю за попадание значения целого типа в допустимый
интервал и, в данном примере, к выходу на ИС.
Установку и снятие флажка, контролирующего (check) выход за интервал значений целых
чисел данного типа, можно проводить непосредственно в коде операторами checked и
unchecked. Замените вторую строку предыдущего кода строкой
j = checked((byte)(i + 1));
Попробуйте работу этой версии при снятом и установленном флажке компилятора.
Результатом всегда должен быть выход на ИС.
Теперь замените строкой
j = unchecked((byte)(i + 1));
Так же попробуйте разные режимы компиляции. Результатом всегда должен быть 0.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
112
9.3 Типы с плавающей запятой

32-битовые
(псевдоним float, интервал [1.5×10−45;3.4×1038], точность 7 знаков)
public struct Single:

IComparable, IFormattable, IConvertible,
IComparable<float>, IEquatable<float>
64-битовые
(псевдоним double, интервал [5.0×10−324;1.7×10308], точность 15-16 знаков)
public struct Double:
IComparable, IFormattable, IConvertible,
IComparable<double>, IEquatable<double>
Типы float и double имеют ряд статических свойств и методов. Наберите double. (с
точкой), чтобы получить их полный список. Константы double.Epsilon и float.Epsilon
хранят минимальные отличные от нуля числа соответствующих типов. Распечатайте их.
Поработайте с другими свойствами и методами.
К типам с плавающей запятой применимы все арифметические операции и неприменимы
все бинарные операции.
Вещественные числа, выраженные в своей обычной (с десятичной точкой: -1.0) или
экспоненциальной (3.5e-23) форме, имеют по умолчанию тип double.
Арифметические операции позволяют смешивать целочисленные типы и тип double.
Например, можно записать C.WriteLine(1e-13 + '\x13');.Результат неявно
преобразуется к типу double.
В отличие от целых типов операции с вещественными типами не приводят к
переполнению.
Например, наберите код
C.WriteLine(1.000000000000001 * double.MaxValue);//14 нулей
//Суффиксы f(или F) и d(или D) определяют типы (по умолчанию d или D)
C.WriteLine(0.0f/0.0D);
C.WriteLine(float.NegativeInfinity + double.PositiveInfinity);
Посмотрите результат. Сравните с результатом другого кода
int i = new int();
// аналогично инициализации нулем int i = 0;
C.WriteLine(new double() /i);// отношение 0.0/0 возвратит NaN
C.WriteLine(0 / i);
// Возвратит ИС
9.4 Десятичный тип (decimal)
public struct Decimal: IComparable, IFormattable, IConvertible,
IComparable<decimal>, IEquatable<decimal>
Это числа с фиксированной запятой. Хранятся в памяти как целые 96-битовые числа со
знаком и с информацией о положении десятичной точки внутри числа. Максимальное
абсолютное значение (decimal.Max) равно 296. Большое число (28) значащих цифр делает
тип decimal предпочтительным в банковских расчетах.
Полная числовая форма объекта типа decimal имеет вид (–1)s x c x 10-e, где знак s есть
0 или 1, коэффициент c лежит в интервале 0<c<296, а показатель (масштаб) e – в
интервале 0 ≤ e ≤ 28.
Постоянные типа decimal должны содержать суффикс m или M, (-5.0e23m или
123456789M) чтобы отличать их от постоянных типа double или целочисленных типов.
В арифметических операциях числа типа decimal можно смешивать с целыми типами
(1234.6M*0xdE), но не с типами с плавающей запятой (-3.0e-26M+(decimal)7.7e7). Числа
типа decimal, как и целые, создают объект ИС при выходе за интервал определения.
У типа decimal есть статические свойства, методы и операторы, среди которых:
 бинарные арифметические операторы *, /, +, -, % и соответствующие им методы
public static decimal operator *(decimal d1, decimal d2);
public static decimal Multiply(decimal d1, decimal d2);
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
113
//…
public static decimal operator %(decimal d1, decimal d2);
public static decimal Remainder(decimal d1, decimal d2);

унарные операторы ++, --, +, public static decimal operator -(decimal d);
//…
и метод
public static decimal Negate(decimal d);

операторы сравнения ==, !=, <=, >=, <, >

public static bool operator ==(decimal d1, decimal d2);
//…
операторы явного преобразования из типа decimal в другие числовые
типы и
методы, их поддерживающие
public static explicit operator sbyte(decimal value);
public static byte ToSByte(decimal value);
//…

операторы неявного преобразования целочисленных типов в тип decimal
public static implicit operator decimal(byte value);
//…
Примечание
Ранее уже встречался пример с переопределением операторов языка. Кроме перечисленных здесь и ранее
операторов, язык C# допускает переопределение операторов сдвига >>, <<, побитовых операторов ~, &, |, ^
и логических операторов !, true, false.
Один из статических методов структуры decimal метод GetBits возвращает битовое
представление объекта типа decimal в форме массива из четырех 4-ехбайтовых целых
чисел (без знака). Первые три элемента этого массива содержат целое представление
объекта decimal, а в 4-ом элементе находится бит знака и информация о положении
десятичной запятой.
Наберите в классе ValueTest метод Print, распечатывающий массив представления типа
decimal целыми числами,
static void Print(decimal d)
{
int[] db = decimal.GetBits(d);
//Один из конструкторов типа decimal берет параметром массив целых чисел
C.WriteLine("\t\tdecimal={0:C}", new decimal(db));
for (int i = 0; i < db.Length; i++)
C.Write("|{0,-16:x}|"+(i==db.Length-1?"\n":"\t"), db[i]);
}
а в методе Main - несколько обращений к Print с разными параметрами
Print(decimal.MinValue);
Print(decimal.MinusOne);
Print(-1e-28m);
Print(decimal.Zero);
Print(1e-28m);
Print(decimal.One);
Print(decimal.MaxValue);
Проанализируйте результат.
9.5 Логический тип (bool)
public struct Boolean : IComparable, IConvertible,
IComparable<bool>, IEquatable<bool>
Объекты типа bool имеют лишь два значения true и false. К ним применимы уже
встречавшиеся нам операторы: унарный ! и бинарные &, |, ^, &&, ||. Какие-либо
преобразования числовых типов и типа bool отсутствуют, но постоянные true и false
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
114
могут быть представлены в символьном виде статическими полями TrueString и
FalseString или нестатическим методом ToString.
Наберите пример
C.WriteLine(bool.FalseString);
bool b = true;
C.WriteLine(b.ToString());
Посмотрите результат.
9.6 Перечислимый тип, или нумераторы (enum)
Объектами класса
public abstract class Enum :ValueType, IComparable,
IFormattable, IConvertible
являются постоянные значения в символьной форме, используемые для перечисления
качественных характеристик. Примеры использования таких объектов встречались выше
(TypeCode, NumberStyles).
Заметим, что класс Enum является с одной стороны ссылочным типом (class). Поэтому к
нему применим модификатор abstract. В то же время класс Enum является наследником
ValueType и в этом смысле типом struct. Синтаксис языка C# позволяет описать
перечислимые типы, или нумераторы, являющиеся наследниками класса Enum и
относящиеся к типам-значениям.
Для описания объектов-нумераторов в языке C# используется служебное слово enum, а
само описание типа enum представляет собой описание множества объектов-имен. Под
каждым именем находится определенное значение любого целочисленного типа кроме
char. Это так называемый «нижележащий» (underlying), или базовый тип (не путать с
базовым типом класса в наследовании!). По умолчанию нижележащим типом
нумераторов является int. Имена объектов-нумераторов инициализируются конкретными
значениями базового типа. (Заметим, что ни тип enum, ни его объекты, хотя и называются
«нумераторы», или объекты перечислимого типа, не имеют никакого отношения к
интерфейсам IEnumerator, IEnumerable и к классам, реализующим эти интерфейсы.)
Предположим необходимо описать состояние приложения, работающего с базой данных.
Это может быть состояние просмотра, редактирования, вычислений, меняющих
содержание базы, и т.п. Все эти состояния удобно перечислить, назвав их своими именами
– exam, edit, comp. Внутри класса ValueTest (но вне метода Main; внутри метода нельзя
описывать типы!) наберите код, описывающий тип нумератора State
enum State {exam , edit, comp };
В методе Main наберите код
C.WriteLine("type:{0};\tbase type:{1};\tunderlying type:{2}",
typeof(State).Name,
typeof(State).BaseType.ToString(),State.comp.GetTypeCode());
Посмотрите результат. Метод GetTypeCode в данном случае возвратит нижележащий
объекта-нумератора Int32.
Класс Enum имеет ряд статических методов. Наберите пример, добавив код
// GetValues возвращает массив чисел «нижележащего» типа нумератора
int[] ea = (int[])Enum.GetValues(typeof(State));
// GetNames возвращает массив имен нумератора
string[] en = (string[])Enum.GetNames(typeof(State));
for (int i = 0; i < ea.Length;i++)
C.WriteLine("const={0}\t;name:{1}", ea[i],en[i]);
Посмотрите результат.
Получить нижележащее значение нумератора проще явным преобразованием типа.
Например, наберите код
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
тип
115
C.WriteLine((int)State.comp);
Из этих примеров следует, что по умолчанию объекты нумераторов нумеруются
нижележащим типом от нуля. При необходимости можно ставить в соответствие
нумераторам любые значения нижележащего типа, объявив эти значения в описании
enum State {exam =-1000, edit, comp };
или
enum State {exam =-1000, edit=105, comp=0 };
Проверьте оба варианта кода.
Нижележащий тип нумератора (по умолчанию int), может быть явно указан при
описании нумератора. Например,
enum State :ulong {exam =ulong.MaxValue, edit=0UL, comp=ulong.MinValue };
Проверьте код, заменив предварительно строку
int[] ea = (int[])Enum.GetValues(typeof(State));
строкой
ulong[] ea = (ulong[])Enum.GetValues(typeof(State));
Из последнего примера видно, что нумераторы из одного множества могут иметь
одинаковые нижележащие значения.
Можно описать объект типа State. Наберите в методе Main код
State state=State.comp;
C.WriteLine((ulong)state);
Объект state может иметь не только одно из значений State.exam, State.edit, или
State.comp. Возможны любые сочетания этих значений, полученные с помощью
побитовых операций между нижележащими значениями.
Флаги
Наличие побитовых операций позволяет использовать перечислимый тип как множество,
элементами которого являются не только отдельные объекты нумератора, но и их
произвольные сочетания. К примеру, опишите нумератор
enum Жених {красив=1, высок=2, умен=4, богат=8, добр=0x10, молод=0x20};
Ясно, что «жених» может обладать любым сочетанием перечисленных качеств. Для
построения таких сочетаний опишите в классе ValueTest статический метод BitPattern,
возвращающий содержание байта в виде строки - набора единиц и нулей
static string BitPattern(byte b)
{
int i = 0;
// Вариант алгоритма, в котором строка накапливается слева направо
string result = Convert.ToString(b >> 7);
while (i < 7)
result += Convert.ToString(b >> 7 - ++i & 1);
/*Другой вариант алгоритма – строка накапливается справа налево
string result = Convert.ToString(b & 1);
while (i++ < 7)
result = Convert.ToString((b >>= 1) & 1) + result;
*/
return result;
}
Обратите внимание на очередность выполнения арифметических операций внутри цикла
первого варианта кода: ++, -, >>, &. Проверьте работу метода BitPattern, обращаясь к
нему в методе Main с разными параметрами. Например,
C.WriteLine(BitPattern(127)); C.WriteLine(BitPattern(255));
и т.д.
В методе Main наберите код, распечатывающий битовый формат каждого нумератора типа
Жених.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
116
int[] ea = (int[])Enum.GetValues(typeof(Жених));
string[] en = (string[])Enum.GetNames(typeof(Жених));
C.WriteLine("{0}\tpattern",typeof(Жених).Name);
for (int i = 0; i < ea.Length; i++)
C.WriteLine("{0,-7}\t{1}",en[i],BitPattern((byte)ea[i]));
Проанализируйте результат.
Введите переменную идеал типа Жених и распечатайте ее с помощью кода
Жених идеал = 0;
for (int i = 0; i < ea.Length; i++) идеал |= (Жених)ea[i];
C.WriteLine("идеал\t{0}", BitPattern((byte) идеал));
Посмотрите результат.
Добавьте строку кода, выделяющую жениха с одним качеством красив
C.WriteLine("{0}",(Жених)(идеал & Жених.красив));
Это поясняет, как можно работать с объектами перечислимого типа, используя их как
битовые флаги.
9.7 Итераторы
Для перечисления всех элементов типа Жених в предыдущем примере использовался
обычный оператор цикла for. Это связано с отсутствием реализации интерфейса
IEnumerable у нумераторов.
Язык C# позволяет сделать объекты любого класса перечислимыми и применять к его
элементам цикл foreach,in, не реализуя в полной мере интерфейсы IEnumerable и
IEnumerator. С этой целью в классе должен быть описан метод, именуемый итератором.
Код итератора должен определять, как именно осуществляется доступ к перечисляемым
элементам класса.
Опишите в качестве примера в классе ValueTest новый класс Enums<T>, снабженный
итератором
class Enums<T> where T:struct
{
// Это итератор, применяемый для перечисления объектов-нумераторов
public IEnumerator<T> GetEnumerator()
{
// Создается массив нижележащих значений данного нумератора типа T
int[] ea = (int[])Enum.GetValues(typeof(T));
// Нижележащие значения преобразуются в тип нумератора T и возвращаются
// методом-итератором в процессе перечисления
for (int i = 0; i < ea.Length; i++)
yield return (T)(ValueType)ea[i];
}
}
Класс Enums<T> позволит использовать цикл foreach,in для нумераторов типа Жених.
Проверьте это, набрав в методе Main код
Enums<Жених> женихи = new Enums<Жених>();
Жених идеал = 0;
foreach (Жених жених in женихи) идеал |= жених;
C.WriteLine("идеал\t{0}", BitPattern((byte)идеал));
С помощью команды отладчика Debug.Step Into проследите за тем, как работает итератор.
Посмотрите результат. Служебное слово yield в сочетании с return (возможно еще
сочетание yield break) позволяет не только вернуть требуемое значение элемента
перечисляемого типа, но и запомнить последнее значение, чтобы продолжить работу
оператора цикла for с текущего места.
Описанный класс Enums<T> имеет ограничение where T:struct на тип T, которое
означает, что тип T может быть только структурой, т.е. типом-значением. Этого
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
117
ограничения не достаточно, если неявно считается, что тип T должен быть нумератором
(наследником Enum). Для чистоты кода в итератор GetEnumerator следует ввести
оператор, создающий ИС типа ArgumentException в случае, если аргументом окажется
тип T, отличный от нумератора
if (typeof(T).BaseType != typeof(System.Enum))
throw (new System.ArgumentException("Должен быть типа System.Enum"));
Этот код следует поместить в самом начале метода GetEnumerator.
Проверьте новую версию итератора GetEnumerator. Опишите в методе Main объект класса
Enums<T>, в котором тип T является структурой, но не нумератором (например, типом
int), и примените к этому объекту оператор foreach, in.
9.8 Атрибуты
Добавьте к уже имеющемуся коду еще одну строку
C.WriteLine(идеал ^ Жених.красив);
Проверьте результат. Результатом должно быть число 62.
Теперь перед описанием нумератора Жених добавьте строку атрибута
[Flags]
enum Жених {красив=1, высок=2, умен=4, богат=8, добр=0x10, молод=0x20};
Вновь активизируйте код. Вместо числа 62 приложение напечатает весь список объектов
типа Жених, за исключением красив.
Классы атрибутов (в частности, Flags) являются наследниками класса System.Attribute.
Все классы атрибутов имеют суффикс Attribute, но при ссылке могут писаться без
суффикса. Так полное имя класса Flags есть FlagsAttribute. Замените в коде имя Flags
именем FlagsAttribute, ошибки не будет.
Перед описанием практически любого элемента программы (класс, свойство, метод, поле,
параметры метода) можно помещать информацию в форме атрибута, декларирующую
некоторые устойчивые свойства этого элемента. Такая описательная информация
может быть в дальнейшем использована средой .NET или другими классами и
приложениями, ссылающимися на объекты с атрибутами. Другими словами, атрибуты
играют роль сообщений, предназначенных для среды или других классов.
В данном случае ссылка на стандартный класс Flags «сообщает», что нумераторы типа
Жених будут использоваться как битовые флаги. Это сообщение может «никто» не читать
и никак не использовать. Но метод Write класса Console (или, возможно, метод,
вызываемый методом Write) запрашивает с помощью статического метода
GetCustomAttributes класса Attribute наличие атрибутов у нумератора Жених и
действует в зависимости от наличия атрибута Flags.
Синтаксис языка C# требует, чтобы конструктор класса атрибута указывался в
квадратных скобках перед элементом кода, к которому этот атрибут относится. Если
конструктор не имеет параметров, то круглые скобки после имени класса писать не
обязательно. Но можно записать и в полной форме [Flags()] - ошибки не будет
(проверьте). Реально конструктор вызывается, и объект атрибута создается только
методом GetCustomAttributes (или GetCustomAttribute) в отношении того элемента
кода, которому атрибут приписан. В нашем случае атрибут Flags приписан нумератору
Жених. Метод Write вызывает метод GetCustomAttributes, который создает экземпляр
класса Flags.
Для выяснения атрибутов у типа Жених наберите код
Attribute[] aЖ = Attribute.GetCustomAttributes(typeof(Жених));
foreach (Attribute a in aЖ) C.WriteLine(a.ToString());
и посмотрите результат.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
118
То, что атрибут Flags приписан нумератору Жених, следует из контекста кода. Но могут
встретиться ситуации, в которых надо явно указать, к какому элементу кода относится
атрибут. Для этого используются метки принадлежности атрибутов.
Например, атрибут
[assembly: AssemblyTitle("ValueProject")]
относится к сборке в целом. На это указывает метка assembly:. Посмотрите в связи с этим
содержимое файла AssemblyInfo.cs, содержащего манифест сборки проекта
ValueProject и состоящего из атрибутов сборки (в окне SolutionExplorer в узле
ValueProject.Properties).
В случае с атрибутом Flags можно было бы написать
[type:Flags]
Проверьте. Метка type: указывает на то, что атрибут относится к следующему за ним
описанию типа (в данном случае типа Жених).
В случае сборки метку assembly: опускать нельзя. То же относится и к атрибутам
исполняемого модуля (.dll или .exe) с меткой module:.
Наберите внутри метода Main код, определяющий атрибуты текущей сборки и
распечатывающий заголовок сборки как свойство Title атрибута
AssemblyTitleAttribute
//Создается массив атрибутов текущей сборки статическим методом
//GetCustomAttributes класса Attribute
//Статическим методом GetAssembly класса Assembly из пространства имен
//System.Reflection можно создать объект текущей сборки, если указать
//в качестве параметра тип любого класса из сборки.
//В данном случае указан нумератор Жених, хотя это может быть любой тип,
//описанный в сборке
Attribute[] aa = Attribute.GetCustomAttributes(
System.Reflection.Assembly.GetAssembly(typeof(Жених)));
foreach (Attribute a in aa)
{
C.WriteLine(a.ToString());
if (a is System.Reflection.AssemblyTitleAttribute)
C.WriteLine("AssemblyTitle:{0}",(a as
System.Reflection.AssemblyTitleAttribute).Title);
}
У конструкторов классов атрибутов могут быть параметры. В частности, все атрибуты
сборки вызывают конструкторы с параметрами.
Программист может создавать свои классы атрибутов, приписывая их объекты элементам
своего кода.
9.9 Nullable-типы (объекты структуры Nullable<T>)
Если объект описан локально внутри метода и не инициализирован, то его значение не
определено и никакие действия с ним не возможны. Наберите в методе Main код
int i; i.ToString();
и попытайтесь скомпилировать программу. Компилятор подчеркнет обращение к объекту
i, указав на его отсутствие. Действительно, объект i лишь описан как объект класса int,
но его конструктор не вызывался (ни явно, ни не явно).
Инициализировать объект можно явным вызовом конструктора оператором new, либо,
если это тип-значение или string, оператором присвоения =. После инициализации с
объектом можно работать – он имеет конкретное значение.
Если объект является полем класса и не инициализирован оператором присваивания, то
при вызове конструктора объект принимает значение по умолчанию (в зависимости от
типа объекта это null, false или 0). Так, если описание int i; в приведенном примере
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
119
перенести наружу метода Main, сделав i полем класса ValueTest, и добавить к нему
модификатор static (так как i используется в теле статического метода Main), то никаких
проблем с вызовом i.ToString(); в методе Main не будет. Поле i будет
инициализировано нулем. Попробуйте.
Иногда возникает необходимость поддерживать объект типа-значения в состоянии,
которое не имеет значения, но, тем не менее, является определенным состоянием, которое
можно установить, отменить, проверить. У ссылочных типов такое состояние есть, это
null, а у типов-значений нет.
Предположим, что создается код класса, в котором некое поле (пусть того же типа int или
bool) должно быть задано пользователем. Программа должна реагировать на то, задано
значение поля или нет. Проверять это, сравнивая поле с его значением по умолчанию
(нулем в случае int или false в случае bool) нельзя, так как значение по умолчанию
имеет смысл и может быть задано пользователем. Отсюда потребность в типахзначениях, которые могут находиться в состоянии null, как и ссылочные типы. Важно
понять, что null это не значение объекта, а состояние, в котором объект не имеет
никакого значения.
Термин “nullable”, введенный для таких типов, трудно перевести на русский язык одним
словом. Предоставляем читателю поиск подходящего на его взгляд односложного
термина, обозначающего объект, который может иметь какое-либо значение и
находиться в состоянии без какого-либо значения.
Все объекты nullable-типа являются объектами полиморфной структуры
System.Nullable<T>. Здесь T может быть любым типом, хотя строить объекты имеет
смысл лишь для типов-значений - ведь ссылочные типы имеют состояние null по своей
природе.
Заголовок структуры имеет вид
public struct Nullable<T>:where T struct;
Важнейшими свойствами nullable-типов являются свойства
bool HasValue {get;};
и
object Value {get;};
Свойство HasValue возвращает, является ли объект null или нет (значение true, когда не
null).
Свойство Value возвращает определенное значение объекта лишь в том случае, если
объект не null. В противном случае свойство Value создает объект ИС типа
InvalidOperationException.
Кроме указанных свойств, структура Nullable<T> имеет




конструктор с параметром типа T (объект, превращаемый в nullable-тип),
знакомые по предыдущим типам методы, наследуемые от Object,
две версии метода GetValueorDefault, возвращающие значение объекта либо
значение по умолчанию (default) данного типа объектов,
и операторы преобразования типов – явного из Nullable<T> в T
public static explicit operator T(Nullable<T> value);
и не явного из T в Nullable<T>
public static implicit operator Nullable<T>(T value);
Наберите в методе Main код
Nullable<int> ni = new Nullable<int>(1);
C.WriteLine("HasValue={0};Value={1}",ni.HasValue,ni.Value);
ni = null;
//Одна версия GetValueOrDefault возвращает
//стандартное значение по умолчанию типа int, т.е. 0
//Вторая - назначенное значение по умолчанию. В данном случае -1000000
C.WriteLine("default value={0} или {1}", ni.GetValueOrDefault(),
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
120
ni.GetValueOrDefault(-1000000));
Посмотрите результат.
Язык C# позволяет сократить вызов конструктора, опустив new Nullable<int> и, тем
самым, записав кратко
Nullable<int> ni = 1;
В этой записи используется также способность объектов Nullable<int> неявно
преобразовывать тип int (единица) в тип Nullable<int> (объект ni).
Языком C# вводится псевдоним имени Nullable<T> в виде T?. Так что, предыдущая
строка кода может иметь вид (проверьте)
int? ni = 1;
Операции с nullable-типами проводятся как с обычными типами. Наберите код
int?[] na1 = { 1 }, na2 = { -1 };// nullable-типы массивов целых
na1[0] += na2[0];
int i=(int)na1[0];
// явное преобразование к обычному типу
C.WriteLine("na1={0};na2={1};i={2}",na1[0],na2[0],i);
Проверьте результат. Посмотрите так же код
C.WriteLine(na2[0] - null == null);
Смешивание в выражениях объектов nullable-типов и обычных типов допустимо только
при явном преобразовании. Например, код i= i+ na2[0] не пройдет, но i = i+
(int)na2[0] пройдет. Попробуйте (i типа int).
Если присвоить na2[0] = null, то попытка преобразования i = (int)na2[0] создаст
ИС. В то же время следующий код будет работать
int j=0;
C.WriteLine(j + null == null);
Наберите код
na2[0] = null;//Замените затем на na2[0] = 10 и также распечатайте i
i = na2[0] ?? -5;
Распечатайте i и посмотрите результат.
Условный оператор ?? специально введен в C# для работы с nullable–типами. Он
возвращает второй операнд (в данном случае -5), если первый не имеет значения, либо
значение первого операнда. Фактически последняя строка является сокращенной записью
выражения (проверьте)
i = na2[0].GetValueOrDefault(-5);
Еще один пример – можно описать объект нумератора Жених как nullable-тип. Одним из
состояний объекта nжених будет отсутствие жениха вообще (null).
Жених? nжених;
Поработайте с этим объектом.
Вопросы для самоконтроля
Основные отличия структур от классов типа class.
Описание и инициализация структур.
Перечислите целочисленные типы структур.
Арифметические операции над целочисленными типами.
Неявные и явные преобразования целочисленных типов.
Регулировка целочисленного переполнения в опциях проекта. Операторы checked
и unchecked.
7. Типы с плавающей запятой.
8. Тип с фиксированной запятой. Его основные характеристики.
9. Описание перечислимого типа (enum) и свойства его объектов.
10. Битовые флаги и объекты перечислимого типа (enum).
11. Как описывается и работает итератор?
1.
2.
3.
4.
5.
6.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
121
12. Атрибуты и их роль.
13. Описание объектов nullable-типа и операции с ними.
Занятие 10. Асинхронный код (Threading)
Обычно процесс обработки и переноса информации совершается синхронно – проходит в
одном потоке, или одной «нитью» (thread). Операции выполняются друг за другом в
жесткой последовательности. Часто возникают ситуации, в которых имеет смысл сделать
процесс обработки информации асинхронным по отношению к процессу обмена.
Например, пользователь намерен лишь изредка выводить накопленную информацию на
экран монитора. Остальное время процесс обработки должен продолжаться. В такой
ситуации имеет смысл создать отдельную нить процесса для обработки информации,
оставив за основной нитью функции управления обменом информацией с пользователем.
Выполните некоторые подготовительные действия:
 Добавьте к Solution1 еще один проект типа ConsoleApplication, назвав его ThreadProject.
 Переименуйте файл, созданный средой по умолчанию, в ThreadTest.cs. В этом случае
скелет класса, созданный средой в файле ThreadTest.cs, должен получить то же имя
ThreadTest.
 Включите для проекта StartUp Project.
 В список using добавьте две строки
using System.Threading;
using C = System.Console;
10.1 Класс System.Threading.Thread
В методе Main класса ThreadTest наберите код
/*
Класс Thread из пр-ва имен System.Threading имеет конструктор с одним
параметром – делегатом типа ThreadStart.
Делегат делегирует нити t метод,код которого выполняется объектом-нитью t.
Здесь делегируемый метод задан явно в виде бесконечного цикла,
while (true)d++,увеличивающего значение локальной переменной d на 1
*/
decimal d = 0; // локальная переменная
Thread t = new Thread(new ThreadStart(delegate { while (true)d++; }));
C.WriteLine(d);// Печать начального значения
t.Start();
// Нить становится активной;
// метод,делегированный нити t,
// начинает работать – значение d начинает расти
while (true)
// Основная нить обменивается информацией с пользователем
if (C.ReadLine() == "")
C.WriteLine(d);// Когда пользователь нажимает Enter, он видит значение d
else break;
// Набор любого символа и Enter прерывает цикл
if (t.IsAlive)
// если нить t активна
t.Abort();
// нить t прекращает работу
Основная нить блокируется методом ReadLine класса Console и позволяет вновь
образованной нити t делать свою работу – выполнять код делегированного ей метода.
Активизируйте код.
Каждая нить имеет свой номер, возвращаемый свойством ManagedThreadId. Перед
активизацией нити t (строка t.Start();) добавьте строку кода
C.WriteLine("mainId={0};tId={1}",
Thread.CurrentThread.ManagedThreadId,t.ManagedThreadId);
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
122
В результате распечатаются номера обеих нитей – главной CurrentThread и нити t.
10.2 Асинхронное делегирование
Метод асинхронного делегирования очень похож на рассмотренный метод. Разница в том,
что не создается новый объект-нить, а используется уже готовая нить из «пула» нитей.
Такой пул есть у среды по умолчанию.
Добавьте к имеющемуся коду перед активизацией нити t строку кода, распечатывающую
свойство IsThreadPoolThread – принадлежит ли нить t «пулу»
C.WriteLine("tIsPooled:{0}", t.IsThreadPoolThread);
Результат должен быть отрицательным (False).
При асинхронном делегировании метод, код которого должен выполняться в отдельной
нити (в нашем примере это код while (true)d++;), передается в начале объекту делегата,
как и в предыдущем примере. Тип делегата с сигнатурой передаваемого метода
описывается в коде. Пусть в нашем случае это будет делегат с простейшей сигнатурой
delegate void Ad();
Опишите его внутри класса ThreadTest.
Внутри метода Main уберите весь предыдущий код (окружите его комментаторскими
скобками /**/), объявите экземпляр ad делегата Ad и делегируйте ему указанный метод
явным образом
decimal d = 0;
Ad ad = new Ad( delegate {while (true) d++;});
Теперь, после набора ad. (с точкой) должен появиться список не статических членов
делегата ad. Среди них есть два метода BeginInvoke и EndInvoke, используемые в
асинхронном программировании:
 Метод BeginInvoke «поручает» нити из «пула» выполнение делегируемого метода
и на этом заканчивает свою работу. Метод BeginInvoke создается транслятором, и
в число его параметров входят все параметры делегируемого метода. В данном
случае у делегируемого метода нет параметров. У метода BeginInvoke есть два
собственных параметра, используемые для связи с созданной им нитью – делегат,
вызываемый после завершения работы нити, и объект, который может служить для
обработки результатов работы нити. Если параметры не используются, им
передаются null. Метод BeginInvoke возвращает объект типа IAsyncResult,
свойства и методы которого позволяют следить за состоянием созданной нити.
 Метод EndInvoke ожидает окончания работы делегируемого метода, получает
после окончания работы все его параметры-ссылки (с модификаторами ref и out)
и возвращает тип делегируемого метода. В нашем случае у метода EndInvoke один
собственный параметр и он возвращает void, как и делегируемый метод.
Рекомендуется всегда вызывать метод EndInvoke.
В методе Main в продолжение имеющегося кода наберите
C.WriteLine(d);
IAsyncResult ar = ad.BeginInvoke(null, null);
while (true)
if (C.ReadLine() == "")
C.WriteLine(d);
else break;
ad.EndInvoke(ar);
C.WriteLine(d);
C.ReadLine();
Посмотрите результат. Код работает, как и прежде, но…. Попытка выйти из цикла while
(true) в главной нити нажатием любого символа и Enter приводит к методу EndInvoke,
который ждет окончания работы нити. Внутри делегируемого метода стоит другой
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
123
бесконечный цикл while (true), поэтому ждать придется долго. Сделайте этот
последний цикл конечным.
Опишите в классе ThreadTest константу
const decimal dmax = 1e6m;
В коде делегируемого метода (делегат ad) замените в цикле while (true) условие true
условием (d < dmax), чтобы цикл стал конечным. Вновь активизируйте приложение.
Теперь, после выхода из цикла while (true) главной нити EndInvoke ждет не долго.
Можно организовать код так, чтобы окончание работы цикла главной нити while (true)
проходило естественным путем после окончания работы делегируемого метода. Из-за
условия true выхода из цикла главной нити цикл прерывается лишь при нажатии клавиш.
У интерфейса IAsyncResult есть свойство IsCompleted, которое принимает значение
true после завершения работы делегируемого метода. Замените в главной нити цикл
while (true) циклом while (!ar.IsCompleted). В этом случае цикл главной нити
должен прерываться не только нажатием клавиш, но и окончанием делегируемого метода.
Испытайте код.
Ожидаемый эффект – автоматический выход из всех циклов к последним операторам
C.WriteLine(d);C.ReadLine();, не получится! Это связано с блокировкой, производимой
методом ReadLine внутри цикла главной нити – условие !ar.IsCompleted не
проверяется, пока ReadLine не выполнится. Наберите весь цикл главной нити в виде
while (!ar.IsCompleted)
if (C.KeyAvailable)
if (C.ReadLine() == "")
C.WriteLine(d);
else break;
Свойство KeyAvailable класса Console возвращает false, если ни одна из клавиш не
нажата, но не ждет, как ReadLine, нажатия клавиши. Теперь цикл главной нити будет
непрерывно проверять значение ar.IsCompleted, и должен завершиться автоматически,
когда это значение станет true.
Вопросы для самоконтроля
1. Как создается и используется объект класса Thread?
2. Технология асинхронного делегирования.
Занятие 11. Ввод-вывод (System.IO)
Добавьте новый проект типа ConsoleApplictaion к Solution1; назовите его IOTestProject;
появившийся файл с кодом переименуйте в IOTest.cs - автоматически шаблон класса в этом
файле будет переименован в IOTest. Включите для проекта StartUp Project и добавьте в
список using две строки
using System.IO;
using C = System.Console;
11.1 Возможности класса Console
Почти все предыдущие примеры использовали не только операторы, обрабатывающие
информацию, но и методы типа Read и Write класса Console, которые переносили
информацию.
В случае класса Console методы Read и Write общаются по умолчанию со стандартными
потоками ввода-вывода. Стандартный поток ввода направлен от клавиатуры, а вывода –
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
124
на экран дисплея, в специальное окно операционной системы, через которое пользователь
общается с консольным приложением («черное окно»). Говорят, что методы типа Read
«читают из потока», а Write – «пишут в поток».
Стандартные потоки создаются методами OpenStandardInput и OpenStandardOutput
класса Console и являются наследниками абстрактного класса Stream (поток). Проверьте
это кодом
C.WriteLine(C.OpenStandardInput().GetType().BaseType.Name);
C.WriteLine(C.OpenStandardOutput().GetType().BaseType.Name);
C.ReadLine();
Обращаясь к Read или Write, класс Console вызывает соответствующие методы объектов,
ссылки на которые возвращаются свойствами Console.In и Console.Out.
Проверьте это кодом в методе Main класса IOTest
C.Out.WriteLine("Hi!");
C.In.ReadLine();
По умолчанию свойства In и Out класса Console возвращают ссылки на объекты классов
– наследников абстрактных классов TextReader и TextWriter.
Убедитесь в этом, набрав код
C.WriteLine(C.In.GetType().BaseType.Name);
C.WriteLine(C.Out.GetType().BaseType.Name);
C.ReadLine();
Классы TextReader и TextWriter и их наследники потоками не являются и в каком-либо
«родстве» с классом Stream не состоят. В общем случае объекты этих и подобных им
пишущих и читающих классов связаны с некоторым абстрактным «устройством» (device),
способным хранить информацию. Среди наследников классов TextReader и TextWriter

классы
StreamReader, StreamWriter. Объекты
StreamWriter могут общаться только с потоками -
классов StreamReader и
наследниками класса Stream:
конструкторы объектов содержат параметр, явно или неявно указывающий на тип
потока - носителя информации, и у всех у них есть свойство BaseStream,
возвращающее поток. Потоки различаются хранилищами информации (backing store)
- файлы на диске (FileStream), память (MemoryStream), сеть (NetworkStream) и т.п.
 классы StringReader, StringWriter. Объекты этих классов обмениваются
информацией со строкой – объектом типа string или StringBuilder.
Стандартные потоки, используемые классом Console, могут быть заменены любыми
другими устройствами. В классе Console существуют методы SetIn и SetOut,
позволяющие заменить один объект, реализующий методы ввода (вывода), другим.
Наберите код, выбрав в качестве «пишущего» класса StreamWriter, а в качестве потока
класс FileStream - поток записи в файл. Для этого достаточно вызвать конструктор
класса StreamWriter, указав имя файла (возможно, с маршрутом), в который поток
переносит информацию
StreamWriter sw = new StreamWriter("file.txt");
C.SetOut(sw);
C.WriteLine(sw.BaseStream.GetType().Name);
C.ReadLine();
Окно останется пустым, но результатом должно быть появление файла file.txt в
каталоге IOTestProject\bin\debug. (Относительный маршрут к каталогу вывода можно найти,
открыв окно свойств проекта командой Project.IOTestProject Properties…, выбрать закладку
Build и в открывшейся справа внизу панели Output посмотреть строку OutputPath).
Остановите приложение. Откройте файл file.txt командой File.Open.File…и убедитесь, что
он… пуст. Сотрите файл file.txt.
Причина проста. После использования устройство должно быть освобождено. Для этого
у объектов класса StreamWriter существует метод Close. Этот метод освобождает не
только сам пишущий объект sw, но и связанный с ним поток BaseStream, который в свою
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
125
очередь освобождает занятое устройство. Перед строкой C.ReadLine() добавьте вызов
этого метода
sw.Close();
Вновь активизируйте приложение, закройте его и посмотрите содержание файла
file.txt. Результатом должна быть строка FileStream – имя класса потока,
обслужившего запись в файл.
11.2 Оператор using
Сотрите файл и проведите еще одну редакцию кода
using (StreamWriter sw = new StreamWriter("file.txt"))
{
C.SetOut(sw);
C.WriteLine(sw.BaseStream.GetType().Name);
}
C.ReadLine();
В этой редакции код будет работать нормально – так, как если бы метод Close был вызван
явно. Это пример использования стандартного оператора using, который автоматически
освобождает объект (в данном случае закрывает объект sw) после своего завершения.
Перед вызовом C.ReadLine()добавьте строку кода
C.SetIn(TextReader.Null);
Статическое свойство Null класса TextReader
возвращает объект класса NullTextReader,
читающий из «ниоткуда». Поэтому, приложение не будет ждать нажатия Enter для своего
завершения.
Сотрите последнюю строку
C.SetIn(TextReader.Null);
Для возврата к выводу в стандартный поток следует набрать код
StreamWriter so = new StreamWriter(C.OpenStandardOutput());
C.SetOut(so);
C.WriteLine(C.Out.GetType().Name);
Проверьте его работу. Ожидаемый вывод в черное окно, скорее всего не произойдет. Но
если добавить вызов метода
so.Flush();
то все будет нормально.
Метод Flush очищает буфер, выводя его содержимое в поток (в данном случае – поток
направлен на дисплей). У объектов StreamWriter есть свойство AutoFlush, которое,
будучи установленным true, приводит к автоматической очистке буфера. Вставьте строку
so.AutoFlush = true;
перед вызовом WriteLine
и уберите Flush. Испытайте новую редакцию кода.
11.3 Бинарный ввод-вывод
Наряду с классами TextReader и TextWriter и их наследниками, отвечающими за
передачу символьной информации, существуют классы BinaryReader, BinaryWriter,
объекты которых читают и пишут информацию в бинарном виде. Классы BinaryReader,
BinaryWriter связаны с потоками – их объекты имеют свойство BaseStream, как и
объекты классов StreamReader, StreamWriter.
В методе Main класса IOTest объявите несколько локальных переменных разного типа
int i = int.MaxValue;
double d = double.NegativeInfinity;
string s = "Hi!";
DateTime dt = DateTime.Now;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
126
bool? b = true;
Random rnd = new Random();// Класс случайных чисел
byte[] buffer = new byte[10];
rnd.NextBytes(buffer); // buffer-массив из 10 байт со случайными значениями
Дальше наберите код, который пишет эти переменные в бинарный файл с именем misc
FileStream fs = new FileStream("misc", FileMode.Create);
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write(i); bw.Write(d); bw.Write(s);
bw.Write(dt.ToBinary()); bw.Write(b.Value);
bw.Write(buffer);
}
C.ReadLine();
Записью в файловый поток fs занимаются различные версии метода Write объекта bw
типа BinaryWriter.
Активизируйте код и посмотрите содержание файла misc в том же каталоге
IOTestProject\bin\debug\ с помощью команды File.Open.File…. Файл misc бинарный, поэтому
будут распечатаны байты файла в 16-ричной системе и символы, им соответствующие.
Добавьте код, читающий и распечатывающий бинарный файл misc, используя методы
чтения различных типов объектов класса BinaryReader
BinaryReader br = new BinaryReader(new FileStream("misc", FileMode.Open));
C.WriteLine("i={0}", br.ReadInt32());
C.WriteLine("d={0}", br.ReadDouble());
C.WriteLine("s={0}", br.ReadString());
C.WriteLine("dt={0}",DateTime.FromBinary(br.ReadInt64()));
C.WriteLine("b={0}", br.ReadBoolean());
byte[] rb=br.ReadBytes(buffer.Length);
foreach (byte bt in rb)
C.Write("{0}\t", bt);
C.ReadLine();
Посмотрите результат. Обратите внимание на нумератор FileMode, используемый в
конструкторе файлового потока. Этот нумератор перечисляет возможные режимы работы
с файлом при его создании, записи, чтении.
Классы потоков имеют собственные методы чтения-записи. Наберите код,
демонстрирующий работу этих методов
byte[] buffer = new byte[5] { 1, 2, 3, 5, 7 };
using (FileStream fw = new FileStream("bytes", FileMode.Create))
fw.Write(buffer, 0, 5);//Пишет 5 байтов, начиная с нулевого
using (FileStream fr = new FileStream("bytes", FileMode.Open))
{
//Метод Seek устанавливает поток в позицию,сдвинутую на 4,считая с начала
fr.Seek(4, SeekOrigin.Begin);
C.WriteLine(fr.ReadByte());//Читает один байт в текущей позиции
}
C.ReadLine();
Посмотрите результат, в том числе и файл bytes, созданный приложением.
Наберите еще один пример, в котором используется класс Buffer, позволяющий
представить любой массив в виде последовательности байтов. В частности, можно
увидеть внутренний формат объекта типа double.
//Посмотрите результат при разных значениях объекта double и для разных типов
double[] d = new double[1] { double.NaN };
//Оператор sizeof возвращает размер объекта указываемого типа
byte[] buffer = new byte[sizeof(double)];
for (int i = 0; i < buffer.Length; i++)
//Буфер заполняется байтами,из которых состоит объект типа double
buffer[i] = Buffer.GetByte(d, i);
using (FileStream fw = new FileStream("bytes", FileMode.Create))
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
127
fw.Write(buffer, 0, buffer.Length);//В поток записываются все байты буфера
using (FileStream fr = new FileStream("bytes", FileMode.Open))
{
while (fr.Position < fr.Length)//Считываются все байты из потока
C.WriteLine(fr.ReadByte());
}
C.ReadLine();
Посмотрите результаты этого кода для разных значений объекта типа double и для
объектов других типов.
11.4 Файловая система. Манипуляции с каталогами и файлами
Информацию о файловой системе и манипуляции с ней обеспечивают методы и свойства
классов Path, Directory, File и наследников FileInfo и DirectoryInfo абстрактного
класса FileSystemInfo из пространства имен System.IO.
Например, абстрактный класс Path определяет все «платформозависимые» элементы
маршрутной строки (path). В частности, статическое поле DirectorySeparatorChar класса
Path содержит символ, разделяющий каталоги в маршрутной строке (в Unix это slash “/”, а
в Windows и Macintosh это backslash “\”), а поле VolumeSeparatorChar – символ,
отделяющий имя устройства (volume) от остальной части маршрутной строки (это “:” для
Windows и Macintosh и “/” для Unix).
Наберите текст, распечатывающий все символы, недопустимые в маршрутной строке с
помощью метода GetInvalidPathChars
foreach (char ch in Path.GetInvalidPathChars())
C.WriteLine("{0}\t{1}",(int)ch<32?' ':ch,(int)ch);
C.ReadLine();
Если в этом же коде вызвать метод GetInvalidFileNameChars,
то получим список
символов, недопустимых в имени файла.
Другой пример показывает, как с помощью класса Path создать временный пустой файл
string tmp = Path.GetTempFileName();
C.WriteLine("tmp={0}",tmp);
Посмотрите, появился ли этот файл в файловой системе.
Проанализируйте маршрутную строку вновь созданного временного файла
//есть ли у маршрута корневая часть
C.WriteLine("IsPathRooted={0}",Path.IsPathRooted(tmp));
C.WriteLine("FileName:{0}",Path.GetFileName(tmp)); // имя файла с раширением
C.WriteLine("PathRoot:{0}", Path.GetPathRoot(tmp));// корневая часть маршрута
C.WriteLine("Extension:{0}", Path.GetExtension(tmp));
// расширение
C.WriteLine("Directory:{0}", Path.GetDirectoryName(tmp));// каталог
Класс Directory позволяет манипулировать каталогами файловой системы.
Наберите пример, в котором метод CreateDirectory создает каталог с заданным
маршрутом – внутри каталога временного хранения, полученного методом
Path.GetTempPath, создается каталог с именем One.
C.WriteLine(
Directory.CreateDirectory(Path.GetTempPath()+@"\One").Attributes);
Метод CreateDirectory возвращает объект класса DirectoryInfo, чье свойство
Attributes распечатывается приведенным кодом. Проверьте, появился ли каталог One в
нужном месте.
Следующий пример
string cd;
C.WriteLine("cd={0};time={1}",
cd=Directory.GetCurrentDirectory(),Directory.GetCreationTime(cd));
foreach (string f in Directory.GetFiles(cd))
C.WriteLine(Path.GetFileNameWithoutExtension(f));
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
128
показывает текущий каталог, время его создания и распечатывает имена без расширений
всех файлов, находящихся в каталоге.
Класс Directory является «стерильным» - модификатор sealed не позволяет ему иметь
наследников. К тому же все доступные члены класса Directory являются статическими
методами. Такими же свойствами обладает и класс File, который позволяет
манипулировать отдельными файлами.
Наберите пример
File.AppendAllText(Path.GetTempPath()+"Hello.txt", "Hello!");
Проверьте, появился ли файл Hello.txt в каталоге временного хранения
строка Hello!.
и есть ли в нем
Добавьте код
string source = Path.GetTempPath() + "Hello.txt",
dest =Directory.GetCurrentDirectory() + @"\Hi!";
if (!File.Exists(dest))
{
File.Move(source, dest);//Файл перемещается в новое место с новым именем
File.WriteAllBytes(dest, new byte[1] { 65 });//записывается бинарный массив
}
Теперь файл Hello.txt, образующийся в каталоге временного хранения, должен
оказаться в текущем каталоге под именем Hi! и не со строкой Hello!, а с символом A
(код 65). Проверьте, остается ли на месте прежний файл и появляется ли новый.
Следующий код демонстрирует поиск всех файлов с расширением .txt в текущем
каталоге
File.AppendAllText(Directory.GetCurrentDirectory()+@"\Hello.txt", "Hello!");
DirectoryInfo di=new DirectoryInfo(Directory.GetCurrentDirectory());
FileSystemInfo[] files = di.GetFileSystemInfos("*.txt");
C.WriteLine("Numder of files={0}", files.Length);
C.ReadLine();
Посмотрите результат.
11.5 Изолированная область хранения файлов (isolated storage)
используется для хранения данных, формируемых пользовательскими настройками
приложений, приложениями и компонентами, загружаемыми из сети, и т.д. Изоляция
осуществляется на уровне пользователей-клиентов, на уровне приложений и/или
отдельных сборок (assemblies). Физическое размещение изолированных файлов зависит
от операционной системы и для программного кода не существенно. Обычно
изолированные области (stores) находятся в каталогах пользовательского профиля по
маршрутам <user>\Local Settings\Application Data\... либо <user>\Application Data\....
Типы изоляции областей хранения характеризуются значениями объектов перечислимого
типа IsolatedStorageScope из пространства имен System.IO.IsolatedStorage.
Для манипуляций с файловой системой изолированной области хранения используются
объекты класса IsolatedStorageFile, из того же пространства имен. Класс
IsolatedStorageFile является наследником абстрактного класса IsolatedStorage. У
него нет конструкторов с открытым доступом. Объекты класса IsolatedStorageFile
создаются его статическими методами GetStore, GetUserStoreForAssembly,
GetUserStoreForDomain и т.д., освобождаются методом Close и уничтожаются вместе с
их содержимым статическим методом Remove, либо его нестатической версией.
Первым параметром общего метода GetStore (имеющего довольно много версий)
является указание на тип изоляции файла. Специализированные методы без параметров
создают объекты с конкретным типом изоляции. Например, методы
GetUserStoreForAssembly и GetUserStoreForDomain создают объекты, изолирующие
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
129
область хранения по данным «пользователь + сборка» и «пользователь + сборка +
приложение» соответственно. Эти методы действуют так же, как если бы первым
параметром метода GetStore был IsolatedStorageScope.User |
IsolatedStorageScope.Assembly и, соответственно, IsolatedStorageScope.User |
IsolatedStorageScope.Assembly | IsolatedStorageScope.Domain.
Создав объект типа IsolatedStorageFile, следует связать его с файловым потоком типа
IsolatedStorageFileStream – наследником класса FileStream. При этом с одним
объектом типа IsolatedStorageFile можно связать любое число потоков типа
IsolatedStorageFileStream и, соответственно, любое число физических файлов.
Наберите пример кода
//Создаются два объекта изолированных областей хранения (stores):
//assStore - изолированный по пользователю и сборке и
//dmbStore - изолированный по пользователю, сборке и приложению
IsolatedStorageFile assStore = IsolatedStorageFile.GetUserStoreForAssembly(),
dmnStore = IsolatedStorageFile.GetUserStoreForDomain();
//для объекта assStore создаются и используются для записи
//два потока sass1 и sass2
using (Stream sass1 = new IsolatedStorageFileStream("AssemblyData1",
FileMode.Create, assStore))
using (StreamWriter sw = new StreamWriter(sass1))
sw.Write("Данные 1 для сборки");
using (Stream sass2 = new IsolatedStorageFileStream("AssemblyData2",
FileMode.Create, assStore))
using (StreamWriter sw = new StreamWriter(sass2))
sw.Write("Данные 2 для сборки");
//для объекта dmnStore создается и используется для записи
//один поток sdmn
using (Stream sdmn = new IsolatedStorageFileStream("AppDomainData",
FileMode.Create, dmnStore))
using (StreamWriter sw = new StreamWriter(sdmn))
sw.Write("Данные для приложения");
C.ReadLine();
Найдите созданные файлы AssemblyData1, AssemblyData2 и AppDomainData в файловой
системе и прочтите содержимое этих файлов командой File.Open.File….
Аналогично реализуется обратная операция чтения файлов, соответствующих
определенным объектам изоляции. Например, наберите в продолжение предыдущего код
//Поток s используется для соединения с файлом
using (Stream s = new IsolatedStorageFileStream("AppDomainData",
FileMode.Open, dmnStore))
//Чтение и распечатка содержимого потока
using (StreamReader sr = new StreamReader(s))
C.WriteLine(sr.ReadLine());
Посмотрите результат.
Метод CreateDirectory объектов типа IsolatedStorageFile создает каталог внутри
области изолированного хранения данного типа, методы DeleteDirectory и DeleteFile
уничтожают каталог и конкретный файл, методы GetDirectoryNames и GetFileNames
возвращают списки каталогов и файлов данного объекта.
В качестве упражнения испытайте различные варианты кода с применением указанных
методов. Имейте в виду, что перед вызовом статической версии метода Remove все
объекты класса IsolatedStorageFile, подвергающиеся уничтожению, должны быть
закрыты методом Close.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
130
11.6 Сохранение объектов в форме файлов
Использование атрибута [Serializable] перед описанием класса делает объекты этого
класса «годными для хранения и перемещения» (serializable). Объекты такого класса можно
в компактной форме хранить в памяти, переносить на внешний носитель или передавать
по сети.
Например, наберите внутри класса IOTest описание класса sClass с одним полем i в
форме
[Serializable]
class sClass
{
public int i;
}
Добавьте к списку using в начале файла IOTestProject строку
using System.Runtime.Serialization.Formatters.Binary;
В этом пространстве имен находится класс BinaryFormatter, который мы будем
использовать для бинарного форматирования объекта класса sClass.
В методе Main класса IOTest наберите код
sClass sclass = new sClass();//Создается экземпляр sclass класса sClass
sclass.i = 99;
//Полю i присваивается (произвольное) значение 99
//Объект serializer класса BinaryFormatter будет использоваться для
//бинарного форматирования текущего состояния объекта sclass
BinaryFormatter serializer = new BinaryFormatter();
//В каталоге “Мои документы” (Personal)
//текущего профиля (SpecialFolder)
//в файле Saved.sclass (произвольное имя)
//будет храниться бинарная копия объекта sclass.
//Полное имя файла fileName.
string fileName = System.Environment.GetFolderPath
(System.Environment.SpecialFolder.Personal) + @"\Saved.sclass";
//Методом Create класса File
//создается поток для связи с файлом fileName
using (Stream aFileStream = File.Create(fileName))
//Методом Serialize объекта serializer в поток aFileStream,
//связанный с файлом fileName,
//направляется текущее состояние объекта sclass в бинарной форме
serializer.Serialize(aFileStream, sclass);
//Полное имя созданного файла выводится в черное окно
C.WriteLine(fileName);
C.ReadLine();
В результате выполнения этого кода текущее содержимое объекта sclass будет
сохранено в бинарном файле Saved.sclass каталога «Мои документы». Проверьте.
Обратная операция восстановления объекта из файла (deserialisation) требует аналогичного
кода, который следует добавить к уже имеющемуся коду в методе Main
//В переменную rsClass будет помещен объект класса sClass
//после его восстановления из файла fileName
sClass rsClass;
//Создается объект класса BinaryFormatter
BinaryFormatter deserializer = new BinaryFormatter();
//Метод OpenRead класса File создает файловый поток из файла fileName
using (Stream aFileStream = File.OpenRead(fileName))
//Метод Deserialize объекта deserializer возвращает
//объект класса sClass из файлового потока
rsClass = (sClass)(deserializer.Deserialize(aFileStream));
C.WriteLine(rsClass.i);//В черном окне распечатывается значение поля i
C.ReadLine();
Проверьте.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
131
11.7 Класс SerialPort
из пространства имен System.IO.Ports может быть полезным при программировании
обмена информацией с последовательными портами.
Добавьте в список using строку
using System.IO.Ports;
Объекты класса SerialPort обладают свойствами и методами, которые позволяют
открывать (Open) и закрывать (Close) доступ к порту, считывать информацию (методы
типа Read) и писать информацию (методы типа Write), использовать объект потока
(BaseStream), регулировать скорость обмена информацией (BaudRate), объем буферов
обмена (WriteBufferSize, ReadBufferSize) и т.д. Наберите в методе Main код
foreach (string s in SerialPort.GetPortNames())
C.WriteLine("Port \"{0}\"",s);//Перечисление имен всех портов
using (SerialPort sp = new SerialPort())//создается объект порта по умолчанию
{
C.WriteLine("BaudRate={0}",sp.BaudRate);// Скорость передачи информации
//Размер буфера вывода
C.WriteLine("WriteBufferSize={0}", sp.WriteBufferSize);
sp.Open();
// Порт открывается
sp.Write("Hi!");
// В порт пишется строка
}
C.ReadLine();
Посмотрите результат.
У класса SerialPort есть некоторое количество свойств-делегатов, которые определяются
в C# служебным словом event (событие).
Пусть необходимо как-либо отреагировать на такое событие, как освобождение порта. У
объектов класса SerialPort есть событие Disposed, наступающее непосредственно перед
освобождением порта. В качестве одного из операторов в блоке using предыдущего кода
(после любой строки C.WriteLine(…)) начните набирать код sp.Disposed+=. В этой точке
набора среда подскажет текст, который следует набрать дальше. Нажмите клавишу Tab,
позволив среде вставить этот текст. Следует новая подсказка. Новое нажатие Tab позволит
среде набрать шаблон обработчика события – метода, который будет делегирован объекту
sp в качестве обработчика события освобождения порта.
В результате
 в методе Main добавится строка
sp.Disposed+=new EventHandler(sp_Disposed);
Здесь EventHandler - класс делегатов, описанный в System; весь оператор есть
добавка в цепочку методов делегата sp.Disposed (обработчиков события) нового
метода с именем sp_Disposed.

В классе IOTest появится описание этого метода
static void sp_Disposed(object sender, EventArgs e)
{
// Возможно, здесь появится также оператор вида
// throw new Exception("The method or operation is not implemented.");
// Его можно закрыть комментарием, как в этом тексте
}
Поместите внутрь метода sp_Disposed код
C.WriteLine("Port \"{0}\" is open? {1}", (sender as SerialPort).PortName,
(sender as SerialPort).IsOpen);
C.ReadLine();
Обратите внимание на использование параметра sender и оператора as. Параметр sender
(посыльный) ссылается на объект, «возбуждающий» событие. В данном случае
посыльным является объект sp типа SerialPort. В обработчике используется этот факт.
Но, так как свойства PortName и IsOpen присущи именно SerialPort, а не object
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
132
(параметр sender описан как object), то необходимо рассматривать параметр sender как
объект типа SerialPort. Это и делает оператор as. Оператор as указывает, что ссылка на
sender должна рассматриваться как ссылка на объект типа SerialPort.
Посмотрите и проанализируйте результат.
11.8 События (event)
При работе с делегатами, описанными как события, к ним применимы только две
операции += и -=. Первый оператор добавляет метод, делегируемый в качестве
обработчика, а второй оператор убирает делегируемый метод из цепочки делегируемых
методов. В первом случае вызывается метод доступа add, связанный с описанным
событием, а во втором случае – метод доступа remove.
Методы доступа могут не указываться при описании события.
Например, наберите внутри класса IOTest описание делегата aDlgtType и класса aClass с
описанием события anEvent типа этого делегата.
delegate void aDlgtType(object sender);
class aClass
{
internal event aDlgtType anEvent;
internal void DoSmth()
{
if (anEvent != null) anEvent(this);
}
}
В классе aClass есть метод DoSmth, выполнение которого
собственно и является
«событием» и внутри которого должны вызываться делегируемые (содержащиеся в
anEvent) методы, если они вообще там есть (anEvent != null). В качестве фактического
параметра sender делегируемого метода в anEvent подставляется текущий экземпляр
this объекта класса aClass.
После этого в методе Main наберите код, использующий событие anEvent. После набора
символов ac.anEvent += используйте подсказки среды и нажимайте клавишу Tab.
aClass ac = new aClass();
ac.anEvent += new aDlgtType(ac_anEvent);
ac.DoSmth();
Среда должна набрать шаблон метода
static void ac_anEvent(object sender)
{
throw new Exception("The method or operation is not implemented.");
}
Этот метод делегируется событию anEvent. Префикс ac_ в названии метода-обработчика
события содержит имя объекта ac, которому принадлежит событие, и который будет
выступать в качестве this.
Закомментируйте оператор throw и наберите внутри метода ac_anEvent код
C.WriteLine("Объект класса {0} вызвал DoSmth", sender.ToString());
C.ReadLine();
Активизируйте приложение и посмотрите результат.
Теперь добавьте еще один обработчик к тому же событию anEvent. Для этого, после
строки
ac.anEvent += new aDlgtType(ac_anEvent);
наберите подобную ей строку кода
ac.anEvent += new aDlgtType(ac2_anEvent);
На этот раз не следуйте слепо подсказкам среды, а назовите обработчик по-своему
ac2_anEvent. Наберите код нового обработчика ac2_anEvent
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
133
static void ac2_anEvent(object sender)
{
C.WriteLine("++Объект класса {0} вызвал DoSmth", sender.ToString());
C.ReadLine();
}
Уберите строку C.ReadLine();в теле первого обработчика ac_anEvent. Вновь проверьте
результат работы приложения. Оба обработчика должны работать в той
последовательности, в которой они подсоединены к событию anEvent.
Добавьте после новой строки
ac.anEvent +=new aDlgtType(ac2_anEvent);
еще одну строку кода
ac.anEvent -= new aDlgtType(ac_anEvent);
Это уберет первый обработчик ac_anEvent из цепочки
обработчиков события anEvent.
Посмотрите результат.
Использование событий вместо делегатов имеет одно важное преимущество. Описав в
классе некоторое событие, можно при инициализации класса присоединить к этому
событию делегата, описанного в разделе private того же класса. Этот делегат будет
вызываться всегда, когда наступает описанное событие. Пользователь класса может
только присоединить операцией += к уже имеющемуся в событии делегату своего
делегата, либо убрать своего делегата обратной операцией -=. Но он не может очистить
событие от всех делегатов, либо убрать из цепочки делегата, описанного в недоступной
ему области класса.
11.9 Методы доступа add и remove
Можно явно описать методы доступа add и remove, которыми пользуется событие
anEvent. Для этого следует несколько изменить описание класса aClass
сlass aClass
{
aDlgtType anevent;
internal event aDlgtType anEvent
{
add
{
anevent += value; // добавление делегируемого метода-обработчика
C.WriteLine("обработчик {0} добавлен", value.Method.Name);
}
remove
{
anevent -= value; // удаление делегируемого метода-обработчика
C.WriteLine("обработчик {0} удален", value.Method.Name); }
}
internal void DoSmth()
{
if (anevent != null) anevent(this);
}
}
В этом примере методы add и remove не только выполняют свое основное назначение добавляют и убирают делегируемые методы-обработчики события anEvent, но также
печатают операторами C.WriteLine некоторую информацию в окне консоли. Посмотрите
результат работы кода.
Обратите внимание на то, что при явном описании методов add и remove в классе aClass
необходимо описать поле anevent типа aDlgtType, которое хранит цепочку вызываемых
методов. Содержание поля anevent меняется методами add и remove, параметр value в
которых содержит ссылку на добавляемый (в add) или убираемый (в remove) обработчик.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
134
Из этих примеров следует, что член класса event подобен свойству, типом которого
является класс делегата.
Вопросы для самоконтроля
1.
2.
3.
4.
5.
6.
7.
8.
Что такое стандартные потоки ввода-вывода?
Использование оператора using.
Как создать файл для временного хранения информации?
Для чего используется изолированная область хранения файлов?
Как сохраняется состояние объекта в файловой системе?
Как работает оператор as, и что несет в себе параметр sender?
Синтаксис описания и использования событий.
В чем преимущество использования событий, а не делегатов?
Занятие 12. Документирование кода
Документирование элементов кода – классов, их членов – полей, методов, свойств и т.д.
является составной частью действий программиста при составлении приложения.
Примерами документирования являются статьи справочной системы MSDN, описывающие
стандартные классы и их элементы.
Информация, предлагаемая пользователю в статьях MSDN, стандартным образом
структурирована. Отдельные элементы этой структуры отражаются и в других
справочных инструментах. Например, наберите в методе Main ссылку на класс
SerialPort и наведите на это имя курсор. На экране появится справочное окошко (hint) с
надписью “Class System.IO.Ports.SerialPort Represents a serial port resource.”. Та же надпись
появляется и в списке, предлагаемом IntelliSense, при наведении курсора на элемент списка
SerialPort. В статье справочной системы, посвященной классу SerialPort, первой строкой
будет та же строка, кратко указывающая на смысл класса SerialPort. Эту статью можно
получить, если поставить каретку на имя класса в тексте программы и нажать клавишу F1.
12.1 Object Browser
Строку с кратким описанием класса SerialPort можно обнаружить и с помощью еще одного
справочного инструмента Object Browser, окно которого открывается командой View.Other
Windows.Object Browser. На левой панели окна находится древесная структура “Browse: All
Components”. В ней сведены все присутствующие в solution1 проекты и доступные проектам
пространства имен и классы. В этой панели раскройте узел system, в нем пространство
имен System.Io.Ports, далее наведите и щелкните по имени класса SerialPort. На правой
нижней панели появится в частности надпись “Summary: Represents a serial port resource”.
Термин Summary (краткое изложение) есть один из элементов упомянутой выше структуры
справочной статьи о классе SerialPort.
12.2 Документирование проекта
Среда предоставляет возможность структурного документирования создаваемых проектов
по типу статей справочной системы.
Для этого, в свойствах текущего проекта IOTestProject (окно свойств открывается по
команде Project.IOTestProject Properties… или из окна Solution Explorer по правому щелчку над
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
135
проектом IOTestProject и командой Properties…) выберите закладку Build и в открывшейся
справа внизу панели Output установите флажок XML Documentation File. При этом в правом от
флажка редакционном окошке должен оказаться маршрут и имя файла с расширением .xml
(маршрут и имя, конечно, можно выбирать по вкусу).
После настройки среды на вывод документации в указанный файл будет записываться
информация, документирующая проект в специальном текстовом формате XML (eXtensible
Markup Language). Текст в формате XML состоит из содержательной информации и «мета
информации» (или «тэгов»). Тэги отражают структуру содержательной части.
Например, поставьте перед строкой начала описания класса сlass aClass текущего
проекта три символа slash /// подряд. Среда располагает инструментом Smart Comment
Editing, похожим на IntelliSense. Автоматически, в продолжение текста /// инструмент Smart
Comment Editing вставит текст вида
/// <summary>
///
/// </summary>
В оставшейся не заполненной строке наберите текст, поясняющий смысл класса aClass.
Например, пусть эта строка примет вид
/// Представляет пример события
В данном случае будущими тэгами являются <summary> и </summary>. Все тэги в XML–
формате ограничивают обозначаемый ими содержательный текст и выделяются среди
содержательного текста угловыми скобками <>. Тэги существуют парами – первый
ограничивает содержательную информацию слева (или сверху), а второй с тем же именем,
но с дополнительным знаком / – справа (или снизу).
Для проверки работы созданного комментария summary к классу aClass начните набирать
в методе Main его имя. Увидев в списке IntelliSense полное имя класса aClass, наведите на
него курсор. Должна появиться строка, только что вставленная в раздел summary. То же
должно происходить после полного набора имени aClass и наведении на это имя курсора.
Справочное окошко должно содержать вставленную строку. Наконец, в окне Object Browser
в пространстве имен IOTestProject под именем aClass (или IOTest.aClass, если класс aClass
описан внутри класса IOTest) должна содержаться та же информация.
После компиляции новой редакции кода в указанном в настройках каталоге должен
появиться файл с расширением xml. Откройте его командой File.Open.File…и посмотрите
содержимое. Среди информации, касающейся сборки в целом, должны существовать
строки, введенные в текст программы в отношении класса aClass.
XML-файл документации может быть оформлен в виде Code Comment Web Report с помощью
инструмента, вызываемого командой Tools.Build Comment Web Pages… (если этот инструмент
есть в вашей среде).
Существует довольно много рекомендуемых тэгов, используемых в оформлении
документации. Для примера используйте еще один тэг <remarks> в применении к тому
же классу aClass.
Поставьте курсор в конец строки с закрывающим тэгом
/// </summary>
и нажмите Enter. С
новой строки появится знак ///. После этого достаточно набрать
открывающую угловую скобку <, чтобы получить список рекомендуемых тэгов. Выберите
remarks (замечания) и закройте угловую скобку >. Среда добавит закрывающий тэг
</remarks>. Войдя между ними, нажмите Enter, вновь вернитесь в ту же точку и нажмите
Enter еще раз. Появится пустая строка с тройным slash /// в начале. Наберите в этой строке
замечание так, чтобы окончательный комментарий к классу aClass выглядел следующим
образом
/// <summary>
/// Представляет пример события
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
136
///
///
///
///
</summary>
<remarks>
Это тестирующий класс
</remarks>
Наличие замечаний не изменит справки о классе aClass в IntelliSense и в справочном окошке,
но скажется на информации о классе aClass в Object Browser и, естественно, самом XMLфайле. Проверьте.
Еще один пример комментариев. Теперь применим их не к классу в целом, а к методу
ac2_anEvent класса IOTest. Перед описанием метода ac2_anEvent дайте комментарий
/**<summary>
Обработчик класса IOTest
</summary>*/
/// <param name="sender">
/// Объект, создающий событие
/// </param>
Обратите внимание на другой, альтернативный вариант ограничителей комментария
/**…*/. Скомпилируйте проект. Войдите в Object Browser, щелкните по имени класса IOTest.
В правом верхнем окне должен появиться список всех его методов. Щелкните по методу
ac2_anEvent. В нижней части окна увидите только что набранный комментарий.
12.3 Рекомендации к практике программирования. Имена
Опыт показывает, что в практике программирования следует придерживаться некоторых
ограничений, не связанных с синтаксисом языка.
Имена
Имена переменных, использованные в настоящем курсе при написании кода, являются
хорошим примером того, как не следует обозначать переменные. Оправданием этому
является то, что единственной целью любого примера было привлечение внимания к
работе конкретной языковой конструкции. Реализация осмысленного приложения в
качестве примера потребовала бы одновременного вовлечения в код многих элементов
языка. В то же время, при реализации осмысленного приложения следует осмысленно
подходить к выбору имен переменных.
1. Если при выборе имени возникает трудность, то, скорее всего, эта же трудность
существует и в понимании смысла именуемого элемента.
2. Имя должно быть говорящим, но не слишком длинным.
3. При выборе имени следует обращать больше внимания на то
1) для чего используется метод (имя метода),
2) что хранит в себе поле (имя поля),
3) что устанавливает и возвращает свойство (имя свойства обычно совпадает с
именем соответствующего поля: полю currentName отвечает свойство
CurrentName),
4) какому событию могут быть делегированы методы (имя события),
5) какими качествами будет обладать объект, класс которого реализует
интерфейс с данным именем (имя интерфейса принято начинать прописной
буквой I),
6) что представляют собой объекты класса, и какие функции они выполняют
(имя класса)
и т.д. Не следует пытаться отразить в имени то, как выполняются функции.
4. Локальные переменные так же желательно снабжать говорящими именами.
Исключения могут составлять лишь параметры циклов i, j.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
137
5. Избегайте применения в коде явной записи числовых констант (15, 1e-2 и т.п.).
Используйте говорящие имена и описывайте их как const.
6. В именах методов, возвращающих объект, следует отражать смысл этого объекта.
Например, GetName или EditColor.
7. Следите за тем, чтобы изменения в коде отражались и на именах элементов,
подвергшихся изменениям.
8. Осмысленные имена должны иметь не только классы, методы и т.д., но и
пространства имен, файлы и каталоги, участвующие в приложении.
9. Перегруженные методы должны выполнять одну и ту же функцию, отраженную в
их имени.
10. Имена классов не следует повторять в именах его методов и свойств. Если класс
назван Employer, не следует его свойство называть EmployerName, назовите просто
Name.
11. Имена логических (bool) полей и свойств удобнее читать, если они содержат Is или
Has как в HasValue или IsEdited.
12. Избегайте излишних аббревиатур. Использование аббревиатур должно быть
однородным во всем коде. Например, если аббревиатура temp используется для
temporary, не следует ее же использовать для template или temperature.
12.4 Комментарии
Любой код должен обязательно сопровождаться комментариями. Обдумывание
комментария часто серьезно сказывается на самом процессе разработки кода.
1. Комментировать следует все классы и их члены параллельно с процессом
разработки кода. Лучше давать комментарий, отвечающий стандартам XMLдокументирования.
2. Локальный комментарий к коду (внутри методов) лучше располагать в отдельных
строках.
3. Если комментарий делается в конце строки, то его следует выравнивать с
подобным комментарием на соседних строках.
4. Не следует украшать комментарий рамками, или набирать строку звездочек. Это
отвлекает.
5. Комментировать следует то, для чего используется тот или иной фрагмент кода, но
не интерпретировать содержание кода.
6. Отделяйте текст комментария от ограничителей // или /* пробелом.
12.5 Форматирование кода
Внешний вид текста кода значительно ускоряет его чтение и отладку. В среде
разработчика поддержанию формата кода уделено много внимания. Следует использовать
команды Edit.Advanced.Format Document или Format Selection при редактировании кода.
Значительную помощь могут оказать настройки редактора кода, которые устанавливаются
в окне Options (команда Tools.Options…). Так, через узел Environment.Fonts and Colors можно
настроить цветовую палитру элементов кода. Удачная цветовая настройка значительно
облегчает чтение кода, так как несет в себе информацию о типе элементов. Настройка
форматирования находится в узле TextEditor.C#.Formatting.
Каждый оператор вызова метода лучше помещать в отдельной строке. Длина строки
должна иметь разумные ограничения. При необходимости переноса следует подчеркивать
форматом начало и конец полной строки.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
138
Следует избегать сложных и громоздких фрагментов кода, разбивая их на более мелкие и
легко обозримые фрагменты. Используйте для этого логическое разделение кода на
отдельные модули (файлы). Используйте в полной мере возможности среды и объектноориентированной технологии.
Хорошей моделью программирования в C# является библиотека классов .NET.
Занятие 13. Пример оконного приложения в .NET
Строится оконное приложение интегрирования задач механики. Создается интерфейс и
класс, интегрирующий произвольную механическую систему. Приводятся примеры
решения динамической задачи с использованием классов .NET, применяемых при
создании оконного интерфейса.
13.1 Интерфейс механической системы IMechSystem
Положение механической системы характеризуется обобщенными координатами. Число
независимых обобщенных координат q1, q2,…, qs, достаточных для описания положения
системы, называется числом степеней свободы s. Сами обобщенные координаты могут
иметь в принципе любую физическую размерность, но чаще всего это декартовые
координаты с размерностью длины или безразмерные углы.
Состояние движения, или, просто, состояние механической системы характеризуется как
положением, так и набором обобщенных импульсов p1, p2,…, ps, каждый из которых
соответствует отдельной степени свободы, как и координата. Физическая размерность
обобщенного импульса pk равна отношению размерности энергии к размерности
соответствующей обобщенной скорости dqk/dt. Если координата имеет размерность
длины, то соответствующий импульс имеет размерность обычного импульса – количества
движения. Если координата имеет размерность угла, т.е. безразмерна, то обобщенный
импульс, ей соответствующий, является проекцией момента импульса на ось,
относительно которой совершается поворот.
Задача о движении механической системы предполагает, что состояние движения
механической системы непрерывно меняется во времени t согласно уравнениям движения
dqi/dt = Fqi (q, p, t); dpi/dt = Fpi (q, p, t).
Функции Fqi (q, p, t), Fpi (q, p, t) определяют скорость изменения координат и импульсов в
некотором пространстве состояний, или фазовом пространстве. Каждая точка фазового
пространства q1, q2,…, qs, p1, p2,…, ps описывает состояние механической системы, через
которые проходит или может пройти механическая система. Функции Fqi (q, p, t), Fpi (q, p,
t) являются, вообще говоря, функциями состояния, через которое проходит механическая
система в данный момент времени, или текущего состояния. Мы будем называть эти
функции пропагаторами, так как они определяют распространение (propagation)
механической системы в пространстве состояний. Математически механическая система
полностью характеризуется своими уравнениями движения, а, следовательно, своими
пропагаторами.
В начале опишем два типа переменных, которые нам понадобятся в описании членов
интерфейса механической системы.
Естественно описывать состояние, отвечающее одной степени свободы, структурой QP
вида
public struct QP
{
/// <summary>
/// координата
/// </summary>
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
139
public double q;
/// <summary>
/// импульс
/// </summary>
public double p;
};
Пропагаторы будем рассматривать как объекты делегатов типа Propagator
/// <summary>
/// Прототип функций, меняющих состояние системы,
/// стоящих в правой части уравнений движения механической системы
/// </summary>
public delegate double Propagator (ushort i, double time, double [ ] y);
Интерфейс механической системы, который мы назовем IMechSystem, должен описывать
такие свойства как число степеней свободы s, время t, состояние q1, q2,…, qs, p1, p2,…, ps,
пропагаторы уравнений движения. Кроме этого, любое решение задачи о движении
предполагает наличие интегратора, класса, его реализующего, и методов, задающего
начальные условия интегрирования и производящего само интегрирование.
Каждая механическая система должна иметь имя собственное. Поэтому первым членом
интерфейса IMechSystem опишем свойство Name
/// <summary>
/// Возвращает и устанавливает имя механической системы
/// </summary>
string Name { set; get;}
Далее поместим свойства, перечисленные выше
/// <summary>
/// Возвращает число степеней свободы системы
/// </summary>
ushort s { get;}
/// <summary>
/// Возвращает текущее время
/// </summary>
double Time { get;}
/// <summary>
/// Возвращает ссылку на массив текущего состояния системы
/// </summary>
QP [ ] State { get;}
/// <summary>
/// Возвращает текущий пропагатор /// правую часть уравнений изменения координат
/// </summary>
Propagator Fq { get;}
/// <summary>
/// Возвращает текущий пропагатор /// правую часть уравнений изменения импульса
/// </summary>
Propagator Fp { get;}
/// <summary>
/// Возвращает ссылку на текущий экземпляр интегратора
/// </summary>
IIntegrator Integrator { get;}
/// <summary>
/// Устанавливает и возвращает ссылку на текущий тип класса интегратора
/// </summary>
Type IntegratorClass { get;set;}
Наконец, методы интерфейса IMechSystem
/// <summary>
/// Задает начальное состояние движения механической системы
/// </summary>
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
140
/// <param name="startTime">
/// Начальное время
/// </param>
/// <param name="startState">
/// Начальное состояние.
/// Массив записей координат и импульсов для каждой степени свободы.
/// </param>
void BeginFrom (double startTime, QP [ ] startState);
/// <summary>
/// Основной метод интегрирования.
/// Определяет состояние в заданный момент времени
/// </summary>
/// <param name="EndTime">
/// Заданный момент времени
/// </param>
/// <returns>
/// true при нормальном окончании,
/// false, если прервано обработчиком
/// </returns>
bool PropagateTo (double EndTime);
Посмотрите полный текст модуля, содержащего интерфейс IMechSystem.
Обратите внимание, что описание интерфейса принадлежит пространству имен
Integrators.MechSystems. Это пространство имен является вложенным для Integrators,
где описан интерфейс IIntegrator и классы интеграторов, рассмотренных во введении к
курсу. Такое описание обеспечивает возможность прямой ссылки на классы интеграторов,
используемые в интерфейсе IMechSystem и классе MechSystem, реализующем этот
интерфейс.
13.2 Класс MechSystem
Рассмотрим реализацию интерфейса IMechSystem классом MechSystem.
Добавим тип обработчика событий – делегата, который будем использовать в классе .
/// <summary>
/// Прототип обработчика события, наступающего после создания
/// интегратора в классе MechSystem.
/// </summary>
public delegate void SimpleEventHandler (Object Sender);
Класс MechSystem будет прямым наследником класса Object. Кроме того, MechSystem
должен реализовывать (говорят еще, наследовать) интерфейс IMechSystem. Это можно
выразить заголовком
public class MechSystem : Object, IMechSystem
Далее необходимо прописать, как минимум, реализацию тех свойств и методов, которые
объявляет интерфейс IMechSystem.
В начале дадим комментарий к реализации свойства Name
/// <summary>
/// Хранит имя экземпляра механической системы
/// </summary>
string name;
/// <summary>
/// Устанавливает и возвращает имя мех.системы
/// </summary>
/// <remarks>
/// Имя возвращается так же методом ToString(),
/// унаследованным от object-класса
/// </remarks>
public string Name
{
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
141
set
{
//Устанавливается имя механической системы
name = value == null || value == string.Empty ?
base.ToString () + " " + GetHashCode ().ToString () :
value;
}
get
{
return name;
}
}
/// <summary>
/// Измененная версия наследуемого виртуального метода
/// </summary>
/// <returns>
/// Имя экземпляра механической системы (значение поля name)
/// </returns>
public override string ToString ()
{
return name;
}
В классе MechSystem со свойством Name связано поле name, которое хранит имя
механической системы и доступно только членам класса. Метод ToString унаследован
классом MechSystem от класса Object. В контексте реализации класса MechSystem методу
ToString предлагается возвращать имя механической системы name.
Обратите внимание на реализацию метода set свойства Name. Имя механической системы
не может быть пустым, или отсутствовать вообще. Отсутствующее имя заменяется
именем класса base.ToString с добавлением случайного целого числа, определяющего
хэш-код объекта. Это число возвращается методом GetHashCode, унаследованным от
Object.
Далее описываются реализации свойств числа степеней свободы s и пропагаторов Fq, Fp,
объявленных интерфейсом IMechSystem
/// <summary>
/// Хранит число степеней свободы.
/// </summary>
ushort S;
/// <summary>
/// Возвращает число степеней свободы.
/// </summary>
public ushort s
{
get { return S; }
}
/// <summary>
/// Хранит пропагатор - правую часть уравнений изменения координат.
/// </summary>
Propagator fq;
/// <summary>
/// Хранит пропагатор - правую часть уравнений изменения импульса.
/// </summary>
Propagator fp;
/// <summary>
/// возвращает пропагатор - правую часть уравнений изменения координат
/// </summary>
public Propagator Fq { get { return fq; } }
/// <summary>
/// возвращает пропагатор - правую часть уравнений изменения импульса
/// </summary>
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
142
public Propagator Fp { get { return fp; } }
Параллельно в описание класса MechSystem вводятся поля, хранящие число степеней
свободы и ссылки на пропагаторы и имеющий доступ private, как и положено
большинству полей.
Следующий член класса описывает метод Equations, делегируемый интегратору и
содержащий уравнения движения
/// <summary>
///Дифференциальные уравнения, делегируемые интегратору (уравнения движения)
/// </summary>
/// <param name="t">
/// текущее время
/// </param>
/// <param name="y">
/// ссылка на текущее состояние
/// </param>
/// <param name="f">
/// ссылка на текущие значения пропагаторов
/// </param>
void Equations (
// текущее время
double t,
// текущее состояние
double [ ] y,
// правая часть уравнений
double [ ] f
)
{
// для всех уравнений считается правая часть f[i] в состоянии t,y
for ( ushort i = 0; i < 2 * S; i++ )
// (i & 1)==1, если i-нечетное
f [i] = (i & 1) == 1 ? fp ((ushort)(i / 2), t, y) :
fq ((ushort)(i / 2), t, y);
}
Метод Equations обращается к пропагаторам механической системы за вычислением
правой части интегрируемых уравнений. Обратите внимание на преобразование целых
типов в аргументах пропагаторов.
Описываются свойства Time, State и Integrator, объявленные интерфейсом
IMechSystem. Добавляется описание соответствующих полей, хранящих текущие значения
этих свойств.
/// <summary>
/// Хранит текущее время
/// </summary>
double time;
/// <summary>
/// Возвращает текущее время
/// </summary>
public double Time { get { return time; } }
/// <summary>
/// Хранит текущее состояние системы
/// </summary>
QP [ ] state;
/// <summary>
/// Возвращает текущее состояние системы
/// </summary>
public QP [ ] State { get { return state; } }
/// <summary>
/// Хранит тенкущий интегратор
/// </summary>
IIntegrator integrator;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
143
/// <summary>
/// Возвращает текущий интегратор.
/// </summary>
public IIntegrator Integrator { get { return integrator; } }
Поле integratorCreated класса MechSystem хранит текущую ссылку на обработчик
события, наступающего сразу вслед за созданием интегратора.
/// <summary>
/// Хранит текущую ссылку на обработчик события,
/// наступающего после создания интегратора.
/// </summary>
public readonly SimpleEventHandler integratorCreated;
Модификатор readonly указывает, что поле integratorCreated доступно
только для
чтения и инициализируется только внутри конструктора экземпляров.
Обработчик integratorCreated вызывается вслед за инициализацией интегратора,
которая проводится методом set свойства IntegratorClass. Ниже описано свойство
IntegratorClass и поле integratorClass
/// <summary>
/// Хранит текущий тип класса интегратора.
/// </summary>
Type integratorClass;
/// <summary>
/// Возвращает и устанавливает тип текущего класса интегратора.
/// </summary>
/// <remarks>
/// При установке типа класса создается экземпляр этого типа.
/// </remarks>
public Type IntegratorClass
{
get { return integratorClass; }
set
{
if ( fq == null )
throw (new ApplicationException
("\nПравая часть уравнений изменения координат не задана?!!"));
if ( fp == null )
throw (new ApplicationException
("\nПравая часть уравнений изменения импульсов не задана?!!"));
if ( value == null )
throw (new ApplicationException
("\nКласс интегратора не задан?!!"));
// Если класс интегратора совпадает с уже имеющимся
// метод set завершается
if ( value == integratorClass ) return;
// Поле класса интегратора принимает новое значение
integratorClass = value;
// создается экземпляр интерфейса IIntegrator
integrator = (IIntegrator)Activator.CreateInstance (
integratorClass,
// Формируется массив аргументов для
// конструктора класса IntegratorClass
new object [2]
{ (ushort)(2 * S), new TEquations (Equations) }
);
// выполняется обработчик события (если он есть)
// после создания интегратора
if ( integratorCreated != null ) integratorCreated (this);
}
}
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
144
Обратите внимание на возбуждаемые исключительные ситуации, а также на метод
CreateInstance библиотечного класса Activator, применяемый для создания экземпляра
интегратора по известному классу интегратора.
В реализации конструктора экземпляров класс MechSystem предлагает три перегруженные
версии
//Конструкторы класса
/// <summary>
/// Конструктор класса
/// </summary>
/// <param name="s">
/// Число степеней свободы (>0!)
/// </param>
/// <param name="fq">
/// пропагатор для координаты
/// </param>
/// <param name="fp">
/// пропагатор для импульса
/// </param>
/// <param name="IntegratorClass">
/// класс интегратора
/// </param>
/// <param name="name">
/// имя экземпляра механической системы
/// </param>
/// <param name="integratorCreated">
/// обработчик события, наступающего после создания интегратора
/// </param>
public MechSystem (
ushort s, Propagator fq, Propagator fp,
Type IntegratorClass, string name,
SimpleEventHandler integratorCreated
)
{
if ( s == 0 )
throw (new ApplicationException
("\nЧисло степеней свободы равно нулю?!!"));
if ( fq == null )
throw (new ApplicationException
("\nПравая часть уравнений изменения координат не задана?!!"));
if ( fp == null )
throw (new ApplicationException
("\nПравая часть уравнений изменения импульсов не задана?!!"));
// инициализируется поле числа степеней свободы
S = s;
// резервируется память для массива-состояния
state = new QP [S];
// инициализируются поля пропагаторов
this.fq = fq; this.fp = fp;
// Устанавливается имя механической системы
Name = name;
// инициализируется обработчик создания интегратора
this.integratorCreated = integratorCreated;
// Устанавливается тип класса интегратора
this.IntegratorClass = IntegratorClass;
}
/// <summary>
/// Конструктор класса
/// </summary>
/// <param name="s">
/// Число степеней свободы (>0!)
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
145
/// </param>
/// <param name="fq">
/// пропагатор для координаты
/// </param>
/// <param name="fp">
/// пропагатор для импульса
/// </param>
/// <param name="IntegratorClass">
/// класс интегратора
/// </param>
/// <param name="name">
/// имя экземпляра механической системы
/// </param>
/// <remarks>
///предполагается отсутствие обработчика события после создания интегратора
/// </remarks>
public MechSystem (
ushort s, Propagator fq, Propagator fp,
Type IntegratorClass, string name)
// вызывается первый конструктор
: this (s, fq, fp, IntegratorClass, name, null) { }
/// <summary>
/// Конструктор класса
/// </summary>
/// <param name="s">
/// Число степеней свободы (>0!)
/// </param>
/// <param name="fq">
/// пропагатор для координаты
/// </param>
/// <param name="fp">
/// пропагатор для импульса
/// </param>
/// <param name="IntegratorClass">
/// класс интегратора
/// </param>
/// <remarks>
/// предполагается отсутствие обработчика события после создания
/// интегратора и имя экземпляра присваивается по умолчанию
/// </remarks>
public MechSystem (
ushort s, Propagator fq,
Propagator fp, Type IntegratorClass)
: this (s, fq, fp, IntegratorClass, null, null) { }
Обратите внимание, что основным является содержание конструктора первой версии с
максимальным числом параметром. Другие две версии конструкторов просто вызывают
первую версию, подставляя ссылки null на место соответствующих параметров.
В основном конструкторе оператор
this.IntegratorClass = IntegratorClass;
приводит к вызову метода set свойства IntegratorClass. Это определяет экземпляр
интегратора, и поле integrator принимает конкретное значение. Тем самым после
создания экземпляра класса MechSystem все его поля оказываются определенными за
исключением полей time и state.
Эти поля задаются методом и только им
///
///
///
///
///
<summary>
Задает начальное состояние движения механической системы
</summary>
<param name="startTime">
Начальное время
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
146
/// </param>
/// <param name="startState">
/// Начальное состояние.
/// Массив записей координат и импульсов для каждой степени свободы.
/// </param>
public void BeginFrom (double startTime, QP [ ] startState)
{
time = integrator [0] = startTime;
for ( ushort i = 1; i <= 2 * S; i++ )
integrator [i] = (i & 1) == 1 ?
state [(i - 1) / 2].q = startState [(i - 1) / 2].q :
state [(i - 1) / 2].p = startState [(i - 1) / 2].p;
}
Параллельно метод BeginFrom придает осмысленные значения начальным условиям
интегрирования, так как определяет все элементы индексатора интегратора integrator.
После обращения к методу BeginFrom начальные условия движения заданы. Это
позволяет вызвать метод
/// <summary>
/// Интегрирование до состояния в заданный момент времени
/// </summary>
/// <param name="EndTime">
/// Задаваемый момент времени
/// </param>
/// <returns>
/// true при нормальном окончании,
/// false, если выполнение прервано обработчиком
/// </returns>
public bool PropagateTo (double EndTime)
{
// Вызывается цикл интегрирования
bool result = integrator.IntegrateTo (EndTime);
// Результат интегрирования сохраняется в полях time и state
time = integrator [0];
for ( ushort i = 0; i < 2 * S; i++ )
if ( (i & 1) == 1 )
state [i / 2].p = integrator [(ushort)(i + 1)];
else
state [i / 2].q = integrator [(ushort)(i + 1)];
// Возвращается true, если интегрирование окончено нормально
// и false, если оно прервано обработчиком внутри IntegrateTo
return result;
}
Вызывать метод PropagateTo можно сколько угодно раз. Интегрирование будет
возобновляться от текущего момента времени EndTime, если метод IntegrateTo не был
прерван обработчиком интегратора.
Это все члены, из которых состоит класс MechSystem. Полное содержание модуля с
описанием класса находится по ссылке MechSystem.
13.3 Windows-приложение MechSystTesting. Начало
Откроем среду MS Visual Studio 2005 и построим проект, тестирующий интеграторы и
класс MechSystem на примерах простых механических систем.
1. Создайте в начале пустой контейнер solution. Для этого
a. командой File.New Project… откройте окно New Project.
b. На древесной структуре левой панели окна New Project выберите узел Other
Project Types. Visual Studio Solutions. На правой панели среда выделит тип
Blank Solution.
c. В строке Name наберите имя контейнера solMechSystemTest и нажмите OK.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
147
2.
3.
4.
5.
6.
7.
8.
9.
d. Откройте окно Solution Explorer (из меню View). В списке должен быть один
пункт solution ‘solMechSystemTest’ (0 projects).
Добавьте в solution новый контейнер типа project. Для этого
a. в Solution Explorer, щелкнув правой кнопкой над пунктом solution…,
откройте контекстное меню и выберите команду Add. New Project…. Эта
команда откроет окно Add New Project.
b. На правой панели окна Add New Project выберите пункт Empty Project.
c. В строке Name наберите имя проекта Integrators и нажмите Add. Убедитесь,
что в древесной структуре окна Solution Explorer появится узел с именем
Integrators.
Теперь добавьте к пустому проекту Integrators пустой файл для написания кода.
Для этого
a. откройте контекстное меню над пунктом Integrators и вызовите команду
Add. New Item…. Откроется окно Add. New Item - Integrators.
b. Выберите пункт Code File.
c. В строке Name наберите IIntegrator.cs и щелкните Add. Откроется пустое
окно IIntegrator.cs. Убедитесь, что в окне Solution Explorer также появился
новый узел IIntegrator.cs.
В файл IIntegrator.cs необходимо копировать текст модуля с описанием интерфейса
интегратора IIntegrator, обсуждавшегося во введении к курсу. Сохраните файл
IIntegrator.cs командой File. Save IIntegrator.cs.
Добавьте к проекту Integrators еще один Code File, который назовите TIntegrator.cs.
Копируйте в него текст модуля с описанием абстрактного класса TIntegrator.
Сохраните новый файл TIntegrator.cs. Структура класса TIntegrator так же
обсуждалась во введении.
Добавьте к проекту Integrators новый Code File, назвав его RK4.cs. Копируйте в
него текст модуля с описанием класса RK4. Сохраните новый файл RK4.cs. Класс
RK4 так же обсуждался во введении.
Каждый из созданных файлов кода является компилируемым модулем. Общим для
всех трех модулей является то, что они принадлежат одному пространству имен
Integrators и являются частями одного проекта с тем же именем.
Теперь необходимо определить тип проекта Integrators. Для этого
a. вызовите команду Project. Integrators properties…. Откроется окно
Integrators.
b. На левой панели окна Integrators выберите пункт Application.
c. В элементе под именем Output Type: выберите пункт Class Library. Мы
хотим, чтобы три созданные файла компилировались в один исполняемый
модуль типа библиотеки классов с именем Integrators.dll.
d. Командой File. Save сохраните окно свойств Integrators.
e. Командой Build. Build Integrators скомпилируйте библиотеку. Убедитесь,
что компиляция прошла без ошибок. После этого посмотрите содержимое
стандартного каталога Projects файловой системы MS Visual Studio 2005. В
нем среда образовала каталог solMechSystemTest, внутри которого находится
каталог проекта Integrators с файлами IIntegrator.cs, TIntegrator.cs, RK4.cs. В
каталоге Bin есть каталог Debug, в котором должен находиться
скомпилированный файл библиотеки интеграторов Integrators.dll.
Закончив создание библиотеки интеграторов, можно перейти к созданию
библиотеки с пространством имен Integrators.MechSystem. Для этого, как и
выше,
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
148
a. к solution solMechSystemTest добавьте пустой проект (Empty Project) с
именем Integrators.MechSystem.
b. к проекту Integrators.MechSystem добавьте Code File с именем
IMechSystem.cs. Копируйте в этот файл модуль с описанием интерфейса
IMechSystem. Сохраните содержание файла IMechSystem.cs.
c. к тому же проекту Integrators.MechSystem добавьте еще один Code File с
именем MechSystem.cs. Копируйте в этот файл модуль с описанием класса
MechSystem. Сохраните содержание файла MechSystem.cs.
d. Убедитесь, что в окне Solution Explorer появились все необходимые узлы.
Вызовите командой Project. Integrators.MechSystem properties…окно свойств
проекта Integrators.MechSystem и выберите в качестве типа исполняемого
модуля (Output File) библиотеку классов (Class Library). Сохраните окно
свойств.
e. Проведите компиляцию новой библиотеки классов Integrators.MechSystem
командой Build. Build Integrators.MechSystem. Среда укажет на ошибку
компиляции. Дело в том, что в коде библиотеки Integrators.MechSystem
используется класс IIntegrator. Интерфейс IIntegrator описан в
библиотеке Integrators. Поэтому в ссылках (references) новой библиотеки
Integrators.MechSystem необходимо указать библиотеку Integrators.
f. Для этого в окне Solution Explorer найдите узел References проекта
Integrators.MechSystem. В контекстном меню над этим узлом выберите
команду Add Reference…. Откроется окно Add Reference. Откройте закладку
Projects этого окна. В списке должен находиться только проект Integrators.
Выделите его и нажмите Add.
g. Вновь проведите компиляцию библиотеки Integrators.MechSystem командой
Build. Build Integrators.MechSystem. На этот раз компиляция должна
завершиться без ошибок. Убедитесь, что файл Integrators.MechSystem.dll
создан и находится в нужном месте файловой системы.
10. Закроем окна с файлами библиотек. Добавим к solution solMechSystemTest новый
проект. Опять войдем в окно Solution Explorer, вызовем через контекстное меню
над пунктом solution solMechSystemTest… команду Add. New Project…. Эта команда
откроет окно Add New Project.
11. На этот раз в качестве типа проекта выберем стандартный тип Windows Application.
Дадим ему имя prMechSystemTest и нажмем Add. В результате среда создаст группу
файлов, смысл и назначение которых мы вкратце рассмотрим.
13.4 Файлы и классы windows-приложения
Кодовые файлы, созданные средой для windows-приложения, носят по умолчанию имена
Program.cs, Form1.cs и Form1.Designer.cs.
Основной файл windows-приложения. Класс Application
Изменим имя файла Program.cs, действуя командой Rename из контекстного меню в окне
Solution Explorer над этим файлом. Это лучший способ переименования. Не надо
переименовывать файлы проекта из файловой системы с помощью команд OS. Пусть
файл имеет имя MechSystemTest.cs. Такое же имя MechSystemTest приобретет класс,
описанный в этом файле.
Вот как будет выглядеть файл MechSystemTest.cs после переименования
using System;
using System.Collections.Generic;
using System.Windows.Forms;
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
149
namespace prMechSystemTest
{
static class MechSystemTest
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main ()
{
Application.EnableVisualStyles ();
Application.SetCompatibleTextRenderingDefault (false);
Application.Run (new Form1 ());
}
}
}
Здесь описана известная нам функция Main, с выполнения которой начинается
проект и по
окончании которой он завершается.
Класс MechSystemTest, который предложила среда, имеет модификатор static. Это
означает, что класс MechSystemTest имеет только статические члены и не может создавать
объекты. Фактически в нем есть только статический метод Main, хотя при желании можно
добавлять и другие статические методы, поля, свойства.
Атрибут [STAThread] (Single Threaded Apartment Thread), предшествующий описанию метода
Main, для нашего приложения не существен.
Метод Main вызывает три статических метода библиотечного класса Application. Первые
два метода выполняют форматирующие функции изображения объектов на экране.
Последний метод Run содержит в себе основной цикл обработки сообщений проекта.
Параметром метода Run является объект библиотечного класса Form. Здесь этот объект
создается во время обращения к методу Run как объект класса-наследника, носящего по
умолчанию имя Form1. Фактически, выполнение приложения начинается и заканчивается
методом Run класса Application.
Класс Application является прямым наследником класса Object. Его заголовок имеет вид
public sealed class Application
Модификатор sealed говорит о том, что класс Application не может иметь наследников.
Хотя класс Application не объявлен как статический, в основном у него есть только
статические методы и свойства.
Набрав внутри метода Main строку Application., посмотрите список всех свойств и
методов класса Application. Поинтересуйтесь через MSDN (нажав F1) смыслом этих
свойств и методов.
К примеру, полезное статическое свойство ExecutablePath класса Application
возвращает маршрутную строку и имя исполняющего модуля проекта. Это позволяет
программно определять местонахождение исполняющего модуля во время работы
программы.
Статический метод DoEvents класса Application позволяет на каждом шаге цикла
реализовать сообщения, ожидающие в очереди окончания этого цикла. Другой
статический метод Exit класса Application позволяет в любой момент остановить
выполнение приложения.
Событие ApplicationExit позволяет задать обработчик, код которого будет работать
непосредственно перед закрытием приложения. Другое событие ApplicationIdle
позволяет занять время, остающееся у приложения свободным (idle).
Отметим, что основной файл проекта MechSystemTest.cs редко редактируется
непосредственно. Поэтому окно с этим файлом можно закрыть.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
150
Файлы формы windows-приложения. Класс Form
В кодовых файлах с именами Form1 и Form1.Designer описан класс Form1.
Описание класса Form1 принадлежит тому же пространству имен prMechSystemTest, что и
описание класса MechSystemTest, рассмотренного выше. Описание Form1 распределено
на два физических файла Form1.cs и Form1.Designer.cs, хотя образует один
компилируемый модуль. В обоих файлах заголовок описания содержит модификатор
partial, который позволяет объединить обе части в единое целое.
Первая часть описания класса Form1 находится в файле Form1.cs.
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel;
System.Data;
System.Drawing;
System.Text;
System.Windows.Forms;
namespace prMechSystemTest
{
public partial class Form1 : Form
{
public Form1 ()
{
InitializeComponent ();
}
}
}
В этой части описания указано, что класс Form1 наследует от библиотечного класса Form.
Далее приводится описание конструктора объектов класса Form1, в котором вызывается
единственный метод InitializeComponent. В первой части описания класса Form1
программист описывает свои поля, методы, свойства, наполняет кодом обработчики
событий, управляющих формой Form1 и работой приложения (run time).
Метод InitializeComponent описан во второй части описания класса Form1, находящейся
в файле «проектировщика» Form1.Designer.cs. Содержимое этого файла управляется в
процессе визуального проектирования (design time). Для этого среда открывает отдельное
окно Form1.cs [Designer], содержащее изображение окна приложения. Свойства класса
Form1 визуально воспроизводятся в изображении окна приложения. Собственно говоря,
объект класса Form1, создаваемый при вызове метода Application.Run (new Form1 ()),
и есть окно приложения, или форма. Среда создает этот объект на этапе проектирования,
помогая программисту в задании параметров формы и управляющих элементов,
размещаемых на ней.
В описании класса Form1, находящемся в файле Form1.Designer.cs, среда описывает поле
components, доступное только членам класса Form1, и два метода Dispose и
InitializeComponent.
В процессе проектирования среда редактирует содержание метода InitializeComponent
класса Form1. Метод InitializeComponent содержит инициализацию полей класса Form1,
унаследованных от своего предка Form, а также операторы создания объектов,
размещаемых на форме в процессе проектирования, и инициализацию их полей.
Обычно не возникает необходимости редактировать код в файле дизайнера
Form1.Designer.cs непосредственно «руками». Для этого существуют возможности
визуального проектировщика с его окном Form1.cs [Designer]. Именно по этой причине
описание класса Form1 разделено средой на два файла. Один управляется дизайнером
среды, другой в основном программистом.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
151
13.5 Проектирование формы
В начале немного поработаем со свойствами формы. Для этого откроем окно дизайнера
Form1.cs [Designer] с изображением формы. Затем командой View. Properties Window
откроем окно Properties.
В этом окне есть два списка: список свойств, который изображен на рисунке, и список
событий, который вызывается щелчком по значку с изображением молнии. Имена свойств
и событий в этих списках можно упорядочить по смыслу, как на рисунке, либо по
алфавиту, щелкнув по значку с изображением стрелки с буквами A-Z.
Списки свойств и событий класса Form1 довольно обширны. Смысл каждого свойства и
события подсказывается их именем. Кроме того, внизу окна Properties дается краткий
комментарий к выделенному свойству или событию. Если требуется более подробное
описание, его можно получить из справочной системы, нажав F1. Просмотрите в начале
краткое описание свойств и событий класса Form1.
Чтобы лучше понять смысл свойств, измените то или иное значение какого-либо свойства.
Обратите внимание на то, как дизайнер реагирует на эти изменения. Во-первых,
изменение может сказаться на внешнем виде окна. Например, измените свойство Text
(заголовок окна) на MechSystemsTest.
Во-вторых, изменения свойств и событий будут отражаться на содержании файла
проектировщика Form1.Designer.cs, в частности, на содержании метода
InitializeComponent. Измените свойство Name окна на fMechSystTest. Это не изменит
внешнего вида окна, но отразится на содержании всех файлов проекта. Ведь изменилось
имя класса. Убедитесь в этом сами.
В области окна можно размещать различные управляющие элементы. Перейдем в окно
дизайнера Form1.cs [Designer]. Списки управляющих элементов находятся в окне Toolbox,
которое можно открыть командой View.Toolbox.
К примеру, поместите на форму кнопку. Это элемент класса Button, который есть в
списках All Windows Forms и Common Controls. Щелкнув по значку Button, а затем гделибо на поле окна, мы получим изображение кнопки на рабочей поверхности окна. По
умолчанию имя кнопки (свойство Name) будет button1 и на ней будет изображен тот же
текст (свойство Text). Сделайте это и посмотрите, какие изменения произошли в методе
InitializeComponent файла Form1.Designer.cs. Теперь добавьте к событию Click кнопки
button1 обработчик. Для этого можно выделить изображение кнопки на форме, открыть
окно Properties и дважды щелкнуть по пустому правому полю события Click. Еще проще
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
152
- дважды щелкнуть по самой кнопке. Среда должна добавить к событию кнопки
обработчик с именем button1_Click и направить нас к пустому телу этого обработчика в
кодовом файле Form1.cs. Параллельно в методе InitializeComponent файла
проектировщика Form1.Designer.cs появится строка, указывающая на добавление
обработчика события button1_Click типа EventHandler к событию Click кнопки
button1
this.button1.Click += new System.EventHandler (this.button1_Click);
Нас более не будет интересовать содержимое файла Form1.Designer.cs. Мы достаточно
познакомились с той ролью, которую он играет в приложении. Поэтому окно с этим
файлом можно закрыть, оставив лишь два окна – поле визуального проектирования
Form1.cs [Designer] и кодовый файл Form1.cs.
Добавьте в обработчик button1_Click строку
BackColor = Color.Aqua;
Дайте команду Debug.Start Debugging.
В появившемся окне приложения кликнете кнопку button1 и убедитесь, что форма стала
голубой (Aqua). Закройте приложение.
Теперь можно убрать кнопку button1 из проекта, выделив ее в окне проектирования
Form1.cs [Designer] и нажав Delete. Обработчик так же следует удалить, выделив его
строки в кодовом окне Form1.cs и нажав тот же Delete.
О классе Form
Класс Form, наследником которого является наш класс fMechSystTest, имеет довольно
большое количество предков, отделяющих его от корневого класса Object библиотеки
.net. Непосредственным предком класса Form является класс ContainerControl. Далее по
восходящей, это ScrollableControl, Control, Component, MarshalByRefObject и,
наконец, Object. Кроме того, предки класса Form реализуют ряд интерфейсов.
Класс MarshalByRefObject является абстрактным, однако все объекты его наследников
имеют возможность передавать (marshaling) ссылку (reference) на себя через границу
области приложения (application domain), в которой они созданы. Для этого они используют
своего «заместителя», или агента (proxy), который при первом обращении к объекту из-за
границы пересылается запрашивающему приложению. При вторичном обращении proxy
запрашивает объект, остающийся в своей родной области. Запрашивающее приложение
общается с proxy, как если бы это был сам объект, агентом которого proxy является. В
процессе запросов и реакций объекта происходит «маршализация» – упаковка, пересылка
и распаковка данных, направляемых объекту и от него.
Класс Component позволяет своим объектам быть элементами контейнера. Классы
контейнеров устанавливают имена своим компонентам и при своем собственном
освобождении освобождают все свои элементы-компоненты. Компонент реализует также
метод Dispose, позволяющий явным образом освобождать так называемые
«неуправляемые ресурсы» (unmanaged resources) типа дескрипторов файлов, которые не
может освободить автоматический сборщик мусора.
Класс Control реализует большое количество свойств и методов, которые обеспечивают
визуализацию компоненты. Объекты класса Control имеют границы изображения,
положение и размеры, обеспечивают обработку сообщений от клавиатуры и мышки.
Объекты класса Control могут быть частью коллекции, на которую ссылается свойство
Parent. Например, свойство Parent кнопки, размещенной на форме, содержит ссылку на
форму. У самой формы свойство Parent равно null. Свойство Controls объектов класса
Control содержит ссылку на коллекцию объектов, для которых данный объект является
Parent. Например, одним из элементов коллекции Controls формы может быть кнопка,
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
153
помещенная на форме. При размещении объекта на форме среда в методе
InitializeComponent пишет код вида
this.Controls.Add (this.button1);
Этот код добавляет кнопку button1 в коллекцию Controls формы this.
Таким образом, объекты класса Control могут содержать в себе другие объекты,
т.е.
быть контейнером для других объектов, и могут сами быть элементами коллекции,
принадлежащей другому объекту. Здесь термин «контейнер» применяется в смысле
визуального размещения на экране, а не в упомянутом выше смысле контейнеров в
определении объектов класса Component. Другими словами, свойство Container,
присутствующее у класса Control как у наследника Component, не имеет отношения к
свойству Controls объектов класса Control.
Классы ScrollableControl и ContainerControl добавляют специфические свойства
объектам класса Control, когда они играют роль контейнеров других объектов. Объекты
класса ScrollableControl могут автоматически изображать на своей поверхности ту или
иную полосу скроллинга в том случае, когда входящие в их состав элементы коллекции
Controls по своим размерам и положению выходят за границы контейнера. Объекты
класса ContainerControl позволяют управлять установкой фокуса ввода на элементы
своей коллекции Controls. Например, менять фокус ввода клавишей Tab.
Фокус ввода может быть установлен только на одном из элементов коллекции Controls,
но не на самом объекте класса ContainerControl. Элемент, находящийся в фокусе,
является активным, т.е. используется в данный момент для управления приложением,
ввода и вывода информации и т.п. Наличие фокуса может менять внешний вид элемента.
Например, кнопка, находящаяся в фокусе, имеет специфический пунктирный контур на
своем поле. Но главное, что элемент, находящийся в фокусе ввода, может управляться с
клавиатуры. Объекты класса Control и его наследников могут получать фокус ввода,
если соблюдается ряд условий.
13.6 Конструирование формы fMechSystTest
Добавьте в пока пустую форму fMechSystTest объект класса ToolStripContainer из
списка Menus & Toolbars окна Toolbox (щелкните над элементом ToolStripContainer, а
затем над поверхностью формы). В автоматически появившемся диалоговом окне
ToolStripContainer Tasks выберите команду Dock Fill in Form. Новый объект с именем
toolStripContainer1 закроет всю рабочую поверхность окна (без заголовка).
Объект toolStripContainer1 состоит из пяти контейнеров – центральный контейнер с
именем ContentPanel и 4 контейнера TopToolStripPanel и т.д. по 4-ем сторонам
прямоугольника центральной части. На всех пяти панелях можно размещать различные
элементы управления.
Поместите на верхнюю панель главное меню. Для этого в списке Menus & Toolbars окна
Toolbox выберем элемент MenuStrip и поместите его на верхнюю панель контейнера
(щелчок над MenuStrip и, затем, над верхней панелью).
Аналогичным образом поместите объект класса StatusStrip из того же списка на
нижнюю панель контейнера. Это будет строка состояния, в которой можно отображать
текущее состояние приложения и другую изменчивую информацию.
На левую и правую панели контейнера поместите по одному объекту класса ToolStrip.
Объекты класса также являются контейнерами, внутрь которых можно помещать
отдельные управляющие элементы.
Можно не занимать место под заголовок окна. Для этого свойство FormBorderStyle
установите в None. Оно останется без заголовка, но и без закрывающей кнопки.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
154
Чтобы компенсировать эту потерю, в объекте класса ToolStrip на правой панели
выберите кнопку типа Button. Для этого щелкните по правой панели на месте с
изображением стрелочки и выберите команду Add ToolStripButton. Выберите элемент
Button из появившегося списка.
Наведите курсор на появившуюся кнопку и вызовите контекстное меню, нажав правую
кнопку мышки. В этом меню выберите подменю DisplayStyle, а в нем установите команду
Text, заменив установленную по умолчанию Image. Это будет кнопка, закрывающая
приложение. Измените ее свойство Text на Close. Для этого из того же контекстного
меню вызовите окно Properties, найдите в нем свойство Text и измените его.
Щелкните дважды по кнопке Close. Среда создаст обработчик toolStripButton1Click
события «клика» кнопки с именем toolStripButton1 и надписью Close, перенеся нас в
окно кодового файла Form1.cs. Наберите текст, закрывающий приложение
Application.Exit ();
Испытайте работу кнопки, запустив приложение командой Debug.Start Debugging.
Вернитесь к окну проектировщика Form1.cs [Designer]. Отредактируйте меню на верхней
панели. Щелкните по меню и наберите текст Problems. В следующем пункте ниже
наберите «Частица в среде» (без кавычек), далее «Частица в поле тяжести» и еще две
задачи «Осциллятор» и «Маятник». Проверьте, запустив проект, видны ли все 4 пункта
меню Problems.
Выбор любого из этих пунктов должен приводить к выбору конкретной механической
системы и определять, какие именно уравнения движения интегрировать.
Для визуализации имени выбранной задачи перенесите на центральный контейнер из
списка Common Controls окна Toolbox объект класса Label. Расположите этот объект в
центре окна. Система даст объекту имя label1. В окне свойств label1 уберите
содержание свойства Text. Пусть оно будет пустым.
Введите в класс fMechSystTest поле problemIndex, которое будет хранить номер задачи,
выбранной из меню. Опишите так же свойство ProblemIndex, которое будет
устанавливать и возвращать значение этого поля. Для этого внутри скобок описания
класса в кодовом файле Form1.cs поместите текст
///
///
///
int
///
///
///
int
{
<summary>
Хранит текущий индекс задачи
</summary>
problemIndex;
<summary>
Устанавливает и возвращает индекс задачи
</summary>
ProblemIndex
set
{
problemIndex = value;
}
get { return problemIndex; }
}
Теперь постройте обработчик, который будет реагировать на выбор пункта меню и
устанавливать свойство ProblemIndex. Для этого выделите заголовок меню Problems и
вызовите окно его свойств Properties. Имя выделенного элемента меню по умолчанию
problemsToolStripMenuItem. В верхней части окна свойств это имя должно быть указано.
Войдите в список событий (значок молнии) и выберите пункт DropDownItemsClicked.
Дважды щелкните по правому полю в этой строке. В кодовом файле должен появиться
заголовок обработчика problemsToolStripMenuItem_DropDownItemClicked с его телом. В
теле обработчика наберите код
ProblemIndex =
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
155
problemsToolStripMenuItem.DropDownItems.IndexOf (e.ClickedItem);
label1.Text = e.ClickedItem.Text;
Поле problemIndex должно получить значение, отвечающее выбранному пункту меню
Problems. Проверьте это. Для этого установите точку прерывания (breakpoint) в только что
набранной строке, щелкнув по левой кромке окна на этой строке. Должна появиться
красная точка на левой кромке окна. Запустите приложение командой Debug.Start
Debugging. Выберите какой-либо пункт меню. Программа должна остановиться в точке
прерывания. После этого, выбирайте команду Debug.Step Into, пока не сможете прочесть
значение поля problemIndex. Сравните это значение с тем пунктом, который выбрали.
Первый пункт меню «Частица в среде» должен отвечать номеру 0. Далее по порядку.
Если все в порядке, то точку прерывания можно убрать, щелкнув по ней мышкой. Объект
label1 должен отобразить имя задачи.
Теперь можно написать ту часть кода, которая определяет механическую систему.
Для этого, во-первых, необходимо в нашем проекте prMechSystemTest сослаться на те
библиотеки, в которых описан класс интегратора и класс механической системы.
Откройте окно Solution Explorer и над пунктом References проекта вызовите контекстное
меню. В нем выберите команду Add Reference…. В появившемся окне Add Reference
зайдите на закладку Projects и добавьте ссылки на обе имеющиеся там библиотеки
Integrators и Integrators.MechSystem. После этого в начале кодового файла Form1.cs после
всех строк, начинающихся с using, добавьте две строки
using Integrators;
using Integrators.MechSystems;
Это позволит сократить запись вызовов объектов из указанных библиотек.
Добавьте в том же файле к коду класса формы fMechSystTest описание поля, хранящего
ссылку на механическую систему
/// <summary>
/// Хранит текущую механическую систему
/// </summary>
MechSystem mechSyst;
Теперь необходимо написать сами уравнения движения в форме пропагаторов, которые
мы должны передать механической системе в качестве параметров ее конструктора.
Напомню, что уравнения движения для выбранных механических систем можно записать
в форме дифференциальных уравнений первого порядка для координаты и импульса. Мы
рассматриваем одномерные системы. У каждой из них по одной координате q и импульсу
p. В масштабированной записи уравнения движения имеют вид
1. Частица в среде dq/dt = p; dp/dt = -p. Здесь масштаб массы равен массе частицы, а
масштаб времени отношению массы частицы к коэффициенту трения;
2. Частица в поле тяжести dq/dt = p; dp/dt = -1. Здесь масштаб массы равен массе
частицы, а масштаб времени обратному корню из ускорения свободного падения;
3. Осциллятор dq/dt = p; dp/dt = -q. Здесь масштаб массы равен массе частицы, а
масштаб времени обратной частоте осциллятора;
4. Маятник dq/dt = p; dp/dt = -sin q. Здесь масштаб массы равен массе частицы,
масштаб длины длине маятника, а масштаб времени корню из отношения длины
маятника к ускорению свободного падения. Координатой является угол, а
импульсом является момент импульса маятника.
Учитывая это, опишите метод, содержащий пропагатор fp изменения импульса в
системах, описанных в меню Problems.
///
///
///
///
///
///
<summary>
Вычисляет правую часть уравнения для импульса
</summary>
<param name="i">
Номер степени свободы
</param>
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
156
/// <param name="t">
/// Время
/// </param>
/// <param name="y">
/// Массив текущего состояния системы
/// </param>
/// <returns>
/// Значение производной импульса по времени
/// </returns>
double fp (ushort i, double t, double [ ] y)
{
switch ( problemIndex )
{
case 0: return -y [1];//Частица в среде
case 1: return -1.0; //Частица в поле тяжести
case 2: return -y [0];//Осциллятор
case 3: return -Math.Sin (y [0]);//Маятник
default: return Double.NaN;
}
}
Метод fp содержит уравнения изменения импульса всех четырех задач. Оператор switch
выбирает то уравнение, которое соответствует выбранной задаче problemIndex.
Ссылка на метод fp используется при инициализации поля mechSyst. Инициализацию
поля mechSyst можно провести в обработчике события Load, который вызывается после
загрузки формы. Откройте окно Properties и выберите в нем класс формы fMechSystTest.
На странице событий найдите строку Load и дважды щелкните по правому полю. В
появившемся обработчике fMechSystTest_Load наберите текст вызова конструктора
механической системы
mechSyst = new MechSystem
( 1,// Число степеней свободы
// уравнение изменения координаты
delegate (ushort i, double t, double [ ] y) { return y [1]; },
// уравнение изменения импульса
new Propagator (fp),
// Интегратор
typeof(RK4)
);
Обратите внимание, что метод, задающий пропагатор изменения координаты, описан
явно. Это объясняется тем, что этот метод состоит всего из одного оператора,
возвращающего импульс y[1] для любой задачи.
Проверьте проект на синтаксис командой Build.Rebuild prMechSystemTest. Ошибок быть не
должно.
Для тестирования нам необходимо выводить значение какой-нибудь сохраняющейся
величины. В трех задачах 2-4 такой величиной является полная энергия. В задаче о
движении частицы в среде энергия не сохраняется, но сохраняется выражение p*exp(t) –
начальный импульс. Полная энергия и выписанная выше функция зависят от текущего
состояния и времени. Это интегралы движения. Наберите два метода, которые вычисляют
интегралы движения наших задач в любом состоянии
///
///
///
///
///
///
///
///
///
<summary>
Вычисляет потенциальную энергию
</summary>
<param name="q">
Координата положения
</param>
<returns>
Значение потенциальной энергии
</returns>
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
157
double potentialEnergy (double q)
{
switch ( problemIndex )
{
case 1: return q;
case 2: return 0.5*q*q;
case 3: return -Math.Cos (q);
default: return Double.NaN;
}
}
/// <summary>
/// Вычисляет интеграл движения задачи
/// </summary>
/// <param name="t">
/// время
/// </param>
/// <param name="q">
/// координата
/// </param>
/// <param name="p">
/// импульс
/// </param>
/// <returns>
/// Значение интеграла движения
/// </returns>
double integral (double t, double q, double p)
{
switch ( problemIndex )
{
case 0: return p * Math.Exp (t);
case 1:
case 2:
case 3: return 0.5 * p * p + potentialEnergy (q);
default: return Double.NaN;
}
}
Из контекста методов должно быть понятно их назначение.
Теперь необходимо позаботиться о вводе начальных условий интегрирования. При
тестировании интегратора лучше использовать для этих целей генератор случайных
чисел. Опишите объект rnd класса Random как поле формы, добавив код
/// <summary>
/// Хранит ссылку на объект, вырабатывающий случайные числа.
/// </summary>
Random rnd = new Random ();
Инициализация генератора rnd обеспечит случайную инициализацию последовательности
псевдослучайных чисел, возвращаемых методами генератора rnd. Нам понадобится метод
NextDouble этого объекта, возвращающий случайное вещественное число в интервале
[0;1).
Введите поле, хранящее начальное состояние механической системы, и метод,
формирующий случайное начальное состояние
/// <summary>
/// Хранит ссылку на массив начального состояния системы
/// </summary>
QP [ ] startState = new QP [1];
/// <summary>
/// Формирует случайное начальное состояние системы
/// </summary>
void randomState ()
{
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
158
startState [0].p = 2.0 * rnd.NextDouble () - 1.0;
switch ( problemIndex )
{
case 0:
case 1:
case 2:
startState [0].q = 2.0 * rnd.NextDouble () - 1.0;
return;
case 3:
startState [0].q = Math.PI * (2.0 * rnd.NextDouble () - 1.0);
return;
}
}
Из описания следует, что во всех задачах начальный импульс будет случайным числом в
интервале [0;1). Случайная координата для первых трех задач будет иметь случайные
значения в том же интервале, а в задаче о маятнике – в интервале [-π; π).
Теперь необходимо определиться с тем, какое событие будет отвечать определению
случайных начальных условий. Пусть это будет клик некоторой кнопки. Введите для
этого кнопку на левой панели так же, как это было сделано на правой панели для кнопки
Close. На новой кнопке напишите Rnd start. Среда даст кнопке имя toolStripButton2.
Щелкните по кнопке дважды. Появится обработчик клика этой кнопки
toolStripButton2_Click.
Желательно контролировать возвращаемые значения координаты и импульса начального
состояния и значение интеграла движения в начальном состоянии. Для этого используйте
нижнюю панель, на которой находится объект класса StatusStrip. Щелкните по
StatusStrip и добавьте в него объект класса StatusLabel. Среда даст новому объекту
довольно длинное имя. Дайте ему новое, более краткое имя startLabel и сотрите
начальное значение свойства Text этого объекта, чтобы оно не светилось на экране.
Теперь в обработчике toolStripButton2_Click наберите текст
randomState ();
startLabel.Text = string.Format(
"StartState: q={0:f};p={1:f};integral={2:f}",
startState [0].q, startState [0].p,
integral(0,startState [0].q, startState [0].p));
mechSyst.BeginFrom (0, startState);
Проверьте работу кода командой Debug.Start Debugging. Выбирайте разные задачи и
щелкайте кнопкой Rnd start. На нижней панели должны появляться случайные значения
координаты, импульса и соответствующее значение интеграла движения в начальный
момент времени. Начальный момент времени всегда равен нулю. Метод BeginFrom
устанавливает начальное состояние механической системы.
Следующий шаг состоит в организации интегрирования уравнений движения с заданными
начальными условиями.
Для этого можно организовать цикл с заданным шагом по времени и вывод результатов
интегрирования на экран. Опишите поле, определяющее шаг интегрирования
/// <summary>
/// Хранит шаг интегрирования по времени
/// </summary>
double step = 0.05;
Пусть этот шаг остается неизменным.
Теперь поместите на форму объект класса Timer из списка Components окна Toolbox.
Среда назовет объект timer1. Свойство Interval объекта установите в 1000. Это
интервал времени в мсек, через который будет проводиться интегрирование с шагом step.
Добавьте в левом контейнере кнопку, которая будет запускать и останавливать процесс
интегрирования. Установите ее свойство DisplayStyle в Text, как и в предыдущих кнопках.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
159
Измените свойство Text этой кнопки на Start, а свойство Enabled установите в false.
Последнее будет указывать, что процесс нельзя стартовать до выбора начальных условий.
Но после выбора начальных условий кнопка Start должна работать. У нас начальные
условия выбираются кликом кнопки Rnd start. Поэтому в обработчик клика
toolStripButton2_Click кнопки Rnd start добавьте строку
toolStripButton3.Enabled = true;
Теперь выберите событие Click кнопки Start (ее имя
по умолчанию toolStripButton3)
и дважды щелкните по правому полю. В кодовом файле откроется обработчик
toolStripButton3_Click, в котором наберите код, стартующий механическую систему,
включая таймер, если он выключен, и наоборот
toolStripButton2.Enabled = !timer1.Enabled = !timer1.Enabled;
toolStripButton3.Text = timer1.Enabled ? "Stop" : "Start";
Параллельно изменяется свойство Enabled кнопки Rnd start (кнопка с именем
toolStripButton2), которая должна быть заблокирована во время счета и разблокирована,
если счет не идет. Надпись на кнопке toolStripButton3 должна быть Start в состоянии
ожидания счета и Stop во время счета. Теперь та же кнопка, которая стартовала процесс,
должна останавливать процесс интегрирования.
Добавьте в нижнем контейнере (строке статуса) дополнительный объект типа
StatusLabel. Среда даст новому объекту довольно длинное имя. Измените имя этого
объекта на processLabel и сотрите его свойство Text. В объекте processLabel будут
размещаться результаты интегрирования – текущее время, текущее состояние и интеграл
движения системы. Кроме того, текст объекта должен очищаться при задании новых
начальных условий. Поэтому в обработчик клика toolStripButton2_Click кнопки Rnd
start добавьте строку
processLabel.Text = string.Empty;
Теперь выберите событие Tick объекта timer1. Дважды щелкните по правому полю,
чтобы сформировать обработчик этого события. В кодовом файле появится обработчик
timer1_Tick.Внутри обработчика наберите код, интегрирующий уравнения движения
механической системы и размещающий результат интегрирования в тексте объекта
processLabel
mechSyst.PropagateTo (mechSyst.Time + step);
processLabel.Text = string.Format
("time={0:f};q={1:f};p={2:f};integral={3:f}",
mechSyst.Time, mechSyst.State [0].q, mechSyst.State [0].p,
integral (mechSyst.Time, mechSyst.State [0].q, mechSyst.State [0].p));
Осталось согласовать щелчок пункта меню с текущим состоянием приложения. Этот
щелчок должен прерывать счет, разблокировать кнопку Rnd start (с именем
toolStripButton2) и заблокировать кнопку Start (с именем toolStripButton3),
устанавливая ее надпись в Start. Кроме того, следует очистить текст в обеих метках
startLabel и processLabel в строке статуса. С этой целью добавьте в метод set свойства
ProblemIndex дополнительный код так, чтобы весь метод set имел вид
problemIndex = value;
toolStripButton2.Enabled=
!toolStripButton3.Enabled= timer1.Enabled = false;
toolStripButton3.Text = "Start";
startLabel.Text = processLabel.Text = string.Empty;
Теперь можно испытать работу приложения командой Debug.Start Debugging.
Компьютерные технологии (программирование на C#). Фомин Г.В. 2009. ЮФУ.
Download