ネストしたImmutableオブジェクトの更新(C#)
C#(7.0)でImmutableクラスを作成する場合、下記のような記述になります。
public class Person { public int Age { get; } public string Name { get; } public Person Child { get; } public Person(int age, string name, Person child) { Age = age; Name = name; Child = child; } }
下記のようなデータがあったとして、Richardの年齢を更新したデータを作成するのはとても面倒です。
var person = new Person(35, "John", new Person(10, "Richard", null));
そこでリフレクションを利用して、簡単に更新データを作成できるようにしてみたいと思います。
コード内で利用されているCreate、Scan、Zipオペレータは下記記事を参照してください。
LINQ独自オペレータ メモ - 何でもプログラミング
With関数定義
ExpressionTreeを利用してオブジェクトを更新します。
一つのオブジェクトの更新は、MemberwiseCloneとBackingFieldの更新で実現しています。
自動生成される<Name>k__BackingFieldは予告なく名称が変わる可能性がありますので、不安な場合はprivate set;を追加して、PropertyInfo.SetValueを利用するようにしてください。
public static TObj With<TObj, TValue>(this TObj obj, Expression<Func<TObj, TValue>> expression, TValue value) { // MemberExpressionをたどる var members = EnumerableEx.Create(expression.Body as MemberExpression, x => x.Expression as MemberExpression) .TakeWhile(x => x != null) .ToArray(); // 各メンバの現在値取得 var objs = members.Reverse() .Scan((object)obj, (x, m) => ((PropertyInfo)m.Member).GetValue(x)) .ToArray(); // 値を下層から順次更新 return (TObj)objs.Reverse().Skip(1) .Zip(members.Select(x => x.Member.Name)) .Aggregate((object)value, (state, x) => With(x.Item1, x.Item2, state)); } static object With(object obj, string propertyName, object value) { var type = obj.GetType(); var clone = type.GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(obj, null); type.GetField($"<{propertyName}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(clone, value); return clone; }
利用
冒頭のデータに対し下記の様に記述することにより、Richardの年齢が11に更新された新たなデータが作成されます。
var newPerson = person.With(x => x.Child.Age, 11);