面向对象的程序具有以下几个特点:
(1)所有的函数都放入到一个类中,成为某个类的成员,类是编程的基本单元。
(2)外界不能直接调用类的成员函数,必须先创建一个对象,再通过对象来调用这些函数。
(3)只有声明为public的函数可以被外界调用,其余声明为private的函数是私有的,外界无法访问。
对于小实例而言,面向对象程序和结构化程序相比没有明显的优越性,而且显得更麻烦,但如果是大规模的软件系统,则面向对象程序就有着结构化程序不可比拟的优势。
计算两个日期间一共有多少天数:
(1)采用结构化编程,自己动手写几个函数,计算出最后天数
(2)面向对象编程。其实.NET Framework本身就提供了两个类DataTime和TimeSpan可完成同样的功能。
DateTime dd1, dd2;
dd1 = new DateTime(2006, 3, 8);
dd2 = new DateTime(1999, 5, 10);
//两个日期对象相减,得到一个TimeSpan对象, Days是这一TimeSpan对象的属性
int ddays = (dd1 - dd2).Days;
Console.WriteLine(ddays);//结果:
对比一下,显示使用.NET Framework提供的现成类比我们手工编写代码完成同样的工作开发效率要高得多。.NET Framework中所提供更多现成代码都是以面向对象的形式封装的。实践证明,当需要大规模的复用代码以提高生产率时,面向对象比结构和技术更有效。
面向对象拥有下面四个基本特性:
(1)封装
(2)抽象
(3)继承
真实世界中,事物之间有着一种派生关系,如下图:
在计算机世界中,以面向对象的观点不仅将上述事物抽象为类,而且将事物之间的派生关系也一并模拟出来,这种关系成为继承:
在面向对象软件系统中,继承除了模拟现实世界中事物关系这一作用,还是一个“重用已有代码而又不破坏已有代码”的方法。
现在要开发一个 B 项目,架构设计师发现以前完成的 A 项目中有部分类完全可以在 B项目中重用,但需要增强这些类的功能以便适用于 B 项目。如果从 A 项目中直接抽取这些类的源代码并加以修改,虽然可以满足 B 项目的需要,但现在却需要维护两套功能类似的类代码,加大了管理的成本。在这种情况下,选择从 A 项目的类中用继承的方法派生出新类用在 B 项目中是一个可选的方案,既满足了新项目的需要,又避免了大量的重复代码与双倍的代码维护成本。
(4)多态
用“给张三一个水果”来指代“给张三一个苹果”和“给张三一串香蕉”,虽然在语义上比较“模糊”,但其适用性更广了,除了“苹果”和“香蕉”,以后还可以是“梨子”、“西瓜”、“葡萄”……,都可以用“给张三一个水果”来代表。这种用一个比较抽象的事物来取代具体的事物的表达方法, 在面向对象软件中用“多态”这一特性来模拟。
在编程时应用多态的方法,可以用一句话来表示:用抽象的类编程。(这里,我们将接口也看成是一种特殊的抽象类,抽象类中可以有非抽象方法,而接口中所有方法都是抽象方法)
即在代码中本应使用某一具体子类的地方使用较为抽象的基类对象, 这种方法所带来的好处是多态的代码具有“变色龙”的特性,即在不同的条件下,同样代码可以完成不同的功能。
适当地在开发中应用多态特性,可以开发出可扩充性很强的系统。
与使用C语言等结构和编程语言不一样,使用C#编程,所有的代码几乎都放在类中,不存在独立于类之外的函数。因此,类是面向对象编程的基本单元。
字段即变量,方法即函数。
类的字段一般代表类中被处理的数据,类的方法大多代表对这些数据的处理过程或用于实现某种特定的功能,方法中的代码往往需要访问字段保存的数据。
在C#中,定义若干个变量,写若干个函数,将这些代码按以下格式汇集起来,再起个有意义的名字,就完成了一个类的定义:
[public|private] class 类名
{
[public|private] 数据类型 变量名;
[public|private] 数据类型 函数名(参数列表)
{
}
}
两个构成重载关系的函数必须满足以下条件:
(1)函数名相同
(2)参数类型不同,或参数个数不同
注意:函数返回值类型的不同不是判断函数重载的条件
类的构造函数
当使用 new 关键字创建一个对象时,一个特殊的函数被自动调用,这就是类的构造函数。
在 C#中,类的构造函数与类名相同,没有返回值。
class A
{
//类A的构造函数
public A()
{
}
}
类的构造函数在以类为模板创建对象时被自动调用。构造函数一般用于初始化类的私有数据字段。
引用类型与值类型
值类型的变量一定义之后就马上可用。
引用类型的变量定义之后,还必须用 new 关键字创建对象后才可以使用
所有值类型的变量都是在线程堆栈中分配的。
所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。
类库
为了提高软件开发的效率,人们在整个软件开发过程中大量应用了软件工程的模块化原则,将可以在多个项目中使用的代码封装为可重用的软件模块,这些可复用的软件模块,再开发新项目就成为“重用已有模块,再开发部分新模块,最后将新旧模块组装起来”的过程。
在组件化开发大行其道的今天,人们通常将可以重用的软件模块称为“软件组件”。
在全面向对象的.NET软件平台之上,软件组件的表现形式为“程序集(Assembly)”,可以通过在visual studio中创建并编译一个类库项目得到一个程序集。
类库项目编译之后,会生成一个动态链接库(DLL)文件,这就是可以被重用的.NET软件组件——程序集。默认情况下,类库文件名就是项目名加上.dll后缀。
每个类库项目都拥有一个默认的命名空间,可以通过类库项目的属性窗口来指定。
需要仔细区分“类库项目”、“程序集”和“命名空间”这三个概念的区别:
(1)每个类库项目编译之后,将会生成一个程序集。
(2)类库项目中可以拥有多个类,这些类可属于不同的命名空间。
(3)不同的类库项目可以定义相同的命名空间。
根据上述三个特性,可以得到以下结论:
**“命名空间”是一个逻辑上的概念,它的物理载体是“程序集”,具体体现为“DLL”(或EXE)文件。在 Visual Studio 中,可通过创建“类库”类型的项目生成程序集。
一个程序集可以有多个命名空间,而一个命名空间也可以分布于多个程序集。**
一旦生成了一个程序集, 在其他项目中就可以通过添加对这一程序集的引用而使用此程序集中的类。其方法是在“项目”菜单中选择“添加程序集”命令,激活“浏览”卡片,选择一个现有的程序集文件(DLL 或 EXE)。
一个项目添加完对特定程序集的引用之后,就可以直接创建此程序集中的类了,当然要注意指明其命名空间。
方法重载、隐藏和虚方法调用
总的来说,子类方法与父类方法之间的关系可以概括为以下三种。
1、扩充(Extend): 子类方法,父类没有;
2、重载(Overload):子类有父类的同名函数,但参数类型或数目不一样;
3、完全相同:子类方法与父类方法从方法名称到参数类型完全一样。
当子类与父类拥有完全一样的方法时,称“子类隐藏了父类的同名方法”,请看示例项目 HideExamples:
class Parent
{
public void HideF()
{
System.Console.WriteLine("Parent.HideF()");
}
}
class Child : Parent
{
public void HideF()
{
System.Console.WriteLine("Child. HideF()");
}
}
请注意现在子类和父类都拥有了一个完全相同的方法 HideF(),于是问题发生了,请看
以下代码:
Child c = new Child();
c.HideF();//调用父类的还是子类的同名方法?
上述代码运行时,输出:
Child.HideF()
修改一下代码:
Parent p = new Parent();
p.HideF();//调用父类的还是子类的同名方法?
上述代码运行结果:
Parent.HideF()
由此可以得出一个结论:
当分别位于父类和子类的两个方法完全一样时,调用哪个方法由对象变量的类型决定。
然而,面向对象的继承特性允许子类对象被当成父类对象使用,这使问题复杂化了,请读者看以下代码,想想会出现什么结果?
Child c = new Child();
Parent p;
p = c;33
p.HideF();//调用父类的还是子类的同名方法?
上述代码的运行结果是:
Parent.HideF()
这就意味着即使 Parent 变量 p 中实际引用的是 Child 类型的对象,通过 p 调用的方法还是 Parent 类的!
如果确实希望调用的子类的方法,应这样使用:
((Child)p).HideF();
即先进行强制类型转换。
回到前面 Parent 和 Child 类的定义, Visual Studio 在编译这两个类时,会发出一个警告:
警告 1 “HideExamples.Child.HideF()”隐藏了继承的成员
“HideExamples.Parent.HideF()”。如果是有意隐藏,请使用关键字 new。
虽然上述警告并不影响程序运行结果,却告诉我们代码不符合 C#的语法规范,修改
Child 类的定义如下:
class Child : Parent
{
public new void HideF()
{
System.Console.WriteLine("Child.HideF()");
}
}
“new”关键字明确告诉 C#编译器,子类隐藏父类的同名方法,提供自己的新版本。
由于子类隐藏了父类的同名方法, 所以如果要在子类方法的实现代码中调用父类被隐藏的同名方法,请使用 base 关键字,示例代码如下:
base.HideF(); //调用父类被隐藏的方法
重写与虚方法调用
上述隐藏的示例中,由于子类隐藏了父类的同名方法,如果不进行强制转换,就无法通过父类变量直接调用子类的同名方法,哪怕父类变量引用的是子类对象。
这是不太合理的。我们希望每个对象都只干自己职责之内的事,即如果父类变量引用
的是子类对象,则调用的就是子类定义的方法,而如果父类变量引用的就是父类对象,则调用的是父类定义的方法。这就是说,希望每个对象都“各人自扫门前雪,莫管他人瓦上霜”。
为达到这个目的,可以在父类同名方法前加关键字 virtual,表明这是一个虚方法,子类可以重写此方法: 即在子类同名方法前加关键字 override, 表明对父类同名方法进行了重写。
请看示例代码(示例项目 VirtualExamples):
class Parent
{
public virtual void OverrideF()
{
System.Console.WriteLine("Parent.OverrideF()");
}
}
class Child : Parent
{
public override void OverrideF()
{
System.Console.WriteLine("Child.OverrideF()");
}
}
请看以下使用代码:
Child c = new Child();
Parent p;
p = c;
p.OverrideF();//调用父类的还是子类的同名方法?
上述代码的运行结果是:
Child.OverrideF()
这一示例表明,将父类方法定义为虚方法,子类重写同名方法之后,通过父类变量调用此方法,到底是调用父类还是子类的,由父类变量引用的真实对象类型决定,而与父类变量无关!
换句话说,同样一句代码:
p.OverrideF();
在 p 引用不同对象时,其运行的结果可能完全不一样!因此,如果我们在编程时只针对父类变量提供的对外接口编程,就使我们的代码成了“变色龙”,传给它不同的子类对象(这些子类对象都重写了父类的同名方法),它就干不同的事。
这就是面向对象语言的“虚方法调用(Virtual Method Invoke)”特性。
很明显,“虚方法调用”特性可以让我们写出非常灵活的代码,大大减少由于系统功能扩充和改变所带来的大量代码修改工作量。
由此给出以下结论:
面向对象语言拥有的“虚方法调用”特性,使我们可以只用同样的一个语句,在运行
时根据对象类型而执行不同的操作。
接口
来看以下这句话:
鸭子是一种鸟,会游泳,同时又是一种食物。
如何在面向对象的程序中表达这种关系?
如果使用 C++,可以设计成让鸭子(Duck)类继承自两个父类(鸟 Bird 和食物 Food)。
但在 C# 中所有的类都只能有一个父类,此方法不可行。
为了解决这一问题, C#引入了接口(interface)这一概念,并规定“一个类可以实现多个接口”。
显式实现接口
上面讲到,某个类可以实现多个接口,当创建一个此类的对象之后,通过引用这个对象的对象变量可以访问其所有的公有方法(包括自身的公有方法以及由接口定义的公有方法以)。在这种情况下,根本分不清哪些方法是由接口定义的,哪些是由类自己定义的。 C#提供了一种“显式接口”实现机制,可以区分开这两种情况,一个示例代码如下:
interface IMyInterface
{
void func();
}
public class A:IMyInterface
{
void IMyInterface.func()
{
//……
}
public void func2()
{
//……
}
}
请注意在方法 func 前以粗体突出显示的接口名称,这就是 C#对接口 IMyInterface 的显式实现方式。
当类 A 显式实现接口 IMyInterface 之后,只能以下面这种方式访问接口定义的方法:
IMyInterface a = new A();
a.func();
以下代码将不能通过编译:
A a = new A();
a.func();40
由此得到一个结论:
如果一个类显式实现某个接口,则只能以此接口类型的变量为媒介调用此接口所定义
的方法,而不允许通过类的对象变量直接调用。
或者这样说:
被显式实现的接口方法只能通过接口实例访问,而不能通过类实例直接访问。