目录
3.1 重写基类成员(virtual和override关键字)
面向对象编程
C#是面对对象的语言,所有面向对象的语言(包括C++、Java..)都具有3个基本的特征。
3个基本的特征:
- 封装:把客观事物封装成类,并将类内部的现实隐藏,以保证数据的完整性。
- 继承:通过继承可以复用父类的代码。
- 多态:允许将子对象赋值给父对象的一种能力。
1.封装
封装的定义:封装就是把类内部的数据隐藏起来,不让对象实例直接对其操作。
封装的使用:C#提供了属性来对类内部的状态进行操作,在C#中可以通过访问修饰符来体现封装这一特性。
封装的意义:封装的意义在于使外部操作者在调用一个类时,通过属性这一限制,来更好的保证数据的完整性(注释1)。
举例:如果我们想写一个功能,我们需要记录一个用户的年龄,用户需要填写他本人的年龄来完成记录。
如果不用封装的特性,我们将实现功能的这个类的所有类成员全声明为public,用户可以任意填写他的年龄,无论是-100还是300,这明显是错误的年龄。
但如果我们运用属性对类进行封装,将age设置为private,编写一个Age属性,用户只能通过访问Age来填写年龄,我们再在属性中添加逻辑代码来设置年龄的范围,以此规范用户的输入。从而达到保护数据完整性的目的。
示例代码:
属性的示例:设置一个人的年龄字段,将赋值范围设置在0-150之间
public class People
{
//私有字段声明
private int age;
private string name;
private string sex;
private string high;
//公有属性:属性类型必须和字段属性一致
public string Age
{
//get访问器:负责对字段值进行读取(返回字段)
get
{
return age;
}
//set访问器:负责对字段值进行赋值(把用户传入的值赋给字段)
//set访问器里有隐式参数value
set
{
//设置年龄范围在0-150之间
if(value<0 || value>150)
{
//抛出异常代码
throw(new ArgumentOutOfRangeException("AgeIntProPery"),value,"年龄必须在0-150之间"));
}
//value是隐式参数:表示用户传入的值
age=value;
}
}
//只读属性
public string Sex
{
get
{
return sex;
}
}
//只写属性
public string Name
{
private get
{
return name;
}
set
{
name=value;
}
}
//自动实现属性
public string High
{
get;set;
}
}
2.继承
继承的定义:在C#中,一个类可以继承自另一个已有的类(密封类除外)。被继承的类称为基类(也叫父类),继承的类称为派生类(也叫子类),子类可以获得父类除构造函数和析构函数以外的所有成员。
(注意:静态类是密封的,不能被继承。)
继承的意义:通过继承,程序可实现对父类代码的重复使用。父类中定义的代码不需要再子类中重复定义,大大减少代码重复部分的编写。
继承的语法:
class 基类{...}
class 派生类:基类{...}
使用继承的要求:
- 父类的私有成员会被子类继承,但不能被子类访问。
- 子类可以调用公用或保护方法间接对私有成员进行访问。
- 与C++不同,C++支持多重继承。而C#仅支持派生于一个基类,这被称为单继承。
- 当我们把一个类称为派生类时,通常是继承自某个不是objcet的类。
2.1 基类&派生类
2.1.1 所有类都派生自 object 类
类object是唯一的非派生类,因为它是继承层次结构的基础。除了特殊的类object,所有的类都是派生类。
没有显示说明基类的派生类都默认继承自object类。
2.1.2 基类的访问
基类访问的意义:父类的私有成员会被子类继承,但不能被子类访问。如果子类必须访问被隐藏的继承成员,可以使用基类访问(base access)表达式。
基类访问的语法:base.基类私有成员
基类访问表达式由关键字base后面跟一个点和成员组成。
2.2 密封类(sealed关键字)
密封类的定义:被sealed关键字修饰的类被称为密封类。密封类只能被用作独立的类,它无法被继承。
密封类的语法:sealed class 类名{..}
2.3 子类的初始化顺序
子类的初始化顺序的意义:当我们初始化一个子类时,除了会调用子类的构造函数外,同时也会调用基类的构造函数。
子类的初始化顺序:
- 初始化类的实例字段;
- 调用基类的构造函数(如果没有指明基类,则调用System.Object的构造函数);
- 调用子类的构造函数。
注意:强烈反对对构造函数进行虚方法。
2.4 抽象类
抽象类的定义:抽象类是指设计为被覆写的类。抽象类用abstract关键字修饰,只能作为其他类的基类,因为它不能实例化。
抽象类的使用要求:
- 必须用abstract关键字修饰。
- 不能创建抽象类的实例。
- 抽象类可以包含抽象成员和非抽象成员。
- 抽象类可以派生自另一个抽象类。
2.4.1 抽象成员
抽象成员的定义:抽象对象是指设计为被覆写的函数成员。
使用抽象成员的要求:
- 抽象成员必须是函数成员或属性、事件、索引器。
- 必须用abstract修饰符标记。
- 该类不能被实例。
3.多态
多态的定义:多态即相同类型的对象调用相同方法表现出不同的行为。
由于可以继承基类的所有成员,子类就都有了相同的行为,但有时子类的某些行为需要相互区分,子类需要覆写父类中的方法来实现子类特有的行为,这样的技术在面向对象的编程中就是多态
3.1 重写基类成员(virtual和override关键字)
只有基类成员声明为virtual或abstract时,才能被派生类重写,如果子类想改变虚方法的实现行为,则必须使用override关键字。
virtual和override关键字的定义:虚方法可以使基类的引用访问升到派生类中(见图3.1)。在基类中使用virtual关键字,可以把需要在子类中表现为不同行为的方法定义为虚方法;在派生类中使用overrride关键字,可以把基类的方法进行重写。

