Опубликован: 02.12.2009 | Уровень: специалист | Доступ: платный | ВУЗ: Тверской государственный университет
Лекция 7:

Делегаты. Функциональный тип данных

< Лекция 6 || Лекция 7: 123456 || Лекция 8 >

Анонимные методы и контекст

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

/// <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;
        }
    }

Заметьте, анонимный метод, определенный в методе Traverse, использует аргумент min этого метода, локальную переменную n метода Traverse, поле 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()));
 }

Обратите внимание: в лямбда-выражениях можно опускать задание типов аргументов, оставив только имена аргументов. В этом случае компилятор попытается вывести их типы из контекста, задающего тело метода. Для облегчения его работы полезно явно задавать сигнатуру метода, как это и сделано в некоторых из приведенных примеров. Стоит ли говорить, что результаты работы этого метода эквиваленты тем, что получены при старых определениях.

Анонимный метод, заданный лямбда-выражением, может также использовать контекст. Приведу аналог метода Traverse из класса 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 вызывается из внешнего слоя (клиентами класса), где и определяются callback -функции. Как показано выше, callback -функции могут иметь имена и находиться, например, в классе Functions, а могут быть заданы анонимными методами или, что еще проще, лямбда-выражениями.

Многие из функций операционной системы Windows, входящие в состав Win API 32, требуют при своем вызове задания callback -функций. Примером может служить работа с объектом операционной системы Timer. Конструктор этого объекта является функцией высшего порядка, и ей в момент создания объекта необходимо в качестве параметра передать callback -функцию, вызываемую для обработки событий, которые поступают от таймера.

Пример работы с таймером приводить сейчас не буду, ограничусь лишь сообщением синтаксиса объявления конструктора объекта Timer:

public Timer(TimerCallback callback,object state,
     int dueTime, int period);

Первым параметром конструктора является функция обратного вызова callback, которая принадлежит функциональному классу TimerCallback, заданному делегатом:

public delegate void TimerCallback(object state);
< Лекция 6 || Лекция 7: 123456 || Лекция 8 >
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Дмитрий Штаф
Дмитрий Штаф
Россия
Дмитрий Слапогузов
Дмитрий Слапогузов
Россия, Бийск