深入解析C#编程中的事件

编程

  一个事件是一个使对象或类可以提供公告的成员。用户可以通过提供事件句柄来为事件添加可执行代码。事件使用事件声明来声明:

  一个事件声明既可以是一个事件域声明也可以是事件属性声明。在每种情况中,声明都可以由属性集合, new 修饰符, 四个访问修饰符的有效组合 和一个静态修饰符组成。

  一个事件声明的类型必须是一个代表类型, 而那个代表类型必须至少同事件本身一样可访问。

  一个事件域声明与一个声明了一个或多个代表类型域的域声明相应。在一个事件域声明中不允许有readonly 修饰符。

  一个事件属性声明与声明了一个代表类型属性的属性声明相应。除了同时包含get访问符和set访问符的事件属性声明,成员名称和访问符声明对于那些属性声明来说都是相同的,并且不允许包含virtual、 override和abstract 修饰符。

  在包含一个事件成员声明的类或结构的程序文字中,事件成员与代表类型的私有域或属性相关,而这个成员可以用于任何允许使用域或属性的上下文中。

  如果一个类或结构的程序文字外面包含一个事件成员声明,这个事件成员就只能被作为+= 和 -= 操作符 (§的右手操作数使用。这些操作符被用来为事件成员附加或去掉事件句柄,而这个事件成员的访问操作符控制操作在其中被执行的上下文。

  由于+= 和 -= 是唯一可以在声明了事件成员的类型外的事件上使用的操作,外部代码可以为一个事件添加或去掉句柄,但是不能通过任何其他途径获得或修改基本事件域或事件属性的数值。

  在例子中

public delegate void EventHandler(object sender, Event e);
public class Button: Control
{
 public event EventHandler Click;
 protected void OnClick(Event e) {
  if (Click != null) Click(this, e);
 }
 public void Reset() {
  Click = null;
 }
}

  对使用Button类中的Click事件域没有限制。作为演示的例子,这个域可以在代表调用表达式中被检验、修改和使用。类Button中的OnClick方法"引起"Click事件。引起一个事件的概念与调用由事件成员表示的代表正好相同-因此,对于引起事件没有特殊的语言构造。注意代表的调用是通过检查保证代表是非空后才进行的。

  在类Button的声明外面,成员Click只能被用在+= 和 -= 操作符右手边,如下

b.Click += new EventHandler(...);

  它把一个代表附加到事件Click的调用列表中,并且

b.Click -= new EventHandler(...);

  它把一个代表从Click事件的调用列表中删除。

  在一个形式为x += y 或 x -= y的操作中,当x是一个事件成员而引用在包含x的声明的类型外面发生时,操作的结果就是void(在赋值后与x的数值相反)。这个规则禁止外部代码直接检查事件成员的基本代表。
下面的例子介绍了事件句柄如何附加到上面的类Button的实例中:

public class LoginDialog: Form
{
 Button OkButton;
 Button CancelButton;
 public LoginDialog() {
  OkButton = new Button(...);
  OkButton.Click += new EventHandler(OkButtonClick);
  CancelButton = new Button(...);
  CancelButton.Click += new EventHandler(CancelButtonClick);
 }
 void OkButtonClick(object sender, Event e) {
  // Handle OkButton.Click event
 }
 void CancelButtonClick(object sender, Event e) {
  // Handle CancelButton.Click event
 }
}

  这里,构造函数LoginDialog创建了两个Button实例,并且把事件句柄附加到事件Click中。

  事件成员是典型域,就像上面的Button例子中所示。在每个事件消耗一个域存储的情况是不可接受的,一个类可以声明事件属性来替代事件域,并且使用私有机制来存储基本的代表。(设想在某种情况下,大多数事件都是未处理的,每个事件使用一个域就不能被接受。使用属性而不是域的能力允许开发人员在空间和时间上面取得一个折中方案。)

  在例子中

class Control: Component
{
 // Unique keys for events
 static readonly object mouseDownEventKey = new object();
 static readonly object mouseUpEventKey = new object();
 // Return event handler associated with key
 protected Delegate GetEventHandler(object key) {...}
 // Set event handler associated with key
 protected void SetEventHandler(object key, Delegate handler) {...}
 // MouseDown event property
 public event MouseEventHandler MouseDown {
  get {
   return (MouseEventHandler)GetEventHandler(mouseDownEventKey);
  }
  set {
   SetEventHandler(mouseDownEventKey, value);
  }
 }
 // MouseUp event property
 public event MouseEventHandler MouseUp {
  get {
   return (MouseEventHandler)GetEventHandler(mouseUpEventKey);
  }
  set {
   SetEventHandler(mouseUpEventKey, value);
  }
}
}

  类Control为事件提供了一种内部存储机制。方法SetEventHandler用一个key来与代表数值相关,而方法GetEventHandler返回与key相关的当前代表。大概基本的存储机制是按照把空代表类型与key相关不会有消耗而设计的,因此无句柄的事件不占用存储空间。

 实例变量初始化函数

  当一个构造函数没有构造初始化函数或一个形式为base(...)的构造函数初始化函数,构造函数就就隐含的执行被类中声明的实例域的变量初始化函数指定的初始化。这与赋值序列相关,这些赋值在直接基类构造函数的隐式调用前,在构造函数的入口被直接执行。变量初始化函数按照它们在类声明中出现的文字顺序执行。

  构造函数执行

  可以把一个实例变量初始化函数和一个构造函数初始化函数,看作是自动插在构造函数主体中的第一条语句前。例子

using System.Collections;
class A
{
 int x = 1, y = -1, count;
 public A() {
  count = 0;
 }
 public A(int n) {
  count = n;
 }
}
class B: A
{
 double sqrt2 = Math.Sqrt(2.0);
 ArrayList items = new ArrayList(100);
 int max;
 public B(): this(100) {
  items.Add("default");
 }
 public B(int n): base(n - 1) {
  max = n;
 }
}

  包含了许多变量初始化函数,并且也包含了每个形式(base和this)的构造函数初始化函数。这个例子与下面介绍的例子相关,在那里,每条注释指明了一个自动插入的语句(自动插入构造函数调用所使用的语法不是有效的,至少用来演示这个机制)。

using System.Collections;
class A
{
 int x, y, count;
 public A() {
  x = 1; // Variable initializer
  y = -1; // Variable initializer
  object(); // Invoke object() constructor
  count = 0;
 }
 public A(int n) {
  x = 1; // Variable initializer
  y = -1; // Variable initializer
  object(); // Invoke object() constructor
  count = n;
 }
}
class B: A
{
 double sqrt2;
 ArrayList items;
 int max;
 public B(): this(100) {
  B(100); // Invoke B(int) constructor
  items.Add("default");
 }
 public B(int n): base(n - 1) {
  sqrt2 = Math.Sqrt(2.0); // Variable initializer
  items = new ArrayList(100); // Variable initializer
  A(n - 1); // Invoke A(int) constructor
  max = n;
 }
}
 

  注意变量初始化函数被转换为赋值语句,并且那个赋值语句在对基类构造函数调用前执行。这个顺序确保了所有实例域在任何访问实例的语句执行前,被它们的变量初始化函数初始化。例如:

class A
{
 public A() {
  PrintFields();
 }
 public virtual void PrintFields() {}
}
class B: A
{
 int x = 1;
 int y;
 public B() {
  y = -1;
 }
 public override void PrintFields() {
  Console.WriteLine("x = {0}, y = {1}", x, y);
 }
}
 

  当new B() 被用来创建B的实例时,产生下面的输出:

x = 1, y = 0

  因为变量初始化函数在基类构造函数被调用前执行,所以x的数值是1。可是,y的数值是0(int的默认数值),这是因为对y的赋值直到基类构造函数返回才被执行。

  默认构造函数

  如果一个类不包含任何构造函数声明,就会自动提供一个默认的构造函数。默认的构造函数通常是下面的形式

public C(): base() {}

  这里C是类的名称。默认构造函数完全调用直接基类的无参数构造函数。如果直接基类中没有可访问的无参数构造函数,就会发生错误。在例子中

class Message
{
 object sender;
 string text;
}

  因为类不包含构造函数声明,所以提供了一个默认构造函数。因此,这个例子正好等同于

class Message
{
 object sender;
 string text;
 public Message(): base() {}
}
  私有构造函数

  当一个类只声明了私有的构造函数时,其他类就不能从这个类派生或创建类的实例。私有构造函数通常用在只包含静态成员的类中。例如:

public class Trig
{
private Trig() {} // Prevent instantiation
public const double PI = 3.14159265358979323846;
public static double Sin(double x) {...}
public static double Cos(double x) {...}
public static double Tan(double x) {...}
}

  Trig 类提供了一组相关的方法和常数,但没有被例示。因此,它声明了一个单独的私有构造函数。注意至少要必须声明一个私有构造函数来避免自动生成默认的构造函数(它通常有公共的访问性)。

  可选的构造函数参数

  构造函数的this(...) 形式通常用于与实现可选的构造函数参数的关联上。在这个例子中

class Text
{
 public Text(): this(0, 0, null) {}
 public Text(int x, int y): this(x, y, null) {}
 public Text(int x, int y, string s) {
  // Actual constructor implementation
 }
}

  前两个构造函数只是为丢失的参数提供了默认的数值。两个都使用了一个this(...)构造函数的初始化函数来调用第三个构造函数,它实际上做了对新实例进行初始化的工作。效果是那些可选的构造函数参数:

Text t1 = new Text(); // Same as Text(0, 0, null)
Text t2 = new Text(5, 10); // Same as Text(5, 10, null)
Text t3 = new Text(5, 20, "Hello");

  析构函数

  析构函数是一个实现破坏一个类的实例的行为的成员。析构函数使用析构函数声明来声明:

  一个析构函数声明的标识符必须为声明析构函数的类命名,如果指定了任何其他名称,就会发生一个错误。

  析构函数声明的主体指定了为了对类的新实例进行初始化而执行的语句。这于一个有void返回类型的实例方法的主体相关。

  例子

class Test
{
 static void Main() {
  A.F();
  B.F();
 }
}
class A
{
 static A() {
  Console.WriteLine("Init A");
 }
 public static void F() {
  Console.WriteLine("A.F");
 }
}
class B
{
 static B() {
  Console.WriteLine("Init B");
 }
 public static void F() {
  Console.WriteLine("B.F");
 }
}

  会产生或者是下面的输出:

Init A
A.F
Init B
B.F

  或者是下面的输出:

Init B
Init A
A.F
B.F

时间: 2025-01-26 23:27:05

深入解析C#编程中的事件的相关文章

深入解析C++程序中激发事件和COM中的事件处理_C 语言

本机 C++ 中的事件处理在处理本机 C ++ 事件时,您分别使用 event_source 和 event_receiver 特性设置事件源和事件接收器,并指定 type=native.这些特性允许应用它们的类在本机的非 COM 上下文中激发和处理事件. 声明事件 在事件源类中,对一个方法声明使用 __event关键字可将该方法声明为事件.请确保声明该方法,但不要定义它:这样做会产生编译器错误,因为将该方法转换为事件时编译器会隐式定义它.本机事件可以是带有零个或多个参数的方法.返回类型可以是

深入解析C++编程中线程池的使用_C 语言

为什么需要线程池目前的大多数网络服务器,包括Web服务器.Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短. 传 统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务.任务执行完毕后,线程退出,这就是是"即时创建,即 时销毁"的策略.尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于 不停的创建线程,销

深入解析C++编程中对设计模式中的策略模式的运用_C 语言

策略模式也是一种非常常用的设计模式,而且也不复杂.下面我们就来看看这种模式. 定义:策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换.策略模式让算法独立于使用它的客户而独立变化. 角色:     抽象策略角色(Strategy): 抽象策略类.     具体策略角色(ConcreteStrategy):封装了继续相关的算法和行为.     环境角色(Context):持有一个策略类的引用,最终给客户端调用. UML图: 例子: #include <iostream>

解析C++编程中的bad_cast异常_C 语言

由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常.语法 catch (bad_cast) statement 备注 bad_cast 的接口为: class bad_cast : public exception { public: bad_cast(const char * _Message = "bad cast"); bad_cast(const bad_cast &); virtual ~bad_cast(); }; 以下代码包含

深入解析C++编程中范围解析运算符的作用及使用_C 语言

范围解析运算符 :: 用于标识和消除在不同范围内使用的标识符. 语法 复制代码 代码如下: :: identifier class-name :: identifier namespace :: identifier enum class :: identifier enum struct :: identifier 备注identifier 可以是变量.函数或枚举值.具有命名空间和类以下示例显示范围解析运算符如何与命名空间和类一起使用: namespace NamespaceA{ int x;

深入解析Java编程中面向字节流的一些应用_java

文件输入输出流 文件输入输出流 FileInputStream 和 FileOutputStream 负责完成对本地磁盘文件的顺序输入输出操作. [例]通过程序创建一个文件,从键盘输入字符,当遇到字符"#"时结束,在屏幕上显示该文件的所有内容 import java.io.*; class ep10_5{ public static void main(String args[]){ char ch; int data; try{ FileInputStream a=new FileI

深入解析C++编程中的纯虚函数和抽象类_C 语言

C++纯虚函数详解 有时在基类中将某一成员函数定为虚函数,并不是基类本身的要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义. 纯虚函数是在声明虚函数时被"初始化"为0的函数.声明纯虚函数的一般形式是 virtual 函数类型 函数名 (参数表列) = 0; 关于纯虚函数需要注意的几点: 纯虚函数没有函数体: 最后面的"=0"并不表示函数返回值为0,它只起形式上的作用,告诉编译系统"这是纯虚函数"; 这是一个

深入解析C++编程中的静态成员函数_C 语言

C++静态成员函数 与数据成员类似,成员函数也可以定义为静态的,在类中声明函数的前面加static就成了静态成员函数.如 static int volume( ); 和静态数据成员一样,静态成员函数是类的一部分,而不是对象的一部分. 如果要在类外调用公用的静态成员函数,要用类名和域运算符"::".如 Box::volume( ); 实际上也允许通过对象名调用静态成员函数,如 a.volume( ); 但这并不意味着此函数是属于对象a的,而只是用a的类型而已. 与静态数据成员不同,静态成

解析C++编程中的#include和条件编译_C 语言

文件包含的作用 所谓"文件包含"处理是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件之中.C++提供了#include命令用来实现"文件包含"的操作.如在file1.cpp中有以下#include命令: #include ″file2.cpp″ 它的作用见图示意. "文件包含"命令是很有用的,它可以节省程序设计人员的重复劳动. #include命令的应用很广泛,绝大多数C++程序中都包括#include命令.现在,