Pull to refresh

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

Programming *.NET *
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# 3.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 принимает как лямбды, так и анонимные методы, так и обычные методы и всё под одним обьявлением. Разве это не удивительно удобно?

Итог
Чего же мы добились? Ключевой метод PerformOperation сократился до разумной длины.
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);
}


Словарь operations можно расширять как угодно. Можно при желании создавать его из xml-файла, из фабрики калькуляторов, да хоть из текста введенного пользователем. Лишь бы операции приводились к типу Func<double, double, double>.

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

Уголок любителя динамических языков
А вот в JavaScript я всегда мог написать
var operations = { "+": function(x, y) { return x + y; } };

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

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

Апдейт, где я срываю покровы.
Переписав наш PerformOperation на использование делегатов мы можем расширить функциональность калькулятора. Добавим метод DefineOperation в класс Calculator:
public void DefineOperation(string op, Func<double, double, double> body)
{
	if (_operations.ContainsKey(op))
		throw new ArgumentException(string.Format("Operation {0} already exists", op), "op");
	_operations.Add(op, body);
}

Теперь для наглядности добавим операцию взятия по модулю:
var calc = new Calculator();
calc.DefineOperation("mod", (x, y) => x % y);
var mod = calc.PerformOperation("mod", 3.0, 2.0);
Assert.AreEqual(1.0, mod);

Сделать такой же трюк невозможно без изменения кода калькулятора если PerformOperation использует switch.
Tags:
Hubs:
Total votes 75: ↑55 and ↓20 +35
Views 198K
Comments Comments 181