深入C#(一) winboy20(翻译)
关键字 C#,面向对象
出处 http://mspress.microsoft.com/
第一部分 打下基础
第一章 面向对象编程基础
1.1面向对象编程基础
本章的目的是引导你通过面向对象编程这个术语,理解面向对象的重要性。许多高级语言,如c++和微软的Visual Basic,声称“支持对象”,但仅有少数语言支持所有构成面向对象编程的原则。C#就是这样一种语言:它是一种真正面向对象、基于组件的语言。因此,要从这本书获得尽可能多的知识,你需要很好的把握其中的概念。我知道这样的概念章节通常被读者略过,他们希望马上编码,但除非你认为自己是“技术天才”,我建议你阅读这一章。那些对面向对象编程仅略有所知的人,你会在这一章获得许多。同时,记住以后的章节都会用到本章讨论的术语和概念。
如我所言,许多语言都声称是面向对象或基于对象,但很少真正是这样。C++则不同,因为不可否认,它是深刻移植于C语言。太多的OOP想法必须牺牲C++来满足一些C的遗传属性。甚至连JAVA语言,尽管它很好,但作为面向对象的语言仍然有很多限制。举例来说,我提到在JAVA中简单类型和对象类型是不同处理和动作的。不过,本章的焦点不是比较不同语言对OOP原则的正确性。本章宁愿提供一个客观和与语言无关的解释OOP原则的教程。
在我们开始之前,我得说面向对象编程决不是一个市场推销短语(尽管对某些人来说是这样)、一个新语法、或一个新的应用编程界面(API)。面向对象编程是一系列崭新的概念和想法。它是一种用计算机语言思考问题的方式,是用一种更加直觉的方法来拥抱问题、因而也更加有生产力的一种方式。
我的第一个工作是用Pascal语言去编写一个《冰天假日》票房收入的记录和应用的程序。当我转移到其他工作和应用时,我用PL/I 和 RPG III (及 RPG/400)。
一些年以后,我开始用C语言编程。在这些实例中,我很容易应用我从以前的经验学到的知识。对我来说一个成功的语言的学习曲线是更短的,尽管语言的复杂性,因为直到我用C++编程时,其他所有我用过的语言仅仅是语法上的不同。
然而,如果你刚接触面向对象编程,我先警告你:以前用非面向对象语言编程的经验在这没有用!面向对象编程是一种用来设计和编写问题的解决方案的完全不同的思维方式。事实上,研究表明编程新手比我们这些原来学过过程性语言如BASIC, COBOL和 C的人更快地学习面向对象的语言。
这些人不需要“忘却”任何过程性编程的习惯,而这些会阻碍他们对OOP的理解。他们以一个干净的石板开始。如果你用过程性语言编程很多年了,C#是你的第一个面向对象语言,我能给你最好的建议就是保持一个开放的头脑,在你摊开你的双手说:”我可以欺骗[插入你的过程性的语言]”之前,执行我所说的建议.任何一个有过程性编程背景的人转而学面向对象编程都会经历这个学习曲线,这个曲线很有用.用面向对象语言编程的收益是无法计算的,不管是更高效地编写代码,还是建立一个在第一次编写好后更易修改和扩展的系统.可能开始看上去并不是这样。然而,经历了20多年发展的软件(包括使用面向对象语言的8年)显示了:当正确应用是,OOP概念,确实如它所许诺那样起作用。不需多少功夫,我们就很容易发现到底哪里出来麻烦。
1.2诸物皆为对象
对面向对象语言来说,所有的问题实体都通过对象的概念来表达。(注意在这本书中我将用Coad/Yourdon定义“问题域”――即一个问题域是你尝试解决的问题,不管它的特殊的复杂性、术语、挑战及其他等等)你也许会猜测,对象是面向对象编程的中心思想。我们中的大部分并没有用结构、数据包、函数调用和指针来思考问题,而是通过对象来思考。让我们看一个例子。
如果你在编写一个发票管理应用软件,需要记录发票的详细行数,从客户的观点来看哪个方法直觉性更强呢?
l 非面向对象方法 我将采用一个数据结构来代表发票头。发票的头结构将包括发票详细结构的双链表,每个都包含一个总的线数。于是,为了得到发票的总数,我需要定义一个命名为totalInvoiceAmount的变量,并初始赋值为0。获得一个指针来指向发票的头结构,获得链接表详细数目的头文件,然后贯穿详细数目的链接表。当我读每个详细线性结构时,我将得到他的成员变量—包括totalInvoiceAmount的值和增量。
l 基于对象的方法 我有一个发票对象,我发送了一个消息给这个对象来要求整个数量。我无需知道该信息在对象内部是怎样储存的,但当我处理非基于对象的数据结构时则必须如此。我简单地以自然的方式处理该对象,通过发送消息来提出要求。(对象能够发送的消息组被称为对象接口。在下面的段落里,我将解释为什么用接口思考而不是用实现,如我所作,在基于对象的方法中是公正的)
显然,基于对象的方法更加直觉,更加接近我们思考问题的方式。在第二种解决方式中,发票对象可能与一系列具体对象相关,发送消息给每个对象需要很多行。然而,如果你把它作为一个整体来看待的话,就不必在意它是怎么实现的,因为面向对象编程的一个基本特性就是封装性—-对象隐藏内部数据和方法的能力及提供每个部件的接口,从而是组件编程成为可能。只要对象能够执行一项任务,那么执行的内幕就不重要。你仅需给对象提供一个接口,然后使用这个接口让对象为你执行一个给定任务。(我稍后将在本章解释封装和接口的概念。)要点是我们用来模仿现实世界的具体问题的程序将更加容易设计和编写,因为它使我们用更加自然的方式来思考问题。
注意,第二种方法要求一个代表你完成工作的对象,即综合详细代码。对象不仅仅象结构那样含有数据。对象的定义是,包含数据和操作数据的方法。这意味着当考虑一个问题域时,我们可以设计比必要的数据结构更多的东西。我们也能够知道哪种方法能够更好的与给定的对象结合,使对象能够按照功能进行很好的封装。接下来及后续部分的例子将阐明这个概念
注意:
本章的代码摘录展现了面向对象编程的概念。请务必牢记在心:我在这里用的虽然是C#的代码,但他们的概念却是属于OOP(面向对象编程),对其他任何一种编程语言不一定有效。出于比较的目的,本章我还用了一些C的例子,它不是基于对象的。
让我们假设你正在编写一个程序,来计算你们新公司仅有的雇员,Amy的工资。如果使用C语言的话,你可能会用到跟以下代码相似的东西,来将某一特定数据与雇员联系起来:
struct EMPLOYEE
{
char szFirstName[25];
char szLastName[25];
int iAge;
double dPayRate;
};
下面是你如何通过EMPLOYEE这个结构来计算Amy的工资:
void main()
{
double dTotalPay;
struct EMPLOYEE* pEmp;
pEmp = (struct EMPLOYEE*)malloc(sizeof(struct EMPLOYEE));
if (pEmp)
{
pEmp->dPayRate = 100;
strcpy(pEmp->szFirstName, "Amy");
strcpy(pEmp->szLastName, "Anderson");
pEmp->iAge = 28;
dTotalPay = pEmp->dPayRate * 40;
printf("Total Payment for %s %s is %0.2f",
pEmp->szFirstName, pEmp->szLastName, dTotalPay);
}
free(pEmp);
}
在本例中,代码是基于一个结构中的数据及一些使用这个结构的外部代码(相对于这个结构)。因此会有什么问题呢?主要问题是提取中的一个:EMPLOYEE结构的使用者必须知道对一个雇员太多的数据。为什么?假设一段时间后你想改变Amy工资比率的计算方式,举例来说,决定新的净收入时,你需要考虑FICA(美国联邦社会保险捐款法)和其他混合税率。那么,你不仅要更改所有使用EMPLOYEE结构的客户代码,而且你还需要写出变更文档—为你的公司将来的程序员—事实是用法的改变发生了。
现在让我们看看C#的例子
using System;
class Employee
{
public Employee(string firstName, string lastName,
int age, double payRate)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.payRate = payRate;
}
protected string firstName;
protected string lastName;
protected int age;
protected double payRate;
public double CalculatePay(int hoursWorked)
{
// Calculate pay here.
return (payRate * (double)hoursWorked);
}
}
class EmployeeApp
{
public static void Main()
{
Employee emp = new Employee ("Amy", "Anderson", 28, 100);
Console.WriteLine("\nAmy's pay is $" + emp.CalculatePay(40));
}
}
在C#的雇员应用例子中,用户只要简单地调用对象的CalculatePay方法,让其计算自己的工资。这种方法的好处就是用户不必担心对象内部工资是怎样计算的。如果将来的某个时候你决定改变工资计算的方法,改变将对现有的代码没有任何影响。这种级别的提取是使用对象的一个基本的好处。
现在,可以下有依据的评论可能是:我可以通过创建一个访问EMPLOYEE结构的函数来提取客户端的C代码。然而,事实是我必须创建一个完全跟以前的机构分离的函数,这实在是一个头痛的问题。当你使用面向对象的语言,如C#时,对象的数据和操作数据的方法(即接口)通常是一体的。 2002-1-13
牢记在心只有相应的对象方法才能修改它的对象变量。如先前看到的例子,每个Employee成员变量都被定义成受保护地访问(protected),除了其中的CalculatePay方法,它被定义为public(公共的)。访问限制被用来定义访问的权利,决定某一代码必须有一给定的类成员。在受保护protected的情况下,源类能够访问其成员,但客户端不能。公有修饰符public使源类和客户代码都能都能够访问该成员。我将会在第五章“类”中更详细地介绍访问修饰符,但是现在要记住的首要事情是修饰符使你能够保护关键的类成员不被错误使用。
对象 vs 类
对象与类的不同是许多刚接触面向对象编程这个术语的编程者迷惑的问题。为了描述两者之间的不同之处,我们还是举EmployeeApp这个例子,这次不是一个雇员,而是整个公司的雇员。
使用C语言我们可以定义一个雇员数组—基于雇员EMPLOYEE结构—从此开始。因为我们不知道公司一天要聘用多少雇员,我们可以用一个静态的数字来创建这个数组,比如说10,000。然而,假设你公司现在仅有Amy一个雇员,这就不是最高效地利用资源的方式。相反地,在该应用中,我们可以定义一个EMPLOYEE结构的链表,按需动态分配内存。
我的观点是我们精确地做了我们不该做的事情。我们把自己的精力花费在考虑语言和机器上—即有多少内存可以分配,何时分配—而不是去考虑根本问题。使用对象,我们可以集中精力在问题的逻辑而不是解决问题的机器上。
有很多种方法定义类,将它与对象区别开。你可以将类考虑为有方法联系的简单的一个数据类型(如字符型,整型,长整型)。而对象是类型或类的一个实例。然而,我最喜爱的定义是:类是对象的一个蓝图。作为一个开发者,你就像工程师创建房屋的蓝图一样创建程序的蓝图。一旦蓝图完成了,你仅仅拥有任一给定类型房屋的蓝图。然而,任何人都能购买这个蓝图,建立相同的房屋。与此类似,类是一个给定功能函数集合的蓝图,建立在特定类上的对象拥有所有该类所拥有的功能。
实例化
作为面向对象编程的一个独特条款,实例化就是简单地创建类的实例,这个实例就是对象。在下面的例子里,我们做的就是为对象创建或说明一个类。换言之,这时并没有分配内存因为我们仅仅拥有对象的蓝图,而不是对象本身。
class Employee
{
public Employee(string firstName, string lastName,
int age, double payRate)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.payRate = payRate;
}
protected string firstName;
protected string lastName;
protected int age;
protected double payRate;
public double CalculatePay(int hoursWorked)
{
// Calculate pay here.
return (payRate * (double)hoursWorked);
}
}
为了实例化这个类并使用它,我们必须用类似于以下的方法来定义这个实例:
public static void Main()
{
Employee emp = new Employee ("Amy", "Anderson", 28, 100);
}
在这个例子中,emp被定义为Employee类型,通过new运算符实例化。Emp变量代表Employee类的实例,被认为是一个Employee对象。实例化后,我们能够通过其公有成员与该对象联系。举例来说,我们可以调用emp对象的CalculatePay方法。如果我们没有一个实际的对象,我们就不能这么做。(有一个例外,就是当我们处理静态成员时。我将会在第五章和第六章“方法”讨论静态成员。)
看看下面的C#代码:
public static void Main()
{
Employee emp = new Employee();
Employee emp2 = new Employee();
}
这儿有两个实例-- emp 和emp2—来自于相同的Employee类。当每个对象有相同的性能时,每个实例里将包含它自有的实例数据,而且能够分开处理。与此相似,我们能够创建一整个数组或Employee对象集。第七章,“属性,数组和索引”将详细介绍数组。不过,我这里想陈述的观点是:大部分面向对象语言都支持定义对象数组的能力。这反过来让你方便地组合对象,通过调用对象数组的方法或编写子程序。将这与你必须处理链表的工作相比方便的多--链表需要你考虑将每个项目都与前后的项目结合。