可以通过序列化轻松实现深度克隆,但是仅跨非空字段复制需要更多条件逻辑,在这种情况下,我将其称为 Coalesce,因此我将方法命名为 @987654321 @。如果您愿意,您可以将其重构为扩展方法,但我不推荐它,而是将其放在静态帮助器类中。尽管这可能很有用,但我不鼓励将其作为生产业务运行时的“goto”。
对这些类型的解决方案使用 Reflection 通常是最低效的机制,但它为我们提供了很大的灵活性,并且非常适合模拟、原型设计和快速单元测试表达式。
- 虽然不在此示例中,但可以轻松添加检查以排除高级场景的
[Obsolete] 属性
以下示例使用Property Name 比较,因此您不必传入相同类型的对象。请注意,已创建 IsNull 和 IsValueType 方法来封装这些概念,从而简化您可能希望对此方法进行的调整。
- 此方法还在继续之前检查属性是否可以读取/写入,这允许我们支持源对象上的只读属性,当然我们不会尝试写入只读属性。
- 解析和写入的最终值包含在抑制任何错误的 try catch 语句中,需要进行一些调整才能使这样的代码普遍工作,但对于简单的类型定义应该可以正常工作。
/// <summary>
/// Deep Copy the top level properties from this object only if the corresponding property on the target object IS NULL.
/// </summary>
/// <param name="source">the source object to copy from</param>
/// <param name="target">the target object to update</param>
/// <returns>A reference to the Target instance for chaining, no changes to this instance.</returns>
public static void CoalesceTo(object source, object target, StringComparison propertyComparison = StringComparison.OrdinalIgnoreCase)
{
var sourceType = source.GetType();
var targetType = target.GetType();
var targetProperties = targetType.GetProperties();
foreach(var sourceProp in sourceType.GetProperties())
{
if(sourceProp.CanRead)
{
var sourceValue = sourceProp.GetValue(source);
// Don't copy across nulls or defaults
if (!IsNull(sourceValue, sourceProp.PropertyType))
{
var targetProp = targetProperties.FirstOrDefault(x => x.Name.Equals(sourceProp.Name, propertyComparison));
if (targetProp != null && targetProp.CanWrite)
{
if (!targetProp.CanRead)
continue; // special case, if we cannot verify the destination, assume it has a value.
else if (targetProp.PropertyType.IsArray || targetProp.PropertyType.IsGenericType // It is ICollection<T> or IEnumerable<T>
&& targetProp.PropertyType.GenericTypeArguments.Any()
&& targetProp.PropertyType.GetGenericTypeDefinition() != typeof(Nullable<>) // because that will also resolve GetElementType!
)
continue; // special case, skip arrays and collections...
else
{
// You can do better than this, for now if conversion fails, just skip it
try
{
var existingValue = targetProp.GetValue(target);
if (IsValueType(targetProp.PropertyType))
{
// check that the destination is NOT already set.
if (IsNull(existingValue, targetProp.PropertyType))
{
// we do not overwrite a non-null destination value
object targetValue = sourceValue;
if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{
// TODO: handle specific types that don't go across.... or try some brute force type conversions if neccessary
if (targetProp.PropertyType == typeof(string))
targetValue = targetValue.ToString();
else
targetValue = Convert.ChangeType(targetValue, targetProp.PropertyType);
}
targetProp.SetValue(target, targetValue);
}
}
else if (!IsValueType(sourceProp.PropertyType))
{
// deep clone
if (existingValue == null)
existingValue = Activator.CreateInstance(targetProp.PropertyType);
CoalesceTo(sourceValue, existingValue);
}
}
catch (Exception)
{
// suppress exceptions, don't set a field that we can't set
}
}
}
}
}
}
}
/// <summary>
/// Check if a boxed value is null or not
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of null in here.
/// </remarks>
/// <param name="value">Value to inspect</param>
/// <param name="valueType">Type of the value, pass it in if you have it, otherwise it will be resolved through reflection</param>
/// <returns>True if the value is null or primitive default, otherwise False</returns>
public static bool IsNull(object value, Type valueType = null)
{
if (value is null)
return true;
if (valueType == null) valueType = value.GetType();
if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType)
{
// Handle nullable types like float? or Nullable<Int>
if (valueType.IsGenericType)
return value is null;
else
return Activator.CreateInstance(valueType).Equals(value);
}
// treat empty string as null!
if (value is string s)
return String.IsNullOrWhiteSpace(s);
return false;
}
/// <summary>
/// Check if a type should be copied by value or if it is a complexe type that should be deep cloned
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of Object vs Value/Primitive here.
/// </remarks>
/// <param name="valueType">Type of the value to check</param>
/// <returns>True if values of this type can be straight copied, false if they should be deep cloned</returns>
public static bool IsValueType(Type valueType)
{
// TODO: any specific business types that you want to treat as value types?
// Standard .Net Types that can be treated as value types
if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType || valueType == typeof(string))
return true;
// Support Nullable Types as Value types (Type.IsValueType) should deal with this, but just in case
if (valueType.HasElementType // It is array/enumerable/nullable
&& valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
return true;
return false;
}
因为我们在这里使用反射,所以我们无法利用 Generics 可以为我们提供的优化。如果您想将此应用到生产环境,请考虑使用 T4 模板来编写此逻辑的通用类型版本作为您的业务类型的扩展方法。
深度克隆 -
你会注意到我特别跳过了数组和其他 IEnumerable 结构...有一大堆蠕虫支持它们,最好不要让一种方法尝试 Deep 复制,所以将嵌套调用 CoalesceTo 取出,然后对树中的每个对象调用 clone 方法。
数组/集合/列表的问题在于,在克隆之前,您需要确定一种方法将源中的集合与目标中的集合同步,您可以根据 Id 字段或像[KeyAttribute] 这样的某种属性,但这种实现需要高度特定于您的业务逻辑,并且超出了这篇已经很可怕的帖子的范围;)
Decimal 和 DateTime 这样的类型在这些类型的场景中是有问题的,它们不应该与 null 进行比较,而是我们必须将它们与它们的默认类型状态进行比较,同样我们不能使用泛型 @987654330 @ 运算符或值在这种情况下,因为类型只能在运行时解析。
所以我已经更改了您的类,以包含此逻辑如何处理 DateTimeOffset 的示例:
public class Employee
{
public int EmployeeID { get; set; }
public string EmployeeName { get; set; }
public DateTimeOffset Date { get; set; }
public float? Capacity { get; set; }
Nullable<int> MaxShift { get; set; }
public Address ContactAddress { get; set; }
}
public class Address
{
public string Address1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
public static void TestMethod1()
{
Employee employee = new Employee();
employee.EmployeeID = 100;
employee.EmployeeName = "John";
employee.Capacity = 26.2f;
employee.MaxShift = 8;
employee.Date = new DateTime(2020,1,22);
employee.ContactAddress = new Address();
employee.ContactAddress.Address1 = "Park Ave";
employee.ContactAddress.City = "New York";
employee.ContactAddress.State = "NewYork";
employee.ContactAddress.ZipCode = "10002";
Employee employeeCopy = new Employee();
employeeCopy.EmployeeID = 101;
employeeCopy.EmployeeName = "Tom";
employeeCopy.ContactAddress = new Address();
CoalesceTo(employee, employeeCopy);
}
这会产生以下对象图:
{
"EmployeeID": 101,
"EmployeeName": "Tom",
"Date": "2020-01-22T00:00:00+11:00",
"Capacity":26.2,
"MaxShift":8,
"ContactAddress": {
"Address1": "Park Ave",
"City": "New York",
"State": "NewYork",
"ZipCode": "10002"
}
}