Перейти к содержимому

Делегат как параметр метода c

  • автор:

Использование делегатов (Руководство по программированию на C#)

Делегат — это тип, который безопасно инкапсулирует метод, схожий с указателем функции в C и C++. В отличие от указателей функций в C делегаты объектно-ориентированы, типобезопасны и безопасны. Тип делегата задается его именем. В следующем примере объявляется делегат с именем Callback , который может инкапсулировать метод, использующий в качестве аргумента значение string и возвращающий значение void:

public delegate void Callback(string message); 

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

// Create a method for a delegate. public static void DelegateMethod(string message)
// Instantiate the delegate. Callback handler = DelegateMethod; // Call the delegate. handler("Hello World"); 

Типы делегатов являются производными от класса Delegate в .NET. Типы делегатов являются запечатанными — от них нельзя наследовать, а от Delegate нельзя создавать производные пользовательские классы. Так как экземпляр делегата является объектом, его можно передать в качестве аргумента или назначить свойству. Это позволяет методу принимать делегат в качестве параметра и вызывать делегат в дальнейшем. Эта процедура называется асинхронным обратным вызовом и обычно используется для уведомления вызывающего объекта о завершении длительной операции. Когда делегат используется таким образом, коду, использующему делегат, не требуются сведения о реализации используемого метода. Данные функциональные возможности аналогичны возможностям, предоставляемым интерфейсами инкапсуляции.

Обратный вызов также часто используется для задания настраиваемого метода сравнения и передачи этого делегата в метод сортировки. Это позволяет сделать коду вызывающего объекта частью алгоритма сортировки. В следующем примере метод использует тип Del тип в качестве параметра.

public static void MethodWithCallback(int param1, int param2, Callback callback)

Затем в данный метод можно передать созданный ранее делегат:

MethodWithCallback(1, 2, handler); 

и получить следующие выходные данные в окне консоли:

The number is: 3 

При использовании делегата в качестве абстракции методу MethodWithCallback не нужно выполнять непосредственный вызов консоли, то есть его можно создавать без учета консоли. Метод MethodWithCallback просто подготавливает строку и передает ее в другой метод. Это очень удобно, так как делегируемый метод может использовать любое количество параметров.

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

public class MethodClass < public void Method1(string message) < >public void Method2(string message) < >> 

Вместе с рассмотренным ранее статическим методом DelegateMethod есть три метода, для которых можно создать оболочку с помощью экземпляра Del .

При вызове делегат может вызывать сразу несколько методов. Это называется многоадресностью. Чтобы добавить в список методов делегата (список вызова) дополнительный метод, необходимо просто добавить два делегата с помощью оператора сложения или назначения сложения («+» или «+ lang-csharp» name=»csProgGuideDelegates#27″>var obj = new MethodClass(); Callback d1 = obj.Method1; Callback d2 = obj.Method2; Callback d3 = DelegateMethod; //Both types of assignment are valid. Callback allMethodsDelegate = d1 + d2; allMethodsDelegate += d3;

На данном этапе список вызова делегата allMethodsDelegate содержит три метода — Method1 , Method2 и DelegateMethod . Три исходных делегата d1 , d2 и d3 остаются без изменений. При вызове allMethodsDelegate все три метода вызываются по порядку. Если делегат использует параметры, передаваемые по ссылке, эта ссылка передается после каждого из трех методов, а все изменения одного из методов становятся видны в следующем методе. Если любой из методов вызывает неперехваченное исключение, это исключение передается в вызывающий делегат объект, а последующие методы в списке вызова не вызываются. Если делегат имеет возвращаемое значение и (или) выходные параметры, он возвращает возвращаемое значение и параметры последнего вызванного метода. Чтобы удалить метод из списка вызовов, используйте вычитание или операторы присваивания вычитания ( — или -= ). Например:

//remove Method1 allMethodsDelegate -= d1; // copy AllMethodsDelegate while removing d2 Callback oneMethodDelegate = allMethodsDelegate - d2; 

Поскольку типы делегата являются производными от System.Delegate , в делегате можно вызывать методы и свойства, определенные этим классом. Например, чтобы определить число методов в списке вызова делегата, можно использовать код:

int invocationCount = d1.GetInvocationList().GetLength(0); 

Делегаты, в списке вызова которых находятся несколько методов, является производным от MulticastDelegate, являющегося подклассом класса System.Delegate . Приведенный выше код работает в любом из случаев, так как оба класса поддерживают GetInvocationList .

Групповые делегаты часто используются при обработке событий. Объекты источников событий отправляют уведомления объектам получателей, зарегистрированным для получения данного события. Чтобы зарегистрироваться для получения события, объект получателя создает метод, предназначенный для обработки этого события, затем создает делегат для этого метода и передает его в источник события. Когда происходит событие, источник вызывает делегат. После этого делегат вызывает в объекте получателя обработки события, предоставив ему данные события. Тип делегата для данного события задается источником события. Дополнительные сведения см. в разделе События.

Назначение сравнения делегатов двух различных типов во время компиляции вызовет ошибку компиляции. Если экземпляры делегата статически относятся к типу System.Delegate , сравнение допустимо, но во время выполнения будет возвращено значение false. Например:

delegate void Callback1(); delegate void Callback2(); static void method(Callback1 d, Callback2 e, System.Delegate f) < // Compile-time error. //Console.WriteLine(d == e); // OK at compile-time. False if the run-time type of f // is not the same as that of d. Console.WriteLine(d == f); >

См. также

  • Руководство по программированию на C#
  • Делегаты
  • Использование вариативности в делегатах
  • Вариативность в делегатах
  • Использование вариативности в универсальных методах-делегатах Func и Action
  • События

Совместная работа с нами на GitHub

Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.

Делегаты

Делегат представляет собой объект, который может ссылаться на метод. Следовательно, когда создается делегат, то в итоге получается объект, содержащий ссылку на метод. Более того, метод можно вызывать по этой ссылке. Иными словами, делегат позволяет вызывать метод, на который он ссылается.

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

  • адрес метода, на котором он вызывается;
  • аргументы (если есть) этого метода;
  • возвращаемое значение (если есть) этого метода.

Как только делегат создан и снабжен необходимой информацией, он может динамически вызывать методы, на которые указывает, во время выполнения. Каждый делегат в .NET Framework (включая специальные делегаты) автоматически снабжается способностью вызывать свои методы синхронно или асинхронно. Этот факт значительно упрощает задачи программирования, поскольку позволяет вызывать метод во вторичном потоке выполнения без ручного создания и управления объектом Thread.

Определение типа делегата в C#

Тип делегата объявляется с помощью ключевого слова delegate. Ниже приведена общая форма объявления делегата:

delegate возвращаемый_тип имя (список_параметров);

где возвращаемый_тип обозначает тип значения, возвращаемого методами, которые будут вызываться делегатом; имя — конкретное имя делегата; список_параметров — параметры, необходимые для методов, вызываемых делегатом. Как только будет создан экземпляр делегата, он может вызывать и ссылаться на те методы, возвращаемый тип и параметры которых соответствуют указанным в объявлении делегата.

Самое главное, что делегат может служить для вызова любого метода с соответствующей сигнатурой и возвращаемым типом. Более того, вызываемый метод может быть методом экземпляра, связанным с отдельным объектом, или же статическим методом, связанным с конкретным классом. Значение имеет лишь одно: возвращаемый тип и сигнатура метода должны быть согласованы с теми, которые указаны в объявлении делегата.

Давайте рассмотрим пример:

using System; namespace ConsoleApplication1 < // Создадим делегат delegate int IntOperation (int i, int j); class Program < // Организуем ряд методов static int Sum(int x, int y) < return x + y; >static int Prz(int x, int y) < return x * y; >static int Del(int x, int y) < return x / y; >static void Main() < // Сконструируем делегат IntOperation op1 = new IntOperation(Sum); int result = op1(5, 10); Console.WriteLine("Сумма: " + result); // Изменим ссылку на метод op1 = new IntOperation(Prz); result = op1(5, 10); Console.WriteLine("Произведение: " + result); Console.ReadLine(); >> > 

Создание делегатов

Главный вывод из данного примера заключается в следующем: в тот момент, когда происходит обращение к экземпляру делегата IntOperation, вызывается метод, на который он ссылается. Следовательно, вызов метода разрешается во время выполнения, а не в процессе компиляции.

Базовые классы System.MulticastDelegate и System.Delegate

Когда компилятор C# обрабатывает тип делегата, он автоматически генерирует запечатанный (sealed) класс, унаследованный от System.MulticastDelegate. Этот класс (в сочетании с его базовым классом System.Delegate) предоставляет необходимую инфраструктуру для делегата, чтобы хранить список методов, подлежащих вызову в более позднее время. Например, если просмотреть делегат IntOperation из предыдущего примера с помощью утилиты ildasm.exe, обнаружится класс, показанный на рисунке:

Как видите, сгенерированный компилятором класс IntOperation определяет три общедоступных метода. Invoke() — возможно, главный из них, поскольку он используется для синхронного вызова каждого из методов, поддерживаемых объектом делегата; это означает, что вызывающий код должен ожидать завершения вызова, прежде чем продолжить свою работу. Может показаться странным, что синхронный метод Invoke() не должен вызываться явно в коде C#. Invoke() вызывается «за кулисами», когда применяется соответствующий синтаксис C#.

Методы BeginInvoke() и EndInvoke() предлагают возможность вызова текущего метода асинхронным образом, в отдельном потоке выполнения. Имеющим опыт в многопоточной разработке должно быть известно, что одной из основных причин, вынуждающих разработчиков создавать вторичные потоки выполнения, является необходимость вызова методов, которые требуют определенного времени для завершения. Хотя в библиотеках базовых классов .NET предусмотрено целое пространство имен, посвященное многопоточному программированию (System.Threading), делегаты предлагают эту функциональность в готовом виде.

Ниже показаны некоторые избранные методы System.MulticastDelegate:

public abstract class MulticastDelegate : Delegate < // Возвращает список методов, на которые "указывает" делегат. public sealed override Delegate[] GetlnvocationList (); // Перегруженные операции. public static bool operator ==(MulticastDelegate d1, MulticastDelegate d2) ; public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2) ; // Используются внутренне для управления списком методов, поддерживаемых делегатом. private IntPtr _invocationCount; private object _invocationList; >

Класс System.MulticastDelegate получает дополнительную функциональность от своего родительского класса System.Delegate. Ниже показан фрагмент определения класса:

public abstract class Delegate : ICloneable, ISerializable < // Методы Для взаимодействия со списком функций. public static Delegate Combine(params Delegate[] delegates); public static Delegate Combine(Delegate a, Delegate b) ; public static Delegate Remove(Delegate source, Delegate value); public static Delegate RemoveAll(Delegate source, Delegate value); // Перегруженные операции. public static bool operator ==(Delegate d1, Delegate d2) ; public static bool operator != ( Delegate d1, Delegate d2) ; // Свойства, показывающие цель делегата. public Methodlnfo Method < get; >public object Target < get; >>

Запомните, что вы никогда не сможете напрямую наследовать от этих базовых классов в коде (при попытке сделать это выдается ошибка компиляции). Тем не менее, при использовании ключевого слова delegate неявно создается класс, который «является» MulticastDelegate. В следующей таблице описаны основные члены, общие для всех типов делегатов:

Методы и свойства делегатов C#

Член Назначение
Method Это свойство возвращает объект System.Reflection.Method, который представляет детали статического метода, поддерживаемого делегатом
Target Если метод, подлежащий вызову, определен на уровне объекта (т.е. не является статическим), то Target возвращает объект, представляющий метод, поддерживаемый делегатом. Если возвращенное Target значение равно null, значит, подлежащий вызову метод является статическим
Combine() Этот статический метод добавляет метод в список, поддерживаемый делегатом. В C# этот метод вызывается за счет использования перегруженной операции += в качестве сокращенной нотации
GetlnvokationList() Этот метод возвращает массив типов System.Delegate, каждый из которых представляет определенный метод, доступный для вызова
Remove()
RemoveAll()
Эти статические методы удаляют метод (или все методы) из списка вызовов делегата. В C# метод Remove () может быть вызван неявно, посредством перегруженной операции -=

Делегат как параметр метода c

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

Рассмотрим подробный пример. Пусть у нас есть класс, описывающий счет в банке:

public class Account < int sum; // Переменная для хранения суммы // через конструктор устанавливается начальная сумма на счете public Account(int sum) =>this.sum = sum; // добавить средства на счет public void Add(int sum) => this.sum += sum; // взять деньги с счета public void Take(int sum) < // берем деньги, если на счете достаточно средств if (this.sum >=sum) this.sum -= sum; > >

В переменной sum хранится сумма на счете. С помощью конструктора устанавливается начальная сумма на счете. Метод Add() служит для добавления на счет, а метод Take — для снятия денег со счета.

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

public class Account < int sum; public Account(int sum) =>this.sum = sum; public void Add(int sum) => this.sum += sum; public void Take(int sum) < if (this.sum >= sum) < this.sum -= sum; Console.WriteLine($"Со счета списано у.е."); > > >

Но что если наш класс планируется использовать в других проектах, например, в графическом приложении на Windows Forms или WPF, в мобильном приложении, в веб-приложении. Там строка уведомления

Console.WriteLine($"Со счета списано у.е.");

не будет иметь большого смысла.

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

Console.WriteLine($"Со счета списано у.е.");

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

Изменим класс, применив делегаты:

// Объявляем делегат public delegate void AccountHandler(string message); public class Account < int sum; // Создаем переменную делегата AccountHandler? taken; public Account(int sum) =>this.sum = sum; // Регистрируем делегат public void RegisterHandler(AccountHandler del) < taken = del; >public void Add(int sum) => this.sum += sum; public void Take(int sum) < if (this.sum >= sum) < this.sum -= sum; // вызываем делегат, передавая ему сообщение taken?.Invoke($"Со счета списано у.е."); > else < taken?.Invoke($"Недостаточно средств. Баланс: у.е."); > > >

Для делегирования действия здесь определен делегат AccountHandler . Этот делегат соответствует любым методам, которые имеют тип void и принимают параметр типа string .

public delegate void AccountHandler(string message);

В классе Account определяем переменную taken , которая представляет этот делегат:

AccountHandler? taken;

Теперь надо связать эту переменную с конкретным действием, которое будет выполняться. Мы можем использовать разные способы для передачи делегата в класс. В данном случае определяется специальный метод RegisterHandler, в котором в переменную taken передается реальное действие:

public void RegisterHandler(AccountHandler del)

Таким образом, делегат установлен, и теперь его можно вызывать. Вызов делегата производится в методе Take:

public void Take(int sum) < if (this.sum >= sum) < this.sum -= sum; // вызываем делегат, передавая ему сообщение taken?.Invoke($"Со счета списано у.е."); > else < taken?.Invoke($"Недостаточно средств. Баланс: у.е."); > >

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

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

Теперь протестируем класс в основной программе:

// создаем банковский счет Account account = new Account(200); // Добавляем в делегат ссылку на метод PrintSimpleMessage account.RegisterHandler(PrintSimpleMessage); // Два раза подряд пытаемся снять деньги account.Take(100); account.Take(150); void PrintSimpleMessage(string message) => Console.WriteLine(message);

Здесь через метод RegisterHandler переменной taken в классе Account передается ссылка на метод PrintSimpleMessage . Этот метод соответствует делегату AccountHandler. Соответственно там, где в классе Account вызывается делегат taken, в реальности будет выполняться метод PrintSimpleMessage.

Через параметр message метод PrintSimpleMessage получит переданное из делегата сообщение и выведет его на консоль:

Со счета списано 100 у.е. Недостаточно средств. Баланс: 100 у.е.

Таким образом, мы создали механизм обратного вызова для класса Account, который срабатывает в случае снятия денег. Здесь мы выводим сообщение на консоль. Да, мы могли бы просто выводить сообщение на консоль и без делегатов. Однако с делегатом для класса Account не важно, как это сообщение выводится. Классу Account даже не известно, что вообще будет делаться в результате списания денег. Он просто посылает уведомление об этом через делегат.

В результате, если мы создаем консольное приложение, мы можем через делегат выводить сообщение на консоль. Если мы создаем графическое приложение Windows Forms или WPF, то можно выводить сообщение в виде графического окна. А можно не просто выводить сообщение. А, например, записать при списании информацию об этом действии в файл или отправить уведомление на электронную почту. В общем любыми способами обработать вызов делегата. И способ обработки не будет зависеть от класса Account.

Добавление и удаление методов в делегате

Хотя в примере наш делегат принимал адрес на один метод, в действительности он может указывать сразу на несколько методов. Кроме того, при необходимости мы можем удалить ссылки на адреса определенных методов, чтобы они не вызывались при вызове делегата. Итак, изменим в классе Account метод RegisterHandler и добавим новый метод UnregisterHandler, который будет удалять методы из списка методов делегата:

public delegate void AccountHandler(string message); public class Account < int sum; AccountHandler? taken; public Account(int sum) =>this.sum = sum; // Регистрируем делегат public void RegisterHandler(AccountHandler del) < taken += del; >// Отмена регистрации делегата public void UnregisterHandler(AccountHandler del) < taken -= del; // удаляем делегат >public void Add(int sum) => this.sum += sum; public void Take(int sum) < if (this.sum >= sum) < this.sum -= sum; taken?.Invoke($"Со счета списано у.е."); > else taken?.Invoke($"Недостаточно средств. Баланс: у.е."); > >

В первом методе объединяет делегаты taken и del в один, который потом присваивается переменной taken . Во втором методе из переменной taken удаляется делегат del .

Применим новые методы:

Account account = new Account(200); // Добавляем в делегат ссылку на методы account.RegisterHandler(PrintSimpleMessage); account.RegisterHandler(PrintColorMessage); // Два раза подряд пытаемся снять деньги account.Take(100); account.Take(150); // Удаляем делегат account.UnregisterHandler(PrintColorMessage); // снова пытаемся снять деньги account.Take(50); void PrintSimpleMessage(string message) => Console.WriteLine(message); void PrintColorMessage(string message) < // Устанавливаем красный цвет символов Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(message); // Сбрасываем настройки цвета Console.ResetColor(); >

В целях тестирования мы создали еще один метод — PrintColorMessage, который выводит то же самое сообщение только красным цветом. Ссылка на этот метод также передается в метод RegisterHandler, и таким образом ее получит переменная taken.

В строке account.UnregisterHandler(PrintColorMessage); этот метод удаляется из списка вызовов делегата, поэтому этот метод больше не будет срабатывать. Консольный вывод будет иметь следующую форму:

Со счета списано 100 у.е. Со счета списано 100 у.е. Недостаточно средств. Баланс: 100 у.е. Недостаточно средств. Баланс: 100 у.е. Со счета списано 50 у.е.

Делегат в качестве параметра

Добрый вечер уважаемые знатоки! У меня возник такой вопрос: мне нужен метод примерно следующего вида:

public void MyMethod(Func del)

Собственно в чем проблема: нужно чтобы делегат del вызывался в методе некоторое количество раз (например тысячу как указано выше), но дело в том, что Func — это некий «общий» тип — в действительности это может быть и Func и Func и Func. Более ого, T1, T2 и T3 могут быть int, float, double. Кроме того я не знаю, как можно было бы передавать кроме делегата еще и набор его параметров, с которыми он должен вызываться. Хотелось бы чтобы я имел возможность делать что-то такое:

var del1 = new Func(() => 10); var del2 = new Func(i => i); var del3 = new Func((i, j) => i + j); MyMethod(del1); MyMethod(del2, parametersForDel); MyMethod(del3, parametersForDel); 

Можно ли как-то написать некий общий метод MyMethod, который мог бы получать в качестве аргумента любой интересующий меня делегат указанного типа? Или же под каждый Func нужно писать свой метод? И как быть с передачей параметров для этих делегатов? Заранее спасибо

Отслеживать
371 1 1 золотой знак 5 5 серебряных знаков 13 13 бронзовых знаков
задан 23 сен 2013 в 19:41
1,303 2 2 золотых знака 20 20 серебряных знаков 35 35 бронзовых знаков

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

23 сен 2013 в 19:51

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

23 сен 2013 в 20:59

Скорее всего, вы делаете что-то неправильно: вам по идее не должно такого хотеться. Опишите лучше вашу реальную задачу, подумаем. То, что вам нужно исходя из вопроса, это просто метод void MyMethod(Action a) , которому можно передавать различные лямбды: MyMethod(d1) , MyMethod(() => d2(arg)) , MyMethod(() => d3(arg1, arg2)) и т. д.

23 сен 2013 в 21:14

реальная задача такая — есть много однотипных методов одного из трех видов: Func, Func и Func. Нужно сравнивать эти методы по скорости их выполнения. То есть мне нужен метод который мог бы получать делегат любого из этих видов, много раз вызывать его и измерять скорость выполнения этого делегата.

24 сен 2013 в 6:44

@JuniorTwo: О, это уже теплее. А откуда вы собираетесь брать параметры? Всё время одинаковые или каждый раз новые?

24 сен 2013 в 9:43

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

О, тестирование производительности! Тестирование — довольно сложная тема, очень сложно сделать его полностью правильно. Считается, что это хорошее задание для новичка, но на самом деле правильные тесты производительности должен бы написать архитектор.

Давайте разберём, что мы хотим тестировать.

Во-первых, запуск метода один раз — не очень правильная штука. На выполнение метода влияют сотни случайных параметров: например, случайный всплеск активности антивируса или вытесненная на диск страничка кода. Поэтому необходимо прогнать метод много раз: результаты одного прогона статистически иррелевантны. (По поводу того, сколько же раз нужно прогонять тест: не берите число с потолка, а спросите коллегу-математика!)

Далее, тест должен быть проведён с одними и теми же параметрами! Иначе наше усреднение не имеет смысла: мы усредняем несравнимые величины. Если в функции одна ветка кода быстрая, а другая медленная, усреднять время пробега по ним бессмысленно: надо иметь два теста, тестирующие разные ветки.

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

class PerformanceTester < const int repetitions = 1000; // может быть, нужна внешняя параметризация static public TimeSpan ComputeAverageExecutionTime(Action a) < var executionTicks = new List(); for (int i = 0; i < repetitions; i++) executionTicks.Add(MeasureTime(a)); double averageTicks = executionTicks.Average(); return new TimeSpan((long)averageTicks); >static private long MeasureTime(Action a) // in ticks < var stopwatch = new Stopwatch(); stopwatch.Start(); a(); stopwatch.Stop(); return stopwatch.ElapsedTicks; >> 
void f() < /* . */ >void g(int arg) < /* . */ >void h(ClassArg arg1, int arg2) < /* . */ >. var r1 = PerformanceTester.ComputeAverageExecutionTime(f); var r2 = PerformanceTester.ComputeAverageExecutionTime(() => g(1)); ClassArg arg1 = new ClassArg(); // не включаем конструктор в лямбду var r3 = PerformanceTester.ComputeAverageExecutionTime(() => h(arg1, 0)); 

Но это — не окончательное решение. В нём есть недостатки, которые мы попробуем закрыть.

Во-первых, JIT. Первый пробег функции будет медленнее из-за того, что в этот момент она будет скомпилирована just-in-time-компилятором. Плюс необходимые части кода будут загружены в память. Значит, необходимо включить «разогрев кода» в тест:

static public TimeSpan ComputeAverageExecutionTime(Action a) < // разогреваем код: a(); var executionTicks = new List(); for (int i = 0; i

Во-вторых, среди большого числа пробегов всё равно будут случайные «выбросы» вверх и вниз. Чтобы нивелировать их, надо знать среднеквадратичное отклонение σ, и отбраковывать результаты, отличающиеся от оценочного среднего на более чем 3σ. Но для этого нужна продвинутая математика, а мы можем по-простому выкинуть наибольшее и наименьшее значения, это тоже довольно хорошая эвристика.

var maxVal = executionTicks.Max(); if (executionTicks.Count(v => v == maxVal) < executionTicks.Count / 3) executionTicks.RemoveAll(v =>v == maxVal); var minVal = executionTicks.Min(); if (executionTicks.Count(v => v == minVal) < executionTicks.Count / 3) executionTicks.RemoveAll(v =>v == minVal); // здесь осталось не менее [[1000 * 2/3] * 2/3] = 444 элементов, // значит, список не пуст, и исключение не выбросится double averageTicks = executionTicks.Average(); 

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

class PerformanceTester < static public TimeSpan ComputeExecutionTimeForSmallFunctions( Action execute1000times, Action execute1time) < // разогреваемся execute1time(); var ticks = MeasureTime(execute1000times); return new TimeSpan((long)(ticks / 1000.0)); >// остальные функции > 
void q() < /* что-то очень быстрое */ >var r4 = PerformanceTester.ComputeExecutionTimeForSmallFunctions( () => < for (int i = 0; i < 1000; i++) q(); >q); 

Недостатки: в измерение всё же включается косвенный вызов делегата, но теперь делегат вызывается не 1000, а всего 1 раз, и вносит лишь 1/1000 искажения. К счастью, для маленькой функции можно смело поднимать константу 1000 до больших значений, например, до 1000000. Заодно и результаты случайных больших отклонений от среднего, с которыми мы боролись для длинных функций отбрасыванием минимального и максимального значения, нивелируются. Кроме того, ко времени выполнения функции прибавляется время на управление циклом.

В-четвёртых, не забывайте, что производительность сильно зависит от настроек компилятора. Поэтому никогда не тестируйте производительность кода, скомпилированного в DEBUG-режиме. Кроме того, пробег в Visual Studio замедляет скорость кода приблизительно вдвое (!), так как отладчик специально «просит» JIT-компилятор оптимизировать поменьше. Поэтому после окончания отладки запускайте тесты производительности только вне Visual Studio, и только скомпилированные в RELEASE-режиме.

В-пятых, не забывайте о наличии довольно умного оптимизатора! Он имеет право заинлайнить вызов функции, и если вы игнорируете результат, выкинуть всё вычисление! Поэтому для случая ComputeExecutionTimeForSmallFunctions возвращаемое значение надо бы как-то использовать, хотя бы запоминать в массиве, добавлять в аккумулятор или что-то ещё наподобие.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *