C# Notizen 6 事件及其处理

一、理解事件
事件采用发布/订阅模型,其中发行者决定在什么情况下引发事件,而订户决定为响应事件而执行的操作。事件可以有多个订户,在这种情况下,将在事件被引发时同步调用事件处理程序。如果没有订户,事件将不会被引发。订户可处理来自多个发行者的多个事件。
ps:委托(delegate)
在传统编程术语中,事件是一种回调,能够将方法作为参数传递给另一个方法。在 C#中,这些回调被称为委托。事件处理程序不过是通过委托调用的方法。
委托类似于C和C++中的函数指针以及Delphi封装(closure),是一种类型安全的回调编写方法。委托在调用方(而不是声明方)的安全权限下运行。
委托类型定义了一个方法签名,可将任何具有兼容签名的方法与委托相关联。委托签名和常规方法签名之间的一个不同之处是,前者包含返回类型,可在参数列表中使用修饰符params。

二、订阅和取消订阅
要响应另一个类发布的事件,可订阅它:定义一个事件处理程序,其签名与事件的委托签名匹配;然后使用加法赋值运算符(+ =)将事件处理程序与事件相关联。如下代码演示了如何订阅一个基于委托ElapseEventHandler的事件。

var timer = new Timer(1000);
timer.Elapsed +=new ElapsedEventHandler(TimerElapsedHandler);
void TimerElapsedHandler(object sender,ElapsedEventArgs e)
{    
    MessageBox.Show("The timer has expired.");
}

第一个参数总是名为 sender,其类型为 object,它表示引发事件的对象;第二个参数是传递给事件处理程序的数据,名为e,其类型为EventArgs或从EventArgs派生而来的类型。事件处理程序的返回类型总是为void。

ps:方法组推断
上述程序演示了关联事件处理程序的传统方法,但 C#允许使用一种更简单的语法,这被称为方法组推断(method group inference)。下面使用方法组推断语法重写了上述程序:

var timer = new Timer(1000);
timer.Elapsed += TimerElaspsedHandler;
void TimerElapsedHandler(object sender,ElapsedEventArgs e)
{    
    MessageBox.Show("The timer has expired.");
}

虽然Visual Studio自动使用传统语法来关联事件处理程序,但是当关联自己的事件处理程序时,通常使用方法组推断语法。

可以任何方式给事件处理程序命名,但是为了确保一致性,最好通过组合如下部分来生成名称:提供事件的对象名、要处理的事件的名称以及字样Handler。

虽然很多类都使用这种方式来订阅事件,但是Visual Studio使得订阅事件很容易,尤其是订阅用户界面控件发布的事件时。
当你不希望事件被引发时调用相应的事件处理程序,必须取消订阅该事件。另外,订户对象删除前,必须取消订阅事件;否则,发行者将继续保存一个引用,它指向表示订户事件处理程序的委托,这将禁止垃圾收集器释放订户对象。
要取消订阅事件,可删除XAML标记中相应的属性或使用减法赋值运算符(− =),如下所示。
timer.Elapsed -= TimerElapsedHandler;

ps:匿名方法
匿名方法让你能够编写一个未命名的内联语句块,并在调用委托时执行它。

如下代码使用的是匿名方法,而不是命名委托:

var timer = new Timer(1000);
timer.Elapsed += delegate(object sender,ElapsedEventArgs e)
{    
    MessageBox.Show("The timer has expired.");
}

虽然将匿名方法用作事件处理程序带来了很多方便之处,但是取消订阅事件时将不那么容易

三、发布事件
类和结构都可发布事件(虽然事件通常用于类中),为此只需使用简单的事件声明。事件可基于任何有效的委托类型,但是标准做法是让事件基于委托 EventHandler 和EventHandler<T>。这些委托是在.NET Framework中预定义的,专门用于定义事件。
定义自己的事件时,要做出的第一个决策是,是否要将自定义数据发送给事件。.NET Framework 提供了 EventArgs 类,预定义的事件委托类型都支持它。如果要向事件发送自定义数据,需要从EventArgs派生出一个新类;否则,可直接使用EventArgs类型,但以后就不能修改它了,否则将破坏兼容性。因此,总是应该创建一个从EventArgs派生而来的新类(哪怕这个类最初为空),以便以后能够灵活地添加数据。
如下代码是一个从EventArgs派生而来的类

public class CustomEventArgs:System.EventArgs
{
    private object data;    
    public CustomEventArgs(object data)    
    {        
        this.data = data;   
     }    
    public Object Data    
    {       
        get       
        {            
            return this.data;        
        }    
    }
}

声明事件时,通常使用类似于字段的语法。如果没有从 EvnetArgs 派生出新类,就使用委托类型EventHandler,如下所示

public class Contact
{    
    public event EventHandler AddresChanged;
}

如果从 EventArgs 派生出了新类,就需要使用泛型委托 EventHandler<T>,并用从EventArgs派生而来的类替换T。
虽然通常使用类似于字段的事件定义,但是它并非总是效率最高的,尤其是当类包含大量事件时。类包含大量事件时,通常只有其中的几个事件有订户。使用字段声明语法时,每个事件都需单独声明,这带来了大量不必要的开销。
为解决这种问题,C#还允许使用属性语法定义事件,如下所示

public class Contact
{    
    private EventHandlerList events = new EventHandlerList();    
    private static readonly object addressChangedEventKey = new object();    
    public event EventHandler AddressChanged    
    {       
         add       
         {            
             this.events.AddHandler(addressChangedEventKey, value);
         }       
         remove        
         {            
             this.events.RemoveHandler(addressChangedEventKey, value);        
         }    
    }
}

第3行声明了一个EventHandlerList变量,这种类型专门设计用于存储事件委托列表,这样对于所有有订户的事件,都可在一个变量中存储其对应的列表项。接下来,第4行声明了一个只读的静态object变量,该变量名为addressChangedEventKey,它是在EventHandlerList中用于表示事件的键。最后,第6~16行声明了实际的事件。
访问器add将委托实例加入列表,而remove将其删除。这两个访问器都使用预定义的键来添加和删除实例。
对于事件,一种方便而一致的描述方法是,将其分为事前事件和事后事件。

事后事件最常见,它在对象的状态发生变化后发生。事前事件也称为可撤销的事件,它在对象状态发生变化前发生,让你能够撤销事件;这些事件使用 CancelEventArgs 类来存储事件数据,这个类添加了一个Cancel属性,可在代码中读写它。创建自己的可撤销事件时,应从CancelEventArgs类派生自定义的事件数据类。

四、引发事件
如果没有发起事件的机制,定义事件就没有多大意义。发起事件也称为引发或触发事件,它遵循一种标准模式。通过遵循模式,使用事件将更容易,因为相关的结构定义明确且一致。
如下演示了完整的事件处理程序

public class Contact
{    
    public event EventHandler<AddressChangedEventArgs> AddressChanged;    
    private string address;

    protected virtual void OnAddressChaged(AddressChangeEventArgs e)    
    {        
        EventHandler<AddressChangedEventArgs> handler = AddressChanged;        
        if (handler != null)         
        {            
            handler (this, e);        
        }    
    }    
    public string Address    
    {        
        get 
        { 
            return this.address;    
        }        
        set         
        {            
            this.address = value;            
            AddressChangedEventArgs args = new AddressChangedEventArgs (this.address);
            OnAddressChaged (args);        
        }    
    }
}

第3行使用委托EventHandler<T>声明了事件。第7~14行声明了一个受保护的虚方法,用于引发该事件。通过将这个方法声明为protected和virtual,让派生类能够通过重写这个方法(而不是订阅事件)来处理这个事件;对派生类来说,这是一种更方便、更自然的机制。最后,第22~23行创建了一个新的AddressChangedEventArgs实例并引发了事件。如果事件没有自定义数据,就可以使用EventArgs.Empty字段表示空EventArgs。
ps:引发使用属性语法定义的事件
如果事件是使用属性语法定义的,那么在用于引发事件的方法中,需要以稍微不同的方式从句柄列表中获取事件句柄,如下所示。

protected virtual void OnAddressChaged(AddressChangeEventArgs e)    
{        
    var handler = events [addressChangedEventKey] as EventHandler<AddressChangedEventArgs>;        
    if (handler != null)         
    {            
        handler (this, e);        
    }
}

根据约定,给引发事件的方法命名时,以 On 打头,然后是事件的名称。对于非密封类的非静态事件,事件引发方法应声明为protected和virtual。对于静态事件、密封类的非静态事件以及结构的事件,事件引发方法应声明为公有的。事件引发方法的返回类型总是void,且只接收一个参数,该参数名为e,其类型为EventArg或其合适的派生类。
这个方法遵循一种标准模式,即创建事件的一个临时备份(第 9 行),以免出现竞态条件(race condition):在 null检查(第 10行)和引发事件(第 12行)之间,最后一个订户取消订阅。
多线程和事件
这种模式只能避免一种可能的竞态条件—事件在检查后变成了null;仅当代码是多线程时,遵循这种模式才显得重要。编写多线程事件时,必须应对众多复杂的情形。例如,在执行依赖于某种状态的代码前,必须确保以线程安全的方式提供了这种状态。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 一 关于委托 1.委托的概念: C# 中的委托(Delegate)是一种引用类型变量,它类似于C的函数指针,...
    SharaYuki阅读 3,575评论 1 9
  • 事件 事件含义 事件由对象引发,通过我们提供的代码来处理。一个事件我们必须订阅(Subscribe)他们,订阅一个...
    天堂迈舞阅读 2,959评论 1 7
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,513评论 1 51
  • 城市堆肥,纸板箱是非常好的褐色材料。 最后一张4周左右,今天翻堆,基本已经都变泥土了。 (这一箱刚好是夏季高温,腐...
    艾小农阅读 1,854评论 0 0