C#类
1.2 类的继承
在1.3节,定义了一个描述个人情况的类Person,如果我们需要定义一个雇员类,当然可以从头开始定义雇员类Employee。但这样不能利用Person类中已定义的函数和数据。比较好的方法是,以Person类为基类,派生出一个雇员类Employee,雇员类Employee继承了Person类的数据成员和函数成员,既Person类的数据成员和函数成员成为Employee类的成员。这个Employee类叫以Person类为基类的派生类,这是C#给我们提出的方法。C#用继承的方法,实现代码的重用。
1.7.1 派生类的声明格式
派生类的声明格式如下:
属性 类修饰符 class 派生类名:基类名 {类体}
雇员类Employee定义如下:
class Employee:Person//Person类是基类
{ private string department;//部门,新增数据成员
private decimal salary;//薪金,新增数据成员
public Employee(string Name,int Age,string D,decimal S):base(Name,Age)
{//注意base的第一种用法,根据参数调用指定基类构造函数,注意参数的传递
department=D;
salary=S;
}
public new void Display()//覆盖基类Display()方法,注意new,不可用override
{ base.Display();//访问基类被覆盖的方法,base的第二种用法
Console.WriteLine("部门:{0} 薪金:{1}",department,salary);
}
}
修改主函数如下:
class Class1
{ static void Main(string[] args)
{ Employee OneEmployee=new Employee("李四",30,"计算机系",2000);
OneEmployee.Display();
}
}
Employee类继承了基类Person的方法SetName()、SetAge(),数据成员name和age,即认为基类Person的这些成员也是Employee类的成员,但不能继承构造函数和析构函数。添加了新的数据成员department和salary。覆盖了方法Display()。请注意,虽然Employee类继承了基类Person的name和age,但由于它们是基类的私有成员,Employee类中新增或覆盖的方法不能直接修改name和age,只能通过基类原有的公有方法SetName()和SetAge()修改。如果希望在Employee类中能直接修改name和age,必须在基类中修改它们的属性为protected。
1.7.2 base 关键字
base关键字用于从派生类中访问基类成员,它有两种基本用法:
l 在定义派生类的构造函数中,指明要调用的基类构造函数,由于基类可能有多个构造函数,根据base后的参数类型和个数,指明要调用哪一个基类构造函数。参见上节雇员类Employee构造函数定义中的base的第一种用法。
l 在派生类的方法中调用基类中被派生类覆盖的方法。参见上节雇员类Employee的Display()方法定义中的base的第二种用法。
1.7.3 覆盖基类成员
在派生类中,通过声明与基类完全相同新成员,可以覆盖基类的同名成员,完全相同是指函数类型、函数名、参数类型和个数都相同。如上例中的方法Display()。派生类覆盖基类成员不算错误,但会导致编译器发出警告。如果增加new修饰符,表示认可覆盖,编译器不再发出警告。请注意,覆盖基类的同名成员,并不是移走基类成员,只是必须用如下格式访问基类中被派生类覆盖的方法:base.Display()。
1.7.4 C#语言类继承特点
C#语言类继承有如下特点:
l C#语言只允许单继承,即派生类只能有一个基类。
l C#语言继承是可以传递的,如果C从B派生,B从A派生,那么C不但继承B的成员,还要继承A中的成员。
l 派生类可以添加新成员,但不能删除基类中的成员。
l 派生类不能继承基类的构造函数、析构函数和事件。但能继承基类的属性。
l 派生类可以覆盖基类的同名成员,如果在派生类中覆盖了基类同名成员,基类该成员在派生类中就不能被直接访问,只能通过base.基类方法名访问。
l 派生类对象也是其基类的对象,但基类对象却不一定是其派生类的对象。例如,前边定义的雇员类Employee是Person类的派生类,所有雇员都是Person类的成员,但很多Person类的成员并不是雇员,可能是学生、自由职业者、儿童等。因此C#语言规定,基类的引用变量可以引用其派生类对象,但派生类的引用变量不可以引用其基类对象。
1.3 类的成员
由于C#程序中每个变量或函数都必须属于一个类或结构,不能象C或C++那样建立全局变量,因此所有的变量或函数都是类或结构的成员。类的成员可以分为两大类:类本身所声明的以及从基类中继承来的。
1.8.1 类的成员类型
类的成员包括以下类型:
l 局部变量:在for、switch等语句中和类方法中定义的变量,只在指定范围内有效。
l 字段:即类中的变量或常量,包括静态字段、实例字段、常量和只读字段。
l 方法成员:包括静态方法和实例方法。
l 属性:按属性指定的get方法和set方法对字段进行读写。属性本质上是方法。
l 事件:代表事件本身,同时是事件处理函数的代表。
l 索引指示器:允许象数组那样使用索引访问类中的数据成员。
l 操作符重载:采用重载操作符的方法定义类中特有的操作。
l 构造函数和析构函数。
包含有可执行代码的成员被认为是类中的函数成员,这些函数成员有方法、属性、索引指示器、操作符重载、构造函数和析构函数。
1.8.2 类成员访问修饰符
访问修饰符用于指定类成员的可访问性,C#访问修饰符有4种:private、protected、public和internal。private声明私有成员,私有数据成员只能被类内部的函数使用和修改,私有函数成员只能被类内部的函数调用。派生类虽然继承了基类私有成员,但不能直接访问它们,只能通过基类的公有成员访问。protected声明保护成员,保护数据成员只能被类内部和派生类的函数使用和修改,保护函数成员只能被类内部和派生类的函数调用。public声明公有成员,类的公有函数成员可以被类的外部程序所调用,类的公有数据成员可以被类的外部程序直接使用。公有函数实际是一个类和外部通讯的接口,外部函数通过调用公有函数,按照预先设定好的方法修改类的私有成员和保护成员。internal声明内部成员,内部成员只能在同一程序集中的文件中才是可以访问的,一般是同一个应用(Application)或库(Library)。
1.4 类的字段和属性
一般把类或结构中定义的变量和常量叫字段。属性不是字段,本质上是定义修改字段的方法,由于属性和字段的紧密关系,把它们放到一起叙述。
1.9.1 静态字段、实例字段、常量和只读字段
用修饰符static声明的字段为静态字段。不管包含该静态字段的类生成多少个对象或根本无对象,该字段都只有一个实例,静态字段不能被撤销。必须采用如下方法引用静态字段:类名.静态字段名。如果类中定义的字段不使用修饰符static,该字段为实例字段,每创建该类的一个对象,在对象内创建一个该字段实例,创建它的对象被撤销,该字段对象也被撤销,实例字段采用如下方法引用:实例名.实例字段名。用const修饰符声明的字段为常量,常量只能在声明中初始化,以后不能再修改。用readonly修饰符声明的字段为只读字段,只读字段是特殊的实例字段,它只能在字段声明中或构造函数中重新赋值,在其它任何地方都不能改变只读字段的值。例子:
public class Test
{ public const int intMax=int.MaxValue;//常量,必须赋初值
public int x=0;//实例字段
public readonly int y=0;//只读字段
public static int cnt=0;//静态字段
public Test(int x1,int y1)//构造函数
{ //intMax=0;//错误,不能修改常量
x=x1;//在构造函数允许修改实例字段
y=y1;//在构造函数允许修改只读字段
cnt++;//每创建一个对象都调用构造函数,用此语句可以记录对象的个数
}
public void Modify(int x1,int y1)
{ //intMax=0;//错误,不能修改常量
x=x1;
cnt=y1;
//y=10;//不允许修改只读字段
}
}
class Class1
{ static void Main(string[] args)
{ Test T1=new Test(100,200);
T1.x=40;//引用实例字段采用方法:实例名.实例字段名
Test.cnt=0;//引用静态字段采用方法:类名.静态字段名
int z=T1.y;//引用只读字段
z=Test.intMax;//引用常量
}
}
1.9.2 属性
C#语言支持组件编程,组件也是类,组件用属性、方法、事件描述。属性不是字段,但必然和类中的某个或某些字段相联系,属性定义了得到和修改相联系的字段的方法。C#中的属性更充分地体现了对象的封装性:不直接操作类的数据内容,而是通过访问器进行访问,借助于get和set方法对属性的值进行读写。访问属性值的语法形式和访问一个变量基本一样,使访问属性就象访问变量一样方便,符合习惯。
在类的基本概念一节中,定义一个描述个人情况的类Person,其中字段name和age是私有字段,记录姓名和年龄,外部通过公有方法SetName和SetAge修改这两个私有字段。现在用属性来描述姓名和年龄。例子如下:
using System;
public class Person
{ private string P_name="张三"; //P_name是私有字段
private int P_age=12; //P_age是私有字段
public void Display() //类的方法声明,显示姓名和年龄
{ Console.WriteLine("姓名:{0},年龄:{1}",P_name,P_age);
}
public string Name //定义属性Name
{ get
{ return P_name;}
set
{ P_name=value;}
}
public int Age //定义属性Age
{ get
{ return P_age;}
set
{ P_age=value;}
}
}
public class Test
{ public static void Main()
{ Person OnePerson= new Person();
OnePerson.Name="田七";//value="田七",通过set方法修改变量P_Name
string s=OnePerson.Name;//通过get方法得到变量P_Name值
OnePerson.Age=20;//通过定义属性,既保证了姓名和年龄按指定方法修改
int x=OnePerson.Age;//语法形式和修改、得到一个变量基本一致,符合习惯
OnePerson.Display();
}
}
在属性的访问声明中,只有set访问器表明属性的值只能进行设置而不能读出,只有get访问器表明属性的值是只读的不能改写,同时具有set访问器和get访问器表明属性的值的读写都是允许的。
虽然属性和字段的使用方法类似,但由于属性本质上是方法,因此不能把属性当做变量,一些使用变量的地方并不能使用属性,例如不能把属性作为函数的引用型参数或输出参数。
1.5 类的方法
方法是类中用于执行计算或其它行为的成员。所有方法都必须定义在类或结构中。
1.10.1 方法的声明
方法的声明格式如下:
属性 方法修饰符 返回类型 方法名(形参列表){方法体}
方法修饰符包括new、public、protected、internal、private、static、virtual、sealed、override、abstract和extern。这些修饰符有些已经介绍过,其它修饰符将逐一介绍。返回类型可以是任何合法的C#数据类型,也可以是void,即无返回值。形参列表的格式为:(形参类型 形参1,形参类型 形参2,...),可以有多个形参。不能使用C语言的形参格式。
1.10.2 方法参数的种类
C#语言的方法可以使用如下四种参数(请注意和形参类型的区别):
l 值参数,不含任何修饰符。
l 引用参数,以ref修饰符声明。
l 输出参数,以out修饰符声明。
l 数组参数,以params修饰符声明。
1. 值参数
当用值参数向方法传递参数时,程序给实参的值做一份拷贝,并且将此拷贝传递给该方法,被调用的方法不会修改实参的值,所以使用值参数时,可以保证实参的值是安全的。如果参数类型是引用类型,例如是类的引用变量,则拷贝中存储的也是对象的引用,所以拷贝和实参引用同一个对象,通过这个拷贝,可以修改实参所引用的对象中的数据成员。
2. 引用参数
有时在方法中,需要修改或得到方法外部的变量值,C语言用向方法传递实参指针来达到目的,C#语言用引用参数。当用引用参数向方法传递实参时,程序将把实参的引用,即实参在内存中的地址传递给方法,方法通过实参的引用,修改或得到方法外部的变量值。引用参数以ref修饰符声明。注意在使用前,实参变量要求必须被设置初始值。
3. 输出参数
为了把方法的运算结果保存到外部变量,因此需要知道外部变量的引用(地址)。输出参数用来把外部变量引用(地址)传递给方法,所以输出参数也是引用参数,与引用参数的差别在于调用方法前无需对变量进行初始化。在方法返回后,传递的变量被认为经过了初始化。值参数、引用参数和输出参数的使用见下例:
using System;
class g{public int a=0;}//类定义
class Class1
{ public static void F1(ref char i)//引用参数
{ i='b';}
public static void F2(char i)//值参数,形参类型为值类型
{ i='d';}
public static void F3(out char i)//输出参数
{ i='e';}
public static void F4(string s)//值参数,形参类型为字符串
{ s="xyz";}
public static void F5(g gg)//值参数,形参类型为引用类型
{ gg.a=20;}
public static void F6(ref string s)//引用参数,形参类型为字符串
{ s="xyz";}
static void Main(string[] args)
{ char a='c';
string s1="abc";
F2(a);//值参数,不能修改外部的a
Console.WriteLine(a);//因a未被修改,显示c
F1(ref a);//引用参数,函数修改外部的a的值
Console.WriteLine(a);//a被修改为b,显示b
Char j;
F3(out j);//输出参数,结果输出到外部变量j
Console.WriteLine(j);//显示e
F4(s1);//值参数,参数类型是字符串,s1为字符串引用变量
Console.WriteLine(s1);//显示:abc,字符串s1不被修改
g g1=new g();
F5(g1);//值参数,但实参是一个类引用类型变量
Console.WriteLine(g1.a.ToString());//显示:20,修改对象数据
F6(ref s1);//引用参数,参数类型是字符串,s1为字符串引用变量
Console.WriteLine(s1);//显示:xyz,字符串s1被修改
}
}
4. 数组参数
数组参数使用params说明,如果形参表中包含了数组参数,那么它必须是参数表中最后一个参数,数组参数只允许是一维数组。比如string[]和string[][]类型都可以作为数组型参数。最后,数组型参数不能再有ref和out修饰符。见下例:
using System;
class Class1
{ static void F(params int[] args)//数组参数,有params说明
{ Console.Write("数组包含{0}个元素:",args.Length);
foreach (int i in args)
Console.Write(" {0}",i);
Console.WriteLine();
}
static void Main(string[] args)
{ int[] a = {1,2,3};
F(a);//实参为数组类引用变量a
F(10, 20, 30, 40);//等价于F(new int[] {10,20,30,40});
F(new int[] {60,70,80,90});//实参为数组类引用
F();//等价于F(new int[] {});
F(new int[] {});//实参为数组类引用,数组无元素
}
}
程序输出
数组包含3个元素:1 2 3
数组包含4个元素:10 20 30 40
数组包含4个元素:60,70,80,90
数组包含0个元素:
数组包含0个元素:
方法的参数为数组时也可以不使用params,此种方法可以使用一维或多维数组,见下例:
using System;
class Class1
{ static void F(int[,] args)//值参数,参数类型为数组类引用变量,无params说明
{ Console.Write("数组包含{0}个元素:",args.Length);
foreach (int i in args)
Console.Write(" {0}",i);
Console.WriteLine();
}
static void Main(string[] args)
{ int[,] a = {{1,2,3},{4,5,6}};
F(a);//实参为数组类引用变量a
//F(10, 20, 30, 40);//此格式不能使用
F(new int[,] {{60,70},{80,90}});//实参为数组类引用
//F();//此格式不能使用
//F(new int[,] {});//此格式不能使用
}
}
程序输出
数组包含4个元素:1 2 3 4 5 6
数组包含4个元素:60,70,80,90
1.10.3 静态方法和实例方法
用修饰符static声明的方法为静态方法,不用修饰符static声明的方法为实例方法。不管类生成或未生成对象,类的静态方法都可以被使用,使用格式为:类名.静态方法名。静态方法只能使用该静态方法所在类的静态数据成员和静态方法。这是因为使用静态方法时,该静态方法所在类可能还没有对象,即使有对象,由于用类名.静态方法名方式调用静态方法,静态方法没有this指针来存放对象的地址,无法判定应访问哪个对象的数据成员。在类创建对象后,实例方法才能被使用,使用格式为:对象名.实例方法名。实例方法可以使用该方法所在类的所有静态成员和实例成员。例子如下:
using System;
public class UseMethod
{ private static int x=0;//静态字段
private int y=1;//实例字段
public static void StaticMethod()//静态方法
{ x=10;//正确,静态方法访问静态数据成员
//y=20;//错误,静态方法不能访问实例数据成员
}
public void NoStaticMethod()//实例方法
{ x=10;//正确,实例方法访问静态数据成员
y=20;//正确,实例方法访问实例数据成员
}
}
public class Class1
{ public static void Main()
{ UseMethod m=new UseMethod();
UseMethod.StaticMethod();//使用静态方法格式为:类名.静态方法名
m.NoStaticMethod();//使用实例方法格式为:对象名.实例方法名
}
}
1.10.4 方法的重载
在C#语言中,如果在同一个类中定义的函数名相同,而参数类型或参数个数不同,认为是不相同的函数,仅返回值不同,不能看作不同函数,这叫做函数的重载。前边Person类中定义了多个构造函数就是重载的例子。在C语言中,若计算一个数据的绝对值,则需要对不同数据类型求绝对值方法使用不同的方法名,如用abs()求整型数绝对值,labs()求长整型数绝对值,fabs()求浮点数绝对值。而在C#语言中,可以使用函数重载特性,对这三个函数定义同样的函数名,但使用不同的参数类型。下面是实现方法:
using System;
public class UseAbs
{ public int abs(int x)//整型数求绝对值
{ return(x<0 ? -x:x);}
public long abs(long x)//长整型数求绝对值
{return(x<0 ? -x:x);}
public double abs(double x)//浮点数求绝对值
{return(x<0 ? -x:x);}
}
class Class1
{ static void Main(string[] args)
{ UseAbs m=new UseAbs();
int x=-10;
long y=-123;
double z=-23.98d;
x=m.abs(x);
y=m.abs(y);
z=m.abs(z);
Console.WriteLine("x={0},y={1},z={2}",x,y,z);
}
}
类的对象调用这些同名方法,在编译时,根据调用方法的实参类型决定调用哪个同名方法,计算不同类型数据的绝对值。这给编程提供了极大方便。
1.10.5 操作符重载
操作符重载是将C#语言中的已有操作符赋予新的功能,但与该操作符的本来含义不冲突,使用时只需根据操作符出现的位置来判别其具体执行哪一种运算。操作符重载,实际是定义了一个操作符函数,操作符函数声明的格式如下:
static public 函数返回类型 operator 重新定义的操作符(形参表)
C#语言中有一些操作符是可以重载的,例如:+、-、!、 ~、++、--、true、false、*、 /、%、&、|、^、<<、>>、==、!=、>、<、>=、<=等等。但也有一些操作符是不允许进行重载的,例如:=、&&、||、?:、new、typeof、sizeof、is 等。
下边的例子,定义一个复数类,并且希望复数的加减乘除用符号+、-、*、/来表示。
using System;
class Complex//复数类定义
{ private double Real;//复数实部
private double Imag;//复数虚部
public Complex(double x,double y)//构造函数
{ Real=x;
Imag=y;
}
static public Complex operator - (Complex a)//重载一元操作符负号,注意1个参数
{ return (new Complex(-a.Real,-a.Imag));}
static public Complex operator +(Complex a,Complex b)//重载二元操作符加号
{ return (new Complex(a.Real+b.Real,a.Imag+b.Imag));}
public void Display()
{ Console.WriteLine("{0}+({1})j",Real,Imag);}
}
class Class1
{ static void Main(string[] args)
{ Complex x=new Complex(1.0,2.0);
Complex y=new Complex(3.0,4.0);
Complex z=new Complex(5.0,7.0);
x.Display();//显示:1+(2)j
y.Display();//显示:3+(4)j
z.Display();//显示:5+(7)j
z=-x;//等价于z=opeator-(x)
z.Display();//显示:-1+(-2)j
z=x+y;//即z=opeator+(x,y)
z.Display();//显示:4+(6)j
}
}
1.10.6 this关键字
每个类都可以有多个对象,例如定义Person类的两个对象:
Person P1=new Person("李四",30);
Person P2=new Person("张三",40);
因此P1.Display()应显示李四信息,P2.Display()应显示张三信息,但无论创建多少个对象,只有一个方法Display(),该方法是如何知道显示哪个对象的信息的呢?C#语言用引用变量this记录调用方法Display()的对象,当某个对象调用方法Display()时,this便引用该对象(记录该对象的地址)。因此,不同的对象调用同一方法时,方法便根据this所引用的不同对象来确定应该引用哪一个对象的数据成员。this是类中隐含的引用变量,它是自动被赋值的,可以使用但不能被修改。例如:P1.Display(),this引用对象P1,显示李四信息。P2.Display(),this引用对象P2,显示张三信息。
1.6 类的多态性
在面向对象的系统中,多态性是一个非常重要的概念。C#支持两种类型的多态性,第一种是编译时的多态性,一个类的对象调用若干同名方法,系统在编译时,根据调用方法的实参类型及实参的个数决定调用哪个同名方法,实现何种操作。编译时的多态性是通过方法重载来实现的。C#语言的方法重载以及操作符重载和C++语言的基本一致。
第二种是运行时的多态性,是在系统运行时,不同对象调用一个名字相同,参数的类型及个数完全一样的方法,会完成不同的操作。C#运行时的多态性通过虚方法实现。在类的方法声明前加上了virtual修饰符,被称之为虚方法,反之为非虚方法。C#语言的虚方法和C++语言的基本一致。下面的例子说明了虚方法与非虚方法的区别:
using System;
class A
{ public void F()//非虚方法
{ Console.Write(" A.F");}
public virtual void G()//虚方法
{ Console.Write(" A.G");}
}
class B:A//A类为B类的基类
{ new public void F()//覆盖基类的同名非虚方法F(),注意使用new
{ Console.Write(" B.F");}
public override void G()//覆盖基类的同名虚方法G(),注意使用override
{ Console.Write(" B.G");}
}
class Test
{ static void F2(A aA)//注意,参数为A类引用变量
{ aA.G();}
static void Main()
{ B b=new B();
A a1=new A();
A a2=b;//允许基类引用变量引用派生类对象,a2引用派生类B对象b
a1.F();//调用基类A的非虚方法F(),显示A.F
a2.F();//F()为非虚方法,调用基类A的F(),显示A.F
b.F();//F()为非虚方法,调用派生类B的F(),显示B.F
a1.G();//G()为虚方法,因a1引用基类A对象,调用基类A的G(),显示A.G
a2.G();//G()为虚方法,因a2引用派生类B对象,调用派生类B的G(),显示B.G
F2(b);//实参为派生类B对象,由于A aA=b,调用派生类B的函数G(),显示B.G
F2(a1);//实参为基类A对象,调用A类的函数G(),显示A.G
}
}
那么输出应该是:
A.F A.F B.F A.G B.G B.G A.G
注意例子中,不同对象调用同名非虚方法F()和同名虚方法G()的区别。a2虽然是基类引用变量,但它引用派生类对象b。由于G()是虚方法,因此a2.G()调用派生类B的G(),显示B.G。但由于F()是非虚方法,a2.F()仍然调用基类A的F(),显示A.F。或者说,如果将基类引用变量引用不同对象,或者是基类对象,或者是派生类对象,用这个基类引用变量分别调用同名虚方法,根据对象不同,会完成不同的操作。而非虚方法则不具备此功能。
方法F2(A aA)中,参数是A类类型,F2(b)中形参和实参的关系是:A aA=b,即基类引用变量aA引用派生类对象b,aA.G()调用派生类B的函数G(),显示B.G。同理,F2(a1)实参为基类A对象,调用A类的函数G(),显示A.G。
在类的基本概念一节中,定义一个描述个人情况的类Person,其中公有方法Display()用来显示个人信息。在派生雇员类Employee中,覆盖了基类的公有方法Display(),以显示雇员新增加的信息。我们希望隐藏这些细节,希望无论基类还是派生类,都调用同一个显示方法,根据对象不同,自动显示不同的信息。可以用虚方法来实现,这是一个典型的多态性例子。例子
using System;
public class Person
{ private String name="张三";//类的数据成员声明
private int age=12;
protected virtual void Display()//类的虚方法
{ Console.WriteLine("姓名:{0},年龄:{1}",name,age);
}
public Person(string Name,int Age)//构造函数,函数名和类同名,无返回值
{ name=Name;
age=Age;
}
static public void DisplayData(Person aPerson)//静态方法
{ aPerson.Display();//不是静态方法调用实例方法,如写为Display()错误
}
}
public class Employee:Person//Person类是基类
{ private string department;
private decimal salary;
public Employee(string Name,int Age,string D,decimal S):base(Name,Age)
{ department=D;
salary=S;
}
protected override void Display()//重载虚方法,注意用override
{ base.Display();//访问基类同名方法
Console.WriteLine("部门:{0} 薪金:{1} ", department,salary);
}
}
class Class1
{ static void Main(string[] args)
{ Person OnePerson=new Person("李四",30);
Person.DisplayData(OnePerson);//显示基类数据
Employee OneEmployee=new Employee("王五",40,"财务部",2000);
Person.DisplayData(OneEmployee); //显示派生类数据
}//两次调用完全相同的方法,实参不同,实现的功能不同
}
运行后,显示的效果是:
姓名: 李四,年龄:30
姓名: 王五,年龄:40
部门:财务部 薪金:2000
1.7 抽象类和抽象方法
抽象类表示一种抽象的概念,只是希望以它为基类的派生类有共同的函数成员和数据成员。抽象类使用abstract修饰符,对抽象类的使用有以下几点规定:
l 抽象类只能作为其它类的基类,它不能直接被实例化。
l 抽象类允许包含抽象方法,虽然这不是必须的。抽象方法用abstract修饰符修饰。
l 抽象类不能同时又是密封的。
l 抽象类的基类也可以是抽象类。如果一个非抽象类的基类是抽象类,则该类必须通过覆盖来实现所有继承而来的抽象方法,包括其抽象基类中的抽象方法,如果该抽象基类从其它抽象类派生,还应包括其它抽象类中的所有抽象方法。
请看下面的示例:
abstract class Figure//抽象类定义,表示一个抽象图形
{ protected double x=0,y=0;
public Figure(double a,double b)
{ x=a;
y=b;
}
public abstract void Area();//抽象方法,无实现代码,抽象图形无法计算面积
}
class Square:Figure///类Square定义,矩形类
{ public Square(double a,double b):base(a,b)
{}
public override void Area()//不能使用new,必须用override
{ Console.WriteLine("矩形面积是:{0}",x*y);}//显示矩形面积
}
class Circle:Figure///类Circle定义,圆类
{ public Circle(double a):base(a,a)
{}
public override void Area()
{ Console.WriteLine("圆面积是:{0}",3.14*x*y);}//显示圆的面积
}
class Class1
{ static void Main(string[] args)
{ Square s=new Square(20,30);
Circle c=new Circle(10);
s.Area();
c.Area();
}
}
程序输出结果为:
矩形面积是:600
圆面积是:314
抽象类Figure提供了一个抽象方法Area(),并没有实现它,类Square和Circle从抽象类Figure中继承方法Area(),分别具体实现计算矩形和圆的面积。
在类的基本概念一节中,定义一个描述个人情况的类Person,它只是描述了一个人最一般的属性和行为,因此不希望生成它的对象,可以定义它为抽象类。
注意:C++程序员在这里最容易犯错误。C++中没有对抽象类进行直接声明的方法,而认为只要在类中定义了纯虚函数,这个类就是一个抽象类。纯虚函数的概念比较晦涩,直观上不容易为人们接受和掌握,因此C#抛弃了这一概念。
1.8 密封类和密封方法
有时候,我们并不希望自己编写的类被继承。或者有的类已经没有再被继承的必要。C#提出了一个密封类(sealed class)的概念,帮助开发人员来解决这一问题。
密封类在声明中使用sealed修饰符,这样就可以防止该类被其它类继承。如果试图将一个密封类作为其它类的基类,C#编译器将提示出错。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。
C#还提出了密封方法(sealed method)的概念。方法使用sealed修饰符,称该方法是一个密封方法。在派生类中,不能覆盖基类中的密封方法。
1.9 接口
与类一样,在接口中可以定义一个和或个方法、属性、索引指示器和事件。但与类不同的是,接口中仅仅是它们的声明,并不提供实现。因此接口是函数成员声明的集合。如果类或结构从一个接口派生,则这个类或结构负责实现该接口中所声明的所有函数成员。一个接口可以从多个接口继承,而一个类或结构可以实现多个接口。由于C#语言不支持多继承,因此,如果某个类需要继承多个类的行为时,只能使用多个接口加以说明。
1.14.1 接口声明
接口声明是一种类型声明,它定义了一种新的接口类型。接口声明格式如下:
属性 接口修饰符 interface 接口名:基接口{接口体}
其中,关键字interface、接口名和接口体是必须的,其它项是可选的。接口修饰符可以是new、public、protected、internal和private。接口声明例子如下:
public interface IExample
{//所有接口成员都不能包括实现
string this[int index] {get;set;}//索引指示器声明
event EventHandler E;//事件声明
void F(int value);//方法声明
string P { get; set;}//属性声明
}
声明接口时,需注意以下内容:
l 接口成员只能是方法、属性、索引指示器和事件,不能是常量、域、操作符、构造函数或析构函数,不能包含任何静态成员。
l 接口成员声明不能包含任何修饰符,接口成员默认访问方式是public。
1.14.2 接口的继承
类似于类的继承性,接口也有继承性。派生接口继承了基接口中的函数成员说明。接口允许多继承,一个派生接口可以没有基接口,也可以有多个基接口。在接口声明的冒号后列出被继承的接口名字,多个接口名之间用分号分割。例子如下:
using System;
interface IControl
{ void Paint();
}
interface ITextBox:IControl//继承了接口Icontrol的方法Paint()
{ void SetText(string text);
}
interface IListBox:IControl//继承了接口Icontrol的方法Paint()
{ void SetItems(string[] items);
}
interface IComboBox:ITextBox,IListBox
{//可以声明新方法
}
上面的例子中,接口ITextBox和IListBox都从接口IControl中继承,也就继承了接口IControl的Paint方法。接口IComboBox从接口ITextBox和IListBox中继承,因此它应该继承了接口ITextBox的SetText方法和IListBox的SetItems方法,还有IControl的Paint方法。
1.14.3 类对接口的实现
前面已经说过,接口定义不包括函数成员的实现部分。继承该接口的类或结构应实现这些函数成员。这里主要讲述通过类来实现接口。类实现接口的本质是,用接口规定类应实现哪些函数成员。用类来实现接口时,接口的名称必须包含在类声明中的基类列表中。
在类的基本概念一节中,定义一个描述个人情况的类Person,从类Person可以派生出其它类,例如:工人类、公务员类、医生类等。这些类有一些共有的方法和属性,例如工资属性。一般希望所有派生类访问工资属性时用同样变量名。该属性定义在类Person中不合适,因为有些人无工资,如小孩。如定义一个类作为基类,包含工资属性,但C#不支持多继承。可行的办法是使用接口,在接口中声明工资属性。工人类、公务员类、医生类等都必须实现该接口,也就保证了它们访问工资属性时用同样变量名。例子如下:
using System;
public interface I_Salary//接口
{ decimal Salary//属性声明
{ get;
set;
}
}
public class Person
{…//参见1.9.2属性节Person类定义,这里不写出了。
}
public class Employee:Person,I_Salary//Person类是基类,I_Salary是接口
{//不同程序员完成工人类、医生类等,定义工资变量名称可能不同
private decimal salary;
public new void Display()
{ base.Display();
Console.WriteLine("薪金:{0} ",salary);
}
//工人类、医生类等都要实现属性Salary,保证使用的工资属性同名
public decimal Salary
{ get
{ return salary;}
set
{ salary=value;}
}
}
public class Test
{ public static void Main()
{ Employee S=new Employee();
S.Name="田七";//修改属性Name
S.Age=20;//修改属性Age
S.Salary=2000;//修改属性Salary
S.Display();
}
}
如果类实现了某个接口,类也隐式地继承了该接口的所有基接口,不管这些基接口有没有在类声明的基类表中列出。因此,如果类从一个接口派生,则这个类负责实现该接口及该接口的所有基接口中所声明的所有成员。
1.10 代表(delegate)类型
本节介绍C#的一个新的引用数据类型:代表类型,也翻译为委托类型。在功能上它类似C语言的函数指针,目的是通过创建代表类型对象去调用函数。使用代表类型的第一步,是声明一个新代表类型,指明这个新代表类型对象要调用的函数的返回值类型,参数的个数及类型,因此C#中的代表类型是类型安全的。生成一个新代表类型声明格式如下:
属性集 修饰符 delegate 函数返回类型 定义的代表标识符(函数形参列表);
修饰符包括new、public、protected、internal和private。例如我们可以声明一个能代表返回类型为int,无参数的函数的代表类型,类型名为MyDelegate,类型声明如下:
public delegate int MyDelegate();//只能代表返回类型为int,无参数的函数
声明了代表类型MyDelegate,可以创建代表类型MyDelegate的对象,用这个对象去代表一个静态方法或非静态的方法,所代表的方法必须为int类型,无参数。看下面的例子:
using System;
delegate int MyDelegate();//声明一个代表类型,类型名为MyDelegate,注意声明位置
public class MyClass
{ public int InstanceMethod()//非静态的方法,注意方法为int类型,无参数
{ Console.WriteLine("调用了非静态的方法。");
return 0;
}
static public int StaticMethod()//静态方法,注意方法为int类型,无参数
{ Console.WriteLine("调用了静态的方法。");
return 0;
}
}
public class Test
{ static public void Main ()
{ MyClass p = new MyClass();
//用new建立代表类型MyDelegate对象,d引用的对象代表了方法InstanceMethod
MyDelegate d=new MyDelegate(p.InstanceMethod);//参数是被代表的方法
d();//因d引用的对象代表了方法InstanceMethod,用此方式调用这个非静态方法
//用new建立代表类MyDelegate对象,d引用的对象代表了方法StaticMethod
d=new MyDelegate(MyClass.StaticMethod);//参数是被代表的方法
d();//因d引用的对象代表了方法StaticMethod,用此方式调用这个静态方法
}
}
程序的输出结果是:
调用了非静态的方法。
调用了静态的方法。