C# の CallerMemberName で呼び出し元を取得する

ログやデバッグメッセージに「どのメソッドから呼ばれたか」を含めたいとき、呼び出し元の名前をハードコードするのは保守性が悪い。メソッド名を変更するたびにログ文字列も直す必要があるからだ。CallerMemberName 属性を使えば、コンパイラが呼び出し元のメンバー名を自動的に埋め込んでくれる。

基本的な使い方

CallerMemberNameSystem.Runtime.CompilerServices 名前空間に属する属性で、省略可能パラメータに付与して使う。

using System;
using System.Runtime.CompilerServices;

public class Logger
{
    public static void Log(
        string message,
        [CallerMemberName] string memberName = "")
    {
        Console.WriteLine($"[{memberName}] {message}");
    }
}

このメソッドを呼び出すと、第 2 引数を明示しなくても呼び出し元のメンバー名が自動で入る。

public class UserService
{
    public void CreateUser(string name)
    {
        Logger.Log("ユーザー作成開始");
        // 処理...
        Logger.Log("ユーザー作成完了");
    }
}

出力は [CreateUser] ユーザー作成開始[CreateUser] ユーザー作成完了 になる。文字列のハードコードが不要なので、メソッド名をリファクタリングしても自動的に追従する。

関連する属性

CallerMemberName の他にも、呼び出し元の情報を取得する属性が用意されている。

CallerMemberName

呼び出し元のメソッド名やプロパティ名を取得する。最もよく使われる。

CallerFilePath

呼び出し元のソースファイルのフルパスを取得する。ファイルの特定に便利だが、パスが長くなりやすい。

CallerLineNumber

呼び出し元の行番号を取得する。型は int で、デフォルト値は 0 を指定する。

これらを組み合わせると、詳細なログが取れる。

public static void DetailedLog(
    string message,
    [CallerMemberName] string member = "",
    [CallerFilePath] string file = "",
    [CallerLineNumber] int line = 0)
{
    Console.WriteLine($"{file}({line}) [{member}] {message}");
}

出力例は C:\Projects\MyApp\UserService.cs(15) [CreateUser] ユーザー作成開始 のようになる。Visual Studio のエラー一覧と同じ形式にしておくと、クリックで該当行にジャンプできるため実用的だ。

INotifyPropertyChanged での活用

CallerMemberName が最も威力を発揮するのは、WPF や MAUI のデータバインディングで使う INotifyPropertyChanged の実装だろう。

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

プロパティの setter から OnPropertyChanged() を引数なしで呼ぶだけで、そのプロパティ名が自動的に渡される。

public class UserViewModel : ViewModelBase
{
    private string _name = "";
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();  // "Name" が自動で渡される
        }
    }

    private int _age;
    public int Age
    {
        get => _age;
        set
        {
            _age = value;
            OnPropertyChanged();  // "Age" が自動で渡される
        }
    }
}

CallerMemberName がなかった頃は OnPropertyChanged("Name") のように文字列を渡していた。タイプミスしてもコンパイルエラーにならないため、バインディングが動かない原因を探すのに苦労したものだ。

文字列指定(旧来の方法)

OnPropertyChanged("Name") のように手書きする。リネーム時に追従せず、タイプミスが検出できない。

CallerMemberName(現在の方法)

引数なしで呼ぶだけ。コンパイラが正確なプロパティ名を埋め込むため、ミスが起きない。

コンパイル時に解決される

CallerMemberName はリフレクションではなく、コンパイル時の置換で実現されている。IL レベルでは単なる文字列定数として埋め込まれるため、実行時のパフォーマンスコストはゼロだ。リフレクションで MethodBase.GetCurrentMethod() を使う方法と比べると、速度面で圧倒的に有利になる。

コンパイラが呼び出し元のメンバー名を特定

省略可能パラメータのデフォルト値を文字列定数で置換

実行時は通常の文字列引数として処理

この仕組みのおかげで、ログ出力やプロパティ変更通知のようなホットパスでも安心して使える。呼び出し元の情報が必要な場面では、まず CallerMemberName 系の属性を検討するのがよいだろう。