Регистрация | Вход 
Просмотр статьи
Как устроены делегаты

Создание своего делегата «с нуля»

Введение

В с++ мы могли взять указатель на функцию и вызвать ее через этот указатель. Делегат в С# - своеобразный указатель на функцию. То есть благодаря делегатам, мы можем передать метод как параметр другому методу (это называется 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) //обратите внимание на ключевое слово params
{      
   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;
    //цепочка
    //эти делегаты также будут вызываться при вызове Invoke этого делегата
    List<MyDelegate> Chain = new List<MyDelegate>();

    //закрытый конструктор, используется в operator + и operator -
    private MyDelegate() {}

   //Вызвать метод
    public void Invoke(params object[] parameters) //обратите внимание на ключевое слово params
    {
        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)
    {
        //проверка, имеют ли методы текущего и SomeDelegate делегатов одинаковую сигнатуру
        //если бы проверка не выполнялась, то невозможно было бы вызывать делелгаты из цепочки
        //Метод IsEqual определен чуть ниже
        if ( !IsEqual(SomeDelegate.Method.GetParameters(), Method.GetParameters()))
            throw new ArgumentException("Делегат имеет несовместимую сигнатуру", "SomeDelegate");

        Chain.Add(SomeDelegate);  
    }
//используется в методе Combine
    private bool IsEqual(ParameterInfo[] parameterInfo, ParameterInfo[] parameterInfo_2)
    {
        //сравниваю каждый элемент массива parameter_Info и каждым элементом 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;

        //создаю клон d1
        MyDelegate d = new MyDelegate();
        d.Method = d1.Method;
        d.Target = d1.Target;
        d.Chain = d1.Chain;

        //добавляю в его цепочку d2
        d.Combine(d2);

        //d – и есть тот самый третий делегат
        return d;
    }

    //пердполагается, что d2 есть в цепочке d1. 
    //После вычитания получиться третий делегат, копия d1 но без d2 в цепочке.
    public static MyDelegate operator -(MyDelegate d1, MyDelegate d2)
    {
        if (d2 == null) return d1;

        //создаю клон d1
        MyDelegate d = new MyDelegate();
        d.Method = d1.Method;
        d.Target = d1.Target;
        d.Chain = d1.Chain;

        //удаляю из его цепочки d2
        d.Remove(d2);

        //d и есть тот самый третий делегат
        return d;
    }

    public List<MyDelegate> GetInvokationList()
    {
        return Chain;
    }

}

Все здесь замечательно, кроме реализации Invoke. Пердставте, что будет если в цепочку добавить самого себя: this.Combine(this). Вот код из Invoke в котором ошибка:

//вызываем все делегаты из цепочки
foreach (MyDelegate m in Chain)
{
//если m == this, то получиться бесконечная рекурсия!!!
m.Invoke(m.Target, parameters);
}

Что бы избежать этого, сделаем так:

//вызываем все делегаты из цепочки
foreach (MyDelegate m in Chain)
{
//Теперь рекурсии не будет, просто второй раз вызовется метод, как и ожидал пользователь
m.Method.Invoke(m.Target, parameters);
}

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

class MyDelegate
{
    //метод 
    MethodInfo Method; 
    //если метод экземплярный, то объект через который его будем вызвать
    Object Target;
    //цепочка
    //эти делегаты также будут вызываться при вызове Invoke этого делегата
    List<MyDelegate> Chain = new List<MyDelegate>();

    //закрытый конструктор, используется в operator + и operator -
    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");

        //unchecked
        //{
            Method = target.GetMethod(method, (BindingFlags)0xFFFFFFF);
        //}

        if (Method == null) throw new ArgumentException("Указанного метода не существует", "method");
    }
    //Вызвать метод
    public void Invoke(params object[] parameters) //обратите внимание на ключевое слово params
    {
        if (Method == null) throw new InvalidOperationException("Объект не проинициализирован");

        Method.Invoke(Target, parameters);
        //вызываем все делегаты из цепочки
        foreach (MyDelegate m in Chain)
        {
            //если вызывать m.Invoke, 
            //то при попадании в цепочку себя(this.Combine(this) )
            //появляется бесконечный цикл
            m.Method.Invoke(m.Target, parameters);
        }
       

    }
    //добавить делегат в цепочку 
    public void Combine(MyDelegate SomeDelegate)
    {
        //проверка, имеют ли методы текущего и SomeDelegate делегатов одинаковую сигнатуру
        //если бы проверка не выполнялась, то невозможно было бы вызывать делелгаты из цепочки
        if ( !IsEqual(SomeDelegate.Method.GetParameters(), Method.GetParameters()))
            throw new ArgumentException("Делегат имеет несовместимую сигнатуру", "SomeDelegate");

        Chain.Add(SomeDelegate);
       
    }
    //используется в методе Combine
    private bool IsEqual(ParameterInfo[] parameterInfo, ParameterInfo[] parameterInfo_2)
    {
        //сравниваю каждый элемент массива parameter_Info и каждым элементом 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;

        //создаю клон d1
        MyDelegate d = new MyDelegate();
        d.Method = d1.Method;
        d.Target = d1.Target;
        d.Chain = d1.Chain;

        d.Combine(d2);
        return d;
    }
    //пердполагается, что d2 есть в цепочке d1. После вычитания, он удаляется из цепочки.
    public static MyDelegate operator -(MyDelegate d1, MyDelegate d2)
    {
        if (d1 == null) throw new ArgumentException("Уменьшаемое равно null", "Уменьшаемое");
        if (d2 == null) return d1;

        //создаю клон 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

Автор: PFight
Дата публикации: 26.10.2008
Число просмотров: 2120

Возврат


Copyright 2007-2009 by Alexander Ignatyev