这是「C# 学习加固」系列的第 3 篇,共 5 篇;可通过文末的系列导航继续阅读。
23. 面向对象概念#
面向对象是最主流的编程思想
核心就是把相关数据和行为组织成一个整体来看待
比如我们学习的结构体,我们将英雄的数据(攻击力、生命值等)与英雄的行为(移动、攻击等)包装到一个结构体中,这个过程就是抽象
其中“英雄”是类型,而具体的某个英雄“大法师”是对象,所以我们下节课就会学习非常重要的概念:“类与对象”
所以最重要的两件事情要明白:
- 分类:对游戏中很多食物进行分类包装,比如小兵、英雄等等,大多情况下我们将较为相同的单位归为同一个类型
- 对象:生活中“狗”是一个类型,而具体的“大黄”才是实体,也就是对象,所以生成对象也叫“实例化”
24. 类与对象基本语法#
和结构体非常类似,但是struct要改为class,后期我们会详细介绍结构体与类的区别
25. 继承#
封装#
将事物的数据、行为抽象成类,这个过程就是封装。并且我们自由的决定哪些成员对外公开或隐藏,所以上节课我们的行为其实叫封装
也可以理解为:类的产生过程就是封装
继承#
继承的核心意义是在结构上获得父类中的所有成员
比如,动物都可以吃,猫、狗等继承动物后,也具备了吃的能力
父类也叫基类,子类也叫派生类
支持多层继承,但是一个类只能有一个父类(比如动物-狗-柯基)
26. 继承后的构造函数#
*基类中存在的构造函数,子类必须也做对应的实现
this#
- this表示当前类型的对象
base#
- base也是当前类型的对象
- 但是在程序上他的类型的基类,所以一般用来专门访问基类成员的
- 一般情况下,是不需要用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年的一次会议上名为“数据的抽象与层次”的演说中首先提出。
概念#
里氏转换主要是:子类和父类的类型转换
- 一个猫也是一个动物,一定的
- 一个动物可能是一个猫,可能但不一定的
应用场景#
假设我们提供一个函数,让动物作为参数,然后间接调用动物的Cry函数,我们应该如何实现?
我们要解决的是,我们具体的对象都是Cat、KejiDog等类型,那岂不是每一种类型都要提供一个函数作为重载?
结论#
隐式转换,子类转父类,没有风险的,内容一定兼容
显式转换,父类转子类,有风险,要确保类型兼容
- Cat cat = (Cat)animal ,转换过程就可能报错
- 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!");
}
}
}