如何找到调用当前方法的方法?

641

在C#中登录时,如何获取调用当前方法的方法名称?我了解System.Reflection.MethodBase.GetCurrentMethod(),但我希望在堆栈跟踪中再向下多走一步。我考虑过解析堆栈跟踪,但我希望找到一种更为明确的、更为简洁的方法,类似于Assembly.GetCallingAssembly() 但是适用于方法。


23
如果您正在使用 .net 4.5 beta 或更高版本,您可以使用CallerInformation API - Rohit Sharma
6
来电者信息也更快 - dove
9
我使用BenchmarkDotNet快速创建了三种主要方法(StackTrace, StackFrameCallerMemberName)的基准测试,并将结果发布在此处的gist链接中,供其他人查看:https://gist.github.com/wilson0x4d/7b30c3913e74adf4ad99b09163a57a1f。 - Shaun Wilson
如果你想查找方法被调用的位置,而不运行它,请记住,在通过反射调用方法时,Shift + F12 不起作用。有时候你需要使用 Ctrl+F 搜索方法名称字符串。 - David Klempfner
18个回答

654

试试这个:

using System.Diagnostics;
// Get call stack
StackTrace stackTrace = new StackTrace(); 
// Get calling method name
Console.WriteLine(stackTrace.GetFrame(1).GetMethod().Name);

一句话简介:
(new System.Diagnostics.StackTrace()).GetFrame(1).GetMethod().Name

15
你也可以只创建你需要的框架,而不是整个堆栈: - Joel Coehoorn
243
new StackFrame(1).GetMethod().Name; - Joel Coehoorn
14
然而这并不是完全可靠的。让我们在评论中看看是否有效!在控制台应用程序中尝试以下内容,您会发现编译器优化会破坏它。静态 void Main(string[] args) { CallIt(); }私有的静态 void CallIt() { Final(); }静态 void Final() { StackTrace trace = new StackTrace(); StackFrame frame = trace.GetFrame(1); Console.WriteLine("{0}.{1}()", frame.GetMethod().DeclaringType.FullName, frame.GetMethod().Name); } - BlackWasp
12
如果编译器进行内联或尾递归优化该方法时,这种方法就行不通了。此时堆栈会被折叠,你会发现其他值与预期值不同。但是,如果你只在Debug版本中使用它,它将工作得很好。 - Abel
51
过去我所做的是在寻找堆栈跟踪的方法前添加编译器属性 *[MethodImplAttribute(MethodImplOptions.NoInlining)]*。这样可以确保编译器不会内联该方法,并且堆栈跟踪将包含真实的调用方法(对于大多数情况,我不担心尾递归)。 - Jordan Rieger
显示剩余7条评论

510

在C# 5中,您可以使用调用方信息获取该信息:

//using System.Runtime.CompilerServices;
public void SendError(string Message, [CallerMemberName] string callerName = "") 
{ 
    Console.WriteLine(callerName + "called me."); 
} 

你还可以获取[CallerFilePath][CallerLineNumber]


15
你好,这不是C# 5,它在4.5版本中可用。 - AFract
62
C#语言版本与.NET版本不相同。 - kwesolowski
7
看起来 [CallerTypeName] 在当前的 .Net Framework(4.6.2)和 Core CLR 中被删除了。 - Ph0en1x
4
@Ph0en1x,它从未在框架中,我的观点是如果有的话会很方便,例如如何获取调用成员的类型名称 - stuartd
4
@DiegoDeberdt - 我读过使用这个东西没有反射副作用,因为它在编译时完成所有工作。我相信这是关于调用方法的准确描述。 - cchamberlain
显示剩余5条评论

130
您可以使用调用者信息和可选参数:
public static string WhoseThere([CallerMemberName] string memberName = "")
{
       return memberName;
}

这个测试说明了这一点:

[Test]
public void Should_get_name_of_calling_method()
{
    var methodName = CachingHelpers.WhoseThere();
    Assert.That(methodName, Is.EqualTo("Should_get_name_of_calling_method"));
}

