在第一章中你已经看到了如何声明一个新类Helloworld。在第二章,你也也学习了C#内建的原类型。你也知道了控制流,还有如何声明方法。现在是时候讨论如何定义属于自己的类型了。这在任何C#程序都是核心结构。C#作为一个面向对象的语言是完全支持类和构建对象。
本章为你引入c#的面向对象编程方式。重点是如何定义类,这是对象的模板。
在前面的章节的程序结构一直都是面向对象的编程。不管怎样,用来包裹这些构造,你就能创建更大,更有组织的程序,并具有可维护性。从结构化,控制流程编程过渡到面向对象编程是革命性的变革。因为这提供了额外的组织形式。这带来的结果就是,小程序变的有点简单,但更重要的,这将能创建更大的程序,因为编程代码被更好的组织起来。
面向对象编程还有一个关键的特性是,避免从零编写一个新程序,你能收集从前工作中的对象,并扩展类的新特性,添加更多类,然后用新功能重组。
本章深入讨论C#是如何支持封装的。比如,类型,属性,还有访问修饰符。下一章在本章基础上,介绍继承和多态。
面向对象的编程
今天程序能成功的关键是,在越来越大的应用中用结构化的方式实施复杂的需求。面向对象的编程就提供了一套实现方法。预想将面向对象的程序转换成结构程序是很困难的。除了某些无意义简单的程序。
面向对象编程最基本的结构式类和对象。这是对现实世界的模块、模板概念的在编程中的抽象形式。比如,类型OpticalStorageMedia有一个方法Eject()用于从播放器中弹出CD/DVD。类OpticalStorageMedia就是对真实世界的程序化的抽象。
类依据三个面向对象的原则:封装,继承和多态。
封装
封装是将细节隐藏,当需要时就可以访问这些细节,对细节的合理封装,很容易让大程序理解。无意中修改被保护的数据,由于改变代码是在封装的边界内,代码就可以很容易保持。方法就是封装的例证。尽管可以将方法中的代码直接嵌入调用者代码中,不过,将代码重构编入方法提供了有益的封装。
继承
看看下面的例子,DVD是光学媒体的一种。它可以存储数字影片。CD是另一种有不同性质的光学媒体。它们两者的版权实现是不同的,并且存储容量也不同。不同的存储介质,都有特定的特性,不过都会有些基本功能,比如,支持文件系统,媒体文件是只读还是可写。
如果你为每个存储媒体定义一个类型。你就要定义一个类继承。这是一系列的is a关系。基础类是所有存储介质驱动器的源头。它可以是StorageMedia类。比如,CD,DVD,硬盘,软驱,等等都是StorageMedia类型。然而,CD和DVD并需要直接源自StorageMedia。而它们是源自一个中间类型,OpticalStorageMedia。你可以用UML来查看类继承图。
继承关系需要至少两个类,其中一个是更普遍的版本。在图5.1中,硬盘是更具体的版本,尽管HardDrive有许多特殊类型,它依然是StorageMedia。反之则说不通。StorageMedia类型不一定是hardDrive类型。图5.1中的继承关系中就包含了多个类。
特定类型是衍生类型或叫子类。而通用的类型叫基类或超类。在继承关系中类的另一种较常用的术语是父亲和子孙。前者是更通用的类。
从另一个类衍生或继承到特定的类型,是指自定义基类以便能面向特定用途。同理,基类是衍生类的通用实现。
继承的关键是,所有衍生类集成基类的成员。通常,基类的成员实现能够被修改。衍生类能包含基类的成员。
衍生类允许你以层级的关系组织你的类,孩子类会有比父亲类更多的特性。
多态
多态包含两个意思,一个是“多”另一个是“形式”。在对象的上下文中,多态的意思就是一个简单的方法或类型能有多种实现。如果你想有一个媒体播放器。它能播放音乐CD,DVD还有MP3.然而,这就需要依赖不同媒体类型精确实现Play()方法。在CD对象上调用Play()或在DVD播放,这都会播放音乐。所有的类型都知道其基类,OpticalStorageMedia,并都有各自的方法声明。多态是一个规范,类型能处理方法实现的细节,因为,在多个衍生类中都有方法出现,它们都共享基类中的同样的方法声明。
定义和类实例
定义一个类,首先用关键字class,后跟标示符。
清单5.1
————————————————————————
class Employee
{
}
————————————————————————
类中的代码都要包含在类声明之后的花扩号中。虽然这不是必须做的,但通常将类放入各自的文件中。这样就能很容易的找到特定类。习惯上,类名就是文件名。
一旦你定义了一个新类,你就能将它构建入framework中。换句话说,你能声明一个此类型的变量,或将这个新类作方法参数传递。
class Program
{
class Employee
{
//...
}
static void Main()
{
Employee employee1, employee2;
}
static void IncreaseSalary(Employee employee)
{
// ...
}
}
类声明下面的花括号划分出执行界限。
定义对象和类
在非正式的讨论中,对象和类是可以混用的。然而,对象和类却有着不同的意思。类是一个对象在运行期的模板。对象是一个类的实例。类就像一个模具,将会创建一个部件。对象就是模具对于的部件。从类创建对象的过程叫实例化,因为对象是类的实例。
你已经定义了一个新类,现在是实例化这个类型的时候了。C#使用new关键字类实例化一个对象
清单5.3
class Program
{
class Employee
{
//...
}
static void Main()
{
Employee employee1 = new Employee();
Employee employee2;
employee2 = new Employee();
IncreaseSalary(employee1);
}
static void IncreaseSalary(Employee employee)
{
// ...
}
}
不要吃惊,赋值能和声明语句在同一行,也可以分开操作。
不像原始类型,并没有直接指定Employee。代替的操作是,用new操作符来在运行时为Employee对象分配内存,实例化这个对象,并返回此实例的引用。
尽管分配内存是显式的,但并没有相应的回收内存的操作符。在程序结束以前,自动回收对象内存。垃圾回收器负责内存自动分配。由它确定没有被活动对象引用的对象,然后重新将内存分配给别的对象。内存会被系统重新分配,它并不是在编译时定位。
封装的第一季:用方法分组对象数据。
如果你收到用员工第一个名字做索引的卡片,用员工第二个名字做索引的卡片,还有一个用他们薪水做索引的卡片。这些卡片没什么价值,unless you knew that the cards were in order in each stack。所以,为了得到员工的全名就需要搜索两个卡片。
在非面向对象的语言上下文中,将一组项目装入一个封套中,同样面向对象编程将方法和数据一起装入对象。提供了一组类成员以便不再单独处理。
实例化字段
面向对象设计的一个关键目的就是将数据分组。本节讨论如何将数据加入类。在面向对象的术语中,类中存储数据的变量叫做成员变量,此术语仅对C#有效。更为精确的术语是字段,用内置类型为存储数据的单元命名。实例化字段是在类层次为对象要存储的数据声明一个变量。因此,联合就是字段类型和字段之间的关系。
声明一个实例字段
在清单5.4中,Employee已经被修改成包含三个字段的类:FirstName,LastName,Salary
清单5.4
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
}
对于增加的这些字段,employee类的实例可以存储一些基本数据。所以这些字段都用了public访问修饰符。字段上的public指示了除了employee以外别的类也可以访问这些字段。
和本地变量声明一样,一个字段的声明包含和字段相关的数据类型。此外,在声明期,会给字段分配一个初始值。
清单5.5
class Employee
{
public string FirstName;
public string LastName;
public string Salary = "Not enough";
}
访问一个实例字段
你可以设置或检索字段内的数据。然而,字段是不能包含static修饰符的。你能从对象上访问一个实例字段,而不能通过类直接访问。
实例方法
在类中提供了一个处理员工姓名格式化的方法,来替代main()方法中的WriteLIne()方法。这样做是符合类封装性的。
使用this关键字
你能获得类的实例成员引用的类。为了明确指示能访问的字段或方法。你需要使用this、this是一个简单内含字段,它返回对象本身。
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
public string GetName()
{
return FirstName + " " + LastName;
}
public void SetName(string newFirstName, string newLastName)
{
this.FirstName = newFirstName;
this.LastName = newLastName;
}
}
用代码格式避免歧义
在SetName()方法中,你不用使用this关键字,因为FirstName和newFirstName是显然不一样的。考虑另外一种情况,如果用FirstName代替newFirstName
清单5.10
class Employee
{
public string FirstName;
public string LastName;
public string Salary;
public string GetName()
{
return FirstName + " " + LastName;
}
// Caution: Parameter names use Pascal case
public void SetName(string FirstName, string LastName)
{
this.FirstName = FirstName;
this.LastName = LastName;
}
}
文件存储和载入
封装第二季:信息隐藏
除了将数据和方法一起封装入一个简单的单元。封装还可以隐藏一个对象的数据和行为的内部细节。在一定程度上,方法也是做这个事。方法声明是对所有调用者可见的。而内部实现是隐藏的。面向对象的编程也具有这个特性,然而,它提供了一个工具用来控制成员在类外部的现实范围。不将成员公开给类外部是私有成员。
在面向对象的编程中,封装不仅仅是分组数据和行为的术语,它还可以将数据隐藏在类内部以便最小限度的曝光类的访问。
访问修饰符的作用就是封装。使用public,你明确的指示了可以从Employee类的外部修改字段。换句话说,可以从Program类访问Employee类。
为了将Password字段隐藏,不让别的类触及到。你要使用private访问修饰符。这样在Program类中你就不能访问到Password字段了。
注意当没有为成员指定访问修饰符,默认的就是private。换句话说,成员默认是私有的。程序员需要明确指定成员为公有。
属性
在上一节,访问修饰符,演示了,如何使用private关键字封装一个密码,阻止从类的外部访问。这样的封装太彻底了。比如,有时你想定义一个字段,外部类只能读它,并不能改变它的值。还有,你可能要向类中写入某些数据,但你想验证数据。有太多的例子需要即时创建数据。
通常,编程语言会有这样的特性,将一个字段值为私有,然后为访问和修改数据分别提供getter和setter方法。
清单5.16
class Employee
{
private string FirstName;
// FirstName getter
public string GetFirstName()
{
return FirstName;
}
// FirstName setter
public void SetFirstName(string newFirstName)
{
if (newFirstName != null && newFirstName != "")
{
FirstName = newFirstName;
}
}
private string LastName;
// LastName getter
public string GetLastName()
{
return LastName;
}
// LastName setter
public void SetLastName(string newLastName)
{
if (newLastName != null && newLastName != "")
{
LastName = newLastName;
}
}
// ...
}
这一改变,会影响编程的方式,你不再需要使用赋值操作符设置数据。
声明属性
C#为这种模式提供了实现语法。这种语法叫做属性
自动实现属性
C#3.0中,属性语法中包含一个精简版本。
命名习惯
用属性校验
只读属性和只写属性
在Get和Set上的访问修饰符
属性作为虚字段
静态
静态字段
为了定义一个在多个实例中都可以访问到的数据,需要使用static关键字。
在这个例子中,NextId字段声明包含static修饰符,所以这就叫做静态字段。而Id就是一个单纯存储位置。NextId在Employee类的实例中共享数据。