Learning Hard C# 学习笔记: 5.C#中的面向对象编程

发布时间 2023-10-05 22:05:25作者: ErgoCogito

目前除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 。