字段临时缓存包装器
前言
在实际开发中,我们有时候存在一种需求,例如对于某个字段,我们希望在某个明确的保存节点前对字段的修改都仅作为缓存保留,最终是否应用这些修改取决于某些条件,比如玩家对游戏设置的修改可能需要玩家明确确认应用修改后才会保存下来,在此之前玩家在游戏界面上的所有修改都是临时的。
本文基于这个需求探索出了一种解决方案“字段临时缓存包装器”,通过创建字段或用于存储字段临时数据的数据结构的副本来实现临时缓存,虽然我们同样可以采用直接声明一个副本字段的方式来达到同样的目的,但是这可能会增加冗余代码,且不利于代码的维护,通过包装器来封装临时缓存的通用逻辑,与具体业务逻辑隔离。
代码
v1.0
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);
}
}
v1.1
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;
}
}
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 (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");
if (_isValueType) value = _value;
else
{
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 (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");
if (_isValueType) value = _value;
else
{
string jsonStr = JsonConvert.SerializeObject(_value);
value = JsonConvert.DeserializeObject<T>(jsonStr);
}
}
/// <summary>
/// 释放包装器所包装的字段
/// <para>提示:当所包装字段实现了IDisposable接口时该方法才有效</para>
/// </summary>
public void Dispose()
{
if (_isDisposed) throw new InvalidOperationException("The wrapper is disposed.");
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()
{
if (Application.isPlaying)
{
intArrayWrapper = TempWrapper<int[]>.WrapByBinary(ref intArray);
strArrayWrapper = TempWrapper<string[]>.WrapByJson(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));
Instantiate(transform);
}
}
void OnDestroy()
{
intArrayWrapper.Dispose();
strArrayWrapper.Dispose();
structaArrayWrapper.Dispose();
classaWrapper.Dispose();
structaWrapper.Dispose();
tfWrapper.Dispose();
}
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.UnwrapByJson(ref strArray);
structaArrayWrapper.UnWrapByBinary(ref structaArray);
structaWrapper.UnWrapByBinary(ref structA);
classaWrapper.UnWrapByBinary(ref classA);
}
}
#endif
v1.0
用例ID | 用例名称 | 前者测试 | 预期结果 | 是否通过 |
---|---|---|---|---|
1 | 简单值类型数组 | 无 | 可缓存 | 通过 |
2 | 不可变引用类型数组 | 无 | 可缓存 | 通过 |
3 | 复合值类型数组 | 无 | 可缓存 | 通过 |
4 | 自定义引用类型 | 无 | 可缓存 | 通过 |
5 | 自定义值类型 | 无 | 可缓存 | 通过 |
6 | Unity对象 | 无 | 可缓存 | 通过 |
v1.1
用例ID | 用例名称 | 前者测试 | 预期结果 | 是否通过 |
---|---|---|---|---|
1 | 简单值类型数组 | 无 | 可缓存 | 通过 |
2 | 不可变引用类型数组 | 无 | 可缓存 | 通过 |
3 | 复合值类型数组 | 无 | 可缓存 | 通过 |
4 | 自定义引用类型 | 无 | 可缓存 | 通过 |
5 | 自定义值类型 | 无 | 可缓存 | 通过 |
6 | Unity对象 | 无 | 可缓存 | 通过 |
分析
字段临时缓存包装器有三种包装字段的方式,分别是WrapByBinary、WrapByJson和WrapByCustom,三种方式各有优缺点,择优而用。WrapByBinary采用二进制序列化和反序列化生成字段副本,该方法仅可用于被 Serializable 标记的字段类型。WrapByJson采用JSON序列化和反序列化生成字段副本,它虽然比前者包装范围更广,但是不可避免可能会依赖第三方用于JSON序列化和反序列化的库。WrapByCustom则是对前两种方式的补充,当前两种方式都不适用时,则可以自定义包装方式,例如对于Unity对象来说,需要通过Instantiate方法创建对象副本,这个时候就只能用自定义的方法进行包装。
返回的包装器提供了一些属性和方法,可用于判断是否为值类型、缓存的字段和缓存字段的引用(值类型),提供了针对WrapByBinary和WrapByJson包装方法的解包方法,还提供了显式释放包装器的方法。对于包装值类型时,我们可以通过获取缓存字段的引用来避免拷贝,解包方法用于将临时缓存的数据重新写入被包装字段中。通过显式释放包装器可以保证那些使用了非托管资源的类型(实现了IDisposable接口)进行资源的释放工作,从而避免内存泄漏等问题。
但是该包装器存在一些不可避免的限制,若所包装字段越复杂,其性能损耗越高,这是不可避免的。而对于复杂类型,建议自定义一个数据结构作为临时缓存的包装类型。
注意:不要使用包装器去包装其声明所在的类,特别是对于自定义包装逻辑要避免无限递归,否则会导致栈溢出或内存泄漏问题。示例代码如下:
// 该方法将导致栈溢出 public class A { TempWrapper<A> wrapper; public A() { wrapper = TempWrapper<A>.WrapByCustom(() => new A()); } } // 该方法将导致内存泄漏 public class B:Monobehaviour { TempWrapper<Transform> wrapper; void Awake() { // Instantiate方法去克隆当前组件对象的Transform组件就会导致无限递归 wrapper = TempWrapper<Transform>.WrapByCustom(() => Instantiate(transform)); } }
版本改进
版本号 | 改进内容 |
---|---|
v1.1 | 1.实例方法不使用线程锁,仅对静态方法使用线程锁; 2.手动执行Dispose方法释放包装器后,所有对公开实例成员的访问都将触发异常; 3.删除IsDisposed属性; |
...... | ...... |
系列文章
......
如果这篇文章对你有帮助,请给作者点个赞吧!