如何将对象的属性映射到C#中的构造函数参数以进行自动复制构造

时间:2016-12-14 10:27:20

标签: c# immutability lenses

我有一个不可变的对象。例如下面的简单案例。

class Person {
   public string Name {get;}
   public int Age {get;}
   public Person(string name, int age){
       Name = name;
       Age = age;
   } 
}

现在我希望有一个通用的扩展方法,例如

public static class ObjectExtensions {
  public void T With<T,P>(this T target, Expression<Func<T,P>> selector, P value){
      /* some implementatation */
  }
}

这样我才能做到

var person = new Person("brad", 12).With(p=>p.Age,55);
person.Age.Should().Be(55);

With方法应使用反射来将构造函数参数名称与现有对象上的属性进行匹配。

  • 表现不是问题。
  • 如果构造函数参数名称与属性名称不匹配,则运行时失败。 (罗斯林分析仪可以解决这个问题)
  • 在不希望浅克隆之后使用反射来使用私有设置器设置属性。 (我目前有一个解决方案)

目的是除了具有名称与属性匹配的参数的构造函数之外,不需要向不可变类添加额外的方法。

带有可为空参数的显式With方法,例如

class Person {
   public string Name {get;}
   public int Age {get;}
   public Person(string name, int age){
       Name = name;
       Age = age;
   }
   public Person With(string? name, int? age){
       return new Person(name ?? this.Name, age ?? this.Age);
   }  

}

虽然优雅不是我正在寻找我的用例的解决方案。

1 个答案:

答案 0 :(得分:1)

这是一个例子。这可能会改进以缓存类型信息的速度,但它是您可以改进的基线。正如我在评论中提到的,这假设构造函数有一个名为与属性相同的参数,忽略大小写。它还假设只有一个构造函数,但您可以随意更新它。

public static T With<T, P>(this T target, Expression<Func<T, P>> selector, P value)
{
    var expression = selector.Body as MemberExpression;

    if (expression == null)
    {
        throw new InvalidOperationException();
    }

    var name = expression.Member.Name;

    var constructor = typeof(T).GetConstructors().First();
    var args = GetParamaters(target, value, constructor, name);

    return (T)Activator.CreateInstance(typeof(T), args.ToArray());
}

private static IEnumerable<object> GetParamaters<T, P>(T target, P value, ConstructorInfo constructor, string name)
{
    foreach (var parameterInfo in constructor.GetParameters())
    {
        if (parameterInfo.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
        {
            yield return value;
        }
        else
        {
            var property =
                typeof(T).GetProperties()
                    .First(x => x.Name.Equals(parameterInfo.Name, StringComparison.InvariantCultureIgnoreCase));
            yield return property.GetValue(target, null);
        }
    }
}