虽然 StackTrace 在大多数情况下运行得相当快且不会影响性能,但 Caller Information 的速度仍然更快。在1000次迭代的示例中,我发现它要快40倍。


2
只有从 .Net 4.5 开始才可用。 - DerApe
1
请注意,如果调用者传递了参数,则此方法不起作用:CachingHelpers.WhoseThere("wrong name!"); ==> "wrong name!",因为 CallerMemberName 仅替换默认值。 - Olivier Jacot-Descombes
1
@dove 你可以将任何显式的 this 参数传递到扩展方法中。此外,Olivier 是正确的,你可以传递一个值,并且 [CallerMemberName] 不会被应用;相反,它作为一个覆盖功能,用于替代通常使用的默认值。事实上,如果我们查看 IL,我们可以看到生成的方法与对 [opt] arg 发出的正常方法没有区别,因此注入 CallerMemberName 是 CLR 的行为。最后,文档中提到:“调用者信息属性[...] 影响省略参数时传递的默认值”。 - Shaun Wilson
4
这很完美,而且适用于async操作,这一点是StackFrame无法帮助您的。此外,它不会影响从lambda函数中调用。 - Aaron
@GrigoryZhadko [CallerFilePath] 应该会给你那个。 - dove
显示剩余3条评论

99

两种方法的快速回顾,其中速度比较是重要的部分。

http://geekswithblogs.net/BlackRabbitCoder/archive/2013/07/25/c.net-little-wonders-getting-caller-information.aspx

在编译时确定调用者

static void Log(object message, 
[CallerMemberName] string memberName = "",
[CallerFilePath] string fileName = "",
[CallerLineNumber] int lineNumber = 0)
{
    // we'll just use a simple Console write for now    
    Console.WriteLine("{0}({1}):{2} - {3}", fileName, lineNumber, memberName, message);
}

使用栈来确定调用者

static void Log(object message)
{
    // frame 1, true for source info
    StackFrame frame = new StackFrame(1, true);
    var method = frame.GetMethod();
    var fileName = frame.GetFileName();
    var lineNumber = frame.GetFileLineNumber();

    // we'll just use a simple Console write for now    
    Console.WriteLine("{0}({1}):{2} - {3}", fileName, lineNumber, method.Name, message);
}

两种方法的比较

Time for 1,000,000 iterations with Attributes: 196 ms
Time for 1,000,000 iterations with StackTrace: 5096 ms
所以你看,使用属性要快得多!实际上快了近25倍。

1
这种方法似乎是更优越的方式。在 Xamarin 中也可以使用,而不会出现命名空间不可用的问题。 - lyndon hughey

70
我们可以在阿萨德先生的代码(当前被接受的答案)上稍微改进一下,只实例化我们实际需要的帧,而不是整个堆栈:

我们可以仅实例化实际需要的帧,而非整个堆栈,从而在阿萨德先生的代码(目前被接受的答案)上进行改进:

new StackFrame(1).GetMethod().Name;

尽管很可能仍需要使用完整的堆栈来创建单个帧,但这样做可能会表现得更好。此外,它仍然具有Alex Lyman指出的相同注意事项(优化器/本机代码可能会破坏结果)。最后,您可能需要检查确保new StackFrame(1).GetFrame(1)不返回null,尽管这种可能性似乎不太可能。

请参阅相关问题: 您可以使用反射找到当前正在执行的方法的名称吗?


1
new ClassName(…) 等于 null,这种情况真的可能吗? - Display Name
1
很好的是,这也适用于.NET Standard 2.0。 - srbrills

61
一般而言,您可以使用System.Diagnostics.StackTrace类来获取System.Diagnostics.StackFrame,然后使用GetMethod()方法获取System.Reflection.MethodBase对象。但是,这种方法存在一些注意事项
  1. 它表示运行时堆栈--优化可能会内联一个方法,并且您将看不到堆栈跟踪中的该方法。
  2. 它将不会显示任何本机框架,因此如果有可能您的方法被本机方法调用,那么这将无法工作,实际上目前没有可用的方法来实现它。

