ооп теория
.pdf
Первым делом объявлены три функциональных класса - три делегата: Proc, MesToPers, Fun1. Каждый из них описывает множество функций фиксированной сигнатуры.
В классе OwnDel описаны четыре метода: Plus, Minus, Plus1, Minus1,
сигнатуры которых соответствуют сигнатурам, задаваемых классами
Proc и Fun1.
Поля p1 и f1 класса OwnDel являются экземплярами классов Proc и Fun1.
В конструкторе класса поля p1 и f1 связываются с конкретными методами Plus или Minus, Plus1 или Minus1. Связывание с той или иной функцией в данном случае определяется значением поля sign.
Заметьте, экземпляры делегатов можно рассматривать как ссылки
(указатели на функции), а методы тех или иных классов с соответствующей сигнатурой можно рассматривать как объекты, хранимые в динамической памяти. В определенный момент происходит связывание ссылки и объекта (в
этой роли выступают не обычные объекты, имеющие поля, а методы,
задающие код). Взгляд на делегата как на указатель функции характерен для программистов, привыкших к С++.
Приведу теперь процедуру, тестирующую работу созданного класса:
public void TestOwnDel()
{
int account = 1000, account1=0; OwnDel oda = new OwnDel('+');
Console.WriteLine("account = {0}, account1 = {1}", account, account1);
oda.p1(ref account); account1=oda.f1(account); Console.WriteLine("account = {0}, account1 = {1}",
account, account1);
}
Клиент класса OwnDel создает экземпляр класса, передавая конструктору знак той операции, которую он хотел бы выполнить над своими счетами - account и account1. Вызов p1 и f1, связанных к моменту
381
вызова с закрытыми методами класса, приводит к выполнению нужных функций.
В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в самом классе. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся.
Рассмотрим многократно встречавшийся класс Person, слегка изменив его определение:
class Person
{
//конструкторы
public Person(){name =""; id=0; salary=0.0;} public Person(string name){this.name = name;} public Person (string name, int id, double salary)
{this.name = name; this.id=id; this.salary = salary;} public Person (Person pers)
{this.name = pers.name; this.id = pers.id; this.salary = pers.salary;}
//методы
public void ToPerson(string mes)
{
this.message = mes;
Console.WriteLine("{0}, {1}",name, message);
}
//свойства
private string name; private int id; private double salary;
private string message; //доступ к свойствам
public string |
Name |
|
{get {return(name);} |
set {name = value;}} |
|
public double |
Salary |
|
{get {return(salary);} set {salary = value;}} |
||
public int Id |
|
|
{get {return(id);} |
set {id = value;}} |
|
}//class Person
Класс Person устроен обычным способом: у него несколько перегруженных конструкторов, закрытые поля и процедуры-свойства для доступа к ним. Особо обратить внимание прошу на метод класса ToPerson,
сигнатура которого совпадает с сигнатурой класса, определенной введенным
382
ранее делегатом MesToPers. Посмотрите, как клиент класса может связать
этот метод с экземпляром делегата, определенного самим клиентом:
Person man1 = new Person("Владимир");
MesToPers mestopers = new MesToPers(man1.ToPerson); mestopers("пора работать!");
Обратите внимание, что поскольку метод ToPerson не является статическим методом, то при связывании необходимо передать и объект,
вызывающий метод. Более того, переданный объект становится доступным экземпляру делегата. Отсюда сразу же становится ясным, что экземпляры делегата - это не просто указатели на функцию, а более сложно организованные структуры. Они, по крайней мере, содержат пару указателей на метод и на объект, вызвавший метод. Вызываемый метод в своей работе использует как информацию, передаваемую ему через аргументы метода, так и информацию, хранящуюся в полях объекта. В данном примере переданное сообщение "пора работать" присоединится к имени объекта, и
результирующая строка будет выдана на печать. В тех случаях, когда метод,
связываемый с экземпляром делегата, не использует информацию объекта,
этот метод может быть объявлен как статический метод класса. Таким образом, инициализировать экземпляры делегата можно как статическими,
так и динамическими методами, связанными с конкретными объектами.
Последние три строки были добавлены в вышеприведенную тестирующую процедуру. Взгляните на результаты ее работы.
Рис. 20.1. Объявление делегатов и создание их экземпляров ФУНКЦИИ ВЫСШИХ ПОРЯДКОВ
383
Одно из наиболее важных применений делегатов связано с функциями высших порядков. Функцией высшего порядка называется такая функция
(метод) класса, у которой один или несколько аргументов принадлежат к функциональному типу. Без этих функций в программировании обойтись довольно трудно. Классическим примером является функция вычисления интеграла, у которой один из аргументов задает подынтегральную функцию.
Другим примером может служить функция, сортирующая объекты.
Аргументом ее является функция Compare, сравнивающая два объекта. В
зависимости от того, какая функция сравнения будет передана на вход функции сортировки, объекты будут сортироваться по-разному, например, по имени, или по ключу, или по нескольким полям. Вариантов может быть много, и они определяются классом, описывающим сортируемые объекты.
ВЫЧИСЛЕНИЕ ИНТЕГРАЛА
Давайте более подробно рассмотрим ситуацию с функциями высшего порядка на примере задачи вычисления определенного интеграла с заданной точностью. С этой целью создадим класс, в котором будет описан делегат,
определяющий контракт, коему должны удовлетворять подынтегральные функции. В этом же классе определим метод, вычисляющий интеграл. По сути самой задачи этот метод представляет собой функцию высшего порядка.
Приведу программный код, описывающий класс:
public class HighOrderIntegral
{
//delegate
public delegate double SubIntegralFun(double x); public double EvalIntegral(double a, double b,
double eps,SubIntegralFun sif)
{
int n=4;
double I0=0, I1 = I( a, b, n,sif);
for( n=8; n < Math.Pow(2.0,15.0); n*=2)
{
I0 =I1; I1=I(a,b,n,sif); if(Math.Abs(I1-I0)<eps) break;
}
if(Math.Abs(I1-I0)< eps)
384
Console.WriteLine("Требуемая точность достигнута! "+ " eps = {0}, достигнутая точность ={1}, n=
{2}",
eps,Math.Abs(I1-I0),n);
else
Console.WriteLine("Требуемая точность не достигнута! "+ " eps = {0}, достигнутая точность ={1}, n=
{2}",
eps,Math.Abs(I1-I0),n);
return(I1);
}
private double I(double a, double b, int n, SubIntegralFun sif)
{
//Вычисляет частную сумму по методу трапеций double x = a, sum = sif(x)/2, dx = (b-a)/n; for (int i= 2; i <= n; i++)
{
x += dx; |
sum += sif(x); |
}
x = b; sum += sif(x)/2; return(sum*dx);
}
}//class HighOrderIntegral
Прокомментирую этот текст:
Класс HighOrderIntegral предназначен для работы с функциями. В него вложено описание функционального класса - делегата SubIntegralFun,
задающего класс функций с одним аргументом типа double и
возвращающих значение этого же типа.
Метод EvalIntegral - основной метод класса позволяет вычислять определенный интеграл. Этот метод есть функция высшего порядка,
поскольку одним из его аргументов является подынтегральная функция, принадлежащая классу SubIntegralFun.
Для вычисления интеграла применяется классическая схема. Интервал интегрирования разбивается на n частей, и вычисляется частичная сумма по методу трапеций, представляющая приближенное значение интеграла. Затем n удваивается, и вычисляется новая сумма. Если разность двух приближений по модулю меньше заданной точности eps,
то вычисление интеграла заканчивается, иначе процесс повторяется в цикле. Цикл завершается либо по достижении заданной точности, либо
385
когда n достигнет некоторого предельного значения (в нашем случае - 215).
Вычисление частичной суммы интеграла по методу трапеций реализовано закрытой процедурой I.
Впоследствии класс может быть расширен, и помимо вычисления интеграла он может вычислять и другие характеристики функций.
Чтобы продемонстрировать работу с классом HighOrderIntegral, приведу еще класс Functions, где описано несколько функций, удовлетворяющих контракту,
который задан классом SubIntegralFun:
class Functions
{
//подынтегральные функции
static public double sif1(double x)
{
int k = 1; int b = 2; return (double)(k*x +b);
}
static public double sif2(double x)
{
double a = 1.0; double b = 2.0; double c= 3.0; return (double)(a*x*x +b*x +c);
}
}//class Functions
А теперь рассмотрим метод класса клиента, выполняющий создание нужных
объектов и тестирующий их работу:
public void TestEvalIntegrals()
{
double myint1=0.0; HighOrderIntegral.SubIntegralFun hoisif1 =
new HighOrderIntegral.SubIntegralFun(Functions.sif1); HighOrderIntegral hoi = new HighOrderIntegral();
myint1 = hoi.EvalIntegral(2,3,0.1e-5,hoisif1); Console.WriteLine("myintegral1 = {0}",myint1); HighOrderIntegral.SubIntegralFun hoisif2 =
new HighOrderIntegral.SubIntegralFun(Functions.sif2); myint1= hoi.EvalIntegral(2,3,0.1e-5,hoisif2); Console.WriteLine("myintegral2 = {0}",myint1);
}//EvalIntegrals
386
Здесь создаются два экземпляра делегата и объект класса HighOrderIntegral,
вызывающий метод вычисления интеграла. Результаты работы показаны на
20.2.
Рис. 20.2. Вычисление интеграла с использованием функций высших порядков
ПОСТРОЕНИЕ ПРОГРАММНЫХ СИСТЕМ МЕТОДОМ "РАСКРУТКИ".
ФУНКЦИИ ОБРАТНОГО ВЫЗОВА
Метод "раскрутки" является одним из основных методов функционально-
ориентированного построения сложных программных систем. Суть его состоит в том, что программная система создается слоями. Вначале пишется ядро системы - нулевой слой, реализующий базовый набор функций. Затем пишется первый слой с новыми функциями, которые интенсивно вызывают в процессе своей работы функции ядра. Теперь система обладает большим набором функций. Каждый новый слой расширяет функциональность системы. Процесс продолжается, пока не будет достигнута заданная функциональность. На рис.20.3, изображающем схему построения системы
методом раскрутки, стрелками показано, как функции внешних слоев вызывают функции внутренних слоев.
387
Рис. 20.3. Построение системы методом "раскрутки"
Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время они были созданы методом раскрутки. Это позволило написать на 95% на языке С транслятор с языка С и операционную систему. Благодаря этому, обеспечивался легкий перенос транслятора и операционной системы на компьютеры с разной системой команд. Замечу, что в те времена мир компьютеров отличался куда большим разнообразием, чем в нынешнее время. Для переноса системы на новый тип компьютера достаточно было написать ядро системы в соответствии с машинным кодом данного компьютера, далее работала раскрутка.
При построении систем методом раскрутки возникает одна проблема.
Понятно, что функциям внешнего слоя известно все о внутренних слоях и они без труда могут вызывать функции внутренних слоев. Но как быть, если функциям внутреннего слоя необходимо вызывать функции внешних, еще не написанных и, возможно, еще не спроектированных слоев? Возможна ли симметрия вызовов? На первый взгляд, это кажется невозможным. Но программисты придумали, по крайней мере, два способа решения этой проблемы. Оба они используют контракты. Один основан на функциях
388
обратного вызова, другой - на наследовании и полиморфизме. Мы разберем оба способа, но начнем с функций обратного вызова.
Пусть F - функция высшего порядка с параметром G функционального типа.
Тогда функцию G, задающую параметр (а иногда и саму функцию F),
называют функцией обратного вызова (callback функцией). Термин вполне понятен. Если в некотором внешнем слое функция Q вызывает функцию внутреннего слоя F, то предварительно во внешнем слое следует позаботиться о создании функции G, которая и будет передана F. Таким образом, функция Q внешнего слоя вызывает функцию F внутреннего слоя,
которая, в свою очередь (обратный вызов) вызывает функцию G внешнего слоя. Чтобы эта техника работала, должен быть задан контракт. Функция высших порядков, написанная во внутреннем слое, задает следующий контракт: "всякая функция, которая собирается меня вызвать, должна передать мне функцию обратного вызова, принадлежащую определенному мной функциональному классу, следовательно, иметь известную мне сигнатуру".
Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс
HighOrderIntegral - это внутренний слой нашей системы. В нем задан делегат,
определяющий контракт, и функция EvalIntegral, требующая задания функции обратного вызова в качестве ее параметра. Функция EvalIntegral
вызывается из внешнего слоя, где и определяются callback функции из класса
Functions.
Многие из функций операционной системы Windows, входящие в состав Win API 32, требуют при своем вызове задания callback-функций. Примером может служить работа с объектом операционной системы Timer.
Конструктор этого объекта является функцией высшего порядка, и ей в
389
момент создания объекта необходимо в качестве параметра передать callback-функцию, вызываемую для обработки событий, которые поступают от таймера.
Пример работы с таймером приводить сейчас не буду, ограничусь лишь сообщением синтаксиса объявления конструктора объекта Timer:
public Timer(TimerCallback callback,object state, int dueTime, int period);
Первым параметром конструктора является функция обратного вызова callback,
которая принадлежит функциональному классу TimerCallback, заданному
делегатом:
public delegate void TimerCallback(object state);
НАСЛЕДОВАНИЕ И ПОЛИМОРФИЗМ - АЛЬТЕРНАТИВА ОБРАТНОМУ ВЫЗОВУ
Сегодня многие программные системы проектируются и разрабатываются не в функциональном, а в объектно-ориентированном стиле. Такая система представляет собой одно или несколько семейств интерфейсов и классов,
связанных отношением наследования. Классы-потомки наследуют методы своих родителей, могут их переопределять и добавлять новые методы.
Переопределив метод родителя, потомки без труда могут вызывать как собственный метод, так и метод родителя; все незакрытые методы родителя им известны и доступны. Но может ли родитель вызывать методы,
определенные потомком, учитывая, что в момент создания родительского метода потомок не только не создан, но еще, скорее всего, и не спроектирован? Тем не менее, ответ на этот вопрос положителен.
Достигается такая возможность опять-таки благодаря контрактам,
заключаемым при реализации полиморфизма.
390
