Зачем нужны делегаты в C#?

в 23:58, , рубрики: .net, delegaте, делегат, Программирование, метки: , ,

image
Продумывая архитектуру очередного класса вы понимаете, что вам очень бы пригодилась возможность передать в качестве аргумента кусок исполняемого кода. Это позволило бы вам избежать веретеницы if-ов и case-ов и сделало бы ваш код более элегантным Девушки восхищенно бы охали и непременно оставляли бы вам свой телефончик в комментах. Кхм… что-то я увлекся.

Итак как это делается в C#? Например вы пишете калькулятор и у вас есть простейшая логика:

public double PerformOperation(string op, double x, double y)
{
	switch (op)
	{
		case "+": return x + y;
		case "-": return x - y;
		case "*": return x * y;
		case "/": return x / y;
		default: throw new ArgumentException(string.Format("Operation {0} is invalid", op), "op");
	}
}

Это простое и изящное решение имеет право на жизнь, но у него есть некоторые проблемы:

  • Софт изменчив. Завтра вам понадобится добавить взятие по модулю и тогда придется перекомпилировать класс. На определенных стадиях проекта это недешевое удовольствие для потребителей вашего класса.
  • Код в текущем виде не имеет никаких проверок входных данных. Если их добавить, то switch неприлично разрастется.

Ма лаасот, как говорят мои израильские друзья.

Во первых надо инкапсулировать код в функции:

switch (op)
{
	case "+": return this.DoAddition(x, y);
	case "-": return this.DoSubtraction(x, y);
	case "*": return this.DoMultiplication(x, y);
	case "/": return this.DoDivision(x, y);
	default: throw new ArgumentException(string.Format("Operation {0} is invalid", op), "op");
}
...
private double DoDivision(double x, double y) { return x / y; }
private double DoMultiplication(double x, double y) { return x * y; }
private double DoSubtraction(double x, double y) { return x - y; }
private double DoAddition(double x, double y) { return x + y; }

Во вторых надо вообще избавиться от свитча:

private delegate double OperationDelegate(double x, double y);
private Dictionary<string, OperationDelegate> _operations;

public Calculator()
{
	_operations =
		new Dictionary<string, OperationDelegate>
		{
			{ "+", this.DoAddition },
			{ "-", this.DoSubtraction },
			{ "*", this.DoMultiplication },
			{ "/", this.DoDivision },
		};
}

public double PerformOperation(string op, double x, double y)
{
	if (!_operations.ContainsKey(op))
		throw new ArgumentException(string.Format("Operation {0} is invalid", op), "op");
	return _operations[op](x, y);
}

Что мы сделали? Мы вынесли определение операций из кода в данные — из свитча в словарь.

private delegate double OperationDelegate(double x, double y);
private Dictionary<string, OperationDelegate> _operations;

Делегат это обьект указывающий на функцию. Вызывая делегат, мы вызываем функцию на которую он указывает. В данном случае мы создаем делегат на функцию принимающую два double параметра и возращающую double. Во второй строке мы создаем маппинг между символом операции (+-*/) и её функцией.
Таким образом мы разрешили первый недостаток: список операций можно изменять по своему усмотрению.

К сожалению мы поимели лишний делегат, да и запись вида

{ "+", this.DoAddition }

не так понятна как

case "+": return x + y;

Начиная с C# 2.0 мы можем разрулить эту проблему внедрением анонимных методов:

{ "+", delegate(double x, double y) { return x + y; } },
{ "-", delegate(double x, double y) { return x - y; } },
{ "*", this.DoMultiplication },
{ "/", this.DoDivision },

Здесь для сложения и вычитания я использую анонимные методы, а для умножения и деления полноценные методы. Но всё равно слишком много воды...

На помощь приходит C# 4.0 с лямбдами:

private Dictionary<string, Func<double, double, double>> _operations =
	new Dictionary<string, Func<double, double, double>>
	{
		{ "+", (x, y) => x + y },
		{ "-", (x, y) => x - y },
		{ "*", this.DoMultiplication },
		{ "/", this.DoDivision },
	};

Во-о-т, уже гораздо лучше — девушки уже строчат комменты!

Func<double, double, double> эквивалентно delegate double Delegate(double x, double y)

Сигнатура фанка читается как Func<тип первого аргумента, тип второго аргумента, тип результата>. Сам по себе Func это тот же делегат, но с генериками. Помимо удобства записи, Func принимает как лямбды, так и анонимные методы, так и обычные методы и всё под одним обьявлением. Разве это не удивительно удобно?

Таким образом в C# можно писать изящные, типизированные конструкции практически без лишней воды.

Уголок любителя динамических языков
А вот в JavaScript я всегда мог написать

var operations = { "+": function(x, y) { return x + y; } };

Нафига спрашивается всякие фанки-шманки?

Отвечаю: C# это строго-типизированный язык, который строго следит за тем чтобы типы совпадали и не падали в рантайме. За попытку присвоения неправильных типов, он бьет по рукам при компиляции. Поэтому ему требуется формальное указание о всех фигурирующих типах.

Автор: rroyter

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js