There is a way in Scala to selectively change a field while making a copy of the immutable object:
case class Phone(areaCode: String, number: String) // declare type
val original = Phone("832","123-4567") // instantiate
val changed = original.copy(areaCode = "425") // get an updated copy
While in C# it looks like a real pain as usual. One need to create copy ctor and modifiers:
public class Phone
{
Phone(Phone source)
: this(source.AreaCode, source.Number)
{
}
public Phone(string areaCode, string number)
{
AreaCode = areaCode;
Number = number;
}
public string AreaCode { get; private set; }
public string Number { get; private set; }
public Phone WithAreaCode(string value) =>
new Phone(this) { AreaCode = value };
public Phone WithNumber(string value) =>
new Phone(this) { Number = value };
}
We are probably supposed to charge per line…
Here is what I use to mitigate the problem. The immutable type is as easy as it could be:
public class Phone
{
public Phone(string areaCode, string number)
{ AreaCode = areaCode; Number = number; }
public string AreaCode { get; }
public string Number { get; }
}
But there is an extension method to do the trick:
var original = new Phone("832", "123-4567");
var changed = original.With(p => p.AreaCode, "425");
which is defined as:
public static class Cloneable
{
public static TSource With<TSource, TProperty>(this TSource source, Expression<Func<TSource, TProperty>> selector, TProperty value) =>
source.With(new Binding(selector.Target(), value));
static TSource With<TSource>(this TSource source, Binding change) =>
(TSource)Activator.CreateInstance(typeof(TSource), source.Parameters(change));
static object[] Parameters<TSource>(this TSource source, Binding change)
{
var values = source.Values().Update(change)
.ToDictionary(b => b.Name, b => b.Value, StringComparer.OrdinalIgnoreCase);
return typeof(TSource).GetConstructors().Single().GetParameters()
.Select(p => values[p.Name])
.ToArray();
}
static IEnumerable<Binding> Values<TSource>(this TSource source) =>
typeof(TSource).GetProperties()
.Select(p => new Binding(p.Name, p.GetValue(source)));
static IEnumerable<Binding> Update(this IEnumerable<Binding> values, Binding change) =>
values.Select(b => b.Name == change.Name ? change : b);
static string Target<TSource, TProperty>(this Expression<Func<TSource, TProperty>> selector) =>
selector.Body is MemberExpression member && member.Member is PropertyInfo property
? property.Name
: throw new ArgumentException();
class Binding
{
public Binding(string name, object value)
{ Name = name; Value = value; }
public string Name { get; }
public object Value { get; }
}
}
public struct Pair(object First, object Second);
this is quite nice :-) \$\endgroup\$