跳过正文
  1. 文章/

C# 学习加固笔记(三):继承与多态

Lickba
作者
Lickba
热爱游戏,也喜欢记录创作过程与踩过的坑。
C# 学习加固 - 这篇文章属于一个选集。
§ 3: 本文

这是「C# 学习加固」系列的第 3 篇,共 5 篇;可通过文末的系列导航继续阅读。

23. 面向对象概念
#

面向对象是最主流的编程思想

核心就是把相关数据和行为组织成一个整体来看待

比如我们学习的结构体,我们将英雄的数据(攻击力、生命值等)与英雄的行为(移动、攻击等)包装到一个结构体中,这个过程就是抽象

其中“英雄”是类型,而具体的某个英雄“大法师”是对象,所以我们下节课就会学习非常重要的概念:“类与对象”

所以最重要的两件事情要明白:

  1. 分类:对游戏中很多食物进行分类包装,比如小兵、英雄等等,大多情况下我们将较为相同的单位归为同一个类型
  2. 对象:生活中“狗”是一个类型,而具体的“大黄”才是实体,也就是对象,所以生成对象也叫“实例化”

24. 类与对象基本语法
#

和结构体非常类似,但是struct要改为class,后期我们会详细介绍结构体与类的区别

25. 继承
#

封装
#

将事物的数据、行为抽象成类,这个过程就是封装。并且我们自由的决定哪些成员对外公开或隐藏,所以上节课我们的行为其实叫封装

也可以理解为:类的产生过程就是封装

继承
#

  1. 继承的核心意义是在结构上获得父类中的所有成员

    比如,动物都可以吃,猫、狗等继承动物后,也具备了吃的能力

  2. 父类也叫基类,子类也叫派生类

  3. 支持多层继承,但是一个类只能有一个父类(比如动物-狗-柯基)

26. 继承后的构造函数
#

*基类中存在的构造函数,子类必须也做对应的实现

this
#

  1. this表示当前类型的对象

base
#

  1. base也是当前类型的对象
  2. 但是在程序上他的类型的基类,所以一般用来专门访问基类成员的
  3. 一般情况下,是不需要用base的,因为默认就可以访问基类成员

*但是实际上在执行上this和base是同一个对象

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        Animal animal = new Animal("动物");
        animal.Name = "小动物";
        animal.Eat();

        Dog daHuang = new Dog("大黄","柯基");
        Debug.Log(daHuang.Name);
        daHuang.Eat();

        Cat xiaoBai = new Cat("小白");
        Debug.Log(xiaoBai.Name);
        xiaoBai.Eat();
    }

    // 基类/父类
    public class Animal
    {
        public string Name;

        public Animal(string name)
        {
            Name = name;
        }

        // 函数也是对象的成员
        public void Eat()
        {
            Debug.Log($"{Name}Eating");
        }
    }

    // 派生类/子类
    public class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;
        // 我们已经约束了,实例化一个Animal必须有名字
        // 但是这里出现了冲突,因为实例化一个Dog却没有名字

        // this:谁调用了这个函数,谁就是this
        // base:当前类的基类
        // this和base都是对象,但是程序上认为这个结构(类)不一样
        // 要调用当前类型的成员,用this
        // 要调用父类的成员,用base,但是一般不太会用,除非需要特别声明是基类中
        public Dog(string name,string dogType):base(name)
        {
            // base.dogType 不能通过base访问当前类的成员
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }
    }

    public class Cat : Animal
    {
        public Cat(string name) : base(name)
        {
        }
    }
}

27. 多态-虚函数
#

多态
#

先有封装,才有继承

先有继承,才有多态

多态:字面意思,多种形态,同一操作的不同行为,这里的操作可以理解为函数,举例:

父类类动物中有一个“叫”函数,子类猫,狗当然也可以叫,但是实际上猫狗的叫声并不相同,但是也许他们具备一些共同点,比如张嘴、闭嘴

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        Dog daHuang = new Dog("大黄","汪汪汪!","柯基");
        Cat xiaoBai = new Cat("小白","喵喵喵~");
        daHuang.Cry();
        xiaoBai.Cry();
    }

    // 基类/父类
    public class Animal
    {
        public string Name;
        public string CryString;

        public Animal(string name, string cryString)
        {
            Name = name;
            CryString = cryString;
        }

        // 虚函数
        // 1.提供默认逻辑,因为有一些子类不想要重写可以不重写
        // 2.提供可选的逻辑
        public virtual void Cry()
        {
            // 下列是可选的
            // 可选的:就是子类想要就可以要,不想要也可以不要
            Debug.Log($"{Name}张嘴了!");
        }
    }

    // 派生类/子类
    public class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;

        public Dog(string name,string cryString,string dogType):base(name,cryString)
        {
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }

        // 重写
        // 重载:是同名函数,但是不同参数
        // 重写:是同名函数,参数相同,但是实现不同
        public override void Cry()
        {
            Debug.Log($"{Name}开始蓄力!");
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只狗!");
        }
    }

    public class Cat : Animal
    {
        public Cat(string name,string cryString) : base(name,cryString)
        {

        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只猫!");
        }
    }

}

