🪩

【.NET】開発時に活用できる属性

に公開

はじめに

アプリやライブラリ開発時に地味に使える属性について、よく使うものから意外に使えていなかったものなど備忘録代わりに記載します。

デバッグを支援する属性

DebuggerDisplayAttribute

デバッガー変数ウィンドウのインスタンスの値表示をカスタマイズできる属性になります。引数に任意の文字列を設定したり、任意のフィールド、プロパティ、メソッドを設定することができるため、うまく使うことでデバッグ時に必要なインスタンスの値を表示することができます。

例えば、以下のIntWrapperクラスを実装した場合、DebuggerDisplayAttributeを追加しない状態だと型名(今回だとIntWrapper)が表示されます。

サンプルクラス
public class IntWrapper(int value)
{
    public int Value { get; } = value;
}

DebuggerDisplayAttribute1

ではDebuggerDisplayAttributeにValueプロパティを指定すると、Valueプロパティがデバッガー変数ウィンドウに表示されます。

DebuggerDisplayAttributeを追加
+ [DebuggerDisplay("{Value}")]
public class IntWrapper(int value)
{
    public int Value { get; } = value;
}

DebuggerDisplayAttribute2

デバッガー変数ウィンドウのインスタンスの値表示は、ToStringメソッドをオーバーライドすることでも変更することができますが、ToStringメソッドはロジックとして使用している場合も多く、デバッグ用の表示文字列が返却されるとは限らないので、私個人としてはDebuggerDisplayAttributeを使う方が明示的にもどういう値を表示するかわかりやすくなるため、こちらの使用を推したいところです。

https://learn.microsoft.com/en-us/visualstudio/debugger/using-the-debuggerdisplay-attribute?view=vs-2022

DebuggerTypeProxyAttribute

デバッガー変数ウィンドウのメンバー情報の表示内容を大きく変更したい場合にはDebuggerTypeProxyAttributeを使います。表示型ではなくDebuggerTypeProxyAttributeで指定したプロキシ型に定義したメンバー情報を表示するように変更できます。

例えば、IntWrapperクラス用のIntWrapperDebugViewクラスを作成します。

IntWrapperクラス用のプロキシクラス
public class IntWrapperDebugView(IntWrapper intWrapper)
{
    public string DebuggerDisplay => $"IntWrapper.Valueの値は{intWrapper.Value}";
}

それを[DebuggerTypeProxy(typeof(IntWrapperDebugView))]で追加し、デバッグで確認します。

DebuggerTypeProxyAttributeを追加
+ [DebuggerTypeProxy(typeof(IntWrapperDebugView))]
public class IntWrapper(int value)
{
    public int Value { get; } = value;
}

DebuggerDisplayAttribute3

このようにIntWrapperDebugViewクラスで定義したプロパティなどが表示されるようになります。
もちろんDebuggerDisplayAttributeと併用できるため、以下のようにカスタムすることもできます。

DebuggerDisplayAttributeとDebuggerTypeProxyAttributeを併用した場合
[DebuggerDisplay("{Value}")]
[DebuggerTypeProxy(typeof(IntWrapperDebugView))]
public class IntWrapper(int value)
{
    public int Value { get; } = value;
}

DebuggerDisplayAttribute4

ただし、DebuggerTypeProxyAttributeは幅広くカスタマイズできる分、不要な情報まで表示したり、表示用データを作成する処理が起因でデバッガーが重くなるなどもありうるので、注意が必要です。

https://learn.microsoft.com/en-us/dotnet/framework/debug-trace-profile/enhancing-debugging-with-the-debugger-display-attributes

DebuggerStepThroughAttribute

名前の通り、デバッグ時にブレイクポイントやステップ実行で止まらないようにする属性になります。ブレイクポイントで止めた後にステップ実行で変数などの状態を確認する時、一つずつ実行するため、単純なプロパティのゲッター/セッターや値を返すだけのメソッドなど多くの場合では確認する必要が無い処理まで止まってしまうことがあります。単純なプロパティやメソッドなどには追加しておくとデバッグ作業が多少快適になると思います。

https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/debugger/just-my-code?view=vs-2015&redirectedfrom=MSDN

コンパイラ関連属性

DoesNotReturnAttribute

指定したメソッドはどんな状況でも戻らないことを示す属性になります。主要な使うパターンとしては例外をスローするメソッドなどを作成する際に活用できます。

CS8602警告が発生するケース
public class Class1
{
    public void Test(string? args)
    {
        if (args == null)
        {
            // 直接 throwを書く場合は問題なし!    
            // throw new ArgumentNullException("引数がnullのため、例外をスローしました");
            ThrowArgumentNullException();
        }

        // 引数の値を使った処理を実装する。
        // CS8602警告が発生!
        var length = args.Length;
    }

