15

例如在 F# 中我们可以定义

type MyRecord = {
    X: int;
    Y: int;
    Z: int 
    }

let myRecord1 = { X = 1; Y = 2; Z = 3; }

并更新它我可以做

let myRecord2 = { myRecord1 with Y = 100; Z = 2 }

这太棒了,而且记录自动实现 IStructuralEquality 无需额外努力的事实让我希望在 C# 中实现这一点。但是,也许我可以在 F# 中定义我的记录,但仍然能够在 C# 中执行一些更新。我想像一个API

MyRecord myRecord2 = myRecord
    .CopyAndUpdate(p=>p.Y, 10)
    .CopyAndUpdate(p=>p.Z, 2)

有没有办法,我不介意肮脏的黑客,来实现 CopyAndUpdate 如上所述?CopyAndUpdate 的 C# 签名将是

T CopyAndUpdate<T,P>
   ( this T
   , Expression<Func<T,P>> selector
   , P value
   )
4

3 回答 3

10

可以做到,但是正确地做到这一点将非常困难(而且绝对不适合我的答案)。以下简单实现假设您的对象只有读写属性和无参数构造函数:

class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
}

这略微违背了这一点,因为您可能希望在不可变类型上使用它 - 但是您总是必须使用所有参数调用构造函数,并且不清楚如何将构造函数参数(当您创建实例时)与您可以读取的属性。

With方法创建一个新实例,复制所有属性值,然后设置您要更改的实例(使用PropertyInfo从表达式树中提取的 - 无需任何检查!)

public static T With<T, P>(this T self, Expression<Func<T, P>> selector, P newValue)
{
  var me = (MemberExpression)selector.Body;
  var changedProp = (System.Reflection.PropertyInfo)me.Member;

  var clone = Activator.CreateInstance<T>();
  foreach (var prop in typeof(T).GetProperties())
    prop.SetValue(clone, prop.GetValue(self));

  changedProp.SetValue(clone, newValue);
  return clone;
}

以下演示按预期运行,但正如我所说,它有很多限制:

var person = new Person() { Name = "Tomas", Age = 1 };
var newPerson = person.With(p => p.Age, 20);

一般来说,我认为使用像With这里这样的基于反射的通用方法可能不是一个好主意,除非你有很多时间来正确实现它。如果值不是,则为您使用的每种类型实现一种方法可能更容易,该With方法采用可选参数并将其值设置为克隆值(手动创建)null。签名将类似于:

public Person With(string name=null, int? age=null) { ... }
于 2013-08-09T17:15:13.867 回答
7

您可以使用可选参数实现类似的效果:

class MyRecord {
    public readonly int X;
    public readonly int Y;
    public readonly int Z;
    public MyRecord(int x, int y, int z) {
        X = x; Y = y; Z = z;
    }
    public MyRecord(MyRecord prototype, int? x = null, int? y = null, int? z = null)
        : this(x ?? prototype.X, y ?? prototype.Y, z ?? prototype.Z) { }
}

var rec1 = new MyRecord(1, 2, 3);
var rec2 = new MyRecord(rec1, y: 100, z: 2);

这实际上非常接近 F# 为记录生成的代码。

于 2013-08-09T17:27:05.590 回答
0

万一其他人遇到这个问题,我最近需要做同样的事情,并且能够接受@Tomas Petricek 的回答并将其扩展为使用不可变记录:

    public static T With<T, P>(this T self, Expression<Func<T, P>> selector, P newValue)
    {
        var me = (MemberExpression)selector.Body;
        var changedProp = (System.Reflection.PropertyInfo)me.Member;

        var constructor = typeof(T).GetConstructors()[0];
        var parameters = constructor.GetParameters().Select(p => p.Name);
        var properties = typeof(T).GetProperties();
        var args = parameters
            .Select(p => properties.FirstOrDefault(prop => String.Equals(prop.Name,p, StringComparison.CurrentCultureIgnoreCase)))
            .Select(prop => prop == changedProp ? newValue : prop.GetValue(self))
            .ToArray();

        var clone = (T) constructor.Invoke(args);

        return clone;
    }

用法

// F#
type Person = 
{
    Name : string      
    Age : int
}

// C#
var personRecord = new Person("John",1);
var newPerson = personRecord.With(p => p.Age, 20);
于 2018-10-17T04:46:00.393 回答