28. 多态-抽象类与抽象函数
#

抽象类
#

抽象:从许多事物中,舍弃个别的、非本质的属性,抽出共同的、本质的属性,叫抽象,是形成概念的必要手段

我们学习了类与对象,过程中我们会发现一些问题:

类是一种结构/概念,对象是一个实体,比如英雄是类,而大魔法师是对象。

我们之前学习的案例中有两种情况:

第一种:我们认为Animal是个类,基于这个类去实例化对象,也就是没有使用继承,是一种比较简单的结构

第二种:Animal是最上层,Cat和Dog继承Animal,最终我们实例化对象时其实是使用Cat、Dog这种类,因为我们认为如果产生一个具体的猫,我们使用猫的结构,但是我们不去直接实例化一个动物,因为动物是一个更加抽象的概念。他就应该停在概念层面。

第二种情况下,实际上是我们依然可以new一个动物,但是往往我们不希望如此,所以我们需要一种技术去避免如此,但是要注意,这种避免不是必然,而是由程序员去决定是否需要避免

避免的方式就是抽象类

避免的方式就是抽象类

抽象类:高度抽象,不需要实例化的类型,但是其子类可以实例化,一般处于继承链的上端,需要实例化对象的类型不能使用抽象类

抽象函数
#

抽象类除了无法直接实例化外,还具备定义抽象函数的功能

抽象函数:我不知道我的子类会怎么实现,所以不会像虚函数那样提供一个基本/默认实现,因为事实上我都不知道我的子类有哪些,但是我认为继承我的一定要有这些功能

先知道语法、特点,后面我们会研究这些东西的妙用

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        // Animal animal = new Animal("小动物", "吱吱吱~"); // 不允许实例化抽象类
        KeJiDog daHuang = new KeJiDog("大黄","汪汪汪!","柯基");
        Cat xiaoBai = new Cat("小白","喵喵喵~");
        daHuang.Move();
        xiaoBai.Move();
    }

    // 基类/父类
    // 抽象类
    public abstract class Animal
    {
        public string Name;
        public string CryString;

        public Animal(string name, string cryString)
        {
            Name = name;
            CryString = cryString;
        }

        // 虚函数
        // 1.提供默认逻辑,因为有一些子类不想要重写可以不重写
        // 2.提供可选的逻辑
        public virtual void Cry()
        {
            // 下列是可选的
            // 可选的:就是子类想要就可以要,不想要也可以不要
            Debug.Log($"{Name}张嘴了!");
        }

        // 抽象函数
        // 业务上存在飞翔、跑步、游泳等各种情况,我们并不知道具体的子类如何实现
        // 没有函数体
        public abstract void Move(); // 抽象方法,子类必须实现
}

    // 派生类/子类
    // 第二层,Dog也是抽象类
    // 抽象类不需要实现基类中的抽象函数(我也不知道我下面的子类会怎么实现)
    public abstract class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;

        public Dog(string name,string cryString,string dogType):base(name,cryString)
        {
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }

        // 重写
        // 重载:是同名函数,但是不同参数
        // 重写:是同名函数,参数相同,但是实现不同
        public override void Cry()
        {
            Debug.Log($"{Name}开始蓄力!");
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只狗!");
        }

        // 重写的是基类中的抽象函数
        public override void Move()
        {
            Debug.Log($"{Name}开始奔跑!");
        }
    }

    public class KeJiDog : Dog
    {
        // 他的构造函数约束是自身基类的
        public KeJiDog(string name, string cryString, string dogType) : base(name, cryString, dogType)
        {
        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"抖了抖小短腿!");
        }
        public override void Move()
        {
            base.Move(); // 调用基类的 Move 方法
            Debug.Log($"抖了抖柯基翘臀!");
        }
    }

    public class Cat : Animal
    {
        public Cat(string name,string cryString) : base(name,cryString)
        {

        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只猫!");
        }

        public override void Move()
        {
            Debug.Log($"{Name}开始跳跃!");
        }
    }
}

29. 里氏转换
#

在面向对象的程序设计中,里氏转换原则(Liskov Substitution principle)是对子类型的特别定义。它由芭芭拉·拉斯科夫(Barbara Liskov)在1987年的一次会议上名为“数据的抽象与层次”的演说中首先提出。

概念
#

里氏转换主要是:子类和父类的类型转换

  1. 一个猫也是一个动物,一定的
  2. 一个动物可能是一个猫,可能但不一定的

应用场景
#

假设我们提供一个函数,让动物作为参数,然后间接调用动物的Cry函数,我们应该如何实现?

我们要解决的是,我们具体的对象都是Cat、KejiDog等类型,那岂不是每一种类型都要提供一个函数作为重载?

