Можем ли мы получить доступ к функции копирования и обновления F # из С#?

Например, в 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 без каких-либо дополнительных усилий, заставляет меня хотеть этого в С#. Однако, возможно, я могу определить свои записи в F #, но все же смогу выполнить некоторые обновления на С#. Я предполагаю, что API, как

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

Есть ли способ, и я не против грязных хаков, чтобы реализовать CopyAndUpdate, как указано выше? Значением С# для CopyAndUpdate будет

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

Ответ 1

Это можно сделать, но делать это правильно будет довольно сложно (и это определенно не поместится в моем ответе). Следующая простая реализация предполагает, что ваш объект имеет только свойства чтения и записи и конструктор без параметров:

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) { ... }

Ответ 2

Вы можете добиться чего-то подобного, используя необязательные аргументы:

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 # генерирует для записей.