字段临时缓存包装器

前言

在实际开发中,我们有时候存在一种需求,例如对于某个字段,我们希望在某个明确的保存节点前对字段的修改都仅作为缓存保留,最终是否应用这些修改取决于某些条件,比如玩家对游戏设置的修改可能需要玩家明确确认应用修改后才会保存下来,在此之前玩家在游戏界面上的所有修改都是临时的。


本文基于这个需求探索出了一种解决方案“字段临时缓存包装器”,通过创建字段或用于存储字段临时数据的数据结构的副本来实现临时缓存,虽然我们同样可以采用直接声明一个副本字段的方式来达到同样的目的,但是这可能会增加冗余代码,且不利于代码的维护,通过包装器来封装临时缓存的通用逻辑,与具体业务逻辑隔离。

代码

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Newtonsoft.Json;

/// <summary>
/// 临时包装器
/// </summary>
/// <typeparam name="T">字段类型</typeparam>
/// <remarks>
/// 该类主要用于创造某个字段的副本作为该字段的临时缓存,避免直接修改源字段。
/// </remarks>
public class TempWrapper<T> : IDisposable
{
    /// <summary>
    /// 是否为值类型
    /// <para>提示:若为true则表示包装字段为值类型,否则为引用类型</para>
    /// </summary>
    public static bool isValueType => _isValueType;

    /// <summary>
    /// 缓存字段
    /// <para>提示:对于值类型而言,该属性涉及拷贝</para>
    /// </summary>
    public T value
    {
        get
        {
            if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");
            return _value;
        }
        set
        {
            if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");
            _value = value;
        }
    }

    /// <summary>
    /// 获取引用
    /// <para>提示:对于值类型而言,该属性直接返回引用从而避免拷贝</para>
    /// </summary>
    public ref T refrence
    {
        get
        {
            if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");
            return ref _value;
        }
    }

    /// <summary>
    /// 是否已经释放
    /// </summary>
    public bool isDisposed => _isDisposed;

    static readonly bool _isValueType = typeof(T).IsValueType;
    static readonly bool _isDisposable = typeof(IDisposable).IsAssignableFrom(typeof(T));
    static readonly object _key = new object();
    T _value;
    bool _isDisposed;

    TempWrapper() { }

    /// <summary>
    /// 包装指定字段并返回包装类
    /// </summary>
    /// <param name="value">待包装字段的引用</param>
    /// <remarks>
    /// <para>提示:采用二进制序列化和反序列化生成字段副本</para>
    /// <para>提示:该方法仅可用于被 <c>Serializable</c> 标记的字段类型</para>
    /// </remarks>
    public static TempWrapper<T> WrapByBinary(ref T value)
    {
        lock (_key)
        {
            try
            {
                TempWrapper<T> wrapper = new TempWrapper<T>();

                if (_isValueType) wrapper._value = value;
                else
                {
                    using (MemoryStream ms = new MemoryStream())
                    {
                        IFormatter formatter = new BinaryFormatter();
                        formatter.Serialize(ms, value);
                        ms.Seek(0, SeekOrigin.Begin);
                        wrapper._value = (T)formatter.Deserialize(ms);
                    }
                }

                return wrapper;
            }
            catch (Exception e)
            {
                throw new InvalidOperationException("Failed to wrap.", e);
            }

        }
    }

    /// <summary>
    /// 包装指定字段并返回包装类
    /// <para>提示:采用JSON序列化和反序列化生成字段副本</para>
    /// </summary>
    /// <param name="value">待包装字段的引用</param>
    public static TempWrapper<T> WrapByJson(ref T value)
    {
        lock (_key)
        {
            try
            {
                TempWrapper<T> wrapper = new TempWrapper<T>();

                if (_isValueType) wrapper._value = value;
                else
                {
                    string jsonStr = JsonConvert.SerializeObject(value);
                    wrapper._value = JsonConvert.DeserializeObject<T>(jsonStr);
                }

                return wrapper;
            }
            catch (Exception e)
            {
                throw new InvalidOperationException("Failed to wrap.", e);
            }
        }
    }

    /// <summary>
    /// 包装生成器所生成的字段并返回包装类
    /// </summary>
    /// <param name="creator">生成器</param>
    public static TempWrapper<T> WrapByCustom(Func<T> creator)
    {
        lock (_key)
        {
            try
            {
                TempWrapper<T> wrapper = new TempWrapper<T>() { _value = creator() };
                return wrapper;
            }
            catch (Exception e)
            {
                throw new InvalidOperationException("Failed to wrap.", e);
            }
        }
    }