结论
#

  1. 隐式转换,子类转父类,没有风险的,内容一定兼容

  2. 显式转换,父类转子类,有风险,要确保类型兼容

    1. Cat cat = (Cat)animal ,转换过程就可能报错
    2. Cat cat = animal as Cat ,转换过程不会报错,但是使用过程可能会报错
using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        // Animal animal = new Animal("小动物", "吱吱吱~"); // 不允许实例化抽象类
        KeJiDog daHuang = new KeJiDog("大黄","汪汪汪!","柯基");
        Cat xiaoBai = new Cat("小白","喵喵喵~");

        DoCry(xiaoBai); // 里氏转换-隐式转换,子类转父类
        KejiMove(daHuang); // 传递参数时依然是子类转父类
    }

    public void DoCry(Animal animal)
    {
        Debug.Log("开始叫");
        animal.Cry();
    }

    public void KejiMove(Animal animal) // 此处约定传递过来的参数要么是Animal,要么是Animal的子类通过隐式转换传递过来的
    {
        // 强制转换,会报错
        KeJiDog keji = (KeJiDog)animal; // 显式转换,父类转子类,有风险,要确保类型兼容
        keji.Move(); // 调用子类特有的方法

        //Cat cat = (Cat)animal; // 类型不兼容,会立刻报错
        //cat.Move();

        // as转换,不会报错
        Cat keji2 = animal as Cat; // 安全转换,如果类型不兼容,不会立刻报错,但是返回的是 null
        keji2?.Move(); // 使用安全转换后,调用 Move 方法前需要检查是否为 null
    }

    // 基类/父类
    // 抽象类
    public abstract class Animal
    {
        public string Name;
        public string CryString;

        public Animal(string name, string cryString)
        {
            Name = name;
            CryString = cryString;
        }

        // 虚函数
        // 1.提供默认逻辑,因为有一些子类不想要重写可以不重写
        // 2.提供可选的逻辑
        public virtual void Cry()
        {
            // 下列是可选的
            // 可选的:就是子类想要就可以要,不想要也可以不要
            Debug.Log($"{Name}张嘴了!");
        }

        // 抽象函数
        // 业务上存在飞翔、跑步、游泳等各种情况,我们并不知道具体的子类如何实现
        // 没有函数体
        public abstract void Move(); // 抽象方法,子类必须实现
        

}

    // 派生类/子类
    // 第二层,Dog也是抽象类
    // 抽象类不需要实现基类中的抽象函数(我也不知道我下面的子类会怎么实现)
    public abstract class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;

        public Dog(string name,string cryString,string dogType):base(name,cryString)
        {
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }

        // 重写
        // 重载:是同名函数,但是不同参数
        // 重写:是同名函数,参数相同,但是实现不同
        public override void Cry()
        {
            Debug.Log($"{Name}开始蓄力!");
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
        }

        // 重写的是基类中的抽象函数
        public override void Move()
        {
            Debug.Log($"{Name}开始奔跑!");
        }
    }

    public class KeJiDog : Dog
    {
        // 他的构造函数约束是自身基类的
        public KeJiDog(string name, string cryString, string dogType) : base(name, cryString, dogType)
        {
        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"抖了抖小短腿!");
        }
        public override void Move()
        {
            base.Move(); // 调用基类的 Move 方法
            Debug.Log($"抖了抖柯基翘臀!");
        }
    }

    public class Cat : Animal
    {
        public Cat(string name,string cryString) : base(name,cryString)
        {

        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只猫!");
        }

        public override void Move()
        {
            Debug.Log($"{Name}开始跳跃!");
        }
    }

}

30. 访问修饰符和类中类
#

public:公开的

private:私有的,只有自己可以访问,子类都不行

protected:自己以及子类都可以访问,但是不相关的不行

如果不写访问修饰符,默认就是private

访问:就是“对象.成员”,成员包括了成员函数和构造函数,但是构造函数一般都是public

public string Name; 	// 公开属性,可以在任何地方访问
private string age; 	// 私有属性,只能在 Animal 类内部访问
protected string color; // 受保护属性,可以在 Animal 类及其派生类中访问

类中类/嵌套类/内部类
#

也就是class中也可以定义一个class的情况

如果使用public,用 类.内部类 来访问类型,如Cat.CatData,其中CatData定义在Cat中

如果使用private,就相当于保护起来了,外界无法访问

using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        // 通过类型名.类型名去访问嵌套类
        Animal.AnimaTest animaTest = new Animal.AnimaTest();
        animaTest.Test();
    }
}

// 没有被嵌套的,只能被定义成公开的
public class Animal
{
    public void Test()
    {
        // 内部使用和平时没什么区别
        AnimaTest animaTest = new AnimaTest();
        animaTest.Test();
    }

    // 嵌套类可以修改访问修饰符
    // private:只能被嵌套自身的上级访问
    public class AnimaTest
    {
        public void Test()
        {
            Debug.Log("Hello from AnimaTest!");
        }
    }
}
C# 学习加固 - 这篇文章属于一个选集。
§ 3: 本文