Суббота, 27.04.2024, 04:34
Приветствую Вас Гость | RSS

Лекции

Меню сайта
Форма входа
Категории раздела
ТАУ (Теория автоматического управления) [31]
лекции по ТАУ
Экология [151]
учебник
Бухгалтерский учет и налогообложение в строительстве [56]
Дементьев А.Ю. - Практическое пособие
Психология [104]
Пип
информатика [80]
с# Карли Ватсон
современные стулья [0]
новинки
Поиск

Главная » 2010 » Февраль » 11 » События
00:26
События
События
В этой последней главе, посвященной ООП, будет обсуждаться один из наибо-
лее часто встречающихся в .NET приемов ООП — использование событий.
Обсуждение начнется с основ — с вопроса о том, что на самом деле представ-
ляют из себя события. После этого будет рассмотрено применение некоторых про-
стых событий на практике. Изучив этот вопрос, мы познакомимся с тем, каким
образом можно создавать и использовать собственные события.
Во второй части главы будет усовершенствована библиотека классов CardLib
с помощью включения в нее события. Кроме того — поскольку это наша послед-
няя остановка перед знакомством с более сложными темами — мы позволим себе
немного развлечься и создадим приложение для карточных игр, которое будет ис-
пользовать созданную нами библиотеку классов.
Что такое событие?
События похожи на исключительные ситуации в том смысле, что они создаются
(генерируются) объектами, и у нас имеется возможность задать код, который будет
выполняться при их наступлении. Между ними, однако, существует несколько важ-
ных отличий. Наиболее существенным является отсутствие конструкции, предназна-
ченной для обработки событий и эквивалентной конструкции t r y . . .catch. Вместо
этого на события необходимо подписываться. Подписаться на некоторое событие —
означает указать код, который должен выполняться при наступлении данного со-
бытия и который имеет форму обработчика событий.
У события может быть несколько обработчиков, приписанных к нему, и в слу-
чае наступления события все они будут вызваны. В их числе могут быть обработ-
чики событий, которые является частью класса, к которому принадлежит объект,
сгенерировавший событие, однако с тем же успехом обработчики событий могут
находиться и в других классах.
Сами по себе обработчики событий являются простыми функциями. Единствен-
ное ограничение, налагаемое на функцию — обработчика событий заключается
в том, что ее сигнатура (возвращаемое значение и параметры) должна соответст-
вовать обрабатываемому событию. Эта сигнатура является составной частью
определения события и задается с помощью делегата.
Именно благодаря тому, что делегаты используются
при обработке событий, они оказываются такими полезными;
именно по этой причине мы посвятили некоторое время
их изучению в главе 6; возможно, вы пожелаете перечитать
268 Глава 12
Приложение
тот раздел, чтобы освежить в памяти информацию о том
что представляют собой делегаты и каким образом
они используются.
Последовательность действий при обработке событий примерно такова:
Во-первых, в приложении создается объект, который может генерировать не-
которое событие. В качестве примера рассмотрим приложение, которое занимается
немедленной отправкой сообщений, а создаваемый им объект представляет собой
соединение с удаленным пользователем (см. рис. слева).
Этот объект мог бы сгенерировать событие, когда, на-
пример, по данному соединению приходит сообщение
от удаленного пользователя.
Создает
Соединение
Затем приложение подписывается на событие. При-
ложение для немедленной отправки сообщений может
добиться этого путем определения функции, допускаю-
щей использование с типом делегата, задаваемым дан-
ным событием, и передающей событию ссылку на эту
функцию. В качестве такой функции-обработчика со-
бытия может использоваться метод или какой-либо
другой объект — скажем, объект, представляющий не-
которое устройство вывода сообщений, предназначенное
для немедленного вывода сообщений при их поступле-
нии (см. рис. справа).
Приложение Соединение
Наступление
события
Вывод
Hi Мит
Когда возникает некоторое событие, подписавшийся
на него обработчик получает уведомление. При поступ-
лении сообщения через соединение осуществляется вы-
зов метода — обработчика события на объект устройства
вывода. Поскольку здесь применяется стандартный метод,
то объект, сгенерировавший событий, имеет возмож-
ность передать любую относящуюся к данному событию
информацию посредством параметров, что делает аппа-
рат событий весьма гибким. В данном примере одним из
параметров может быть текст, используемой для вывода
сообщения посредством обработчика события на объект,
представляющий устройство вывода (см. рис. слева).
Использование событий
В этом разделе сначала будет рассматриваться код, требующийся для обработ-
ки событий, а затем вопрос о том, каким образом программист может определять
и использовать свои собственные события.
Обработка событий
Чтобы обработать событие, на него необходимо подписаться путем задания
функции-обработчика этого события, причем сигнатура этой функции должна сов-
падать с сигнатурой, которая задается делегатом, предназначенным для работы
с данным событием. В качестве примера рассмотрим простой объект — таймер,
генерирующий события, в результате наступления которых вызывается функция-
обработчик.
События 269
Практикум: обработка событий
1. Создайте новое консольное приложение с именем chi2Ex0i
В Директории C:\BegCSharp\Chapterl2\.
2. Модифицируйте код в ciassi.cs следующим образом:
using System;
using System»Timers;
namespace Chl2Ex01
class Classl
static int counter = 0;
static string displayString =
"This string will appear one letter at a time. ";
static void Main(string[] args)
Timer myTimer = new Timer (100) ;
myTimer.Elapsed += new ElapsedEventHandler(WriteChar);
myTimer.Start{);
Console .ReadLine (•:) ;
static void WriteChar(object source, ElapsedEventArgs e)
Console.Write(displayString[counter++ % displayString.Length3);
}
3. Запустите приложение (нажатие клавиши Enter в процессе
работы приложения приведет к прекращению его выполнения):
raC\BegCSharp\Glidpl:eri2\Chl2EKOI\bin\D
This string will appear огуе letter at a time* This string will appeal* one letterg
at a tine». This? string will appear one letter at a time» This string will appea
r one letter at a tiifne» Ibis string uill appear one letter at a time. Ibis s-trirt
$ will appear one letter at a tine- Tliis string will appear one letter at a time
- Ibis string will appear one le J
Как это работает
Объект, ИСПОЛЬЗуемыЙ ДЛЯ событий, представляет Собой экземпляр класса System.
Timers-Timer. При инициализации этого объекта задается интервал времени
(в миллисекундах). После того как объект Timer запускается посредством вызова
его метода start о , генерируется последовательность событий, отстоящих друг от
друга во времени на заданную величину интервала. Функция Main о инициализирует
объект Timer с временным интервалом, равным 100 миллисекундам, поэтому по-
сле запуска он генерирует события с частотой 10 раз в секунду:
static void Main(string[] args)
{
Timer myTimer = new Timer (100) ;
270 Глава 12
Object 8г<и**ег | R&iP-yyi <;«•?>; c<; ; : x
S- С) 5уйеп»^ййдгой«а ]••'•••••*& Obvt^) i
!-iff!rtwv*i •••• • . ^ . :: .. • •••••-•:.
j .!••'•?: fflSBffl[:'': • ' •''• • •' • •: :
d
Объект Timer обладает событием,
которое называется Elapsed (истек
интервал), что можно увидеть, вос-
пользовавшись окном Object Browser,
представленным на рисунке слева.
Сигнатура, необходимая для об-
работчика рассматриваемого вида
событий, соответствует типу делегата
System. "Timers . ElapsedEventHandler,
который является одним из стандарт-
ных делегатов, определенных в .NET
Framework. Этот делегат может ис-
пользоваться для функций, обладаю-
щих следующей сигнатурой:
void functionName{object source, ElapsedEventArgs e);
Объект Timer передает ссылку на самого себя в первом параметре и экземпляр
объекта ElapsedEventArgs — во втором. На данном этапе можно не обращать на
эти параметры никакого внимания — позже мы вернемся к их рассмотрению.
В нашем коде имеется метод с совпадающей сигнатурой:
static void WriteChar(object source, ElapsedEventArgs e)
{
Console.Write(displayString[counter++ % displayString.Length]);
Данный метод использует два статических поля класса classi — counter
и displayString — для вывода отдельного символа. При каждом вызове этого
метода будет выводиться новый символ.
Следующая задача — привязать этот обработчик к событию, т. е. подписаться
на событие. С этой целью используется оператор + = , который позволяет вклю-
чить в событие обработчик в виде нового экземпляра делегата, инициализирован-
ного с используемым методом обработчика событий:
static void Main(string[] args)
{
Timer myTimer = new Timer (100) ;
myTimer.Tick += new EventHandler(WriteChar);
Эта команда (где используется присущий делегатам странный на вид синтаксис)
добавляет обработчик в список обработчиков, вызываемых при возникновении со-
бытия Elapsed. Можно добавлять в этот список сколь угодно много обработчиков,
при этом все они должны удовлетворять требуемому критерию. Каждый из этих
обработчиков будет вызываться при наступлении события поочередно.
Все, что осталось необходимым выполнить в функции MainO,— это запустить
таймер:
myTimer.Start();
Поскольку не нужно, чтобы приложение окончилось до того, как будет обрабо-
тано хотя бы одно событие, то следует приостановить выполнение функции Main о .
Простейшим способом такой приостановки является ожидание ввода пользователя,
так как выполнение этой команды не сможет завершиться ранее, чем пользователь
введет строку текста и/или нажмет клавишу Enter.
Console.ReadLine();
События
271
зывает описанный метод
нением оператора d
о к а з ы в а е т с я — M , объеКт
^ "З С Т у П Л е н и и °4 eP^°ro события он вы-
? В Ы П 0 Л Н Я е т с я параллельно с выпол-
Определение событий
этой главы, и создать
^ с о б ы т и я , ^
Рассмотренную в начале
'УД- генериро-
2. Добавьте новый класс - c o m . c t i m - , ф а й л С о „ , „ . ^
using System;
using System.Timers;
namespace Chl2ExO2
Public delegate void MessageHandler (string messageText);
public class Connection
public event MessageHandler Mess age Arrived;
private Timer pollTimer;
public Connection()
pollTimer = new Timer (100) ?
} PonT i mer.Elapsed +- neW EiapSedEventHandler(cheokForMesSage);
public void Connect()
public void Disconnect()
Private void CheckForMessage<object source, E 1aPseaEvent A r g s
e )
if ((randOIn.Next(9) == 0)
(MessageArrived i= null))
MessageArrived(-Hello Mum]*);
272 Глава 12
3. Добавьте НОВЫЙ класс — Display — В файл Display, cs:
using System;
namespace Chl2ExO2
public class Display
public void DisplayMessage(string message)
Console.WriteLine("Message arrived: {0}", message);
4. Внесите следующие изменения в файл classi.cs:
using System;
namespace Chl2ExO2
class Classl
static void Main(string[] args)
Connection myConnection = new Connection () ;
Display myDisplay = new Display () ;
myConnection.MessageArrived +==
new MessageHandler (myDisplay.DisplayMessage);
myConnection.Connect();
Console.ReadLine();
5. Запустите приложение
(см. рис. слева).
Как это работает
Класс, который выполняет львиную долю
работы в данном приложении,— это класс
connection. Объект Timer используется эк-
земплярами этого класса во многом анало-
гично тому, как он использовался в первом
приведенном в данной главе примере, т. е.
инициализируется конструктором класса
и предоставляет доступ к информации о его
состоянии (включен или выключен) через
ФУНКЦИИ Connect() И Disconnect() :
public class Connection
{
private Timer pollTimer;
public Connection()
{
pollTimer = new Timer (100) ;
pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);
>
public void Connect()
{
pollTimer.Start();
События , 273
public void Disconnect()
{
pollTimer.Stop();
Кроме того, в конструкторе регистрируется обработчик событий для события
Elapsed — точно так же, как это делалось в первом примере. Метод обработчи-
ка — checkForMessage () — будет генерировать событие в среднем один раз на
десять вызовов. Но прежде чем перейти к рассмотрению соответствующего кода,
следует познакомиться с собственно определением события.
Прежде чем определять событие, необходимо определить тип делегата, который
будет использоваться с данным событием, т. е. позволяющий определить, какой
именно сигнатуре должен соответствовать метод, выполняющий обработку собы-
тия. Это достигается за счет использования стандартного синтаксиса описания де-
легата, который определяется как общий в пространстве имен chiiExO2, ДЛЯ ТОГО
чтобы обеспечить доступ к нему из внешнего кода:
namespace Chl2ExO2
{
public delegate void MessageHandler (string messageText);
Этот тип делегата, которому присвоено имя MessageHandler, определяет сигнатуру
функции типа void, обладающей одним параметром типа string. Этот параметр
может быть использован для передачи объекту Display сообщения, полученного от
объекта Connection.
После описания делегата (либо использования подходящего делегата, описан-
ного ранее) появляется возможность определить собственно событие как член
класса Connection:
public class Connection
{
public event MessageHandler MessageArrived;
Далее СОбыТИЮ Присваивается ИМЯ (в данном Случае — ИМЯ MessageArrived),
после чего это событие объявляется с помощью ключевого слова event и указыва-
ется тип делегата, который необходимо использовать (MessageHandler, определен-
ный ранее).
После того как событие определено подобным способом, появляется возмож-
ность вызывать его наступление простым обращением по имени — точно таким
же образом, как если бы оно представляло собой метод с сигнатурой, определяе-
мой делегатом. Например, вызвать наступление данного события можно следую-
щим образом:
MessageArrived("This is a message.*);
Если бы делегат был описан как не имеющий параметров, то можно было бы
вызвать наступление события проще:
MessageArrived();
Напротив, можно было бы описать большее число параметров, что потребова-
ло бы большего количества кода для генерации события.
В данном случае метод CheckForMessage о (проверка, не пришло ли сообщение)
имеет следующий вид:
274 [ Глава 12
private void CheckForMessage(object source, ElapsedEventArgs e)
{
Console.WriteLine("Checking for new messages.*) ;
Random random = new Random () ;
if ((random. Next (9) == 0) && (MessageArrived != null))
{
MessageArrived("Hello Mum! •) ;
Здесь используется экземпляр объекта класса Random, который описывался в пред-
шествующих главах. Он генерирует случайное число в диапазоне между 0 и 9,
и наступление события вызывается только в том случае, если это число равно 0,
что должно происходить в 10% случаев. Это позволяет эмулировать опрос соеди-
нения на предмет поступления сообщения, которое не обязательно приходит каж-
дый раз при выполнении проверки.
Обратите внимание на то, что здесь используется некоторая дополнительная
логика. Наступление события вызывается только в том случае, если значение вы-
ражения MessageArrived != null равняется true. Это выражение, в котором также
используется синтаксис делегата, хотя и в несколько необычном виде, означает:
"Имеются ли у данного события какие-либо подписчики?". Если подписчиков не
существует, то MessageArrived получает значение null, и в этом случае нет ника-
кого смысла генерировать наступление события.
Класс, являющийся подписчиком данного события, называется Display и состо-
ит из единственного метода DisplayMessageO, который описывается следующим
образом:
public class Display
{
public void DisplayMessage(string message)
{
Console.WriteLine("Message arrived: {0}*, message);
Сигнатура этого метода соответствует сигнатуре, описываемой делегатом дан-
ного типа (и являющейся общей, что представляет собой необходимое условие для
обработчиков событий во всех классах, отличных от класса, в котором генерирует-
ся данное событие), поэтому данный метод может использоваться для реакции на
наступление события MessageArrived.
Все, что теперь необходимо сделать в методе MainO,— это инициализировать
Экземпляры КЛаССОВ Connection И Display И запуСТИТЬ процесс. Код, который ДЛЯ
этого нужно использовать, аналогичен коду из первого примера:
static void Main(string[] args)
{
Connection myConnection = new Connection () ;
Display myDisplay = new Display () ;
myConnection.MessageArrived +=
new MessageHandler (myDisplay.DisplayMessage);
.myConnection.Connect();
Console.ReadLine();
}
В данном случае, после того как аппарат работы с событиями запущен посред-
ством обращения к методу Connect о объекта connection, для приостановки вы-
полнения функции MainO вызывается метод Console.ReadLineO.
События 275
Многоцелевые обработчики событий
Сигнатура, используемая для события Timer.Elapsed, содержит два параметра,
тип которых очень часто встречается в различных обработчиках событий. Вот эти
параметры:
• object source — ссылка на объект, который вызвал
наступление события
• ElapsedEventArgs e — параметры, передаваемые событием
Причина, по которой в данном событии — да и во многих других событиях тоже —
используется параметр типа object, заключается в том, что очень часто необхо-
димо использовать один и тот же обработчик событий для обработки нескольких
одинаковых событий, генерируемых различными объектами, и при этом отличать,
каким именно объектом сгенерировано данное событие.
Чтобы пояснить и проиллюстрировать это, следует несколько расширить пре-
дыдущий пример.
Практикум: Использование многоцелевого обработчика событий
1. Создайте новое консольное приложение с именем chi2ExO3
В директории C:\BegCSharp\Chapterl2\.
2. Скопируйте КОД В файлы Classl.cs, Connection.cs И Display.cs
из соответствующих файлов в chi2ExO2 и убедитесь в том, что
во всех файлах изменены пространства имен с chi2ExO2 на chi2ExO3.
3. Добавьте НОВЫЙ класс — MessageArrivedEventArgs, который находится
В файле MessageArrivedEventArgs.cs!
using System;
namespace Chl2ExO3
public class MessageArrivedEventArgs : EventArgs
private string message;
public string Message
get:
Sireturn message;
.}•
public MessageArrivedEventArgs()
message = "No message sent,*;
}
public MessageArrivedEventArgs(string newMessage)
}
276
— ~ Глава 12
4. Внесите следующие изменения в файл connection.cs:
namespace Chl2ExO3
{
public delegate void MessageHandler(Connection source,
MessageArrivedEventArgs e);
public class Connection
{
public event MessageHandler MessageArrived;
private string name; • •
: •: ; : r; ; '••• : : - • • •' • 00ЩШЖШФШМШШШ^Ш§0^%:% -v;-::i? t
{
get
return name; : :
s e t •.-,•;.•..••'•
name = value;
private void CheckForMessage(object source, EventArgs e)
Console.WriteLine('Checking for new messages.*) ;
Random random = new Random () ;
if ((random. Next (9) •• 0) && (MessageArrived J=null))
{ '' '»'
MessageArrived(this, new MessageArrivedEventArgs("Hello Mum!•))\
5. Внесите следующие изменения в файл Display.cs:
public void DisplayMessage(Connection source, MessageArrivedEventArgs e)
{
Console.WriteLine("Message arrived from: {0}", source.Name);
Console.WriteLine("Message Text: {0}*, e.Message);
>
6. Внесите следующие изменения в файл ciassi.cs:
static void Main (string [J args)
{
Connection myConnectionl = new Connection () ;
myConnectionl.Name = "First connection.";
Connection myConnection2 = new Connection () ;
myConnection2.Name = "Second connection.";
Display myDisplay = new Display() ;
myConnectionl.MessageArrived +=
new MessageHandler(myDisplay.DisplayMessage);
myConnectionl.Connect();
myConnection2.Connect();
Console.ReadLine();
События 277
Che
Ch»
Che
He s
Пек
Ch©
Che
Cl»e
Che
Hes
lies
Che
lies
Поз
Che
eking foi*
eking for
sag©
sago
ekxng
ckiftSf
cki«0
eking
sag©
ekin«j
Cliins
eking
'Checking
Checkiny
lext
for
•fer
f o r
ai*r-i«
Text
f o r
fti-ri*
Text
f o r
f o r
f o r
Cheeks^ fer
Che
Che
eking
Checking
Cheeking
f o r
f o r
f o r
f o r
neu ness
new niegs
ted fro^s
: Hello H
new riwss
new mess
new ness
new mess
^ed fro»:
: Hello П
new «ess
Л2Й f*»Or»S
: Hello П
•ftew PSOSS
«e» mess
new rmss
new rsess
ftew ness
neu. ness
ftow n«ss
neu Fieas
new ness
ages»
' l ? i r s t «o
unf
"*ses*
Fljfst со
it.nt
ages»
S«c©nd с
ages.
ftSfCSf «•
ages. ;
sVges»
agres .
ages*
meet ion*
annection,
Qei3
F!
1
j
fefl
7. Запустите приложение (см. рис. слева).
Как это работает
Передавая ссылку на объект, который вызвал
наступление события в качестве одного из пара-
метров обработчика событий, мы получаем возмож-
ность настраивать реакцию обработчика событий
на конкретные объекты. Эта ссылка обеспечивает
возможность доступа к исходному объекту, вклю-
чая его свойства.
Передавая параметры, содержащиеся в классе,
КОТОрыЙ наследуется ОТ System.EventArgs (как,
например, ElapsedEventArgs), МЫ ПОЛучаеМ ВОЗ-
МОЖНОСТЬ передавать в качестве параметров (как, например, параметр Message
В классе MessageArrivedEventArgs) любую дополнительную информацию.
Кроме того, такие параметры позволяют использовать преимущество полимор-
физма. МОЖНО было бы Определить обработчик события MessageArrived СЛедуЮ-
щим способом:
public void DisplayMessage(object source, EventArgs e)
{
••••••••v:\Console-.WriteLine('Message arrived from: { 0 } % .
((Connection)source).Name);
Console.WriteLine("Message Text: {0}*,
((MessageArrivedEventArgs)e).Message);
и видоизменить определение делегата в файле connection.es таким образом:
public delegate void MessageHandler(object source, EventArgs e) ;
Приложение будет выполняться в точности так же, как оно выполнялось и ранее,
однако в этом случае функция DisplayMessage о стала более гибкой (по крайней
мере в теории — для получения естественного продукта потребуется дополнитель-
ная реализация). Тот же самый обработчик событий мог бы работать и с другими
событиями, такими как Timer.Elapsed, хотя для этого потребуется внести некото-
рые дополнительные изменения в тело обработчика, которые позволят ему дол-
жным образом обрабатывать параметры, передаваемые при наступлении события
(в результате приведения ИХ ТИПа К ТИПу Объектов Connection И MessageArrived-
EventArgs в данном случае возникнет исключительная ситуация и вместо этого
следует использовать оператор as).
Прежде чем продолжить движение вперед, следует обратить внимание на то,
что результатом выполнения данного примера будут, скорее всего, пары событий,
сгенерированных двумя объектами connection. Такая ситуация возникает благода-
ря способу, с помощью которого происходит генерирация случайных чисел классом
Random. В момент создания экземпляра объекта типа Random используется некото-
рое начальное значение. Оно применяется для получения последовательности
псевдослучайных чисел с помощью некоторой сложной функции. (Компьютеры не
имеют возможности создавать действительно случайные числа.) Можно задавать
начальное значение в конструкторе экземпляра Random, однако если он не исполь-
зуется (как в настоящем примере), то в качестве начального значения использует-
ся текущее время. Поскольку мы создаем экземпляры и начинаем использовать
оба наших объекта connection в последовательных строках кода, то это с большой
278 Глава 12
вероятностью может привести к тому, что при реакции на наступление событий
Timer.Elapsed ими будут использоваться одни и те же начальные значения, поэ-
тому в том случае, когда один из объектов отправляет свое сообщение, второй,
вероятнее всего, будет отправлять свое. Это прямое следствие высокой скорости
обработки инструкций, и, для того чтобы обойти эту ситуацию, необходимо зада-
вать начальные значения каким-то другим способом. Одно из возможных решений,
которое здесь не будет реализовываться, заключается в том, чтобы использовать
единственный ЭКЗеМПЛЯр Класса Random, ДОСТУПНЫЙ ИЗ Обоих Объектов Connection.
В таком случае оба этих объекта будут использовать одну и ту же последователь-
ность случайных чисел, поэтому одновременная отправка сообщений окажется
маловероятной.
Возвращаемые значения и обработчики событий
Все обработчики событий, рассматриваемые до настоящего момента, обладали
возвращаемым типом void. Существует возможность задавать для события тип
возвращаемого значения, однако это может привести к некоторым проблемам, ко-
торые связаны с тем, что наступление некоторого события может приводить к вы-
зову нескольких обработчиков. Если каждый из вызванных обработчиков будет
возвращать какое-либо значение, то становится непонятным, какое именно воз-
вращаемое значение будет получено.
В системе эта проблема решается таким образом, что доступным оказывается
только последнее возвращаемое обработчиком значение, и это будет значение,
возвращаемое обработчиком, который последним подписался на данное событие.
Вероятно, существуют ситуации, когда такая функциональная возможность ока-
жется полезной, хотя авторам так и не удалось придумать ни одной такой ситуа-
ции. Рекомендуется использовать обработчики событий типа void и избегать
параметры типа out.
Расширение и использование CardLib
Теперь следует внести некоторые дополнения в библиотеку классов, созданную
в предшествующей главе,— chiicardLib. Будет использоваться проект по созда-
нию библиотеки классов с именем chi2CardLib, в котором изначально находится
код, идентичный коду в chiicardLib (не считая того, что в нем используются имена
ИЗ пространства Имен Chl2CardLib ВМеСТО ChllCardLib).
Событие, которое мы собираемся включить в библиотеку, будет генерироваться
в тот момент, когда последний объект card получается в объекте Deck с помощью
метода GetCardO, ПОСЛе чего инициируется наступление события LastCardDrawn
(последняя сданная карта). Это событие позволит подписчикам автоматически пе-
ретасовывать колоду, уменьшая объем вычислений, которые приходится выпол-
нять Клиенту. Делегат, Определенный ДЛЯ данНОГО События (LastCardDrawnHandler),
должен передавать ссылку на соответствующий объект Deck, для того чтобы об-
работчик мог иметь доступ к методу shuffle о независимо от своего местонахож-
дения:
namespace Chl2CardLib
{
public delegate void LastCardDrawnHandler(Deck currentDeck);
События 279
Код, который используется для определения и генерирования события, весьма
прост:
public event LastCardDrawnHandler LastCardDrawn;
public Card GetCard(int cardNum)
{
if (cardNum >= 0 && cardNum <= 51)
{
if ((cardNum == 51) && (LastCardDrawn != null))
LastCardDrawn(this);
return cards[cardNum];
}
else
throw new CardOutOfRangeException((Cards)cards.Сlone());
}
Вот и весь код, который потребовался для включения события в определение
класса Deck. Теперь осталось только воспользоваться им.
Клиентская программа карточной игры для CardLib
Позор, если, потратив столько времени на разработку библиотеки CardLib, мы
так и не воспользуемся ей. Перед тем как завершить раздел, посвященный ООП
в С# и .NET Framework, самое время немного поразвлечься и написать основу для
приложения, которое использует хорошо знакомые нам классы, связанные с играль-
ными картами.
Как и в предшествующих главах, мы добавим клиентское консольное приложе-
ние в chi2CardLib, добавим ссылку на проект chi2CardLib и объявим этот проект
начальным. Это приложение мы назовем chi2Cardciient.
Для начала создадим новый класс, который мы назовем Player (игрок), в новом
файле в приложении chi2Cardciient — Player.cs. В нем будет содержаться част-
ное поле cards с именем hand (рука), частное строковое поле с именем name и два
поля, доступных в режиме "только чтение": Name и piayHand. Эти поля использу-
ются для представления частных полей. Обратите внимание на то, что, хотя свой-
ство PiayHand доступно в режиме "только чтение", у нас будет доступ на запись по
ссылке на возвращаемое поле hand, что позволит изменять карты, находящиеся на
руках у данного игрока.
Мы также спрячем конструктор по умолчанию, объявив его как частный, и до-
бавим общий конструктор не по умолчанию, которому будут передаваться первона-
чальные Значения СВОЙСТВа Name Экземпляров класса Player.
Наконец, мы предусмотрим метод типа bool с именем HasWonO (выиграл). Этот
метод будет возвращать значение true, если все карты, находящиеся на руках
у данного игрока, имеют одну масть (довольно простое условие выигрыша, но
в данном случае это не играет особой роли).
Код в Player.cs имеет следующий вид:
using System;
using Chl2CardLib;
namespace Chl2CardClient
{
public class Player
^ _ ^ ^ _ . {
private Cards hand;
private string name;
280 Глава 12
public string Name
• : : / ^ ' '•••• : : ••• : • ••• ••• g e t • •, . . • •' ' ••; • ; - ' • ' ' . ' ' [ ' : . ] \ : . '. • '. . : • • : • ' • • • , : ; 7 ' • ' : y - . ' : . . ' ' • . • . • . '
{ .. • '• ; : - ;
return name;
public Cards PlayHand
{
get
{ '
return hand;
} ' ' ' ' '
} . - ; / . ^ : V ; r ' — : •••..••.::. •••..•;•• - : - - : - - , ; : : ;
private PlayerO
{
} •
public Player (string newName)
{
name = newName;
hand = new Cards ();
} . •...•
public boc-1 HasWon ()
t
b o o l w o n = t r u e ; •;': , ;' '••.•••. ••
Suit match = hand[0].suit;
for (int i = 1; i < hand.Count; i++)
{
won &= hand[i].suit == match;
}
return won;
Далее следует определить класс, который будет вести карточную игру. Назовем
его Game (игра). Он будет размещен В файле Game.cs В проекте Chl2CardClient.
В качестве членов этого класса предусмотрено наличие четырех частных полей:
• piayDeck — переменная типа Deck, в которой содержится
используемая колода карт
• currentcard — значение типа int, которое используется
в качестве указателя на очередную сдаваемую карту
• players — массив объектов типа Player, которые представляют
участников карточной игры
• discardedCards — семеЙСТВО объектов Cards, которые
уже вышли из игры, но пока еще не возвращены в колоду
Конструктор данного класса по умолчанию инициализирует и перетасовывает
колоду Deck, находящуюся в piayDeck, устанавливает указатель currentcard на О
(на Первую Карту В piayDeck) И привязывает Обработчик СОбыТИЙ Reshuffle ()
(пересдача) к событию piayDeck.LastcardDrawn. Названный обработчик просто
перетасовывает колоду, включая карты, находящиеся в discardedCards, и подго-
тавливает новую колоду к раздаче, устанавливая указатель currentcard на 0.
События 281
В классе Game содержатся также два полезных метода: SetPiayers (), задающий
игроков (в виде массива объектов типа Player), и DeaiHandsO, сдающий карты
игрокам (по 7 карт каждому). Число игроков должно находиться в диапазоне от 2
до 7, что гарантирует достаточное количество карт для каждого из них.
Наконец, в файл включается метод PiayGame (), в котором содержится логика
игры. Мы вернемся к этой функции, после того как рассмотрим код в файле
ciassi.cs. Весь остальной код файла Game.cs имеет следующий вид:
using Systern;
using Chl2CardLib;
namespace Chl2CardClient
public class Game
private int currentCard;
private Deck playDeck;
private Player[.] players;
private Cards discardedCards;
public Game()
currentCard = 0;
playDeck = new Deck (true) ;
playDeck.LastCardDrawn += new LastCardDrawnHandler(Reshuffle);
playDeck.Shuffle();
. : private void Reshuffle(Deck currentDeck)
currentDeck.Shuffle();
discardedCards - new Cards();
currentCard = 0;
public void SetPiayers(Player[] newPlayers)
! if (newPlayers.Length > 7)
throw new ArgumentException(^A maximum of 7 players may play this" +
: * game.");
if (newPlayers.Length < 2)
throw new ArgumentExceptionCA minimum of 2 players may play this" +
• . •:'':''••• •' • " g a m e . " ) ; . • -: ' :
; :
: players = newPlayers; : •: •
private void DealHarids ()
for (int p - 0; p < players.Length; p++)
• : . . : Л;:-1; :; for (int с = 0; с < 7; с++)
players [p] . PlayHand. Add (playDeck. GetCard (currentCard++))'.;"".
} • • :; ; : • : • : ;; ; •
public int PiayGame()
// Сюда будет включаться дальнейший код.
}
Глава 12
В файле ciassi.cs содержится функция Maino, которая инициализирует и за-
пускает игру. Эта функция выполняет следующие шаги:
• На экран выводится заставка.
• Пользователь получает приглашение на ввод числа игроков,
которое должно находиться в интервале между двумя и семью.
• В соответствии с введенным числом игроков определяются
Объекты ТИПа Player.
• Каждому пользователю выводится приглашение,
запрашивающее его имя, которое будет использоваться
при инициализации соответствующего объекта типа Player в массиве.
• Создается Объект Game И С ПОМОЩЬЮ метода SetPlayers ()
определяются игроки, которые будут принимать участие в игре.
• Начинается собственно игра посредством метода PiayGame ().
• Возвращаемое значение метода PiayGame () типа int используется
для вывода сообщения о том, кто из игроков выиграл
(возвращаемое значение представляет собой индекс победителя
В массиве Объектов ТИПа Player).
Код, выполняющий все эти действия (снабженный комментариями для большей
ясности), приводится ниже:
static void Main (string [] args)
{
// Вывод заставки
Console.WriteLine<*KarliCards: a new and exciting card game.');
Console.WriteLineCTo win you must have 7 cards of the same suit in" +
* your hand.");
Console.WriteLine();
// Приглашение для ввода числа игроков
bool inputOK = false;
int choice = -1; ... .:
. . dO-' • '• • • •'• • . • : ; • : • : : ,' . • : • : • • • • • • • , . \ ,• . : . . : " •• ' ' •' : / •' •••• ' • . • • • : • • • - # '.: : •• •• . •
{
Console.WriteLine('How many players (2-7)?*);
string input = Console.ReadLineO ;
try
// Попытка преобразовать введенную информацию в корректное
// число игроков
choice = Convert.ToInt32(input);
if ((choice >= 2) && (choice <= 7))
inputOK = true;
}
catch
{
// В случае неудачи продолжаем выводить приглашение
} while (inputOK == false);
//Инициализация массива объектов Player.
Player[] players = new Player[choice];
События
//Запрос имен игроков.
for (int p = 0; р < players.Lerxgth; р++)
{
Console.WriteLine(•Player {0}, enter your name:*, p + 1)
string playerName = Console,ReadLineO ;
players[p] = new Player (playerName) ;
/ / Начало игры
Game newGame = new Game () ; -.
newGame.SetPlayers(players);
int whoWon = newGame.PiayGame();
// Вывод информации о выигравшем игроке
Console.WriteLine(*{0} has won the game!", players[whoWon].Name);
Теперь обратимся к методу PlayGameO, представляющему основное тело при-
ложения. Этот метод не будет описываться подробно, так как он снабжен коммен-
тариями, что делает его несколько более понятным. На самом деле, за некоторыми
исключениями, в коде нет ничего столь уж сложного.
Игра протекает следующим образом: каждый игрок видит собственные карты
и одну открытую карту на столе. Игроки могут взять либо открытую карту, либо
новую карту из колоды. Взяв карту, игрок должен сбросить одну карту, положив ее
либо на место открытой карты, если им была взята именно открытая карта, либо
поверх лежащей на столе (отправляя предыдущую карту, лежащую на столе, в се-
мейство discardedCards).
Ключевым моментом, который необходимо иметь в виду при изучении кода, яв-
ляется способ, используемый для манипуляций над объектами типа card. Теперь
становится понятным, почему они были описаны как переменные ссылочного типа,
а не как значимого (посредством использования структуры). Один объект card может
находиться одновременно в нескольких местах, поскольку на него могут ссылаться
объект Deck, ПОЛЯ hand различных объектов Player, семеЙСТВО discardedCards
и объект piaycard (поскольку карта в настоящий момент находится на столе). Это
значительно упрощает процедуру отслеживания движения карт и используется,
в частности, в коде, который отвечает за сдачу очередной карты из колоды. Карта
может быть принята только в том случае, если она не находится у какого-нибудь
Игрока На руках, на СТОЛе ИЛИ В семействе discardedCards,
Код имеет следующий вид:
public int PlayGameO
{
// Игра происходит только при наличии игроков
. ••••• if (players == nulI) . • ' .'•. ..;,•'•-•:/:-.'••:••:•:••:•:••••':•'.,'..• V
. • . r e t u r n - 1 ; •• • . - ^ \ . • •• . • • • • • • • • •• . • • . : , : ; • . • , • • • • , • • • • • • • •
// Первоначальная сдача карт.
DealHands();
// Инициализация переменных, используемых в карточной игре, включая
//карту, которая кладется на стол первой: playCard.
bool GameWon = false; •
int currentPlayer;
Card playCard = playDeck.GetCard(currentCard++);
284 .^_
Глава 12
y:z:?G2:zrj::rmeTc*до
// Проход по всем игрокам для каждого круга игры
for (currentPlayer = 0; currentPlayer < players.LengthcurrentPlayer++)
'
{ • : ' • ' -: - - • " ^ ' •.":.;
// Вывод информации о текущем игроке, об имеющихся у него на
// руках картах и о карте, лежащей на столе
ConS
3oll e <wr i,t e
T
L i n e ( M 0 }'S t U r n'*' ^^[currentPlayer] .Name);
Console.WriteLine("Current hand:");
foreach (Card card in players[currentPlayer].PlayHand)
Console.WriteLine(card);
Console.WriteLine(-Card in play: {0}", playCard);
// Вывод подсказки игроку - л ю б о взять открытую карту со стола,
// либо.новую карту из колоды
bool inputOK = false;
do
' (
Console.WriteLine("Press T to take card in play or D to • +
Mraw: ") ;
string input = Console.ReadLineO ;
if (input.ToLowerO == ut")
{
// Добавление карты, взятой со стола, к картам,
// находящимся у игрока
Console.WriteLine("Drawn: {0}*, playCard)-
Players[currentPlayer].PlayHand.Add(playCard);
inputOK = true;
}
if (input.ToLowerO == "d")
{
// Добавление карты, взятой из колоды, к картам,
// находящимся у игрока
Card newCard;
// Добавление карты только в том случае, если она еще
// не на руках
bool cardlsInPlayerHand;
do
newCard = playDeck.GetCard(currentCard++);
cardlsInPlayerHand = false;
// Просмотр в цикле карт всех игроков с целью
// обнаружения карты newCard у кого-либо на руках
foreach (Player testPlayer in players)
cardlsInPlayerHand |=
testPlayer.PlayHand.Contains(newCard);
} while (cardlsInPlayerHand);
// Включение обнаруженной карты в карты, находящиеся
/ / н а р у к а х . . . • • • • : ' . . • ; • • • • • • • • ; : ' ; . . . : ' : , • • / : ' : . " : : . . ; . ; • • : . . • .
Console.WriteLine("Drawn: {0}", newCard);
players[currentPlayer].PlayHand.Add(newCard);
: input OK = true;
>
} while (inputOK == false);
События
// Вывод перенумерованных карт в новом раскладе
Console.WriteLine("New hand:");
for (int i = 0; i < players[currentPlayer].PlayHand.Count; i++)
{
Console.WriteLine("{0}: {1}", i + 1,
players[currentPlayer].PlayHand[i- ]);
- / / Приглашение игроку снести какутр-либо карту
inputOK = false;
int choice = -1;
do
{
Console.WriteLine("Choose card to discard:*);
string input = Console.ReadLineO ;
try
{
// Попытка преобразовать введенную информацию в допустимый
// диапазон карт
choice ~ Convert .-To Int 3 2 (input) ;
if ((choice > 0) && (choice <= 8) )
inputOK = true;
}
catch
{
// В случае неудачи при попытке преобразовать введенную
i: .•// информацию, продолжаем выводить приглашение
}
} while (inputOK == false);
//Помещаем ссылку; на сносимую карту в playCard (кладем карту
// на стол) , после чего удаляем ее из карт, находящихся на руках
playCard = players[currentPlayer].PlayHand[choice - 1] ;
players[currentPlayer].PlayHand.RemoveAt(choice - 1 ) ;
Console.WriteLine("Discarding: {0}", playCard);
// Разделение текста, выводящегося для различных игроков
Console.WriteLine () ;
// Проверка на предмет того, не выиграл ли кто-либо из игроков/
// и выход из цикла по игрокам в таком случае
GameWon = players [currentPlayer] .HasWonO ;
if (GameWon -= true)
: v ;.'.••• . b r e a k ; • . ... • .,'.'' ' .'':'- : • ' • ' : ' ".'".
> v Шй:. '••' ;•' V : ' '
} while (GameWon == false);
// Завершение игры и возврат информации о выигравшем игроке.
return currentPlayer;
Вы можете развлечься и поиграть в эту игру, только не забудьте уделить неко-
торое время подробному изучению кода. Вы также можете попробовать включить
точку останова в метод Reshuffle о и запустить игру с 7 игроками. Если при этом
карты будут постоянно сдаваться, а сданная карта — сбрасываться, то очень скоро
произойдет пересдача, поскольку при игре с 7 игроками остаются свободными толь-
ко три карты. Таким способом вы сможете убедиться, что все работает правильно,
наблюдая за тем, как эти три карты появляются заново.
286 Глава 12
Итоги
В этой главе мы рассмотрели важную тему, посвященную событиям и их обра-
ботке. Несмотря на то, что тема эта довольно сложна и поначалу с трудом воспри-
нимается, код, который используется при работе с событиями, оказывается очень
простым, и мы обязательно будем широко использовать обработчики событий
в остальной части книги.
Рассмотрев несколько простых иллюстративных примеров событий и способов их
обработки, мы также внесли соответствующие изменения в библиотеку cardLib,
которой мы занимались на протяжении нескольких предшествующих глав. После
завершения создания библиотеки она была использована в простом приложении
для карточной игры. Само приложение может служить демонстрацией большинства
приемов программирования, с которыми мы успели познакомиться в первой части
настоящей книги.
Этой главой завершается не только исчерпывающее о
Категория: информатика | Просмотров: 1226 | Добавил: basic | Рейтинг: 0.0/0
Всего комментариев: 0
Имя *:
Email *:
Код *:
Календарь
«  Февраль 2010  »
ПнВтСрЧтПтСбВс
1234567
891011121314
15161718192021
22232425262728
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

krutoto.ucoz.ru
Бесплатный конструктор сайтов - uCoz