C#における呼び出し元のメソッドの取得方法

この記事は、 KENTEM TechBlog アドベントカレンダー2024 5日目、12月5日の記事です。

私のプロジェクトでは、C#を用い、メソッドの変数名そのものを取得する実装を作っていました。

しかし、可変長引数が含まれるメソッドでは少し工夫が必要でした。

今回は、C#において、呼び出し元のメソッド名を取得する方法をまとめました。

CallerMemberName属性を用いる

概要

C#では[]で囲うことでクラスやメンバーに追加情報を与える「属性」という機能があります。

例えば、[TestMethod]や[ObsoleteAttribute]といった属性は広く知られていると思います。

属性の中のCallerMemberNameを用いることで、本題にも記した呼び出し元のメソッド名を取得することができます。

learn.microsoft.com

下記に示すソースコードの①の例では、CallerMemberName属性が付いているGetCallerMemberName関数を呼び出しているのがTestMethod関数であることから、"TestMethod"という文字列がmessageに格納されています。

②の例でも同様に、GetCallerMemberName関数を呼び出しているのがTestPropertyプロパティであることから、"TestProperty"という文字列がmessageに格納されています。

using System.Runtime.CompilerServices;

public class Program
{

    static void Main(string[] args)
    {
        TestMethod(); // ①「TestMethod関数の値:TestMethod」と出力される
        Console.WriteLine(TestProperty);  // ②「TestPropertyの値:TestProperty」と出力される
    }

    private static void TestMethod()
    {
        var message = GetCallerMemberName();
        Console.WriteLine("TestMethod関数の値:" + message);
    }

    private static string TestProperty
    {
        get
        {
            var message = GetCallerMemberName();
            return "TestPropertyの値:" + message;
        }
    }

    private static string GetCallerMemberName([CallerMemberName] string memberName = "")
    {
        return memberName;
    }
}
TestMethod関数の値:TestMethod
TestPropertyの値:TestProperty

CallerMemberNameを用いた問題点

基本的には、CallerMemberName属性を用いることで、呼び出し元のメソッド名の取得が可能です。

しかし、CallerMemberName属性を用いる場合、可変長引数と併用することができない問題があります。

下記の例では、呼び出し元のメソッド名(=Main)と可変長引数をConsole.WriteLineで出力しようとしています。

ところが、出力結果を見てみると、呼び出し元のメソッド名であるMainではなく、可変長の第一引数が格納されています。

また、可変長引数の第二引数以降と第一引数が別々のConsole.WriteLineで出力されていることが確認でき、予期していない結果となっております。

using System.Runtime.CompilerServices;

public class Program
{
    static void Main(string[] args)
    {
        var message = GetCallerMemberName("aa", "bb", "cc", "dd");  // messageには"Main"を格納したいが、"aa"が格納されてしまう
        Console.WriteLine("関数名:" + message);
    }

    private static string GetCallerMemberName([CallerMemberName] string memberName = "", params string[] parameters)
    {
        foreach (var parameter in parameters)
        {
            Console.WriteLine(parameter);
        }
        return memberName;
    }
}
bb
cc
dd
関数名:aa

解決方法

呼び出し元のメソッドの取得と可変長引数の使用を同時にできないという課題があり、調査を行っておりました。

私は、2つのメソッドに分割するという方法で取り組みました。

メソッド名を取得をGetCallerMemberName、可変長引数の代入をParametersという名前で定義を行いました。

using System.Runtime.CompilerServices;

public class Program
{
    static void Main(string[] args)
    {
        var message = GetCallerMemberName().Parameters("aa", "bb", "cc", "dd");
        Console.WriteLine("関数名:" + message);
    }

    private static string GetCallerMemberName([CallerMemberName] string memberName = "")
    {
        return memberName;
    }
}

public static class ProgramExtensitons
{
    internal static string Parameters(this string methodName, params string[] parameters)
    {

        foreach (var parameter in parameters)
        {
           Console.WriteLine(parameter);
        }
        return methodName;
    }
}

