Reputation: 32162
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
)
Upvotes: 16
Views: 1322
Reputation: 1247
In case anyone else is stumbling upon this, I recently needed to do the same thing and was able to take @Tomas Petricek's answer and expand it to work with immutable records:
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;
}
Usage
// F#
type Person =
{
Name : string
Age : int
}
// C#
var personRecord = new Person("John",1);
var newPerson = personRecord.With(p => p.Age, 20);
Upvotes: 0
Reputation: 47904
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.
Upvotes: 7
Reputation: 243051
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) { ... }
Upvotes: 10