    /// <summary>
    /// 解包包装器并赋值给指定的字段
    /// </summary>
    /// <remarks>
    /// <para>提示:采用二进制序列化和反序列化解包</para>
    /// <para>提示:该方法仅可用于被 <c>Serializable</c> 标记的字段类型</para>
    /// </remarks>
    public void UnWrapByBinary(ref T value)
    {
        if (_isValueType) value = _value;
        else
        {
            lock (_key)
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    IFormatter formatter = new BinaryFormatter();
                    formatter.Serialize(ms, _value);
                    ms.Seek(0, SeekOrigin.Begin);
                    value = (T)formatter.Deserialize(ms);
                }
            }
        }
    }

    /// <summary>
    /// 解包包装器并赋值给指定的字段
    /// <para>提示:采用JSON序列化和反序列化解包</para>
    /// </summary>
    public void UnwrapByJson(ref T value)
    {
        if (_isValueType) value = _value;
        else
        {
            lock (_key)
            {
                string jsonStr = JsonConvert.SerializeObject(_value);
                value = JsonConvert.DeserializeObject<T>(jsonStr);
            }
        }
    }

    /// <summary>
    /// 释放包装器所包装的字段
    /// <para>提示:当所包装字段实现了IDisposable接口时该方法才有效</para>
    /// </summary>
    public void Dispose()
    {
        if (_isDisposed) return;

        DoDispose(true);
        GC.SuppressFinalize(this);
    }

    void DoDispose(bool disposing)
    {
        if (_isDisposed) return;

        _isDisposed = true;
        if (disposing && _isDisposable && _value is IDisposable ds)
            ds.Dispose();
    }

    ~TempWrapper()
    {
        DoDispose(false);
    }
}

测试

#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

// TempWrapper测试脚本
public class TempWrapperTest : MonoBehaviour
{
    [SerializeField] int[] intArray;
    [SerializeField] string[] strArray;
    [SerializeField] StructA[] structaArray;
    [SerializeField] ClassA classA;
    [SerializeField] StructA structA;
    [SerializeField] Transform tf;

    [Serializable]
    struct StructA
    {
        public string key;
        public int value;

        public override string ToString()
        {
            return $"(key:{key},value:{value})";
        }
    }

    [Serializable]
    class ClassA
    {
        public string key;
        public StructA structA;

        public override string ToString()
        {
            StringBuilder builder = new StringBuilder("[key:");
            builder.Append(key).Append(",").Append($"StructA:{structA}");
            builder.Append("]");
            return builder.ToString();
        }
    }

    TempWrapper<int[]> intArrayWrapper;
    TempWrapper<string[]> strArrayWrapper;
    TempWrapper<StructA[]> structaArrayWrapper;
    TempWrapper<ClassA> classaWrapper;
    TempWrapper<StructA> structaWrapper;
    TempWrapper<Transform> tfWrapper;

    void Awake()
    {
        intArrayWrapper = TempWrapper<int[]>.WrapByBinary(ref intArray);
        strArrayWrapper = TempWrapper<string[]>.WrapByBinary(ref strArray);
        structaArrayWrapper = TempWrapper<StructA[]>.WrapByBinary(ref structaArray);
        classaWrapper = TempWrapper<ClassA>.WrapByBinary(ref classA);
        structaWrapper = TempWrapper<StructA>.WrapByBinary(ref structA);
        tfWrapper = TempWrapper<Transform>.WrapByCustom(() => Instantiate(tf));
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            PrintWrapper();
            PrintHashCode();
            PrintWrapperHashCode();
        }

