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

Books / 3_C#_2005_для_чайников_(Дэвис-2008)

.pdf
Скачиваний:
86
Добавлен:
24.03.2015
Размер:
15.46 Mб
Скачать

{

string s = String . Format("#{0} = { l : C } " ,

G e t A c c o u n t N u m b e r ( ) , G e t B a l a n c e ( ) ) ;

return s;

}

}

Функция Mai n () создает банковский счет и вносит на него сумму 123.454 — т.е.

}сумму с дробным количеством копеек. Затем функция Mai n () вносит на счет еще одну долю копейки и выводит баланс счета.

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

Создание

о б ъ е к т а

б а н к о в с к о г о

с ч е т а

Вклад

$123 .45

 

 

 

 

 

Счет =

#1001

=

$123 .45

 

Вклад

$0.00

 

 

 

 

 

В р е з у л ь т а т е

с ч е т

=

#1001 =

$123.46

Нажмите

<Enter>

д л я

з а в е р ш е н и я п р о г р а м м ы . . .

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

Проблема, конечно, в том, что 123.454 выводится как 123.45. Чтобы избе­ жать проблем, банк принимает решение округлять вклады и снятия до бли­ жайшей копейки. Простейший путь осуществить это — конвертировать сче­ та в decimal и использовать метод Decimal . Round ( ) , как это сделано в демонстрационной программе DecimalBankAccount .

// DecimalBankAccount - с о з д а н и е б а н к о в с к о г о

/ /

и с п о л ь з о в а н и е м п е р е м е н н о й т и п а decimal д л я

//

баланса с ч е т а

using System;

с ч е т а с х р а н е н и я

namespace DecimalBankAccount

