C# 和 Unity 核心概念快速指南:目的与应用

目录

1. 泛型(Generics):万能容器与操作者

2. virtual 虚方法:预留定制空间

3. base 关键字:回溯父类

4. System.Type:代码的“身份证”

5. 单例模式:全局唯一管理者

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):获取 MyClassSystem.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):子类无缝替换父类

  • 是什么?

    • 面向对象设计原则之一:如果 ST 的子类型,那么在任何使用 T 类型对象的地方,都应该可以用 S 类型的对象来替换,且程序行为保持正确。

  • 有什么用?

    • 保证程序稳定性: 确保继承关系是逻辑上合理的,避免子类“破坏”父类的预期行为。

    • 提高代码可维护性: 允许你基于父类接口编写通用代码,无需关心具体的子类实现。

    • 促进扩展性: 添加新子类时,不影响现有代码的正确性。

  • 示例体现(本文单例模式):

    • GameManager (子类) 继承 SingletonMono<GameManager> (父类),并在 Awake() 中通过 base.Awake() 调用父类逻辑,确保了单例的唯一性机制不被破坏。当你在代码中通过 SingletonMono<T>MonoBehaviour 引用来操作 GameManager 实例时,其核心单例行为是可预测和正确的,符合 LSP。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值