Разработка приложений на C# в среде Visual Studio 2005

Интерфейсы, делегаты, события в C#

Разбить на страницы
Показывать лекцию целиком

Интерфейсы и делегаты

Интерфейсы

Интерфейсы - это чисто абстрактные классы, в которых все методы являются только объявлениями и не содержат реализацию. В этом случае вместо ключевого слова abstract перед объявлением класса нужно использовать ключевое слово interface. Но объявления самих методов ключевым словом abstract подтверждать не нужно и они не являются виртуальными. Интерфейсы можно рассматривать как предписание, которое нужно немедленно реализовать в ближайшем наследующем классе.

Интерфейсы предназначены только для наследования. Они используются в классах только в качестве ближайшего предка и определяют сигнатуру методов, свойств, индексаторов и событий, которые все до единого обязан реализовать наследующий этот интерфейс класс. То есть не допускается многоуровневое наследование через классы без немедленной реализации. При этом сигнатура в реализации члена должна в точности совпадать с сигнатурой члена, объявленного в интерфейсе. Однако допускается многоуровневое наследование одних интерфейсов другими. Но как только многоуровневая цепочка наследования интерфейсов передается классу, этот класс должен немедленно реализовать члены всех унаследованных интерфейсов. Класс, наследующий интерфейс, называется реализующим интерфейс классом.

Интерфейсы не могут содержать других членов, кроме объявлений методов, свойств, индексаторов и событий. Запрещено включать в них поля, конструкторы и деструкторы,методы перегрузки операторов. Ни один член интерфейса не может быть объявлен статическим.

Запрещено в объявлениях методов интерфейсов использовать модификаторы доступа, поскольку все методы интерфейса считаются общедоступными по умолчанию. Но в реализующем классе, наследующем интерфейс, для каждого метода это нужно подтверждать явно, используя только модификатор public.

Для интерфейсов, в отличие от классов, язык C# допускает множественное наследование. Вслед за именем производного класса после двоеточия может следовать список наследуемых интерфейсов, разделенных запятыми. Если кроме интерфейсов наследуется еще и класс, то его имя должно возглавлять этот список, то есть базовый класс, если он есть, всегда должен стоять впереди интерфейсов.

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

Такой прием обеспечивает совместимость отдельных частей программы. Он снижает количество возможных ошибок программирования, перекладывая задачу контроля за ошибками на плечи компилятора за счет дополнительной формализации (бюрократизации) процесса решения.

Поскольку интерфейс относится к чисто абстрактным базовым классам, допустимо (а часто и очень удобно) объявить ссылку типа интерфейса, которая может ссылаться на любой объект, порожденный классом, наследующим (и сразу реализующим) интерфейс. Ссылка типа интерфейса имеет те же самые возможности, что и ссылка типа базового класса, которая может ссылаться на любые объекты, порожденные производными от него классами. И точно также, как в ссылках базовых классов, полномочия ссылки типа интерфейса можно изменять явным приведением типов.

Если сам интерфейс как тип объявлен с модификатором доступа public, то он доступен любому внешнему коду. Если ключевое слово public опущено или использовано ключевое слово internal, то интерфейс доступен коду только текущей сборки, в которой он объявлен. При этом наследующий класс не должен иметь больший уровень доступности, чем уровень доступности наследуемых интерфейсов.

Открытая реализация членов интерфейса

Пусть разрабатывается набор классов для реализации моделей проигрывателей от разных фирм. Клиентский код, использующий модели проигрывателей, не будет зависеть от их конкретной реализации при условии, что все функции, которые он использует, будут иметь одинаковые имена и одинаковую сигнатуру. Это обязательное условие для всех проигрывателей и устанавливает интерфейсный тип, который они наследуют.

using System;
    
namespace Test
{
    // Перечисление допустимых напряжений
    public enum StdVoltage
    {
        V110 = 110,
        V127 = 127,
        V220 = 220,
        V380 = 380
    }
    
    class Power // Электропитание
    {
        // Поле
        StdVoltage voltage = StdVoltage.V220;
        
        // Свойство
        protected StdVoltage Voltage 
        {
            get { return voltage; }
            set 
            {
                // Заменяем поле при соответствии стандарту
                switch (value)
                {
                    case StdVoltage.V110:
                    case StdVoltage.V127:
                    case StdVoltage.V220:
                    case StdVoltage.V380:
                        voltage = value;
                        break;
                }
            }
        }
    
        // Виртуальный метод
        public virtual void ShowVoltage()
        {
            // Приводим тип перечисления к целому числу
            Console.WriteLine("Питание {0} Вольт", 
    (int)voltage);
        }
    }
    
    interface IPlayer
    {
        // Все объявления по умолчанию public
        void Start();
        void Stop();
        void Pause();
    }
    