    private void ThrowArgumentNullException()
    {
        throw new ArgumentNullException("引数がnullのため、例外をスローしました");
    }
}

上記のTestメソッドを例にすると、引数がnullの場合にはThrowArgumentNullExceptionメソッド経由で例外を発生し、それ以外では引数のプロパティにアクセスする処理を実装しています。
この場合、引数のプロパティにアクセスしている部分では、nullではないことが明らかですが、コンパイラはそれを判断できないため、CS8602警告(nullの可能性がある変数にアクセスしようとする際に発生する警告)が発生してしまいます。

このように、DoesNotReturnAttributeを追加することで、警告を出さないようにすることが可能です。

DoesNotReturnAttributeを追加
+   [DoesNotReturn]
    private void ThrowArgumentNullException()
    {
        throw new ArgumentNullException("引数がnullのため、例外をスローしました");
    }

実際に.NETランタイムでも例外スロー用のヘルパーメソッドであるArgumentNullException.ThrowIfNullメソッドなど同じようなケースで使用されています。

https://github.com/dotnet/runtime/blob/e1f19886fe3354963a4a790c896b3f99689fd7a5/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs#L54-L61
https://github.com/dotnet/runtime/blob/e1f19886fe3354963a4a790c896b3f99689fd7a5/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs#L95-L97

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis

MaybeNullWhenAttribute

メソッドの戻り値の値(bool型)によって、パラメータがnullの可能性があるかどうか指定する場合に使用します。

主な場面としては、Tryパターンを実装しているメソッドの引数に追加されている印象があります。.NETランタイムでも多々使用されていて、代表的なメソッドだとDictionary<TKey,TValue>.TryGetValueメソッドになり、戻り値にfalseを返す場合には引数がnullの可能性もあるとコンパイラに伝えています。

https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs#L1042-L1053

Dictionary<TKey,TValue>.TryGetValueメソッドだと、MaybeNullWhenAttributeが無い場合、defaultの部分でCS8601警告が発生します。

Dictionary<TKey,TValue>.TryGetValueメソッド
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
    ref TValue valRef = ref FindValue(key);
    if (!Unsafe.IsNullRef(ref valRef))
    {
        value = valRef;
        return true;
    }

    // MaybeNullWhenが無い場合、CS8601警告が発生!
    value = default;
    return false;
}

ライブラリ開発時にTryパターンを使うメソッドがある場合には、使っていきたいですね。

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis#postconditions-maybenull-and-notnull

NotNullWhenAttribute

メソッドの戻り値の値(bool型)によって、パラメータがnullではないことを指定する場合に使用します。
以下のソースコードだと、TryAppendメソッドが成功時には必ず引数に文字列が設定されていますが、引数の型がnull許容型になっていることもあり、CS8602警告が発生します。

CS8602警告が発生するケース
public bool TryAppend(string? s, out string? s2)
{
    if (s == null)
    {
        s2 = null;
        return false;
    }
    s2 = s + "World!";
    return true;
}

public void Test()
{
    if (TryAppend("Hello", out string? s2))
    {
        // CS8602警告!
        var length = s2.Length;
    }
}

そのため[NotNullWhen(bool)]を追加することで、戻り値にtrueを返す場合には引数がnullではないとコンパイラに伝えることで、CS8602警告を消すことができます。

NotNullWhenを追加
- public bool TryAppend(string? s, out string? s2)
+ public bool TryAppend(string? s, [NotNullWhen(true)] out string? s2)

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis#conditional-post-conditions-notnullwhen-maybenullwhen-and-notnullifnotnull

ObsoleteAttribute

これは非推奨になった型やメソッドなどに追加することで、警告やコンパイルエラーを出すことができる属性です。古いAPIを互換性として残す際に非推奨として知らせる場合や破壊的変更を行った場合に使用します。
機能には、非推奨の理由と回避策を表示するメッセージとコンパイラエラーを発生するかどうかを指定する情報以外に、省略可能プロパティとしてDiagnosticIdプロパティ(診断用ID)とUrlFormatプロパティもあり、こちらはVisual Studioだとエラー一覧ウィンドウのコード列の情報をカスタマイズするものになります。

設定例
[Obsolete(message:"非推奨の理由と回避策を記載します", error:false, DiagnosticId = "診断用ID", UrlFormat = "https://contoso.com/obsoletion-warnings/{0}")]

https://learn.microsoft.com/en-us/dotnet/api/system.obsoleteattribute?view=net-9.0

まとめ

今回上げた属性は、パフォーマンスが向上するものではなく、開発時の効率を上げたり、警告を出さないようにするものになります。こういうものは、自発的に調べたり、著名なOSSのソースコードなどを読まないと知る機会がないので、今後も継続的に情報収集は必要ですね。

Discussion