{

public class Program

{

public static voi d Main(string[] args)

{

/ / О т к р ы т и е б а н к о в с к о г о с ч е т а

C o n s o l e . W r i t e L i n e ( " С о з д а н и е о б ъ е к т а " +

 

" б а н к о в с к о г о с ч е т а " ) ;

BankAccount ba =

n e w B a n k A c c o u n t ( ) ;

b a . I n i t B a n k A c c o u n t ( ) ;

/ / В к л а д н а с ч е т

 

double dDeposit =

1 2 3 . 4 5 4 ;

C o n s o l e . W r i t e L i n e ( " В к л а д { 0 : C } " , d D e p o s i t ) ; b a . D e p o s i t ( d D e p o s i t ) ;

/ / Б а л а н с с ч е т а

[пава 11. Классы

239

C o n s o l e . W r i t e L i n e ( " С ч ет = { o } " (

b a . G e t S t r i n g ( ) ) ;

// Добавляем очень

малую

величину

double dAdditio n =

0.002;

 

Console . WriteLine("Вклад

{ 0 : C } " ,

d A d d i t i o n ) ;

b a . D e p o s i t ( d A d d i t i o n ) ;

 

 

//Результат

Console . WriteLine(" В результате счет = { о } " , ba .GetString ()'');

// Ожидаем подтверждения пользователя Console . WriteLine("Нажмите <Enter> для " +

"завершения п р о г р а м м ы . . . " ) ;

C o n s o l e . R e a d ( ) ;

}

//BankAccount - определение класса, представляющего

//простейший банковский счет

public class

BankAccount

{

 

 

p r i v a t e

static int n N e x t A c c o u n t N u m b e r = 1000 ;

p r i v a t e

int

n A c c o u n t N u m b e r ;

// хранение

баланса в виде одной переменной типа decimal

p r i v a t e

decimal m B a l a n c e ;

// Init - инициализация банковского счета с нулевым

//балансом и использованием очередного глобального

//номера

public v o i d InitBankAccount()

{

nAccountNumber = + + n N e x t A c c o u n t N u m b e r ; mBalance = 0;

}

// G e t B a l a n c e - получение текущего баланса public double GetBalance()

return (double)mBalance;

// A c c o u n t N u m b e r

public int GetAccountNumber()

{

return n A c c o u n t N u m b e r ;

}

public void SetAccountNumber(int nAccountNumber)

this . nAccountNumber = n A c c o u n t N u m b e r ;

}

240

Часть IV. Объектно-ориентированное программирование

Глава

// Deposit - позволен любой положительный вклад public voi d Deposit(doubl e dAmount)

{

if (dAmount > 0.0)

{

//Округление к ближайшей копейке перед внесением

//вклада

decimal mTemp = (decimal)dAmount; mTemp = Decimal . Round(mTemp , 2 ) ;

mBalance += m T e m p ;

}

// Withdraw - вы можете снять со счета любую сумму, не

//превышающую баланс ; функция возвращает реально снятую

//сумму

public decimal Withdraw(decima l dWithdrawal)

{

if (mBalance <= dWithdrawal)

{

dWithdrawal = m B a l a n c e ;

}

mBalance -= dWithdrawal ; return dWithdrawal ;

// GetString

- возвращает информацию о состоянии счета в

// виде

строки

public string GetString()

string

s =

String . Format("#{0} = { l : C } " ,

 

 

G e t A c c o u n t N u m b e r ( ) ,

 

 

GetBalance() ) ;

return

s;

 

Внутреннее представление поменялось на использование значений типа decimal, который в любом случае более подходит для работы с банковским счетом, чем тип dou ­ ble. Метод Deposit () теперь применяет функцию Decimal . Round () для округле­ ния вкладываемой суммы до ближайшей копейки. Вывод программы оказывается таким, как и ожидалось:

Создание

объекта

банковского счета

Вклад $123.45

 

 

 

Счет =

#1001

=

 

$123 .45

Вклад $0.00

 

 

 

В результате счет

=

#1001 = $123 .45

Нажмите <Enter>

для

завершения программы . . .

Выводы

Вы можете сказать, что нужно было с самого начала писать программу Bank count с использованием decimal, и, пожалуй, с вами можно согласиться. Но дед этом. Могут быть разные приложения и ситуации. Главное, что класс BankAcoi оказался в состоянии решить проблему так, что не пришлось вносить никаких изма в использующую его программу (обратите внимание, что открытый интерфейс ш изменился: метод Balance () так и возвращает значение типа double).

Повторюсь еще раз: приложение, использующее класс BankAccount должно изменяться при изменении класса.

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

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

Определение свойств класса

Методы G e t X O и SetX() , продемонстрированные в программе BankAccou называются функциями доступа (access functions). Хотя их использование теоретичи является хорошей привычкой, на практике это зачастую приводит к грустным резуж там. Судите сами — чтобы увеличить на 1 член nAccountNumber, требуется пис следующий код:

SetAccountNumber(GetAccountNumber() + 1 ) ;

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

public int AccountNumbe r

// Скобки не нужны

getjretur n nAccountNumber; }

// Фигурные скобки и точка с

 

//

запятой

set{nAccountNumber = value;}

//

value - ключевое слово

Раздел get реализуется при чтении свойства, a set — при записи. В приведена далее фрагменте исходного текста свойство Balance является свойством только д чтения, так как здесь определен только раздел get:

public double Balance

{

get

{

242

Часть

IV.

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

return (double)mBalance;

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

BankAccount ba = ne w BankAccount () ;

//Записываем свойство AccountNumbe r ba. AccountNumber = 1001 ;

//Считываем оба свойства

Console. WriteLine ("#{ 0} = { l : C } " , ba .AccountNumber, ba.Balance) ;

Свойства AccountNumbe r и Balance очень похожи на открытые члены-данные как внешне, так и в использовании. Однако свойства позволяют классу защитить свои внутренние члены (так, член mBalanc e остается при этом private). Обратите внима­ ние, что Balance выполняет приведение типа— точно так же может производиться любое количество вычислений. Свойства вовсе не обязательно должны представлять со­ бой одну строку кода.

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

Balance().

Свойства совсем не обязательно неэффективны. Компилятор С# может опти­ мизировать простую функцию доступа так, что она будет генерировать не больше машинных команд, чем непосредственное обращение к члену. Это важно не только для прикладных программ, но и для самого С#. Библиотека С# широко использует свойства, и то же должны делать и вы — даже для обраще­ ния к членам-данным класса из методов этого же класса.

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

Статические члены-данные могут быть доступны через статические свойства, как по­ казано в следующем простейшем примере:

public class BankAccount

private static int nNextAccountNumbe r = 1 0 0 0 ; public static int NextAccountNumbe r

get {return nNextAccountNumber; }

Свойство NextAccountNumber доступно посредством указания имени его класса, так как оно не является свойством конкретного объекта.

// Считываем свойство NextAccountNumbe r

int nValue = BankAccount . NextAccountNumber;

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

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

Глава 11. Классы

243

p u b l ic static int AccountNumbe r

{

//

Получение значения переменной и увеличение

ее значения,

//

чтобы в следующий раз получить уже новое ее

значение

get{retur n ++nNextAccountNumber;}

}

Это свойство увеличивает статический член класса перед тем, как вернуть результат. Однако это не слишком умная идея, ведь пользователь ничего не знает о такой ocoбен ности и не подозревает, что происходит что-то помимо чтения значения. Увеличение ременной в данном случае представляет собой побочное действие.

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

Управление доступом — это только половина проблемы. Рождение объекта — один из самых важных этапов в его жизни. Класс, конечно, может предоставить метод до инициализации вновь созданного объекта, но беда в том, что приложение может попро сту забыть его вызвать. В таком случае члены-данные класса окажутся заполнен] "мусором", и корректной работы от такого объекта ждать не придется.

С# решает эту проблему путем вызова инициализирующей функции автоматичен

Например, в строке

MyObject mo = ne w M y O b j e c t O ;

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

Не путайте термины класс и объект. Dog — это класс, но собака Scooter-I это объект класса Dog.

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

public static void Main(string[] args)

{

int n ; double d;

double dCalculatedValu e = n + d;

}

244

Часть IV.

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

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

Use of unassigned local v a r i a b l e 'n'

Use of unassigned local v a r i a b l e 'd'

Однако C# предоставляет конструктор по умолчанию для объектов классов, который инициализирует члены-данные значением 0 для встроенных переменных, false — для логических и null — для ссылок. Рассмотрим следующую простую демонстрационную программу:

using System;

 

 

 

namespace

Test

 

 

 

(

 

 

 

 

public

class Program

 

 

{

 

 

 

 

public static

void M a i n (string []

args)

{

 

 

 

 

//

Сначала

создаем объект

 

 

MyObject localObject = n e w

M y O b j e c t O ;

Console . WriteLine("localObject . n = { o } " , l o c a l O b j e c t . n ) ;

if

(localObject.nextObject

==

null)

{

Console . WriteLine("localObject . nextObject = n u l l " ) ;

}

// Ожидаем подтверждения пользователя Console.WriteLine("Нажмите <Enter> для " +

"завершения программы .. . " ) ;

Console.Read();

public class MyObject

{

internal int n;

internal MyObject n e x t O b j e c t ;

Эта программа определяет класс MyObject, который содержит переменную п типа int и ссылку на объект nextO b j ect, позволяющую создавать связанные списки объек­ тов. Функция Main () создает объект класса MyO b j ect и выводит начальное содержи­ мое его членов. Вывод этой программы имеет вид

localObject .п = О

localObject .nextObject = null

Нажмите <Enter> для завершения программы ...

С# при создании объекта выполняет небольшой код по ишшиализации объекта и его чле­ нов. Если бы не этот код, члены-данные localOb j ect. п и localOb j ect. nextOb j ect содержали бы какие-то случайные значения, попросту говоря — "мусор".

[Сод, инициализирующий значения при создании, называется конструктором. Эн "конструирует" класс в смысле инициализации его членов.

Глава 11. Классы

245

С# гарантирует, что объект начинает существование в определенном состоянии: полненным нулями. Однако для многих классов (пожалуй, для подавляющего большен ства) такое нулевое состояние не является корректным. Рассмотрим класс BankА count, о котором уже шла речь ранее в этой главе,

public class BankAccount

{

int nAccountNumber ; double dBalance ;

I I . . . д р у г и е члены

}

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

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

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

public

voi d Main(string[] args)

{

 

 

BankAccount

ba = ne w B a n k A c c o u n t ( ) ;

}

 

 

public

class

BankAccount

{

 

 

//Номера банковских счетов начинаются с 1000 и

//назначаются последовательно в возрастающем порядке static int nNextAccountNumbe r = 1000 ;

//Для каждого счета поддерживаются его номер и баланс int nAccountNumber ;

double dBalance ;

//Конструктор BankAccount - обратите внимание на его имя

public BankAccount() // Требуются круглые скобки, могут

//иметься аргументы, возвращаемый

//тип отсутствует

{

nAccountNumbe r = ++nNextAccountNumber ; dBalance = 0.0;

}

I I . . . прочие члены . . .

}

246

Часть

IV.

Объектно-ориентированное

программирована

Содержимое конструктора BankAccount то же, что и у первоначального метода InitBankAccount (). Однако конструктор имеет некоторые особенности:

он всегда имеет то же имя, что и сам класс;

он не имеет возвращаемого типа, даже типа void;

функция M a i n () не должна вызывать никаких дополнительных функций для инициализации объекта при его создании.

Создание объектов

Теперь посмотрим на конструкторы в деле. Для этого рассмотрим програм­ му DemonstrateDefaultConstruetor .

// DemonstrateDef a u l t C o n s t r u c t o r - демонстрация

работы

// конструкторов по

умолчанию;

создает класс с

конструктором

// и рассматриваем

несколько сценариев

 

using System;

 

 

 

namespace DemonstrateDef a u l t C o n s t r u c t o r

 

I

 

 

 

// MyObject - создание класса

с "многословным"

// конструктором и внутренним объектом public class MyObject

I

// Этот член - данные является свойством класса

static MyOtherObject staticObj = n e w M y O t h e r O b j e c t ( ) ;

// Этот член - данные является свойством объекта MyOtherObject d y n a m i c O b j ;

// Конструктор (с обильным выводом на экран) public M y O b j e c t O

{

Console.WriteLine("Начало конструктора M y O b j e c t " ) ;

Console.WriteLine("(Статические члены - данные

"

+

 

"конструируются до вызова этого " +

 

"конструктора)11) ;

 

 

Console.WriteLine("Теперь динамически создаем

"

+

 

"нестатический ч л е н - д а н н ы е : ") ;

dynamicObj =

n e w M y O t h e r O b j e c t О ;

 

 

Console.WriteLine("Завершение конструктора M y O b j e c t " ) ;

// MyOtherObject

- у этого класса тоже многословный

 

// конструктор,

но внутренние члены - данные отсутствуют

public class MyOtherObj ect

 

 

public MyOtherObject ()

 

 

{

.

 

 

Console.WriteLine("Конструирование M y O t h e r O b j e c t " ) ;

Глава 11, Классы

247

p u b l ic class Program

{

public static void Main(string[] args)

{

Console . WriteLine("Начал о функции M a i n ( ) " ) ; Console . WriteLine("Создание локального объекта " +

 

 

 

"MyObject

в M a i n ( ) : " ) ;

 

 

MyObject

localObject = ne w

M y O b j e c t O ;

 

 

// Ожидаем подтверждения пользователя

 

 

Console . WriteLine("Нажмите

<Enter> для " +

 

 

 

"завершения п р о г р а м м ы . . . " ) ;

 

 

C o n s o l e . R e a d ( ) ;

 

 

}

 

 

 

1

}

 

 

 

 

}

 

 

 

 

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

 

Начало

функции

Main()

 

 

Создание локального объекта MyObject в M a i n O :

 

Конструирование

MyOtherObjec t

 

 

Начало

конструктора MyObject

 

 

(Статические члены - данные конструируются до вызова

 

этого

конструктора)

 

 

Теперь динамически создаем нестатический член - данные :

 

Конструирование

MyOtherObject

 

 

Завершение конструктора MyObject

 

 

Нажмите <Enter>

для завершения программы ...

Вот реконструкция происходящего при запуске программы.

1.Программа начинает работу, и функция Mai n () выводит начальное сообщение сообщение о предстоящем создании локального объекта MyObject.

2.Функция M a i n () создает объект localObject типа MyObject.

3.MyObject содержит статический член staticObj класса MyOtherObject. В

статические члены-данные создаются до первого запуска конструктора MyObject В этом случае С# присваивает переменной staticObj ссылку на вновь создан» объект перед тем, как передать управление конструктору MyObject.

4.Конструктор MyObject получает управление. Он выводит начальное сообща и напоминает, что статический член уже сконструирован до того, как начал pal ту конструктор MyO b j ect О .

5.После объявления о своих намерениях по динамическому созданию нестатн ского члена конструктор MyObject создает объект класса MyOtherObject использованием оператора new, что сопровождается выводом второго сообща о создании MyOtherObjec t на экран.

6.Управление возвращается конструктору MyObject, который, в свою очередь возвращает управление функции Mai n ().

7.Программа выполнена.

248

Часть

IV.

Объектно-ориентированное

программировав