PG日誌

受託系 PG が C# の事を書いています

C#でstringを一括でEmptyに初期化する(Refrection使用)

C# で例えば string のプロパティが 100個あるクラスがあった場合、コンストラクタで1つ1つメンバーへ手で初期化を記述するは、大変手間な上に、初期化が漏れているなどのケースがあります *1。また、受け取ったクラスのstringがnullのままでstring.Emptyで初期化したほうがいいケースが稀にあります。もちろん、普通にテスト済みならば、そういったこと事にならないハズですが、使用した際に 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

というわけで、リフレクションを用いて一気に初期化してしまおうかと思います。

サポートするケース

初期化する際に求められる基本的な性能を以下の通り想定します。

  1. 指定したオブジェクトの public な Property を全て string.Empty で初期化できる。
  2. Prperty は set 操作が public なものだけを対象とする。
  3. ネストしている参照オブジェクトも初期化できる。
  4. 循環参照している場合、一度初期化してるものは再初期化しない。
  5. 配列とリストも初期化を行う。

また、作成の際に考慮しないことは以下の通りです。

  1. パフォーマンスと処理効率(リフレクションを使用して初期化を行う予定)
  2. 最大処理件数の考慮(相互参照以外で、MemoryOverflow, StakOverflow の可能性は考慮しない)

です。

コード例

利用者側コードは以下の通りです。

public void Main(string[] args)
{
    var target = new Sample();

    MemberUtil.SetEmptyToString(target); // ここで全てのフィールドが初期化される
}

実際の処理の中身ですが、以下の通りです。

public static class MemberUtil
{
    //
    // Public Methods
    // - - - - - - - - - - - - - - - - - - - -
    
    public static void SetEmptyToString(object target)
    {
        if (target == null)
        {
            throw new ArgumentNullException(nameof(target));
        }

        setEmptyToString(target, new List<object>());
    }

    //
    // Private Methods
    // - - - - - - - - - - - - - - - - - - - -

    private static void setEmptyToString(object target, IList<object> attackedList)
    {
        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))
            {
                setEmptyToProperty(info, target);
            }
            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);
                }
            }
            else if(info.PropertyType.IsClass)
            {
                object item = info.GetValue(target);
                if (item == null)
                {
                    continue;
                }

                setEmptyToString(item, attackedList);
            }
        }
    }

    // 指定した1つのプロパティにstring.Emptyを設定する
    private static void setEmptyToProperty(PropertyInfo info, object target)
    {
        if (info.PropertyType == typeof(string))
        {
            if (!info.SetMethod.IsPublic)
            {
                return;
            }

            info.SetValue(target, string.Empty);
        }
    }
}

使用するときの課題

当初の目的は達成できましたが、この操作、自分でメンバーを初期化するよりコストが 23 ~ 27 倍程度かかります。100万件で試したところ、コンストラクタで初期化が0.2秒 vs リフレクションで初期化は5秒と、かなり開きのある結果になってしました。とは言っても、リフレクションを使用しているので当然の結果かと思います。

*1:そもそもそういう設計をするなと言う意見はここでは置いておきます。

*2:あくまで私見です。