目前除C#外流行的面向对象编程的几个语言分别是:Java, C++等;
面向对象的语言都具有以下特征:
- 封装 - 将客观事物封装成类, 并将类内部的实现隐藏,以保证数据的完整性;
- 继承 - 子类通过继承可以复用父类的代码;
- 多态 - 允许将子对象赋值给父对象的一种能力.
5.1 封装
封装指的是把类内部的数据隐藏起来,不让对象实例直接对其操作。C#中提供了属性机制来对类内部的状态进行操作。在C#中,封装可以通过public 、private 、protected 和internal 等关键字来体现。
为什么要将类内部的数据封装起来呢?下面通过一个例子来解释其必要性,具体的代码如下。
// 不使用封装特性来定义一个Person类
public class Person
{
public string _name;
public int _age;
}
当把字段定义为公共类型时,外部对象可以对类内部的数据进行任意的操作,很可能导致当前值不符合系统的业务逻辑。下面的代码演示了公共数据存在的问题。
class Program
{
static void Main(string[] args)
{
Person p = new Person();
p._name = "Learning Hard";
// -5赋给age字段显然是不符合业务逻辑的,因为人的年龄不可能为负数
p._age = -5;
}
}
在以上代码中,尽管把-5 赋给Person 的_age 属性没有引起编译错误,但这并不符合业务逻辑,因为在现实生活中,人的年龄不可能为负值。当我们把类的字段定义为公共类型时,外部对象可以直接对类内部的数据进行操作,此时无法对这些操作进行一些逻辑判断,这就是公共数据的问题所在。
面向对象编程中的封装特性,是一种保护状态数据完整性的方法,在面向对象编程中,应更多地定义私有数据字段。C#提供属性机制来对这种私有字段数据进行间接的操作,并且可以在属性的定义中加入更多的逻辑判断。利用封装技术,我们可以有效地对外部隐藏类内部的数据,从而避免数据损坏。
下面的代码演示了在C#中,使用封装技术后类的定义过程。
public class Person
{
private string _name;
private int _age;
public string Name
{
get { return _name; }
set { _name = value; }
}
public int Age
{
get { return _age; }
set
{
// 在属性定义中,可以根据系统的业务逻辑添加逻辑代码
if (value < 0 || value > 120)
{
throw (new ArgumentOutOfRangeException("AgeIntPropery", value, "年龄必须在0-120之间"));
}
_age=valuel;
}
}
}
使用了封装技术之后,外部数据只能对属性进行操作。如果把不符合逻辑的值赋给属性Age ,就会在运行时抛出异常,客户端调用可以通过捕获该异常,进行相关的错误处理操作。
简而言之, 封装是为了更好地将代码与现实业务结合.
5.2 继承
在C#中,一个类可以继承另外一个已有的类(密封类除外),被继承的类称为基类(或父类),继承的类称为派生类(或子类),子类将获得基类除构造函数和析构函数以外的所有成员。此外,静态类是密封的,也不能被继承。
例如,牛、羊、马等都是动物,但它们是不同种类的动物,除了具有动物的共性外,它们还具有各自的特点,如不同的用途、不同的发声方式等。我们可以把动物定义为牛、羊、马的父类,这样子类不但继承了基类除构造函数和析构函数以外的所有成员,还可以添加自己特有的成员。
通过继承,程序可实现对父类代码的复用。因为子类可继承父类的所有成员,父类中定义的代码便不需要在子类中进行重复定义了。
5.2.1 CSharp中的继承
C#与C++不同,C#仅支持派生于一个基类,而C++则支持多重继承。但C#可以继承多个接口,接口的内容会在第6章中介绍。
下面的代码演示了C#中继承的使用方法。
class Program
{
static void Main(string[] args)
{
Horse horse = new Horse();
horse.Age = 2;
Console.WriteLine("马的年龄为:{0}", horse.Age);
Sheep sheep = new Sheep();
sheep.Age = 1;
Console.WriteLine("羊的年龄为:{0}", sheep.Age);
Console.Read();
}
}
// 基类
public class Animal
{
private int _age;
public int Age
{
get { return _age; }
set
{
// 这里假设牛的寿命为10年
if (value < 0 || value > 10)
{
throw (new ArgumentOutOfRangeException("AgeIntPropery", value, "年龄必须在0-10之间"));
}
_age = value;
}
}
}
// 马 (子类)
public class Horse:Animal
{
}
// 羊 (子类)
public class Sheep:Animal
{
}
在以上代码中,虽然各个子类并没有定义Age 属性,但由于它们都继承自基类Animal ,基类中又定义了Age 属性,所以子类也就继承了父类中的Age 。通过继承,避免了在子类中重复定义Age ,从而达到代码复用的目的。
需要注意的是,子类并不能对父类的私有成员进行直接访问,它只可对保护成员和公有成员进行访问。如果把上面代码中的Age 属性定义为私有属性,Main 函数就不能对该属性进行赋值操作了。私有成员也会被子类继承,但子类不能直接访问私有成员,子类可以通过调用公有或保护方法来间接地对私有成员进行访问。
5.2.2 密封类
密封类不可以被另一个类继承, 强行继承编译会产生错误.
作用:
-
防止继承:密封类不能被其他类继承,这样可以防止其他类对该类进行修改或继承并重写其方法,确保类的完整性和稳定性。
-
提高性能:由于密封类不能被继承,编译器可以对其进行一些优化,提高代码的执行效率。
-
安全性:密封类可以防止其他类通过继承和重写方法来篡改其内部逻辑,增强了代码的安全性。
应用场景:
-
工具类:一些工具类不需要被继承或修改,可以将其定义为密封类,以确保其功能的完整性和稳定性。
-
不可变类:一些不可变类,如字符串类,不希望被继承或修改,可以将其定义为密封类,以确保其不被修改。
-
性能敏感类:对于一些性能关键的类,如数学计算类,为了提高性能,可以将其定义为密封类,以允许编译器进行优化。
5.2.3 子类的初始化顺序
子类继承父类之后, 当我们初始化子类时, 除了会调用子类的构造函数外,同时也会调用基类的构造函数。
子类的初始化顺序如下:
-
初始化类的实例字段;
-
调用基类的构造函数,如果没有指明基类,则调用System.Object 的构造函数;
-
调用子类的构造函数。
下面例子演示了子类的初始化顺序.
class Program
{
static void Main(string[] args)
{
// 初始化子类实例
ChildA child = new ChildA();
child.Print();
Console.Read();
}
}
public class Parent
{
// ②调用基类构造函数
public Parent()
{
Console.WriteLine("基类构造函数被调用");
}
}
public class ChildA : Parent
{
// 创建一个ChildA对象时,
// ①初始化它的实例字段
private int FieldA = 3;
// ③调用子类构造函数
public ChildA()
{
Console.WriteLine("子类构造函数被调用");
}
public void Print()
{
Console.WriteLine(FieldA);
}
}
输出结果如下:
基类构造函数被调用
子类构造函数被调用
3
5.3 多态
由于可以继承基类的所有成员,子类就都有了相同的行为,但是有时子类的某些行为需要相互区别,子类需要覆写父类中的方法来实现子类特有的行为,这样的技术在面向对象的编程中就是多态。多态即相同类型的对象调用相同的方法却表现出不同行为的现象。
5.3.1 使用virtual 和override 关键字实现方法重写
只有基类成员声明为virtual 或abstract 时,才能被派生类重写;而如果子类想改变虚方法的实现行为,则必须使用override 关键字。下面的代码演示了C#对多态的支持:
class Program
{
static void Main(string[] args)
{
Animal horse = new Horse();
horse.Voice();
Animal sheep = new Sheep();
// 相同类型的对象调用相同的方法表现出不同的行为
sheep.Voice();
Console.Read();
}
}
// 动物基类
public class Animal
{
private int _age;
public int Age
{
get { return _age; }
set
{
// 这里假设牛的寿命为10年
if (value < 0 || value > 10)
{
throw (new ArgumentOutOfRangeException("AgeIntPropery", value, "年龄必须在0-10之间"));
}
_age = value;
}
}
// 几乎所有动物都具有发出声音的能力
// 但是对于动物的子类来说,每个子类发出的声音都是不一样的
public virtual void Voice()
{
Console.WriteLine("动物开始发出声音");
}
}
// 马 (子类),子类应重写基类的方法,以实现自己特有的行为
public class Horse : Animal
{
// 通过override关键字来重写父类方法
public override void Voice()
{
// 调用基类的方法
base.Voice();
Console.WriteLine("马发出嘶……嘶……嘶……的声音");
}
}
// 羊 (子类)
public class Sheep : Animal
{
// 重写父类方法
public override void Voice()
{
// 通过base语句来调用父类的方法
base.Voice();
Console.WriteLine("羊发出咩……咩……咩……的声音");
}
}
上面的代码通过使用virtual 关键字,把基类中需要在子类中表现为不同行为的方法定义为虚方法,然后在子类中使用override 关键字对基类方法进行重写。这样,每个基类在调用相同的方法时将表现出不同的行为,这段代码正是C#中多态的实现。
如果子类还想继续访问基类定义的方法,则可使用base 关键字来完成调用。
代码运行结果如下:
动物开始发出声音
马发出嘶...嘶..嘶...的声音
动物开始发出声音
羊发出咩...咩...咩...的声音
从上面的运行结果可以看出,相同类型的对象调用相同的方法确实表现了不同的行为,这就是多态的精髓所在。
但是,上面的代码还存在一个问题:我们可以通过new 操作符创建Animal 基类的实例,可Animal 基类的作用是为所有子类提供公共成员,它是一个抽象的概念,在实际的系统中我们希望能避免创建该类的实例。该怎么办呢?
对于C#,可以使用abstract 关键字来防止在代码中直接创建这样类的实例,正如下面的代码所示:
public abstract class Animal
{
…
}
如果尝试创建Animal 实例, 则会报错:无法创建抽象类或接☐"_4_6.Animal"的实例
5.3.2 阻止派生类重写虚成员
前面曾介绍到,用sealed 关键字可以防止一个类被其他类继承。同样,也可以使用sealed 关键字来阻止派生类重写虚成员。例如,我们希望Horse 的继承类不再具有扩展Voice 方法的行为,则可以使用sealed 关键字来停止虚拟继承,如以下代码所示:
public class Horse : Animal
{
// 通过override关键字来重写父类方法
public sealed override void Voice()
{
// 调用基类的方法
base.Voice();
Console.WriteLine("马发出嘶……嘶……嘶……的声音");
}
}
尝试在Horse 的派生类中重写Voice 方法,则会收到“无法对密封成员进行复写”的错误信息。
5.3.3 使用新成员隐藏基类成员
如果想在派生类中定义与基类成员同名的成员,则可以使用new 关键字把基类成员隐藏起来。
public class Animal
{
public void Eat()
{
Console.WriteLine("动物吃方法");
}
}
public class Horse : Animal
{
// 想在派生类中也定义一个Eat方法,则会收到一个警告信息
public void Eat()
{
Console.WriteLine("马吃的方法");
}
}
如果不使用new 关键字,在派生类中定义一个与基类成员同名的成员,编译器将产生警告信息。
Horse.Eat0将隐藏继承的成员”Animal.Eat()",若要使当前成员重写该实现,请添动加关键字override,否则,添加关键字new.
在实际的软件系统中,若确实需要添加某个方法,但是该方法又与基类的方法同名,那么可以使用new 关键字把基类成员隐藏。下面的代码演示了使用new 关键字来隐藏基类成员的方法:
public class Horse : Animal
{
// 使用new关键字进行修饰,从而隐藏了基类中同名成员
public new void Eat()
{
Console.WriteLine("马吃的方法");
}
}
如果此时仍然想访问基类的成员,则可使用强制类型转换,把子类强制转换成基类类型,从而访问隐藏的基类成员(类型转换的内容将在第10章中介绍)。具体的实现代码如下:
class Program
{
static void Main(string[] args)
{
// 调用Horse中Eat方法
Horse horse = new Horse();
horse.Eat();
// 调用基类的Eat方法
((Animal)horse).Eat();
Console.Read();
}
}
5.4 所有类的父类:System.Object
在C#中,所有的类都派生自System.Object 类。如果定义的类没有指定任何基类,编译器就会自动把Object 类当作它的基类。和其他类一样,System.Object 类也定义了一组共有的成员,其定义如下:
public class Object
{
// 方法
// 构造函数
public Object();
// 虚成员,子类可以重写这些方法
public virtual bool Equals(object obj);
protected virtual void Finalize();
public virtual int GetHashCode();
public virtual string ToString();
// 实例成员
public Type GetType();
protected object MemberwiseClone();
// 静态成员
public static bool Equals(object objA, object objB);
public static bool ReferenceEquals(object objA, object objB);
}
本章详细介绍了C#中面向对象的3个特性——封装、继承和多态。通过这些内容,我们了解了将字段定义为私有的原因,学习了如何去继承一个类,以及如何去覆写和隐藏基类成员。最后,本章还简单地介绍了.NET中所有类的父类——System.Object 。