结构和类
结构和类的共同点都是属于抽象数据类型,包含数据和数据的操作。不同点在于结构偏重于数据语意,而类偏重於行为语意。我们不关心对象内部是怎么实现的,我们关心的是他提供给我什么接口,有什么操作。从技术上来说,结构属于值类型,而类属于引用类型。结构不能指定继承基类类型,类可以。不过结构和类都能实现接口。
一、应用场合
结构的应用场合:
一、自定义数据类型,数据成员是公开的,提供工具函数。
二、抽象的数据类型,数据成员是密封的,提供相关的数据操作函数。
总之,都是围绕数据作文章。
类的应用场合:
一、提供一组类,形成一个有机整体,形成一个系统,类数据成员是密封的,只提供相互通信的函数接口。
类主要通过不同的类组成一个类间通信的系统。而类自身是整个系统的一部分。
二、成员和可访问性
作为抽象数据类型的工具,类和结构提供了丰富的封装功能。
1.字段定义数据成员,无封装
2.属性封装字段的访问方式
3.函数提供可用操作
4.事件提供了处理消息的模型
5.索引器封装了数据集合
另外
构造函数、析构函数负责初始化和清理垃圾(在c#中需要使用IDispose模式)
对于成员,可以有三大类,1、类成员,所有实例共享;(static 修饰)2、对象公开成员;(public 修饰)3、对象私有成员。其中公开成员是最重要的,私有成员属于内部实现细节。
公开成员是对象的特征,因为对于使用者来说,只能通过公开成员和对象进行互动。可以通过提取公开成员,形成一个独立接口,用来隔离具体的实现。这样,设计者便可以提供不同的类实现给客户。
三、泛型和接口
类和结构的共同点都是属于实现,而接口属于规范,客户端应该避免直接接触具体的实现,否则客户端就需要根据实现的变化而变化,这明显是不经济的。但是类和结构又有差别,类侧重行为,因此和接口更加搭配;而结构侧重数据,而接口是没有数据的,并且接口属于引用类型,当结构作为值类型转化成接口,就会产生装箱,会有性能问题。因此,接口一般不会配合结构来使用,而结构应该作为更加单纯的数据单元,不应该添加太多功能性。如果需要设计功能丰富的组件,最好是基于类来实作。当结构作为数据单元,它要修改设计的余地很少,因为数据单元的应用场合更多是被其他人修改,不具太多自主性。结论是结构自身便可以作为规范。
泛型是未完成的类型,因此提供了客户端自定义类型的机会。泛型的每一个实例,都共享相同或者类似的代码,那么客户端需要泛型的理由是什么?
c#是强类型的系统,就算逻辑上一样,因为类型不同,在编译器看来都是完全不同的代码。当客户端需要为不同类型采取相同的逻辑的时候,泛型就提供了一个快捷的,越过强类型限制的渠道。比如有个int + int 和 float + float在我们人类看来几乎是一摸一样,但是强类型的编译器要求你重复书写代码,泛型能做到T + T,然后让客户端用可支持+操作的参数生成任何类型。
从客户端的角度,它不在乎实现是怎样的,它在乎的是接口是怎样的。泛型对客户端的意义在于强化了接口,从提供特定类型的接口到提供满足特定条件的一定范围内的类型的新型接口。这就让可用性大大增强。(按照实际应用情况,我们应该将泛型视为常态,单一类型的接口才是特殊态)。
四、类和结构的实际例子
这一节是补充上来的。因为上面都是泛泛而谈,这一节用代码来说明问题。
1.典型的类
设想一个两元运算,a ?b = c,其中?未知,但是知道何时执行运算。
class A { int a, b, c; //内部数据 public class abArgs : EventArgs { public int A { set; get; } public int B { set; get; } public abArgs(int a, int b) { A = a; B = b; } } //事件的参数类型 public event EventHandler<abArgs> calcEvent; //事件 public delegate int calcType(int x, int y); //算法的委托类型 calcType calcFunc; //内部数据 public calcType CalcFunc { set{calcFunc = value;}} //算法委托 public int X { set { a = value; calc(); } } //a public int Y { set { b = value; calc(); } } //b void OnCalc() { if (calcEvent != null) calcEvent(this, new abArgs(a, b)); } //内部引发事件过程 public int Result { get { return c; } } //c public class CalcException : Exception { public CalcException(string mess) : base(mess) { } } //异常类型 //内部计算过程 void calc() { if (calcFunc != null) c = calcFunc(a, b); else throw new CalcException("未指定运算函数。");OnCalc(); } }
类A 提供了ab的设置,结果c的获取,但是A并不知道运算算法,所以需要外部提供。A 会引发运算事件 calcEvent ,对该事件感兴趣的对象可以侦听这个事件。 类A设计上不完美,它当ab放生变化立刻就调用运算过程,而不把这个执行时机交给客户。一般合理的做法是当我们准备好ab之后,才运算。根据A设计的特点,我设计了一个异常类型,在算法委托没有准备好的时候发生运算过程就引发该异常。
将A的接口提取出来就是这样:
interface IA { event EventHandler<A.abArgs> calcEvent; A.calcType CalcFunc { set; } int X { set; } int Y { set; } int Result { get; } }
比较明显的不足是A在内部嵌套了类型,假设我们根据接口设计了class B,里面却有个A.abArgs,让人难堪。通过抽取接口,一方面代码变得很直观,方便客户端使用。另一方面,我们可以实现新的类型。
以下是重新设计的A2,将运算放在获取结果的过程内:
class A2 : IA{ EventHandler<A.abArgs> mcalcEvent; event EventHandler<A.abArgs> IA.calcEvent{ add { mcalcEvent += value; } remove { mcalcEvent -= value; } } A.calcType mCalcFunc; A.calcType IA.CalcFunc{set { mCalcFunc = value; }} int mX; int IA.X{set { mX = value; Modify = true; }} int mY; int IA.Y { set { mY = value; Modify = true; }} bool Modify = false; int mResult; int IA.Result{get{ if (!Modify) return mResult; if (mCalcFunc == null) throw new A.CalcException("未指定运算函数。"); mResult = mCalcFunc(mX, mY); Modify = false; if (mcalcEvent != null) mcalcEvent(this, new A.abArgs(mX, mY)); return mResult; }} }
A2使用接口IA,并且是“显式实现”。A2是完全封闭的实现,自身没有公开成员,只能通过接口访问。对于使用IA的客户来说,可以无痛苦的接受新的A2,并能得到一点点性能改善(减少了运算的次数)。
2. 初始化和终结处理