Наследование и полиморфизм - альтернатива обратному вызову
Сегодня многие программные системы проектируются и разрабатываются не в функциональном, а в объектно-ориентированном стиле. Такая система представляет собой одно или несколько семейств интерфейсов и классов, связанных отношением наследования. Классы-потомки наследуют методы своих родителей, могут их переопределять и добавлять новые методы. Переопределив метод родителя, потомки без труда могут вызывать как собственный метод, так и метод родителя; все незакрытые методы родителя им известны и доступны. Но может ли родитель вызывать методы, определенные потомком, учитывая, что в момент создания родительского метода потомок не только не создан, но еще, скоре всего, и не спроектирован? Тем не менее, ответ на этот вопрос положителен. Достигается такая возможность опять-таки благодаря контрактам, заключаемым при реализации полиморфизма.
О полиморфизме говорилось достаточно много в предыдущих лекциях. Тем не менее, позволю напомнить суть дела. Родитель может объявить свой метод виртуальным, в этом случае в контракте на метод потомку разрешается переопределить реализацию, но он не имеет права изменять сигнатуру виртуального метода. Когда некоторый метод родителя Q вызывает виртуальный метод F, то, благодаря позднему связыванию, реализуется полиморфизм и реально будет вызван не метод родителя F, а метод F, который реализован потомком, вызвавшим родительский метод Q. Ситуация в точности напоминает раскрутку и вызов обратных функций. Родительский метод Q находится во внутреннем слое, а потомок с его методом F определен во внешнем слое. Когда потомок вызывает метод Q из внутреннего слоя, тот, в свою очередь, вызывает метод F из внешнего слоя. Сигнатура вызываемого метода F в данном случае задается не делегатом, а сигнатурой виртуального метода, которую, согласно контракту, потомок не может изменить. Давайте вернемся к задаче вычисления интеграла и создадим реализацию, основанную на наследовании и полиморфизме.
Идея примера такова. Вначале построим родительский класс, метод которого будет вычислять интеграл от некоторой подынтегральной функции, заданной виртуальным методом класса. Далее построим класс-потомок, наследующий родительский метод вычисления интеграла и переопределяющий виртуальный метод, в котором потомок задаст собственную подынтегральную функцию. При такой технологии, всякий раз, когда нужно вычислить интеграл, нужно создать класс-потомок, в котором переопределяется виртуальный метод. Приведу пример кода, следующего этой схеме:
class FIntegral{ //базовый класс, в котором определен метод вычисления //интеграла и виртуальный метод, задющий базовую //подынтегральную функцию public double EvaluateIntegral(double a, double b, double eps) { int n=4; double I0=0, I1 = I( a, b, n); for( n=8; n < Math.Pow(2.0,15.0); n*=2) { I0 =I1; I1=I(a,b,n); if(Math.Abs(I1-I0)<eps)break; } if(Math.Abs(I1-I0)< eps) 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) { //Вычисляет частную сумму по методу трапеций 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); } protected virtual double sif(double x) {return(1.0);} }//FIntegral
Этот код большей частью знаком. В отличие от класса HighOrderIntegral, здесь нет делегата, у функции EvaluateIntegral нет параметра функционального типа. Вместо этого тут же в классе определен защищенный виртуальный метод, задающий конкретную подынтегральную функцию. В качестве таковой выбрана самая простая функция, тождественно равная единице.
Для вычисления интеграла от реальной функции единственное, что теперь нужно сделать - это задать класс-потомок, переопределяющий виртуальный метод. Вот пример такого класса:
Принципиально задача решена. Осталось только написать фрагмент кода, запускающий вычисления. Он оформлен в виде следующей процедуры:
public void TestPolymorphIntegral(){ FIntegral integral1 = new FIntegral(); FIntegralSon integral2 = new FIntegralSon(); double res1 = integral1.EvaluateIntegral(2.0,3.0,0.1e-5); double res2 = integral2.EvaluateIntegral(2.0,3.0,0.1e-5); Console.WriteLine("Father = {0}, Son = {1}", res1,res2);}//PolymorphIntegralВзгляните на результаты вычислений.
Рис. 20.4. Вычисление интеграла, использующее полиморфизм
20. Лекция: Функциональный тип в C#. Делегаты
20.4
Делегаты как свойства
В наших примерах рассматривалась ситуация, при которой в некотором классе объявлялись функции, удовлетворяющие контракту с делегатом, но создание экземпляров делегата и их инициирование функциями класса выполнялось в другом месте, там, где предполагалось вызывать соответствующие функции. Чаще всего, создание экземпляров удобнее возложить на класс, создающий требуемые функции. Более того, в этом классе делегат можно объявить как свойство класса, что позволяет "убить двух зайцев". Во-первых, с пользователей класса снимается забота создания делегатов, что требует некоторой квалификации, которой у пользователя может и не быть. Во-вторых, делегаты создаются динамически, в тот момент, когда они требуются. Это важно как при работе с функциями высших порядков, когда реализаций, например, подынтегральных функций, достаточно много, так и при работе с событиями класса, в основе которых лежат делегаты.
Рассмотрим пример, демонстрирующий и поясняющий эту возможность при работе с функциями высших порядков. Идея примера такова. Спроектируем два класса:
- класс объектов Person с полями: имя, идентификационный номер, зарплата. В этом классе определим различные реализации функции Compare, позволяющие сравнивать два объекта по имени, по номеру, по зарплате, по нескольким полям. Самое интересное, ради чего и строится данный пример: для каждой реализации Compare будет построена процедура-свойство, которая задает реализацию делегата, определенного в классе Persons;
- класс Persons будет играть роль контейнера объектов Person.
В этом классе будут определены операции над объектами. Среди операций нас, прежде всего, будет интересовать сортировка объектов, реализованная в виде функции высших порядков. Функциональный параметр будет задавать класс функций сравнения объектов, реализации которых находятся в классе Person. Делегат, определяющий класс функций сравнения, будет задан в классе Persons.
Теперь, когда задача ясна, приступим к ее реализации. Класс Person уже появлялся в наших примерах, поэтому он просто дополнен до нужной функциональности. Добавим методы сравнения двух объектов Person:
//методы сравненияprivate static int CompareName(Person obj1, Person obj2){ return(string.Compare(obj1.name,obj2.name));}private static int CompareId(Person obj1, Person obj2){ if( obj1.id > obj2.id) return(1); else return(-1);}private static int CompareSalary(Person obj1, Person obj2){ if( obj1.salary > obj2.salary) return(1); else if(obj1.salary < obj2.salary)return(-1); else return(0);}private static int CompareSalaryName(Person obj1, Person obj2){ if( obj1.salary > obj2.salary) return(1); else if(obj1.salary < obj2.salary)return(-1); else return(string.Compare(obj1.name,obj2.name));}Заметьте, методы закрыты и, следовательно, недоступны извне. Их четыре, но могло бы быть и больше, при возрастании сложности объекта растет число таких методов. Все методы имеют одну и ту же сигнатуру и удовлетворяют контракту, заданному делегатом, который будет описан чуть позже. Для каждого метода необходимо построить экземпляр делегата, который будет задавать ссылку на метод. Поскольку не все экземпляры нужны одновременно, то хотелось бы строить их динамически, в тот момент, когда они понадобятся. Это можно сделать, причем непосредственно в классе Person. Закрытые методы будем рассматривать как закрытые свойства и для каждого из них введем статическую процедуру-свойство, возвращающую в качестве результата экземпляр делегата со ссылкой на метод. Проще написать, чем объяснить на словах:
//делегаты как свойстваpublic static Persons.CompareItems SortByName{ get {return(new Persons.CompareItems(CompareName));}}public static Persons.CompareItems SortById{ get {return(new Persons.CompareItems(CompareId));}}public static Persons.CompareItems SortBySalary{ get {return(new Persons.CompareItems(CompareSalary));}}public static Persons.CompareItems SortBySalaryName{ get {return(new Persons.CompareItems(CompareSalaryName));}}Всякий раз, когда будет запрошено, например, свойство SortByName класса Person, будет возвращен объект функционального класса Persons.CompareItems, задающий ссылку на метод CompareName класса Person. Объект будет создаваться динамически в момент запроса.
Класс Person полностью определен, и теперь давайте перейдем к определению контейнера, содержащего объекты Person. Начну с определения свойств класса Persons:
class Persons{ //контейнер объектов Person //делегат public delegate int CompareItems(Person obj1, Person obj2); private int freeItem = 0; const int n = 100; private Person[]persons = new Person[n];}В классе определен функциональный класс - делегат CompareItems, задающий контракт, которому должны удовлетворять функции сравнения элементов.
Контейнер объектов реализован простейшим образом в виде массива объектов. Переменная freeItem - указатель на первый свободный элемент массива. Сам массив является закрытым свойством, и доступ к нему осуществляется благодаря индексатору:
//индексаторpublic Person this[int num]{ get { return(persons[num-1]); } set { persons[num-1] = value; }}Добавим классический для контейнеров набор методов - добавление нового элемента, загрузка элементов из базы данных и печать элементов:
public void AddPerson(Person pers){ if(freeItem < n) { Person p = new Person(pers); persons[freeItem++]= p; } else Console.WriteLine("Не могу добавить Person");}public void LoadPersons(){ //реально загрузка должна идти из базы данных AddPerson(new Person("Соколов",123, 750.0)); AddPerson(new Person("Синицын",128, 850.0)); AddPerson(new Person("Воробьев",223, 750.0)); AddPerson(new Person("Орлов",129, 800.0)); AddPerson(new Person("Соколов",133, 1750.0)); AddPerson(new Person("Орлов",119, 750.0));}//LoadPersonspublic void PrintPersons(){ for(int i =0; i<freeItem; i++) { Console.WriteLine("{0,10} {1,5} {2,5}", persons[i].Name, persons[i].Id, persons[i].Salary); }}//PrintPersonsКонечно, метод LoadPerson в реальной жизни устроен по-другому, но в нашем примере он свою задачу выполняет. А теперь определим метод сортировки записей с функциональным параметром, задающим тот или иной способ сравнения элементов:
//сортировкаpublic void SimpleSortPerson(CompareItems compare){ Person temp = new Person(); for(int i = 1; i<freeItem;i++) for(int j = freeItem -1; j>=i; j--) if (compare(persons[j],persons[j-1])==-1) { temp = persons[j-1]; persons[j-1]=persons[j]; persons[j] = temp; }}//SimpleSortObject}//PersonsЕдинственный аргумент метода SimpleSortPerson принадлежит классу CompareItems, заданному делегатом. Что касается метода сортировки, то реализован простейший алгоритм пузырьковой сортировки, со своей задачей он справляется. На этом проектирование классов закончено, нужная цель достигнута, показано, как можно в классе экземпляры делегатов задавать как свойства. Для завершения обсуждения следует продемонстрировать, как этим нужно пользоваться. Зададим, как обычно, тестирующую процедуру, в которой будут использоваться различные критерии сортировки:
public void TestSortPersons(){ Persons persons = new Persons(); persons.LoadPersons(); Console.WriteLine (" Сортировка по имени: "); persons.SimpleSortPerson(Person.SortByName); persons.PrintPersons(); Console.WriteLine (" Сортировка по идентификатору: "); persons.SimpleSortPerson(Person.SortById); persons.PrintPersons(); Console.WriteLine (" Сортировка по зарплате: "); persons.SimpleSortPerson(Person.SortBySalary); persons.PrintPersons(); Console.WriteLine (" Сортировка по зарплате и имени: "); persons.SimpleSortPerson(Person.SortBySalaryName); persons.PrintPersons();}//SortPersonsРезультаты работы сортировки данных изображены на рис. 20.5.
Рис. 20.5. Сортировка данных
20. Лекция: Функциональный тип в C#. Делегаты
20.5