对象池与享元模式

在我人生的第一次面试中,面试官问我:

可以举几个例子吗?关于Unity中可能用到的设计模式。

我当时对设计模式的学习并没有很深入,因此回答了:对象池应该是享元模式。

但很快遭到了他的否决,我再次提议,应该是享元,但他还是摇了摇头。

毕竟是公司主程,我只能低下头表示,自己会回去看看。

在9个月之后的下午,公司项目中,我被分到的功能,需要使用尘封已久的对象池,我就心血来潮写下来这篇文章

来纠正我当时的错误

关于享元模式与对象池的区别

我比较喜欢举例子,其实两者的区别很大

区别一:对象的使用

比如-127~128的int,如果是享元模式,那当我们去取"0"的时候,就是把原原本本的0给你了;

如果是对象池,那当我们去取"0"的时候,会去池中看看我还有没有多余的"0",有就给你,没就new一个

区别二:对象的状态(内部与外部)

这就没有简单的例子了

比如租房,房子是内部,是不变的,中介是外部的,是你去租房时,传给房子的参数

如果是享元模式,当我们带笨笨中介去房源时,就会得到1000的房租,而狡猾中介是2333
当然内部数据也是能修改的,比如我给房子刷上超级油漆,那带不同外部中介去,就会得到不同房租

而对象池,你带中介去,他说房源有人正在看,给你现场造个房子,全是外部数据的影子

区别三:根本就不是一种存在

区别二里也看得出来,是不可能去现场造房子的

享元模式是一种数据处理用的模式,属于结构型模式,注重外部对象与内部对象之间的衔接。

对象池是一种对象使用方式,即构造型模式,注重对象的使用

相同点

都是为了优化内存而生的

3/15补充

今天又看到了一个好玩的例子

比如我这有一片树林,我准备创建一棵树。

如果是对象池,我会从我的仓库中拿出一颗树,更改它的属性后放在这。如果我的仓库中没有我想要的树先创建一棵树再拿出来。

如果是享元模式,我会找到树林的母树,再照着母树一颗树。
这时我之后的行为可分为两种:
一种是类似于使用不同颜色的画笔画树,以此得到不同的纸片(改变外部属性),
一种是类似于砍倒这棵母树(影响内部属性或改变行为)。
进行前者时一切正常,只有我操作的物体才会受到影响,
而后者则会让后来者再画树时,得到完全不一样的图案。

对象池源码

对象池单例类

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

interface IResetable
{
    void OnReset();
}
/*
 * 使用方法
 * 1.	所有频繁创建回收的物体,用对象池创建回收
 *      GameObjectPool.Instance.CreatObject("类型", 对象, 位置, 旋转);
 *      GameObjectPool.Instance.CollectObject(对象);
 * 2.	需要创建对象时执行,如果每次创建都需要对对象进行初始化,那就让对象实现IResetable接口。
 */
public class GameObjectPool : MonoSingleton<GameObjectPool>
{
    private Dictionary<string, List<GameObject>> cache;

    //初始化
    public override void Init()
    {
        base.Init();
        cache = new Dictionary<string, List<GameObject>>();
    }

    /// <summary>
    /// 为场景显示物体
    /// </summary>
    /// <param name="key">物体类别</param>
    /// <param name="prefab">物体预制体</param>
    /// <param name="pos">创建时位置</param>
    /// <param name="rotate">创建时角度</param>
    /// <returns></returns>
    public GameObject CreatObject(string key, GameObject prefab, Vector3 pos, Quaternion rotate)
    {
        GameObject go = FindUsableGO(key);
        if (go == null) go = AddObject(key, prefab);
        //设置物体并return
        go.transform.position = pos;
        go.transform.rotation = rotate;
        go.SetActive(true);
        foreach (var item in go.GetComponents<IResetable>())
        {
            item.OnReset();
        }
        return go;
    }

    //找到可用的物体
    private GameObject FindUsableGO(string key)
    {
        if (cache.ContainsKey(key))
            return cache[key].Find(g => !g.activeInHierarchy);
        return null;
    }

    //添加物体
    private GameObject AddObject(string key, GameObject prefab)
    {
        GameObject go = Instantiate(prefab);
        if (!cache.ContainsKey(key)) cache.Add(key, new List<GameObject>());
        cache[key].Add(go);
        return go;
    }

    /// <summary>
    /// 回收物体至对象池
    /// </summary>
    /// <param name="go">物体</param>
    /// <param name="delay">延迟时间</param>
    public void CollectObject(GameObject go, float delay = 0)
    {
        go.SetActive(false);
    }

    /// <summary>
    /// 移除该类别
    /// </summary>
    /// <param name="key">类别</param>
    public void Clear(string key)
    {   //如果用for,记得从后往前删,list会自动补上
        foreach (GameObject item in cache[key])
        {
            Destroy(item);
        }
        cache.Remove(key);
    }

    /// <summary>
    /// 移除对象池所有物体
    /// </summary>
    public void ClearAll()
    {
        //foreach 只读元素
        //遍历 字典 集合
        //foreach (KeyValuePair<string, List<GameObject>> item in cache)
        //{//异常:无效操作(因为把元素删了,IEnumerable需要用被删的那个元素去MoveNext())
        //    Clear(item.Key);//删除了字典记录,cache.Remove(key)
        //}

        //做法就是建一个新的,和删掉的元素不相干
        List<string> keys = new List<string>(cache.Keys);//之所以能赋值,是因为他们都有IEnumerable
        foreach (string item in keys)
        {
            Clear(item);//删了字典里的key,列表里的key没动。
        }
    }
}
posted @ 2024-03-15 20:33  被迫吃冰淇淋的小学生  阅读(87)  评论(0)    收藏  举报