掌控 .NET 对象!






4.83/5 (23投票s)
克隆、序列化和深度比较任何 .NET 对象,无论类型如何
引言
您是否曾经不得不为复杂类型实现 ICloneable
?很快就会变得难以管理,不是吗?那么 IEquatable<T>
呢?这里有一个好问题:当您需要使用 BinaryFormatter
序列化一个对象图(以便传输或存储)时,而树中有一个您不控制且不可序列化的类型怎么办?XML 可以解决问题,对吧?但是当您将对象传递给 XmlSerializer
时,存在一些您不控制且未参与的只读属性。现在怎么办?创建自己的代理类型并在某个实用程序中处理封送操作?对我来说,这听起来很麻烦。这就是为什么我决定再做一次,然后永不再做。:)
要克隆一个对象,您真正需要的是什么?归根结底,您只需要对象的结构及其简单值。如果您知道这两点,就可以构造一个新对象副本。
对象之间的深度比较呢?一样。如果一个对象的结构及其每个简单值等于另一个对象的,那么这些对象就是值等价的。
您知道吗,序列化未知类型的过程要求我们将对象结构及其简单(隐式可序列化)值存储在新结构中,以便可以序列化。
由于所有三个功能都依赖于对您的对象执行相同的操作,因此提供这些功能的所有扩展方法都依赖于同一个类:ObjectGraph
。
背景
本文重点介绍几个小型扩展方法,它们都使用一个名为 ObjectGraph
的新类。此类将对象分解为其最简单的值,同时保持成员关联。这使得对象能够以细粒度的方式进行分析和操作,无论类型如何。
本文使用 .NET Framework 3.5。
Using the Code
代码非常易于使用
var instance = new ComplexType // this object could be anything at all
{
Id = 47,
Name = "My Complex Type",
ArbitraryValue = ArbitraryEnum.Foo,
Values = new List<string>(new[]{"Value1", "Value2", "Value3"})
};
// extension method: Clone
ComplexType clone = instance.Clone(); // a true deep copy
// extension method: ToBinaryString
string serializedInstance = instance.ToBinaryString(); // a base-64 encoded byte array
// extension method: ToObject<T>
var deserializedInstance = serializedInstance.ToObject<ComplexType>(); // another clone!
// extension method : ValueEquals
bool isCloneEqual = instance.ValueEquals(clone); // true
bool isRoundTripEqual = instance.ValueEquals(deserializedInstance); // also true :)
工作原理
这里最大的约定破坏者是可以直接使用 BinaryFormatter
序列化任何对象,即使它们没有用 [Serializable]
装饰。这是一个简单的技巧:正在序列化的对象不是您的对象。它实际上是一个包装类(ObjectGraph
),它是 100% 可序列化的,并且存储了足够的信息以便在反序列化后完全重新生成您的对象。
当 ObjectGraph
包装一个对象时,可能会发生一些事情,具体取决于被包装的对象。如果被包装的对象是简单类型,即代码识别为可直接序列化的类型,则存储对象的原始值,包装操作完成。如果对象已在当前图中被包装,则存储指向原始包装器的指针。如果对象是其他对象的数组,则单独包装并存储数组项。如果对象是复杂类型,则其每个成员变量都会被包装并存储在键名为名称的字典中。
为什么是成员变量?这是关键。无论您的类的 public
接口是什么,如果该类持有状态信息,它将以成员变量的形式存在。自动属性会为其生成变量,但都一样。一旦我获得了对象所有变量的值,我就可以使用基于反射的实例化来创建对象的精确副本,或者将它们与任何其他类型匹配的对象进行比较。
ObjectGraph
的大部分代码在您尝试脱离上下文阅读单个方法时会失去意义,因此,如果这不够清晰,我表示歉意,但这是 private
ObjectGraph
构造函数;它应该能说明 ObjectGraph
如何分析它包装的对象。
private ObjectGraph(object data, GraphRegistry registry, bool isRootGraph)
{
// make sure to unhook all pointers created during scan
using (new DisposableContext(() => { if (isRootGraph) registry.Clear(); }))
{
_isValueBased = data.IsValueBased();
if (_isValueBased) _value = data;
else
{
_pointer = registry.Register(data, this);
if (_pointer == null)
{
_isArray = data is Array;
if (_isArray)
{
_arrayItems = GetItems((Array) data,
registry).ToList();
// CLR gens type names for arrays using the
// {itemTypeName}[{length}] syntax.
_type = Regex.Replace(
data.GetType().AssemblyQualifiedName,
@"\[\d*\]", string.Empty);
return;
}
_state = GetValues(data, registry);
}
}
_type = data != null ? data.GetType().AssemblyQualifiedName : string.Empty;
}
}
关注点
查看源代码中的单元测试。它们表明,除了 CLR 类型和自定义类型之外,匿名类型也将与 ObjectGraph
类愉快地配合使用。
说到这里,源代码中包含的单元测试实际上不是单元测试;它们是带有 BDD 命名语义的集成测试,所有这些都是完全不恰当的。它们之所以存在,只是为了让我(以及您)能够快速调试代码。请不要认为本文试图讨论实现 TDD 或 BDD 的正确方法。事实上,这里有一个免责声明:本文演示了糟糕的测试习惯。
另外,由于在对象扫描和重新生成中都使用了间接递归,我担心足够深度的图可能会导致 StackOverflowException
发生。我在实际使用中未能实现这一点,因此对于大多数场景来说可能没问题。提前告知。
最后,我想感谢那些成员迅速对一些关键反馈做出回应,这些反馈促使该组件达到了目前的地位。您的输入非常宝贵!
尽情享受 :)
历史
- 12/19/08:提交初稿
- 12/20/08:提交二稿及代码修订以支持循环引用
- 1/11/09:提交终稿