3 类和接口谢新冲.docx
《3 类和接口谢新冲.docx》由会员分享,可在线阅读,更多相关《3 类和接口谢新冲.docx(68页珍藏版)》请在冰豆网上搜索。
3类和接口谢新冲
第3章类、接口与结构
前面几章对C#的简单数据类型、数组、运算符和表达式以及流控制方法作了详细的介绍。
现在则要进入到面向对象的编程技术,接触到C#最引人入胜之处。
本章介绍C#中面向对象的程序设计的基本方法,包括类的定义、类的成员、类的继承、修饰符,并介绍与类相关的接口、结构、枚举等。
3.1类、域、方法
C#是面向对象的语言,面向对象的一个重要特点是对象具有“封装性”。
类是实现封装的主要手段。
C#的程序中将事物表达成类(class),每个类通过域(field)和方法(method)和其他一些成员来表达事物的状态和行为。
事实上,编写C#程序的主要任务就是定义各种类以及类中的各种成员。
3.1.1定义类中的域和方法
C#程序由很多类构成,类中包括域和方法等。
其中域表明对象的状态,方法表明对象所具有的行为。
程序中,类的定义包括类头和类体两个步骤,其中类体用一对大括号{}括起,类体中包括域(field)、方法(method)、属性(property)、索引、嵌套类定义等等成员。
这里首先介绍域和方法。
如定义表示“人”的类Person如下:
classPerson{
publicstringname;
publicintage;
publicvoidsayHello(){
Console.WriteLine("Hello!
Mynameis"+name);}
publicstringgetInfo(){
return"Name:
"+name+",Age:
"+age;}
}
类头使用关键字class标志类定义的开始,class关键字后面跟着用户定义的类的类名。
类名的命名应符合C#对标识符命名的要求。
类体中包括域和方法。
域和方法都是类的成员。
一个类中可以定义多个域和方法。
一个类可以通过UML(统一建模语言)图中的类图表示出来,如图31所示,类图中的类用一个矩形来表示,上部是类名,中间是域的表示,底部是方法的表示。
图31在UML图中表示的类
1.域
域表示事物的性质状态。
有时,域又称为域变量、成员变量等。
上例中有两个域,name(表示姓名)和age(表示年龄),其类型分别是string和int。
域也是变量,定义域的方式与上一章中变量的定义方法相同。
即:
类型名域名;
如:
intage;
在定义域名时,还可以赋初始值。
如:
intage=0;
如果不赋初始值,系统会自动赋一个缺省值:
数值型为0,bool型为false,引用型为null。
此外,定义域变量前,还可以加修饰符,最常见的修饰符为public,表示公共可访问;而修饰符为private或者没有修饰符时,表示只有本类的成员才可以访问。
有关修饰符的详细内容将在第3.4节中讲述。
2.方法
方法表示类的动态行为,即类所具有的功能和操作。
C#的方法与其他语言中的函数或过程类似,是用来完成某种操作的程序片断。
方法由方法头和方法体组成,其一般格式如下:
修饰符返回值类型方法名(形式参数列表){
方法体各语句;
}
其中形式参数列表的格式为:
形式参数类型1形式参数名1,形式参数类型2形式参数名2,……
小括号()是方法的标志,不能省略;方法名是标识符,要求满足标识符的规则;形式参数是方法从调用它的环境输入的数据;返回值是方法在操作完成后返还给调用它的环境的数据,返回值都有类型,若没有返回值,则使用void表示。
修饰符可以没有,也可以有多个。
最常用的修饰符是public或private。
如在上例中,有一个方法sayHello,其定义如下:
publicvoidsayHello(){
Console.WriteLine(“Hello!
Mynameis“+name);
}
该方法的返回类型为void(没有返回值),参数为空,方法体中有一条语句。
如果方法有返回值,则在方法体中,必须有return语句,return语句后跟上返回值。
如:
publicboolisOlderThan(intanAge){
boolflg;
if(age>anAge)flg=true;elseflg=false;
returnflg;
}
这里的方法isOlderThan用于判断年龄是否比某个值(anAge)大。
anAge是参数,返回值是bool型。
3.1.2构造方法与析构方法
1.构造方法
程序中经常需要创建对象,在创建对象的同时将调用这个对象的构造函数完成对象的初始化工作。
构造方法(construtor),也称构造函数、构造器,它是一种特殊的、与类同名的方法,专门用于创建对象、完成初始化工作。
构造方法的特殊性主要体现在如下的几个方面:
(1)构造方法的方法名与类名相同。
(2)构造方法没有返回类型,也不能写void。
(3)构造方法的主要作用是完成对象的初始化工作。
(4)构造方法一般不能显式地直接调用,而是用new来调用。
(5)在创建(new)一个类的新对象时,系统会自动调用该类的构造方法为新对象初始化。
我们知道,在声明域变量时可以为它赋初值,那么为什么还需要构造方法呢?
这是因为,构造方法可以带上参数,而且构造方法还可以完成赋值之外的其他一些复杂操作。
如,可以给Person类加上一个构造方法如下:
Person(stringn,inta){
name=n;
age=a;
}
在该构造方法中,将给定的参数赋给域变量。
2.缺省构造方法
一般情况下,类都有一个至多个构造方法,如果在定义类对象时没有定义任何构造方法,系统会自动产生一个构造方法,称为缺省构造方法(defaultconstructor),或称为默认构造方法。
缺省构造方法不带参数,并且方法体为空。
例如,如果上面的Person类没有定义构造方法,则系统产生的缺省构方法如下:
publicPerson(){}
值得注意的是,一旦用户提供了一个或多于一个的构造方法,系统就不会提供缺省构造方法。
3.析构方法
创建对象要用构造方法,与此相对,释放对象要用析构方法(destructor),也称析构函数。
析构方法是用符号~开始的并且与类同名的方法,该方法不带参数,则不能写返回类型,也不能有修饰符。
也就是说析构方法的形式如下:
~类名(){….}
例如,在类Person类中定义析构方法如下:
classPerson{
……
~Person(){
……
}
}
一个类的析构方法最多只有一个;如果没有提供析构方法,则系统自动生成一个。
由于对象的释放是由系统自动进行的,不能由程序控制,所以析构方法不能由程序显式调用,而是由系统在释放对象时自动调用。
从这个意义上,普通对象的析构方法并不是十分重要。
3.1.3对象的创建与使用
C#程序定义类的最终目的是使用它,下面讨论如何创建类的对象,即实例化对象。
创建对象前首先要声明变量,声明变量的格式为:
类名变量名;
创建对象的一般格式为:
变量名=new构造方法(参数);
以上两句可以合写成一句为:
类名变量名=new构造方法(参数);
例如:
Personp=newPerson(“Liming”,20);
其中,new是为新建对象的运算符。
它以类为模板,开辟空间并执行相应的构造方法。
new实例化一个对象,返回对该对象的一个引用(即该对象所在的内存地址)。
这里声明的变量,称为对象变量,它是引用型的变量。
与其他变量一样,引用型变量要占据一定的内存空间,同时,它所引用的对象实体(也就是用new创建的对象实体)也要占据一定的空间。
通常对象实体占用的内存空间要大得多,对象是创建的具体实例。
以Person类为例,其中定义了2个域(name和age)和一些方法,这些域和方法保存在一块内存中,这块内存就是p所引用的对象所占用的内存。
变量p与它所引用的实体所占据的关系,是一种引用关系,可以用用图32表示。
实际上,name又是一个引用型变量,它所引用的实体(字符串)又会占据一定的空间。
图32对象变量及其所引用的对象实体
多次用new,将多生成不同的对象,这些对象分别对应于不同的内存空间,它们的值是不同的,可以完全独立地分别对它们进行操作。
要访问或调用一个对象的域或方法,需要用运算符“.”连接对象这个对象和其域或方法。
如:
Console.WriteLine(p.name);
p.sayHello();
由于只能通过对象变量来访问该对象的域或方法,不通过引用变量就无法访问其中的域或方法。
对于访问者而言,这个对象是封装成一个整体的,这正体现了面向对象的程序设计的“封装性”。
3.1.4方法的重载
1.方法的重载
在面向对象的程序设计语言中,有一些方法的含义相同,但带有不同的参数,这些方法使用相同的名字,这就叫方法的重载(overloading)。
方法重载是实现“多态”的一种方式。
多个方法享有相同的名字,但是这些方法的参数列表必须不同,即:
或者参数个数不同,或者是参数类型不同,或者参数类型的顺序不同。
(注意:
在这里,参数类型是关键,仅仅参数的变量名不同是不行的。
)方法重载时,返回值的类型可以相同,也可以不同。
下例中我们通过方法重写分别接收一个或几个不同数据类型的数据。
publicvoidsayHello(){
Console.WriteLine("Hello!
Mynameis"+name);
}
publicvoidsayHello(Personanother){
Console.WriteLine("Hello,"+another.name+"!
Mynameis"+name);
}
这里,两个函数都叫sayHello,都表示问好。
一个不带参数,表示对大家问好;一个带另一个Person对象作参数,表示对某个人问好。
在调用这两个方法时,可以不带参数,也可以带一个Person对象作参数。
编译器会自动根据所带参数的类型来决定具体调用方法。
注意:
在调用方法时,若没有找到类型相匹配的方法,则编译器会找可以兼容的类型来进行调用。
如int类型可以找到使用double类型参数的方法。
若不能找到兼容的方法,则编译不能通过。
例31OverloadingTest.cs方法的重载。
1usingSystem;
2classOverloadingTest
3{
4staticvoidF(){
5Console.WriteLine("F()");
6}
7staticvoidF(objecto){
8Console.WriteLine("F(object)");
9}
10staticvoidF(intvalue){
11Console.WriteLine("F(int)");
12}
13staticvoidF(inta,intb){
14Console.WriteLine("F(int,int)");
15}
16staticvoidF(int[]values){
17Console.WriteLine("F(int[])");
18}
19staticvoidMain(){
20F();
21F
(1);
22F((object)1);
23F(1,2);
24F(newint[]{1,2,3});
25}
26}
程序中的F()方法有各种不同的重载形式,结果如图33。
图33程序运行结果
2.构造方法的重载
构造方法也可以重载,要求使用不同的参数个数、或不同的参数类型、或不同的参数类型顺序。
构造方法的重载,可以让用户用不同的参数来构造对象。
如以下是Person的两种构造方法。
Person(stringn,inta){
name=n;
age=a;
}
Person(stringn)
{
name=n;
age=-1;
}
前一个构造方法中,带有姓名及年龄信息;后一个构造方法,只有姓名信息,年龄信息未定,用一个特殊值(-1)。
3.签名
在方法的重载时,经常提到“参数列表”这一概念。
这一概念的更正式的称呼是“签名(Signature)”。
签名不仅针对方法,它还会针对构造方法、索引、操作符等等。
简单地说,签名由方法名称、它的参数的类型和参数的修饰符组成。
方法的签名中不包括返回类型,并且不包括参数的名称。
在定义类型,方法的重载允许类、结构或接口用相同的名称声明多个方法,但是要求所提供的方法的签名都是互不相同的。
下面的例子介绍了一系列方法声明和它们的签名。
voidF();//F()
voidF(intx);//F(int)
voidF(refintx);//F(refint)
voidF(outintx);//F(outint)
voidF(intx,inty);//F(int,int)
intF(strings);//F(string)
intF(inty);//F(int)
注意参数修饰符是签名的一部分。
这样F(int)、F(refint)和F(outint)都是互不相同的签名。
此外注意第二个和最后一个方法的声明的返回类型,它们的签名都是F(int)。
这样,它们不能同时存在于一同一个类中,否则会产生编译时错误。
3.1.5使用this
在方法中,可以使用一个关键词this,来表示这个对象本身。
具体地说,在普通方法中,this表示调用这个方法的对象;在构造方法中,this表示所新创建的对象。
1.使用this来访问域及方法
在方法及构造方法中,可以使用this来访问对象的域和方法。
例如,方法sayHello中使用name和使用this.name是相同的。
voidsayHello(){
Console.WriteLine("Hello!
Mynameis"+name);
}
与
voidsayHello(){
Console.WriteLine("Hello!
Mynameis"+this.name);
}
的含义是相同的。
2.使用this解决局部变量与域同名的问题
使用this还可以解决局部变量(方法中的变量)或参数变量与域变量同名的问题。
如在构造方法中,经常这样用:
publicPerson(intage,stringname)
{
this.age=age;
this.name=name;
}
这里,this.age表示域变量,而age表示的是参数变量。
3.构造方法中,用this调用另一构造方法
构造方法中,还可以用this来调用另一构造方法。
如
publicPerson():
this(0,"")
{
//构造方法的其他语句;
}
如果在构造方法中,调用另一构造方法,使用的方法是在构造方法的方法头后面用一个冒号(:
),然后使用this(),如果有参数,还可以带参数。
则这条调用语句必须放在第一句。
(关于更构造方法的更复杂的问题,将在第4章中进一步讲述。
)
4.使用this的注意事项
在使用this时,要注意this指的是调用“对象”本身,不是指本“类定义”中看见的变量或方法。
这就不难理解以下几点注意事项:
(1)通过this不仅可以引用该类中定义的域和方法,还可以引用该类的父类中定义的域和方法。
(2)由于它指的是对象,所以this不能通过this来引用类变量(staticfield)、类方法(staticmethod)。
同时,在static方法中,不能使用this。
事实上,在所有的非static方法中,都隐含了一个参数this。
系统在调用这些方法时,会自动传入对这个对象的引用,即传入this。
例32Person.cs定义类及其域及方法
1usingSystem;
2classPerson{
3stringname;
4intage;
5
6Person(stringn,inta){
7name=n;
8age=a;
9}
10
11Person(stringn){
12name=n;
13age=-1;
14}
15
16Person():
this("",0){
17}
18
19voidsayHello(){
20Console.WriteLine("Hello!
Mynameis"+name);
21}
22
23voidsayHello(Personanother){
24Console.WriteLine("Hello,"+another.name+"!
Mynameis"+name);
25}
26
27boolisOlderThan(intanAge){
28boolflg;
29if(age>anAge)flg=true;elseflg=false;
30returnflg;
31}
32
33publicstaticvoidMain(string[]args)
34{
35}
36}
3.2属性、索引
属性(Property)、索引(Indexer)也是C#类中的重要成员。
本节介绍这两个成员。
3.2.1属性
属性,即Property,用于表达事物的状态。
由上节知道,域(Field)也是用来表示事物的状态的,属性则用另一种方式来表示事物的状态。
1.属性的定义
由于属性是表达事物的状态的,属性的存取方式可以是读,也可以是写,读、写属性分别用get及set来进行表示。
在类中定义属性的一般方法是:
修饰符类型名属性名
{
get
{
}
set
{
}
}
其中读、写属性的过程分别用get方法及set方法来进行表示。
如果没有set方法则表示属性是只读的;如果没有get方法则表示属性是只写的。
例如:
可以考虑在Person类中定义一个Name属性:
classPerson
{
privatestringmyName;
publicstringName
{
get
{
returnmyName;
}
set
{
myName=value;
}
}
}
在属性的“获取方法”(get方法)中,用return来返回一个事物的属性值。
在属性“设置方法”(set方法)中可以使用一个特殊的value变量。
该变量包含用户指定的值,通常在set方法中,将用户指定的值记录到一个域变量中。
2.属性的访问
在访问属性时,可以用
对象.属性
的方式来对属性进行访问。
如:
Personp=newPerson();
p.Name=“LiMing”;
Console.WriteLine(p.Name);
对属性的访问,实际上调用相应的set或get方法。
如上面的代码中,p.Name=“LiMing”表示对p变量的属性进行设置,相当于调用set_Name(stringvalue)方法;而Console.WriteLine(p.Name)表示对p变量的属性进行获取,相当于调用get_Name()方法。
事实上,编译器自动产生相应的方法,如对于上面的Name属性,产生的方法是:
voidset_Name(stringvalue);
stringget_Name();
例33PersonProperty.cs展示如何声明和使用读/写属性。
例中定义了一个Person类,它有两个读/写属性:
Name(string)和Age(int),还有一个只读属性Info(string)。
1usingSystem;
2classPerson
3{
4privatestringmyName="N/A";
5privateintmyAge=0;
6
7publicstringName
8{
9get
10{
11returnmyName;
12}
13set
14{
15myName=value;
16}
17}
18
19publicintAge
20{
21get
22{
23returnmyAge;
24}
25set
26{
27myAge=value;
28}
29}
30
31publicstringInfo
32{
33get
34{
35return"Name:
"+Name+",Age:
"+Age;
36}
37}
38
39publicstaticvoidMain()
40{
41Console.WriteLine("SimpleProperties");
42Personperson=newPerson();
43Console.WriteLine(person.Info);
44
45person.Name="Joe";
46person.Age=99;
47Console.WriteLine(person.Info);
48
49person.Age+=1;
50Console.WriteLine(person.Info);
51}
52}
运行结果如图34:
图34声明和使用读/写属性
3.属性与域的比较
属性与域都可以用来表示事物的状态,从使用的角度上看,它们都比较相似。
但它们还是有一定的差别:
(1)属性可以实现只读或只写,而域不能。
(2)属性的set方法可以对用户指定的值(value),进行有效性检查,从而保证只有正确的状态才会得到设置,而域不能。
(3)属性的get方法不仅可以返回域变量的值,还可以返回一些经过计算或处理过的数据,如上例中的只读属性Info,它返回的由Name及Age组合过的字符串。
(4)由于属性在实现时,实际上是方法,所以可以具有方法的一些优点,如可以定义抽象属性等等。
由此可见,在C#中,属性更好地表达了事物的状态的设置和获取。
所以在C#中,一般采取以下原则:
(1)若在类的内部记录事物的状态信息,则用域变量;
(2)