6. 里氏替换原则(Liskov Substitution Principle - LSP):子类无缝替换父类
1. 泛型(Generics):万能容器与操作者
-
是什么?
-
一种编写类型无关代码的方式。用一个占位符
T
来代表未来要使用的具体数据类型。
-
-
有什么用?
-
代码复用: 写一次代码,就能处理多种类型的数据,避免重复编写。
-
类型安全: 编译时就能检查类型错误,而不是等到运行时。
-
性能: 对于值类型,避免装箱拆箱,提高效率。
-
示例:
List<T>
可以是List<int>
、List<string>
等。
-
代码示例(泛型容器):
public class Box<T> // T 是一个类型占位符
{
public T Content; // 可以存储任何 T 类型的内容
public Box(T item)
{
Content = item;
Debug.Log($"创建了 {typeof(T).Name} 类型的盒子。");
}
}
// 用法示例:
public class GenericUsage : MonoBehaviour
{
void Start()
{
Box<int> intBox = new Box<int>(123); // T 此时是 int
Box<string> stringBox = new Box<string>("Hello"); // T 此时是 string
Debug.Log($"整数盒子的内容: {intBox.Content}");
Debug.Log($"字符串盒子的内容: {stringBox.Content}");
}
}
泛型约束 (where T : ...
):
-
是什么?
-
限制
T
必须满足的条件。
-
-
有什么用?
-
确保操作合法: 保证泛型代码能安全地对
T
执行特定操作。 -
提高类型安全: 编译时就能检查
T
是否符合要求。 -
常见约束:
class
(引用类型),struct
(值类型),new()
(有无参构造函数),MyBaseClass
(基类),IMyInterface
(接口)。 -
Unity 特有:
where T : MonoBehaviour
(T 必须是 MonoBehaviour 或其子类)。
-
代码示例(泛型约束):
// 限制 T 必须是 MonoBehaviour 或其子类
public class ComponentProcessor<T> : MonoBehaviour where T : MonoBehaviour
{
public void ProcessComponent(GameObject targetObject)
{
T component = targetObject.GetComponent<T>(); // 可以安全调用 GetComponent<T>()
if (component != null)
{
Debug.Log($"处理了 {targetObject.name} 上的 {typeof(T).Name} 组件。");
}
else
{
Debug.LogWarning($"{targetObject.name} 上没有 {typeof(T).Name} 组件。");
}
}
}
// 用法示例:
public class ConstraintUsage : MonoBehaviour
{
public GameObject playerGO; // 拖入一个游戏对象
void Start()
{
// 实例化一个处理 Rigidbody 的处理器
ComponentProcessor<Rigidbody> rbProcessor = gameObject.AddComponent<ComponentProcessor<Rigidbody>>();
rbProcessor.ProcessComponent(playerGO); // 尝试处理 playerGO 上的 Rigidbody
// 实例化一个处理 Collider 的处理器
ComponentProcessor<Collider> colliderProcessor = gameObject.AddComponent<ComponentProcessor<Collider>>();
colliderProcessor.ProcessComponent(playerGO); // 尝试处理 playerGO 上的 Collider
}
}
2. virtual
虚方法:预留定制空间
-
是什么?
-
在父类中声明一个方法,允许子类选择性地使用
override
关键字提供自己的实现。
-
-
有什么用?
-
多态性: 允许你用父类引用调用方法,但实际执行的是对象在运行时的真实类型所定义的方法。
-
提供默认行为: 父类可以有默认实现,子类按需修改。
-
示例: 基础敌人有
Attack()
,近战敌人可以重写Attack()
实现近战逻辑。
-
代码示例:
// 父类:敌人
public class Enemy : MonoBehaviour
{
public virtual void Attack() // virtual 关键字:允许子类重写
{
Debug.Log($"{gameObject.name}: 执行普通攻击!"); // 默认攻击行为
}
}
// 子类:近战敌人
public class MeleeEnemy : Enemy
{
public override void Attack() // override 关键字:重写父类的 Attack 方法
{
Debug.Log($"{gameObject.name}: 挥舞大刀进行近战攻击!"); // 近战特有攻击行为
}
}
// 子类:远程敌人
public class RangedEnemy : Enemy
{
public override void Attack() // override 关键字:重写父类的 Attack 方法
{
Debug.Log($"{gameObject.name}: 射出弓箭进行远程攻击!"); // 远程特有攻击行为
}
}
// 用法示例(演示多态):
public class PolymorphismTest : MonoBehaviour
{
public Enemy basicEnemy; // 关联 Enemy 脚本的对象
public Enemy meleeEnemy; // 关联 MeleeEnemy 脚本的对象
public Enemy rangedEnemy; // 关联 RangedEnemy 脚本的对象
void Start()
{
// 即使变量类型是 Enemy,实际调用的也是各自重写后的 Attack 方法
basicEnemy.Attack(); // 输出: BasicEnemy: 执行普通攻击!
meleeEnemy.Attack(); // 输出: MeleeEnemy: 挥舞大刀进行近战攻击!
rangedEnemy.Attack(); // 输出: RangedEnemy: 射出弓箭进行远程攻击!
}
}
3. base
关键字:回溯父类
-
是什么?
-
用于在子类中访问其直接父类(基类)的成员。
-
-
有什么用?
-
调用父类构造函数: 子类构造函数初始化前,调用父类的构造函数 (
: base(...)
)。 -
执行父类方法: 在子类重写的方法内部,可以调用父类的原始实现 (
base.Method()
)。这允许子类在扩展行为的同时,保留父类的基础逻辑。
-
代码示例:
// 父类:角色
public class Character : MonoBehaviour
{
public Character() { Debug.Log("Character 构造函数被调用。"); }
public virtual void Move()
{
Debug.Log($"{gameObject.name}: 角色正在移动。"); // 基础移动逻辑
}
}
// 子类:玩家
public class Player : Character
{
// 调用父类构造函数 (通常用于非 MonoBehaviour 类)
public Player() : base() { Debug.Log("Player 构造函数被调用。"); }
public override void Move() // 重写 Move 方法
{
base.Move(); // 调用 Character 基类的 Move 方法,执行基础移动逻辑
Debug.Log($"{gameObject.name}: 玩家执行了额外的冲刺动作。"); // 玩家特有移动逻辑
}
}
// 用法示例:
public class BaseKeywordTest : MonoBehaviour
{
void Start()
{
Player player = new GameObject("Player").AddComponent<Player>();
player.Move();
// 期望输出:
// Player: 角色正在移动。 (来自 base.Move())
// Player: 玩家执行了额外的冲刺动作。 (来自 Player 自己的 Move())
}
}
4. System.Type
:代码的“身份证”
-
是什么?
-
一个类,包含了关于类型本身的所有元数据信息(例如,它的名称、它属于什么命名空间、它有什么方法、属性等)。
-
-
有什么用?
-
运行时类型检查: 在程序运行时获取任何类型的详细信息。
-
反射(Reflection): 动态地创建对象,调用方法,访问属性。
-
常见用法:
-
typeof(MyClass)
:获取MyClass
的System.Type
对象。 -
.ToString()
:将System.Type
对象转换为其完整名称字符串。 -
obj.name = typeof(T).ToString();
:将GameObject
的名字设置为泛型T
所代表的类型的名称。
-
-
代码示例 (typeof(T).ToString()
):
using UnityEngine;
public class Namer<T> : MonoBehaviour where T : class // Namer 泛型类
{
void Start()
{
// obj 是这个脚本所附加的游戏对象
// typeof(T).ToString() 会得到 T 这个具体类型的字符串名称
// 例如,如果 T 是 GameManager,那么 obj.name 就会被设置为 "GameManager"
this.gameObject.name = typeof(T).ToString();
}
}
// 示例类型1:一个普通的 C# 类
public class MyGameSettings {}
// 示例类型2:一个 MonoBehaviour 脚本
public class GameFlowManager : MonoBehaviour {}
// 用法示例:
public class NamingTest : MonoBehaviour
{
void Start()
{
// 创建一个用于 MyGameSettings 的命名器
// 这里的 T 就是 MyGameSettings
GameObject settingsObj = new GameObject();
settingsObj.AddComponent<Namer<MyGameSettings>>(); // 附加脚本,T = MyGameSettings
// 运行后,settingsObj 的名字将变为 "MyGameSettings"
// 创建一个用于 GameFlowManager 的命名器
// 这里的 T 就是 GameFlowManager
GameObject flowObj = new GameObject();
flowObj.AddComponent<Namer<GameFlowManager>>(); // 附加脚本,T = GameFlowManager
// 运行后,flowObj 的名字将变为 "GameFlowManager"
}
}
5. 单例模式:全局唯一管理者
-
是什么?
-
一种设计模式,确保一个类在应用程序中只有一个实例,并提供一个全局唯一的访问点来获取这个实例。
-
-
有什么用?
-
全局数据/功能: 适用于需要全局访问和控制的管理器类,如游戏管理器、音频管理器、UI 管理器等。
-
资源控制: 避免重复创建和管理昂贵的资源。
-
-
Unity 中
MonoBehaviour
单例的特殊性:-
不能直接
new
一个MonoBehaviour
实例。它必须作为组件附加到GameObject
上。 -
需要考虑跨场景不销毁 (
DontDestroyOnLoad
) 和重复实例处理。
-
代码示例(泛型 MonoBehaviour
单例):
using UnityEngine;
// 泛型单例基类,用于继承 MonoBehaviour 的单例
// T 必须是 MonoBehaviour 或其子类
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance; // 存储单例实例
public static T Instance // 公共静态属性,用于获取单例
{
get
{
if (_instance == null) // 如果实例不存在
{
_instance = FindObjectOfType<T>(); // 尝试在场景中查找现有实例
if (_instance == null) // 如果还是没有找到
{
GameObject singletonObject = new GameObject(); // 创建新的游戏对象
singletonObject.name = typeof(T).ToString() + " (Singleton)"; // 根据 T 的名字命名
_instance = singletonObject.AddComponent<T>(); // 添加 T 类型的组件
DontDestroyOnLoad(singletonObject); // 防止场景切换时销毁
Debug.Log($"[Singleton] 新创建了单例: {singletonObject.name}");
}
else
{
Debug.Log($"[Singleton] 找到已存在的单例: {_instance.name}");
DontDestroyOnLoad(_instance.gameObject); // 确保已存在的也不销毁
}
}
return _instance;
}
}
// Awake 方法:用于处理重复实例和确保唯一性
protected virtual void Awake()
{
if (_instance == null) // 如果当前是第一个实例
{
_instance = this as T; // 设为单例
DontDestroyOnLoad(gameObject); // 防止销毁
Debug.Log($"[Singleton] Awake: '{gameObject.name}' 被确认为单例。");
}
else if (_instance != this) // 如果已存在另一个实例
{
Debug.LogWarning($"[Singleton] Awake: 场景中已存在 '{typeof(T).Name}' 的单例。销毁重复实例:'{gameObject.name}'。");
Destroy(gameObject); // 销毁重复的自己
}
}
}
// 实际的 GameManager 单例
public class GameManager : SingletonMono<GameManager> // 继承单例基类,T 就是 GameManager
{
public int score = 0;
// 可以重写 Awake 并调用 base.Awake()
protected override void Awake()
{
base.Awake(); // 确保单例唯一性逻辑执行
if (Instance == this) // 如果当前实例是唯一的
{
Debug.Log("[GameManager] 游戏管理器初始化。");
}
}
public void AddScore(int amount)
{
score += amount;
Debug.Log($"分数: {score}");
}
}
// 另一个脚本访问单例
public class GamePlay : MonoBehaviour
{
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
GameManager.Instance.AddScore(10); // 通过 GameManager.Instance 访问单例
}
}
}
6. 里氏替换原则(Liskov Substitution Principle - LSP):子类无缝替换父类
-
是什么?
-
面向对象设计原则之一:如果
S
是T
的子类型,那么在任何使用T
类型对象的地方,都应该可以用S
类型的对象来替换,且程序行为保持正确。
-
-
有什么用?
-
保证程序稳定性: 确保继承关系是逻辑上合理的,避免子类“破坏”父类的预期行为。
-
提高代码可维护性: 允许你基于父类接口编写通用代码,无需关心具体的子类实现。
-
促进扩展性: 添加新子类时,不影响现有代码的正确性。
-
-
示例体现(本文单例模式):
-
GameManager
(子类) 继承SingletonMono<GameManager>
(父类),并在Awake()
中通过base.Awake()
调用父类逻辑,确保了单例的唯一性机制不被破坏。当你在代码中通过SingletonMono<T>
或MonoBehaviour
引用来操作GameManager
实例时,其核心单例行为是可预测和正确的,符合 LSP。
-