Введение
В с++ мы могли взять указатель на функцию и вызвать ее через этот указатель. Делегат в С# - своеобразный указатель на функцию. То есть благодаря делегатам, мы можем передать метод как параметр другому методу (это называется CallBack метод). Такой способ очень часто встречается в FCL, и поэтому уметь работать с делегатами надо!
Сначала я опишу создание своего класса-делегата «с нуля» (даже не производного от Delegate), а в конце я расскажу, чем он отличается от обычных делегатов.
Создаем свой делегат «с нуля».
Делегат – оболочка для метода. То есть это объект, хранящий указатель на какой-то метод. Вот этот указатель будет представлен объектом класса MethodInfo. Получить его можно зная его имя и тип, в котором требуемый метод определен. Кроме того, для вызова экземплярных методов (не статических) нам еще потребуется и экземпляр этого типа. Поэтому наши конструкторы будут иметь вид:
public MyDelegate(Type type, string name) {..}
|
type – тип в котором определен метод, name – имя метода. Этот конструктор для статических методов.
public MyDelegate(object target, string name){..}
|
target – экземпляр нужного нам типа, target.GetType() – сам тип, name – собственно имя метода. Этот конструктор для экземплярных методов.
В классе Type есть много методов, позволяющих получить информацию о членах типа. Я воспользуюсь методом GetMethod. Он принимает первым параметром имя метода и возвращает объект MethodInfo – то что нам нужно. Но перегрузка этого метода GetMethod(string name) находит только открытые(public) методы. Чтобы найти любой, даже закрытый(private) нужно воспользоваться другой перегрузкой – GetMethod(string name, BindingFlags restrictions). Параметр restrictions опеределяет какие методы нужно искать (public, static …). Т.к. нам нужны все методы, то в этом параметре мы передедим BindingFlags.Public | BindingFlags.Static | … или просто (BindingFlags)0xFFFFFFF.
Итак, уже можно написать реализацию конструкторов:
class MyDelegate
{
MethodInfo Method;
Object Target;
public MyDelegate(Type target, string method)
{
Method = target.GetMethod(method, (BindingFlags)0xFFFFFFF);
}
public MyDelegate(object target, string method)
{
Method = target.GetType().GetMethod(method, (BindingFlags)0xFFFFFFF);
Target = target;
}
}
|
Для вызова «обернутого» метода определим метод Invoke. Он будет вызывать одноименный метод Invoke класса MethodInfo(который представляет собой указатель на «обернутый» нашим делегатом метод). Вызов этого метда – всеравно если бы мы вызывали наш метод обычным спобом, только вместо параметров он принмает object[], в котором должны лежать в нужном порядке параметры метода. кроме того, если вызываемый метод экземплярный, то нужно передать Invoke экземпляр. Для статических методов этот параметр игнорируется. Учитываю все это, приступим к определению Invoke:
class MyDelegate
{
MethodInfo Method;
Object Target;
public void Invoke(object[] parameters)
{
Method.Invoke(Target, parameters);
}
}
|
Использовать его потом можно будет следующим образом:
object[] parameters = new object[]{ “Parameter1”, “Parameter2”};
mydelegate.Invoke(parameters);
|
Согласитесь немного неудобно создавать object[]. Предоставим это компилятору! Для этого немного изменим опеределение Invoke:
public void Invoke(params object[] parameters)
{
Method.Invoke(Target, parameters);
}
|
Теперь вызывать Invoke можно так:
mydelegate.Invoke(“Parameter1”, “Parameter2”);
Компилятор сам создает object[], а мы радуемся красивому коду!!!
Основовную свою функцию наш делегат уже может выполнять: в него можно обернуть указатель на метод, и в дальнейшем вызвать этот метод. Но настоящий делегат, кроме этого, должен еще уметь хранить указатели сразу на несколько методов! И при вызове Invoke вызывать все эти методы. Реализовать это можно разными способами, я предлагаю так: будем хранить коллекцию делегатов List<MyDelegate>(назовем ее цепочкой) при вызове Invoke, вызвать Invoke каждого из этой цепочки. Кроме того будет метод Combine, Remove – управляющие цепочкой, потом я перегружу оператор +, который будет возвращать третий делегат, при вызове Invoke которого будут вызваться Invoke обоих слагаемых. Еще будет перегружен оператор -, который будет удалять из цепочки уменьшаемого вычитаемый. Итак, приступим:
class MyDelegate
{
MethodInfo Method;
Object Target;
List<MyDelegate> Chain = new List<MyDelegate>();
private MyDelegate() {}
public void Invoke(params object[] parameters)
{
if (Method == null) throw new InvalidOperationException("Объект не проинициализирован");
Method.Invoke(Target, parameters);
foreach (MyDelegate m in Chain)
{
m.Invoke(m.Target, parameters);
}
}
public void Combine(MyDelegate SomeDelegate)
{
if ( !IsEqual(SomeDelegate.Method.GetParameters(), Method.GetParameters()))
throw new ArgumentException("Делегат имеет несовместимую сигнатуру", "SomeDelegate");
Chain.Add(SomeDelegate);
}
private bool IsEqual(ParameterInfo[] parameterInfo, ParameterInfo[] parameterInfo_2)
{
if (parameterInfo.Length != parameterInfo_2.Length) return false;
for (int i = 0; i < parameterInfo.Length; i++)
{
if (parameterInfo[i] != parameterInfo_2[i]) return false;
}
return true;
}
public void Remove(MyDelegate CombineedDelegate)
{
Chain.Remove(CombineedDelegate);
}
public static MyDelegate operator + (MyDelegate d1, MyDelegate d2)
{
if (d1 == null)return d2;
if (d2 == null) return d1;
MyDelegate d = new MyDelegate();
d.Method = d1.Method;
d.Target = d1.Target;
d.Chain = d1.Chain;
d.Combine(d2);
return d;
}
public static MyDelegate operator -(MyDelegate d1, MyDelegate d2)
{
if (d2 == null) return d1;
MyDelegate d = new MyDelegate();
d.Method = d1.Method;
d.Target = d1.Target;
d.Chain = d1.Chain;
d.Remove(d2);
return d;
}
public List<MyDelegate> GetInvokationList()
{
return Chain;
}
}
|
Все здесь замечательно, кроме реализации Invoke. Пердставте, что будет если в цепочку добавить самого себя: this.Combine(this). Вот код из Invoke в котором ошибка:
foreach (MyDelegate m in Chain)
{
m.Invoke(m.Target, parameters);
}
|
Что бы избежать этого, сделаем так:
foreach (MyDelegate m in Chain)
{
m.Method.Invoke(m.Target, parameters);
}
|
И последняя доработка. Если например при создании делегата пользователь передал неправильное имя метода, то лучше если исключение возникнед именно при создании делегата, а не при попытке вызвать Invoke. Поэтому лучше проверять параметры в методах. Итак, вот конечный код класса MyDelegate:
class MyDelegate
{
MethodInfo Method;
Object Target;
List<MyDelegate> Chain = new List<MyDelegate>();
private MyDelegate()
{ }
public MyDelegate(object target, string method)
{
if (target == null) throw new ArgumentException("Параметр равен null", "target");
if (method == null) throw new ArgumentException("Параметр равен null", "method");
Method = target.GetType().GetMethod(method, (BindingFlags)0xFFFFFFF);
Target = target;
if (Method == null) throw new ArgumentException("Указанного метода не существует", "method");
}
public MyDelegate(Type target, string method)
{
if (target == null) throw new ArgumentException("Параметр равен null", "target");
if (method == null) throw new ArgumentException("Параметр равен null", "method");
Method = target.GetMethod(method, (BindingFlags)0xFFFFFFF);
if (Method == null) throw new ArgumentException("Указанного метода не существует", "method");
}
public void Invoke(params object[] parameters)
{
if (Method == null) throw new InvalidOperationException("Объект не проинициализирован");
Method.Invoke(Target, parameters);
foreach (MyDelegate m in Chain)
{
m.Method.Invoke(m.Target, parameters);
}
}
public void Combine(MyDelegate SomeDelegate)
{
if ( !IsEqual(SomeDelegate.Method.GetParameters(), Method.GetParameters()))
throw new ArgumentException("Делегат имеет несовместимую сигнатуру", "SomeDelegate");
Chain.Add(SomeDelegate);
}
private bool IsEqual(ParameterInfo[] parameterInfo, ParameterInfo[] parameterInfo_2)
{
if (parameterInfo.Length != parameterInfo_2.Length) return false;
for (int i = 0; i < parameterInfo.Length; i++)
{
if (parameterInfo[i] != parameterInfo_2[i]) return false;
}
return true;
}
public void Remove(MyDelegate CombineedDelegate)
{
Chain.Remove(CombineedDelegate);
}
public static MyDelegate operator + (MyDelegate d1, MyDelegate d2)
{
if (d1 == null)return d2;
if (d2 == null) return d1;
MyDelegate d = new MyDelegate();
d.Method = d1.Method;
d.Target = d1.Target;
d.Chain = d1.Chain;
d.Combine(d2);
return d;
}
public static MyDelegate operator -(MyDelegate d1, MyDelegate d2)
{
if (d1 == null) throw new ArgumentException("Уменьшаемое равно null", "Уменьшаемое");
if (d2 == null) return d1;
MyDelegate d = new MyDelegate();
d.Method = d1.Method;
d.Target = d1.Target;
d.Chain = d1.Chain;
d.Remove(d2);
return d;
}
public List<MyDelegate> GetInvokationList()
{
return Chain;
}
}
|
Чем отличается MyDelegate от обычных делегатов?
Если говоить о конкретно о реализации – никто не знает. Но общую разницу я сейчас попробую показать.
Что значит обычный делегат? Я подразумеваю под эти термином делегат определенный примерно таким образом:
delegate void MyStandartDelegate(string param1, string param2);
Вот эта строчка кода при компиляции превращается во что-то похожее на класс MyDelegate, но со следующими отличиями:
- Класс MyStandartDelegate производный от MulticastDelegate, который в свою очередь производный от Delegate.
- Метод Invoke имеет сигнатуру, использованную при объявлении делегата. В нашем случае он будет выглядеть так:
void Invoke(string param1, string param2){…}
Это позволяет контролировать типы параметров еще на этапе компиляции, в то время как в MyDelegate проверка осуществляется только на этапе выполнения. С другой стороны, этот делегат может ссылаться на методы только с такой сигнатурой, которая у Invoke, мой же делегат может ссылаться на что угодно.
- Конструктор не требует ни Type, ни object, ни строки – имени метода. Достаточно просто передать имя метода(без кавычек), и все остальное само собой определяется.
- Указатель на метод выражен int’овым числом. Как они так сделали не знаю…
- Цепочка тоже немного по другому реализованна.
Методы Combine и Remove – Статические методы класса Delegate. Они в целом похожи на мои operator+ и operator-.
Используется обынчный массив, а не List как у меня. В результате добавления / удаления создается новый массив.
Делегат может ссылаться либо на один метод, либо на группу объектов Delegate.
- Компилятор здорово упрощает использование делегатов.А именно:
Не нужно созадавать объект-делегат, просто указываете имя метода там где нужно, и компилятор автоматически генерирует код по созданию объекта.
Не нужно определять метод обратного вызова. Если код этого метода достаточно прост, можно написать так:
delegate(string Param){/*код метода*/}
и компилятор сам сгенерирует метод. Кроме того, если в теле метода не используются параметры, их можно не писать:
delegate{/*код метода, не использующий параметры*/}
- Есть еще много других членов, не будем их рассматривать. Если есть желание, обратитесь к MSDN…
Заключение
В это статье было рассмотрено строение делегатов на примере создания собственного делегата «с нуля». Конечно, реализация MyDelegate очень далека от реализации реальных делегатов, но в целом достаточно похожа. По край не мере это было интересно, создать такую космическую арматуру (. Удачи!
Богатырев Павел (pfight at vestace.ru)
26.10.2008