第三课:构建类
在面向对象的语言中,大量的工作要在对象里去做。除了最简单的应用所有的工作都需要构造一个或多个自定义的类,每个类中有多个属性和方法被用于去完成对象的任务。这课讨论如何创建自定义的类。
学完这课,你将能够:
■ 描述和使用继承.
■ 描述和使用接口.
■ 描述和使用部分类.
■ 创建泛型类型和使用内建的泛型类型。
■ 响应事件和引发事件。
■ 添加属性去描述程序集和方法。
■ Move a type from one class library to another using type forwarding.
什么是继承?
.NET Framework有几千个类,每个类有许多方法和属性。如果.NET Framework没有被非常一致性地实现,保持跟踪所有的类和成员是不可能的。举个例子来说,每个类都有一个ToString 方法,它做了一个事—把类的实例转化为字符串。相似的,很多类支持相同的操作,比如比较两个实例是否相等。因为继承和接口使一致性成为可能。使用继承从已存在的类去创建新类。你能通过继承System.Application
Exception去创建自定义异常。
class DerivedException : System.ApplicationException
{
public override string Message
{
get { return "An error occurred in the application."; }
}
}
你能抛出和捕捉新的异常因为这个自定义类继承了基类的行为,如下:
try
{
throw new DerivedException();
}
catch (DerivedException ex)
{
Console.WriteLine("Source: {0}, Error: {1}", ex.Source, ex.Message);
}
自定义异常不仅支持throw/catch,且可使用包括继承自System.Application-
Exception的成员。继承的另一个好处是能够相互交换地使用继承类,使功能得到扩展。
什么是接口?
接口,如同合同,定义了一系列的类的成员,这些类实现了接口提供的成员。例如,
IComparable 接口定义了CompareTo方法,它定义了两个类的实例能够去比较判断是否相等。所有的类无论是内建的还是自定义的只要实现了IComparable接口,就可以比较判断是否相等。接口IDisposable提供了一个方法Dispose,使创建类的实例的程序集去释放实例消费的任何资源。去创建一个实现IDisposable接口的类:
1. 类声明
// C#
class BigClass
{
}
2. 加入接口声明
class BigClass : IDisposable
{
}
类 描述
IComparable 实现它的类型可以排序,例如,数字和字符串类。
IComparable 被要求排序。
IDisposable 定义了一些通过手动去销毁一个对象。这个接口对于消费资源的大对象
是很重要的,或锁住对资源的访问比如数据库。
IConvertible 使一个类转换为一个基类型,例如Boolean, Byte, Double,String.
ICloneable 支持对象的拷贝。
IEquatable 允许你去比较一个类的实例为了判断是否相等,例如如果你实现这个接口 1 你能说是否(a == b)”.
IFormattable 能使你把一个对象的值转化为一个特定格式的字符串,它比ToString方法
提供了更多的灵活性。
你也能创建你自己的接口。如果你需要创建多个自定义类,并且它们行为相似并且能交换使用,那么定义类是有用的。
什么是泛型?
泛型是.NET Framework 的类型系统的一部分,它允许你定义一个类型而不用考虑一些细节。不用去定义参数类型或成员类型,你能够允许代码使用你的类型去定义它。这样它能使客户代码去修正你的类型去满足它自己的特定的需要泛型是在.NET 2.0中新出现的。.NET Framework version 2.0中的System.Collections.Generic namespace包括几个泛型集合类包括Dictionary, Queue, SortedDictionary, 和 SortedList.这些类的功能与它们在System.Collections 中的对应的集合类相同,但是泛型类提供了更好的性能和类型
为什么用泛型?
.NET Framework 的Versions 1.0 and 1.1 不支持泛型.开发者只能使用Object 类 作为参数和成员并且要把Object 类和其它类型相互转化。
相对于用Object类泛型有两个好处:
■ 减少运行时错误:当你在做Object 类和其它类型相互转化时编译器不能检测到类型错误。例如,如果你把一个字符串转换为一个Object 类 然后试图把Object 类转换为一个整形,编译器无法捕捉这个错误,于是运行时将会抛出一个异常。使用泛型允许编译器在你的程序运行之前去捕捉类型错误。同时,你能定义约束去限制一个泛型中使用的类型,使编译器能够探测不相容的类型。
■ 更高的性能:类型转换要求装箱和拆箱(boxing and unboxing),这个过程占用了进程的时间和降低了性能。使用泛型不要求类型转换和装箱(boxing and unboxing),这样做提升了运行时的性能。
真实的世界
我从没有感受到泛型带来的性能提升,但是,根据微软所说的,泛型比使用类型转换的速度更快。事实上,类型转换类型转换的速度是使用泛型速度的几倍。但是你可能在你的应用中不注意性能差别。所有由于它能带来类型安全,所以仍然要用泛型。
如何创建一个泛型类型
首先,测试以下的类。Obj类和Gen类做了相同的事情,但是Obj类使用Object类去赋任何值,而Gen类使用泛型:
class Obj
{
public Object t;
public Object u;
public Obj(Object _t, Object _u)
{
t = _t;
u = _u;
}
}
class Gen<T, U>
{
public T t;
public U u;
public Gen(T _t, U _u)
{
t = _t;
u = _u;
}
}
如你所见, Obj 类有两个Object类型的成员。Gen类有两个类型T和U的成员。依赖你是如何使用代码去使用Gen 类,T 和U 可以是一个string,int,自定义类等等。
创建一个泛型有一个重要的限制:泛型代码仅仅在泛型每种可能构造的实例都能编译通过,泛型代码才是有效的。事实上,当你写泛型代码时,你被限制在Object类的能力中。因此,你能够在你的类中调用ToString或GetHashCode方法,但是不能使用+ 或 > 操作符。这些限制并不能应用到客户代码中,因为它已经为泛型声明了一个类型。
如何使用一个泛型类型
考虑一下console 应用代码,使用了Gen 和 Obj 类:
// Add two strings using the Obj class
Obj oa = new Obj("Hello, ", "World!");
Console.WriteLine((string)oa.t + (string)oa.u);
// Add two strings using the Gen class
Gen<string, string> ga = new Gen<string, string>("Hello, ", "World!");
Console.WriteLine(ga.t + ga.u);
// Add a double and an int using the Obj class
Obj ob = new Obj(10.125, 2005);
Console.WriteLine((double)ob.t + (int)ob.u);
// Add a double and an int using the Gen class
Gen<double, int> gb = new Gen<double, int>(10.125, 2005);
Console.WriteLine(gb.t + gb.u);
如果你在console应用中运行代码,Obj类和Gen类执行的结果是相同的。但是,使用Gen类的代码事实上工作起来会更快,因为它不要求装箱和拆箱。还有,开发者使用Gen类时会更省事。首先,开发者不用手动地去把Object类转化为合适的类型。其次,类型错误会在编译时期被捕捉而不是运行时捕捉。为了显示它的好处,考虑以下代码,它包含一个错误:
Gen<double, int> gc = new Gen<double, int>(10.125, 2005);
Console.WriteLine(gc.t + gc.u);
// Add a double and an int using the Obj class
Obj oc = new Obj(10.125, 2005);
Console.WriteLine((int)oc.t + (int)oc.u);
代码的最后一行有一个错误-- oc.t的值转换成int型而不是double型。不幸的是,编译器没有捕捉到这个错误。而运行时在试图把double型转化为int型时捕捉到这个异常。
如何使用约束:
如果你写的代码是为任何类型编译通过的,那么泛型将会受到极大限制,因为你将被限制在基类Object所能做到的范围内。为了克服这个限制,使用约束去规定类型。
泛型支持四种约束类型:
■ Interface:仅仅允许实现了特点接口的类型去使用你的泛型
■ class: 仅仅允许匹配或继承了特定基类的类型去使用你的泛型
■ Constructor :要求你的类型必须实现一个无参数的构造方法.
■ Reference or value:要求类型不是引用类型就是值类型。
下面的泛型类仅仅被实现了IComparable接口的类型使用:
class CompGen<T>
where T : IComparable
{
public T t1;
public T t2;
public CompGen(T _t1, T _t2)
{
t1 = _t1;
t2 = _t2;
}
public T Max()
{
if (t2.CompareTo(t1) < 0)
return t1;
else
return t2;
}
}
以上的类将会正确地被编译。但,如果删除where子句,编译器将返回一个错误,暗示T不能包括一个CompareTo的定义。通过约束泛型使类型必须要实现IComparable,你要保证CompareTo方法能够有效。
事件
大部分的工程是非线性的。在Windows Form 应用中,你可能必须等待一个用户去点击一个按钮或按一个键,然后对这个事件做出相应。在服务器应用中,你可能必须等待一个网络请求。这些能力通过.NET Framework中的事件来提供。
什么是事件?
一个事件是一个对象发出的消息,表明一个行为的发生。这个行为被用户的交互行为所引发,比如一个鼠标的点击,或鼠标被其它应用程序所触发。引发事件的对象被称为event sender.捕捉事件的对象并且对它进行响应的对象被称为event receiver。在事件交互中,event sender类不知道哪个对象或方法将会收到(或处理)它所引发的事件。需要的是一个在源与接受器之间的媒介物(类似于指针的机制)。.NET Framework 定义了一个特殊的类型(Delegate),它能提供一个函数指针的功能。
什么是Delegate?
一个delegate是一个方法的引用。不像其它的类,一个delegate类有一个签名,并且它仅能抓住匹配delegate签名的方法的引用。一个delegate等同于类型安全的方法指针或回调。Delegate有其他的很多用途,这里只讨论delegate的事件处理的功能。一个delegate声明对于定义一个delegate类足够了。声明提供了delegate的签名,公共语言运行时提供了执行。以下的例子显示了一个事件代理的声明:
public delegate void AlarmEventHandler(object sender, EventArgs e);
标准的事件句柄的签名定义了一个方法,这个方法不会返回一个值,它的第一个参数是Object类型并且指向引发一个事件的实例,第二个参数是继承EventArgs类型并且抓住了事件数据。如果事件没有生成事件数据,第二个参数仅仅是一个EventArgs的一个实例。否则,第二个参数要继承EventArgs并且提供属性或变量去抓住事件数据。
EventHandler是一个已定义好的delegate,它代表一个事件的处理方法, 并且这个事件不应生成数据。如果你的事件生成了数据,你必须提供你自己定义的事件数据类型,同时你要创建一个delegate,在delegate中的第二个参数是你自己定义的类型,或用泛型的EventHandler delegate把泛型参数换成你的定义的参数类型。
为了让处理事件的方法与事件作关联,向事件加一个delegate的实例。这个事件处理器会在事件发生时被调用,除非删除这个delegate。
如何响应一个事件
你必须做两件事去响应一个事件:
■ 创建一个方法去响应事件。这个方法必须匹配Delegate 签名。典型地,这意味着它必须返回空置并且接受两个参数:
一个 Object类型和一个EventArgs (或其基类)。
以下代码所示:
private void button1_Click(object sender, EventArgs e)
{
// Method code
}
■ 加入事件处理器去指示哪个方法应当接收事件,如下代码:
this.button1.Click += new System.EventHandler(this.button1_Click);
NET Framework 2.0 包括一个新出现的EventHandler的泛型版本
当事件被引发时,你定义的方法将会运行。
如何引发一个事件
为了引发一个事件你必须做三件事:
■创建一个代理
public delegate void MyEventHandler(object sender, EventArgs e);
■ 创建一个事件成员
public event MyEventHandler MyEvent;
■ 当你需要引发这个事件时,在一个方法中调用代理:
MyEventHandler handler = MyEvent;
EventArgs e = new EventArgs();
if (handler != null)
{
// Invokes the delegates.
handler(this, e);
}
// Note that C# checks to determine whether handler is null.
还有,如果你需要向事件句柄传递信息是,可以写一个继承EventArgs 类的自定义类。
什么是Attributes?
Attributes可以描述一个type,method,或property,并使之能够通过使用Reflection的技术去用程序查询到它们。一般用途如下:
■ 定义一个类要求的安全权限
■ 定义安全权限去拒绝降低安全风险。
■ 什么功能,比如支持序列化。
■ 通过提供标题,描述和版权信息去描述一个assembly
Attribute 类型都从System.Attribute base class 继承并且要写在 []里. 以下代码展示如何去加入assembly attributes :
[assembly: AssemblyTitle("ch01cs")]
[assembly: AssemblyDescription("Chapter 1 Samples")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Microsoft Learning")]
[assembly: AssemblyProduct("ch01cs")]
[assembly: AssemblyCopyright("Copyright © 2006")]
[assembly: AssemblyTrademark("")]
Visual Studio 在你创建一个项目的时候,为你的assembly自动地创建一些标准的attributes, 包括标题,描述,公司,指南,和版本。你应当为你创建的工程去编辑这些attributes 因为默认不会包含一些重要的信息比如描述。
为了使一个类能够被序列化你必须加入Serializable attribute ,代码如下:
[Serializable]
class ShoppingCartItem
{
}
没有Serializable attribute, 一个类不能够被序列化。类似的,下面的代码使用
attributes 去声明它需要读C:"boot.ini 文件. 因为这个attribute的存在,如果它没有这个特定的权限,runtime 将在执行之前抛出一个异常:
using System;
using System.Security.Permissions;
[assembly:FileIOPermissionAttribute(SecurityAction.RequestMinimum, Read=@"C:"boot.ini")]
namespace DeclarativeExample
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
什么是类型传递?
类型传递是一个属性(在TypeForwardedTo中实现),它允许你将一个类型从一个程序集(程序集A)移动到另一个程序集(程序集B),并且在客户端实例化程序集A时不需要重新编译,就可以运行。在一个组件(程序集)载入并被客户端应用程序使用后,你可以用类型传递将组件中一个类型移动到另一个程序集,而客户端应用程序仍将保持工作,不需要重新编译。类型传递只能使用在从已存在的应用程序引用的组件。当你重新编译一个应用程序时,在应用程序中使用的任何类型都必须是恰当的程序集引用(这个程序集已存在)。
1.添加一个TypeForwardedTo属性到来源程序集类库。
2.将类型声明代码剪切
3.将剪切的类型声明代码粘贴到目的类库。
4.编译两个类库
下面代码示范将TypeA移动到DestLib类库的属性声明。
using System.Runtime.CompilerServices;
[assembly:TypeForwardedTo(typeof(DestLib.TypeA))]