Saturday, November 9, 2013

Использование событий с C#

    Тема этой статьи - вероятно, вторая по сложности, с которой могут сталкиваться новички при изучении программирования на языке C#. События в C# объявляются с помощью ключевого слова event. События в .Net Framework могут быть основаны как на собственном делегате, так и на шаблоне  EventHandler<TEventArgs>. События - это особый тип многоадресных делегатов, которые можно вызвать только из класса или структуры, в которой они объявлены (класс издателя). Если на событие подписаны другие классы или структуры, их методы обработчиков событий будут вызваны, когда класс издателя инициирует событие. Опишем краткий порядок действий, основанный на шаблоне EventHandler<TEventArgs>.
  Если нужно переопределить данные, которые передаются пользователю, нам необходимо объявить класс для пользовательских данных в области, видимой для классов издателя и подписчика. Затем добавьте необходимые члены для хранения данных пользовательских событий. В данном примере возвращается простая строка.
public class CustomEventArgs : EventArgs
{
    public CustomEventArgs(string s)
    {
        msg = s;
    }
    private string msg;
    public string Message
    {
        get { return msg; }
    }
}
Реализация класса EventArgs выглядит следующим образом:
    // Summary:
    //     System.EventArgs is the base class for classes containing event data.
    [Serializable]
    [ComVisible(true)]
    public class EventArgs
    {
        // Summary:
        //     Represents an event with no event data.
        public static readonly EventArgs Empty;

        // Summary:
        //     Initializes a new instance of the System.EventArgs class.
        public EventArgs();
    }
Объявления event будет выглядеть таким образом:
public event EventHandler<CustomEventArgs> CustomEvent;
Рассмотрим пример взятый с MSDN для того, чтобы посмотреть, как работают события в .Net Framework.
class Program
{
    public event EventHandler<CustomEventArgs> CustomEvent;

    static void Main(string[] args)
    {
        var pub = new Publisher();
        var sub1 = new Subscriber("sub1", pub);
        var sub2 = new Subscriber("sub2", pub);

        // Call the method that raises the event.
        pub.DoSomething();

        // Keep the console window open
        Console.WriteLine("Press Enter to close this window.");
        Console.ReadKey();
    }
}

// Class that publishes an event
class Publisher
{
    // Declare the event using EventHandler<T>
    public event EventHandler<CustomEventArgs> RaiseCustomEvent;

    public void DoSomething()
    {
        // Write some code that does something useful here
        // then raise the event. You can also raise an event
        // before you execute a block of code.
        OnRaiseCustomEvent(new CustomEventArgs("Did something"));

    }

    // Wrap event invocations inside a protected virtual method
    // to allow derived classes to override the event invocation behavior
    protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
    {
        // Make a temporary copy of the event to avoid possibility of
        // a race condition if the last subscriber unsubscribes
        // immediately after the null check and before the event is raised.
        EventHandler<CustomEventArgs> handler = RaiseCustomEvent;

        // Event will be null if there are no subscribers
        if (handler != null)
        {
            // Format the string to send inside the CustomEventArgs parameter
            e.Message += String.Format(" at {0}", DateTime.Now.ToString());

            // Use the () operator to raise the event.
            handler(this, e);
        }
    }
}

//Class that subscribes to an event
class Subscriber
{
    private string id;
    public Subscriber(string ID, Publisher pub)
    {
        id = ID;
        // Subscribe to the event using C# 2.0 syntax
        pub.RaiseCustomEvent += HandleCustomEvent;
    }

    // Define what actions to take when the event is raised.
    void HandleCustomEvent(object sender, CustomEventArgs e)
    {
        Console.WriteLine(id + " received this message: {0}", e.Message);
    }
}

// Define a class to hold custom event info
public class CustomEventArgs : EventArgs
{
    public CustomEventArgs(string s)
    {
        message = s;
    }
    private string message;

