概要
プロパティの値が特定の場合は変更する、という処理は頻繁に発生すると思います。
これを簡略化するために、1つのラムダ式からプロパティの取得と変更を行う2つの式木を生成します。
これにより、1つのプロパティを指定するラムダ式を書くだけで値の取得と変更が出来るようになります。
具体例
まず、下のようなClassを考えてみます。
2つのプロパティFirstName
とLastName
があります。
ModifyIfNeed
メソッドを実行すると2プロパティを検証して、空白だったら代わりの文字を代入します。
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void ModifyIfNeed()
{
if (String.IsNullOrEmpty(this.FirstName))
{
this.FirstName = "John";
}
if (String.IsNullOrEmpty(this.LastName))
{
this.LastName = "Doe";
}
}
}
static void Main(string[] args)
{
Console.WriteLine("修正前");
var p = new Person();
Console.WriteLine($"({p.FirstName})-({p.LastName})");
Console.WriteLine("修正後");
p.ModifyIfNeed();
Console.WriteLine($"({p.FirstName})-({p.LastName})");
}
修正前
()-()
修正後
(John)-(Doe)
動作には問題ありませんが、ModifyIfNeed
メソッドの中身は重複しているので、これを解決していきます。
参照渡しでの解決(失敗)
まず思いつくのがプロパティを参照渡しですが、プロパティは参照渡しが出来ません。
このためこのコードはコンパイルできません。
public void ModifyIfNeed()
{
SetTextIfEmpty(ref FirstName, "John");//コンパイルエラー
SetTextIfEmpty(ref LastName, "Doe");//コンパイルエラー
}
//参照渡しで受け取った文字列が空白だった、別の文字列を代入する
private void SetTextIfEmpty(ref string value, string textEmpty)
{
if (String.IsNullOrEmpty(value))
{
value = "John";
}
}
現在の値と代入するデリゲートでの解決
そこで、現在の値と代わりの文字列を代入するデリゲートを渡して、残りは共通化します。
public void ModifyIfNeed()
{
SetTextIfEmpty(this.FirstName, () => this.FirstName = "John");
SetTextIfEmpty(this.LastName, () => this.LastName = "Doe");
}
//文字列が空白だったら、受け取ったデリゲートを実行する
private void SetTextIfEmpty(string value, Action assingAction)
{
if (String.IsNullOrEmpty(value))
{
assingAction();
}
}
最初よりは短く、重複も軽減されていますが、メソッドの引数にプロパティ名を2回書いているため、冗長に見えます。
式木を使った解決方法
そこでさらにこれをシンプルにするため、取得する式木だけ渡して、代入するデリゲートは中で生成してもらう方法を取ります。
public void ModifyIfNeed()
{
SetTextIfEmpty(() => this.FirstName, "John");
SetTextIfEmpty(() => this.LastName, "Doe");
}
//文字列を取得して空白だったら、生成した代入デリゲートを実行する
private void SetTextIfEmpty(Expression<Func<string>> propertySelector, string textEmpty)
{
Utilitiy.SetIf(
propertySelector,
predicate: x => String.IsNullOrEmpty(x),
inputValue: textEmpty);
}
使用する記述は簡潔になりましたが、式木を使うため処理は複雑なったので別クラスに移動しました。
public class Utilitiy
{
/// <summary>
/// 取得する式木を元に条件を満たしたら、代入を実行する
/// </summary>
/// <typeparam name="TValue">値の型</typeparam>
/// <param name="propertySelector">代入される対象を選択する式木 例:() => this.Name</param>
/// <param name="predicate">代入する条件</param>
/// <param name="inputValue">代入する値</param>
public static void SetIf<TValue>(Expression<Func<TValue>> propertySelector, Predicate<TValue> predicate, TValue inputValue)
{
//式木からデリゲート生成、生成したデリゲートを実行して現在の値を取得する
var value = propertySelector.Compile().Invoke();
//条件があてはまるなら、代入処理の実行
if (predicate(value))
{
//プロパティを取得する式木を材料にプロパティへの代入をする式木を組み立てて、デリゲート生成
var assignAction = Utilitiy.CreateAssignActionExpr(propertySelector).Compile();
//生成した代入デリゲートに値を指定して実行
assignAction.Invoke(inputValue);
}
}
/// <summary>
/// 取得する式木を元に代入式木を組み立てる
/// </summary>
/// <typeparam name="TValue">値の型</typeparam>
/// <param name="propertySelector">代入される対象を選択する式木 例:() => this.Name </param>
/// <returns>代入する式木 例: x=>this.Name=x </returns>
public static Expression<Action<TValue>> CreateAssignActionExpr<TValue>(Expression<Func<TValue>> propertySelector)
{
//生成される式:x
var inputExpr = Expression.Parameter(typeof(TValue), "x");
//生成される式:this.Name = x
var assignExpr = Expression.Assign(
propertySelector.Body,
inputExpr);
//生成される式:x => this.Name = x
return Expression.Lambda<Action<TValue>>(assignExpr, inputExpr);
}
}
上の方式ではデリゲートを使用していましたが、今度は直接デリゲートを使わず式木を使います。
式木からデリゲートへの変換は.Compile()
だけなので、簡単です。
難しいのは式木の組み立てで、コードだけだと何をやっているかイメージはつかみにくいと思いますのでなるべくコメントをつけておきました。
ポイントは概要にもあるように、取得する式木から代入する式木を組み立てるところです。
() => this.Name
↓Expressionであれこれ
x => this.Name = x
これにより使用時の記述を短くすることが出来ます。
なおこのメソッド自体はプロパティだけでなく、フィールドやローカル変数に対しても使用可能です。
注意点
式木からデリゲートを生成する部分は少し重いです。
パフォーマンスがネックになるのであれば、内部でデリゲートをキャッシュする必要があります。
参考
http://neue.cc/2011/04/20_317.html
http://blog.shos.info/archives/2013/06/csharp_expression4.html
環境
VisualStudio2017
.NET Framework 4.7