注意:我只是在扩展Firas Assad提供的答案。)


2
在关闭优化的调试模式下,您能否看到堆栈跟踪中的方法? - AttackingHobo
1
@AttackingHobo:是的——除非该方法被内联(开启优化)或是一个本地帧,否则你会看到它。 - Alex Lyman

53

从.NET 4.5开始,您可以使用Caller Information属性:

  • CallerFilePath - 调用函数的源文件;
  • CallerLineNumber - 调用函数的代码行数;
  • CallerMemberName - 调用函数的成员。

    public void WriteLine(
        [CallerFilePath] string callerFilePath = "", 
        [CallerLineNumber] long callerLineNumber = 0,
        [CallerMemberName] string callerMember= "")
    {
        Debug.WriteLine(
            "Caller File Path: {0}, Caller Line Number: {1}, Caller Member: {2}", 
            callerFilePath,
            callerLineNumber,
            callerMember);
    }
    

 

这个功能也存在于“.NET Core”和“.NET Standard”中。

参考资料

  1. Microsoft - Caller Information (C#)
  2. Microsoft - CallerFilePathAttribute
  3. Microsoft - CallerLineNumberAttribute
  4. Microsoft - CallerMemberNameAttribute

19

显然这是一个晚回答,但如果你可以使用.NET 4.5或更高版本,我有一个更好的选择:

internal static void WriteInformation<T>(string text, [CallerMemberName]string method = "")
{
    Console.WriteLine(DateTime.Now.ToString() + " => " + typeof(T).FullName + "." + method + ": " + text);
}

这将打印当前日期和时间,接着是"Namespace.ClassName.MethodName",最后以": text"结束。
示例输出:

6/17/2016 12:41:49 PM => WpfApplication.MainWindow..ctor: MainWindow initialized

使用示例:

Logger.WriteInformation<MainWindow>("MainWindow initialized");

很棒的回答。不过,很遗憾CallerMemberName必须作为参数定义。通常在日志方法的最后一个参数中传入params数组是很常见的,这使得将这两个东西结合起来变得困难。 - devklick

14
请注意,在发布代码中这样做将是不可靠的,因为会进行优化。此外,在沙盒模式(网络共享)下运行应用程序将无法完全抓取堆栈帧。
考虑面向方面编程(AOP),例如PostSharp,它不是从您的代码调用,而是修改您的代码,因此始终知道它在哪里。

你说得完全正确,这在发布版中是行不通的。我不确定我是否喜欢代码注入的想法,但我猜在某种程度上调试语句需要修改代码,但仍然如此。为什么不回到C宏呢?至少你可以看到它。 - ebyrob

9
/// <summary>
/// Returns the call that occurred just before the "GetCallingMethod".
/// </summary>
public static string GetCallingMethod()
{
   return GetCallingMethod("GetCallingMethod");
}

/// <summary>
/// Returns the call that occurred just before the the method specified.
/// </summary>
/// <param name="MethodAfter">The named method to see what happened just before it was called. (case sensitive)</param>
/// <returns>The method name.</returns>
public static string GetCallingMethod(string MethodAfter)
{
   string str = "";
   try
   {
      StackTrace st = new StackTrace();
      StackFrame[] frames = st.GetFrames();
      for (int i = 0; i < st.FrameCount - 1; i++)
      {
         if (frames[i].GetMethod().Name.Equals(MethodAfter))
         {
            if (!frames[i + 1].GetMethod().Name.Equals(MethodAfter)) // ignores overloaded methods.
            {
               str = frames[i + 1].GetMethod().ReflectedType.FullName + "." + frames[i + 1].GetMethod().Name;
               break;
            }
         }
      }
   }
   catch (Exception) { ; }
   return str;
}

哎呀,我应该更好地解释一下“MethodAfter”参数。所以,如果您在“log”类型函数中调用此方法,则需要获取紧接在“log”函数之后的方法。因此,您将调用GetCallingMethod(“log”)。-干杯 - Flanders

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接