C#でstringの初期値はnullです。変数を宣言し下だけではnullが設定されます。ただ、プロパティの初期値にはstring.Emptyにしておきたい場合が良くあります。(nullは設定したくないなども割とよくあります。
そこで、クラスにstringのプロパティが数十個以上あるクラスの場合、コンストラクタやメンバーイニシャライザーで全てに対して値を設定すると結構な手間な上に、初期化が漏れているなどのケースがあります *1。
単体テストも面倒ですし、抜けていてあとから例外が起きる可能性もあります。もちろん、普通に単体テスト済みならば、そういったこと事にならないハズですが、以前自分でも NullReferenceException が発生したことがあり、最確認が面倒だったので、外から一律初期化する仕組みを考えたいと思います。
想定してる状況としては以下の状況です。
public class ManyStringProps { public string Name001 { get; set; } public string Name002 { get; set; } public string Name003 { get; set; } public string Name004 { get; set; } public string Name005 { get; set; } //... この後もずっと続く public ManyStringProps() { this.Name001 = string.Empty; this.Name002 = string.Empty; this.Name003 = string.Empty; this.Name004 = string.Empty; //... ここもずっと続く、たまに初期化されてないプロパティがある } }
最近はプロパティをインラインで初期化できるので以下のような書き方もで、自分でクラスを作成するときの状況は良くなって来ていると思います。
// C# 6.0 + VisualStudio2017からは以下のように記述できる public string Name001 { get; set; } = string.Empty; public string Name002 { get; set; } = string.Empty;
とはいえ、最近の言語の流行りを考えると string の初期値が null かつ、nullが代入可能な(事実上ほぼ)プリミティブ型な時点で割と言語バグの領域かと思います *2。
というわけで、リフレクションを用いて一気に初期化してしまおうかと思います。
サポートするケース
初期化する際に求められる基本的な性能を以下の通り想定します。
- 指定したオブジェクトの public な Property を全て string.Empty で初期化できる。
- Prperty は set 操作が public なものだけを対象とする。
- ネストしている参照オブジェクトも初期化できる。
- 循環参照している場合、一度初期化してるものは再初期化しない。
- 配列とリストも初期化を行う。
また、作成の際に考慮しないことは以下の通りです。
- パフォーマンスと処理効率(リフレクションを使用して初期化を行う予定)
- 最大処理件数の考慮(相互参照以外で、MemoryOverflow, StakOverflow の可能性は考慮しない)
です。
コード例
利用者側コードは以下の通りです。
public void Main(string[] args) { var target = new Sample(); MemberUtil.SetEmptyToString(target); // ここで全てのフィールドが初期化される }
実際の処理の中身ですが、以下の通りです。
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; public static class MemberUtil { // // Public Methods // - - - - - - - - - - - - - - - - - - - - public static void InitStringProps(object target, string initialValue = "") { if (target == null) { throw new ArgumentNullException(nameof(target)); } if (initialValue == null) { throw new ArgumentNullException("null is meaningless."); // 意味ないので例外にする } setEmptyToString(target, new List<object>(), initialValue); } // // Private Methods // - - - - - - - - - - - - - - - - - - - - private static void setEmptyToString(object target, IList<object> attackedList, string initialValue = "") { if (attackedList.Contains(target)) { return; // 識別済みプロパティは処理しない } attackedList.Add(target); // 手を付けたものを記憶しておく PropertyInfo[] infoList = target.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); // 対象プロパティの列挙 foreach (PropertyInfo info in infoList) { if (info.PropertyType == typeof(string)) { setStringToProperty(info, target, initialValue); } else if (info.PropertyType.GetInterfaces().Contains(typeof(IEnumerable))) { object arrayItem = info.GetValue(target); if (arrayItem == null) { continue; } foreach (object inner in arrayItem as IEnumerable) { setEmptyToString(inner, attackedList, initialValue); } } else if (info.PropertyType.IsClass) { object item = info.GetValue(target); if (item == null) { continue; } setEmptyToString(item, attackedList, initialValue); } } } // 指定した1つのプロパティにstring.Emptyを設定する private static void setStringToProperty(PropertyInfo info, object target, string value) { if (info.PropertyType == typeof(string)) { if (!info.SetMethod.IsPublic) { return; // public メソッドじゃないものは除外 } string _str = info.GetValue(target) as string; if (_str != null) { return; // 何か値が入っているものは除外 } info.SetValue(target, value); } } }
使用するときの課題
当初の目的は達成できましたが、この操作、自分でメンバーを初期化するよりコストが 23 ~ 27 倍程度かかります。100万件で試したところ、コンストラクタで初期化が0.2秒 vs リフレクションで初期化は5秒と、かなり開きのある結果になってしました。とは言っても、リフレクションを使用しているので当然の結果かと思います。