目录
4.通过拷贝序列化后的二进制(Binary Serialization)
写在前面,先来一句英文,原文如下:
The maturity of a software engineer is determined by many factors such as knowledge of algorithms, analytical thinking, big-picture focus, debugging skills etc. In addition, the following two factors are important from my point of view:
- A software engineer knows several approaches to solve the same problem.
- A software engineer chooses an approach based on an analysis of its pros and cons.
意思是:一个软件工程师厉不厉害需要从多方面考虑,如算法知识,分析能力,全局视野(大局观),调试技巧等。此外下面两个因素也很重要。
- 知道一个问题能用多种办法解决
- 知道根据不同方法的优缺点来选择最合适的办法
1. 前提
言归正传,假设你要深拷贝你自己的对象,直接赋值是没有的,一般要自己去实现深拷贝的功能。后面会给出五种办法,每一种办法没有绝对的优劣,我们会给出简单的分析。
假设你要拷贝的类是如下设计的:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
其中Person类中嵌套了一个自定义的Address类,为了简化起见,类中没有其它附加方法,当然这不影响本文主题。
2.通过ICloneable接口
基本上所有的面向对象语言都有这样一个接口,就是为了方便你实现深拷贝。C#中也有这样一个接口,这个接口有一克隆函数MemberwiseClone,可以实现属性的拷贝。
实现如下:
public class Person : ICloneable
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
public object Clone()
{
var person = (Person)MemberwiseClone();
person.Address = (Address)Address.Clone();
return person;
}
public void Show()
{
Console.WriteLine("Name: " + Name + "\t" + "Age: " + Age);
Console.WriteLine(" Address: " + Address.City + "--- " + Address.Street);
}
}
public class Address : ICloneable
{
public string City { get; set; }
public string Street { get; set; }
public object Clone()
{
return MemberwiseClone();
}
}
测试代码如下:
static void Main(string[] args)
{
Person p1 = new Person
{
Name = "欧阳锋",
Age = 30,
Address = new Address { City = "西域", Street = "天山" }
};
Person p2 = (Person)p1.Clone();
p2.Name = "欧阳克";
p2.Age = 22;
p2.Address.City = "蒙古";
p2.Address.Street = "乌兰";
p1.Show();
p2.Show();
}
运行结果如下:
Name: 欧阳锋 Age: 30
Address: 西域--- 天山
Name: 欧阳克 Age: 22
Address: 蒙古--- 乌兰
可以看到p1和p2 不一样赋值后并没有相互影响。
优点:不需要自定义接口,完全控制我们克隆的内容。 假设Person类包含用于审核目的的AccountHistory属性。 克隆Person对象时,我们可能需要一个空帐户历史记录。 这样的要求可以用如下代码表达:
public class Person : ICloneable
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
public AccountHistory AccountHistory { get; set; }
public object Clone()
{
var person = (Person)MemberwiseClone();
person.Address = (Address)Address.Clone();
person.AccountHistory = new AccountHistory(); //clear the history
return person;
}
}
缺点:
- 方法Clone返回对象类型。 调用代码必须将对象转换为特定的数据类型。
- 必须为对象图中包含的每个类实现ICloneable接口。
- 接口名称或方法名称不告诉深层副本还是浅层副本返回给调用方。 软件工程师需要深入研究实施细节。
3.自定义拷贝接口
接着上面,我们直接自定义一个接口:
public interface IProtoType<T>
{
T CreateDeepCopy();
}
然后,Person类实现这个接口:
public class Person : IProtoType<Person>
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
public Person CreateDeepCopy()
{
var person = (Person)MemberwiseClone();
person.Address = Address.CreateDeepCopy();
return person;
}
public void Show()
{
Console.WriteLine("Name: " + Name + "\t" + "Age: " + Age);
Console.WriteLine(" Address: " + Address.City + "--- " + Address.Street);
}
}
public class Address : IProtoType<Address>
{
public string City { get; set; }
public string Street { get; set; }
public Address CreateDeepCopy()
{
return (Address)MemberwiseClone();
}
}
运行结果和之前类似。
优点:
- 方法名称CreateDeepCopy准确描述了它的用途。
- IPrototype接口的使用者不再需要强制转换CreateDeepCopy方法的返回类型。
- 完全控制我们使用ICloneable接口进行克隆的方式。
缺点:
- 接口必须由软件工程师手动定义。
- 必须为对象图中包含的每个类实现一个接口。
4.通过拷贝序列化后的二进制(Binary Serialization)
采用二进制序列化无需实现接口,核心思想是:将对象序列化到内存中,然后将其反序列化到新的对象。
我用构建一个静态类,有一个辅助函数:
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
public static class Helper
{
public static T CreateDeepCopy<T>(T obj)
{
//初始化一个内存流对象
using(var ms=new MemoryStream())
{
//声明一个二进制序列化对象
IFormatter formatter = new BinaryFormatter();
//序列化对象
formatter.Serialize(ms, obj);
//将当前流中的位置设定为指定值
ms.Seek(0,SeekOrigin.Begin);
//返回序列化
return (T)formatter.Deserialize(ms);
}
}
}
简单说明一下:
首先是MemoryStream类:MemoryStream和BufferedStream都派生自基类Stream,因此它们有很多共同的属性和方法,但是每一个类都有自己独特的用法。这两个类都是实现对内存进行数据读写的功能,而不是对持久性存储器进行读写。MemoryStream类用于向内存而不是磁盘读写数据。MemoryStream封装以无符号字节数组形式存储的数据,该数组在创建MemoryStream对象时被初始化,或者该数组可创建为空数组。可在内存中直接访问这些封装的数据。内存流可降低应用程序中对临时缓冲区和临时文件的需要。
(
.NET框架提供了两种种串行化的方式:1、是使用BinaryFormatter进行串行化;2、使用XmlSerializer进行串行化。第一种方式提供了一个简单的二进制数据流以及某些附加的类型信息,而第二种将数据流格式化为XML存储。 可以使用[Serializable]属性将类标志为可序列化的。如果某个类的元素不想被序列化,1、可以使用[NonSerialized]属性来标志,2、可以使用[XmlIgnore]来标志。
序列化意思指的是把对象的当前状态进行持久化,一个对象的状态在面向对象的程序中是由属性表示的,所以序列化类的时候是从属性读取值以某种格式保存下来,而类的成员函数不会被序列化,.net存在几种默认提供的序列化,二进制序列化,xml和json序列化会序列化所有的实例共有属性。
)
然后直接使用这个静态函数:
static void Main(string[] args)
{
Person p1 = new Person
{
Name = "欧阳锋",
Age = 30,
Address = new Address { City = "西域", Street = "天山" }
};
Person p2 = Helper.CreateDeepCopy<Person>(p1);
p2.Name = "欧阳克";
p2.Age = 22;
p2.Address.City = "蒙古";
p2.Address.Street = "乌兰";
p1.Show();
p2.Show();
}
运行结果同上。
优点:
- 不再需要实现接口。
- 无论对象多么复杂,都将完全克隆该对象。
- 克隆逻辑存在于一个地方。
缺点:
- Class Person和所有相关的类都需要标记为[Serializable]属性。
- 在浅拷贝和深拷贝之间没有选择。 使用序列化时,只有深拷贝选项可供软件工程师使用。
5.使用XML对象序列化
原理和上面类似,就不细说了,直接丢代码:
public static class Helper
{
public static T CreateDeepCopy<T>(T obj)
{
using (var ms = new MemoryStream())
{
XmlSerializer serializer = new XmlSerializer(obj.GetType());
serializer.Serialize(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
return (T)serializer.Deserialize(ms);
}
}
}
XML序列化不需要[Serializable]属性,这是个好消息。 但是,XML序列化要求每个类都具有无参数的构造函数。 这样的要求看似无害,但并非总是如此。 在Person类中具有无参数的构造函数意味着可以在没有名称,姓氏等的情况下创建实例。我更喜欢在对象创建时设置所有必需的属性,以避免部分初始化对象。 因此,XML序列化在许多情况不被采用。
6.使用拷贝构造函数
这个方法,是在学习面向对象语言的时候,都会有的接触到的,也不用多说,直接上代码
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Address Address { get; set; }
public Person() { }
public Person(Person person)
{
Name = person.Name;
Age = person.Age;
Address = new Address(person.Address);
}
public void Show()
{
Console.WriteLine("Name: " + Name + "\t" + "Age: " + Age);
Console.WriteLine(" Address: " + Address.City + "--- " + Address.Street);
}
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
public Address() { }
public Address(Address address)
{
City = address.City;
Street = address.Street;
}
}
优点: 对我们想拷贝的东西具有完全控制权
缺点:每一个拷贝的类都必须实现拷贝构造函数
7.小结
使用接口和拷贝构造函数,可以是我们对拷贝过程完全控制。而基于序列的的克隆技术又可以是的当我们的对象在修改后,不需要去改动拷贝相关的代码。
还是老生常谈,软件开发没有银弹,所以具体选择哪种方法,你需要根据实际情况,分析优劣,然后再定夺。