For example in F# we can define
type MyRecord = {
X: int;
Y: int;
Z: int
}
let myRecord1 = { X = 1; Y = 2; Z = 3; }
and to update it I can do
let myRecord2 = { myRecord1 with Y = 100; Z = 2 }
That's brilliant and the fact that records automatically implement IStructuralEquality with no extra effort makes me wish for this in C#. However Perhaps I can define my records in F# but still be able to perform some updates in C#. I imagine an API like
MyRecord myRecord2 = myRecord
.CopyAndUpdate(p=>p.Y, 10)
.CopyAndUpdate(p=>p.Z, 2)
Is there a way, and I don't mind dirty hacks, to implement CopyAndUpdate as above? The C# signiture for CopyAndUpdate would be
T CopyAndUpdate<T,P>
( this T
, Expression<Func<T,P>> selector
, P value
)
It can be done, but doing that properly is going to be quite hard (and it definitely won't fit in my answer). The following simple implementation assumes that your object has only read-write properties and parameter-less constructor:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
This slightly defeats the point, because you would probably want to use this on immutable types - but then you always have to call the constructor with all the arguments and it is not clear how to link the constructor parameters (when you create an instance) with the properties that you can read.
The With
method creates a new instance, copies all property values and then sets the one that you want to change (using the PropertyInfo
extracted from the expression tree - without any checking!)
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;
}
The following demo behaves as expected, but as I said, it has lots of limitations:
var person = new Person() { Name = "Tomas", Age = 1 };
var newPerson = person.With(p => p.Age, 20);
In general, I think using a universal reflection-based method like With
here might not be such a good idea, unless you have lots of time to implement it properly. It might be easier to just implement one With
method for every type that you use which takes optional parameters and sets their values to a cloned value (created by hand) if the value is not null
. The signature would be something like:
public Person With(string name=null, int? age=null) { ... }
You could achieve something similar using optional arguments:
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);
This is actually pretty close to the code that F# generates for records.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With