    public string Message
    {
        get { return message; }
        set { message = value; }
    }
}
В данном примере рассмотрено простоту использования события в .Net Framework. Код очень легкий по своему содержанию, поэтому его не нужно отдельно комментировать. Но в данном примере допущена серьёзная ошибка, которая приводит к утечке ресурсов системы. Эта проблема будет рассмотрена дальше в статье. А сейчас для новичков хочу показать пример, как создавать события  на основании собственных делегатов.
public delegate void CustomDelegate(string text);
public event CustomDelegate CustomEvent;
   Очень часто события могут применяться при программировании UI интерфейса. При программировании на Windows Forms это, по сути, есть неотъемлемой частью разработки интерфейса и позволяет уведомлять пользователях об изменениях тех или иных событий пользовательских контролов (В данном случае под контролами имеются визуальные элементы управления пользовательского интерфейса). Например, уведомления пользователя о том что изменился элемент, выбранный в списке, что поменялся текст в поле ввода, что поле ввода потеряло фокус и т.д. Так что будучи прикладным разработчиком, Вы с таким сталкиваетесь часто.

Общие сведения о событиях

События имеют следующие свойства.
  • Издатель определяет момент вызова события, подписчики определяют предпринятое ответное действие.
  • У события может быть несколько подписчиков. Подписчик может обрабатывать несколько событий от нескольких издателей. События, не имеющие подписчиков, никогда не возникают.
  •  Обычно события используются для оповещения о действиях пользователя, таких как нажатия кнопок или выбор меню и их пунктов в графическом пользовательском интерфейсе.
  • Если событие имеет несколько подписчиков, то при его возникновении происходит синхронный вызов обработчиков событий. Сведения об асинхронном вызове событий см. в разделе Асинхронный вызов синхронных методов.
  • В библиотеке классов .NET Framework в основе событий лежит делегат EventHandler и базовый класс EventArgs.
   К событиям применимы следующие ключевые слова: static, virtual, sealed и abstract. Событие можно объявить как статическое событие при помощи ключевого слова static. Событие доступно даже в том случае, если экземпляр класса отсутствует. Событие может быть помечено как виртуальное событие при помощи ключевого слова virtual. Это позволяет производным классам переопределять поведение события при помощи ключевого слова override.  Событие, переопределяющее виртуальное событие, может также быть sealed, которое определяет, что для производных классов оно более не является виртуальным. И наконец, событие можно объявить как abstract, что означает, что компилятор не создаст блоки методов доступа к событию add и remove. Таким образом, производные классы должны предоставлять собственную реализацию.
