C#: Delegate (делегаты) зачем нужны, как использовать и чем заменить?

Сегодня речь пойдет об очень интересной и полезной контсрукции языка программирования C# как delegate. Я постараюсь простыми словами объяснить что это такое и как их использовать.

Так что же такое delegate? Если объяснять простыми словами, то это такой же тип как и int, string и другие, который создан для возможности передавать методы в качестве аргументов функций. Если у Вас возникает вопрос зачем нужно передавать функции как аргументы, то ответ очень простой. В первую очередь посмотрите как реализованы все события в C#. Так же если вы работали с асинхронным кодом (не важно где), то Вы должны знать, что такое callback функции (функции которые вызываются после окончания какого-либо действия, например после получения ответа на POST запрос).

Рассмотрим простой пример создания своего собсвенного типа функции для события. Например у нас есть класс машина, которая может быть в заведенном состоянии, либо нет. Если машина в заведенном состоянии, то она едет с некоторой случайной скоростью. Мы же хотим отлавливать в коде события завода машины, а так же когда ее скорость равна нулю. При этом мы будем избезать такого слова как EventHandler.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace Emulator
{
    class Car: IDisposable
    {
        //Перечисляем возможные состояния двигателя
        public enum EngineState { On, Off }
        
        private Random _rnd = new Random();
        protected double _speed = .0;
        protected EngineState _engineState = EngineState.Off;
        protected bool _disposed = false;

        private const double _MAX_SPEED = 180.0;

        protected virtual void _OnRoad()
        {
            while(_engineState == EngineState.On)
            {
                _speed = _speed + _rnd.NextDouble() * (_rnd.Next(1, 10) / 2 == 0 ? 1 : -1);
                if(_speed < 0) 
                {
                    _speed = 0;
                }
                else if(_speed > _MAX_SPEED)
                {
                    _speed = _MAX_SPEED;
                }
                if (OnSpeedZeroEvent != null && _speed == 0)
                {   //если событие нулевой скорости инициализировано
                    //вызываем его
                    OnSpeedZeroEvent(this, _engineState);
                }
                Thread.Sleep(1000);
            }
            _speed = .0;
            if (OnSpeedZeroEvent != null)
            {
                OnSpeedZeroEvent(this, _engineState);
            }
        }

        //Заводим машину
        public void Go()
        {
            switch(_engineState)
            {
                case EngineState.On:
                    //тут мы будем издавать страшный звук.
                    //Я думаю Вы поняли о чем я :)
                    break;
                case EngineState.Off:
                    _engineState = EngineState.On;
                    new Thread(_OnRoad).Start();
                    if (OnEngineStartEvent != null)
                    {   //если событие старта дивигателя инициализировано
                        //вызываем его
                        OnEngineStartEvent(this);
                    }
                    break;

            }
        }

        //Глушим машину
        public void Stop()
        {
            _engineState = EngineState.Off;
        }

        //Объявляем делегат (стурктуру функции) который мы будем использовать для
        //передачи в качестве аргумента в событие завода машины
        public delegate void OnEngineStart(Car sender);
        //Создаем событие завода машины
        public event OnEngineStart OnEngineStartEvent = null;

        //Объявляем делегат (стурктуру функции) который мы будем использовать для
        //передачи в качестве аргумента в событие остановки машины
        public delegate void OnSpeedZero(Car sender, EngineState state);
        //Создаем событие остановки машины (скорость = 0)
        public event OnSpeedZero OnSpeedZeroEvent = null;
    
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if(_disposed)
            {
                return;
            }
            if(disposing)
            {
                Stop();
            }
            _disposed = true;
        }
    }
}

Как мы видим объявлено два делегата OnEngineStart и OnSpeedZero которые описывают структуру функций. В первом случае это функция, которая принимает один аргумент, во втором два аргумента. Напишем простую программу которая будет демонстрировать работу нашего класса.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Emulator
{
    class Program
    {
        static void Main(string[] args)
        {
            Car car = new Car();
            //инициализировали событие завода машины
            car.OnEngineStartEvent += car_OnEngineStartEvent; 
            //инициализировали событие остановки с помощью лямда выражения
            //разницы в работе нет, но позволяет избежать создание дополнительной функции
            car.OnSpeedZeroEvent += (carObject, carState) =>
            {
                Console.WriteLine("Машина остановилась. Состояние двигателя {0}.", carState);
            };
            string command;
            while ((command = Console.ReadLine().Trim().ToLower()) != "exit")
            {
                switch(command)
                {
                    case "run":
                        car.Go();
                        break;
                    case "stop":
                        car.Stop();
                        break;
                }
            }
        }

        static void car_OnEngineStartEvent(Car sender)
        {
            Console.WriteLine("Машина завелась");
        }
    }
}

Результат работы программы:
Sample

Все это хорошо, но порой огромное количество делегатов Вас может начать раздражать. Поэтому разработчики .NET, возможно будет сказано криво, создали «оболочку» для delegate и реализовали делегат Action. Он позволяет создавать структуру функции на лету. Давайте дополним наш класс событием отключения двигателя с помощью Action.

//Объявляем делегат (стурктуру функции) который мы будем использовать для
//передачи в качестве аргумента в событие завода машины
public delegate void OnEngineStart(Car sender);
//Создаем событие завода машины
public event OnEngineStart OnEngineStartEvent = null;

//Создаем событие выключения двигателя. В качестве делегата выступает функция которая принимает один аргумент типа Car. Если бы нам нужно было передатьвать 
//еще дополнительные аргументы, например int и string, запись бы выглядела как public event Action<Car, int, string> OnEngineStoptEvent = null;
public event Action<Car> OnEngineStoptEvent = null;

 //Глушим машину
public void Stop()
{
  _engineState = EngineState.Off;
  if(OnEngineStoptEvent != null)
  {
    OnEngineStoptEvent(this);
  }
}

Переходим к программе. Тут все делает по полной аналогии.

car.OnEngineStoptEvent += (carObject) =>
{
  Console.WriteLine("Машину заглушили");
};

Как видите разницы нет и в целом не все так сложно. Какой подход выбрать, думайте сами. Всем спасибо.

Запись опубликована в рубрике C# с метками , , , , , , , , , . Добавьте в закладки постоянную ссылку.