Unity单例模式(带你从C#追本溯源)

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

“面对复杂代码,无须畏难。任何宏大的工程,本质上都是微小单元的精密嵌合。我们只需拆解出最小作用单元,理清层级间的逻辑脉络,配合功能解构与模块排除法逆向推导,万千乱绳,终将迎刃而解。”

要介绍Unity里的单例模式,就要先介绍C#里的单例模式,下面就是由C#到Unity的循序渐进的说明

1、C#里的单例模式

要求:该类只能一有个实例,且不能在外部实例化,直接通过类名名称就能得到唯一的对象并提供一个全局访问点来获取该实例。

class Test
{
    private static Test instance=new Test();//提供唯一的静态的实例,同时私有化,防止外部修改
    
    public static Test Instance
    {
        get
        {
            return instance;//public提供公共访问接口,访问instance,达到只能访问不能修改的目的
        }
    }
    //私有构造函数,防止外部创建新的对象
    private Test()
    {
        
    }
}

要介绍单例模式,就要介绍static成员属性私有构造函数

static目的:提供唯一性,因为静态变量/实例和程序同生共死,直到程序结束后才释放。

成员属性:解决3P的局限性(public、protected 、private),这三个只是设置了访问的权限,无法达到只访问,不能修改的目的。

私有构造函数:防止外部创建新的对象。

注意事项

静态函数中不能用非静态成员

非静态函数中可以使用静态成员

--------------------------------------------------

静态类中不能使用非静态函数 成员

非静态类中可以使用 静态函数 成员

如何记忆:静态类是全局性的,类似工具的通用性,放在哪里都能用。所以静态函数中必须是静态成员,静态类中必须是静态函数。而非静态函数则有没有静态成员都行。

实际运用:

using System;
namespace ConsoleApp2
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");
            Test.T.x = 100;
            Console.WriteLine(Test.T.x);
        
        }
    }
    //单例模式
    //单例模式是一种设计模式,确保一个类只有一个实例,并提供全局访问点。
    //练习题:请设计一个单例类,要求该类只能有一个实例,且不能在外部实例化,直接通过类名名称就能得到唯一的对象并提供一个全局访问点来获取该实例。
    class Test
    { 
        private static Test t=new Test();//在类内部创建一个静态对象,并且实例化,保证了在类加载时就创建了唯一的实例
        //Unity里由于继承了MonoBehaviour,所以不能直接使用构造函数来创建对象。
        public int x;
        private Test()//构造函数私有化,外部无法创建对象
        { 
        }
        public static Test T//提供全局访问点,通过静态属性返回唯一实例,希望外部得,又不希望外部修改,所以只提供get访问器
        {
            get { return t; }

        }
    }
}

在Unity中,会有很多模块,比如ObjectManager、MusicManager

public class ObjectManager
{
    private static ObjectManager instance;
    public static ObjectManager GetInstance()
    {
        if(instance ==null)
            instance =new ObjectManager();
        return instance;
    }
}
public class MusicManager
{
    private static MusicManager instance;
    public static MusicManager GetInstance()
    {
        if(instance ==null)
            instance =new ObjectManager();
        return instance;
    }
}

为了更方便则写成泛型模式

public class BaseManager<T> where T:new();//泛型约束,这里的:相当于have,就是where T have new();T 必须有无参构造函数
//对应的是 instance =new T();另外T不能直接说是class,因为class也有有参的。

{
    private static T instance;
    
    public static  T GetInstance()//这里写成函数或者实例(public static T Instance{})其实都是一样的目的,都是为了得到上面的instance;
    {
        if(instance ==null)
            instance =new T();//如果没有就新建一个
        return instance;
    }
    //私有构造函数,防止外部创建新的对象
    private Test()
    {
        
    }
}

实际使用时,只要继承这一基类就行

public class ObjectManager:BaseManager<GameManager>
{
    
}
public class MusicManager:BaseManager<GameManager>
{
    
}
public class Test
{
    void Main()
    {
        ObcectManager.GetInstance();
        MusicManager.GetInstance();
    }
}

2、Unity里的单例模式

注意:继承了MonoBehaviour的脚本不能再新建对象,只能通过拖动对象或者加脚本的api(AddComponent)

在介绍实际常用的模式前,先介绍一般类型的脚本

public class SoundManager:MonoBehaviour
{
    private static SoundManager instance;
    public static SoundManager GetInstance()
    {
        return instance;
    }
    void Awake()
    {
       instance =this;
    }
}
public class MusicManager:MonoBehaviour
{
    private static MusicManager instance;
    public static MusicManager GetInstance()
    {
        return instance;
    }
    void Awake()
    {
       instance =this;
    }
}

每一次写新的管理器,都要把 public static 自己的类名 Instance;Awake() { Instance = this; } 这几行毫无技术含量的废话重新手敲一遍。如果项目里有 20 个管理器,这几十行代码就要重复 20 次!

所以就需要泛型模式

// 1. 必须加上 where T : MonoBehaviour (或者 where T : class),这样 as 关键字才合法
public class SingleMono<T> : MonoBehaviour where T : MonoBehaviour 
{
    private static T instance; 
    