В последнем примере о ключевом слове abstract было упомянуто о методах для события add и remove. Дело в том, что в отличие от делегатов, у которых ключевые методы - это Delegate.Combine и Delegate.Remove, у событий ключевыми методами являются add и remove. Примечание: более подробно о том, как работают делегаты, можно посмотреть в моей предыдущей статье Использование делегатов в C#. Вот как это выглядит на уровне IL кода. (Что такое IL или CIL)
// Events
    .event class [mscorlib]System.EventHandler`1<class ConsoleApplication1.CustomEventArgs> RaiseCustomEvent
    {
        .addon instance void ConsoleApplication1.Publisher::add_RaiseCustomEvent(class [mscorlib]System.EventHandler`1<class ConsoleApplication1.CustomEventArgs>)
        .removeon instance void ConsoleApplication1.Publisher::remove_RaiseCustomEvent(class [mscorlib]System.EventHandler`1<class ConsoleApplication1.CustomEventArgs>)
    }

Возьмем описание с MSDN о том, что такое .addon  и .removeon 
В метаданных для события ему может быть сопоставлено четыре вида методов:
  • Директива .addon определяет метод, используемый для добавления обработчиков событий. При помощи метода GetAddMethod можно извлечь объект EventInfo для этого метода.
  • Директива .removeon определяет метод, используемый для отсоединения обработчиков событий. При помощи метода GetRemoveMethod можно извлечь объект EventInfo для этого метода.
  • Директива .fire определяет метод, используемый для вызова события. При помощи метода GetRaiseMethod можно извлечь объект EventInfo для этого метода.
  • Директива .other указывает любые другие методы, сопоставленные данному событию. При помощи метода GetOtherMethods можно извлечь массив объектов EventInfo для таких методов.
   Особенности работы с событиями:
  При работе с событиями есть несколько особенностей. Более подробно они описаны здесь. Но поскольку ссылки на другие источники пропадают или теряют актуальность, раскрою тонкости работы с событиями.

   Особенность №1 – продление времени жизни подписчика

  При подписке на событие мы добавляем в список вызовов делегата события ссылку на метод, который будет вызван при вызове события. Таким образом, память, занимаемая объектом, подписавшимся на событие, не будет освобождена до его отписки от события или до уничтожения объекта, заключающего в себе событие. Эта особенность является одной из часто встречаемых причин утечек памяти в приложениях.
   Для исправления этого недостатка часто используются weak events, слабые события. Эта тема уже была освещена на Хабре. Примечание: выше в статье я упоминал в примере Publisher->Subscriber,  у которого будет как раз и наблюдаться подобное поведение. В тестовом примере такой утечки не будет. Поскольку объект будет уничтожен.  Но в реальных примерах такой код приводит к утечке памяти, так как в основном Publisher имеет время жизни объекта меньше, чем Subscriber.  Для исправления этого кода нужно наследоваться от интерфейса IDisposable и отписаться от события в методе Dispose(), либо использовать weak events (Об мягких событиях я посвящу следующую статью).

   Особенность №2 – явная реализация интерфейса

   Событие, являющееся частью интерфейса, не может быть реализовано как поле при явной реализации этого интерфейса. В таких случаях следует либо скопировать стандартную реализацию события для реализации как свойство, либо реализовывать эту часть интерфейса неявно. Также, если Вам не нужна потокобезопасность этого события, можно использовать самое простое и эффективное определение:
EventHandler changed;
event EventHandler ISomeInterface.Changed
{
    add { changed += value; }
    remove { changed -= value; }
}
Интерфейс будет выглядеть следующим образом:
internal interface ISomeInterface
{
    event EventHandler Changed;
}

   Особенность №3 – безопасный вызов

   События перед вызовом следует проверять на null, что следует из описанной выше работы делегатов. От этого разрастается код, для избежания чего существует как минимум два способа. Первый способ описан Джоном Скитом (Jon Skeet) в его книге C# in depth:
public event EventHandler Changed = delegate { };

   Коротко и лаконично. Мы инициализируем делегат события пустым методом, поэтому он никогда не будет null. Вычесть из делегата этот метод невозможно, т.к. он определен при инициализации делегата и у него нет ни имени, ни ссылки на него из любого места программы.
 Второй способ заключается в написании метода, содержащего в себе необходимую проверку на null. Этот прием особенно хорошо работает в .NET 3.5 и выше, где доступны методы расширений (extension methods). Так как при вызове метода расширений объект, на котором он вызывается, является всего лишь параметром этого метода, то этот объект может быть пустой ссылкой, что и используется в данном случае.
public static class EventHandlerExtensions
{
    public static void SafeRaise(this EventHandler handler, object sender, EventArgs e)
    {
        if (handler != null)
            handler(sender, e);
    }
    public static void SafeRaise<TEventArgs>(this EventHandler<TEventArgs> handler,
        object sender, TEventArgs e) where TEventArgs : EventArgs
    {
        if (handler != null)
            handler(sender, e);
    }
}
Таким образом, мы можем вызывать события как Changed.SafeRaise(this, EventArgs.Empty), что экономит нам строчки кода. Также можно определить третий вариант метода расширений для случая, когда у нас EventArgs.Empty, чтобы не передавать их явно. Тогда код сократится до Changed.SafeRaise(this), но я не буду рекомендовать такой подход, т.к. для других членов вашей команды это может быть не так явно, как передача пустого аргумента.

   Особенность №4 – что не так со стандартной реализацией

   Если у вас стоит ReSharper, то вы могли наблюдать следующее его сообщение. Команда решарпера правильно считает, что не все ваши пользователи достаточно осведомлены в работе событий\делегатов в плане отписки\вычитания, но тем не менее ваши события должны работать предсказуемо не для ваших пользователей, а с точки зрения событий в .NET, а т.к. там такая особенность есть, то и в вашем коде она должна остаться.

Список источников:
http://habrahabr.ru/post/148562/ - отлично описанная статья о тонкостях работы с событиями.
Jon Skeet, C# in Depth, Second Edition – рекомендую к прочтению, так как книга ознакомит со многими нюансами при работе с языком C#. Книга обязательна к прочтению.
Jeffrey Richter, CLR via C# (Developer Reference) – книга поможет разобраться с тонкостями делегатов и событий. Книга обязательна к прочтению. Очень хорошо дополняет книгу C# in Depth.
MSDN. 10.7 Events — часть спецификации языка C# для .NET 1.1





No comments:

Post a Comment