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

ооп теория

.pdf
Скачиваний:
20
Добавлен:
14.02.2015
Размер:
3.58 Mб
Скачать

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

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

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

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

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

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

Каково следствие семантики вызова по значению? Если вы забыли указать ключевое слово ref или out для аргумента, фактически являющегося выходным, то к нему будет применяться вызов по значению. Даже если в теле метода происходит изменение значения этого аргумента, то оно действует только на время выполнения тела метода. Как только метод заканчивает свою работу (завершается блок), все локальные переменные (в

151

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

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

Говоря о семантике вызова по ссылке и по значению, следует сделать одно важное уточнение. В объектном программировании, каковым является и программирование на C#, основную роль играют ссылочные типы - мы работаем с классами и объектами. Когда методу передается объект ссылочного типа, то все поля этого объекта могут меняться в методе самым беззастенчивым образом. И это несмотря на то, что объект формально не является выходным, не имеет ключевых слов ref или out, использует семантику вызова по значению. Сама ссылка на объект, как и положено,

остается неизменной, но состояние объекта, его поля могут полностью обновиться. Такая ситуация типична и представляет один из основных способов изменения состояния объектов. Именно поэтому ref или out не часто появляются при описании аргументов метода.

ЧТО НУЖНО ЗНАТЬ О МЕТОДАХ?

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

152

Почему у методов мало аргументов?

Методы класса имеют значительно меньше аргументов, чем процедуры и функции в классическом процедурном стиле программирования, когда не используется концепция классов. За счет чего происходит уменьшение числа аргументов у методов? Ведь аргументы играют важную роль: они передают методу информацию, нужную ему для работы, и возвращают информацию -

результаты работы метода - программе, вызвавшей его.

Все дело в том, что методы класса - это не просто процедуры; это процедуры,

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

экземплярами класса. Из полей соответствующего объекта - цели вызова -

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

Поля класса или функции без аргументов?

Поля хранят информацию о состоянии объектов класса. Состояние объекта динамически изменяется в ходе вычислений - обновляются значения полей.

Часто возникающая дилемма при проектировании класса: что лучше -

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

153

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

Если бы синтаксис описания метода допускал отсутствие скобок у функции

(метода), в случае, когда список аргументов отсутствует, то клиент класса мог бы и не знать, обращается ли он к полю или к методу. Такой синтаксис принят, например, в языке Eiffel. Преимущество этого подхода в том, что изменение реализации никак не сказывается на клиентах класса. В языке C#

это не так. Когда мы хотим получить длину строки, то пишем s.Length, точно зная, что Length - это поле, а не метод класса String. Если бы по каким-либо причинам разработчики класса String решили изменить реализацию и заменить поле Length соответствующей функцией, то ее вызов имел бы вид s.Length().

Пример: две версии класса Account

Проиллюстрируем рассмотренные выше вопросы на примере проектирования классов Account и Account1, описывающих такую абстракцию данных, как банковский счет. Определим на этих данных две основные операции - занесение денег на счет и снятие денег. В первом варианте - классе Account - будем активно использовать поля класса. Помимо двух основных полей credit и debit, хранящих приход и расход счета, введем поле balance, которое задает текущее состояние счета, и два поля, связанных с последней выполняемой операцией. Поле sum будет хранить сумму денег текущей операции, а поле result - результат выполнения операции. Полей у класса много, и как следствие, у методов класса аргументов будет немного.

Вот описание нашего класса:

/// <summary>

154

///Класс Account определяет банковский счет. Простейший

///вариант с возможностью трех операций: положить деньги

///на счет, снять со счета, узнать баланс.Вариант с полями

///</summary>

public class Account

{

//закрытые поля класса

int debit=0, credit=0, balance =0; int sum =0, result=0;

///<summary>

///Зачисление на счет с проверкой

///</summary>

///<param name="sum">зачисляемая сумма</param> public void putMoney(int sum)

{

this.sum = sum; if (sum >0)

{

credit += sum; balance = credit - debit; result =1;

}

else result = -1; Mes();

}//putMoney

///<summary>

///Снятие со счета с проверкой

///</summary>

///<param name="sum"> снимаемая сумма</param> public void getMoney(int sum)

{

this.sum = sum; if(sum <= balance)

{

debit += sum; balance = credit - debit; result =2;

}

else result = -2; Mes();

}//getMoney

///<summary>

///Уведомление о выполнении операции

///</summary>

void Mes()

{

switch (result)

{

case 1:

Console.WriteLine("Операция зачисления денег прошла успешно!");

Console.WriteLine("Cумма={0},

Ваш текущий баланс={1}",sum, balance);

break;

case 2:

Console.WriteLine("Операция снятия денег прошла успешно!");

Console.WriteLine("Cумма={0},

Ваш текущий баланс={1}", sum,balance);

break; case -1:

Console.WriteLine("Операция зачисления денег не выполнена!");

Console.WriteLine("Сумма должна быть больше

нуля!");

155

Console.WriteLine("Cумма={0},

Ваш текущий баланс={1}", sum,balance);

break; case -2:

Console.WriteLine("Операция снятия денег не выполнена!");

Console.WriteLine("Сумма должна быть не больше баланса!");

Console.WriteLine("Cумма={0},

Ваш текущий баланс={1}", sum,balance);

break;

default:

Console.WriteLine("Неизвестная операция!"); break;

}

}

}//Account

Как можно видеть, только у методов getMoney и putMoney имеется один входной аргумент. Это тот аргумент, который нужен по сути дела, поскольку только клиент может решить, какую сумму он хочет снять или положить на счет. Других аргументов у методов класса нет - вся информация передается через поля класса. Уменьшение числа аргументов приводит к повышению эффективности работы с методами, так как исчезают затраты на передачу фактических аргументов. Но за все надо платить. В данном случае,

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

как прошла операция, и информируя клиента о текущем состоянии его баланса.

А теперь спроектируем аналогичный класс Account1, отличающийся только тем, что у него будет меньше полей. Вместо поля balance в классе появится соответствующая функция с этим же именем, вместо полей sum и result

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

///<summary>

///Класс Account1 определяет банковский счет.

///Вариант с аргументами и функциями

///</summary>

156

public class Account1

{

//закрытые поля класса int debit=0, credit=0;

///<summary>

///Зачисление на счет с проверкой

///</summary>

///<param name="sum">зачисляемая сумма</param> public void putMoney(int sum)

{

int res =1;

if (sum >0)credit += sum; else res = -1; Mes(res,sum);

}//putMoney

///<summary>

///Снятие со счета с проверкой

///</summary>

///<param name="sum"> снимаемая сумма</param> public void getMoney(int sum)

{

int res=2;

if(sum <= balance())debit += sum; else res = -2;

balance(); Mes(res, sum);

}//getMoney

///<summary>

///вычисление баланса

///</summary>

///<returns>текущий баланс</returns>

int balance()

{

return(credit - debit);

}

///<summary>

///Уведомление о выполнении операции

///</summary>

void Mes(int result, int sum)

 

 

{

 

 

switch (result)

 

 

{

 

 

case 1:

 

 

Console.WriteLine("Операция

зачисления денег

прошла успешно!");

 

 

Console.WriteLine("Cумма={0},

 

Ваш

текущий

баланс={1}",

sum,balance());

 

 

break;

 

 

case 2:

 

 

Console.WriteLine("Операция снятия денег

прошла успешно!");

 

Console.WriteLine("Cумма={0},

 

Ваш

текущий

баланс={1}",

sum,balance());

 

 

break; case -1:

Console.WriteLine("Операция зачисления денег не выполнена!");

Console.WriteLine("Сумма должна быть больше

нуля!");

157

Console.WriteLine("Cумма={0},

 

Ваш

текущий

баланс={1}",

sum,balance());

break; case -2:

Console.WriteLine("Операция снятия денег не

выполнена!");

Console.WriteLine("Сумма должна быть

не больше баланса!");

 

Console.WriteLine("Cумма={0},

 

Ваш

текущий

баланс={1}",

sum,balance());

break;

default:

Console.WriteLine("Неизвестная операция!"); break;

}

}

}//Account1

Сравнивая этот класс с классом Account, можно видеть, что число полей сократилось с пяти до двух, упростились основные методы getMoney и putMoney. Но, в качестве платы, у класса появился дополнительный метод balance(), многократно вызываемый, и у метода Mes теперь появились два аргумента. Какой класс лучше? Однозначно сказать нельзя, все зависит от контекста, от приоритетов, заданных при создании конкретной системы.

Приведу процедуру класса Testing, тестирующую работу с классами

Account и Account1:

public void TestAccounts()

{

Account myAccount = new Account(); myAccount.putMoney(6000); myAccount.getMoney(2500); myAccount.putMoney(1000); myAccount.getMoney(4000); myAccount.getMoney(1000);

//Аналогичная работа с классом Account1

Console.WriteLine("Новый класс и новый счет!");

Account1 myAccount1 = new Account1(); myAccount1.putMoney(6000); myAccount1.getMoney(2500); myAccount1.putMoney(1000); myAccount1.getMoney(4000); myAccount1.getMoney(1000);

}

На рис. 9.1 показаны результаты работы этой процедуры.

158

Рис. 9.1. Тестирование классов Account и Account1

Функции с побочным эффектом

Функция называется функцией с побочным эффектом, если помимо результата, вычисляемого функцией и возвращаемого ей в операторе return,

она имеет выходные аргументы с ключевыми словами ref и out. В языках

C/C++ функции с побочным эффектом применяются сплошь и рядом.

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

функция с побочным эффектом, то a+f(a) может быть не равно f(a) +a, так что теряется коммутативность операции сложения.

Примером такой функции является функция f, приведенная выше. Вот тест,

демонстрирующий потерю коммутативности сложения при работе с этой функцией:

///<summary>

///тестирование побочного эффекта

///</summary>

public void TestSideEffect()

{

159

int a = 0, b=0, c=0;

a =1; b = a + f(ref a); a =1; c = f(ref a)+ a;

Console.WriteLine("a={0}, b={1}, c={2}",a,b,c);

}

На рис. 9.2 показаны результаты работы этого метода.

Рис. 9.2. Демонстрация вызова функции с побочным эффектом

Обратите внимание на полезность указания ключевого слова ref в момент вызова. Его появление хоть как-то оправдывает некоммутативность сложения.

МЕТОДЫ. ПЕРЕГРУЗКА

Должно ли быть уникальным имя метода в классе? Нет, этого не требуется.

Более того, проектирование методов с одним и тем же именем является частью стиля программирования на С++ и стиля C#. Существование в классе методов с одним и тем же именем называется перегрузкой, а сами одноименные методы называются перегруженными.

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

например Square, и всегда, когда нужно вычислить площадь, не задумываясь,

вызывать метод Square, передавая ему известные в данный момент аргументы.

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

160