【C#】リフレクションでnullチェックを自動化する

リフレクションを使って null チェックを自動化する方法です。

C# で null チェックをする場合以下のようなコードを書くと思います。

// サンプル用のクラスと変数宣言
public class Sample
{
   public int A { get; set; }
   public int B { get; set; }
}
var s1 = new Sampme();

// 中身のフィールドをチェックする
if(s1.A is null)  Console.WriteLine("Aはnullです");
if(s1.B is null)  Console.WriteLine("Bはnullです");

もしくはオブジェクト自身にチェック機能を設けたり、「null許容参照型」を使用してnullに対処する事もできます(もっとも null 許容型参照を実装の途中から有効化した場合既存コードに対する影響が少々あるのでできればやりたくないですが…)今回は、そういった方方法ではなくリフレクションを使って null を許可していないプロパティやフィールドに null が設定されているかのチェックする方法の紹介をしたいと思います。

確認環境

確認環境は以下の通りです。

  • .NET Core3.1 + C# 8.0
  • VisuaStudio2019
  • Windows10

使用方法とサンプル

まずは成果物の実装の使い方と説明をしたいと思います。

使用するにあたって以下の条件があります。

  • チェック対象のオブジェクトは INullCheckable を継承していること
  • null チェックしたいフィールドもしくはプロパティに NotNullAttribute を付けること
    • あくまで自作の型を対象にしたチェックなので両方必要にしています
    • この制限はコードを変更すれば廃止できます

で、使い方は以下の通り。

// まず比較したいクラスを以下のように宣言する

public class Root : INullCheckable  // ★(1) null チェックできる型はインターフェースを継承する
{
    // ★(2) チェック較したいプロパティやフィールドには NotNull 属性を付ける
    [NotNull] public string Name1 { get; set; } = ""; 
    public string Name2 { get; set; }
    [NotNull] public int? Value1 { get; set; } = 0;
    public int? Value2 { get; set; }
    [NotNull] public Child1 Child1 { get; set; } = new Child1();
    [NotNull] public Child1 Child2 { get; set; }
    public Child1 Child3 { get; set; }
    public Child1 Child4 { get; set; } = new Child1();
}

public class Child1 : INullCheckable // 入れ子になるクラスも同様
{
    [NotNull] public string ChildName1 { get; set; }
    public string ChildName2 { get; set; }
}

チェックは以下のように行います。

public static void Main(params string[] args)
{
    Root obj = new Root();
    bool result = obj.ValidateNull(); // チェックできる型はメソッドが呼び出せるようになっている

    Console.WriteLine(result); // true : チェックOK / false : null を検出した
}

実装コード

実装コードは以下の通りです。

ちなみに、面倒から解放される代わりに注意点があって、リフレクションなので処理速度がすっごい遅いです。1秒間に1000回とか呼び出して使用するとパフォーマンスが残念な可能性があります。

INullCheckable

null チェック対象するクラスに付与するインターフェースの定義です。何もメソッドが無いマーク用のインターフェースです。

// INullCheckable.cs

/// <summary>
/// null チェックを行うオブジェクトを表すマーカーインターフェースです。
/// </summary>
public interface INullCheckable { }

NotNullAttribute

null チェック対象のプロパティ or フィールドに付与するための属性です。

// NotNullAttribute.cs

/// <summary>
/// null チェックするプロパティもしくはフィールドを指定します。
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class NotNullAttribute : Attribute { }

INullCheckableExtension

コード例の「bool result = obj.ValidateNull();」できるようにするための実装です。

プロパティとフィールドを全部列挙して比較する。INullCheckable があれば再帰してさらに中身を比較する処理になっています。

/// <summary>
/// <see cref="INullCheckable"/> に対する拡張メソッドを定義します。
/// </summary>
public static class INullCheckableExtension
{
    /// <summary>
    /// 指定したオブジェクトと内容を比較します。
    /// </summary>
    public static bool ValidateNull<T>(this T self) where T : INullCheckable
    {
        if (self == null)
        {
            throw new ArgumentNullException(); // ルート要素がnullは処理対象にしない
        }
        return check(self);
    }

    // 再帰的にオブジェクトをNullチェックする処理
    private static bool check<T>(T self) where T : INullCheckable
    {
        var type = self.GetType();

        // 対象プロパティの列挙
        foreach (PropertyInfo p in getProperties(type))
        {
            var value = p.GetValue(self);
            
            var att = p.GetCustomAttribute<NotNullAttribute>();
            if (att != null && value is null)
            {
                Console.WriteLine(p.Name);
                return false;
            }

            if (value is INullCheckable obj)
            {
                if (!check(obj))
                {
                    return false;
                }
            }
        }

        // 対象フィールドの列挙
        foreach (FieldInfo f in getFields(type))
        {
            var value = f.GetValue(self);

            var att = f.GetCustomAttribute<NotNullAttribute>();
            if (att != null && value is null)
            {
                return false;
            }

            if (value is INullCheckable obj)
            {
                if (!check(obj))
                {
                    return false;
                }
            }
        }
        
        return true;
    }

    // 対象プロパティを全部列挙
    public static IEnumerable<PropertyInfo> getProperties(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetProperties(attributes);
    }

    // 対象フィールドを全部列挙
    public static IEnumerable<FieldInfo> getFields(Type type)
    {
        const BindingFlags attributes =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        return type.GetFields(attributes);
    }
}

以上です。