将属性从一个对象复制到另一个对象






4.17/5 (5投票s)
用于复杂对象的深拷贝例程,该例程可以返回与源类型不同的目标类型。
引言
本文介绍了一种将数据从一个对象自动复制到另一个具有相似结构的对象的方法。这类似于 deep_copy(source, destination)
,但可以返回与源类型不同的类型。
背景
如果您使用过 WCF 服务,您会注意到版本控制会导致大量代码重复。我的问题出现在 2.0 版本的服务与 1.0 版本几乎使用相同的结构时,我希望使用相同的服务工作流程代码,但具有不同的结构。
我需要一种方法来将值从一个对象应用/转换为另一个对象,所以我创建了一个方法来自动遍历源对象图,并将属性从一个对象复制到另一个对象。有很多方法可以做到这一点,但我设计的方法足够通用,并且也为自定义/微调/ hack 留下了空间。
基本上,我需要的是自动执行此操作的功能
service_version1.Customer.Name = service_version2.Customer.Name;
service_version1.Customer.Address.Street = service_version2.Customer.Address.Street;
//service_version1.Customer.Address.Zip = ? // Zip not available in service_version2,
// skipped
如何使用
尽管存在一些陷阱,但基本用法如下。
- 将 PropertiesCopier.cs 文件以及(可选的)PropertiesCopier.Checks.cs 文件包含在您的项目中。
- 在源对象和目标对象上调用
PropertiesCopier.CopyProperties
。PropertiesCopier.CopyProperties(sourceObject, destinationObject);
何时使用
您可以在以下情况下使用本文介绍的方法:
-
您希望将属性从一个对象复制到另一个对象,并且源类型和目标类型相同。
一个例子是
form1.label1.Text = anotherForm.label1.Text; ... form1.textbox1.Text = anotherForm.textbox1.Text;
-
您希望将属性从一个对象复制到另一个对象,并且源类型和目标类型不同,但足够相似。
一个例子是
shoes.Color = socks.Color; shoes.Fashionable = car.Fashionable; //shoes.IsLeather does not have a correspondent in the socks object, //so the automatic copy of properties will just skip this property
上面所有代码的替代方案是
PropertiesCopier.CopyProperties(socks, shoes);
一个更好的例子是
service_version1.Customer.Name = service_version2.Customer.Name; service_version1.Customer.Address.Street = service_version2.Customer.Address.Street;
上面所有代码的替代方案是
PropertiesCopier.CopyProperties(service_version2, service_version1);
-
类似于第 2 点和第 3 点,但不同之处在于某些对应的属性类型已更改。
一个例子是
string service_version1.Customer.Address.Zip; int service_version1.Customer.Address.Zip; service_version1.Customer.Address.Zip = service_version2.Customer.Address.Zip;
在这种情况下,您可以手动编辑源代码并创建类似以下内容的内容:
... original algorithm: get source property and value hack: if (source property == 'Customer.Address.Zip') { convert value to destination type} original algorithm: set value to destination ...
这不是推荐的 hack,但如果 hack/异常的数量很少,那么自动化的好处应该超过硬编码 hack 的缺点。
何时不使用
本文介绍的方法默认不支持泛型。我将把它留给您作为一项练习,以修补算法以支持泛型。
实现
CopyProperties
是复制属性算法的入口点 - 在这里您可以设置计数器和其他调试信息。
public static void CopyProperties(object source, object destination)
{
var count = 1;
CopyPropertiesRecursive(source, destination, null, ref count);
Console.WriteLine("Counted: " + (count - 1));
}
CopyPropertiesRecursive
是所有魔法发生的地方(下面几行是代码的伪代码描述)。
此方法的参数之一是 propertiesToOmmit
。您可能需要排除 ExtensionData
属性 - 对于不熟悉 WCF 的人来说:ExtensionData
属性是由 wsdl 工具自动生成的,通常未使用。
private static void CopyPropertiesRecursive
(object source, object destination,
IList<string> propertiesToOmmit, ref int count)
{
var destinationType = destination.GetType();
Type sourceType = null;
if (source != null)
sourceType = source.GetType();
var destinationProperties = destinationType.GetProperties();
//for a type coming from a serialized web service type
if (propertiesToOmmit == null)
propertiesToOmmit = new List<string> { "ExtensionData" };
destinationProperties = Array.FindAll
(destinationProperties, pi => !propertiesToOmmit.Contains(pi.Name));
foreach (var property in destinationProperties)
{
var propertyType = property.PropertyType;
//todo can cache this as: static readonly
//Dictionary<type,object> cache. how about multithreading?
var sourceValue = propertyType.IsValueType ?
Activator.CreateInstance(propertyType) : null;
PropertyInfo propertyInSource = null;
var sourceHasDestinationProperty = false;
//source is null
if (source != null)
{
propertyInSource = sourceType.GetProperty(property.Name);
//source has the property
if (propertyInSource != null)
{
sourceHasDestinationProperty = true;
sourceValue = propertyInSource.GetValue(source, null);
}
else Console.WriteLine("\tsource does not contain property " +
destinationType + " -> " + property.Name);
}
//else
// Console.WriteLine("\tsource was null for " +
// destinationType + " -> " + property.Name);
//it's a complex/container type?
var isComplex = !propertyType.ToString().StartsWith("System");
if (isComplex & !propertyType.IsArray)
{
Console.WriteLine("\tRecursion on: " + property.Name);
//create new destination structure
var ci = propertyType.GetConstructor(Type.EmptyTypes);
var newDestination = ci.Invoke(null);
property.SetValue(destination, newDestination, null);
//Console.WriteLine("\tCalled constructor on " + property.Name);
CopyPropertiesRecursive(sourceValue, newDestination,
propertiesToOmmit, ref count);
continue;
}
var s = count + ". " + property.Name +
(propertyType.IsArray ? "[]" : "") + " = ";
if (!sourceHasDestinationProperty)
s += "[default(" + propertyType + ")] = ";
Console.WriteLine(s + sourceValue);
//todo check for CanWrite and CanRead - if (!toField.CanWrite) continue;
if (propertyType.IsArray & propertyInSource != null)
sourceValue = DeepCopyArray(propertyInSource.PropertyType,
propertyType, sourceValue, source, destination);
property.SetValue(destination, sourceValue, null);
var destinationValue = property.GetValue(destination, null);
count++;
//todo deep assert for arrays
if (!propertyType.IsArray)
Assert.AreEqual(sourceValue, destinationValue,
"Assert failed for property: " + destinationType + "." +
property.Name);
}
}
CopyPropertiesRecursive
的伪代码如下:
void CopyPropertiesRecursive(source, destination)
{
foreach(var property in destination.Properties)
{
T = property.PropertyType;
bool doesSourceHaveProperty = ...;
object sourceValue = doesSourceHaveProperty ? getSourceValue() : default(T);
if (T is service specific structure AND is not array)
{
property.Value = Constructor(T);
CopyPropertiesRecursive(sourceValue,
property.Value); //go recursive on this property
continue;
}
if (T is array)
sourceValue = DeepCopyArray(sourceValue, new type T);
property.Value = sourceValue;
}
}
DeepCopyArray
是一个例程,它接受一个源数组,将其序列化为 XML,将源类型名称/命名空间更改为目标类型名称/命名空间,然后将其反序列化为目标类型的数组。
如果数组很大,此方法的性能会很慢,因此您可能希望将此代码替换为类似 CopyPropertiesRecursive
的反射代码。
private static object DeepCopyArray(Type sourceType,
Type destinationType, object sourceValue, object sourceParent,
object destinationParent)
{
//todo this method ca be made generic and handle more than just arrays
if (sourceValue == null || sourceType == null ||
sourceParent == null || destinationParent == null)
return null;
using (var stream = new MemoryStream(2 * 1024))
{
var serializer = new DataContractSerializer(sourceType);
serializer.WriteObject(stream, sourceValue);
serializer = new DataContractSerializer(destinationType);
//if we know the namespace or type names will be different,
//we must get the xml and REPLACE the
//source type name/namespace to destination source type name/namespace
//if the namespace/type name combination is the same,
//we do not need the if TRUE statement,
//only the ELSE branch
if (true)
{
var xml = Encoding.UTF8.GetString(stream.ToArray());
// our example array serialization looks like below:
// <ArrayOfV1KeyValuePair
// xmlns="urn:mycompany.com/data/version_1.0"
// xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
// <V1KeyValuePair>
// <Key>jet key 1</Key>
// <Value>jet value 1</Value>
// </V1KeyValuePair>
// <V1KeyValuePair>
// <Key>jet key 2</Key>
// <Value>jet value 2</Value>
// </V1KeyValuePair>
// </ArrayOfV1KeyValuePair>
//replace source type name with destination type name
var nameSource = sourceType.Name.Replace("[]", "");
var nameDestination = destinationType.Name.Replace("[]", "");
xml = xml.Replace(nameSource, nameDestination);
//replace source namespace with destination namespace
var sourceNamespace = GetDataContractNamespace(sourceParent);
var destiantionNamespace = GetDataContractNamespace(destinationParent);
xml = xml.Replace(sourceNamespace, destiantionNamespace);
using (var modified = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
{
modified.Position = 0;
return serializer.ReadObject(modified);
}
}
else
{
stream.Position = 0;
return serializer.ReadObject(stream);
}
}
}
private static string GetDataContractNamespace(object instance)
{
if (instance == null)
throw new ArgumentNullException("instance");
var attribute = instance.GetType().GetCustomAttributes
(true).Single(o => o.GetType() == typeof(DataContractAttribute));
return ((DataContractAttribute)attribute).Namespace;
}
此算法与现有算法有何不同?
-
最重要的特点是目标类型可以与源类型不同。这支持 WCF 服务版本控制,但也可以用于其他方式,例如将 Windows 窗体的属性复制到另一个窗体。
-
该算法会深入到目标属性 - 它会递归遍历所有对象图。
-
如果目标不包含源的属性,则会跳过该属性。
-
如果目标包含一个源中不存在的属性,则目标属性及其子图将被初始化为默认值。
换句话说:如果源中没有找到对应的属性,则在目标属性和子属性上调用默认构造函数。
这在以下场景中非常有用:
//we manually copy properties service_version1.Customer.Name = service_version2.Customer.Name; //service_version1.Customer.Address.Zip = ? // service_version2 //does not have a Customer.Address class //we forget to/do not initialize Address and sub properties //we now want to access service_version1.Customer.Address.Zip if (service_version1.Customer.Address.Zip == 6789) //this will throw a null reference exception for Address.Zip { DoSomething(); }
该算法可以处理这种情况,并在源中找不到任何内容或
null
时将所有属性初始化为默认值。 -
该算法包含一个深拷贝数组方法(对于比原始类型稍微复杂一些的类型很有用)- 尽管不是性能最好的。
结论
我创建此方法来将属性从一个对象复制到另一个对象,因为它最适合我的需求,并且在网上找不到类似这样完整的解决方案。
我将根据您的需求来微调此代码,并为您提供一些改进建议。
改进
- 阅读关于其他浅拷贝和深拷贝方法,并决定哪种最适合您的要求。
- 您可以删除许多额外的调试信息。
- 如果源和目标类型相同,您可以优化很多代码,尤其是在
DeepCopyArray
方法中。 - 在将
sourceValue
初始化为default(T)
时,您可以将Activator.CreateInstance(propertyType)
缓存到Dictionary<type,object>
中。 - 为数组添加测试 - 检查目标数组中的元素是否与源数组中的元素相等。
历史
- 2011 年 3 月 27 日:初始发布