    class Panasonic : Power, IPlayer
    {
        public void Start()
        {
            Console.WriteLine("Panasonic: 
    Реализация Start");
        }
    
        public void Stop()
        {
            Console.WriteLine("Panasonic: 
    Реализация Stop");
        }
    
        public void Pause()
        {
            Console.WriteLine("Panasonic: 
    Реализация Pause");
        }
    
        public override void ShowVoltage()
        {
            Voltage = StdVoltage.V110;
            Console.Write("Panasonic: ");
            base.ShowVoltage();
        }
    }
    
    class Sony : Power, IPlayer
    {
        public void Start()
        {
            Console.WriteLine("Sony: 
    Реализация Start");
        }
    
        public void Stop()
        {
            Console.WriteLine("Sony: 
    Реализация Stop");
        }
    
        public void Pause()
        {
            Console.WriteLine("Sony: 
    Реализация Pause");
        }
    
        public override void ShowVoltage()
        {
            Voltage = StdVoltage.V127;
            Console.Write("Sony: ");
            base.ShowVoltage(); 
        }
    }
    
    class Samsung : Power, IPlayer
    {
        public void Start()
        {
            Console.WriteLine("Samsung: 
    Реализация Start");
        }
    
        public void Stop()
        {
            Console.WriteLine("Samsung: 
    Реализация Stop");
        }
    
        public void Pause()
        {
            Console.WriteLine("Samsung: 
    Реализация Pause");
        }
    
        // Здесь мы не переопределяли виртуальную функцию ShowVoltage(),
        // поэтому при использовании экземплярной ссылки будет
        // вызвана ее версия из слоя базового класса с начальным значением 220V
    }
    
    // Вызывающий клиентский код
    class MyClass
    {
        public MyClass()
        {
            // Создаем производные объекты, адресуемые
            // ссылками типа наследуемого интерфейса
            IPlayer[] refInterface = 
                {
                    new Panasonic(),
                    new Sony(),
                    new Samsung()
                };
    
            // Выполняем действия, реализованные индивидуально
            for(int i = 0; i < refInterface.Length; i++)
            {
            refInterface[i].Start();
            refInterface[i].Stop();
            refInterface[i].Pause();
            }
            Console.WriteLine();
    
            // Печатаем индивидуальное питание
            // Для этого поднимаем полномочия ссылки без 
            // проверки, содержится ли в адресуемом объекте 
            // нужный слой (мы-то знаем, что содержится)
            ((Panasonic)refInterface[0]).ShowVoltage();
            ((Sony)refInterface[1]).ShowVoltage();
            // Вызываем виртуальную функцию из слоя базового класса
            // Предварительно на всякий случай проверяем,
            // есть ли в объекте, адресуемом ссылкой типа IPlayer,
            // слой Samsung, чтобы безопасно вызывать его члены
            if (refInterface[2] is Samsung)
            {
                Samsung samsung = (Samsung)refInterface[2];
                Console.Write("Samsung: ");
                // Функции ShowVoltage() в слое Samsung нет, поэтому 
                // будет вызвана версия из слоя базового класса Power
                samsung.ShowVoltage();
            }
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Использование интерфейсов";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 60;
            Console.WindowHeight = 10;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}

Результат выполнения примера будет таким

Диаграмма UML для приведенного кода, построенная в Visual Studio 2005, выглядит следующим образом

Приведем пример использования интерфейса, который наследует другой интерфейс (другие интерфейсы). Такие интерфейсы можно назвать многослойными. Для многослойных интерфейсов существует такое правило: если класс реализует интерфейс, который является производным цепочки других интерфейсов, то класс должен реализовать члены всех интерфейсов цепочки наследования. При этом сами наследуемые интерфейсы как типы должны иметь уровень доступности не ниже уровня доступности наследующего их класса. Но все члены, объявленные в наследуемых интерфейсах, всегда общедоступны в реализующем классе.

using System;
    
namespace Test
{
    interface A
    {
        void Method_A(); // Неявная доступность public
    
        // Method_A() - явно объявлен в A
    }
    
    // Одиночное наследование интерфейсов
    interface B : A
    {
        void Method_B(); // Неявная доступность public
     
        // Method_B() - явно объявлен в B
        // Method_A() - унаследован из A
   }
    
    interface C
    {
        void Method_C();
     
        // Method_C() - явно объявлен в C
   }
    
    // Множественное наследование интерфейсов
    interface D : B, C
    {
        void Method_D();
     
        // Method_D() - явно объявлен 
        // Method_C() - унаследован из C
        // Method_B() - унаследован из B 
        // Method_A() - унаследован из A через B
   }
    
    // Класс, который должен реализовать
    // все унаследованные интерфейсы
    class E : D
    {
        public void Method_A()
        {
            Console.WriteLine("Реализация Method_A()");
        }
    
        public void Method_B()
        {
            Console.WriteLine("Реализация Method_B()");
        }
    
        public void Method_C()
        {
            Console.WriteLine("Реализация Method_C()");
        }
    
        public void Method_D()
        {
            Console.WriteLine("Реализация Method_D()");
        }
    }
    
    // Вызывающий клиентский код
    class MyClass
    {
        public MyClass()
        {
            E ob = new E();
            ob.Method_A();
            ob.Method_B();
            ob.Method_C();
            ob.Method_D();
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Наследование интерфейсов";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 60;
            Console.WindowHeight = 10;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}

Вот результат выполнения примера

Диаграмма UML для приведенного кода, построенная в Visual Studio 2005, выглядит следующим образом

Закрытая реализация членов интерфейса

В предыдущем примере хоть и применялись цепочки интерфейсов и множественное наследование, но все их члены имели разные имена. Поэтому в реализующем классе не возникло коллизий неоднозначности. В однослойном (ненаследующем) интерфейсе тоже не может быть членов с совпадающими именами и сигнатурами.

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

Такой прием можно использовать и тогда, когда все реализуемые члены различные, но нужно ограничить прямую доступность реализующих интерфейс методов от внешнего кода. Например, реализующий класс предыдущего примера можно переписать так

using System;
    
namespace Test
{
    internal interface A // Доступен внутри текущей сборки
    {
        void Method_A(); // Неявная доступность public
    }
    
    // Одиночное наследование интерфейсов
    internal interface B : A // Доступен внутри текущей сборки
    {
        void Method_B(); // Неявная доступность public
    
        // Method_B() - явно объявлен в B
        // Method_A() - унаследован из A
    }
    
    // Изолированный интерфейс
    // Доступность по умолчанию internal
    interface C // Доступен внутри текущей сборки
    {
        void Method_C();
    
        // Method_C() - явно объявлен 
    }
    
    // Множественное наследование интерфейсов
    internal interface D : B, C // Доступен внутри текущей сборки
    {
        void Method_D();
    
        // Method_D() - явно объявлен 
        // Method_C() - унаследован из C
        // Method_B() - унаследован из B 
        // Method_A() - унаследован из B
    }
    
    // Класс, который должен реализовать
    // все унаследованные интерфейсы
    class E : D
    {
        void A.Method_A() // Здесь нельзя употреблять модификаторы доступа 
        {
        Console.WriteLine("Закрытая реализация Method_A()");
        }
        public void Method_A()// Функция доступа к закрытому члену
        {
        A a = this; // Неявное приведение типов (снижение полномочий ссылки)
        a.Method_A();
        }
    
        void B.Method_B() // Здесь нельзя употреблять модификаторы доступа
        {
        Console.WriteLine("Закрытая реализация Method_B()");
        }
        public void Method_B()// Функция доступа к закрытому члену
        {
        B b = this; // Неявное приведение типов (снижение полномочий ссылки)
        b.Method_B();
        }
    
        void C.Method_C() // Здесь нельзя употреблять модификаторы доступа
        {
        Console.WriteLine("Закрытая реализация Method_C()");
        }
        public void Method_C()// Функция доступа к закрытому члену
        {
        //C c = this;
        //c.Method_C();
        // Явное приведение типов (снижение полномочий ссылки)
        ((C)this).Method_C(); // Другой синтаксис
        }
    
        void D.Method_D() // Здесь нельзя употреблять модификаторы доступа
        {
        Console.WriteLine("Закрытая реализация Method_D()");
        }
        public void Method_D()// Функция доступа к закрытому члену
        {
        //D d = this;
        //d.Method_D();
        // Явное приведение типов (снижение полномочий ссылки)
        ((D)this).Method_D(); // Другой синтаксис
        }
    }
    
    // Вызывающий клиентский код
    class MyClass
    {
        public MyClass()
        {
        E ob = new E();
        // Вызываем закрытые члены реализации интерфейса 
        // через публичные функции доступа
        ob.Method_A();
        ob.Method_B();
        ob.Method_C();
        ob.Method_D();
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
        // Настройка консоли
        Console.Title = "Закрытая реализация интерфейсов";
        Console.ForegroundColor = ConsoleColor.White;
        Console.CursorVisible = false;
        Console.WindowWidth = 60;
        Console.WindowHeight = 10;
    
        new MyClass();// Чтобы сработал конструктор
    
        Console.ReadLine();
        }
    }
}

Результат будет таким

Диаграмма UML для приведенного кода, построенная в Visual Studio 2005, выглядит следующим образом

Реализующий класс явно адресует унаследованный от интерфейса метод. Синтаксис допускается только закрытое по умолчанию объявление реализации метода (без применения любых модификаторов доступа), чтобы наверняка исключить неопределенность во внешнем по отношению к реализующему классу коде. Для возможности вызова реализации метода из внешнего кода применяют дополнительные public -функции доступа, из которых вызывают private -реализацию. Они повторяют имена интерфейсных функций или могут иметь любые другие имена, но в последнем случае идея интерфейсов теряет смысл.

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

Если члены интерфейсов имеют не только одинаковое имя, но и одинаковую сигнатуру, и хотя бы один из них реализован, то оставшиеся члены нейтрализовывать необязатально.

Если наследуемые интерфейсы имеют абсолютно одинаковые члены (имя и сигнатура), то их можно рассматривать как одинаковые инструкции, реализовать только один член, а остальные скрывать не необязательно. Это ведь одинаковый шаблон. Но все различающиеся ненужные члены надо скрывать пустой закрытой реализацией.

using System;
    
namespace Test
{
    interface A
    {
        void Show();
        void Foo();
    }
    
    interface B
    {
        void Show();
        int Foo(string str);
    }
    
    // Обязаны реализовать все, но все клиенту не нужно 
    class C : A, B
    {
        // Реализуем то, что нужно клиенту 
        void A.Show()
        {
            Console.WriteLine("Реализация A.Show()");
        }
    
        public void Show()
        {
            ((A)this).Show();
        }
    
        int B.Foo(string str)
        {
            Console.WriteLine("Реализация B.Foo()");
            return 0;
        }
        public int Foo(string str)
        {
            return ((B)this).Foo(str);
        }
    
        // Нейтрализуем все остальное, что клиенту не нужно 
        void A.Foo() { }
    }
    
    // Вызывающий код
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Применение интерфейсов";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 30;
            Console.WindowHeight = 3;
    
            C obj = new C();
            obj.Show();
            obj.Foo(String.Empty);
    
            Console.ReadLine();
        }
    }
}

В приведенном примере метод Show() имеет одинаковую сигнатуру в обоих наследуемых интерфейсах, поэтому достаточно только одной выборочной реализации. Метод Foo() имеет разную сигнатуру, поэтому требует индивидуальной реализации для каждого интерфейса.

Диаграмма UML данного примера будет такой

Результат выполнения

Реализация методов интерфейса как виртуальных

using System;
    
namespace Test
{
    // Объявление интерфейса
    interface IDraw
    {
        void Show();
    }
    
    // Реализуем интерфейсный метод как виртуальный
    class A : IDraw
    {
        string name = "Класс A";
        public virtual void Show()
        {
            Console.WriteLine("\t" + name);
        }
    }
    
    // Переопределяет унаследованный виртуальный метод
    class B : A
    {
        string name = "Класс B";
        public override void Show()
        {
            Console.WriteLine("\t" + name);
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public MyClass()
        {
            Console.WriteLine("Адресация ссылками типов объектов:");
            A a = new A(); a.Show();
            B b = new B(); b.Show();
    
            Console.WriteLine("\nАдресация ссылкой 
              типа унаследованного интерфейса\n"
                + "(динамический полиморфизм):");
            IDraw refInterface = new A(); refInterface.Show();
            refInterface = new B(); refInterface.Show();
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Производить объекты 
              абстрактных классов запрещено";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 60;
            Console.WindowHeight = 10;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}

Применение библиотечных интерфейсов

В библиотеке .NET Framework объявлено множество интерфейсов, применение некоторых из них мы здесь проиллюстрируем.

Создание индексаторов и нумераторов

Пусть некоторый класс содержит коллекцию однотипных элементов, которые нужно перебрать с помощью цикла foreach. Такие коллекции называются нумераторами. В этом случае класс должен реализовать интерфейсы System.Collections.IEnumerable и System .Collections.IEnumerator, расположенные в сборке mscorlib.

В интерфейсе IEnumerable объявлен единственный метод

IEnumerator GetEnumerator()

который возвращает ссылку на сам объект коллекции. Его в реализующем классе нужно определять так

public IEnumerator GetEnumerator()
  { 
           // Приводим ссылку на объект к интерфейсному типу
            // и возвращаем адрес экземпляра класса
            return (IEnumerator)this;
        }

Интерфейс IEnumerator содержит одно свойство и два метода, приведенные в таблице

Члены библиотечного интерфейса IEnumerator
Член Пояснения
public object Current { get; } Возвращает текущий элемент в коллекции
bool MoveNext() Переводит внутренний указатель к следующему элементу коллекции. Если коллекция исчерпана, то возвращается false
void Reset() Устанавливает указатель перед первым элементом коллекции

Создание индексаторов

Пусть мы создали пользовательский класс Person с данными сотрудника и класс Persons, который будет содержать массив объектов класса Person с данными сотрудников. Для того, чтобы можно было обращаться в классе Persons к отдельным объектам-элементам массива, характеризующим отдельных сотрудников, нужно в нем предусмотреть индексатор. Обрабатывать отдельных сотрудников внутри класса Persons можно только как элементы массива. Вот код этого примера

using System;
    
namespace Test
{
    class Person
    {
        // Закрытые поля с данными сотрудника
        string name;    // Имя
        int age;        // Возраст
    
        // Конструктор по умолчанию
        public Person()
        {
            name = "Петров";
            age = 21;
        }
    
        // Конструктор с параметрами
        public Person(string name, int age)
        {
            this.name = name;
            this.age = age;
        }
    
        // Свойства чтения/записи
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
    
        public int Age
        {
            get { return age; }
            set { age = value; }
        }
    }
    
    class Persons
    {
        // Объявляем закрытую ссылку-поле на массив сотрудников
        private Person[] personArray;
    
        // Конструктор
        public Persons()
        {
            personArray = new Person[]{
                new Person("Иванов",20),
                new Person(),// По умолчанию Person("Петров", 21)
                new Person("Сидоров", 22)
            };
        }
    
        // Свойство-индексатор
        public Person this[int index]
        {
            get
            {
                if (index < 0)
                    return personArray[0];
                else if (index > personArray.Length - 1)
                    return personArray[personArray.Length - 1];
                else
                    return personArray[index];
            }
            set
            {
                if (index < 0)
                    personArray[0] = value;
                else if (index > personArray.Length - 1)
                    personArray[personArray.Length - 1] = value;
                else
                    personArray[index] = value;
            }
        }
    
        // Количество сотрудников
        public int Length
        {
            get { return personArray.Length; }
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public MyClass()
        {
            // Создаем список сотрудников
            Persons pers = new Persons();
            // Выводим информацию о сотрудниках по индексатору
            Console.WriteLine("Список сотрудников:");
            for (int i = 0; i < pers.Length; i++)
                Console.WriteLine("{2}) {0}, возраст {1}",
                    pers[i].Name, pers[i].Age, i + 1);
    
            /* Нужно реализовать интерфейсы IEnumerable, IEnumerator
            Console.WriteLine("\nЕще один список сотрудников:");
            int j = 0;
            foreach (Person per in pers)
                Console.WriteLine("{2}) {0}, возраст {1}",
                    per.Name, per.Age, j = j + 1);
            //*/
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Применение индексаторов";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 30;
            Console.WindowHeight = 10;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}

Создание нумераторов

Если для перебора внутренних элементов объекта Persons мы попробуем воспользоваться циклом foreach (закомментирован в конструкторе MyClass() ), то получим сообщение об ошибке. Реализация классом Persons интерфейсов IEnumerable и IEnumerator позволяет превратить экземпляр этого класса в коллекцию. Только нужно не забыть присоединить к файлу пространство имен System.Collections, где находятся эти интерфейсы.

using System;
using System.Collections;
    
namespace Test
{
    class Person
    {
        // Закрытые поля с данными сотрудника
        string name;    // Имя
        int age;        // Возраст
    
        // Конструктор по умолчанию
        public Person()
        {
            name = "Петров";
            age = 21;
        }
    
        // Конструктор с параметрами
        public Person(string name, int age)
        {
            this.name = name;
            this.age = age;
        }
    
        // Свойства чтения/записи
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
    
        public int Age
        {
            get { return age; }
            set { age = value; }
        }
    }
    
    class Persons : IEnumerable, IEnumerator 
    {
        // Реализация IEnumerable
        public IEnumerator GetEnumerator()
        {
            return (IEnumerator)this;
        }
    
        // Реализация IEnumerator
        int pos = -1;   // Текущая позиция элемента в массиве
    
        public bool MoveNext()
        {
            if (pos < personArray.Length - 1)
            {
                pos++;
                return true;
            }
            else
                return false;
        }
    
        public void Reset() { pos = -1; }
    
        public object Current
        {
            get { return personArray[pos]; }
        }
    
        // Объявляем ссылку-поле на массив сотрудников
        private Person[] personArray;
    
        // Конструктор
        public Persons()
        {
            personArray = new Person[]{
                new Person("Иванов",20),
                new Person(),// По умолчанию Person("Петров", 21)
                new Person("Сидоров", 22)
            };
        }
    
        // Свойство-индексатор
        public Person this[int index]
        {
            get
            {
                if (index < 0)
                    return personArray[0];
                else if (index > personArray.Length - 1)
                    return personArray[personArray.Length - 1];
                else
                    return personArray[index];
            }
            set
            {
                if (index < 0)
                    personArray[0] = value;
                else if (index > personArray.Length - 1)
                    personArray[personArray.Length - 1] = value;
                else
                    personArray[index] = value;
            }
        }
    
        // Количество сотрудников
        public int Length
        {
            get { return personArray.Length; }
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public MyClass()
        {
            // Создаем список сотрудников
            Persons pers = new Persons();
            // Выводим информацию о сотрудниках по индексатору
            Console.WriteLine("Список сотрудников:");
            for (int i = 0; i < pers.Length; i++)
                Console.WriteLine("{2}) {0}, возраст {1}",
                    pers[i].Name, pers[i].Age, i + 1);
    
            //* Нужно реализовать интерфейсы IEnumerable, IEnumerator
            Console.WriteLine("\nЕще один список сотрудников:");
            int j = 0;
            foreach (Person per in pers)
                Console.WriteLine("{2}) {0}, возраст {1}",
                    per.Name, per.Age, j = j + 1);
            //*/
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Применение индексаторов";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 30;
            Console.WindowHeight = 10;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}

Делегаты

Делегат, это тип, задающий сигнатуру методов и обеспечивающий механизм их обобщенного вызова. Вначале объявляется сам делегат как тип и сразу же задается прототип функций, которые в будущем могут адресоваться экземплярами этого типа. Затем объявляется ссылка типа делегата, которая будет указывать на коллекцию экземпляров делегата. И наконец, создаются сами объекты делегата, каждый из которых адресует одну функцию, и комбинируются в коллекцию, на которую указывает объявленная ссылка. Коллекция представляет собой список адресуемых делегатом функций и поддерживается операциями += и -= (или статическими методами Delegate.Combine() и Delegate.Remove() ).

Экземпляр делегата способен накапливать в себе ссылки на методы объявленной сигнатуры и вызывать эти методы либо по одному, либо сразу весь список.

Одноадресная работа делегатов

Во время выполнения программы один и тот же делегат можно использовать для вызова разных методов одинаковой сигнатуры простой заменой метода, на который ссылается делегат. В том и состоит главное достоинство делегата, что адресуемый им метод определяется не на этапе компиляции, а в период выполнения. Заменяя в делегате адресуемые функции мы обеспечим их полиморфный вызов.

Область видимости делегата можно регулировать точно так же, как и область видимости любого другого типа в приложении.

using System;
    
namespace Test
{
    class DelegateTest
    {
        // Объявляем функции с сигнатурой делегатов
        public void Show1()
        {
            Console.WriteLine("Вызов: void Show1()");
        }
    
        // Объявляем функции с сигнатурой делегатов
        public void Show2()
        {
            Console.WriteLine("Вызов: void Show2()");
        }
    
        // Объявляем функции с сигнатурой делегатов
        public int Draw1(string str1)
        {
            Console.WriteLine("Вызов: {0}", str1);
            return 1;
        }
    
        // Объявляем функции с сигнатурой делегатов
        public int Draw2(string str2)
        {
            Console.WriteLine("Вызов: {0}", str2);
            return 2;
        }
    
        // Объявляем статическую функцию
        public static int Print(string str)
        {
            Console.WriteLine("Вызов: {0}", str);
            return 0;
        }
    }
       
    // Объявляем делегат в пространстве имен
    delegate void TypeShow();
    
    // Вызывающая сторона
    class MyClass
    {
        // Объявляем делегат в классе
        delegate int TypeDraw(string str);
    
        public MyClass()
        {
            // Создаем экземпляр класса с методами
            DelegateTest delegateTest = new DelegateTest();
    
            // Объявляем ссылки на объекты делегатов
            TypeShow typeShow;
            TypeDraw typeDraw;
    
            // Создаем объекты делегатов
            typeShow = new TypeShow(delegateTest.Show1);
            typeDraw = new TypeDraw(delegateTest.Draw1);
    
            // Вызываем методы посредством делегатов
            typeShow();
            typeDraw("int Draw1(string str1)");
    
            // Адресуемся к другим методам с той же сигнатурой
            typeShow = new TypeShow(delegateTest.Show2);
            typeDraw = new TypeDraw(delegateTest.Draw2);
    
            // Вызываем другие методы посредством делегатов
            typeShow();
            typeDraw("int Draw2(string str2)");
    
            // Вызываем статический метод 
            // посредством подходящего делегата 
            typeDraw = new TypeDraw(DelegateTest.Print);
            typeDraw("static int Print(string str)");
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Применение делегатов";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 40;
            Console.WindowHeight = 7;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}

Делегат может адресовать как методы объекта (экземпляра класса), так и статические методы. Главное, чтобы заголовки объявления делегата и объявления метода совпадали как по типу возвращаемого значения, так и по сигнатуре.

Многоадресная работа делегатов

Для методов, имеющих одинаковую сигнатуру и не возвращающих значение (только с void ) с помощью делегата можно организовать сразу цепочку вызовов.

using System;
    
namespace Test
{
    class MultiTest
    {
        // Статическое поле
        static int x = 1;
    
        // Объявляем функции с одинаковой сигнатурой
        public void Handler1(string name)
        {
            Console.WriteLine(name + x++);
        }
    
        public void Handler2(string name)
        {
            Console.WriteLine(name + x++);
        }
    
        public void Handler3(string name)
        {
            Console.WriteLine(name + x++);
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        // Объявляем делегат в классе
        delegate void TypeHandler(string text);
    
        public MyClass()
        {
            // Создаем экземпляр класса с методами
            MultiTest obj = new MultiTest();
    
            // Создаем объект-делегат и заполняем адресами
            TypeHandler Handler = new TypeHandler(obj.Handler1);
            Handler += new TypeHandler(obj.Handler2);
            Handler += obj.Handler3;    // Упрощенный синтаксис
    
            // Вызываем цепочку методов
            Handler("Вызов: Handler");
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = "Многоадресные делегаты";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 30;
            Console.WindowHeight = 10;
    
            new MyClass();// Чтобы сработал конструктор
    
            Console.ReadLine();
        }
    }
}

Делегаты являются экземплярами типа System.MulticastDelegate, который в свою очередь наследует абстрактный класс System.Delegate. Экземпляр типа MulticastDelegate может хранить в себе одну или сразу несколько ссылок на методы. В любом случае мы не можем явно объявлять делегат с помощью типа MulticastDelegate. Это делается с помощью ключевого слова delegate, но неявно порождается объект класса MulticastDelegate. Если посмотреть на структуру последнего примера через панель Class View, то это становится очевидным.

Пустые делегаты

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

using System;
    
namespace Test
{
    class MultiTest
    {
        // Объявляем функции одного прототипа
        public void Handler1()
        {
            Console.WriteLine("Исполняется Handler1");
        }
    
        public void Handler2()
        {
            Console.WriteLine("Исполняется Handler2");
        }
    
        public void Handler3()
        {
            Console.WriteLine("Исполняется Handler3");
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        // Для заголовка консольного окна
        public static string Title = "Пустые делегаты";
    
        // Объявляем делегат как член класса
        delegate void MyDelegate();
    
        public MyClass()
        {
            // Создаем экземпляр класса с методами
            MultiTest obj = new MultiTest();
    
            // Создаем объект-делегат и заполняем ссылками на функции
            // Инициализируем первой ссылкой
            MyDelegate del = new MyDelegate(obj.Handler1);
            // Добавляем другие ссылки
            del += obj.Handler2;
            del += obj.Handler3;
    
            // Вызываем цепочку методов
            del();
            Console.WriteLine();
    
            // Извлекаем метод и опять вызываем цепочку методов
            del -= obj.Handler1;
            del();
            Console.WriteLine();
    
            // Извлекаем метод и опять вызываем цепочку методов
            del = (MyDelegate)Delegate.Remove(del, new MyDelegate(obj.Handler2));
            del();
            Console.WriteLine();
    
            // Извлекаем последний метод и пытаемся адресоваться к пустому делегату
            del -= obj.Handler3;
            //del(); // Будет выброшено исключение, нужно проверять!!!
            if (del != null)
            {
                del(); // Так все нормально!
            }
            else
                Console.WriteLine("Список делегата исчерпан!");
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = MyClass.Title;
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 30;
            Console.WindowHeight = 11;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}

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

Некоторые члены типа MulticastDelegate

Известно, что в C# методы могут быть статическими ( static ) или экземплярными ( instance ). Статические методы адресуются по имени класса, а экземплярные - по имени экземпляра. При загрузке кода в память компьютера создается единственный объект-тип и несколько объектов-экземпляров. Объект-тип содержит в единственном экземпляре все методы типа и статические данные, общие для всех объектов-экземпляров. А объекты-экземпляры содержат индивидуальные данные, к которым применяются методы объекта-типа. Однако статические методы, которые могут вызываться без создания экземплярных данных, должны обрабатывать только статические данные. Экземплярные же методы могут обрабатывать как индивидуальные, так и общие данные.

Для обычного программирования достаточно рассмотреных нами сведений о делегатах. Но в ряде случаев может потребоваться более тонкая работа с делегатами. Тип MulticastDelegate сам имеет несколько своих собственных членов, но большую часть он наследует от абстрактного класса Delegate. Члены типа MulticastDelegate содержат все необходимые сервисы по управлению делегатами. Некоторые из этих членов экземплярные, а некоторые - статические. Ниже приведены некоторые из сервисов типа MulticastDelegate

Некоторые члены типа MulticastDelegate, унаследованные от Delegate
Член Пояснения
Method { get; } Возвращает последнюю добавленную в делегат ссылку на метод
Target { get; } Возвращает ссылку на объект, связанный с адресуемым делегатом экземплярным методом
DynamicInvoke() Позволяет динамически обратиться к функциям, привязанным к делегату
GetInvocationList() Возвращает список функций, привязанных к делегату
operator == Перегруженный оператор для сравнения содержимого делегатов
operator != Перегруженный оператор, позволяющий определить различие содержимого делегатов
Combine() Добавляет в объект делегата новую ссылку на функцию
Remove() Удаляет из объекта делегата ссылку на указанную функцию
CreateDelegate() Динамически создает делегат

Свойство Method

using System;
    
namespace Test
{
    class MultiTest
    {
        // Статическое поле
        static int x = 1;
    
        // Объявляем функции с одинаковой сигнатурой
        public void Handler1()
        {
            Console.WriteLine("Исполняется Handler1");
        }
    
        public void Handler2()
        {
            Console.WriteLine("Исполняется Handler2");
        }
    
        public void Handler3()
        {
            Console.WriteLine("Исполняется Handler3");
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public static string Title = "Свойство Method";
    
        // Объявляем делегат как член класса
        delegate void MyDelegate();
    
        public MyClass()
        {
            // Создаем экземпляр класса с методами
            MultiTest obj = new MultiTest();
    
            // Создаем объект-делегат и заполняем ссылками на функции
            // Инициализируем первой ссылкой
            MyDelegate del = new MyDelegate(obj.Handler1);
            // Добавляем другие ссылки
            del += obj.Handler2;
            del += obj.Handler3;
    
            // Вызываем цепочку методов
            del();
            Console.WriteLine();
    
            del.Method.Invoke(obj, null); // Вызываем последний метод
    
            Console.WriteLine("\nПрототип последнего метода:\n{0}",
                del.Method.ToString());
    
            Console.WriteLine("\nИмена адресуемых методов:");
            Delegate[] listMethods = del.GetInvocationList();
            for (int i = 0; i < listMethods.Length; i++)
                Console.WriteLine("{0}", listMethods[i].Method.Name);
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = MyClass.Title;
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 30;
            Console.WindowHeight = 14;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}

Свойство Target

Это свойство возвращает объект, связанный с адресуемым делегатом методом.

using System;
    
namespace Test
{
    class MultiTest
    {
        // Объявляем функции с одинаковой сигнатурой
        public void Handler1()
        {
            Console.WriteLine("Исполняется Handler1");
        }
    
        public void Handler2()
        {
            Console.WriteLine("Исполняется Handler2");
        }
    
        public void Handler3()
        {
            Console.WriteLine("Исполняется Handler3");
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public static string Title = "Применение 
    свойства Target делегата";
    
        // Объявляем делегат как член класса
        delegate void MyDelegate();
    
        public MyClass()
        {
            // Создаем экземпляр класса с методами
            MultiTest obj = new MultiTest();
    
            // Создаем объект-делегат и заполняем ссылками на функции
            // Инициализируем первой ссылкой
            MyDelegate del = new MyDelegate(obj.Handler1);
            // Добавляем другие ссылки
            del += obj.Handler2;
            del += obj.Handler3;
    
            // Вызываем цепочку методов
            Console.WriteLine("Многоадресный вызов 
    через делегат");
            del();
            Console.WriteLine();
    
            // Возвращает объект, с которым связан
            // последний адресуемый метод
            Console.WriteLine("Тип объекта: {0}", 
              del.Target.GetType().ToString());
            Console.WriteLine();
    
            // Повышаем полномочия извлеченной из Target ссылки 
            // на объект и повторно адресуемся напрямую 
            Console.WriteLine("Вызов через свойство 
    Target делегата");
            ((MultiTest)del.Target).Handler1();
            ((MultiTest)del.Target).Handler2();
            ((MultiTest)del.Target).Handler3();
            Console.WriteLine();
    
            // Адресуемся напрямую без делегата
            Console.WriteLine("Вызов через объект напрямую");
            obj.Handler1();
            obj.Handler2();
            obj.Handler3();
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = MyClass.Title;
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 40;
            Console.WindowHeight = 18;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}

Методы DynamicInvoke() и GetInvocationList()

Этот метод позволяет вызывать отдельные члены, адресуемые списком объекта-делегата, и задавать требуемые аргументы. Если член списка не имеет аргументов, то в качестве параметра метода используется null, иначе - массив параметров адресуемого члена.

Мы убедились, что объявление ссылки на объект-делегат прописывает только прототип методов, которые корректно может адресовать эта ссылка. Сам делегат как объект создается при заполнении списка адресуемыми функциями. Но при формировании списка в него добавляются только имена методов. Это приемлемо, если вызываются функции с пустой сигнатурой.

using System;
    
namespace Test
{
    class ShowPerson
    {
        // Объявляем функцию с аргументами
        public static void Handler(string name, int age)
        {
            Console.WriteLine("Сотрудник {0}, возраст {1}", name, age);
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public static string Title = "Вызов методов с параметрами";
    
        // Объявляем делегат как член класса
        delegate void MyDelegate(string name, int age);
    
        public MyClass()
        {   
            // Создаем и заполняем объект-делегат
            MyDelegate del = new MyDelegate(ShowPerson.Handler);
            // Добавляем другие ссылки
            int count = 3;
            for (int i = 1; i < count; i++)
            {
                del += ShowPerson.Handler;
            }
    
            // Вызываем цепочку методов с одинаковым параметром
            del("Иванов", 21);
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = MyClass.Title;
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 30;
            Console.WindowHeight = 4;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}

Здесь просматривается очень важный вывод, что все зарегистрированные в делегате методы стороннего класса получают, при вызове их с помощью делегата, одинаковую входную информацию.

А как быть, если с помощью делегата нужно адресовать методы, имеющие разные значения параметров. Вот тут-то и может пригодиться рассматриваемый метод делегата.

public object DynamicInvoke(params object[ ] args)

Но есть еще одна важная проблема, которую можно решить с помощью метода DynamicInvoke(), это надежность кода при адресации вызовов методов посредством делегата. Дело в том, что любой из цепочки метод при традиционном вызове может дать сбой. В результате система выдаст исключение и прервет вызовы остальных методов списка. Исключение мы можем обработать, но только для списка в целом, если не будем контролировать индивидуально каждый член списка делегата. Например, в приведенном фрагменте кода что-то успеет выполниться до выброса исключения, а что-то нет:

try
{
del(null, 0);
}
catch
{            }

Эти проблемы решает метод DynamicInvoke() совместно с GetInvocationList(), как показано в следующем примере

using System;
    
namespace Test
{
    class ShowPerson
    {
        // Функция с аргументами
        public static void Handler(string name, int age)
        {
            Console.WriteLine("Сотрудник {0}, возраст {1}", name, age);
        }
    
        // Проблемная функция с нормальным прототипом
        public static void ProblemHandler(string name, int age)
        {
            // Преднамеренно выбрасываем исключение
            throw new Exception();
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public static string Title = "Применение DynamicInvoke()";
    
        // Объявляем делегат
        delegate void MyDelegate(string name, int age);
    
        public MyClass()
        {
            //
            // Формируем список объекта-делегата
            //
            // Добавляем в список один проблемный метод
            MyDelegate del = new MyDelegate(ShowPerson.ProblemHandler);
    
            // Добавляем еще три нормальных метода
            int count = 3;
            for (int i = 0; i < count; i++)
            {
                del += ShowPerson.Handler;
            }
    
            object[] param = new object[2];// Объявили массив для параметров
            int j = 0;    // Объявили и инициализировали счетчик
            // Перебираем список вызовов делегата, включая и вызов проблемного метода
            foreach (Delegate d in del.GetInvocationList())
            {
                // Индивидуально формируем параметры методов
                switch (j)
                {
                    case 0:// Можно и не задавать, все равно для проблемного метода!
                        param[0] = "Мистер X";
                        param[1] = 99;
                        break;
                    // Для вызовов нормального метода
                    case 1:
                        param[0] = "Иванов";
                        param[1] = 21;
                        break;
                    case 2:
                        param[0] = "Петров";
                        param[1] = 22;
                        break;
                    case 3:
                        param[0] = "Сидоров";
                        param[1] = 23;
                        break;
                }
    
                j++;    // Счетчик
    
                // Защищенно вызываем адресуемые методы индивидуально 
                try
                {
                    d.DynamicInvoke(param);
                }
                catch (Exception exc)
                {
                    string str = d.Method.Name;
                    Console.WriteLine("Сбой метода {0}!!!", str);
    
                    str = exc.Message;   // Системное сообщение
                    // Разбиваем длинное сообщение пополам
                    int pos = str.Length / 2;
                    pos = str.IndexOf(' ', pos);// От средины первый пробел
                    Console.WriteLine("\"" +    // Экранируем кавычки
                        str.Substring(0, pos) +
                        Environment.NewLine +
                        str.Substring(pos + 1) +
                        "\"");                  // Экранируем кавычки
                    Console.WriteLine();        // Отделяем сообщение
                }
            }
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = MyClass.Title;
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 32;
            Console.WindowHeight = 8;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}

Перегруженные операторы 'operator ==' и 'operator !='

Эти операторы позволяют подтвердить или опровергнуть абсолютную идентичность списков функций сравниваемых делегатов.

using System;
    
namespace Test
{
    class Handler
    {
        // Функции
        public void Handler1()
        {
        }
    
        public void Handler2()
        {
            ;
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public static string Title = "Применение 
          операторов '==' и '!='";
    
        // Объявляем делегат
        delegate void MyDelegate();
    
        // Объявляем ссылки на делегаты как 
        // поля для видимости в методах класса
        MyDelegate del0, del1;
    
        public MyClass()
        {
            // Создаем объект
            Handler obj = new Handler();
    
            // Формируем список вызовов объекта-делегата
            del0 = new MyDelegate(obj.Handler1);
            del0 += new MyDelegate(obj.Handler2);
    
            // Еще один делегат с тем же списком вызовов
            del1 = new MyDelegate(obj.Handler1);
            del1 += obj.Handler2;   // Упрощенный синтаксис
    
            // Сравниваем делегаты с полность совпадающими списками
            Compare();
    
            // Делегат прежним содержимым, но в другом порядке
            del1 = new MyDelegate(obj.Handler2);
            del1 += obj.Handler1;   // Упрощенный синтаксис
    
            // Сравниваем делегаты с одинаковым содержимым, но разным порядком
            Compare();
    
            // Изменяем содержимое одного из делегатов
            del0 -= obj.Handler2;
    
            // Опять сравниваем делегаты с разным содержимым
            Compare();
        }
    
        void Compare()
        {
            if (del0 == del1)
                Console.WriteLine("Списки делегатов идентичны");
            else
                Console.WriteLine("Списки делегатов различны");
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = MyClass.Title;
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 33;
            Console.WindowHeight = 4;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}

Обратите внимание, что для равенства делегатов важен не только состав функций, но и порядок их следования в списке.

Методы Combine() и Remove()

Это статические методы, способные получать новый делегат как объединение списков двух делегатов одного и того же типа, или получать новый делегат с усеченным списком при тех же условиях. Методы могут использоваться вместо перегруженных операций ' += ' или ' -= ' при последовательном изменении списка одиночными функциями по синтаксису

MyDelegate del;
            del = new MyDelegate(obj.Handler1);
            del += new MyDelegate(obj.Handler2);
    
            // Или
            del = new MyDelegate(obj.Handler1);
            del = (MyDelegate)Delegate.Combine(del, 
              new MyDelegate(obj.Handler2));
using System;
    
namespace Test
{
    class Handler
    {
        // Функции
        public void Handler1()
        {
            Console.WriteLine("Вызов Handler1()");
        }
    
        public void Handler2()
        {
            Console.WriteLine("Вызов Handler2()");
        }
    }
    
    // Вызывающая сторона
    class MyClass
    {
        public static string Title = "Применение Combine() и Remove()";
    
        // Объявляем делегат как член класса
        delegate void MyDelegate();
    
        public MyClass()
        {
            // Создаем объект
            Handler obj=new Handler();
    
            // Формируем список объекта-делегата из 2 вызовов
            MyDelegate del1 = new MyDelegate(obj.Handler1);
            del1 += new MyDelegate(obj.Handler1);
    
            // Еще один делегат того же типа из 3 вызовов
            MyDelegate del2 = new MyDelegate(obj.Handler2);
            del2 += obj.Handler2;   // Упрощенный синтаксис
            del2 = del2 + obj.Handler2; // То же самое
    
            // Новый делегат из 5 вызовов 
            MyDelegate del3 = (MyDelegate)Delegate.Combine(del1, del2);
    
            // Вызываем 5 функций
            del3();
            Console.WriteLine();
    
            // Вновь формируем делегаты
            del1 = new MyDelegate(obj.Handler1);
            del1 += obj.Handler2;
    
            // Усекаем первый список вторым 
            del2 = (MyDelegate)Delegate.Remove(del3, del1);
    
            // Вызываем оставшиеся 3 функции
            del2();
        }
    }
    
    // Запуск
    class Program
    {
        static void Main()
        {
            // Настройка консоли
            Console.Title = MyClass.Title;
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 33;
            Console.WindowHeight = 10;
    
            new MyClass();// Исполняем
    
            Console.ReadLine();
        }
    }
}

События в C#

Рассылка сообщений с помощью делегата

Чтобы любые объекты могли обмениваться информацией, они должны разговаривать на одном языке. В случае объектно-ориентированного программирования таким условием является одинаковый прототип функций, определяемый делегатом. Многоадресная работа делегата удобна тем, что можно послать одинаковое сообщение сразу нескольким объектам, функции которых зарегистрированы в списке делегата как обработчики этого сообщения. Список делегата можно назвать по-разному:

  • список вызываемых функций,
  • список обработчиков,
  • список адресатов,
  • список получателей и т.д.
  • Напомним, что если список адресатов пуст, то самого объекта-делегата не существует и ссылка на него имеет значение null. Без проверки этого обстоятельства при попытке вызова адресатов пустой ссылкой-делегатом будет сгенерировано стандартное исключение NullReferenceException.

    Вот пример однонаправленной рассылки сообщения

    using System;
        
    namespace Test
    {
        // Образец сообщения определяется делегатом
        delegate void Message(string message);
        
        // Источник сообщения
        class SourceMessage
        {
            // Общедоступное поле ссылки на объект-делегат,
            // который наполнится указателями
            // на функции в классах-получателях 
            public Message mail;
        
            // Необязательное поле с рассылаемым сообщением
            public string message;
        
            // Разослать сообщение - функция диспетчеризации
            public void DispatchMessage(string mess)
            {
                // Сохраняем внешнее сообщение во внутреннем поле
                message = mess;
        
                // Инициируем рассылку сообщения всем, 
                // кто зарегистрировался в объекте-делегате
                if (mail != null)   // Если не пустой делегат
                    mail(mess);
            }
        }
        
        // Получатель сообщения
        class Addressee1
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee1 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Получатель сообщения
        class Addressee2
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee2 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            static public string Title = "Рассылка 
        сообщений делегатом";
        
            public MyClass()
            {
                // Создаем объекты источника и получателей сообщения
                SourceMessage source = new SourceMessage();
                Addressee1 obj1 = new Addressee1();
                Addressee2 obj2 = new Addressee2();
        
                // Формируем список вызовов объекта-делегата
                source.mail += new Message(obj1.Handler);
                source.mail += new Message(obj2.Handler);
        
                // Рассылаем сообщение напрямую через делегат
                source.mail("Первое сообщение");
                Console.WriteLine();
        
                // Рассылаем сообщение через функцию диспетчеризации
                source.DispatchMessage("Второе сообщение");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 32;
                Console.WindowHeight = 10;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Обратите внимание, что при добавлении в делегат первой ссылки на функцию нам не обязательно использовать операцию ' = ', а можно и ' += ', несмотря на то, что делегат еще пустой.

    В классе-источнике сообщения SourceMessage мы объявили два общедоступных поля: ссылку mail на объект-делегат и переменную message для хранения сообщения внутри класса. Поле-переменная message для работы программы не нужна, но мы ее ввели просто для того, чтобы сравнить представления в панели Class View.

    Если теперь посмотреть на состав класса SourceMessage через панель Class View, то мы не сможем различить две этих переменные - обычные поля, да и только. Но ведь поле mail не выполняет роль хранителя ссылки на обычный экземпляр класса или структуры, а предназначено для создания механизма адресации функций.

    Превращение делегата в событие

    А надо бы их различить, поскольку они предназначены совершенного для разных целей и имеют разные механизмы обработки. Чтобы внести ясность в этот вопрос, разработчики C# и библиотеки .NET Framework решили объявлять ссылку на объект делегата, когда он используеся для рассылки сообщений, с ключевым словом event и называть ее событием.

    Остальной механизм работы такого делегата-события остался прежним, кроме одного: теперь событие-делегат является собственностью класса-источника и вызывать его напрямую из класса-приемника как поле сообщений нельзя. Событие должно инициироваться функцией-членом класса, отсылающего сообщение ( функция диспетчеризации ), в котором оно для этих целей и объявлено. В приемнике сообщения можно только подписаться на событие источника путем прикрепления нужных функций-обработчиков, а далее вызвать функцию диспетчеризации владельца события. Но по-прежнему наполнение списка события конкретными адресами функций выполняют объекты-делегаты.

    Объявление в классе-источнике делегата как события означает только возможность на него подписаться функциям внешнего кода, но не гарантирует, что на него действительно кто-то подпишется в момент возбуждения этого события в классе-источнике. Поэтому функция диспетчеризации вначале проверяет, не пустой ли делегат, и только потом выполняет вызов связанных с ним методов-обработчиков.

    В соответствии со сказанным изменим предыдущий пример совсем на немного (выделено в коде) и получим прежний результат, только теперь уже с участием события как члена класса

    using System;
        
    namespace Test
    {
        // Образец сообщения определяется делегатом
        delegate void Message(string message);
        
        // Источник сообщения
        class SourceMessage
        {
            // Общедоступное поле ссылки на объект-делегат,
            // который теперь называется событием и наполняется
            // указателями на функции в классах-получателях 
            public event Message mail;
        
            // Необязательное поле с рассылаемым сообщение
            public string message;
        
            // Разослать сообщение - функция диспетчеризации
            public void DispatchMessage(string mess)
            {
                // Сохраняем внешнее сообщение во внутреннем поле
                message = mess;
        
                // Инициируем рассылку сообщения всем, 
                // кто зарегистрировался в объекте-делегате
                if (mail != null)   // Если не пустой делегат
                    mail(mess);
            }
        }
        
        // Получатель сообщения
        class Addressee1
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee1 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Получатель сообщения
        class Addressee2
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee2 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            static public string Title = "Рассылка 
        сообщений событием";
        
            public MyClass()
            {
                // Создаем объекты источника и получателей сообщения
                SourceMessage source = new SourceMessage();
                Addressee1 obj1 = new Addressee1();
                Addressee2 obj2 = new Addressee2();
        
                // Формируем список обработчиков события с 
          //помощью объектов-делегатов
                source.mail += new Message(obj1.Handler);
                source.mail += new Message(obj2.Handler);
        
                // Рассылаем сообщение только через функцию-член источника
                //source.mail("Первое сообщение");
                source.DispatchMessage("Первое сообщение");
                Console.WriteLine();
        
                // Рассылаем сообщение через функцию диспетчеризации
                source.DispatchMessage("Второе сообщение");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 32;
                Console.WindowHeight = 10;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Если теперь посмотреть на состав класса SourceMessage через панель Class View, то мы можем отличить переменную от события.

    В последнем примере событие объявлено в одном классе, подписка обработчиков выполнена в другом классе, а сами обработчики находятся в третьих двух классах. Но, как правило, обработчики принадлежат тому классу, который содержит в себе экземпляр класса-источника события и в котором выполнена подписка на событие. Кроме того, инициация события выполняется внешними причинами в результате действий пользователя или возникновения каких-то программных условий. Чаще всего событие инициируется самой операционной системой. В нашем случае мы имитируем внешнюю причину, используя для этого нашу функцию диспетчеризации DispatchMessage().

    Когда вместо объекта-делегата для передачи сообщений используется объект-событие, то включенные в список события функции называют обработчиками. Многие клиенты объекта с событием могут подписаться на его обработку. Тогда при возникновении события обработчики клиентов будут выполняться в том порядке, в котором они следуют в списке события. Как и в случае с делегатами, сбой любого обработчика прервет выполнение оставшихся членов списка события.

    Типичный способ создания событий

    В соответствии с парадигмой объектно-ориентированного программирования событие должно передаваться объектом-источником при возникновении определенных обстоятельств. Другие классы, содержащие в себе объект с общедоступным полем-событием, по отношению к этому событию могут выступать только как клиенты (внешний код). Они могут только подписаться традиционным способом на событие и прослушивать его, а при получении - реагировать кодом прикрепленных к событию своих обработчиков. Согласуем код нашего последнего примера с изложенными соображениями

    using System;
        
    namespace Test
    {
        // Образец сообщения определяется делегатом
        delegate void Message(object sender, string message);
        
        // Класс-источник сообщения
        class SourceMessage
        {
            // Общедоступное поле ссылки на событие
            public event Message Mail;
        
            // Метод диспетчеризации события, объявили виртуальным 
            // и защищенным для возможности замещения в наследниках
            protected virtual void OnMail(string mess)
            {
                // Инициируем рассылку сообщения всем, 
                // кто подписался на событие
                if (Mail != null)       // Если не пустой делегат
                    Mail(this, mess);   // Инициируем событие
            }
        
            // Объявляем и инициируем внутреннее поле базовым сообщением
            string message = "Сообщаю, что сработал таймер!!!\n"
                + "Текущее время ";
        
            // Объявляем внутреннее поле для видимости в методах
            System.Timers.Timer timer;
        
            // Конструктор класса-источника сообщения
            public SourceMessage()
            {
                // Создаем и запускаем таймер, который по истечении 
                // заданного времени инициирует рассылку сообщения
                timer = new System.Timers.Timer();
                timer.Interval = 5000d; // Сработает через 5 секунд
                timer.Elapsed += new System.Timers.ElapsedEventHandler
        (timer_Elapsed);
                timer.Start();      // Запускаем таймер
            }
        
            // Инициирование события из внешнего источника
            void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
            {
                // Извлекаем текущее системное время
                DateTime curTime = DateTime.Now; 
                // Дополняем базовое сообщение временем
                message += curTime.ToString();
        
                OnMail(message);    // Вызываем метод диспетчеризации
                timer.Stop();       // Останавливаем системный таймер
            }
        }
        
        // Класс-получатель сообщения
        class MyClass
        {
            static public string Title = 
                "Передача сообщения событием (через 5 секунд!)";
        
            public MyClass()
            {
                // Создаем объект с событием
                SourceMessage obj = new SourceMessage();
        
                // Подписываем на событие обработчик,
                // печатающий полученную информацию,
                // с помощью объекта-делегата
                obj.Mail += new Message(obj_Mail);
            }
        
            // Обработчик. Вызывается автоматически при возникновении причины
            void obj_Mail(object sender, string message)
            {
                String str = "Получена информация от класса "
                    + sender.GetType().Name + ":\n"
                    + "\"" + message + "\"";
                Console.WriteLine(str);
            }
       }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 45;
                Console.WindowHeight = 4;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    В последнем примере в классе-источнике сообщения мы объявили поле-ссылку на событие как общедоступную, чтобы в любом классе-клиенте, содержащем объект с событием, эта ссылка была видна и позволяла подписать обработчики. Для инициации события мы в качестве внешней причины применили срабатывание системного таймера. В реальных условиях это может быть что угодно, но чаще всего события генерируются операционной системы, как реакция на действия пользователя или как ответ на переход программных объектов в определенные состояния.

    Модель программирования, основанная на событиях, сейчас является наиболее популярной, а для некоторых задач - и единственно возможной. Она наиболее приспособлена для построения интерактивных приложений, где требуется организовать диалог с пользователем. Когда пользователь не производит никаких действий, приложение находится в состоянии простоя ( idle - простой) и требует минимальных ресурсов компьютера. Только консольные приложения, многие из которых называют утилитами, выполняются непрерывно от начала до конца, и возможно не требуют применения событий так остро. Но и они для обмена информацией объектов приложения тоже могут использовать механизм событий, в чем мы только что убедились на последнем примере.

    Создание событий с контролем адресатов

    Если мы только используем библиотечные классы, содержащие события, то нам вообще никакой способ создания событий знать необязатально. Уметь подписывать в клиенте обработчики на уже готовые события и понимать причины, приводящие к возникновению этих событий, - это все, что нам необходимо.

    Если мы хотим как-то повлиять на событие библиотечного класса, то нужно помнить, что событие объявляется общедоступными и может наследоваться как обычный член класса. Это дает нам право расширить библиотечный класс, в котором можно выполнить какие-то действия: скрыть событие, объявив его заново с ключевым словом new как закрытое, или переопределить диспетчер события. Диспетчер в библиотечных классах обычно объявляется виртуальным и может быть переопределен в производном классе или даже совсем скрыт для следующих потомков его переобъявлением.

    Независимо от того, создаем ли мы свои классы с событиями, или расширяем библиотечные классы, добавляя в них новые события, о существовании еще одного способа знать не помешает.

    Рассмотренный ранее способ создания объявления событий в классах-источниках считается стандартным. Мы создали таким способом класс с событием и теперь этим событием может пользоваться кто угодно, присоединяя к нему любые обработчики, лишь бы они имели разрешенный прототип. Такой способ не позволяет программисту осуществлять должный контроль за обработчиками при их добавлении и изъятии, как это может потребоваться в некоторых случаях. Для решения подобных задач существует другой способ создания события в классе-источнике, который является более гибким и его называют расширенным. Этот способ позволяет отслеживать в классе, содержащем событие, те обработчики, которые клиентский код будет присоединять к нему.

    Синтаксис создания события расширенным способом сильно напоминает создание в классе свойства. Он также основывается на закрытом базовом поле и также обертывает это поле общедоступным членом. Только вместо автоматически вызываемых аксессоров get и set используются ключевые слова add и remove, а вместо базового поля используется закрытое поле-делегат. Методы add и remove автоматически вызываются при добавлении обработчика в список делегата или удаления его из списка.

    Пусть мы создаем класс с событием и не хотим, чтобы какой-то определенный клиентский класс пользовался этим событием, т.е. один тип не хочет обслуживать другой неприятный ему тип. Тогда код создания события мог бы выглядеть так, как показано в следующем примере

    using System;
        
    namespace Test
    {
        // Объявление делегата как типа 
        delegate void Message(object sender, string message);
        
        // Класс-источник сообщения
        class SourceMessage
        {
            ////////////////////////////////////////////
            // Это был стандартный способ
            ////////////////////////////////////////////
            // public event Message Mail;
        
            ////////////////////////////////////////////
            // Это расширенный способ
            ////////////////////////////////////////////
            // Объявление внутреннего поля как экземпляра делегата
            private Message mail;
        
            // Создание события с контролем адресатов
            public event Message Mail
            {
                add     // Контролируем добавление обработчиков в список
                {
                    // Имя получателя сообщения
                    string targetName = value.Target.GetType().Name;
        
                    // Выявляем того, кого не любим!
                    if (targetName == "BadClass")
                        // Ласково уведомляем 
                        Console.WriteLine("Типу BadClass 
          доступ к событию запрещен!\n");
                    else
                        mail += value;
                }
                remove  // Удаление контролировать не будем. 
             //"Его там не стояло!" 
                {
                    mail -= value;
                }
            }
        
            /////////////////////////////////////////////
            // Все остальное то-же самое, кроме замены
            // события Mail на закрытое поле-делегат mail
            /////////////////////////////////////////////
            // Метод диспетчеризации события. Объявили виртуальным 
            // и защищенным для возможности замещения в наследниках
            protected virtual void OnMail(string mess)
            {
                // Инициируем рассылку сообщения всем, 
                // кто подписался на событие
                // Здесь используется поле-делегат, внутри можно (и нужно!)
                if (mail != null)       // Если не пустой делегат
                    mail(this, mess);   // Инициируем событие
            }
        
            // Объявляем и инициируем внутреннее поле базовым сообщением
            string message = "Сообщаю, что сработал таймер!!!\n"
                + "Текущее время ";
        
            // Объявляем внутреннее поле для видимости в методах
            System.Timers.Timer timer;
        
            // Конструктор класса-источника сообщения
            public SourceMessage()
            {
                // Создаем и запускаем таймер, который по истечении 
                // заданного времени инициирует рассылку сообщения
                timer = new System.Timers.Timer();
                timer.Interval = 5000d; // Сработает через 5 секунд
                timer.Elapsed += new System.Timers.ElapsedEventHandler
        (timer_Elapsed);
                timer.Start();      // Запускаем таймер
            }
        
            // Инициирование события из внешнего источника
            void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
            {
                // Извлекаем текущее системное время
                DateTime curTime = DateTime.Now;
                // Дополняем базовое сообщение временем
                message += curTime.ToString();
        
                OnMail(message);    // Вызываем метод диспетчеризации
                timer.Stop();       // Останавливаем системный таймер
            }
        }
        
        // Хороший класс-получатель сообщения
        class GoodClass
        {
            public GoodClass()
            {
                // Создаем объект с событием
                SourceMessage obj = new SourceMessage();
        
                // Подписываем на событие обработчик,
                // печатающий полученную информацию
                obj.Mail += new Message(obj_Mail);
            }
        
            // Обработчик
            void obj_Mail(object sender, string message)
            {
                String str = this.GetType().Name
                    + " получил информацию от "
                    + sender.GetType().Name + "\n"
                    + "следующего содержания:\n\"" 
         + message + "\"";
                Console.WriteLine(str);
            }
        }
        
        // Плохой класс, которому не разрешено получать сообщения
        class BadClass
        {
            public BadClass()
            {
                // Создаем объект с событием
                SourceMessage obj = new SourceMessage();
        
                // Подписываемся на событие в благостном неведении,
                // что мы попали в черный список и нас
                // не включат в список рассылки
                obj.Mail += new Message(obj_Mail);
            }
        
            // Обработчик
            void obj_Mail(object sender, string message)
            {
                String str = "Получена информация от класса "
                    + sender.GetType().Name + ":\n"
                    + "\"" + message + "\"";
                Console.WriteLine(str);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Расширенное создание 
        события (через 5 секунд!)";
        
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 47;
                Console.WindowHeight = 7;
        
                new GoodClass();// Исполняем хороший класс
                new BadClass(); // Исполняем плохой класс
        
                Console.ReadLine();
            }
        }
    }

    Из распечатки мы видим, что хотя событие класса GoodClass и было инициировано первым, но отработало позднее, поскольку ждало срабатывания системного таймера.

    Создание событий со списком делегатов

    В библиотеке .NET Framework для структурированной поддержки событий создан класс EventHandlerList, который может хранить в себе элементы, выполняющие функцию делегатов с присоединенными к ним обработчиками. Эти элементы маркируются ключами типа Object. Каждому ключу соответствует один или несколько одноадресных делегатов, способных хранить вызовы обработчиков одного и того же прототипа.

    За событием закрепляется определенный ключ и при манипуляции с событием во внешнем коде делегаты, маркированные этим ключом, добавляются в список. Чтобы активизировать событие, достаточно обратиться к списку с закрепленным за событием ключом, все одноадресные делегаты будут извлечены из списка и присоединенные обработчики будут выполнены.

    Поскольку элементы списка реализуют вызов обработчиков одноадресным способом, то использование списка позволяет прикреплять обработчики не только с пустым возвращаемым значением, но и с возвращаемым значением любого типа. Хотя в большинстве случаев это несущественное преимущество, поскольку результат обработки можно вернуть и через аргументы обработчика. Класс EventHandlerList находится в пространстве имен System. ComponentModel. В следующем примере демонстрируется создание событий на основе списка одноадресных делегатов.

    using System;
        
    namespace Test
    {
        // Класс-источник сообщения
        class SourceMessage
        {
            // Создание списка делегатов вместо базовых полей
            System.ComponentModel.EventHandlerList eventList
                = new System.ComponentModel.EventHandlerList();
        
            // Объявление типов делегатов
            public delegate void Message1();
            public delegate int Message2(string message);
            // Непустое возвращаемое значение
            public delegate void Message3(object sender, string message);
        
            // Создание ключей для делегатов
            Object key1 = new Object();
            Object key2 = new Object();
            Object key3 = new Object();
        
            // Создание события на базе списка
            public event Message1 Mail1
            {
                add
                {
                    eventList.AddHandler(key1, value);// Дополняем список делегатов
                }
                remove
                {
                    eventList.RemoveHandler(key1, value);// Удаляем из списка
                }
            }
        
            // Создание события на базе списка
            public event Message2 Mail2
            {
                add
                {
                    eventList.AddHandler(key2, value);// Расширяем список делегатов
                }
                remove
                {
                    eventList.RemoveHandler(key2, value);// Удаляем из списка
                }
            }
        
            // Создание события на базе списка
            public event Message3 Mail3
            {
                add
                {
                    eventList.AddHandler(key3, value);// Расширяем список делегатов
                }
                remove
                {
                    eventList.RemoveHandler(key3, value);// Удаляем из списка
                }
            }
        
            // Симуляция срабатывания события Mail1
            public void DispatchMail1()
            {
                // Извекаем из списка все делегаты для Mail1, помеченные ключом
                Message1 mail1 = (Message1)eventList[key1];
                if (mail1 != null)
                    mail1();
            }
        
            // Симуляция срабатывания события Mail2
            public void DispatchMail2()
            {
                // Извекаем из списка все делегаты для Mail2, помеченные ключом
                Message2 mail2 = (Message2)eventList[key2];
                if (mail2 != null)
                    mail2("\"mail2 из SourceMessage\"");
            }
        
            // Симуляция срабатывания события Mail3
            public void DispatchMail3()
            {
                // Извекаем из списка все делегаты для Mail3, помеченные ключом
                Message3 mail3 = (Message3)eventList[key3];
                if (mail3 != null)
                    mail3(this, "\"mail3 из SourceMessage\"");
            }
        }
        
        // Получатель сообщения
        class MyClass
        {
            // Конструктор
            public MyClass()
            {
                // Создаем объект с событиями
                SourceMessage obj = new SourceMessage();
        
                // Подписываемся на обработчики
                obj.Mail1 += new SourceMessage.Message1(obj_Mail1);
                obj.Mail1 += new SourceMessage.Message1(obj_Mail1);
                obj.Mail2 += new SourceMessage.Message2(obj_Mail2);
                obj.Mail3 += new SourceMessage.Message3(obj_Mail3);
        
                // Запускаем события
                obj.DispatchMail1();
                obj.DispatchMail2();
                obj.DispatchMail3();
            }
        
            // Обработчики
            void obj_Mail1()
            {
                Console.WriteLine("Обработчик события Mail1.");
            }
        
            int obj_Mail2(string message)
            {
                Console.WriteLine("\nОбработчик события Mail2.\n" +
                    "Сообщение: {0}\n", message);
                return 1;
            }
        
            void obj_Mail3(object sender, string message)
            {
                Console.WriteLine("Обработчик события Mail3.\n" +
                    "Сообщение: {0}", message);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение списка делегатов";
        
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 36;
                Console.WindowHeight = 9;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Стандартный делегат EventHandler и стандартный аргумент EventArgs

    Для большинства практических случаев обработчикам событий достаточно двух параметров, первый из которых будет адресовать объект, возбудивший событие, а второй - некоторую дополнительную информацию. Для этих целей существует библиотечный делегат EventHandler. Он используется во множестве библиотечных классов и вполне может удовлетворить большинство наших потребностей.

    Второй аргумент делегата всегда можно расширить, если мы хотим передать с событием свою специфическую информацию. Приведем пример

    using System;
        
    namespace Test
    {
        // Расширение библиотечного класса аргументов
        class MyEventArgs : EventArgs
        {
            private string message; // Поле для хранения сообщения
        
            // Сервис доступа к полю 
            public String Message
            {
                get { return message; }
                set { message = value; }
            }
        }
        
        // Класс-источник сообщения
        class SourceEvent
        {
            // Тип стандартного делегата объявлен в mscorlib.System так
            // public delegate void EventHandler(object sender, 
        System.EventArgs e)
        
            // Создание события на базе стандартного делегата
            public event EventHandler Event;
        
            // Метод диспетчеризации события
            protected virtual void OnEvent(EventArgs args)
            {
                if (Event != null)
                    Event(this, args); // Вызываем обработчики
            }
        
            // Симулятор срабатывания события Event по внешней причине
            public void SimulateEvent()
            {
                // Создаем толстый объект и формируем передаваемую информацию
                MyEventArgs args = new MyEventArgs();
                args.Message = "'Это сообщение 
        поступило с событием'";
        
                // Вызываем функцию диспетчеризации события.
                // Функция ожидает тонкий объект, а ей 
                // передается толстый объект - это нормально
                OnEvent(args);
            }
        }
        
        // Получатель сообщения
        class MyClass
        {
            // Конструктор
            public MyClass()
            {
                // Создаем объект, имеющий событие
                SourceEvent obj = new SourceEvent();
        
                // Подписываемся на обработчики события
                obj.Event += new EventHandler(Handler1);
                obj.Event += new EventHandler(Handler2);
        
                // Вызываем симулятор возникновения события
                obj.SimulateEvent();
            }
        
            void Handler1(object sender, EventArgs e)
            {
                // Хотим извлечь информацию из толстого объекта,
                // значит нужно повысить полномочия тонкой ссылки
                MyEventArgs args = (MyEventArgs)e;
                String message = args.Message;
                Console.WriteLine("(Handler1) Получена 
        информация:\n" + message);
                Console.WriteLine();
            }
        
            void Handler2(object sender, EventArgs e)
            {
                Console.WriteLine("(Handler2) Событие из о
        бъекта {0}\n" +
                    "Передан объект-аргумент {1}",
                    sender.GetType().Name, 
                    e.GetType().Name);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение делегата EventHandler";
        
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 43;
                Console.WindowHeight = 6;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }
    Страницы:

    Интерфейсы и делегаты

    Интерфейсы

    Интерфейсы - это чисто абстрактные классы, в которых все методы являются только объявлениями и не содержат реализацию. В этом случае вместо ключевого слова abstract перед объявлением класса нужно использовать ключевое слово interface. Но объявления самих методов ключевым словом abstract подтверждать не нужно и они не являются виртуальными. Интерфейсы можно рассматривать как предписание, которое нужно немедленно реализовать в ближайшем наследующем классе.

    Интерфейсы предназначены только для наследования. Они используются в классах только в качестве ближайшего предка и определяют сигнатуру методов, свойств, индексаторов и событий, которые все до единого обязан реализовать наследующий этот интерфейс класс. То есть не допускается многоуровневое наследование через классы без немедленной реализации. При этом сигнатура в реализации члена должна в точности совпадать с сигнатурой члена, объявленного в интерфейсе. Однако допускается многоуровневое наследование одних интерфейсов другими. Но как только многоуровневая цепочка наследования интерфейсов передается классу, этот класс должен немедленно реализовать члены всех унаследованных интерфейсов. Класс, наследующий интерфейс, называется реализующим интерфейс классом.

    Интерфейсы не могут содержать других членов, кроме объявлений методов, свойств, индексаторов и событий. Запрещено включать в них поля, конструкторы и деструкторы,методы перегрузки операторов. Ни один член интерфейса не может быть объявлен статическим.

    Запрещено в объявлениях методов интерфейсов использовать модификаторы доступа, поскольку все методы интерфейса считаются общедоступными по умолчанию. Но в реализующем классе, наследующем интерфейс, для каждого метода это нужно подтверждать явно, используя только модификатор public.

    Для интерфейсов, в отличие от классов, язык C# допускает множественное наследование. Вслед за именем производного класса после двоеточия может следовать список наследуемых интерфейсов, разделенных запятыми. Если кроме интерфейсов наследуется еще и класс, то его имя должно возглавлять этот список, то есть базовый класс, если он есть, всегда должен стоять впереди интерфейсов.

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

    Такой прием обеспечивает совместимость отдельных частей программы. Он снижает количество возможных ошибок программирования, перекладывая задачу контроля за ошибками на плечи компилятора за счет дополнительной формализации (бюрократизации) процесса решения.

    Поскольку интерфейс относится к чисто абстрактным базовым классам, допустимо (а часто и очень удобно) объявить ссылку типа интерфейса, которая может ссылаться на любой объект, порожденный классом, наследующим (и сразу реализующим) интерфейс. Ссылка типа интерфейса имеет те же самые возможности, что и ссылка типа базового класса, которая может ссылаться на любые объекты, порожденные производными от него классами. И точно также, как в ссылках базовых классов, полномочия ссылки типа интерфейса можно изменять явным приведением типов.

    Если сам интерфейс как тип объявлен с модификатором доступа public, то он доступен любому внешнему коду. Если ключевое слово public опущено или использовано ключевое слово internal, то интерфейс доступен коду только текущей сборки, в которой он объявлен. При этом наследующий класс не должен иметь больший уровень доступности, чем уровень доступности наследуемых интерфейсов.

    Открытая реализация членов интерфейса

    Пусть разрабатывается набор классов для реализации моделей проигрывателей от разных фирм. Клиентский код, использующий модели проигрывателей, не будет зависеть от их конкретной реализации при условии, что все функции, которые он использует, будут иметь одинаковые имена и одинаковую сигнатуру. Это обязательное условие для всех проигрывателей и устанавливает интерфейсный тип, который они наследуют.

    using System;
        
    namespace Test
    {
        // Перечисление допустимых напряжений
        public enum StdVoltage
        {
            V110 = 110,
            V127 = 127,
            V220 = 220,
            V380 = 380
        }
        
        class Power // Электропитание
        {
            // Поле
            StdVoltage voltage = StdVoltage.V220;
            
            // Свойство
            protected StdVoltage Voltage 
            {
                get { return voltage; }
                set 
                {
                    // Заменяем поле при соответствии стандарту
                    switch (value)
                    {
                        case StdVoltage.V110:
                        case StdVoltage.V127:
                        case StdVoltage.V220:
                        case StdVoltage.V380:
                            voltage = value;
                            break;
                    }
                }
            }
        
            // Виртуальный метод
            public virtual void ShowVoltage()
            {
                // Приводим тип перечисления к целому числу
                Console.WriteLine("Питание {0} Вольт", 
        (int)voltage);
            }
        }
        
        interface IPlayer
        {
            // Все объявления по умолчанию public
            void Start();
            void Stop();
            void Pause();
        }
        
        class Panasonic : Power, IPlayer
        {
            public void Start()
            {
                Console.WriteLine("Panasonic: 
        Реализация Start");
            }
        
            public void Stop()
            {
                Console.WriteLine("Panasonic: 
        Реализация Stop");
            }
        
            public void Pause()
            {
                Console.WriteLine("Panasonic: 
        Реализация Pause");
            }
        
            public override void ShowVoltage()
            {
                Voltage = StdVoltage.V110;
                Console.Write("Panasonic: ");
                base.ShowVoltage();
            }
        }
        
        class Sony : Power, IPlayer
        {
            public void Start()
            {
                Console.WriteLine("Sony: 
        Реализация Start");
            }
        
            public void Stop()
            {
                Console.WriteLine("Sony: 
        Реализация Stop");
            }
        
            public void Pause()
            {
                Console.WriteLine("Sony: 
        Реализация Pause");
            }
        
            public override void ShowVoltage()
            {
                Voltage = StdVoltage.V127;
                Console.Write("Sony: ");
                base.ShowVoltage(); 
            }
        }
        
        class Samsung : Power, IPlayer
        {
            public void Start()
            {
                Console.WriteLine("Samsung: 
        Реализация Start");
            }
        
            public void Stop()
            {
                Console.WriteLine("Samsung: 
        Реализация Stop");
            }
        
            public void Pause()
            {
                Console.WriteLine("Samsung: 
        Реализация Pause");
            }
        
            // Здесь мы не переопределяли виртуальную функцию ShowVoltage(),
            // поэтому при использовании экземплярной ссылки будет
            // вызвана ее версия из слоя базового класса с начальным значением 220V
        }
        
        // Вызывающий клиентский код
        class MyClass
        {
            public MyClass()
            {
                // Создаем производные объекты, адресуемые
                // ссылками типа наследуемого интерфейса
                IPlayer[] refInterface = 
                    {
                        new Panasonic(),
                        new Sony(),
                        new Samsung()
                    };
        
                // Выполняем действия, реализованные индивидуально
                for(int i = 0; i < refInterface.Length; i++)
                {
                refInterface[i].Start();
                refInterface[i].Stop();
                refInterface[i].Pause();
                }
                Console.WriteLine();
        
                // Печатаем индивидуальное питание
                // Для этого поднимаем полномочия ссылки без 
                // проверки, содержится ли в адресуемом объекте 
                // нужный слой (мы-то знаем, что содержится)
                ((Panasonic)refInterface[0]).ShowVoltage();
                ((Sony)refInterface[1]).ShowVoltage();
                // Вызываем виртуальную функцию из слоя базового класса
                // Предварительно на всякий случай проверяем,
                // есть ли в объекте, адресуемом ссылкой типа IPlayer,
                // слой Samsung, чтобы безопасно вызывать его члены
                if (refInterface[2] is Samsung)
                {
                    Samsung samsung = (Samsung)refInterface[2];
                    Console.Write("Samsung: ");
                    // Функции ShowVoltage() в слое Samsung нет, поэтому 
                    // будет вызвана версия из слоя базового класса Power
                    samsung.ShowVoltage();
                }
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Использование интерфейсов";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 60;
                Console.WindowHeight = 10;
        
                new MyClass();// Чтобы сработал конструктор
        
                Console.ReadLine();
            }
        }
    }

    Результат выполнения примера будет таким

    Диаграмма UML для приведенного кода, построенная в Visual Studio 2005, выглядит следующим образом

    Приведем пример использования интерфейса, который наследует другой интерфейс (другие интерфейсы). Такие интерфейсы можно назвать многослойными. Для многослойных интерфейсов существует такое правило: если класс реализует интерфейс, который является производным цепочки других интерфейсов, то класс должен реализовать члены всех интерфейсов цепочки наследования. При этом сами наследуемые интерфейсы как типы должны иметь уровень доступности не ниже уровня доступности наследующего их класса. Но все члены, объявленные в наследуемых интерфейсах, всегда общедоступны в реализующем классе.

    using System;
        
    namespace Test
    {
        interface A
        {
            void Method_A(); // Неявная доступность public
        
            // Method_A() - явно объявлен в A
        }
        
        // Одиночное наследование интерфейсов
        interface B : A
        {
            void Method_B(); // Неявная доступность public
         
            // Method_B() - явно объявлен в B
            // Method_A() - унаследован из A
       }
        
        interface C
        {
            void Method_C();
         
            // Method_C() - явно объявлен в C
       }
        
        // Множественное наследование интерфейсов
        interface D : B, C
        {
            void Method_D();
         
            // Method_D() - явно объявлен 
            // Method_C() - унаследован из C
            // Method_B() - унаследован из B 
            // Method_A() - унаследован из A через B
       }
        
        // Класс, который должен реализовать
        // все унаследованные интерфейсы
        class E : D
        {
            public void Method_A()
            {
                Console.WriteLine("Реализация Method_A()");
            }
        
            public void Method_B()
            {
                Console.WriteLine("Реализация Method_B()");
            }
        
            public void Method_C()
            {
                Console.WriteLine("Реализация Method_C()");
            }
        
            public void Method_D()
            {
                Console.WriteLine("Реализация Method_D()");
            }
        }
        
        // Вызывающий клиентский код
        class MyClass
        {
            public MyClass()
            {
                E ob = new E();
                ob.Method_A();
                ob.Method_B();
                ob.Method_C();
                ob.Method_D();
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Наследование интерфейсов";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 60;
                Console.WindowHeight = 10;
        
                new MyClass();// Чтобы сработал конструктор
        
                Console.ReadLine();
            }
        }
    }

    Вот результат выполнения примера

    Диаграмма UML для приведенного кода, построенная в Visual Studio 2005, выглядит следующим образом

    Закрытая реализация членов интерфейса

    В предыдущем примере хоть и применялись цепочки интерфейсов и множественное наследование, но все их члены имели разные имена. Поэтому в реализующем классе не возникло коллизий неоднозначности. В однослойном (ненаследующем) интерфейсе тоже не может быть членов с совпадающими именами и сигнатурами.

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

    Такой прием можно использовать и тогда, когда все реализуемые члены различные, но нужно ограничить прямую доступность реализующих интерфейс методов от внешнего кода. Например, реализующий класс предыдущего примера можно переписать так

    using System;
        
    namespace Test
    {
        internal interface A // Доступен внутри текущей сборки
        {
            void Method_A(); // Неявная доступность public
        }
        
        // Одиночное наследование интерфейсов
        internal interface B : A // Доступен внутри текущей сборки
        {
            void Method_B(); // Неявная доступность public
        
            // Method_B() - явно объявлен в B
            // Method_A() - унаследован из A
        }
        
        // Изолированный интерфейс
        // Доступность по умолчанию internal
        interface C // Доступен внутри текущей сборки
        {
            void Method_C();
        
            // Method_C() - явно объявлен 
        }
        
        // Множественное наследование интерфейсов
        internal interface D : B, C // Доступен внутри текущей сборки
        {
            void Method_D();
        
            // Method_D() - явно объявлен 
            // Method_C() - унаследован из C
            // Method_B() - унаследован из B 
            // Method_A() - унаследован из B
        }
        
        // Класс, который должен реализовать
        // все унаследованные интерфейсы
        class E : D
        {
            void A.Method_A() // Здесь нельзя употреблять модификаторы доступа 
            {
            Console.WriteLine("Закрытая реализация Method_A()");
            }
            public void Method_A()// Функция доступа к закрытому члену
            {
            A a = this; // Неявное приведение типов (снижение полномочий ссылки)
            a.Method_A();
            }
        
            void B.Method_B() // Здесь нельзя употреблять модификаторы доступа
            {
            Console.WriteLine("Закрытая реализация Method_B()");
            }
            public void Method_B()// Функция доступа к закрытому члену
            {
            B b = this; // Неявное приведение типов (снижение полномочий ссылки)
            b.Method_B();
            }
        
            void C.Method_C() // Здесь нельзя употреблять модификаторы доступа
            {
            Console.WriteLine("Закрытая реализация Method_C()");
            }
            public void Method_C()// Функция доступа к закрытому члену
            {
            //C c = this;
            //c.Method_C();
            // Явное приведение типов (снижение полномочий ссылки)
            ((C)this).Method_C(); // Другой синтаксис
            }
        
            void D.Method_D() // Здесь нельзя употреблять модификаторы доступа
            {
            Console.WriteLine("Закрытая реализация Method_D()");
            }
            public void Method_D()// Функция доступа к закрытому члену
            {
            //D d = this;
            //d.Method_D();
            // Явное приведение типов (снижение полномочий ссылки)
            ((D)this).Method_D(); // Другой синтаксис
            }
        }
        
        // Вызывающий клиентский код
        class MyClass
        {
            public MyClass()
            {
            E ob = new E();
            // Вызываем закрытые члены реализации интерфейса 
            // через публичные функции доступа
            ob.Method_A();
            ob.Method_B();
            ob.Method_C();
            ob.Method_D();
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
            // Настройка консоли
            Console.Title = "Закрытая реализация интерфейсов";
            Console.ForegroundColor = ConsoleColor.White;
            Console.CursorVisible = false;
            Console.WindowWidth = 60;
            Console.WindowHeight = 10;
        
            new MyClass();// Чтобы сработал конструктор
        
            Console.ReadLine();
            }
        }
    }

    Результат будет таким

    Диаграмма UML для приведенного кода, построенная в Visual Studio 2005, выглядит следующим образом

    Реализующий класс явно адресует унаследованный от интерфейса метод. Синтаксис допускается только закрытое по умолчанию объявление реализации метода (без применения любых модификаторов доступа), чтобы наверняка исключить неопределенность во внешнем по отношению к реализующему классу коде. Для возможности вызова реализации метода из внешнего кода применяют дополнительные public -функции доступа, из которых вызывают private -реализацию. Они повторяют имена интерфейсных функций или могут иметь любые другие имена, но в последнем случае идея интерфейсов теряет смысл.

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

    Если члены интерфейсов имеют не только одинаковое имя, но и одинаковую сигнатуру, и хотя бы один из них реализован, то оставшиеся члены нейтрализовывать необязатально.

    Если наследуемые интерфейсы имеют абсолютно одинаковые члены (имя и сигнатура), то их можно рассматривать как одинаковые инструкции, реализовать только один член, а остальные скрывать не необязательно. Это ведь одинаковый шаблон. Но все различающиеся ненужные члены надо скрывать пустой закрытой реализацией.

    using System;
        
    namespace Test
    {
        interface A
        {
            void Show();
            void Foo();
        }
        
        interface B
        {
            void Show();
            int Foo(string str);
        }
        
        // Обязаны реализовать все, но все клиенту не нужно 
        class C : A, B
        {
            // Реализуем то, что нужно клиенту 
            void A.Show()
            {
                Console.WriteLine("Реализация A.Show()");
            }
        
            public void Show()
            {
                ((A)this).Show();
            }
        
            int B.Foo(string str)
            {
                Console.WriteLine("Реализация B.Foo()");
                return 0;
            }
            public int Foo(string str)
            {
                return ((B)this).Foo(str);
            }
        
            // Нейтрализуем все остальное, что клиенту не нужно 
            void A.Foo() { }
        }
        
        // Вызывающий код
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение интерфейсов";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 30;
                Console.WindowHeight = 3;
        
                C obj = new C();
                obj.Show();
                obj.Foo(String.Empty);
        
                Console.ReadLine();
            }
        }
    }

    В приведенном примере метод Show() имеет одинаковую сигнатуру в обоих наследуемых интерфейсах, поэтому достаточно только одной выборочной реализации. Метод Foo() имеет разную сигнатуру, поэтому требует индивидуальной реализации для каждого интерфейса.

    Диаграмма UML данного примера будет такой

    Результат выполнения

    Реализация методов интерфейса как виртуальных

    using System;
        
    namespace Test
    {
        // Объявление интерфейса
        interface IDraw
        {
            void Show();
        }
        
        // Реализуем интерфейсный метод как виртуальный
        class A : IDraw
        {
            string name = "Класс A";
            public virtual void Show()
            {
                Console.WriteLine("\t" + name);
            }
        }
        
        // Переопределяет унаследованный виртуальный метод
        class B : A
        {
            string name = "Класс B";
            public override void Show()
            {
                Console.WriteLine("\t" + name);
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public MyClass()
            {
                Console.WriteLine("Адресация ссылками типов объектов:");
                A a = new A(); a.Show();
                B b = new B(); b.Show();
        
                Console.WriteLine("\nАдресация ссылкой 
                  типа унаследованного интерфейса\n"
                    + "(динамический полиморфизм):");
                IDraw refInterface = new A(); refInterface.Show();
                refInterface = new B(); refInterface.Show();
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Производить объекты 
                  абстрактных классов запрещено";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 60;
                Console.WindowHeight = 10;
        
                new MyClass();// Чтобы сработал конструктор
        
                Console.ReadLine();
            }
        }
    }

    Применение библиотечных интерфейсов

    В библиотеке .NET Framework объявлено множество интерфейсов, применение некоторых из них мы здесь проиллюстрируем.

    Создание индексаторов и нумераторов

    Пусть некоторый класс содержит коллекцию однотипных элементов, которые нужно перебрать с помощью цикла foreach. Такие коллекции называются нумераторами. В этом случае класс должен реализовать интерфейсы System.Collections.IEnumerable и System .Collections.IEnumerator, расположенные в сборке mscorlib.

    В интерфейсе IEnumerable объявлен единственный метод

    IEnumerator GetEnumerator()

    который возвращает ссылку на сам объект коллекции. Его в реализующем классе нужно определять так

    public IEnumerator GetEnumerator()
      { 
               // Приводим ссылку на объект к интерфейсному типу
                // и возвращаем адрес экземпляра класса
                return (IEnumerator)this;
            }

    Интерфейс IEnumerator содержит одно свойство и два метода, приведенные в таблице

    Члены библиотечного интерфейса IEnumerator
    Член Пояснения
    public object Current { get; } Возвращает текущий элемент в коллекции
    bool MoveNext() Переводит внутренний указатель к следующему элементу коллекции. Если коллекция исчерпана, то возвращается false
    void Reset() Устанавливает указатель перед первым элементом коллекции

    Создание индексаторов

    Пусть мы создали пользовательский класс Person с данными сотрудника и класс Persons, который будет содержать массив объектов класса Person с данными сотрудников. Для того, чтобы можно было обращаться в классе Persons к отдельным объектам-элементам массива, характеризующим отдельных сотрудников, нужно в нем предусмотреть индексатор. Обрабатывать отдельных сотрудников внутри класса Persons можно только как элементы массива. Вот код этого примера

    using System;
        
    namespace Test
    {
        class Person
        {
            // Закрытые поля с данными сотрудника
            string name;    // Имя
            int age;        // Возраст
        
            // Конструктор по умолчанию
            public Person()
            {
                name = "Петров";
                age = 21;
            }
        
            // Конструктор с параметрами
            public Person(string name, int age)
            {
                this.name = name;
                this.age = age;
            }
        
            // Свойства чтения/записи
            public string Name
            {
                get { return name; }
                set { name = value; }
            }
        
            public int Age
            {
                get { return age; }
                set { age = value; }
            }
        }
        
        class Persons
        {
            // Объявляем закрытую ссылку-поле на массив сотрудников
            private Person[] personArray;
        
            // Конструктор
            public Persons()
            {
                personArray = new Person[]{
                    new Person("Иванов",20),
                    new Person(),// По умолчанию Person("Петров", 21)
                    new Person("Сидоров", 22)
                };
            }
        
            // Свойство-индексатор
            public Person this[int index]
            {
                get
                {
                    if (index < 0)
                        return personArray[0];
                    else if (index > personArray.Length - 1)
                        return personArray[personArray.Length - 1];
                    else
                        return personArray[index];
                }
                set
                {
                    if (index < 0)
                        personArray[0] = value;
                    else if (index > personArray.Length - 1)
                        personArray[personArray.Length - 1] = value;
                    else
                        personArray[index] = value;
                }
            }
        
            // Количество сотрудников
            public int Length
            {
                get { return personArray.Length; }
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public MyClass()
            {
                // Создаем список сотрудников
                Persons pers = new Persons();
                // Выводим информацию о сотрудниках по индексатору
                Console.WriteLine("Список сотрудников:");
                for (int i = 0; i < pers.Length; i++)
                    Console.WriteLine("{2}) {0}, возраст {1}",
                        pers[i].Name, pers[i].Age, i + 1);
        
                /* Нужно реализовать интерфейсы IEnumerable, IEnumerator
                Console.WriteLine("\nЕще один список сотрудников:");
                int j = 0;
                foreach (Person per in pers)
                    Console.WriteLine("{2}) {0}, возраст {1}",
                        per.Name, per.Age, j = j + 1);
                //*/
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение индексаторов";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 30;
                Console.WindowHeight = 10;
        
                new MyClass();// Чтобы сработал конструктор
        
                Console.ReadLine();
            }
        }
    }

    Создание нумераторов

    Если для перебора внутренних элементов объекта Persons мы попробуем воспользоваться циклом foreach (закомментирован в конструкторе MyClass() ), то получим сообщение об ошибке. Реализация классом Persons интерфейсов IEnumerable и IEnumerator позволяет превратить экземпляр этого класса в коллекцию. Только нужно не забыть присоединить к файлу пространство имен System.Collections, где находятся эти интерфейсы.

    using System;
    using System.Collections;
        
    namespace Test
    {
        class Person
        {
            // Закрытые поля с данными сотрудника
            string name;    // Имя
            int age;        // Возраст
        
            // Конструктор по умолчанию
            public Person()
            {
                name = "Петров";
                age = 21;
            }
        
            // Конструктор с параметрами
            public Person(string name, int age)
            {
                this.name = name;
                this.age = age;
            }
        
            // Свойства чтения/записи
            public string Name
            {
                get { return name; }
                set { name = value; }
            }
        
            public int Age
            {
                get { return age; }
                set { age = value; }
            }
        }
        
        class Persons : IEnumerable, IEnumerator 
        {
            // Реализация IEnumerable
            public IEnumerator GetEnumerator()
            {
                return (IEnumerator)this;
            }
        
            // Реализация IEnumerator
            int pos = -1;   // Текущая позиция элемента в массиве
        
            public bool MoveNext()
            {
                if (pos < personArray.Length - 1)
                {
                    pos++;
                    return true;
                }
                else
                    return false;
            }
        
            public void Reset() { pos = -1; }
        
            public object Current
            {
                get { return personArray[pos]; }
            }
        
            // Объявляем ссылку-поле на массив сотрудников
            private Person[] personArray;
        
            // Конструктор
            public Persons()
            {
                personArray = new Person[]{
                    new Person("Иванов",20),
                    new Person(),// По умолчанию Person("Петров", 21)
                    new Person("Сидоров", 22)
                };
            }
        
            // Свойство-индексатор
            public Person this[int index]
            {
                get
                {
                    if (index < 0)
                        return personArray[0];
                    else if (index > personArray.Length - 1)
                        return personArray[personArray.Length - 1];
                    else
                        return personArray[index];
                }
                set
                {
                    if (index < 0)
                        personArray[0] = value;
                    else if (index > personArray.Length - 1)
                        personArray[personArray.Length - 1] = value;
                    else
                        personArray[index] = value;
                }
            }
        
            // Количество сотрудников
            public int Length
            {
                get { return personArray.Length; }
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public MyClass()
            {
                // Создаем список сотрудников
                Persons pers = new Persons();
                // Выводим информацию о сотрудниках по индексатору
                Console.WriteLine("Список сотрудников:");
                for (int i = 0; i < pers.Length; i++)
                    Console.WriteLine("{2}) {0}, возраст {1}",
                        pers[i].Name, pers[i].Age, i + 1);
        
                //* Нужно реализовать интерфейсы IEnumerable, IEnumerator
                Console.WriteLine("\nЕще один список сотрудников:");
                int j = 0;
                foreach (Person per in pers)
                    Console.WriteLine("{2}) {0}, возраст {1}",
                        per.Name, per.Age, j = j + 1);
                //*/
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение индексаторов";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 30;
                Console.WindowHeight = 10;
        
                new MyClass();// Чтобы сработал конструктор
        
                Console.ReadLine();
            }
        }
    }

    Делегаты

    Делегат, это тип, задающий сигнатуру методов и обеспечивающий механизм их обобщенного вызова. Вначале объявляется сам делегат как тип и сразу же задается прототип функций, которые в будущем могут адресоваться экземплярами этого типа. Затем объявляется ссылка типа делегата, которая будет указывать на коллекцию экземпляров делегата. И наконец, создаются сами объекты делегата, каждый из которых адресует одну функцию, и комбинируются в коллекцию, на которую указывает объявленная ссылка. Коллекция представляет собой список адресуемых делегатом функций и поддерживается операциями += и -= (или статическими методами Delegate.Combine() и Delegate.Remove() ).

    Экземпляр делегата способен накапливать в себе ссылки на методы объявленной сигнатуры и вызывать эти методы либо по одному, либо сразу весь список.

    Одноадресная работа делегатов

    Во время выполнения программы один и тот же делегат можно использовать для вызова разных методов одинаковой сигнатуры простой заменой метода, на который ссылается делегат. В том и состоит главное достоинство делегата, что адресуемый им метод определяется не на этапе компиляции, а в период выполнения. Заменяя в делегате адресуемые функции мы обеспечим их полиморфный вызов.

    Область видимости делегата можно регулировать точно так же, как и область видимости любого другого типа в приложении.

    using System;
        
    namespace Test
    {
        class DelegateTest
        {
            // Объявляем функции с сигнатурой делегатов
            public void Show1()
            {
                Console.WriteLine("Вызов: void Show1()");
            }
        
            // Объявляем функции с сигнатурой делегатов
            public void Show2()
            {
                Console.WriteLine("Вызов: void Show2()");
            }
        
            // Объявляем функции с сигнатурой делегатов
            public int Draw1(string str1)
            {
                Console.WriteLine("Вызов: {0}", str1);
                return 1;
            }
        
            // Объявляем функции с сигнатурой делегатов
            public int Draw2(string str2)
            {
                Console.WriteLine("Вызов: {0}", str2);
                return 2;
            }
        
            // Объявляем статическую функцию
            public static int Print(string str)
            {
                Console.WriteLine("Вызов: {0}", str);
                return 0;
            }
        }
           
        // Объявляем делегат в пространстве имен
        delegate void TypeShow();
        
        // Вызывающая сторона
        class MyClass
        {
            // Объявляем делегат в классе
            delegate int TypeDraw(string str);
        
            public MyClass()
            {
                // Создаем экземпляр класса с методами
                DelegateTest delegateTest = new DelegateTest();
        
                // Объявляем ссылки на объекты делегатов
                TypeShow typeShow;
                TypeDraw typeDraw;
        
                // Создаем объекты делегатов
                typeShow = new TypeShow(delegateTest.Show1);
                typeDraw = new TypeDraw(delegateTest.Draw1);
        
                // Вызываем методы посредством делегатов
                typeShow();
                typeDraw("int Draw1(string str1)");
        
                // Адресуемся к другим методам с той же сигнатурой
                typeShow = new TypeShow(delegateTest.Show2);
                typeDraw = new TypeDraw(delegateTest.Draw2);
        
                // Вызываем другие методы посредством делегатов
                typeShow();
                typeDraw("int Draw2(string str2)");
        
                // Вызываем статический метод 
                // посредством подходящего делегата 
                typeDraw = new TypeDraw(DelegateTest.Print);
                typeDraw("static int Print(string str)");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение делегатов";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 40;
                Console.WindowHeight = 7;
        
                new MyClass();// Чтобы сработал конструктор
        
                Console.ReadLine();
            }
        }
    }

    Делегат может адресовать как методы объекта (экземпляра класса), так и статические методы. Главное, чтобы заголовки объявления делегата и объявления метода совпадали как по типу возвращаемого значения, так и по сигнатуре.

    Многоадресная работа делегатов

    Для методов, имеющих одинаковую сигнатуру и не возвращающих значение (только с void ) с помощью делегата можно организовать сразу цепочку вызовов.

    using System;
        
    namespace Test
    {
        class MultiTest
        {
            // Статическое поле
            static int x = 1;
        
            // Объявляем функции с одинаковой сигнатурой
            public void Handler1(string name)
            {
                Console.WriteLine(name + x++);
            }
        
            public void Handler2(string name)
            {
                Console.WriteLine(name + x++);
            }
        
            public void Handler3(string name)
            {
                Console.WriteLine(name + x++);
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            // Объявляем делегат в классе
            delegate void TypeHandler(string text);
        
            public MyClass()
            {
                // Создаем экземпляр класса с методами
                MultiTest obj = new MultiTest();
        
                // Создаем объект-делегат и заполняем адресами
                TypeHandler Handler = new TypeHandler(obj.Handler1);
                Handler += new TypeHandler(obj.Handler2);
                Handler += obj.Handler3;    // Упрощенный синтаксис
        
                // Вызываем цепочку методов
                Handler("Вызов: Handler");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Многоадресные делегаты";
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 30;
                Console.WindowHeight = 10;
        
                new MyClass();// Чтобы сработал конструктор
        
                Console.ReadLine();
            }
        }
    }

    Делегаты являются экземплярами типа System.MulticastDelegate, который в свою очередь наследует абстрактный класс System.Delegate. Экземпляр типа MulticastDelegate может хранить в себе одну или сразу несколько ссылок на методы. В любом случае мы не можем явно объявлять делегат с помощью типа MulticastDelegate. Это делается с помощью ключевого слова delegate, но неявно порождается объект класса MulticastDelegate. Если посмотреть на структуру последнего примера через панель Class View, то это становится очевидным.

    Пустые делегаты

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

    using System;
        
    namespace Test
    {
        class MultiTest
        {
            // Объявляем функции одного прототипа
            public void Handler1()
            {
                Console.WriteLine("Исполняется Handler1");
            }
        
            public void Handler2()
            {
                Console.WriteLine("Исполняется Handler2");
            }
        
            public void Handler3()
            {
                Console.WriteLine("Исполняется Handler3");
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            // Для заголовка консольного окна
            public static string Title = "Пустые делегаты";
        
            // Объявляем делегат как член класса
            delegate void MyDelegate();
        
            public MyClass()
            {
                // Создаем экземпляр класса с методами
                MultiTest obj = new MultiTest();
        
                // Создаем объект-делегат и заполняем ссылками на функции
                // Инициализируем первой ссылкой
                MyDelegate del = new MyDelegate(obj.Handler1);
                // Добавляем другие ссылки
                del += obj.Handler2;
                del += obj.Handler3;
        
                // Вызываем цепочку методов
                del();
                Console.WriteLine();
        
                // Извлекаем метод и опять вызываем цепочку методов
                del -= obj.Handler1;
                del();
                Console.WriteLine();
        
                // Извлекаем метод и опять вызываем цепочку методов
                del = (MyDelegate)Delegate.Remove(del, new MyDelegate(obj.Handler2));
                del();
                Console.WriteLine();
        
                // Извлекаем последний метод и пытаемся адресоваться к пустому делегату
                del -= obj.Handler3;
                //del(); // Будет выброшено исключение, нужно проверять!!!
                if (del != null)
                {
                    del(); // Так все нормально!
                }
                else
                    Console.WriteLine("Список делегата исчерпан!");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 30;
                Console.WindowHeight = 11;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

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

    Некоторые члены типа MulticastDelegate

    Известно, что в C# методы могут быть статическими ( static ) или экземплярными ( instance ). Статические методы адресуются по имени класса, а экземплярные - по имени экземпляра. При загрузке кода в память компьютера создается единственный объект-тип и несколько объектов-экземпляров. Объект-тип содержит в единственном экземпляре все методы типа и статические данные, общие для всех объектов-экземпляров. А объекты-экземпляры содержат индивидуальные данные, к которым применяются методы объекта-типа. Однако статические методы, которые могут вызываться без создания экземплярных данных, должны обрабатывать только статические данные. Экземплярные же методы могут обрабатывать как индивидуальные, так и общие данные.

    Для обычного программирования достаточно рассмотреных нами сведений о делегатах. Но в ряде случаев может потребоваться более тонкая работа с делегатами. Тип MulticastDelegate сам имеет несколько своих собственных членов, но большую часть он наследует от абстрактного класса Delegate. Члены типа MulticastDelegate содержат все необходимые сервисы по управлению делегатами. Некоторые из этих членов экземплярные, а некоторые - статические. Ниже приведены некоторые из сервисов типа MulticastDelegate

    Некоторые члены типа MulticastDelegate, унаследованные от Delegate
    Член Пояснения
    Method { get; } Возвращает последнюю добавленную в делегат ссылку на метод
    Target { get; } Возвращает ссылку на объект, связанный с адресуемым делегатом экземплярным методом
    DynamicInvoke() Позволяет динамически обратиться к функциям, привязанным к делегату
    GetInvocationList() Возвращает список функций, привязанных к делегату
    operator == Перегруженный оператор для сравнения содержимого делегатов
    operator != Перегруженный оператор, позволяющий определить различие содержимого делегатов
    Combine() Добавляет в объект делегата новую ссылку на функцию
    Remove() Удаляет из объекта делегата ссылку на указанную функцию
    CreateDelegate() Динамически создает делегат

    Свойство Method

    using System;
        
    namespace Test
    {
        class MultiTest
        {
            // Статическое поле
            static int x = 1;
        
            // Объявляем функции с одинаковой сигнатурой
            public void Handler1()
            {
                Console.WriteLine("Исполняется Handler1");
            }
        
            public void Handler2()
            {
                Console.WriteLine("Исполняется Handler2");
            }
        
            public void Handler3()
            {
                Console.WriteLine("Исполняется Handler3");
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public static string Title = "Свойство Method";
        
            // Объявляем делегат как член класса
            delegate void MyDelegate();
        
            public MyClass()
            {
                // Создаем экземпляр класса с методами
                MultiTest obj = new MultiTest();
        
                // Создаем объект-делегат и заполняем ссылками на функции
                // Инициализируем первой ссылкой
                MyDelegate del = new MyDelegate(obj.Handler1);
                // Добавляем другие ссылки
                del += obj.Handler2;
                del += obj.Handler3;
        
                // Вызываем цепочку методов
                del();
                Console.WriteLine();
        
                del.Method.Invoke(obj, null); // Вызываем последний метод
        
                Console.WriteLine("\nПрототип последнего метода:\n{0}",
                    del.Method.ToString());
        
                Console.WriteLine("\nИмена адресуемых методов:");
                Delegate[] listMethods = del.GetInvocationList();
                for (int i = 0; i < listMethods.Length; i++)
                    Console.WriteLine("{0}", listMethods[i].Method.Name);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 30;
                Console.WindowHeight = 14;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Свойство Target

    Это свойство возвращает объект, связанный с адресуемым делегатом методом.

    using System;
        
    namespace Test
    {
        class MultiTest
        {
            // Объявляем функции с одинаковой сигнатурой
            public void Handler1()
            {
                Console.WriteLine("Исполняется Handler1");
            }
        
            public void Handler2()
            {
                Console.WriteLine("Исполняется Handler2");
            }
        
            public void Handler3()
            {
                Console.WriteLine("Исполняется Handler3");
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public static string Title = "Применение 
        свойства Target делегата";
        
            // Объявляем делегат как член класса
            delegate void MyDelegate();
        
            public MyClass()
            {
                // Создаем экземпляр класса с методами
                MultiTest obj = new MultiTest();
        
                // Создаем объект-делегат и заполняем ссылками на функции
                // Инициализируем первой ссылкой
                MyDelegate del = new MyDelegate(obj.Handler1);
                // Добавляем другие ссылки
                del += obj.Handler2;
                del += obj.Handler3;
        
                // Вызываем цепочку методов
                Console.WriteLine("Многоадресный вызов 
        через делегат");
                del();
                Console.WriteLine();
        
                // Возвращает объект, с которым связан
                // последний адресуемый метод
                Console.WriteLine("Тип объекта: {0}", 
                  del.Target.GetType().ToString());
                Console.WriteLine();
        
                // Повышаем полномочия извлеченной из Target ссылки 
                // на объект и повторно адресуемся напрямую 
                Console.WriteLine("Вызов через свойство 
        Target делегата");
                ((MultiTest)del.Target).Handler1();
                ((MultiTest)del.Target).Handler2();
                ((MultiTest)del.Target).Handler3();
                Console.WriteLine();
        
                // Адресуемся напрямую без делегата
                Console.WriteLine("Вызов через объект напрямую");
                obj.Handler1();
                obj.Handler2();
                obj.Handler3();
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 40;
                Console.WindowHeight = 18;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Методы DynamicInvoke() и GetInvocationList()

    Этот метод позволяет вызывать отдельные члены, адресуемые списком объекта-делегата, и задавать требуемые аргументы. Если член списка не имеет аргументов, то в качестве параметра метода используется null, иначе - массив параметров адресуемого члена.

    Мы убедились, что объявление ссылки на объект-делегат прописывает только прототип методов, которые корректно может адресовать эта ссылка. Сам делегат как объект создается при заполнении списка адресуемыми функциями. Но при формировании списка в него добавляются только имена методов. Это приемлемо, если вызываются функции с пустой сигнатурой.

    using System;
        
    namespace Test
    {
        class ShowPerson
        {
            // Объявляем функцию с аргументами
            public static void Handler(string name, int age)
            {
                Console.WriteLine("Сотрудник {0}, возраст {1}", name, age);
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public static string Title = "Вызов методов с параметрами";
        
            // Объявляем делегат как член класса
            delegate void MyDelegate(string name, int age);
        
            public MyClass()
            {   
                // Создаем и заполняем объект-делегат
                MyDelegate del = new MyDelegate(ShowPerson.Handler);
                // Добавляем другие ссылки
                int count = 3;
                for (int i = 1; i < count; i++)
                {
                    del += ShowPerson.Handler;
                }
        
                // Вызываем цепочку методов с одинаковым параметром
                del("Иванов", 21);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 30;
                Console.WindowHeight = 4;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Здесь просматривается очень важный вывод, что все зарегистрированные в делегате методы стороннего класса получают, при вызове их с помощью делегата, одинаковую входную информацию.

    А как быть, если с помощью делегата нужно адресовать методы, имеющие разные значения параметров. Вот тут-то и может пригодиться рассматриваемый метод делегата.

    public object DynamicInvoke(params object[ ] args)

    Но есть еще одна важная проблема, которую можно решить с помощью метода DynamicInvoke(), это надежность кода при адресации вызовов методов посредством делегата. Дело в том, что любой из цепочки метод при традиционном вызове может дать сбой. В результате система выдаст исключение и прервет вызовы остальных методов списка. Исключение мы можем обработать, но только для списка в целом, если не будем контролировать индивидуально каждый член списка делегата. Например, в приведенном фрагменте кода что-то успеет выполниться до выброса исключения, а что-то нет:

    try
    {
    del(null, 0);
    }
    catch
    {            }

    Эти проблемы решает метод DynamicInvoke() совместно с GetInvocationList(), как показано в следующем примере

    using System;
        
    namespace Test
    {
        class ShowPerson
        {
            // Функция с аргументами
            public static void Handler(string name, int age)
            {
                Console.WriteLine("Сотрудник {0}, возраст {1}", name, age);
            }
        
            // Проблемная функция с нормальным прототипом
            public static void ProblemHandler(string name, int age)
            {
                // Преднамеренно выбрасываем исключение
                throw new Exception();
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public static string Title = "Применение DynamicInvoke()";
        
            // Объявляем делегат
            delegate void MyDelegate(string name, int age);
        
            public MyClass()
            {
                //
                // Формируем список объекта-делегата
                //
                // Добавляем в список один проблемный метод
                MyDelegate del = new MyDelegate(ShowPerson.ProblemHandler);
        
                // Добавляем еще три нормальных метода
                int count = 3;
                for (int i = 0; i < count; i++)
                {
                    del += ShowPerson.Handler;
                }
        
                object[] param = new object[2];// Объявили массив для параметров
                int j = 0;    // Объявили и инициализировали счетчик
                // Перебираем список вызовов делегата, включая и вызов проблемного метода
                foreach (Delegate d in del.GetInvocationList())
                {
                    // Индивидуально формируем параметры методов
                    switch (j)
                    {
                        case 0:// Можно и не задавать, все равно для проблемного метода!
                            param[0] = "Мистер X";
                            param[1] = 99;
                            break;
                        // Для вызовов нормального метода
                        case 1:
                            param[0] = "Иванов";
                            param[1] = 21;
                            break;
                        case 2:
                            param[0] = "Петров";
                            param[1] = 22;
                            break;
                        case 3:
                            param[0] = "Сидоров";
                            param[1] = 23;
                            break;
                    }
        
                    j++;    // Счетчик
        
                    // Защищенно вызываем адресуемые методы индивидуально 
                    try
                    {
                        d.DynamicInvoke(param);
                    }
                    catch (Exception exc)
                    {
                        string str = d.Method.Name;
                        Console.WriteLine("Сбой метода {0}!!!", str);
        
                        str = exc.Message;   // Системное сообщение
                        // Разбиваем длинное сообщение пополам
                        int pos = str.Length / 2;
                        pos = str.IndexOf(' ', pos);// От средины первый пробел
                        Console.WriteLine("\"" +    // Экранируем кавычки
                            str.Substring(0, pos) +
                            Environment.NewLine +
                            str.Substring(pos + 1) +
                            "\"");                  // Экранируем кавычки
                        Console.WriteLine();        // Отделяем сообщение
                    }
                }
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 32;
                Console.WindowHeight = 8;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Перегруженные операторы 'operator ==' и 'operator !='

    Эти операторы позволяют подтвердить или опровергнуть абсолютную идентичность списков функций сравниваемых делегатов.

    using System;
        
    namespace Test
    {
        class Handler
        {
            // Функции
            public void Handler1()
            {
            }
        
            public void Handler2()
            {
                ;
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public static string Title = "Применение 
              операторов '==' и '!='";
        
            // Объявляем делегат
            delegate void MyDelegate();
        
            // Объявляем ссылки на делегаты как 
            // поля для видимости в методах класса
            MyDelegate del0, del1;
        
            public MyClass()
            {
                // Создаем объект
                Handler obj = new Handler();
        
                // Формируем список вызовов объекта-делегата
                del0 = new MyDelegate(obj.Handler1);
                del0 += new MyDelegate(obj.Handler2);
        
                // Еще один делегат с тем же списком вызовов
                del1 = new MyDelegate(obj.Handler1);
                del1 += obj.Handler2;   // Упрощенный синтаксис
        
                // Сравниваем делегаты с полность совпадающими списками
                Compare();
        
                // Делегат прежним содержимым, но в другом порядке
                del1 = new MyDelegate(obj.Handler2);
                del1 += obj.Handler1;   // Упрощенный синтаксис
        
                // Сравниваем делегаты с одинаковым содержимым, но разным порядком
                Compare();
        
                // Изменяем содержимое одного из делегатов
                del0 -= obj.Handler2;
        
                // Опять сравниваем делегаты с разным содержимым
                Compare();
            }
        
            void Compare()
            {
                if (del0 == del1)
                    Console.WriteLine("Списки делегатов идентичны");
                else
                    Console.WriteLine("Списки делегатов различны");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 33;
                Console.WindowHeight = 4;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Обратите внимание, что для равенства делегатов важен не только состав функций, но и порядок их следования в списке.

    Методы Combine() и Remove()

    Это статические методы, способные получать новый делегат как объединение списков двух делегатов одного и того же типа, или получать новый делегат с усеченным списком при тех же условиях. Методы могут использоваться вместо перегруженных операций ' += ' или ' -= ' при последовательном изменении списка одиночными функциями по синтаксису

    MyDelegate del;
                del = new MyDelegate(obj.Handler1);
                del += new MyDelegate(obj.Handler2);
        
                // Или
                del = new MyDelegate(obj.Handler1);
                del = (MyDelegate)Delegate.Combine(del, 
                  new MyDelegate(obj.Handler2));
    using System;
        
    namespace Test
    {
        class Handler
        {
            // Функции
            public void Handler1()
            {
                Console.WriteLine("Вызов Handler1()");
            }
        
            public void Handler2()
            {
                Console.WriteLine("Вызов Handler2()");
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            public static string Title = "Применение Combine() и Remove()";
        
            // Объявляем делегат как член класса
            delegate void MyDelegate();
        
            public MyClass()
            {
                // Создаем объект
                Handler obj=new Handler();
        
                // Формируем список объекта-делегата из 2 вызовов
                MyDelegate del1 = new MyDelegate(obj.Handler1);
                del1 += new MyDelegate(obj.Handler1);
        
                // Еще один делегат того же типа из 3 вызовов
                MyDelegate del2 = new MyDelegate(obj.Handler2);
                del2 += obj.Handler2;   // Упрощенный синтаксис
                del2 = del2 + obj.Handler2; // То же самое
        
                // Новый делегат из 5 вызовов 
                MyDelegate del3 = (MyDelegate)Delegate.Combine(del1, del2);
        
                // Вызываем 5 функций
                del3();
                Console.WriteLine();
        
                // Вновь формируем делегаты
                del1 = new MyDelegate(obj.Handler1);
                del1 += obj.Handler2;
        
                // Усекаем первый список вторым 
                del2 = (MyDelegate)Delegate.Remove(del3, del1);
        
                // Вызываем оставшиеся 3 функции
                del2();
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 33;
                Console.WindowHeight = 10;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    События в C#

    Рассылка сообщений с помощью делегата

    Чтобы любые объекты могли обмениваться информацией, они должны разговаривать на одном языке. В случае объектно-ориентированного программирования таким условием является одинаковый прототип функций, определяемый делегатом. Многоадресная работа делегата удобна тем, что можно послать одинаковое сообщение сразу нескольким объектам, функции которых зарегистрированы в списке делегата как обработчики этого сообщения. Список делегата можно назвать по-разному:

  • список вызываемых функций,
  • список обработчиков,
  • список адресатов,
  • список получателей и т.д.
  • Напомним, что если список адресатов пуст, то самого объекта-делегата не существует и ссылка на него имеет значение null. Без проверки этого обстоятельства при попытке вызова адресатов пустой ссылкой-делегатом будет сгенерировано стандартное исключение NullReferenceException.

    Вот пример однонаправленной рассылки сообщения

    using System;
        
    namespace Test
    {
        // Образец сообщения определяется делегатом
        delegate void Message(string message);
        
        // Источник сообщения
        class SourceMessage
        {
            // Общедоступное поле ссылки на объект-делегат,
            // который наполнится указателями
            // на функции в классах-получателях 
            public Message mail;
        
            // Необязательное поле с рассылаемым сообщением
            public string message;
        
            // Разослать сообщение - функция диспетчеризации
            public void DispatchMessage(string mess)
            {
                // Сохраняем внешнее сообщение во внутреннем поле
                message = mess;
        
                // Инициируем рассылку сообщения всем, 
                // кто зарегистрировался в объекте-делегате
                if (mail != null)   // Если не пустой делегат
                    mail(mess);
            }
        }
        
        // Получатель сообщения
        class Addressee1
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee1 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Получатель сообщения
        class Addressee2
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee2 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            static public string Title = "Рассылка 
        сообщений делегатом";
        
            public MyClass()
            {
                // Создаем объекты источника и получателей сообщения
                SourceMessage source = new SourceMessage();
                Addressee1 obj1 = new Addressee1();
                Addressee2 obj2 = new Addressee2();
        
                // Формируем список вызовов объекта-делегата
                source.mail += new Message(obj1.Handler);
                source.mail += new Message(obj2.Handler);
        
                // Рассылаем сообщение напрямую через делегат
                source.mail("Первое сообщение");
                Console.WriteLine();
        
                // Рассылаем сообщение через функцию диспетчеризации
                source.DispatchMessage("Второе сообщение");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 32;
                Console.WindowHeight = 10;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Обратите внимание, что при добавлении в делегат первой ссылки на функцию нам не обязательно использовать операцию ' = ', а можно и ' += ', несмотря на то, что делегат еще пустой.

    В классе-источнике сообщения SourceMessage мы объявили два общедоступных поля: ссылку mail на объект-делегат и переменную message для хранения сообщения внутри класса. Поле-переменная message для работы программы не нужна, но мы ее ввели просто для того, чтобы сравнить представления в панели Class View.

    Если теперь посмотреть на состав класса SourceMessage через панель Class View, то мы не сможем различить две этих переменные - обычные поля, да и только. Но ведь поле mail не выполняет роль хранителя ссылки на обычный экземпляр класса или структуры, а предназначено для создания механизма адресации функций.

    Превращение делегата в событие

    А надо бы их различить, поскольку они предназначены совершенного для разных целей и имеют разные механизмы обработки. Чтобы внести ясность в этот вопрос, разработчики C# и библиотеки .NET Framework решили объявлять ссылку на объект делегата, когда он используеся для рассылки сообщений, с ключевым словом event и называть ее событием.

    Остальной механизм работы такого делегата-события остался прежним, кроме одного: теперь событие-делегат является собственностью класса-источника и вызывать его напрямую из класса-приемника как поле сообщений нельзя. Событие должно инициироваться функцией-членом класса, отсылающего сообщение ( функция диспетчеризации ), в котором оно для этих целей и объявлено. В приемнике сообщения можно только подписаться на событие источника путем прикрепления нужных функций-обработчиков, а далее вызвать функцию диспетчеризации владельца события. Но по-прежнему наполнение списка события конкретными адресами функций выполняют объекты-делегаты.

    Объявление в классе-источнике делегата как события означает только возможность на него подписаться функциям внешнего кода, но не гарантирует, что на него действительно кто-то подпишется в момент возбуждения этого события в классе-источнике. Поэтому функция диспетчеризации вначале проверяет, не пустой ли делегат, и только потом выполняет вызов связанных с ним методов-обработчиков.

    В соответствии со сказанным изменим предыдущий пример совсем на немного (выделено в коде) и получим прежний результат, только теперь уже с участием события как члена класса

    using System;
        
    namespace Test
    {
        // Образец сообщения определяется делегатом
        delegate void Message(string message);
        
        // Источник сообщения
        class SourceMessage
        {
            // Общедоступное поле ссылки на объект-делегат,
            // который теперь называется событием и наполняется
            // указателями на функции в классах-получателях 
            public event Message mail;
        
            // Необязательное поле с рассылаемым сообщение
            public string message;
        
            // Разослать сообщение - функция диспетчеризации
            public void DispatchMessage(string mess)
            {
                // Сохраняем внешнее сообщение во внутреннем поле
                message = mess;
        
                // Инициируем рассылку сообщения всем, 
                // кто зарегистрировался в объекте-делегате
                if (mail != null)   // Если не пустой делегат
                    mail(mess);
            }
        }
        
        // Получатель сообщения
        class Addressee1
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee1 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Получатель сообщения
        class Addressee2
        {
            // Функции
            public void Handler(string message)
            {
                Console.WriteLine("Addressee2 получил:"
                    + "\n\t\"{0}\"", message);
            }
        }
        
        // Вызывающая сторона
        class MyClass
        {
            static public string Title = "Рассылка 
        сообщений событием";
        
            public MyClass()
            {
                // Создаем объекты источника и получателей сообщения
                SourceMessage source = new SourceMessage();
                Addressee1 obj1 = new Addressee1();
                Addressee2 obj2 = new Addressee2();
        
                // Формируем список обработчиков события с 
          //помощью объектов-делегатов
                source.mail += new Message(obj1.Handler);
                source.mail += new Message(obj2.Handler);
        
                // Рассылаем сообщение только через функцию-член источника
                //source.mail("Первое сообщение");
                source.DispatchMessage("Первое сообщение");
                Console.WriteLine();
        
                // Рассылаем сообщение через функцию диспетчеризации
                source.DispatchMessage("Второе сообщение");
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 32;
                Console.WindowHeight = 10;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Если теперь посмотреть на состав класса SourceMessage через панель Class View, то мы можем отличить переменную от события.

    В последнем примере событие объявлено в одном классе, подписка обработчиков выполнена в другом классе, а сами обработчики находятся в третьих двух классах. Но, как правило, обработчики принадлежат тому классу, который содержит в себе экземпляр класса-источника события и в котором выполнена подписка на событие. Кроме того, инициация события выполняется внешними причинами в результате действий пользователя или возникновения каких-то программных условий. Чаще всего событие инициируется самой операционной системой. В нашем случае мы имитируем внешнюю причину, используя для этого нашу функцию диспетчеризации DispatchMessage().

    Когда вместо объекта-делегата для передачи сообщений используется объект-событие, то включенные в список события функции называют обработчиками. Многие клиенты объекта с событием могут подписаться на его обработку. Тогда при возникновении события обработчики клиентов будут выполняться в том порядке, в котором они следуют в списке события. Как и в случае с делегатами, сбой любого обработчика прервет выполнение оставшихся членов списка события.

    Типичный способ создания событий

    В соответствии с парадигмой объектно-ориентированного программирования событие должно передаваться объектом-источником при возникновении определенных обстоятельств. Другие классы, содержащие в себе объект с общедоступным полем-событием, по отношению к этому событию могут выступать только как клиенты (внешний код). Они могут только подписаться традиционным способом на событие и прослушивать его, а при получении - реагировать кодом прикрепленных к событию своих обработчиков. Согласуем код нашего последнего примера с изложенными соображениями

    using System;
        
    namespace Test
    {
        // Образец сообщения определяется делегатом
        delegate void Message(object sender, string message);
        
        // Класс-источник сообщения
        class SourceMessage
        {
            // Общедоступное поле ссылки на событие
            public event Message Mail;
        
            // Метод диспетчеризации события, объявили виртуальным 
            // и защищенным для возможности замещения в наследниках
            protected virtual void OnMail(string mess)
            {
                // Инициируем рассылку сообщения всем, 
                // кто подписался на событие
                if (Mail != null)       // Если не пустой делегат
                    Mail(this, mess);   // Инициируем событие
            }
        
            // Объявляем и инициируем внутреннее поле базовым сообщением
            string message = "Сообщаю, что сработал таймер!!!\n"
                + "Текущее время ";
        
            // Объявляем внутреннее поле для видимости в методах
            System.Timers.Timer timer;
        
            // Конструктор класса-источника сообщения
            public SourceMessage()
            {
                // Создаем и запускаем таймер, который по истечении 
                // заданного времени инициирует рассылку сообщения
                timer = new System.Timers.Timer();
                timer.Interval = 5000d; // Сработает через 5 секунд
                timer.Elapsed += new System.Timers.ElapsedEventHandler
        (timer_Elapsed);
                timer.Start();      // Запускаем таймер
            }
        
            // Инициирование события из внешнего источника
            void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
            {
                // Извлекаем текущее системное время
                DateTime curTime = DateTime.Now; 
                // Дополняем базовое сообщение временем
                message += curTime.ToString();
        
                OnMail(message);    // Вызываем метод диспетчеризации
                timer.Stop();       // Останавливаем системный таймер
            }
        }
        
        // Класс-получатель сообщения
        class MyClass
        {
            static public string Title = 
                "Передача сообщения событием (через 5 секунд!)";
        
            public MyClass()
            {
                // Создаем объект с событием
                SourceMessage obj = new SourceMessage();
        
                // Подписываем на событие обработчик,
                // печатающий полученную информацию,
                // с помощью объекта-делегата
                obj.Mail += new Message(obj_Mail);
            }
        
            // Обработчик. Вызывается автоматически при возникновении причины
            void obj_Mail(object sender, string message)
            {
                String str = "Получена информация от класса "
                    + sender.GetType().Name + ":\n"
                    + "\"" + message + "\"";
                Console.WriteLine(str);
            }
       }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = MyClass.Title;
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 45;
                Console.WindowHeight = 4;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    В последнем примере в классе-источнике сообщения мы объявили поле-ссылку на событие как общедоступную, чтобы в любом классе-клиенте, содержащем объект с событием, эта ссылка была видна и позволяла подписать обработчики. Для инициации события мы в качестве внешней причины применили срабатывание системного таймера. В реальных условиях это может быть что угодно, но чаще всего события генерируются операционной системы, как реакция на действия пользователя или как ответ на переход программных объектов в определенные состояния.

    Модель программирования, основанная на событиях, сейчас является наиболее популярной, а для некоторых задач - и единственно возможной. Она наиболее приспособлена для построения интерактивных приложений, где требуется организовать диалог с пользователем. Когда пользователь не производит никаких действий, приложение находится в состоянии простоя ( idle - простой) и требует минимальных ресурсов компьютера. Только консольные приложения, многие из которых называют утилитами, выполняются непрерывно от начала до конца, и возможно не требуют применения событий так остро. Но и они для обмена информацией объектов приложения тоже могут использовать механизм событий, в чем мы только что убедились на последнем примере.

    Создание событий с контролем адресатов

    Если мы только используем библиотечные классы, содержащие события, то нам вообще никакой способ создания событий знать необязатально. Уметь подписывать в клиенте обработчики на уже готовые события и понимать причины, приводящие к возникновению этих событий, - это все, что нам необходимо.

    Если мы хотим как-то повлиять на событие библиотечного класса, то нужно помнить, что событие объявляется общедоступными и может наследоваться как обычный член класса. Это дает нам право расширить библиотечный класс, в котором можно выполнить какие-то действия: скрыть событие, объявив его заново с ключевым словом new как закрытое, или переопределить диспетчер события. Диспетчер в библиотечных классах обычно объявляется виртуальным и может быть переопределен в производном классе или даже совсем скрыт для следующих потомков его переобъявлением.

    Независимо от того, создаем ли мы свои классы с событиями, или расширяем библиотечные классы, добавляя в них новые события, о существовании еще одного способа знать не помешает.

    Рассмотренный ранее способ создания объявления событий в классах-источниках считается стандартным. Мы создали таким способом класс с событием и теперь этим событием может пользоваться кто угодно, присоединяя к нему любые обработчики, лишь бы они имели разрешенный прототип. Такой способ не позволяет программисту осуществлять должный контроль за обработчиками при их добавлении и изъятии, как это может потребоваться в некоторых случаях. Для решения подобных задач существует другой способ создания события в классе-источнике, который является более гибким и его называют расширенным. Этот способ позволяет отслеживать в классе, содержащем событие, те обработчики, которые клиентский код будет присоединять к нему.

    Синтаксис создания события расширенным способом сильно напоминает создание в классе свойства. Он также основывается на закрытом базовом поле и также обертывает это поле общедоступным членом. Только вместо автоматически вызываемых аксессоров get и set используются ключевые слова add и remove, а вместо базового поля используется закрытое поле-делегат. Методы add и remove автоматически вызываются при добавлении обработчика в список делегата или удаления его из списка.

    Пусть мы создаем класс с событием и не хотим, чтобы какой-то определенный клиентский класс пользовался этим событием, т.е. один тип не хочет обслуживать другой неприятный ему тип. Тогда код создания события мог бы выглядеть так, как показано в следующем примере

    using System;
        
    namespace Test
    {
        // Объявление делегата как типа 
        delegate void Message(object sender, string message);
        
        // Класс-источник сообщения
        class SourceMessage
        {
            ////////////////////////////////////////////
            // Это был стандартный способ
            ////////////////////////////////////////////
            // public event Message Mail;
        
            ////////////////////////////////////////////
            // Это расширенный способ
            ////////////////////////////////////////////
            // Объявление внутреннего поля как экземпляра делегата
            private Message mail;
        
            // Создание события с контролем адресатов
            public event Message Mail
            {
                add     // Контролируем добавление обработчиков в список
                {
                    // Имя получателя сообщения
                    string targetName = value.Target.GetType().Name;
        
                    // Выявляем того, кого не любим!
                    if (targetName == "BadClass")
                        // Ласково уведомляем 
                        Console.WriteLine("Типу BadClass 
          доступ к событию запрещен!\n");
                    else
                        mail += value;
                }
                remove  // Удаление контролировать не будем. 
             //"Его там не стояло!" 
                {
                    mail -= value;
                }
            }
        
            /////////////////////////////////////////////
            // Все остальное то-же самое, кроме замены
            // события Mail на закрытое поле-делегат mail
            /////////////////////////////////////////////
            // Метод диспетчеризации события. Объявили виртуальным 
            // и защищенным для возможности замещения в наследниках
            protected virtual void OnMail(string mess)
            {
                // Инициируем рассылку сообщения всем, 
                // кто подписался на событие
                // Здесь используется поле-делегат, внутри можно (и нужно!)
                if (mail != null)       // Если не пустой делегат
                    mail(this, mess);   // Инициируем событие
            }
        
            // Объявляем и инициируем внутреннее поле базовым сообщением
            string message = "Сообщаю, что сработал таймер!!!\n"
                + "Текущее время ";
        
            // Объявляем внутреннее поле для видимости в методах
            System.Timers.Timer timer;
        
            // Конструктор класса-источника сообщения
            public SourceMessage()
            {
                // Создаем и запускаем таймер, который по истечении 
                // заданного времени инициирует рассылку сообщения
                timer = new System.Timers.Timer();
                timer.Interval = 5000d; // Сработает через 5 секунд
                timer.Elapsed += new System.Timers.ElapsedEventHandler
        (timer_Elapsed);
                timer.Start();      // Запускаем таймер
            }
        
            // Инициирование события из внешнего источника
            void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
            {
                // Извлекаем текущее системное время
                DateTime curTime = DateTime.Now;
                // Дополняем базовое сообщение временем
                message += curTime.ToString();
        
                OnMail(message);    // Вызываем метод диспетчеризации
                timer.Stop();       // Останавливаем системный таймер
            }
        }
        
        // Хороший класс-получатель сообщения
        class GoodClass
        {
            public GoodClass()
            {
                // Создаем объект с событием
                SourceMessage obj = new SourceMessage();
        
                // Подписываем на событие обработчик,
                // печатающий полученную информацию
                obj.Mail += new Message(obj_Mail);
            }
        
            // Обработчик
            void obj_Mail(object sender, string message)
            {
                String str = this.GetType().Name
                    + " получил информацию от "
                    + sender.GetType().Name + "\n"
                    + "следующего содержания:\n\"" 
         + message + "\"";
                Console.WriteLine(str);
            }
        }
        
        // Плохой класс, которому не разрешено получать сообщения
        class BadClass
        {
            public BadClass()
            {
                // Создаем объект с событием
                SourceMessage obj = new SourceMessage();
        
                // Подписываемся на событие в благостном неведении,
                // что мы попали в черный список и нас
                // не включат в список рассылки
                obj.Mail += new Message(obj_Mail);
            }
        
            // Обработчик
            void obj_Mail(object sender, string message)
            {
                String str = "Получена информация от класса "
                    + sender.GetType().Name + ":\n"
                    + "\"" + message + "\"";
                Console.WriteLine(str);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Расширенное создание 
        события (через 5 секунд!)";
        
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 47;
                Console.WindowHeight = 7;
        
                new GoodClass();// Исполняем хороший класс
                new BadClass(); // Исполняем плохой класс
        
                Console.ReadLine();
            }
        }
    }

    Из распечатки мы видим, что хотя событие класса GoodClass и было инициировано первым, но отработало позднее, поскольку ждало срабатывания системного таймера.

    Создание событий со списком делегатов

    В библиотеке .NET Framework для структурированной поддержки событий создан класс EventHandlerList, который может хранить в себе элементы, выполняющие функцию делегатов с присоединенными к ним обработчиками. Эти элементы маркируются ключами типа Object. Каждому ключу соответствует один или несколько одноадресных делегатов, способных хранить вызовы обработчиков одного и того же прототипа.

    За событием закрепляется определенный ключ и при манипуляции с событием во внешнем коде делегаты, маркированные этим ключом, добавляются в список. Чтобы активизировать событие, достаточно обратиться к списку с закрепленным за событием ключом, все одноадресные делегаты будут извлечены из списка и присоединенные обработчики будут выполнены.

    Поскольку элементы списка реализуют вызов обработчиков одноадресным способом, то использование списка позволяет прикреплять обработчики не только с пустым возвращаемым значением, но и с возвращаемым значением любого типа. Хотя в большинстве случаев это несущественное преимущество, поскольку результат обработки можно вернуть и через аргументы обработчика. Класс EventHandlerList находится в пространстве имен System. ComponentModel. В следующем примере демонстрируется создание событий на основе списка одноадресных делегатов.

    using System;
        
    namespace Test
    {
        // Класс-источник сообщения
        class SourceMessage
        {
            // Создание списка делегатов вместо базовых полей
            System.ComponentModel.EventHandlerList eventList
                = new System.ComponentModel.EventHandlerList();
        
            // Объявление типов делегатов
            public delegate void Message1();
            public delegate int Message2(string message);
            // Непустое возвращаемое значение
            public delegate void Message3(object sender, string message);
        
            // Создание ключей для делегатов
            Object key1 = new Object();
            Object key2 = new Object();
            Object key3 = new Object();
        
            // Создание события на базе списка
            public event Message1 Mail1
            {
                add
                {
                    eventList.AddHandler(key1, value);// Дополняем список делегатов
                }
                remove
                {
                    eventList.RemoveHandler(key1, value);// Удаляем из списка
                }
            }
        
            // Создание события на базе списка
            public event Message2 Mail2
            {
                add
                {
                    eventList.AddHandler(key2, value);// Расширяем список делегатов
                }
                remove
                {
                    eventList.RemoveHandler(key2, value);// Удаляем из списка
                }
            }
        
            // Создание события на базе списка
            public event Message3 Mail3
            {
                add
                {
                    eventList.AddHandler(key3, value);// Расширяем список делегатов
                }
                remove
                {
                    eventList.RemoveHandler(key3, value);// Удаляем из списка
                }
            }
        
            // Симуляция срабатывания события Mail1
            public void DispatchMail1()
            {
                // Извекаем из списка все делегаты для Mail1, помеченные ключом
                Message1 mail1 = (Message1)eventList[key1];
                if (mail1 != null)
                    mail1();
            }
        
            // Симуляция срабатывания события Mail2
            public void DispatchMail2()
            {
                // Извекаем из списка все делегаты для Mail2, помеченные ключом
                Message2 mail2 = (Message2)eventList[key2];
                if (mail2 != null)
                    mail2("\"mail2 из SourceMessage\"");
            }
        
            // Симуляция срабатывания события Mail3
            public void DispatchMail3()
            {
                // Извекаем из списка все делегаты для Mail3, помеченные ключом
                Message3 mail3 = (Message3)eventList[key3];
                if (mail3 != null)
                    mail3(this, "\"mail3 из SourceMessage\"");
            }
        }
        
        // Получатель сообщения
        class MyClass
        {
            // Конструктор
            public MyClass()
            {
                // Создаем объект с событиями
                SourceMessage obj = new SourceMessage();
        
                // Подписываемся на обработчики
                obj.Mail1 += new SourceMessage.Message1(obj_Mail1);
                obj.Mail1 += new SourceMessage.Message1(obj_Mail1);
                obj.Mail2 += new SourceMessage.Message2(obj_Mail2);
                obj.Mail3 += new SourceMessage.Message3(obj_Mail3);
        
                // Запускаем события
                obj.DispatchMail1();
                obj.DispatchMail2();
                obj.DispatchMail3();
            }
        
            // Обработчики
            void obj_Mail1()
            {
                Console.WriteLine("Обработчик события Mail1.");
            }
        
            int obj_Mail2(string message)
            {
                Console.WriteLine("\nОбработчик события Mail2.\n" +
                    "Сообщение: {0}\n", message);
                return 1;
            }
        
            void obj_Mail3(object sender, string message)
            {
                Console.WriteLine("Обработчик события Mail3.\n" +
                    "Сообщение: {0}", message);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение списка делегатов";
        
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 36;
                Console.WindowHeight = 9;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }

    Стандартный делегат EventHandler и стандартный аргумент EventArgs

    Для большинства практических случаев обработчикам событий достаточно двух параметров, первый из которых будет адресовать объект, возбудивший событие, а второй - некоторую дополнительную информацию. Для этих целей существует библиотечный делегат EventHandler. Он используется во множестве библиотечных классов и вполне может удовлетворить большинство наших потребностей.

    Второй аргумент делегата всегда можно расширить, если мы хотим передать с событием свою специфическую информацию. Приведем пример

    using System;
        
    namespace Test
    {
        // Расширение библиотечного класса аргументов
        class MyEventArgs : EventArgs
        {
            private string message; // Поле для хранения сообщения
        
            // Сервис доступа к полю 
            public String Message
            {
                get { return message; }
                set { message = value; }
            }
        }
        
        // Класс-источник сообщения
        class SourceEvent
        {
            // Тип стандартного делегата объявлен в mscorlib.System так
            // public delegate void EventHandler(object sender, 
        System.EventArgs e)
        
            // Создание события на базе стандартного делегата
            public event EventHandler Event;
        
            // Метод диспетчеризации события
            protected virtual void OnEvent(EventArgs args)
            {
                if (Event != null)
                    Event(this, args); // Вызываем обработчики
            }
        
            // Симулятор срабатывания события Event по внешней причине
            public void SimulateEvent()
            {
                // Создаем толстый объект и формируем передаваемую информацию
                MyEventArgs args = new MyEventArgs();
                args.Message = "'Это сообщение 
        поступило с событием'";
        
                // Вызываем функцию диспетчеризации события.
                // Функция ожидает тонкий объект, а ей 
                // передается толстый объект - это нормально
                OnEvent(args);
            }
        }
        
        // Получатель сообщения
        class MyClass
        {
            // Конструктор
            public MyClass()
            {
                // Создаем объект, имеющий событие
                SourceEvent obj = new SourceEvent();
        
                // Подписываемся на обработчики события
                obj.Event += new EventHandler(Handler1);
                obj.Event += new EventHandler(Handler2);
        
                // Вызываем симулятор возникновения события
                obj.SimulateEvent();
            }
        
            void Handler1(object sender, EventArgs e)
            {
                // Хотим извлечь информацию из толстого объекта,
                // значит нужно повысить полномочия тонкой ссылки
                MyEventArgs args = (MyEventArgs)e;
                String message = args.Message;
                Console.WriteLine("(Handler1) Получена 
        информация:\n" + message);
                Console.WriteLine();
            }
        
            void Handler2(object sender, EventArgs e)
            {
                Console.WriteLine("(Handler2) Событие из о
        бъекта {0}\n" +
                    "Передан объект-аргумент {1}",
                    sender.GetType().Name, 
                    e.GetType().Name);
            }
        }
        
        // Запуск
        class Program
        {
            static void Main()
            {
                // Настройка консоли
                Console.Title = "Применение делегата EventHandler";
        
                Console.ForegroundColor = ConsoleColor.White;
                Console.CursorVisible = false;
                Console.WindowWidth = 43;
                Console.WindowHeight = 6;
        
                new MyClass();// Исполняем
        
                Console.ReadLine();
            }
        }
    }
    Вернуться к учебному плану