资源描述
第3章 面向对象设计基础
第二章介绍了C#的基本语法,以及使用方法。C#同Java、C++一样是面向对象的编程语言,同时C#更强化了面向对象的概念。本章将介绍面向对象的基础知识并介绍使用C#编写面向对象的应用程序,在C#中,面向对象的开发能够给系统设计、编码、维护提供更多的便利。
3.1 什么是面向对象
面向对象是应用程序开发中一个非常重要的技巧和概念,面向对象并不是什么高深的技术也不是负责的学习体系,面向对象主要是一种设计的思路。使用面向对象进行应用程序开发能够非常好的将现实中的物体进行抽象,这样就在一定程度上丰富了应用程序的结构,不仅如此,面向对象还包括继承、多态等特性以便能够快速构架应用程序。
3.1.1 传统的面向过程
在传统应用程序开发领域(如C语言的开发),或者是早期的基于B/S领域的Web应用程序开发(如ASP)都使用的是传统的面向过程的开发语言。而C++/JAVA/C#等开发都是使用的面向对象的开发语言。面向对象的开发语言更接近人类理解自然的语言,对开发人员来说更加通俗易懂,同时也对“对象”进行了较好的抽象。面向过程的一段C语言代码如下所示。
main()
{
sum(x,y);
get(x,y);
print(x);
print(y);
}
上述代码截取了C语言中的一个代码段,从上述代码中可以看出,C语言中基本没有对象的概念。当执行一个主方法Main时,按照程序逻辑调用不同的函数,来达到运算的目的。传统的面向过程的开发必须规定每一个步骤,或者明确每一种函数,并在程序运行中调用不同的函数来实现运算的目的。面向过程的思想决定了其没有派生、覆盖、继承等特性,所以每当创建一个新的“对象”时,就有可能需要编写更多的代码,这在一定程度上造成了代码的编写和维护的困难。
3.1.2 面向对象的概念
在面向过程的开发当中,开发人员发现在调用函数的时候,很难分清楚函数本身是属于哪个文件的,在代码的阅读上面,不同的开发人员会发现很难读懂其他的开发人员的代码。虽然注释和良好的命名都是必要的,但是还是给开发人员之间的交互造成了极大的困扰。为了解决这个问题,于是有了面向对象的概念。面向对象的一段C#代码如下所示。
class Program //主程序类
{
public int sum(int x, int y) //sum方法
{
return x + y; //返回值
}
static void Main(string[] args) //静态方法
{
int x = 1, y = 2; //声明整型变量
Program sum = new Program(); //创建对象
Console.WriteLine(sum.sum(x, y)); //输出方法返回值
}
}
上述代码中,sum是一个Program对象,sum有一个方法sum进行加法运算。读者可能会疑惑,相对于面向过程,面向对象的代码量好像变多了,而且执行的方法过程并没有太大的区别。其实面向对象解决了代码难以划分结构、代码可读性不高的问题,让程序变得更加容易组织和阅读。例如在.NET中的Convert.ToInt32静态方法,当阅读到此代码的时候,开发人员能够比较清楚的知道这个方法做了什么操。而在调用面向过程中的函数的时候,如果函数的名称是CTi32,而又分不清该函数在上下文中起到的作用时,会比较难以理解这个方法究竟做了什么。
3.1.3 面向组件的概念
面向组件其实是面向对象的另一种加强。在面向对象中,常常需要引用命名空间或者引用头文件(如C++)来说明一个函数所在的对象与另一个同样名称的函数所在的对象是不同的。在传统的应用程序开发过程中,当客户更改了若干需求,往往只需要修改一个很小的功能,就要改动很多的代码,因此就出现了软件危机。
于是人们使用了分层的概念,将代码封装在一个类,然后对类进行组织协调,通过编译器对类或类库进行编译,形成DLL组件。在应用程序中使用DLL,从而提高了代码的重用性。分层的概念也是设计模式的开端。面向组件的概念在平时就已经被读者广泛使用了,例如.NET中的某些类库,还有COM组件等。面向组件的概念对开发人员在设计的思想上要求更高,这些要求不仅仅局限于编码。
3.2 面向对象的C#实现
C#是面向对象的编程语言。在面向对象开发当中,不可避免的要创建一个类,创建类后还需要创建该类的属性和方法来描述对象,然后再创建这个类的对象进行实例化。创建后的对象能够通过类中的属性和方法完成相应的操作。
3.2.1 定义
什么是对象?世间万物皆对象,在生活中,可能是一只猫、一只狗,或者是饼干、一张订单、银行卡等等都是对象。对象描述了一个物体的特征,抽象一个实际的物体或一个方法。
类定义了对象的特征,对对象进行了描述。这些特征包括对象的属性、对象的方法、对象的权限,以及如何访问该对象。在生活中就有很多例子,例如人类属于世界上的动物,并属于哺乳动物,同样,猫、狗、鸟也属于哺乳动物。所以,哺乳动物能够描述人类、猫、狗、鸟的一些基本特性。但是,人类会说话;猫会爬树;鸟会飞,不同的动物之间,实现的功能又不尽相同。所以这些对象之间也是有区别的。
程序开发人员能够为哺乳动物定义基本的相同的特性,如重量、体色、有没有毛之类,同样也能定义哺乳动物是否能够行走和飞行或者进行其他的操作。当然,如果定义了人类能够飞行,这是非常不符合逻辑的,类的设计同时也是需要符合逻辑。
注意:虽然在类设计中,能够自行设计相应的类和方法,以及属性,例如人类能够飞行,但是这个类的设计是不复合逻辑的,也是没有必要的。所以在类设计中,只要尽量完整的描述与现实中相同的属性即可。
3.2.2 创建一个类和其方法
上一节中了解了什么是面向对象,初学者可能还是对什么是面向对象、什么是对象、为什么要使用面向对象等概念很模糊。这里可以通过创建一个类可以深入了解面向对象的概念。在Visual Studio 2008 中,系统提供了类的创建向导,如图3-1所示。
图3-1 创建一个类
创建一个类的步骤如下所示。
(1)在Visual Studio 2008中打开一个项目。
(2)右击该项目,在下拉菜单中选择【添加】选项,在【添加】下拉菜单中选择【新建项】选项。
(3)在弹出对话框中选择【类】选项并,如图3-1所示,并给类一个名称。
(3)单击【添加】按钮,类就添加到项目中了。
技巧:也可以在导航菜单栏中的【文件】菜单栏中选择【新建文件】创建一个类文件。
在创建了类文件之后,就能够编写代码创建类描述对象,为了能够描述哺乳动物,这里创建一个Animal类,示例代码如下所示。
using System;
using System.Collections.Generic;
using System.Linq; //声明命名空间
using System.Text; //不同的命名空间
namespace MyClass //声明命名空间
{
class Animal //定义类
{
}
}
使用C#创建一个类,命名空间会包含在类文件中。默认命名空间通常与创建的项目名称相同,示例代码如下所示。
namespace MyClass //当前程序的命名空间
类被创建之后,可以向类中添加方法、属性等,以便更加清晰的描述该对象,示例代码如下所示。
namespace MyClass
{
public class Animal //创建对象
{
string color; //对象包含的字段
public string get() //对象的方法
{
return color; //执行的方法
}
}
}
在主函数中,可以调用Animal类并创建该类的对象并执行相应的方法,这样就能够很好的描述一个对象并执行相应对象的动作,示例代码如下所示。
static void Main(string[] args)
{
Animal an = new Animal(); //创建对象
an.get(); //执行方法
}
上述代码首先创建了一个Animal对象an,在创建对象后执行了对象的get方法进行该对象的颜色的获取,从而得到了一个动物an的颜色。
3.2.3 类成员
在C#中,类包含若干个组成成员,这些组成成员包括字段、属性、方法、事件等,这些组成成员能够彼此协调用于对象的深入描述。
1.字段
“字段”是包含在类中的对象的值,字段使类可以封装数据,字段的存储可以满足类设计中所需要描述。例如上一节中Animal类中的字段color,就是用来描述动物的颜色。当然,Animal的特性不只颜色,可以声明多个字段描述Animal类的对象,示例代码如下所示。
class Animal
{
public string color; //声明颜色字段
public bool haveFeather; //声明是否含有羽毛字段
public int age; //年龄字段
}
上述代码中,对Animal类声明了另外两个字段,用来描述是否有羽毛和年龄。当需要访问该类的字段的时候,需要声明对象,并使用点“.”操作符实现,Visual Studio 2008中对“.”操作符有智能提示功能,示例代码如下所示。
Animal bird = new Animal(); //创建对象
bird.haveFeather = true; //鸟有羽毛
bird.color = "black"; //这是一只黑色的鸟
2.属性
C#中,属性是类中可以像类的字段一样访问的方法。属性可以为字段提供保护,避免字段在用户创建的对象不知情的情况下下被更改。属性机制非常灵活,提供了读取、编写或计算私有字段的值,可以像公共数据成员一样使用属性。
在C#中,它们被称为“访问器”,为C#应用程序中类的成员的访问提供安全性保障。当一个字段的权限为私有(private)时,不能通过对象的“.”操作来访问,但是可以通过“访问器”来访问,示例代码如下所示。
public class Animal
{
private int _age; //定义私有变量
public int Age { get { return _age; } set { _age = value; } } //赋值属性
}
上述代码中为Animal类声明了一个属性Age,在主程序中,同样可以通过“.”操作符来访问属性,示例代码如下所示。
Animal bird = new Animal(); //创建对象
bird.Age = 1; //Age访问了_age
在Visual Studio 2008中,属性的声明被简化,不再需要冗长的声明,示例代码如下所示。
public class Animal //创建类
{
public int Age { get; set; } //简便的属性编写
}
注意:虽然在VS2008中,简化了代码,但是实现的过程依旧没有改变。
3.方法
方法用来执行类的操作,方法是一段小的代码块。在C#中,方法接收输入的数据参数,并通过参数执行函数体,返回所需的函数值,方法的语法如下所示。
私有级别 返回类型 方法名称(参数1,参数2)
{
方法代码块。
}
方法在类中声明。对方法的声明,需要指定访问级别、返回值、方法名称以及任何必要的参数。参数在方法名称后的括号中,多个参数用逗号分割,空括号表示无参数,示例代码如下所示。
public string output() //一个无参数传递的方法
{
return "没有任何参数"; //返回字符串值
}
public string out_put(string output) //一个有参数传递的方法
{
return output; //返回参数的值
}
上述代码中,创建了两个方法,一个是无参数传递方法output和一个参数传递的方法out_put,在主函数中可以调用该方法,调用代码如下所示。
Animal bird = new Animal(); //创建对象
bird.out_put(); //使用无参数的方法
string str = "我是一只鸟"; //创建字符串用于参数传递
bird.out_put(str); //使用有参数的方法
如上述代码所示,主函数调用了一个方法out_put,并传递了参数“我是一只鸟”。在使用类中的方法前,将“我是一只鸟”赋值给变量str,传递给out_put函数。在上述代码中,“我是一只鸟”或者str都可以作为参数。
在应用程序开发中,方法和方法之间也可以互相传递参数,一个方法可以作为另一个方法的参数,方法的参数还可以作为另一个方法的返回值,示例代码如下所示。
public string output() //一个无参数传递的方法
{
return "没有任何参数"; //返回字符串
}
public string out_put() //使用其他方法返回值的方法
{
string str = output(); //使用另一个方法的返回值
return str; //返回方法的返回值
}
如上述代码所示,out_put使用了output方法,output返回一个字符串“没有任何参数”。在out_put方法中,使用了output方法,并将output方法的返回值赋给str局部变量,并返回局部变量。在方法的编写中,方法和方法之间可以使用同一个变量而互不影响,因为方法内部的变量是局部变量,示例代码如下所示。
public string output() //一个无参数传递的方法
{
string str = "没有任何参数"; //声明局部变量str
return str; //使用局部变量str
}
public string out_put() //一个无参数传递的方法
{
string str = "还是没有任何参数"; //声明局部变量str
return str; //使用局部变量str
}
如上述代码所示,output和out_put方法都没有任何参数,但是却使用了同一个变量str。str是局部变量,str的作用范围都在该变量声明的方法内,称作“作用域”。创建了一个方法,就必须指定该方法是否有返回值。如果有返回值,则必须指定返回值的类型,示例代码如下所示。
public int sum(int number1, int number2) //必须返回int类型的值
{
return number1 + number2; //返回一个int类型的值
}
public void newsum(int number1, int number2) //void表示无返回值
{
int sum = number1 + number2; //没有返回值则不能返回值
}
上述代码中,声明了两个方法,分别为sum和newsum。sum方法中,声明了该方法是共有的返回值为int的方法,而newsum方法声明了是共有的无返回值方法。
4.事件
事件是一个对象向其他对象提供有关事件发生的通知的一种方式。在C#中,事件是使用委托来定义和触发的。类或对象可以通过事件向其他类或对象通知发生的相关事情。发送或引发事件的类称为“发行者”,接收或处理事件的类称为“订阅者”。例如在Web Form中双击按钮的过程,就是一个事件,控件并不对过程做描述,只是负责通知一个事件是否发生。事件具有以下特点:
q 事件通常使用委托事件处理程序进行声明。
q 事件始终通知对象消息并指示需要执行某种操作的一种方式。
q 发行者确定何时引发事件,订阅者确定执行何种操作来响应该事件。
q 一个事件可以有多个订阅者。一个订阅者可处理来自多个发行者的多个事件。
q 没有订阅者的事件永远不会被调用。
q 事件通常用于通知用户操作,例如,图形用户界面中的按钮单击或菜单选择操作。
q 如果一个事件有多个订阅者,当引发该事件时,会同步调用多个事件处理程序,也可以使用异步处理多个事件。
声明委托和事件的示例代码如下所示。
public delegate void AnimalEventHandler(); //声明委托
public class Animal //创建类
{
public event AnimalEventHandler OnFly; //声明事件
public void Fly() //创建类的方法
{
OnFly(); //使用事件
}
}
上述代码定义了一个委托,并针对相关委托声明了一个方法。关于委托和事件,会在后面的章节中讲到,上述代码在主函数调用代码如下所示。
Animal bird = new Animal(); //创建对象
bird.OnFly += new AnimalEventHandler(TestAnimal); //注册事件
bird.Fly(); //使用方法
3.2.4 构造函数和析构函数
构造函数和析构函数是面向对象中一个非常特别的函数,构造函数在对象初始化时被执行而析构函数在对象被销毁时被执行。开发人员无需显式的进行函数的编写,在C#应用程序开发中能够为开发人员提供默认的构造函数和析构函数。
1.构造函数
在变量中,常常需要初始化变量,而在类的声明中,也需要构造和初始化类。在类中,构造函数是在第一次创建对象时调用的方法。在任何时候,只要创建了一个对象,编译器就会默认的调用构造函数并执行构造函数。构造函数与类名相同,并且一个类可以有一个或多个构造函数,示例代码如下所示。
public class Animal //创建一个类
{
public string AnimalName; //创建AnimalName名称字段
public Animal() //使用构造函数
{
AnimalName = "动物"; //赋值私有变量
}
}
上述代码声明了一个Animal类,并使用构造函数,构造函数与类同名。当声明一个对象时,系统默认使用此构造函数进行对象的构造。另外,构造函数也可以传递参数,示例代码如下所示。
public class Animal //创建一个类
{
public string AnimalName; //创建AnimalName名称字段
public Animal() //无参数的构造函数
{
AnimalName = "动物"; //赋值私有变量
}
public Animal(string name) //有参数的构造函数
{
AnimalName = name; //私有变量获取传递的参数
}
}
构造函数可以传递参数,当声明一个对象时,若不指定构造函数,系统会默认使用无参数的构造函数。在初始化时,若指定构造函数,系统会按照指定的构造函数构造对象,示例代码如下所示。
Animal dog = new Animal("狗"); //通过构造函数创建对象
Console.WriteLine(dog.AnimalName); //输出对象的属性
在初始化中,直接将参数初始化并传递给构造函数则会在初始化中为对象中的字段初始化。构造函数方便了开发人员设置默认值、限制实例化来编写灵活并且便于阅读的代码。
2.析构函数
析构函数是将当前对象从内存中移除时运行和调用的方法,析构函数的使用通常是为了确保需要释放的对象资源都得到了适当的处理。析构函数的函数名和类名基本相同,在方法前还需要“~”符号来声明该方法是类的析构函数。对于析构函数,有以下几个特点。
q 只能对类定义析构函数,结构不支持析构函数。
q 一个类只能有一个析构函数。
q 无法继承或重载析构函数。
q 无法调用析构函数,在对象注销时,系统会自动调用。
q 析构函数即没有修饰符也不能为它传递参数。
创建析构函数示例代码如下所示。
public class Animal //创建类
{
public string AnimalName; //创建AnimalName名称字段
public Animal() //使用构造函数
{
AnimalName = "动物"; //赋值共有字段
}
~Animal() //使用析构函数
{
AnimalName = String.Empty; //将字符串清空
}
}
上述代码中,当Animal类的对象被销毁时,同时也将AnimalName设置为String.Empty。构造函数隐式的对对象的基类调用Finalize,所以开发人员无法控制在何时调用析构函数。在C++中,当对象被销毁时,系统会调用析构函数并释放对象,而在C#中,垃圾回收机制会自动清理对象资源。在确保了对象没有被任何应用程序使用后,C#的垃圾回收机制会自动清除不再使用的对象的资源。对于开发人员而言,虽然可以显式的使用Collect进行垃圾回收机制,但是会影响应用程序的性能。
3.3 对象的生命周期
在上一节中声明了类并说明了类成员,这些类成员包括字段、方法、事件、构造函数以及析构函数。类是对象的设计图(也称为模板),类用于描述对象。在创建对象后,对象就开始了其生命周期,只有在生命周期内的对象才能够被使用,否则无法使用相应的对象。
3.3.1 类成员的访问
类声明的方法是以class关键字开头,后面紧接着类名字,并以“{”“}”大括号包裹住类成员,示例代码如下所示。
访问权限 class 类名称
{
类成员
}
例如在3.2.2中创建了一个Animal类,其中类名称就是Animal。在实例化一个对象之后,在主程序或其他代码段中,需要对实例化的对象进行访问,即需要对类成员的访问。访问类成员的方法就是在对象后使用“.”号,并通过Visual Studio 2008智能提示选择相应的类成员,示例代码如下所示。
using System;
using System.Collections.Generic;
using System.Linq; //使用LINQ命名空间
using System.Text; //使用文本命名空间
namespace MyClass
{
public class Animal //创建一个类
{
string type; //声明了类成员string type
public void SetType(string type) //声明了类方法
{
this.type = type; //字段赋值
}
}
class Program //主程序类
{
static void Main(string[] args) //程序入口方法
{
Animal bird = new Animal(); //创建了一个bird对象
bird.SetType("bird"); //引用了一个对象的成员
}
}
}
上述代码中,创建了一个公共类Animal,并创建了类成员字段type和方法SetType。在主函数中,创建了一个Animal对象bird,并通过“.”号访问了类成员SetType方法。在访问类或类成员时,可以通过关键字来限制类或类成员的访问权限,以便只有该类的派生类才能访问或者使用,同时也能够限制类成员的权限,提高类成员的安全性。这些关键字包括public、private、protected和internal。
1.public共有权限
public字段具有最高访问级别,任何它的对象或者其他的类都能对public关键字所修饰的类或类成员进行访问,示例代码如下所示。
public class Animal //共有的类
{
public string type; //共有的字段
public void SetType(string type) //共有的方法
{
this.type = type; //赋值共有字段
}
}
2.private私有权限
private字段具有最低的访问级别,它能够保证类和类成员的安全,却限制了其他类或对象对它的访问。私有成员只有在声明他们的类之后才能访问,示例代码如下所示。
public class Animal
{
private int age; //私有成员
string type; //默认的私有成员
public void SetType(string type)
{
this.type = type; //赋值私有成员
}
}
3.protected保护权限
protected字段具有保护类中字段的功能,能够保证类和类成员的安全性,也能够限制其他类或对象对它的访问。但是与private不同的是,protected能够在类和类的的派生类中使用,比private具有更高的访问级别,又比public拥有更低的访问级别,保证了类的安全性,示例代码如下所示。
public class Animal
{
protected string str; //受保护的成员
}
4.internal程序集保护权限
internal字段修饰的类或类成员只有在同一程序集的文件中内部类型或成员才可以访问,示例代码如下所示。
public class Animal
{
internal string str; //受保护的程序集内的成员
}
这种程序集的文件中内部类型或成员才可以访问的修饰符通常是基于组件开发的,因为它能够使一组组件能够以私有方式进行合作,保证了组件的安全性。通常情况下,ASP.NET中页面控件都是通过内部组件方式进行合作。另一方面,这些访问权限修饰符还能够组合使用,例如protected internal就可以进行组合使用,组合使用所修饰的对象只有该类和该类派生的类的成员才可以访问。
3.3.2 类的类型
每一个类的对象都是独立的对象,对象与对象之间有共同的属性,但是对象与对象之间不存在联系,虽然很多情况下类也可以引用类,示例代码如下所示。
public class Animal //创建类
{
public string type; //创建字符串型共有变量
}
class Program //主程序类
{
static void Main(string[] args) //程序入口方法
{
Animal bird = new Animal(); //bird对象
bird.type = "bird"; //初始化字段
Animal cat = new Animal(); //cat对象
cat.type = "cat"; //初始化字段
}
}
上述代码创建了两个对象,一个对象为bird,另一个为cat对象。在初始化类成员时,为不同的对象的类成员赋了不同值,虽然这些类成员的名称相同,但是“.”号说明了该成员所在的对象是不同的。另外,由于类是引用类型,所以类的对象之间可以互相赋值,示例代码如下所示。
Animal newbird = new Animal(); //创建对象
newbird = bird; //对象之间互相赋值
上述代码将对象newbird初始化后并通过bird赋值,所以对象newbird中的type的值等于对象bird中的type值,因为newbird和bird都是引用的同一个对象。
3.3.3 .NET的垃圾回收机制
当创建一个对象,.NET对该对象初始化并在内存相应位置存储,当一个对象执行析构函数时,该对象被销毁并释放相关资源。在C++中,使用析构函数能够让开发人员显式的释放资源。而在.NET中,由于使用了垃圾回收机制(GC)从而导致开发人员无法控制析构函数是何时被运行的。
垃圾回收机制监视对象的生存周期,当一个对象没有被任何应用程序引用时,垃圾回收器就释放对象所占的内存以及资源。在基于.NET Framework编程时,开发人员无需像C++中显式的释放对象的资源也无需关心对象所占用的内存,因为.NET Framework的垃圾回收器能够监视对象并在相应的时候释放对象的资源。
垃圾回收机器没有固定的工作模式。它的工作间隔是不可预期的,一般情况下,当应用程序占用的内存不足的时候会启用垃圾回收器释放未被引用的对象的资源。在应用程序使用复杂并昂贵的外部资源的时候,.NET机制提供接口能够让开发人员实现垃圾回收,以及资源释放机制,通过实现来自IDisposable接口的Dispost方法可以完成显式的资源释放。
在ASP.NET或者Win Form开发中,显式的使用Dispost方法能够提高应用程序的性能。同样,析构函数也是一种清理资源的方法,在对象析构时,可以用Dispose对对象的资源,以及连接信息进行清空从而对对象进行释放。
3.4 使用命名空间
在应用程序开发过程中,类和类成员的名称是丰富的,为了描述一个具体的对象,需要对类成员进行设计。在设计类和类成员过程中,不可避免的类成员中的方法或者类的名称会出现相同的情况,这样就会使类的使用变得复杂,代码的混乱造成可读性降低,使用命名空间可以解决此类难题。
3.4.1 为什么要用命名空间
正如引言中所述,在设计类和类成员的过程中,不可避免的,类或类成员使用的名称都是相同的,这样就让开发更加复杂
展开阅读全文