出力結果

aa
bb
cc
dd
関数名:Main

StackTraceを用いた方法

概要

他には、StackTraceを用いた方法もあります。 StackTraceを用いることで、StackTraceが呼ばれるまでに呼び出された関数をスタックで取得することができます。

下記のソースコードでは、Mainをエントリーポイントとして、GrandParent, Parent, Child, GrandChild, GetCallerMemberNameの順で呼び出しを行っています。

GetCallerMemberName関数では、引数にindexを指定し、GetCallerMemberName関数から見てindex番目に呼ばれた関数名をConsole.WriteLineで出力しています。

そのため、GetCallerMemberName, GrandChild, Child, Parent, GrandParentという順番で出力されます。

using System.Diagnostics;
using System.Diagnostics;

public class Program
{

    static void Main(string[] args)
    {
        GrandParent();
    }

    private static void GrandParent()
    {
        Parent();
    }

    private static void Parent()
    {
        Child();
    }

    private static void Child()
    {
        GrandChild();
    }

    private static void GrandChild()
    {
        for (int i = 0; i <= 4; i++)
        {
            GetCallerMemberName(i);
        }
    }

    private static void GetCallerMemberName(int index)
    {
        var stackTrace = new StackTrace();
        var name = stackTrace.GetFrame(index).GetMethod().Name;
        Console.WriteLine(name);
        Console.WriteLine("---------------------------");
    }
}
GetCallerMemberName
---------------------------
GrandChild
---------------------------
Child
---------------------------
Parent
---------------------------
GrandParent
---------------------------

StackTraceを用いたメリット

StackTraceを用いれば、可変長引数が含まれる場合においても呼び出し元のメソッド名を取得が可能になります。

using System.Diagnostics;

public class Program
{
    static void Main(string[] args)
    {
        var message = GetCallerMemberName("aa", "bb", "cc", "dd");
        Console.WriteLine("関数名:" + message);
    }

    private static string GetCallerMemberName(params string[] parameters)
    {
        var stackTrace = new StackTrace();
        var name = stackTrace.GetFrame(1).GetMethod().Name;

        foreach (var parameter in parameters)
        {
            Console.WriteLine(parameter);
        }

        return name;
    }
}
aa
bb
cc
dd
関数名:Main

StackTraceを用いたデメリット

StackTraceのデメリットとして、実行時間の遅さが挙げられます。

下記のソースコードでiterationsの値を1×103から1×107まで10倍ずつ変化させたときの実行時間を下図に示します。

StackTraceの方が負荷が大きいことが読み取れます。

この理由として、StackTraceは文字列捜査や無駄なメモリ使用を行うことが挙げられます。

この問題を解決するため、C#5.0ではCallerMemberNameが登場しました。

StackTraceとCallerMemberNameの比較

using System.Diagnostics;
using System.Runtime.CompilerServices;

public class Program
{

    static void Main(string[] args)
    {
        const int iterations = 10000000;

        // Measure StackTrace performance
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int i = 0; i < iterations; i++)
        {
            GetCallerUsingStackTrace();
        }
        stopwatch.Stop();
        Console.WriteLine($"StackTrace: {stopwatch.ElapsedMilliseconds} ms");

        // Measure CallerMemberName performance
        stopwatch.Reset();
        stopwatch.Start();
        for (int i = 0; i < iterations; i++)
        {
            GetCallerUsingCallerMemberName();
        }
        stopwatch.Stop();
        Console.WriteLine($"CallerMemberName: {stopwatch.ElapsedMilliseconds} ms");


        string GetCallerUsingStackTrace()
        {
            StackTrace stackTrace = new StackTrace();
            StackFrame frame = stackTrace.GetFrame(1);
            return frame.GetMethod().Name;
        }

        string GetCallerUsingCallerMemberName([CallerMemberName] string callerName = "")
        {
            return callerName;
        }
    }

}

KENTEMでは、様々な拠点でエンジニアを大募集しています!
建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。
recruit.kentem.jp career.kentem.jp