Проект к данной лекции Вы можете скачать здесь.
Слово делегат ( delegate ) используется в C# для обозначения хорошо известного понятия, давно употребляемого в языках программирования. Делегат C# задает определение функционального типа (класса) данных. Экземплярами такого класса являются функции. Делегаты языка C# наряду со структурами, перечислениями и интерфейсами представляют задание еще одного частного случая классов. Каждый делегат описывает множество функций с заданной сигнатурой. Каждая функция, или если быть более точным и придерживаться терминологии, принятой в объектно-ориентированном программировании, каждый метод, сигнатура которого совпадает с сигнатурой делегата, может рассматриваться как экземпляр класса, заданного делегатом.
[<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>);
Этим объявлением класса задается функциональный тип - множество функций с заданной сигнатурой, у которых аргументы определяются списком, заданным в объявлении делегата, и тип возвращаемого значения определяется типом результата делегата.
Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности:
Так же, как и интерфейсы C#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата. Классы, согласные с контрактом, могут объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Далее они могут создать экземпляр делегата, присвоив ему в качестве значения функцию, удовлетворяющую контракт. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра - object, а у функции, связываемой с экземпляром, соответствующий параметр имеет тип int, хотя и согласованный с типом object, но не совпадающий с ним.
Созданный
Содержательный пример работы с делегатами приводился в проекте первой лекции. Читателю есть смысл вернуться к его рассмотрению. Напомню основные детали проекта, связанные с делегатами. Задача состояла в оценке времени работы группы методов, входящих в состав создаваемого класса MyMath, - sin(x), cos(x), tg(x) и им подобные методы, имеющие одинаковую сигнатуру. Для оценки времени был создан специальный класс TimeValue. В этом классе был описан вложенный класс-делегат:
/// <summary>
/// Класс спроектирован для получения оценок времени
/// выполнения различных методов.
/// Встроенные делегаты определяют сигнатуры этих методов
/// </summary>
public class TimeValue
{
public delegate double DToD(double arg1);
}
В класс Timevalue включен метод EvalTimeDToD, один из аргументов которого принадлежит функциональному типу, заданному делегатом:
/// <summary>
/// Возвращает время в секундах,
/// затраченное на вычисление count раз
/// метода fun с сигнатурой, удовлетворяющей
/// делегату DToD (double to double)
/// </summary>
/// <param name="count">число повторений</param>
/// <param name="fun">имя функции</param>
/// <param name="x">аргумент</param>
/// <returns>время в милисекундах или тиках</returns>
public static double EvalTimeDToD(int count, DToD fun, double x)
{
DateTime start, finish;
double res = 0;
start = DateTime.Now;
for (int i = 1; i < count; i++)
fun(x);
finish = DateTime.Now;
res = (finish - start).ticks;
return res;
}
Используя этот метод, клиенты класса TimeValue без труда могут оценить время работы любого метода, сигнатура которого задается делегатом DToD. Возможности класса легко расширить, определив новые делегаты.
Рассмотрим еще несколько более формальных примеров и начнем с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс QwnDel:
namespace Delegates
{
//объявление классов-делегатов
delegate void Proc(ref int x);
delegate string MesToPers(string s);
class OwnDel
{
public delegate int Fun1(int x);
int Plus1( int x){return(x+100);}//Plus1
int Minus1(int x){return(x-100);}//Minus1
void Plus(ref int x){x+= 100;}
void Minus(ref int x){x-=100;}
//поля класса
public Proc p1;
public Fun1 f1;
char sign;
//конструктор
public OwnDel(char sign)
{
this.sign = sign;
if (sign == '+')
{p1 = new Proc(Plus);f1 = new Fun1(Plus1);}
else
{p1 = new Proc(Minus);f1 = new Fun1(Minus1);}
}
}//class OwnDel
}
Прокомментирую этот текст.
Proc, MesToPers, Fun1. Каждый из них описывает множество функций фиксированной сигнатуры.OwnDel описаны четыре метода: Plus, Minus, Plus1, Minus1, сигнатуры которых соответствуют сигнатурам, задаваемым классами Proc и Fun1.p1 и f1 класса OwnDel являются экземплярами классов Proc и Fun1.p1 и f1 связываются с конкретными методами Plus или Minus, Plus1 или Minus1. Связывание с той или иной функцией в данном случае определяется значением поля sign.Заметьте, поскольку делегаты относятся к ссылочным типам и, соответственно, присваивание является ссылочным присваиванием, то
Приведу теперь метод класса Testing - клиента класса OwnDel, тестирующую работу сервисов этого класса:
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, связанных к моменту вызова с
В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в классе поставщика. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся. Рассмотрим объявление многократно встречавшегося класса Person:
/// <summary>
/// Класс Person с полями name, id, salary, message.
/// Позволяет сравнивать объекты Person
/// по разным критериям.
/// </summary>
class Person
{
//свойства
string name;
int id;
double salary;
string message;
//конструкторы
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 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; }
}
//методы
/// <summary>
/// Передает сообщение объекту
/// </summary>
/// <param name="mes">сообщение</param>
/// <returns>имя с присоединенным сообщением</returns>
public string ToPerson(string mes)
{
this.message = mes;
return string.Format("{0}, {1}",name, message);
}
}//class Person
Класс Person будет использоваться и в других примерах данной лекции. При анализе текста этого класса прошу обратить внимание на метод класса ToPerson, сигнатура которого совпадает с сигнатурой, определенной делегатом MesToPers. Посмотрите, как клиент класса может связать этот метод с экземпляром делегата, определенного самим клиентом:
Person man1 = new Person("Владимир");
MesToPers mestopers = new MesToPers(man1.ToPerson);
Console.WriteLine(mestopers("пора работать!"));
Обратите внимание, что поскольку метод ToPerson не является статическим методом, при связывании необходимо передать и объект, вызывающий метод. Более того, переданный объект становится доступным
В данном примере переданное сообщение "пора работать" присоединится к имени объекта, и результирующая строка будет выдана на печать. В тех случаях, когда метод, связываемый с экземпляром делегата, не использует информацию объекта, этот метод может быть объявлен как статический метод класса. Таким образом, инициализировать экземпляры делегата можно как статическими, так и
Последние три строки были добавлены в вышеприведенную тестирующую процедуру TestOwnDel. Взгляните на результаты ее работы.
(рис 6.1) Объявление делегатов и создание их экземпляровОдно из наиболее важных применений делегатов связано с EvalTimeDToD у нас уже был. Классическим примером является функция вычисления интеграла, у которой один из аргументов задает подынтегральную функцию. Другим примером может служить функция, сортирующая объекты. Аргументом ее является функция Compare, сравнивающая два сложных объекта. В зависимости от того, какая
Давайте более подробно рассмотрим ситуацию с функциями высшего порядка на примере задачи вычисления
//delegate
public delegate double DToD(double x);
/// <summary>
/// Вычисление интеграла.
/// Сигнатура подынтегральной функции
/// определяется делегатом
/// </summary>
public class HighOrderIntegral
{
/// <summary>
/// Вычисление интеграла по методу трапеций
/// </summary>
/// <param name="a">начало интервала интегрирования</param>
/// <param name="b">конец интервала интегрирования</param>
/// <param name="eps">точность вычисления</param>
/// <param name="sif"> подынтегральная функция</param>
/// <returns>значение интеграла</returns>
public double EvalIntegral(double a, double b,
double eps, DToD sif)
{
const int INITIAL_POINTS = 4;
const int MAX_POINTS = 2 << 15;
int n = INITIAL_POINTS;
double I0=0, I1 = I( a, b, n,sif);
for( n *=2; n < MAX_POINTS; n*=2)
{
I0 =I1; I1=I(a,b,n,sif);
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, DToD 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 предназначен для работы с функциями. В него вложено описание уже известного нам функционального класса - делегата DToD, который задает класс функций с одним аргументом типа double, возвращающих значение этого же типа.EvalIntegral - основной метод класса позволяет вычислять DToD.n частей, и вычисляется частичная сумма по n удваивается, и вычисляется новая сумма. Если разность двух приближений по модулю меньше заданной точности eps, то вычисление интеграла заканчивается, иначе процесс повторяется в цикле. Цикл завершается либо по достижении заданной точности, либо когда n достигнет некоторого предельного значения.Чтобы продемонстрировать работу с классом HighOrderIntegral, приведу еще класс Functions, где описано несколько функций, удовлетворяющих контракту, который задан классом DToD:
/// <summary>
/// Статические методы класса задают простые функции
/// </summary>
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()
{
HighOrderIntegral hoi = new HighOrderIntegral();
DToD hoisif1 =
new DToD(Functions.sif1);
double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);
Console.WriteLine("myintegral1 = {0}",myint1);
DToD hoisif2 =
new DToD(Functions.sif2);
myint1= hoi.EvalIntegral(2,3,1e-7,hoisif2);
Console.WriteLine("myintegral2 = {0}",myint1);
}//EvalIntegrals
Здесь создаются два HighOrderIntegral, вызывающий метод вычисления интеграла. Результаты работы показаны на рис. 6.2.
(рис 6.2) Вычисление интеграла с использованием функций высших порядков
Язык C# - молодой, активно развивающийся язык программирования, в каждой версии которого появляются новые средства. Одна из тенденций развития связана с введением анонимности - появлением анонимных методов, анонимных типов данных и соответствующих переменных. Эти средства позволяют уменьшить расстояние между объявлением объекта и его использованием. Когда объект объявляется непосредственно в точке, где он используется, то это делает код более понятным, облегчает его сопровождение, что крайне важно для долгоживущих проектов.
Хороший стиль программирования требует, чтобы локальная переменная объявлялась как можно ближе к точке ее непосредственного использования. А вот с методами и типами данных все немного сложнее.
Давайте обратимся к нашему примеру с вычислением интеграла. У нас был построен отдельный класс Functions, в котором были собраны методы, задающие различные подынтегральные функции. Эти функции можно рассматривать как экземпляры функционального класса, заданного делегатом DToD. Для вычисления интеграла в
HighOrderIntegral hoi = new HighOrderIntegral();
DToD hoisif1 =
new DToD(Functions.sif1);
double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);
Эти строчки позволяли вычислить интеграл от функции с именем sif1 из класса Functions. Все прекрасно, но для понимания того, какова эта функция, приведенных четырех строк недостаточно - необходимо обращаться к классу Functions. Даже если поместить описание функции в класс Testing, то все равно описание функции отделено от ее использования. Анонимные методы позволяют решить эту проблему. Я вначале приведу пример использования анонимного метода, а затем уже рассмотрим общие свойства этих методов. Итак, как решается задача вычисления интеграла с использованием анонимного метода, задающего подынтегральную функцию? Вот пример решения:
public void TestAnonymous()
{
HighOrderIntegral integral = new HighOrderIntegral();
double a = 0, b = Math.PI/2, eps = 1e-7;
double result = integral.EvalIntegral(a, b, eps,
delegate(double x)
{ return Math.Sin(x) + Math.Cos(x); });
Console.WriteLine("Integral(Sin(x) + Cos(x)) " +
"from {0} to {1} = {2} ", a, b, result.ToString());
}
По сути, это те же четыре строчки, дополненные печатью результата. Но! Обратите внимание, подынтегральная функция, в данном случае sin(x) + cos(x), задана с помощью делегата непосредственно в точке вызова явным образом своим описанием! Это анонимная функция - она лишена имени, для нее не указан тип, как подобает приличным объектам языка. Во многом анонимные методы напоминают константы, заданные литералами, которые также заданы своими значениями и тип которых определяется по самому значению.
Давайте теперь подробно разберемся, как это делается. Описание анонимного метода является выражением и может встречаться всюду, где разрешены выражения, - в правой части оператора присваивания в качестве
delegate [(<сигнатура метода>)] <тело метода>
Какому функциональному типу соответствует тип анонимного метода? Нужно сказать, что у анонимного метода нет внутреннего, присущего ему функционального типа. Но для корректного применения анонимного метода его тип должен приводиться к типу делегата, заданного контекстом использования анонимного метода. Поскольку выражение, задающее анонимный метод, может появляться в правой части оператора присваивания или в качестве
Прежде всего поговорим о том, как определяется тип возвращаемого значения анонимного метода, а следовательно, и о том, как реализован анонимный метод - процедурой или функцией. Заметьте, в описание анонимного метода не входит описание возвращаемого методом значения. Компилятор сам определяет, как реализован метод, анализируя его явное описание. Если в описании анонимного метода присутствуют один или несколько операторов return, сопровождаемых возвращаемым значением, то анонимный метод реализован как функция и тип возвращаемого значения определяется типом возвращаемых значений в операторах return. Понятно, что все возвращаемые значения должны быть согласованы по типу, иначе возникнет ошибка периода компиляции. Если операторов return в теле метода нет или они не возвращают значение, то метод реализован как процедура, возвращающая void в качестве результата. Так компилятор может установить соответствие между возвращаемым типом источника и цели.
Когда для анонимного метода не задана сигнатура, то применяется следующее правило установления соответствия типов источника и цели. Анонимный метод соответствует по типу любому делегату с произвольным числом аргументов, если только ни один из аргументов не сопровождается описателями ref или out. Если же функциональный тип формального аргумента или левой части оператора присваивания содержит входные или обновляемые аргументы, то соответствия нет, и это приводит к ошибке периода компиляции.
Когда для анонимного метода задана сигнатура, применятся следующее правило установления соответствия: сигнатуры источника и цели должны совпадать. Заметьте, речь идет о строгом совпадении типов. Не допускается, чтобы тип цели был double, а у источника - int, или тип цели Father, а тип источника Son - потомок Father.
Давайте рассмотрим пример, в котором будут фигурировать разные делегаты и разные анонимные методы, соответствующие делегатам по типам.
class Father { }
class Son : Father { }
delegate string D1(string s, double x);
delegate void D2(string s, out string s1);
delegate string D3(Father item);
public void TestAnonymToDelegate()
{
D1 d1 = delegate {return "OK!";};
D1 d12 = delegate(string s, double x) { return s + x; };
Console.WriteLine(d1("s",5));
Console.WriteLine(d12("12", 3));
string res;
D2 d2 = delegate(string s, out string r){r = s+s;};
d2("Hello ", out res);
Console.WriteLine(res);
D3 d3 = delegate(Father item) { return item.ToString(); };
Console.WriteLine(d3(new Son()));
}
К типу делегата D1 приводятся типы двух анонимных методов с разной сигнатурой. Приведение возможно, поскольку оба анонимных метода возвращают строку в качестве результата, что согласуется с типом возвращаемого значения делегата D1. Анонимный метод, используемый для задания переменной d1, не имеет сигнатуры, что позволяет сопоставить его с типом делегата D1, у которого нет d1 2, полностью совпадает с сигнатурой делегата D1.
Анонимный метод, используемый для задания переменной d2, соответствует по типу делегату D2, требующему, чтобы метод был реализован как процедура и имел out. Аналогично устанавливается соответствие между анонимным методом переменной d3 и типом делегата D3. Естественно, что при вызове d3 в качестве Son, в то время как тип аргумента анонимного метода полностью соответствует типу аргумента делегата D3 и принадлежит родительскому классу Father. Результаты работы тестирующего метода показаны на рис. 6.3.
(рис 6.3) Анонимные методы и делегаты Анонимные методы могут использовать контекст, в котором они появляются. Это означает, что в теле анонимного метода можно применять как локальные переменные и передаваемые по значению аргументы того метода, в котором задается явное описание анонимного метода, так и поля соответствующего класса. Приведу пример подобного анонимного метода. Построим класс, одним из полей которого является массив чисел. В классе есть метод прохода по массиву и выполнение над его элементами некоторого действия, которое определяется функцией, заданной делегатом. Другой метод этого класса конкретизирует обход, задавая функцию анонимным методом. Вот этот класс:
/// <summary>
/// Класс с анонимным методом, использующим контекст
/// </summary>
class WithAnonymous
{
double max;
double[] array;
public WithAnonymous(double max, double[] array)
{
this.max = max; this.array = array;
}
/// <summary>
/// Обход массива
/// </summary>
/// <param name="array"> массив</param>
/// <param name="res"> результат вычислений</param>
/// <param name="action">действие над элементами</param>
void Walk(double[] array, out double res, DToD action )
{
int current = 0; res = 0;
while (current < array.Length)
res += action(array[current++]);
}
/// <summary>
/// Конкретизация обхода анонимным методом
/// </summary>
/// <param name="min">Нижняя граница отбора</param>
/// <param name="N">Число отобранных элементов</param>
/// <param name="res">результат обхода</param>
public void Traverse(double min, out int N, out double res)
{
int n = 0;
Walk(array, out res, delegate(double x) {
if(x > min x < max) {n++; return x;}
else return 0; });
N = n;
}
}
Заметьте, анонимный метод, определенный в методе , использует аргумент min этого метода, локальную переменную n метода , поле max класса WithAnonymous. Локальная переменная для анонимного метода является выходной переменной, и метод изменяет ее значение. Анонимный метод может рассматриваться как
Для полноты картины приведу тестирующий метод из класса Testing:
public void TestWithAnonymous()
{
WithAnonymous wan = new WithAnonymous(20,
new double[] { 17, 23, 12, 45, 7 });
int N;
double res;
wan.Traverse(10, out N, out res);
Console.WriteLine("N= {0}, res = {1}", N, res);
}
Результат, как и следовало ожидать, равен 29.
Анонимные методы прекрасно справляются с поставленной перед ними задачей. Но в C# 3.0 пошли дальше, введя более привычную для математиков форму записи анонимного метода в виде лямбда-выражения. Вместо записи анонимного метода в форме:
delegate [(<сигнатура метода>)] <тело метода>
используется форма, задающая лямбда-выражение:
[(<сигнатура метода>)] => <тело метода>
В методе TestAnonymous анонимный метод определяется следующим образом:
double result = integral.EvalIntegral(a, b, eps,
delegate(double x)
{ return Math.Sin(x) + Math.Cos(x); });
Заменим это определение лямбда-выражением:
double result = integral.EvalIntegral(a, b, eps,
(double x) =>
{ return Math.Sin(x) + Math.Cos(x); });
Результат будет тот же. Приведу теперь аналог метода TestAnonymToDelegate, заменив прежние определения анонимных методов лямбда-выражениями.
public void TestAnonymToLambda()
{
D1 d1 = (s, x) => { return "OK!"; };
D1 d12 = (string s, double x) => { return s + x; };
Console.WriteLine(d1("s", 5));
Console.WriteLine(d12("12", 3));
string res;
D2 d2 = (string s, out string r) => { r = s + s; };
d2("Hello ", out res);
Console.WriteLine(res);
D3 d3 = item => { return item.ToString(); };
Console.WriteLine(d3(new Son()));
}
Обратите внимание: в лямбда-выражениях можно опускать задание типов аргументов, оставив только имена аргументов. В этом случае компилятор попытается вывести их типы из контекста, задающего
Анонимный метод, заданный лямбда-выражением, может также использовать контекст. Приведу аналог метода из класса WithAnonymous:
public void TraverseL(double min, out int N, out double res)
{
int n = 0;
Walk(array, out res, x =>
{
if (x > min x < max) { n++; return x; }
else return 0;
});
N = n;
}
Лямбда-выражения и анонимные типы данных активно используются при работе с запросами к базам данных, так что, надеюсь, к этой теме мы еще вернемся в следующих разделах этого курса. А сейчас продолжим рассмотрение случаев использования функционального типа данных.
Метод "раскрутки" является одним из основных методов функционально-ориентированного построения сложных программных систем. Суть его состоит в том, что программная система создается слоями. Вначале пишется ядро системы - нулевой слой, реализующий базовый набор функций. Затем пишется первый слой с новыми функциями, которые интенсивно вызывают в процессе своей работы функции ядра. Теперь система обладает большим набором функций. Каждый новый слой расширяет функциональность системы. Процесс продолжается, пока не будет достигнута заданная функциональность. На рисунке, изображающем схему построения системы
(рис 6.4) Построение системы методом "раскрутки"Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время компилятор языка и операционная система были написаны на языке С методом раскрутки. В то время существовало множество разных типов компьютеров, каждый из которых имел свою систему команд. Существовала серьезная проблема переноса операционной системы и компилятора с одного типа компьютера на другой. Благодаря раскрутке, ядро, включающее базовые конструкции языка С и операционной системы, писалось на языке ассемблера данного компьютера, а все остальные слои писались на С. Это позволило написать на 95% на языке С транслятор с языка С и операционную систему, что обеспечивало легкий перенос транслятора и операционной системы на компьютеры с разной системой команд.
При построении систем
Пусть F - G функционального типа. Тогда функцию G, задающую параметр (а иногда и саму функцию F ), называют функцией обратного вызова (callback-функцией). Термин вполне понятен. Если в некотором внешнем слое функция Q вызывает функцию внутреннего слоя F, то, предварительно во внешнем слое следует позаботиться о создании функции G, которая и будет передана F. Таким образом, функция Q внешнего слоя вызывает функцию F внутреннего слоя, которая, в свою очередь (обратный вызов), вызывает функцию G внешнего слоя. Чтобы эта техника работала, должен быть задан контракт. Функция высших порядков, написанная во внутреннем слое, задает следующий контракт: "всякая функция, которая собирается меня вызвать, должна передать мне функцию обратного вызова, принадлежащую определенному мной функциональному классу, следовательно, иметь
известную мне сигнатуру".
Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс HighOrderIntegral - это внутренний слой нашей системы. В нем задан делегат, определяющий контракт, и функция EvalIntegral, требующая задания функции обратного вызова в качестве ее параметра. Функция EvalIntegral вызывается из внешнего слоя (клиентами класса), где и определяются -функции. Как показано выше, -функции могут иметь имена и находиться, например, в классе Functions, а могут быть заданы анонимными методами или, что еще проще, лямбда-выражениями.
Многие из функций операционной системы Windows, входящие в состав Win API 32, требуют при своем вызове задания -функций. Примером может служить работа с объектом операционной системы Timer. Конструктор этого объекта является -функцию, вызываемую для обработки событий, которые поступают от таймера.
Пример работы с таймером приводить сейчас не буду, ограничусь лишь сообщением Timer:
public Timer(TimerCallback callback,object state,
int dueTime, int period);
Первым параметром конструктора является функция обратного вызова , которая принадлежит функциональному классу TimerCallback, заданному делегатом:
public delegate void TimerCallback(object state);
Сегодня многие программные системы проектируются и разрабатываются не в функциональном, а в объектно-ориентированном стиле. Такая система представляет собой одно или несколько семейств интерфейсов и классов, связанных
О полиморфизме говорилось достаточно много в предыдущих лекциях. Тем не менее позволю напомнить суть дела. Родитель может объявить виртуальный метод F и вызывать этот метод в некотором методе Q, не обязательно виртуальном. Потомку разрешается переопределить реализацию виртуального метода, но он не имеет права изменять его сигнатуру. Когда потомок вызывает родительский метод Q, благодаря полиморфизму, при работе этого метода будет вызван не метод родителя F, а метод F, реализованный потомком.
Ситуация в точности напоминает раскрутку и вызов обратных функций. Родительский метод Q находится во внутреннем слое, а потомок с его методом F определен во внешнем слое. Когда потомок вызывает метод Q из внутреннего слоя, тот, в свою очередь, вызывает метод F из внешнего слоя. Сигнатура вызываемого метода F в данном случае задается не делегатом, а сигнатурой виртуального метода, которую, согласно контракту, потомок не может изменить.
Давайте вернемся к задаче вычисления интеграла и создадим реализацию, основанную на наследовании и полиморфизме. Идея примера такова. Вначале построим родительский класс, один из методов которого будет вычислять интеграл от некоторой подынтегральной функции. Сама функция известна в классе, и ее вычисление реализовано методом класса, который сделаем виртуальным. Теперь представим себе, что необходимо посчитать интеграл от другой подынтегральной функции. Заново писать метод вычисления интеграла не хочется, хорошая его реализация достаточно сложна. Как воспользоваться уже написанным методом готового класса? Можно было бы внести изменения в исходный класс, заменив одну функцию на другую. Но так поступать нельзя, поскольку класс используется многими клиентами и он "закрыт" для изменений. Правильный путь состоит в том, чтобы "открыть" класс, создав его потомка, который наследует метод вычисления интеграла, но сможет переопределить виртуальный метод, задающий подынтегральную функцию. Приведу пример кода, следующего этой схеме:
/// <summary>
/// Класс, в котором определен метод вычисления интеграла
/// и виртуальный метод, задающий подынтегральную функцию
/// </summary>
class FIntegral
{
/// <summary>
/// Вычисление интеграла методом трапеции
/// </summary>
/// <param name="a">нижний предел интегрирования</param>
/// <param name="b">верхний предел интегрирования</param>
/// <param name="eps">точность вычисления</param>
/// <returns>значение интеграла</returns>
public double EvaluateIntegral(double a, double b, double eps)
{
const int INITIAL_POINTS = 4;
const int MAX_POINTS = 2 << 15;
int n = INITIAL_POINTS;
double I0=0, I1 = I( a, b, n);
for (n *= 2; n < MAX_POINTS; 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 нет параметра функционального типа. Вместо этого тут же в классе определен защищенный виртуальный метод, задающий конкретную подынтегральную функцию. В качестве таковой выбрана самая простая функция, тождественно равная единице.
Для вычисления интеграла от произвольной функции единственное, что нужно сделать, - это задать
class FIntegralSon:FIntegral
{
protected override double sif(double x)
{
double a = 1.0; double b = 2.0; double c= 3.0;
return (double)(a*x*x +b*x +c);
}
}//FIntegralSon
Принципиально задача решена. Осталось только написать фрагмент кода, запускающий вычисления. Он оформлен в виде метода класса Testing:
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
Взгляните на результаты вычислений.
(рис 6.5) Вычисление интеграла, использующее полиморфизмВ наших примерах рассматривалась ситуация, при которой в некотором классе объявлялись функции, удовлетворяющие контракту с делегатом, но создание экземпляров делегата и их инициирование функциями класса выполнялось в другом месте, там, где предполагалось вызывать соответствующие функции. Чаще всего создание экземпляров удобнее возложить на класс, создающий требуемые функции. Более того, в этом классе делегат можно объявить как свойство класса, что позволяет "убить двух зайцев". Во-первых, с пользователей класса снимается забота создания делегатов. Во-вторых,
Рассмотрим пример, демонстрирующий и поясняющий эту возможность при работе с Person и введем новый класс Persons:
Person определим различные реализации функции Compare с одной и той же сигнатурой, позволяющие сравнивать два объекта по имени, по номеру, по зарплате, по нескольким полям. Самое интересное, ради чего и строится данный пример: для каждой реализации Compare будет построена процедура-свойство, которая "на лету" создает Compare ;Persons будет играть роль Person. В этом классе будут определены операции над объектами контейнера. Среди операций нас, прежде всего, будет интересовать сортировка объектов, реализованная в виде Compare из класса Person будут принадлежать этому классу.Теперь, когда задача ясна, приступим к ее реализации. Класс Person дополним до нужной функциональности.
Заметьте, можно было бы объявить класс Person наследником интерфейса IComparable, реализовать в классе метод Compare этого интерфейса, перегрузить знаки операций сравнения, что позволяло бы сравнивать персоны по одному критерию. Но в данном случае это было бы методологически неправильно, поскольку персоны, как и другие сложные объекты, следует сравнивать в зависимости от обстоятельств по разным критериям.
Как же следует организовать класс, чтобы обеспечить работу с разными методами сравнения? Для каждого способа сравнения введем статический метод-свойство, создающий и возвращающий в качестве результата
//делегат
delegate int CompareItems(Person obj1, Person obj2);
Сами методы-свойства зададим, используя лямбда-выражения для определения сравнения по заданному критерию:
//делегаты как свойства
public static CompareItems SortByName
{
get
{
return (person1, person2) =>
{
return string.Compare(person1.name, person2.name);
};
}
}
public static CompareItems SortById
{
get {
return (person1, person2) =>
{
if (person1.Id > person2.Id) return 1;
else return -1;
};
}
}
public static CompareItems SortBySalary
{
get
{
return (person1, person2) =>
{
if (person1.salary > person2.salary) return 1;
else return -1;
};
}
}
public static CompareItems SortBySalaryName
{
get
{
return (person1, person2) =>
{
if (person1.salary > person2.salary) return 1;
else if (person1.salary < person2.salary) return -1;
else return string.Compare(person1.name, person2.name);
};
}
}
Всякий раз, когда будет запрошено, например, свойство SortByName класса Person, будет возвращен объект функционального класса CompareItems. Объект будет создаваться динамически в момент запроса.
Класс Person полностью определен, и теперь давайте перейдем к определению контейнера, содержащего объекты Person:
/// <summary>
/// Контейнер объектов Person
/// Построен на массиве
/// </summary>
class Persons
{
int n = 100; //емкость контейнера
int freeItem; //свободный для заполнения элемент
private Person[] persons; //контейнер
//конструкторы
public Persons()
{
n = 100; freeItem = 0;
persons = new Person[n];
}
public Persons(int n)
{
this.n = n; freeItem = 0;
persons = new Person[n];
}
/// <summary>
/// Индексатор.
/// Доступ к элементам контейнера по индексу
/// </summary>
/// <param name="num">индекс</param>
/// <returns>элемент с заданным индексом</returns>
public Person this[int num]
{
get { return(persons[num-1]); }
set { persons[num-1] = value; }
}
/// <summary>
/// Добавление элементов в контейнер
/// </summary>
/// <param name="pers">добавляемый элемент</param>
/// <returns>true при успешном добавлении</returns>
public bool AddPerson(Person pers)
{
if(freeItem < n)
{
Person p = new Person(pers);
persons[freeItem++]= p;
return true;
}
else return false;
}
/// <summary>
/// Заглушка.
/// Добавление в контейнер 6 фиксированных элементов
/// </summary>
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));
}//LoadPersons
/// <summary>
/// Вывод на консоль
/// </summary>
public void PrintConsolePersons()
{
for(int i =0; i<freeItem; i++)
{
Console.WriteLine("{0,10} {1,5} {2,5}",
persons[i].Name, persons[i].Id, persons[i].Salary);
}
}//PrintConsolePersons
/// <summary>
/// Сортировка контейнера методом пузырька
/// Критерий сортировки задается функцией compare
/// </summary>
/// <param name="compare">
/// функция сравнения элементов контейнера</param>
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
Приведу краткие комментарии к этому тексту. LoadPerson и PrintConsolePersons добавлены в интересах отладки.
Две основные операции определены над контейнером - добавление нового элемента и сортировка. Код метода сортировки следует внимательно проанализировать. У метода сортировки есть аргумент типа CompareItems, и при вызове метода сортировки клиент может передать в качестве Person.
На этом проектирование классов закончено, нужная цель достигнута, показано, как можно в классе Testing тестирующую процедуру, в которой будут использоваться различные критерии сортировки:
public void TestSortPersons()
{
Persons persons = new Persons(6);
persons.LoadPersons();
Console.WriteLine (" Сортировка по имени: ");
persons.SimpleSortPerson(Person.SortByName);
persons.PrintConsolePersons();
Console.WriteLine (" Сортировка по идентификатору: ");
persons.SimpleSortPerson(Person.SortById);
persons.PrintConsolePersons();
Console.WriteLine (" Сортировка по зарплате: ");
persons.SimpleSortPerson(Person.SortBySalary);
persons.PrintConsolePersons();
Console.WriteLine (" Сортировка по зарплате и имени: ");
persons.SimpleSortPerson(Person.SortBySalaryName);
persons.PrintConsolePersons();
}//SortPersons
Заметьте, клиент не создает никаких экземпляров делегата, он просто при вызове метода сортировки передает методу нужное свойство класса Person.
Вот как выглядят результаты работы сортировки данных.
(рис 6.6) Сортировка данныхДавайте просуммируем то, что уже известно о функциональном типе данных. Ключевое слово delegate позволяет задать определение функционального типа (класса), фиксирующее контракт, которому должны удовлетворять все функции, принадлежащие классу. Функциональный класс можно рассматривать как ссылочный тип, экземпляры которого являются ссылками на функции. Заметьте, ссылки на функции - это безопасные по типу указатели, которые ссылаются на функции с жестко фиксированной сигнатурой, заданной делегатом. Следует также понимать, что это не простая ссылка на функцию. В том случае, когда X, вызвавший этот метод.
Вместе с тем, объявление функционального типа не укладывается в синтаксис, привычный для C#. Хотелось бы писать, как принято:
Delegate FType = new Delegate(<определение типа>)
Но так объявлять переменные этого класса нельзя, и стоит понять, почему. Есть ли вообще класс Delegate? Ответ положителен - есть такой класс. При определении функционального типа, например:
public delegate int FType(int X);
переменная FType принадлежит классу Delegate. Почему же ее нельзя объявить привычным образом? Дело не только в синтаксических особенностях этого класса. Дело в том, что класс Delegate является абстрактным классом. Вот его объявление:
public abstract class Delegate: ICloneable, ISerializable
Для абстрактных классов реализация не определена, и это означает, что нельзя создавать экземпляры класса. Класс Delegate служит базовым классом для классов-наследников. Но создавать наследников могут только компиляторы и системные программы, этого нельзя сделать в программе на C#. Именно поэтому введено ключевое слово delegate, которое косвенно позволяет работать с классом Delegate, создавая уже не абстрактный, а реальный класс. Заметьте, при этом все динамические и статические методы класса Delegate становятся доступными программисту.
Трудно, кажется, придумать, что можно делать с делегатами, задающими класс функций. Однако у них есть одно замечательное свойство - функции можно комбинировать. Представьте себе, что существует список работ, которые нужно выполнять, в зависимости от обстоятельств, в разных комбинациях. Если функции, выполняющие отдельные работы, принадлежат одному классу, то для решения задачи можно использовать технику их комбинирования.
Замечу, что возможность комбинирования появилась у делегатов в первую очередь для поддержания работы с событиями. Когда возникает некоторое событие, то сообщение о нем посылается разным объектам, каждый из которых по-своему обрабатывает событие. Реализуется эта возможность на основе комбинирования обработчиков событий.
В чем суть комбинирования? Она прозрачна. К
Понятно, что если есть операция присоединения функций, то должна быть и обратная операция, позволяющая удалять функции из списка вызовов.
Рассмотрим основные методы и свойства класса Delegate. Начнем с двух статических методов - Combine и Remove. Первый из них присоединяет
Combine(del1, del2) Remove(del1, del2)
Аргументы del1 и del2 должны быть одного функционального класса. При добавлении del2 в список, в котором del2 уже присутствует, будет добавлен второй экземпляр. При попытке удаления del2 из списка, в котором del2 нет, Remove благополучно завершит работу, не выдавая сообщения об ошибке.
Класс Delegate относится к неизменяемым классам, поэтому оба метода возвращают ссылку на нового делегата. Возвращаемая ссылка принадлежит родительскому классу Delegate, поэтому ее необходимо явно преобразовать к нужному типу, которому принадлежат del1 и del2. Обычное использование этих методов имеет вид:
del1 = (<type>) Combine(del1, del2); del1 = (<type>) Remove(del1, del2);
Метод GetInvocationList является foreach, поочередно получая элементы списка. Чуть позже появится пример, поясняющий необходимость подобной работы со списком.
Два Method и Target полезны для получения подробных сведений о делегате. Чаще всего они используются в процессе отражения, когда делегат поступает извне и необходима метаинформация, поставляемая с делегатом. Свойство Method возвращает объект класса MethodInfo из пространства имен . Свойство Target возвращает информацию о цели - объекте, вызвавшем делегата, в тех случаях, когда делегат инициируется не статическим методом класса, а динамическим, связанным с вызвавшим его объектом.
У класса Delegate, помимо методов, наследуемых от класса object, есть еще несколько методов, но мы на них останавливаться не будем, они используются не столь часто.
Наряду с методами, над делегатами определены и две операции: "+" и "-", которые являются более простой формой записи добавления в Combine и Remove. Выше написанные присваивания объекту del1 с помощью этих операций могут быть переписаны в виде:
del1 +=del2; del1 -=del2;
Как видите, запись становится проще, исчезает необходимость в задании явного приведения к del1 и del2, естественно, остаются те же, что и для методов Combine и Remove.
Рассмотрим следующую ситуацию. Пусть есть городские службы: милиция, скорая помощь, пожарные. Каждая из служб по-своему реагирует на события, происходящие в городе. Построим примитивную модель жизни города, в котором происходят события, и сообщения о них посылаются службам. В последующей лекции эта модель будет развита. Сейчас она носит формальный характер, демонстрируя, главным образом, работу с делегатами, заодно поясняя ситуации, в которых разумно комбинирование делегатов.
Начнем с построения класса с именем Combination. Этот класс будет моделировать работу городских служб, реагирующих на события, происходящие в городе. Каждая служба будет описываться статическим методом. Сигнатуры всех методов будут одинаковыми и будут соответствовать ранее введенному делегату MesToPers. На входе они будут получать сообщение о событии, а на выходе - выдавать результат своих действий.
Работу с сервисами этого класса построим по описанной ранее технологии. Сами методы будут закрыты для клиентов, а доступ к ним осуществляться через свойства, на лету создающие требуемый
/// <summary>
/// Класс, статические методы которого
/// моделируют работу городских служб
/// </summary>
class Combination
{
// Закрытые сервисы класса
/// <summary>
/// Модель действий милиции
/// </summary>
/// <param name="mes">
/// сообщение о возникшем событии</param>
/// <returns>результат действий</returns>
static string policeman(string mes)
{
if(mes == "Пожар!")
return(mes + " Милиция ищет виновных!");
else
return(mes +" Милиция здесь!");
}
/// <summary>
/// Модель действий скорой помощи
/// </summary>
/// <param name="mes">
/// </param>сообщение о возникшем событии
/// <returns>результат действий</returns>
static string ambulanceman(string mes)
{
if(mes == "Пожар!")
return(mes + " Скорая спасает пострадавших!");
else
return(mes + " Скорая помощь здесь!");
}
/// <summary>
/// Модель действий пожарных
/// </summary>
/// <param name="mes">сообщение о возникшем событии
/// сообщение о возникшем событии</param>
/// <returns>результат действий</returns>
static string fireman(string mes)
{
if(mes =="Пожар!")
return(mes + " Пожарные тушат пожар!");
else
return( mes + " Пожарные здесь!");
}
//доступ к сервисам через свойства
public static MesToPers Policeman
{
get {return (new MesToPers(policeman));}
}
public static MesToPers Fireman
{
get {return (new MesToPers(fireman));}
}
public static MesToPers Ambulanceman
{
get {return (new MesToPers(ambulanceman));}
}
}//class Combination
Службы у нас есть, покажем, как с ними можно работать. С этой целью добавим в класс Testing, где проводятся различные эксперименты, следующую процедуру:
public void TestSomeServices()
{
MesToPers Comb;
Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman,
Combination.Policeman);
Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman);
Console.WriteLine(Comb("Пожар!"));
Вначале объявляется Comb, которой в следующем операторе присваивается ссылка на Combine, чей Ambulanceman и Policeman. Затем к списку вызовов экземпляра Comb присоединяется новый кандидат Fireman. При вызове объекта Comb ему передается сообщение "Пожар!". В результате вызова Comb поочередно запускаются все три экземпляра, входящие в список, каждому из которых передается сообщение.
Этот пример очевидно демонстрирует недостаток предлагаемого механизма, который следует учитывать в реальной работе. При вызове Comb поочередно будут вызываться методы из списка вызовов. Это прекрасно. Но если, как в нашем случае, методы из списка являются функциями, возвращающими значение, то в результате работы все значения, кроме последнего, будут утеряны и на печать будет послано сообщение о результатах работы последней отработавшей функции. Этого эффекта можно избежать, если все методы реализованы как процедуры.
Давайте теперь начнем поочередно отключать делегатов, вызывая затем Comb с новыми сообщениями:
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Fireman);
//Такое возможно: попытка отключить не существующий элемент
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Special);
Console.WriteLine(Comb("Через 30 минут!"));
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Policeman);
Console.WriteLine(Comb("Через час!"));
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Ambulanceman);
//Console.WriteLine(Comb("Через два часа!")); // Comb не определен
В этом фрагменте поочередно отключаются разные службы - милиция, скорая помощь, пожарные, и каждый раз вызывается Comb. После последнего отключения, когда Comb приводит к ошибке, поэтому оператор вызова закомментирован.
Покажем теперь, что ту же работу можно выполнить, используя Delegate:
//операции + и -
Comb = Combination.Ambulanceman;
Console.WriteLine( Comb.Method.Name);
Comb+= Combination.Fireman;
Comb+= Combination.Policeman;
Console.WriteLine(Comb("День города!"));
Comb -= Combination.Ambulanceman;
Comb -= Combination.Fireman;
Console.WriteLine(Comb("На следующий день!"));
}//TestSomeServices
Обратите внимание, здесь демонстрируется вызов свойства Method, возвращающее объект, свойство Name которого выводится на печать. Пора уже взглянуть на результаты, порожденные работой этой процедуры.
(рис 6.7) Службы городаКак быть, если в списке вызовов есть "плохой" экземпляр, при вызове которого возникает ошибка, приводящая к выбрасыванию исключительной ситуации? Тогда стоящие за ним в очереди экземпляры не будут вызваны, хотя они вполне могли бы выполнить свою часть работы. В этом случае полезно использовать метод GetInvocationList и самому клиенту организовать в цикле поочередный вызов сервисов из списка. Вызов метода следует поместить в
Добавим в класс Combination специальную службу, которая при получении сообщения о пожаре выбрасывает исключение:
//метод, вызывающий исключительную ситуацию
/// <summary>
/// Модель действий специальной службы
/// </summary>
/// <param name="mes">
/// сообщение о возникшем событии</param>
/// <returns>результат действий</returns>
static string specialService(string mes)
{
if (mes == "Пожар!")
throw (new Exception("Специальная служба: Кто виноват?"));
return (mes + " Специальная служба здесь!");
}
public static MesToPers Special
{
get { return (new MesToPers(specialService)); }
}
Протестируем ситуацию, в которой возникает исключительная ситуация в процессе вызова одного из методов списка вызовов. Заодно покажем, как, используя метод GetInvocationList, справиться с этой проблемой, так же как и с проблемой потери возвращаемых методами значений.
public void TestBadService()
{
MesToPers Comb;
Comb = Combination.Ambulanceman;
Comb += Combination.Fireman;
Comb += Combination.Special;
Comb += Combination.Policeman;
foreach(MesToPers currentJob in Comb.GetInvocationList())
{
try
{
Console.WriteLine(currentJob("Пожар!"));
}
catch(Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine(currentJob.Method.Name);
}
}
}//BadService
Поясню, как будет работать эта процедура при ее вызове. Вначале две службы нормально отработают, но при вызове третьей службы возникнет исключительная ситуация. Exception перехватит эту ситуацию и напечатает как свойство Message объекта e, так и имя метода, вызвавшего исключительную ситуацию, используя свойство Method объекта, вызвавшего ситуацию. После завершения работы блока обработчика ситуации выполнение программы продолжится, выполнится следующий шаг цикла, и милицейская служба благополучно выполнит свою работу. Вот результаты вывода.
(рис 6.8) "Специальная служба"Заметьте, теперь в цикле можно проследить за результатом выполнения каждого метода из списка вызовов, так что ни одно из возвращаемых значений не будет утеряно. Этот пример демонстрирует корректный способ работы со списком вызова.
Разговор о делегатах еще не закончен. Он будет продолжен в следующей лекции, в которой рассмотрим классы с событиями. События основываются на делегатах.
X вычислять некоторую характеристику result, заданную выражением:
Метод, вычисляющий значение характеристики result, реализуйте как MyMath, являющийся аналогом класса Math из библиотеки FCL. Задайте в этом клаccе общий метод, позволяющий вычислять различные функции ( sin, cos и другие). Реализуйте этот метод как ak по соответствующей рекуррентной формуле.Functions. Класс должен выполнять различные операции над функциями, принадлежащими функциональному типу, заданному делегатом. В число операций класса включите такие методы, как вычисление интеграла, нахождение максимума, минимума, корней функции на заданном интервале, вычисление значения производной функции в заданной точке. В классе-клиенте, использующем сервисы класса Fnctions, используйте анонимные методы и лямбда-выражения. В интерфейсе проекта предусмотрите возможность задания функции как выражением, так и выбором из фиксированного списка.Person и Sorting. В классе Sorting предусмотрите разные методы сортировки, заданные Проект к данной лекции Вы можете скачать здесь.
Слово делегат ( delegate ) используется в C# для обозначения хорошо известного понятия, давно употребляемого в языках программирования. Делегат C# задает определение функционального типа (класса) данных. Экземплярами такого класса являются функции. Делегаты языка C# наряду со структурами, перечислениями и интерфейсами представляют задание еще одного частного случая классов. Каждый делегат описывает множество функций с заданной сигнатурой. Каждая функция, или если быть более точным и придерживаться терминологии, принятой в объектно-ориентированном программировании, каждый метод, сигнатура которого совпадает с сигнатурой делегата, может рассматриваться как экземпляр класса, заданного делегатом.
[<спецификатор доступа>] delegate <тип результата > <имя класса> (<список аргументов>);
Этим объявлением класса задается функциональный тип - множество функций с заданной сигнатурой, у которых аргументы определяются списком, заданным в объявлении делегата, и тип возвращаемого значения определяется типом результата делегата.
Спецификатор доступа может быть, как обычно, опущен. Где следует размещать объявление делегата? Как и у всякого класса, есть две возможности:
Так же, как и интерфейсы C#, делегаты не задают реализации. Фактически между некоторыми классами и делегатом заключается контракт на реализацию делегата. Классы, согласные с контрактом, могут объявить у себя статические или динамические функции, сигнатура которых совпадает с сигнатурой делегата. Далее они могут создать экземпляр делегата, присвоив ему в качестве значения функцию, удовлетворяющую контракт. Заметьте, контракт является жестким: не допускается ситуация, при которой у делегата тип параметра - object, а у функции, связываемой с экземпляром, соответствующий параметр имеет тип int, хотя и согласованный с типом object, но не совпадающий с ним.
Созданный
Содержательный пример работы с делегатами приводился в проекте первой лекции. Читателю есть смысл вернуться к его рассмотрению. Напомню основные детали проекта, связанные с делегатами. Задача состояла в оценке времени работы группы методов, входящих в состав создаваемого класса MyMath, - sin(x), cos(x), tg(x) и им подобные методы, имеющие одинаковую сигнатуру. Для оценки времени был создан специальный класс TimeValue. В этом классе был описан вложенный класс-делегат:
/// <summary>
/// Класс спроектирован для получения оценок времени
/// выполнения различных методов.
/// Встроенные делегаты определяют сигнатуры этих методов
/// </summary>
public class TimeValue
{
public delegate double DToD(double arg1);
}
В класс Timevalue включен метод EvalTimeDToD, один из аргументов которого принадлежит функциональному типу, заданному делегатом:
/// <summary>
/// Возвращает время в секундах,
/// затраченное на вычисление count раз
/// метода fun с сигнатурой, удовлетворяющей
/// делегату DToD (double to double)
/// </summary>
/// <param name="count">число повторений</param>
/// <param name="fun">имя функции</param>
/// <param name="x">аргумент</param>
/// <returns>время в милисекундах или тиках</returns>
public static double EvalTimeDToD(int count, DToD fun, double x)
{
DateTime start, finish;
double res = 0;
start = DateTime.Now;
for (int i = 1; i < count; i++)
fun(x);
finish = DateTime.Now;
res = (finish - start).ticks;
return res;
}
Используя этот метод, клиенты класса TimeValue без труда могут оценить время работы любого метода, сигнатура которого задается делегатом DToD. Возможности класса легко расширить, определив новые делегаты.
Рассмотрим еще несколько более формальных примеров и начнем с объявления трех делегатов. Поместив два из них в пространство имен, третий вложим непосредственно в создаваемый нами класс QwnDel:
namespace Delegates
{
//объявление классов-делегатов
delegate void Proc(ref int x);
delegate string MesToPers(string s);
class OwnDel
{
public delegate int Fun1(int x);
int Plus1( int x){return(x+100);}//Plus1
int Minus1(int x){return(x-100);}//Minus1
void Plus(ref int x){x+= 100;}
void Minus(ref int x){x-=100;}
//поля класса
public Proc p1;
public Fun1 f1;
char sign;
//конструктор
public OwnDel(char sign)
{
this.sign = sign;
if (sign == '+')
{p1 = new Proc(Plus);f1 = new Fun1(Plus1);}
else
{p1 = new Proc(Minus);f1 = new Fun1(Minus1);}
}
}//class OwnDel
}
Прокомментирую этот текст.
Proc, MesToPers, Fun1. Каждый из них описывает множество функций фиксированной сигнатуры.OwnDel описаны четыре метода: Plus, Minus, Plus1, Minus1, сигнатуры которых соответствуют сигнатурам, задаваемым классами Proc и Fun1.p1 и f1 класса OwnDel являются экземплярами классов Proc и Fun1.p1 и f1 связываются с конкретными методами Plus или Minus, Plus1 или Minus1. Связывание с той или иной функцией в данном случае определяется значением поля sign.Заметьте, поскольку делегаты относятся к ссылочным типам и, соответственно, присваивание является ссылочным присваиванием, то
Приведу теперь метод класса Testing - клиента класса OwnDel, тестирующую работу сервисов этого класса:
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, связанных к моменту вызова с
В нашем примере объявление экземпляров делегатов и связывание их с внутренними методами класса происходило в классе поставщика. Клиенту оставалось лишь вызывать уже созданные экземпляры, но эту работу можно выполнять и на стороне клиентского класса, чем мы сейчас и займемся. Рассмотрим объявление многократно встречавшегося класса Person:
/// <summary>
/// Класс Person с полями name, id, salary, message.
/// Позволяет сравнивать объекты Person
/// по разным критериям.
/// </summary>
class Person
{
//свойства
string name;
int id;
double salary;
string message;
//конструкторы
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 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; }
}
//методы
/// <summary>
/// Передает сообщение объекту
/// </summary>
/// <param name="mes">сообщение</param>
/// <returns>имя с присоединенным сообщением</returns>
public string ToPerson(string mes)
{
this.message = mes;
return string.Format("{0}, {1}",name, message);
}
}//class Person
Класс Person будет использоваться и в других примерах данной лекции. При анализе текста этого класса прошу обратить внимание на метод класса ToPerson, сигнатура которого совпадает с сигнатурой, определенной делегатом MesToPers. Посмотрите, как клиент класса может связать этот метод с экземпляром делегата, определенного самим клиентом:
Person man1 = new Person("Владимир");
MesToPers mestopers = new MesToPers(man1.ToPerson);
Console.WriteLine(mestopers("пора работать!"));
Обратите внимание, что поскольку метод ToPerson не является статическим методом, при связывании необходимо передать и объект, вызывающий метод. Более того, переданный объект становится доступным
В данном примере переданное сообщение "пора работать" присоединится к имени объекта, и результирующая строка будет выдана на печать. В тех случаях, когда метод, связываемый с экземпляром делегата, не использует информацию объекта, этот метод может быть объявлен как статический метод класса. Таким образом, инициализировать экземпляры делегата можно как статическими, так и
Последние три строки были добавлены в вышеприведенную тестирующую процедуру TestOwnDel. Взгляните на результаты ее работы.
(рис 6.1) Объявление делегатов и создание их экземпляровОдно из наиболее важных применений делегатов связано с EvalTimeDToD у нас уже был. Классическим примером является функция вычисления интеграла, у которой один из аргументов задает подынтегральную функцию. Другим примером может служить функция, сортирующая объекты. Аргументом ее является функция Compare, сравнивающая два сложных объекта. В зависимости от того, какая
Давайте более подробно рассмотрим ситуацию с функциями высшего порядка на примере задачи вычисления
//delegate
public delegate double DToD(double x);
/// <summary>
/// Вычисление интеграла.
/// Сигнатура подынтегральной функции
/// определяется делегатом
/// </summary>
public class HighOrderIntegral
{
/// <summary>
/// Вычисление интеграла по методу трапеций
/// </summary>
/// <param name="a">начало интервала интегрирования</param>
/// <param name="b">конец интервала интегрирования</param>
/// <param name="eps">точность вычисления</param>
/// <param name="sif"> подынтегральная функция</param>
/// <returns>значение интеграла</returns>
public double EvalIntegral(double a, double b,
double eps, DToD sif)
{
const int INITIAL_POINTS = 4;
const int MAX_POINTS = 2 << 15;
int n = INITIAL_POINTS;
double I0=0, I1 = I( a, b, n,sif);
for( n *=2; n < MAX_POINTS; n*=2)
{
I0 =I1; I1=I(a,b,n,sif);
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, DToD 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 предназначен для работы с функциями. В него вложено описание уже известного нам функционального класса - делегата DToD, который задает класс функций с одним аргументом типа double, возвращающих значение этого же типа.EvalIntegral - основной метод класса позволяет вычислять DToD.n частей, и вычисляется частичная сумма по n удваивается, и вычисляется новая сумма. Если разность двух приближений по модулю меньше заданной точности eps, то вычисление интеграла заканчивается, иначе процесс повторяется в цикле. Цикл завершается либо по достижении заданной точности, либо когда n достигнет некоторого предельного значения.Чтобы продемонстрировать работу с классом HighOrderIntegral, приведу еще класс Functions, где описано несколько функций, удовлетворяющих контракту, который задан классом DToD:
/// <summary>
/// Статические методы класса задают простые функции
/// </summary>
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()
{
HighOrderIntegral hoi = new HighOrderIntegral();
DToD hoisif1 =
new DToD(Functions.sif1);
double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);
Console.WriteLine("myintegral1 = {0}",myint1);
DToD hoisif2 =
new DToD(Functions.sif2);
myint1= hoi.EvalIntegral(2,3,1e-7,hoisif2);
Console.WriteLine("myintegral2 = {0}",myint1);
}//EvalIntegrals
Здесь создаются два HighOrderIntegral, вызывающий метод вычисления интеграла. Результаты работы показаны на рис. 6.2.
(рис 6.2) Вычисление интеграла с использованием функций высших порядков
Язык C# - молодой, активно развивающийся язык программирования, в каждой версии которого появляются новые средства. Одна из тенденций развития связана с введением анонимности - появлением анонимных методов, анонимных типов данных и соответствующих переменных. Эти средства позволяют уменьшить расстояние между объявлением объекта и его использованием. Когда объект объявляется непосредственно в точке, где он используется, то это делает код более понятным, облегчает его сопровождение, что крайне важно для долгоживущих проектов.
Хороший стиль программирования требует, чтобы локальная переменная объявлялась как можно ближе к точке ее непосредственного использования. А вот с методами и типами данных все немного сложнее.
Давайте обратимся к нашему примеру с вычислением интеграла. У нас был построен отдельный класс Functions, в котором были собраны методы, задающие различные подынтегральные функции. Эти функции можно рассматривать как экземпляры функционального класса, заданного делегатом DToD. Для вычисления интеграла в
HighOrderIntegral hoi = new HighOrderIntegral();
DToD hoisif1 =
new DToD(Functions.sif1);
double myint1 = hoi.EvalIntegral(2,3,1e-7,hoisif1);
Эти строчки позволяли вычислить интеграл от функции с именем sif1 из класса Functions. Все прекрасно, но для понимания того, какова эта функция, приведенных четырех строк недостаточно - необходимо обращаться к классу Functions. Даже если поместить описание функции в класс Testing, то все равно описание функции отделено от ее использования. Анонимные методы позволяют решить эту проблему. Я вначале приведу пример использования анонимного метода, а затем уже рассмотрим общие свойства этих методов. Итак, как решается задача вычисления интеграла с использованием анонимного метода, задающего подынтегральную функцию? Вот пример решения:
public void TestAnonymous()
{
HighOrderIntegral integral = new HighOrderIntegral();
double a = 0, b = Math.PI/2, eps = 1e-7;
double result = integral.EvalIntegral(a, b, eps,
delegate(double x)
{ return Math.Sin(x) + Math.Cos(x); });
Console.WriteLine("Integral(Sin(x) + Cos(x)) " +
"from {0} to {1} = {2} ", a, b, result.ToString());
}
По сути, это те же четыре строчки, дополненные печатью результата. Но! Обратите внимание, подынтегральная функция, в данном случае sin(x) + cos(x), задана с помощью делегата непосредственно в точке вызова явным образом своим описанием! Это анонимная функция - она лишена имени, для нее не указан тип, как подобает приличным объектам языка. Во многом анонимные методы напоминают константы, заданные литералами, которые также заданы своими значениями и тип которых определяется по самому значению.
Давайте теперь подробно разберемся, как это делается. Описание анонимного метода является выражением и может встречаться всюду, где разрешены выражения, - в правой части оператора присваивания в качестве
delegate [(<сигнатура метода>)] <тело метода>
Какому функциональному типу соответствует тип анонимного метода? Нужно сказать, что у анонимного метода нет внутреннего, присущего ему функционального типа. Но для корректного применения анонимного метода его тип должен приводиться к типу делегата, заданного контекстом использования анонимного метода. Поскольку выражение, задающее анонимный метод, может появляться в правой части оператора присваивания или в качестве
Прежде всего поговорим о том, как определяется тип возвращаемого значения анонимного метода, а следовательно, и о том, как реализован анонимный метод - процедурой или функцией. Заметьте, в описание анонимного метода не входит описание возвращаемого методом значения. Компилятор сам определяет, как реализован метод, анализируя его явное описание. Если в описании анонимного метода присутствуют один или несколько операторов return, сопровождаемых возвращаемым значением, то анонимный метод реализован как функция и тип возвращаемого значения определяется типом возвращаемых значений в операторах return. Понятно, что все возвращаемые значения должны быть согласованы по типу, иначе возникнет ошибка периода компиляции. Если операторов return в теле метода нет или они не возвращают значение, то метод реализован как процедура, возвращающая void в качестве результата. Так компилятор может установить соответствие между возвращаемым типом источника и цели.
Когда для анонимного метода не задана сигнатура, то применяется следующее правило установления соответствия типов источника и цели. Анонимный метод соответствует по типу любому делегату с произвольным числом аргументов, если только ни один из аргументов не сопровождается описателями ref или out. Если же функциональный тип формального аргумента или левой части оператора присваивания содержит входные или обновляемые аргументы, то соответствия нет, и это приводит к ошибке периода компиляции.
Когда для анонимного метода задана сигнатура, применятся следующее правило установления соответствия: сигнатуры источника и цели должны совпадать. Заметьте, речь идет о строгом совпадении типов. Не допускается, чтобы тип цели был double, а у источника - int, или тип цели Father, а тип источника Son - потомок Father.
Давайте рассмотрим пример, в котором будут фигурировать разные делегаты и разные анонимные методы, соответствующие делегатам по типам.
class Father { }
class Son : Father { }
delegate string D1(string s, double x);
delegate void D2(string s, out string s1);
delegate string D3(Father item);
public void TestAnonymToDelegate()
{
D1 d1 = delegate {return "OK!";};
D1 d12 = delegate(string s, double x) { return s + x; };
Console.WriteLine(d1("s",5));
Console.WriteLine(d12("12", 3));
string res;
D2 d2 = delegate(string s, out string r){r = s+s;};
d2("Hello ", out res);
Console.WriteLine(res);
D3 d3 = delegate(Father item) { return item.ToString(); };
Console.WriteLine(d3(new Son()));
}
К типу делегата D1 приводятся типы двух анонимных методов с разной сигнатурой. Приведение возможно, поскольку оба анонимных метода возвращают строку в качестве результата, что согласуется с типом возвращаемого значения делегата D1. Анонимный метод, используемый для задания переменной d1, не имеет сигнатуры, что позволяет сопоставить его с типом делегата D1, у которого нет d1 2, полностью совпадает с сигнатурой делегата D1.
Анонимный метод, используемый для задания переменной d2, соответствует по типу делегату D2, требующему, чтобы метод был реализован как процедура и имел out. Аналогично устанавливается соответствие между анонимным методом переменной d3 и типом делегата D3. Естественно, что при вызове d3 в качестве Son, в то время как тип аргумента анонимного метода полностью соответствует типу аргумента делегата D3 и принадлежит родительскому классу Father. Результаты работы тестирующего метода показаны на рис. 6.3.
(рис 6.3) Анонимные методы и делегатыАнонимные методы могут использовать контекст, в котором они появляются. Это означает, что в теле анонимного метода можно применять как локальные переменные и передаваемые по значению аргументы того метода, в котором задается явное описание анонимного метода, так и поля соответствующего класса. Приведу пример подобного анонимного метода. Построим класс, одним из полей которого является массив чисел. В классе есть метод прохода по массиву и выполнение над его элементами некоторого действия, которое определяется функцией, заданной делегатом. Другой метод этого класса конкретизирует обход, задавая функцию анонимным методом. Вот этот класс:
/// <summary>
/// Класс с анонимным методом, использующим контекст
/// </summary>
class WithAnonymous
{
double max;
double[] array;
public WithAnonymous(double max, double[] array)
{
this.max = max; this.array = array;
}
/// <summary>
/// Обход массива
/// </summary>
/// <param name="array"> массив</param>
/// <param name="res"> результат вычислений</param>
/// <param name="action">действие над элементами</param>
void Walk(double[] array, out double res, DToD action )
{
int current = 0; res = 0;
while (current < array.Length)
res += action(array[current++]);
}
/// <summary>
/// Конкретизация обхода анонимным методом
/// </summary>
/// <param name="min">Нижняя граница отбора</param>
/// <param name="N">Число отобранных элементов</param>
/// <param name="res">результат обхода</param>
public void Traverse(double min, out int N, out double res)
{
int n = 0;
Walk(array, out res, delegate(double x) {
if(x > min x < max) {n++; return x;}
else return 0; });
N = n;
}
}
Заметьте, анонимный метод, определенный в методе , использует аргумент min этого метода, локальную переменную n метода , поле max класса WithAnonymous. Локальная переменная для анонимного метода является выходной переменной, и метод изменяет ее значение. Анонимный метод может рассматриваться как
Для полноты картины приведу тестирующий метод из класса Testing:
public void TestWithAnonymous()
{
WithAnonymous wan = new WithAnonymous(20,
new double[] { 17, 23, 12, 45, 7 });
int N;
double res;
wan.Traverse(10, out N, out res);
Console.WriteLine("N= {0}, res = {1}", N, res);
}
Результат, как и следовало ожидать, равен 29.
Анонимные методы прекрасно справляются с поставленной перед ними задачей. Но в C# 3.0 пошли дальше, введя более привычную для математиков форму записи анонимного метода в виде лямбда-выражения. Вместо записи анонимного метода в форме:
delegate [(<сигнатура метода>)] <тело метода>
используется форма, задающая лямбда-выражение:
[(<сигнатура метода>)] => <тело метода>
В методе TestAnonymous анонимный метод определяется следующим образом:
double result = integral.EvalIntegral(a, b, eps,
delegate(double x)
{ return Math.Sin(x) + Math.Cos(x); });
Заменим это определение лямбда-выражением:
double result = integral.EvalIntegral(a, b, eps,
(double x) =>
{ return Math.Sin(x) + Math.Cos(x); });
Результат будет тот же. Приведу теперь аналог метода TestAnonymToDelegate, заменив прежние определения анонимных методов лямбда-выражениями.
public void TestAnonymToLambda()
{
D1 d1 = (s, x) => { return "OK!"; };
D1 d12 = (string s, double x) => { return s + x; };
Console.WriteLine(d1("s", 5));
Console.WriteLine(d12("12", 3));
string res;
D2 d2 = (string s, out string r) => { r = s + s; };
d2("Hello ", out res);
Console.WriteLine(res);
D3 d3 = item => { return item.ToString(); };
Console.WriteLine(d3(new Son()));
}
Обратите внимание: в лямбда-выражениях можно опускать задание типов аргументов, оставив только имена аргументов. В этом случае компилятор попытается вывести их типы из контекста, задающего
Анонимный метод, заданный лямбда-выражением, может также использовать контекст. Приведу аналог метода из класса WithAnonymous:
public void TraverseL(double min, out int N, out double res)
{
int n = 0;
Walk(array, out res, x =>
{
if (x > min x < max) { n++; return x; }
else return 0;
});
N = n;
}
Лямбда-выражения и анонимные типы данных активно используются при работе с запросами к базам данных, так что, надеюсь, к этой теме мы еще вернемся в следующих разделах этого курса. А сейчас продолжим рассмотрение случаев использования функционального типа данных.
Метод "раскрутки" является одним из основных методов функционально-ориентированного построения сложных программных систем. Суть его состоит в том, что программная система создается слоями. Вначале пишется ядро системы - нулевой слой, реализующий базовый набор функций. Затем пишется первый слой с новыми функциями, которые интенсивно вызывают в процессе своей работы функции ядра. Теперь система обладает большим набором функций. Каждый новый слой расширяет функциональность системы. Процесс продолжается, пока не будет достигнута заданная функциональность. На рисунке, изображающем схему построения системы
(рис 6.4) Построение системы методом "раскрутки"Успех языка С и операционной системы Unix во многом объясняется тем, что в свое время компилятор языка и операционная система были написаны на языке С методом раскрутки. В то время существовало множество разных типов компьютеров, каждый из которых имел свою систему команд. Существовала серьезная проблема переноса операционной системы и компилятора с одного типа компьютера на другой. Благодаря раскрутке, ядро, включающее базовые конструкции языка С и операционной системы, писалось на языке ассемблера данного компьютера, а все остальные слои писались на С. Это позволило написать на 95% на языке С транслятор с языка С и операционную систему, что обеспечивало легкий перенос транслятора и операционной системы на компьютеры с разной системой команд.
При построении систем
Пусть F - G функционального типа. Тогда функцию G, задающую параметр (а иногда и саму функцию F ), называют функцией обратного вызова (callback-функцией). Термин вполне понятен. Если в некотором внешнем слое функция Q вызывает функцию внутреннего слоя F, то, предварительно во внешнем слое следует позаботиться о создании функции G, которая и будет передана F. Таким образом, функция Q внешнего слоя вызывает функцию F внутреннего слоя, которая, в свою очередь (обратный вызов), вызывает функцию G внешнего слоя. Чтобы эта техника работала, должен быть задан контракт. Функция высших порядков, написанная во внутреннем слое, задает следующий контракт: "всякая функция, которая собирается меня вызвать, должна передать мне функцию обратного вызова, принадлежащую определенному мной функциональному классу, следовательно, иметь
известную мне сигнатуру".
Наш пример с вычислением интеграла хорошо демонстрирует функции обратного вызова и технику "раскрутки". Можно считать, что класс HighOrderIntegral - это внутренний слой нашей системы. В нем задан делегат, определяющий контракт, и функция EvalIntegral, требующая задания функции обратного вызова в качестве ее параметра. Функция EvalIntegral вызывается из внешнего слоя (клиентами класса), где и определяются -функции. Как показано выше, -функции могут иметь имена и находиться, например, в классе Functions, а могут быть заданы анонимными методами или, что еще проще, лямбда-выражениями.
Многие из функций операционной системы Windows, входящие в состав Win API 32, требуют при своем вызове задания -функций. Примером может служить работа с объектом операционной системы Timer. Конструктор этого объекта является -функцию, вызываемую для обработки событий, которые поступают от таймера.
Пример работы с таймером приводить сейчас не буду, ограничусь лишь сообщением Timer:
public Timer(TimerCallback callback,object state,
int dueTime, int period);
Первым параметром конструктора является функция обратного вызова , которая принадлежит функциональному классу TimerCallback, заданному делегатом:
public delegate void TimerCallback(object state);
Сегодня многие программные системы проектируются и разрабатываются не в функциональном, а в объектно-ориентированном стиле. Такая система представляет собой одно или несколько семейств интерфейсов и классов, связанных
О полиморфизме говорилось достаточно много в предыдущих лекциях. Тем не менее позволю напомнить суть дела. Родитель может объявить виртуальный метод F и вызывать этот метод в некотором методе Q, не обязательно виртуальном. Потомку разрешается переопределить реализацию виртуального метода, но он не имеет права изменять его сигнатуру. Когда потомок вызывает родительский метод Q, благодаря полиморфизму, при работе этого метода будет вызван не метод родителя F, а метод F, реализованный потомком.
Ситуация в точности напоминает раскрутку и вызов обратных функций. Родительский метод Q находится во внутреннем слое, а потомок с его методом F определен во внешнем слое. Когда потомок вызывает метод Q из внутреннего слоя, тот, в свою очередь, вызывает метод F из внешнего слоя. Сигнатура вызываемого метода F в данном случае задается не делегатом, а сигнатурой виртуального метода, которую, согласно контракту, потомок не может изменить.
Давайте вернемся к задаче вычисления интеграла и создадим реализацию, основанную на наследовании и полиморфизме. Идея примера такова. Вначале построим родительский класс, один из методов которого будет вычислять интеграл от некоторой подынтегральной функции. Сама функция известна в классе, и ее вычисление реализовано методом класса, который сделаем виртуальным. Теперь представим себе, что необходимо посчитать интеграл от другой подынтегральной функции. Заново писать метод вычисления интеграла не хочется, хорошая его реализация достаточно сложна. Как воспользоваться уже написанным методом готового класса? Можно было бы внести изменения в исходный класс, заменив одну функцию на другую. Но так поступать нельзя, поскольку класс используется многими клиентами и он "закрыт" для изменений. Правильный путь состоит в том, чтобы "открыть" класс, создав его потомка, который наследует метод вычисления интеграла, но сможет переопределить виртуальный метод, задающий подынтегральную функцию. Приведу пример кода, следующего этой схеме:
/// <summary>
/// Класс, в котором определен метод вычисления интеграла
/// и виртуальный метод, задающий подынтегральную функцию
/// </summary>
class FIntegral
{
/// <summary>
/// Вычисление интеграла методом трапеции
/// </summary>
/// <param name="a">нижний предел интегрирования</param>
/// <param name="b">верхний предел интегрирования</param>
/// <param name="eps">точность вычисления</param>
/// <returns>значение интеграла</returns>
public double EvaluateIntegral(double a, double b, double eps)
{
const int INITIAL_POINTS = 4;
const int MAX_POINTS = 2 << 15;
int n = INITIAL_POINTS;
double I0=0, I1 = I( a, b, n);
for (n *= 2; n < MAX_POINTS; 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 нет параметра функционального типа. Вместо этого тут же в классе определен защищенный виртуальный метод, задающий конкретную подынтегральную функцию. В качестве таковой выбрана самая простая функция, тождественно равная единице.
Для вычисления интеграла от произвольной функции единственное, что нужно сделать, - это задать
class FIntegralSon:FIntegral
{
protected override double sif(double x)
{
double a = 1.0; double b = 2.0; double c= 3.0;
return (double)(a*x*x +b*x +c);
}
}//FIntegralSon
Принципиально задача решена. Осталось только написать фрагмент кода, запускающий вычисления. Он оформлен в виде метода класса Testing:
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
Взгляните на результаты вычислений.
(рис 6.5) Вычисление интеграла, использующее полиморфизмВ наших примерах рассматривалась ситуация, при которой в некотором классе объявлялись функции, удовлетворяющие контракту с делегатом, но создание экземпляров делегата и их инициирование функциями класса выполнялось в другом месте, там, где предполагалось вызывать соответствующие функции. Чаще всего создание экземпляров удобнее возложить на класс, создающий требуемые функции. Более того, в этом классе делегат можно объявить как свойство класса, что позволяет "убить двух зайцев". Во-первых, с пользователей класса снимается забота создания делегатов. Во-вторых,
Рассмотрим пример, демонстрирующий и поясняющий эту возможность при работе с Person и введем новый класс Persons:
Person определим различные реализации функции Compare с одной и той же сигнатурой, позволяющие сравнивать два объекта по имени, по номеру, по зарплате, по нескольким полям. Самое интересное, ради чего и строится данный пример: для каждой реализации Compare будет построена процедура-свойство, которая "на лету" создает Compare ;Persons будет играть роль Person. В этом классе будут определены операции над объектами контейнера. Среди операций нас, прежде всего, будет интересовать сортировка объектов, реализованная в виде Compare из класса Person будут принадлежать этому классу.Теперь, когда задача ясна, приступим к ее реализации. Класс Person дополним до нужной функциональности.
Заметьте, можно было бы объявить класс Person наследником интерфейса IComparable, реализовать в классе метод Compare этого интерфейса, перегрузить знаки операций сравнения, что позволяло бы сравнивать персоны по одному критерию. Но в данном случае это было бы методологически неправильно, поскольку персоны, как и другие сложные объекты, следует сравнивать в зависимости от обстоятельств по разным критериям.
Как же следует организовать класс, чтобы обеспечить работу с разными методами сравнения? Для каждого способа сравнения введем статический метод-свойство, создающий и возвращающий в качестве результата
//делегат
delegate int CompareItems(Person obj1, Person obj2);
Сами методы-свойства зададим, используя лямбда-выражения для определения сравнения по заданному критерию:
//делегаты как свойства
public static CompareItems SortByName
{
get
{
return (person1, person2) =>
{
return string.Compare(person1.name, person2.name);
};
}
}
public static CompareItems SortById
{
get {
return (person1, person2) =>
{
if (person1.Id > person2.Id) return 1;
else return -1;
};
}
}
public static CompareItems SortBySalary
{
get
{
return (person1, person2) =>
{
if (person1.salary > person2.salary) return 1;
else return -1;
};
}
}
public static CompareItems SortBySalaryName
{
get
{
return (person1, person2) =>
{
if (person1.salary > person2.salary) return 1;
else if (person1.salary < person2.salary) return -1;
else return string.Compare(person1.name, person2.name);
};
}
}
Всякий раз, когда будет запрошено, например, свойство SortByName класса Person, будет возвращен объект функционального класса CompareItems. Объект будет создаваться динамически в момент запроса.
Класс Person полностью определен, и теперь давайте перейдем к определению контейнера, содержащего объекты Person:
/// <summary>
/// Контейнер объектов Person
/// Построен на массиве
/// </summary>
class Persons
{
int n = 100; //емкость контейнера
int freeItem; //свободный для заполнения элемент
private Person[] persons; //контейнер
//конструкторы
public Persons()
{
n = 100; freeItem = 0;
persons = new Person[n];
}
public Persons(int n)
{
this.n = n; freeItem = 0;
persons = new Person[n];
}
/// <summary>
/// Индексатор.
/// Доступ к элементам контейнера по индексу
/// </summary>
/// <param name="num">индекс</param>
/// <returns>элемент с заданным индексом</returns>
public Person this[int num]
{
get { return(persons[num-1]); }
set { persons[num-1] = value; }
}
/// <summary>
/// Добавление элементов в контейнер
/// </summary>
/// <param name="pers">добавляемый элемент</param>
/// <returns>true при успешном добавлении</returns>
public bool AddPerson(Person pers)
{
if(freeItem < n)
{
Person p = new Person(pers);
persons[freeItem++]= p;
return true;
}
else return false;
}
/// <summary>
/// Заглушка.
/// Добавление в контейнер 6 фиксированных элементов
/// </summary>
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));
}//LoadPersons
/// <summary>
/// Вывод на консоль
/// </summary>
public void PrintConsolePersons()
{
for(int i =0; i<freeItem; i++)
{
Console.WriteLine("{0,10} {1,5} {2,5}",
persons[i].Name, persons[i].Id, persons[i].Salary);
}
}//PrintConsolePersons
/// <summary>
/// Сортировка контейнера методом пузырька
/// Критерий сортировки задается функцией compare
/// </summary>
/// <param name="compare">
/// функция сравнения элементов контейнера</param>
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
Приведу краткие комментарии к этому тексту. LoadPerson и PrintConsolePersons добавлены в интересах отладки.
Две основные операции определены над контейнером - добавление нового элемента и сортировка. Код метода сортировки следует внимательно проанализировать. У метода сортировки есть аргумент типа CompareItems, и при вызове метода сортировки клиент может передать в качестве Person.
На этом проектирование классов закончено, нужная цель достигнута, показано, как можно в классе Testing тестирующую процедуру, в которой будут использоваться различные критерии сортировки:
public void TestSortPersons()
{
Persons persons = new Persons(6);
persons.LoadPersons();
Console.WriteLine (" Сортировка по имени: ");
persons.SimpleSortPerson(Person.SortByName);
persons.PrintConsolePersons();
Console.WriteLine (" Сортировка по идентификатору: ");
persons.SimpleSortPerson(Person.SortById);
persons.PrintConsolePersons();
Console.WriteLine (" Сортировка по зарплате: ");
persons.SimpleSortPerson(Person.SortBySalary);
persons.PrintConsolePersons();
Console.WriteLine (" Сортировка по зарплате и имени: ");
persons.SimpleSortPerson(Person.SortBySalaryName);
persons.PrintConsolePersons();
}//SortPersons
Заметьте, клиент не создает никаких экземпляров делегата, он просто при вызове метода сортировки передает методу нужное свойство класса Person.
Вот как выглядят результаты работы сортировки данных.
(рис 6.6) Сортировка данныхДавайте просуммируем то, что уже известно о функциональном типе данных. Ключевое слово delegate позволяет задать определение функционального типа (класса), фиксирующее контракт, которому должны удовлетворять все функции, принадлежащие классу. Функциональный класс можно рассматривать как ссылочный тип, экземпляры которого являются ссылками на функции. Заметьте, ссылки на функции - это безопасные по типу указатели, которые ссылаются на функции с жестко фиксированной сигнатурой, заданной делегатом. Следует также понимать, что это не простая ссылка на функцию. В том случае, когда X, вызвавший этот метод.
Вместе с тем, объявление функционального типа не укладывается в синтаксис, привычный для C#. Хотелось бы писать, как принято:
Delegate FType = new Delegate(<определение типа>)
Но так объявлять переменные этого класса нельзя, и стоит понять, почему. Есть ли вообще класс Delegate? Ответ положителен - есть такой класс. При определении функционального типа, например:
public delegate int FType(int X);
переменная FType принадлежит классу Delegate. Почему же ее нельзя объявить привычным образом? Дело не только в синтаксических особенностях этого класса. Дело в том, что класс Delegate является абстрактным классом. Вот его объявление:
public abstract class Delegate: ICloneable, ISerializable
Для абстрактных классов реализация не определена, и это означает, что нельзя создавать экземпляры класса. Класс Delegate служит базовым классом для классов-наследников. Но создавать наследников могут только компиляторы и системные программы, этого нельзя сделать в программе на C#. Именно поэтому введено ключевое слово delegate, которое косвенно позволяет работать с классом Delegate, создавая уже не абстрактный, а реальный класс. Заметьте, при этом все динамические и статические методы класса Delegate становятся доступными программисту.
Трудно, кажется, придумать, что можно делать с делегатами, задающими класс функций. Однако у них есть одно замечательное свойство - функции можно комбинировать. Представьте себе, что существует список работ, которые нужно выполнять, в зависимости от обстоятельств, в разных комбинациях. Если функции, выполняющие отдельные работы, принадлежат одному классу, то для решения задачи можно использовать технику их комбинирования.
Замечу, что возможность комбинирования появилась у делегатов в первую очередь для поддержания работы с событиями. Когда возникает некоторое событие, то сообщение о нем посылается разным объектам, каждый из которых по-своему обрабатывает событие. Реализуется эта возможность на основе комбинирования обработчиков событий.
В чем суть комбинирования? Она прозрачна. К
Понятно, что если есть операция присоединения функций, то должна быть и обратная операция, позволяющая удалять функции из списка вызовов.
Рассмотрим основные методы и свойства класса Delegate. Начнем с двух статических методов - Combine и Remove. Первый из них присоединяет
Combine(del1, del2) Remove(del1, del2)
Аргументы del1 и del2 должны быть одного функционального класса. При добавлении del2 в список, в котором del2 уже присутствует, будет добавлен второй экземпляр. При попытке удаления del2 из списка, в котором del2 нет, Remove благополучно завершит работу, не выдавая сообщения об ошибке.
Класс Delegate относится к неизменяемым классам, поэтому оба метода возвращают ссылку на нового делегата. Возвращаемая ссылка принадлежит родительскому классу Delegate, поэтому ее необходимо явно преобразовать к нужному типу, которому принадлежат del1 и del2. Обычное использование этих методов имеет вид:
del1 = (<type>) Combine(del1, del2); del1 = (<type>) Remove(del1, del2);
Метод GetInvocationList является foreach, поочередно получая элементы списка. Чуть позже появится пример, поясняющий необходимость подобной работы со списком.
Два Method и Target полезны для получения подробных сведений о делегате. Чаще всего они используются в процессе отражения, когда делегат поступает извне и необходима метаинформация, поставляемая с делегатом. Свойство Method возвращает объект класса MethodInfo из пространства имен . Свойство Target возвращает информацию о цели - объекте, вызвавшем делегата, в тех случаях, когда делегат инициируется не статическим методом класса, а динамическим, связанным с вызвавшим его объектом.
У класса Delegate, помимо методов, наследуемых от класса object, есть еще несколько методов, но мы на них останавливаться не будем, они используются не столь часто.
Наряду с методами, над делегатами определены и две операции: "+" и "-", которые являются более простой формой записи добавления в Combine и Remove. Выше написанные присваивания объекту del1 с помощью этих операций могут быть переписаны в виде:
del1 +=del2; del1 -=del2;
Как видите, запись становится проще, исчезает необходимость в задании явного приведения к del1 и del2, естественно, остаются те же, что и для методов Combine и Remove.
Рассмотрим следующую ситуацию. Пусть есть городские службы: милиция, скорая помощь, пожарные. Каждая из служб по-своему реагирует на события, происходящие в городе. Построим примитивную модель жизни города, в котором происходят события, и сообщения о них посылаются службам. В последующей лекции эта модель будет развита. Сейчас она носит формальный характер, демонстрируя, главным образом, работу с делегатами, заодно поясняя ситуации, в которых разумно комбинирование делегатов.
Начнем с построения класса с именем Combination. Этот класс будет моделировать работу городских служб, реагирующих на события, происходящие в городе. Каждая служба будет описываться статическим методом. Сигнатуры всех методов будут одинаковыми и будут соответствовать ранее введенному делегату MesToPers. На входе они будут получать сообщение о событии, а на выходе - выдавать результат своих действий.
Работу с сервисами этого класса построим по описанной ранее технологии. Сами методы будут закрыты для клиентов, а доступ к ним осуществляться через свойства, на лету создающие требуемый
/// <summary>
/// Класс, статические методы которого
/// моделируют работу городских служб
/// </summary>
class Combination
{
// Закрытые сервисы класса
/// <summary>
/// Модель действий милиции
/// </summary>
/// <param name="mes">
/// сообщение о возникшем событии</param>
/// <returns>результат действий</returns>
static string policeman(string mes)
{
if(mes == "Пожар!")
return(mes + " Милиция ищет виновных!");
else
return(mes +" Милиция здесь!");
}
/// <summary>
/// Модель действий скорой помощи
/// </summary>
/// <param name="mes">
/// </param>сообщение о возникшем событии
/// <returns>результат действий</returns>
static string ambulanceman(string mes)
{
if(mes == "Пожар!")
return(mes + " Скорая спасает пострадавших!");
else
return(mes + " Скорая помощь здесь!");
}
/// <summary>
/// Модель действий пожарных
/// </summary>
/// <param name="mes">сообщение о возникшем событии
/// сообщение о возникшем событии</param>
/// <returns>результат действий</returns>
static string fireman(string mes)
{
if(mes =="Пожар!")
return(mes + " Пожарные тушат пожар!");
else
return( mes + " Пожарные здесь!");
}
//доступ к сервисам через свойства
public static MesToPers Policeman
{
get {return (new MesToPers(policeman));}
}
public static MesToPers Fireman
{
get {return (new MesToPers(fireman));}
}
public static MesToPers Ambulanceman
{
get {return (new MesToPers(ambulanceman));}
}
}//class Combination
Службы у нас есть, покажем, как с ними можно работать. С этой целью добавим в класс Testing, где проводятся различные эксперименты, следующую процедуру:
public void TestSomeServices()
{
MesToPers Comb;
Comb = (MesToPers)Delegate.Combine(Combination.Ambulanceman,
Combination.Policeman);
Comb = (MesToPers)Delegate.Combine(Comb,Combination.Fireman);
Console.WriteLine(Comb("Пожар!"));
Вначале объявляется Comb, которой в следующем операторе присваивается ссылка на Combine, чей Ambulanceman и Policeman. Затем к списку вызовов экземпляра Comb присоединяется новый кандидат Fireman. При вызове объекта Comb ему передается сообщение "Пожар!". В результате вызова Comb поочередно запускаются все три экземпляра, входящие в список, каждому из которых передается сообщение.
Этот пример очевидно демонстрирует недостаток предлагаемого механизма, который следует учитывать в реальной работе. При вызове Comb поочередно будут вызываться методы из списка вызовов. Это прекрасно. Но если, как в нашем случае, методы из списка являются функциями, возвращающими значение, то в результате работы все значения, кроме последнего, будут утеряны и на печать будет послано сообщение о результатах работы последней отработавшей функции. Этого эффекта можно избежать, если все методы реализованы как процедуры.
Давайте теперь начнем поочередно отключать делегатов, вызывая затем Comb с новыми сообщениями:
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Fireman);
//Такое возможно: попытка отключить не существующий элемент
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Special);
Console.WriteLine(Comb("Через 30 минут!"));
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Policeman);
Console.WriteLine(Comb("Через час!"));
Comb = (MesToPers)Delegate.Remove(Comb,Combination.Ambulanceman);
//Console.WriteLine(Comb("Через два часа!")); // Comb не определен
В этом фрагменте поочередно отключаются разные службы - милиция, скорая помощь, пожарные, и каждый раз вызывается Comb. После последнего отключения, когда Comb приводит к ошибке, поэтому оператор вызова закомментирован.
Покажем теперь, что ту же работу можно выполнить, используя Delegate:
//операции + и -
Comb = Combination.Ambulanceman;
Console.WriteLine( Comb.Method.Name);
Comb+= Combination.Fireman;
Comb+= Combination.Policeman;
Console.WriteLine(Comb("День города!"));
Comb -= Combination.Ambulanceman;
Comb -= Combination.Fireman;
Console.WriteLine(Comb("На следующий день!"));
}//TestSomeServices
Обратите внимание, здесь демонстрируется вызов свойства Method, возвращающее объект, свойство Name которого выводится на печать. Пора уже взглянуть на результаты, порожденные работой этой процедуры.
(рис 6.7) Службы городаКак быть, если в списке вызовов есть "плохой" экземпляр, при вызове которого возникает ошибка, приводящая к выбрасыванию исключительной ситуации? Тогда стоящие за ним в очереди экземпляры не будут вызваны, хотя они вполне могли бы выполнить свою часть работы. В этом случае полезно использовать метод GetInvocationList и самому клиенту организовать в цикле поочередный вызов сервисов из списка. Вызов метода следует поместить в
Добавим в класс Combination специальную службу, которая при получении сообщения о пожаре выбрасывает исключение:
//метод, вызывающий исключительную ситуацию
/// <summary>
/// Модель действий специальной службы
/// </summary>
/// <param name="mes">
/// сообщение о возникшем событии</param>
/// <returns>результат действий</returns>
static string specialService(string mes)
{
if (mes == "Пожар!")
throw (new Exception("Специальная служба: Кто виноват?"));
return (mes + " Специальная служба здесь!");
}
public static MesToPers Special
{
get { return (new MesToPers(specialService)); }
}
Протестируем ситуацию, в которой возникает исключительная ситуация в процессе вызова одного из методов списка вызовов. Заодно покажем, как, используя метод GetInvocationList, справиться с этой проблемой, так же как и с проблемой потери возвращаемых методами значений.
public void TestBadService()
{
MesToPers Comb;
Comb = Combination.Ambulanceman;
Comb += Combination.Fireman;
Comb += Combination.Special;
Comb += Combination.Policeman;
foreach(MesToPers currentJob in Comb.GetInvocationList())
{
try
{
Console.WriteLine(currentJob("Пожар!"));
}
catch(Exception e)
{
Console.WriteLine(e.Message);
Console.WriteLine(currentJob.Method.Name);
}
}
}//BadService
Поясню, как будет работать эта процедура при ее вызове. Вначале две службы нормально отработают, но при вызове третьей службы возникнет исключительная ситуация. Exception перехватит эту ситуацию и напечатает как свойство Message объекта e, так и имя метода, вызвавшего исключительную ситуацию, используя свойство Method объекта, вызвавшего ситуацию. После завершения работы блока обработчика ситуации выполнение программы продолжится, выполнится следующий шаг цикла, и милицейская служба благополучно выполнит свою работу. Вот результаты вывода.
(рис 6.8) "Специальная служба"Заметьте, теперь в цикле можно проследить за результатом выполнения каждого метода из списка вызовов, так что ни одно из возвращаемых значений не будет утеряно. Этот пример демонстрирует корректный способ работы со списком вызова.
Разговор о делегатах еще не закончен. Он будет продолжен в следующей лекции, в которой рассмотрим классы с событиями. События основываются на делегатах.
X вычислять некоторую характеристику result, заданную выражением:
Метод, вычисляющий значение характеристики result, реализуйте как MyMath, являющийся аналогом класса Math из библиотеки FCL. Задайте в этом клаccе общий метод, позволяющий вычислять различные функции ( sin, cos и другие). Реализуйте этот метод как ak по соответствующей рекуррентной формуле.Functions. Класс должен выполнять различные операции над функциями, принадлежащими функциональному типу, заданному делегатом. В число операций класса включите такие методы, как вычисление интеграла, нахождение максимума, минимума, корней функции на заданном интервале, вычисление значения производной функции в заданной точке. В классе-клиенте, использующем сервисы класса Fnctions, используйте анонимные методы и лямбда-выражения. В интерфейсе проекта предусмотрите возможность задания функции как выражением, так и выбором из фиксированного списка.Person и Sorting. В классе Sorting предусмотрите разные методы сортировки, заданные Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.