virtual和override关键字的意义:可以使每个基类在调用相同的方法时表现出不同的行为。这正是C#中多态的实现。
使用virtual和override关键字的要求:
- 派生类的方法和基类的方法有相同的签名和返回类型、以及相同的访问性。
- 基类的方法使用virtual修饰、派生类的方法使用override修饰。
- 如果想派生类只有一个重写,需要在派生类中重写方法的修饰符添加sealed。以此来防止一个类被其他类继承。
- 如果子类还想继续访问基类定义的方法,则可以使用base关键字完成调用。
- 不能覆写static方法或非虚方法。
- 方法、属性和索引器,事件都可以被声明为virtual和override。
示例:
class Program
{
static void Main(string[] args)
{
Animal cat = new Cat();
Animal sheep = new Sheep();
cat.Eat();
sheep.Eat();
}
}
class Animal
{
private int age;
public int Age
{
get{return age;}
set
{
if(value<0 || value>10)
{
throw(new.ArgumentOutOfRangeException("年龄必须在0-10之间"))
}
age=value;
}
}
//虚方法
public virtual void Eat()
{
Console.Write("它正在吃");
}
}
class Sheep:Animal
{
//覆写方法
public override void Eat()
{
Console.WriteLine("羊正在吃草");
}
}
class Cat:Animal
{
//覆写方法
public override void Eat()
{
//通过base语句来调用父类的方法
base.Eat();
Console.WriteLine("鱼");
}
}
3.2 屏蔽基类成员(new关键字)
使用屏蔽的意义:虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽(mask)基类成员。
这么做的原因是基类的方法不一定适用于派生类,这 时我们需要在原方法上作改进,或者创建一个同名的新方法。
使用屏蔽的要求:
- 要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。
- 通过派生类中声明新的带有相同签名的成员,可以屏蔽继承的函数成员。(签名由名称和参数组成,不包括返回类型)
- 用new修饰符修饰派生类中的成员,new关键字在这里的作用是隐藏基类成员。
- 可以屏蔽静态成员。
class LandAnimal
{
public string Name="陆地生物";
public void Eat(string food)
{
Console.WriteLine("{0}吃了{1}",Name,food);
}
...
}
class Sheep
{
new public string Name="羊";
new public void Eat(string food)
{
if(food=="肉")
{
Console.WriteLine("错误");
return;
}
Console.WriteLine("{0}吃了{1}",Name,food);
}
}
3.3 基类的引用
基类的引用的意义:当基类和派生类都有相同名称的方法时,我们又想使用基类的方法时,我们可以通过改变派生类的示例对象的类型,通过强制类型转换将派生类的类型转为基类的作用是产生的变量只能访问基类的成员。(覆写方法除外)
之所以能实现这种方法,是因为派生类的实例由基类的实例和派生类的实例组成。而派生类的引用指向包括基类部分的整个类对象。
示例:
class LandAnimal
{
public string Name="陆地生物";
public void Eat(string food)
{
Console.WriteLine("{0}吃了{1}",Name,food);
}
...
}
class Sheep
{
new public string Name="羊";
new public void Eat(string food)
{
if(food=="肉")
{
Console.WriteLine("错误");
return;
}
Console.WriteLine("{0}吃了{1}",Name,food);
}
}
class Program
{
static void Main(string[] args)
{
Sheep one=new Sheep();
LandAnimal two=(LandAnimal)one; //(LandAnimal)的含义是将one的引用类型转换为基类
one.Eat("草"); //打印 “羊吃了草”
two.Eat("肉"); //打印 “陆地生物吃了肉”
}
}
注释:
1.数据完整性(Data Integrity)是指数据的精确性(Accuracy) 和可靠性(Reliability)。 它是应防止数据库中存在不符合语义规定的数据和防止因错误信息的输入输出造成无效操作或错误信息而提出的。