        if (Input.GetKeyDown(KeyCode.W))
        {
            WriteWrapper();
            UnWrap();
        }
    }

    void PrintWrapper()
    {
        intArrayWrapper.value.LogC("IntArrayWrapper:");
        strArrayWrapper.value.LogC("StrArrayWrapper:");
        structaArrayWrapper.value.LogC(s => $"[key:{s.key},value:{s.value}]", "StructaArrayWrapper:");
        LogUtility.Log("ClassAWrapper:" + classaWrapper.value);
        LogUtility.Log("StructAWrapper:" + structaWrapper.value);
        LogUtility.Log("TfWrapper:" + tfWrapper.value.position);
    }

    void PrintHashCode()
    {
        LogUtility.Log("IntArray:" + intArray.GetHashCode());
        LogUtility.Log("StrArray:" + strArray.GetHashCode());
        LogUtility.Log("StructaArray:" + structaArray.GetHashCode());
        LogUtility.Log("ClassA:" + classA.GetHashCode());
        LogUtility.Log("StructA:" + structA.GetHashCode());
        LogUtility.Log("Tf:" + tf.GetHashCode());
    }

    void PrintWrapperHashCode()
    {
        LogUtility.Log("IntArrayWrapper:" + intArrayWrapper.value.GetHashCode());
        LogUtility.Log("StrArrayWrapper:" + strArrayWrapper.value.GetHashCode());
        LogUtility.Log("StructaArrayWrapper:" + structaArrayWrapper.value.GetHashCode());
        LogUtility.Log("ClassAWrapper:" + classaWrapper.value.GetHashCode());
        LogUtility.Log("StructAWrapper:" + structaWrapper.value.GetHashCode());
        LogUtility.Log("TfWrapper:" + tfWrapper.value.GetHashCode());
    }

    void WriteWrapper()
    {
        List<int> ints = new List<int>(intArrayWrapper.value) { 99, 100 };
        intArrayWrapper.value = ints.ToArray();

        List<string> strs = new List<string>(strArrayWrapper.value) { "D", "E" };
        strArrayWrapper.value = strs.ToArray();

        List<StructA> strcutAs = new List<StructA>(structaArrayWrapper.value)
        {
            new StructA { key = "D", value = 99 },
            new StructA { key = "E", value = 100 }
        };
        structaArrayWrapper.value = strcutAs.ToArray();

        structaWrapper.refrence.key = "E";
        structaWrapper.refrence.value = 1000;

        classaWrapper.value.key = "DE";
        classaWrapper.value.structA.key = "D";
        classaWrapper.value.structA.value = 999;

        tfWrapper.value.position = Vector3.zero;
    }

    void UnWrap()
    {
        intArrayWrapper.UnWrapByBinary(ref intArray);
        strArrayWrapper.UnWrapByBinary(ref strArray);
        structaArrayWrapper.UnWrapByBinary(ref structaArray);
        structaWrapper.UnWrapByBinary(ref structA);
        classaWrapper.UnWrapByBinary(ref classA);
    }
}
#endif
用例ID用例名称前者测试预期结果是否通过
1简单值类型数组可缓存通过
2不可变引用类型数组可缓存通过
3复合值类型数组可缓存通过
4自定义引用类型可缓存通过
5自定义值类型可缓存通过
6Unity对象可缓存通过

分析

字段临时缓存包装器有三种包装字段的方式,分别是WrapByBinary、WrapByJson和WrapByCustom,三种方式各有优缺点,择优而用。WrapByBinary采用二进制序列化和反序列化生成字段副本,该方法仅可用于被 Serializable 标记的字段类型。WrapByJson采用JSON序列化和反序列化生成字段副本,它虽然比前者包装范围更广,但是不可避免可能会依赖第三方用于JSON序列化和反序列化的库。WrapByCustom则是对前两种方式的补充,当前两种方式都不适用时,则可以自定义包装方式,例如对于Unity对象来说,需要通过Instantiate方法创建对象副本,这个时候就只能用自定义的方法进行包装。


返回的包装器提供了一些属性和方法,可用于判断是否为值类型、缓存的字段和缓存字段的引用(值类型),提供了针对WrapByBinary和WrapByJson包装方法的解包方法,还提供了显式释放包装器的方法。对于包装值类型时,我们可以通过获取缓存字段的引用来避免拷贝,解包方法用于将临时缓存的数据重新写入被包装字段中。通过显式释放包装器可以保证那些使用了非托管资源的类型(实现了IDisposable接口)进行资源的释放工作,从而避免内存泄漏等问题。


但是该包装器存在一些不可避免的限制,若所包装字段越复杂,其性能损耗越高,这是不可避免的。而对于复杂类型,建议自定义一个数据结构作为临时缓存的包装类型。

版本改进

......

系列文章

......

如果这篇文章对你有帮助,请给作者点个赞吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值