    public static T GetInstance()
    {
        return instance;
    }
    
    void Awake()
    {
        // 报错❌:无法隐式转换,因为编译器不知道 T 和 SingleMono 的确切关系
        // instance = this; 
        
        // 正确✅:有了上面的 where 约束,这里用 as T 进行安全强转就完全合法了!
        instance = this as T; 
    }
}

注意:这里的instance是指继承了SingleMono<T>的子类,如

// GameManager 继承时,把自己的类型传给了 <T>
public class GameManager : SingleMono<GameManager>
{
    
}

虽然这个变量确实“属于”父类,但它的“数据类型”被设定为了子类

  • “写在哪里”: 它确实写在父类 SingleMono<T> 里面。这就好比父类建了一个空盒子。

  • “类型是什么”: 它的类型是 T

  • 当写下 class GameManager : SingleMono<GameManager> 时,泛型 T 就被替换成了 GameManager

  • 此时在内存里,这行代码实际上变成了: private static GameManager instance;

结论: 父类确实拥有 instance 这个变量(盒子是父类的),但是父类给这个盒子贴了一个标签:“里面只能装 GameManager(子类)这种类型的东西”

所以,当编译器看到单词 this 时,它理所当然地认为:“this 代表当前类的实例,所以 this 的数据类型就是 SingleMono<T>(父类)!”

3、另外,如果没有GameManager去继承基类,那基类里的instance=this as T为什么不会报错?

答案是:不会报错。因为编译器在检查泛型基类时,看的是“契约”,而不是“具体的人”。

我们可以把 C# 编译器检查代码的过程想象成一个**“双重审核”**机制。

当你写下 SingleMono<T>.cs 并保存时,即使整个项目里没有任何一个类去继承它,编译器也会对这个文件进行独立编译。

此时,SingleMono<T> 在 C# 中被称为**“开放泛型类型(Open Generic Type)。它就像一张印着填空题的空白表格。

编译器在审核这张空白表格时,它根本不关心未来填进 <T> 里的字是 GameManager 还是 UIManager。它只按照语法规则和泛型约束来做逻辑推演。

1、为什么不报错?

因为编译器检查 SingleMono<T> 时,依赖的是 where T : MonoBehaviour 这个契约,而不是某个具体的子类。只要契约逻辑通顺,语法就合法。

2、没有子类会怎样?

这张合法完美的“单例图纸”就会静静地躺在你的项目文件夹里,由于没有任何实体去实现它并挂载到场景中,它的 Awake 方法到宇宙毁灭都不会被执行一次。

4、最终框架

using UnityEngine;

// 泛型单例基类模板(限制 T 必须是 MonoBehaviour)
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;

    // 公开的全局访问点
    public static T Instance
    {
        get
        {
            // 1. 【按需懒加载】如果当前没有实例,尝试去场景里找,或者自动创建
            if (instance == null)
            {
                instance = Object.FindAnyObjectByType<T>();

                if (instance == null)
                {
                    // 如果场景里真没有,就自动捏造一个带同名组件的 GameObject
                    GameObject obj = new GameObject(typeof(T).Name);
                    instance = obj.AddComponent<T>();
                }
            }
            return instance;
        }
    }

    // 设为 protected virtual,允许子类重写自己的 Awake
    protected virtual void Awake()
    {
        // 2. 【防重复去重】如果你手动把脚本拖到了场景里,Awake 会先执行
        if (instance == null)
        {
            // 如果我是第一个来的,上位坐好
            instance = this as T;
            
            // 3. 【跨场景保命】切换场景时不销毁我自己和挂载我的 GameObject
            DontDestroyOnLoad(this.gameObject);
        }
        else if (instance != this as T)
        {
            // 如果发现已经有前辈存在了,说明我是切换场景时多出来的“冒牌货”,立刻自爆
            Destroy(this.gameObject);
        }
    }
}

极简实战:

using UnityEngine;

// 把自己的类名 InventoryManager 传给泛型 T
public class InventoryManager : SingletonMono<InventoryManager>
{
    // 子类自己的业务变量
    public int woodCount = 0;
    public int seedCount = 0;

    // 如果子类也需要 Awake 初始化,千万别忘了调用 base.Awake()
    protected override void Awake()
    {
        base.Awake(); // 让基类先执行单例的赋值和去重逻辑
        
        Debug.Log("背包系统初始化完毕!");
    }

    // 子类自己的业务方法
    public void AddSeeds(int amount)
    {
        seedCount += amount;
        Debug.Log($"获得种子!当前种子数量:{seedCount}");
    }
}

这里使用了懒加载模式,继承单例基类模板的InventoryManager直接调用即可

// 砍树或买种子后,直接通过单例增加物品
InventoryManager.Instance.AddSeeds(5);

开发板推荐:天空星STM32F407VET6开发板

超高性价比 STM32主控 | 超高主频 | 一板兼容百芯 | 比赛神器 | 沉金彩色丝印

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值