Jak znaleźć metodę, która wywołała bieżącą metodę?

503

Jak zalogować się w języku C #, jak mogę poznać nazwę metody, która wywołała bieżącą metodę? Wiem wszystko System.Reflection.MethodBase.GetCurrentMethod(), ale chcę przejść o krok dalej w śladzie stosu. Zastanawiałem się nad parsowaniem śladu stosu, ale mam nadzieję znaleźć bardziej przejrzysty sposób, coś w rodzaju Assembly.GetCallingAssembly()metod.

flipdoubt
źródło
22
Jeśli korzystasz z .net 4.5 beta +, możesz użyć CallerInformation API .
Rohit Sharma
5
Informacje o dzwoniącym są również znacznie szybsze
dove
4
I stworzył szybki BenchmarkDotNet odniesienia z trzech głównych metod ( StackTrace, StackFramei CallerMemberName) i publikowane wyniki jako GIST, aby inni mogli zobaczyć tutaj: gist.github.com/wilson0x4d/7b30c3913e74adf4ad99b09163a57a1f
Shaun Wilson

Odpowiedzi:

512

Spróbuj tego:

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

jednowarstwowy:

(new System.Diagnostics.StackTrace()).GetFrame(1).GetMethod().Name

Pochodzi z metody Get Calling Method przy użyciu Reflection [C #] .

Firas Assaad
źródło
12
Możesz także utworzyć potrzebną ramkę, a nie cały stos:
Joel Coehoorn
187
new StackFrame (1) .GetMethod (). Nazwa;
Joel Coehoorn
12
Nie jest to jednak całkowicie wiarygodne. Zobaczmy, czy to działa w komentarzu! Wypróbuj poniższe w aplikacji konsolowej, a zobaczysz, że optymalizacje kompilatora ją psują. static void Main (string [] args) {CallIt (); } private static void CallIt () {Final (); } static void Final () {StackTrace trace = new StackTrace (); StackFrame frame = trace.GetFrame (1); Console.WriteLine ("{0}. {1} ()", frame.GetMethod (). DeclaringType.FullName, frame.GetMethod (). Nazwa); }
BlackWasp
10
Nie działa to, gdy kompilator inline lub tail-call optymalizuje metodę, w którym to przypadku stos jest zwinięty i znajdziesz inne wartości niż oczekiwano. Jeśli użyjesz tego tylko w kompilacjach debugowania, będzie to działać dobrze.
Abel
46
W przeszłości dodałem atrybut kompilatora [MethodImplAttribute (MethodImplOptions.NoInlining)] przed metodą, która będzie wyszukiwać ślad stosu. Zapewnia to, że kompilator nie będzie wstawiał metody bezpośrednio w linii, a ślad stosu będzie zawierał prawdziwą metodę wywoływania (w większości przypadków nie martwię się o rekurencję ogona).
Jordan Rieger
363

W C # 5 możesz uzyskać te informacje za pomocą informacji o dzwoniącym :

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

Możesz także uzyskać [CallerFilePath]i [CallerLineNumber].

Moneta
źródło
13
Witaj, to nie jest C # 5, jest dostępny w 4.5.
AFract
35
Wersje @AFract Language (C #) nie są takie same jak wersja .NET.
kwesolowski
6
@stuartd Wygląda na to, że [CallerTypeName]został usunięty z obecnego frameworku .Net (4.6.2) i Core CLR
Ph0en1x 13.04.16
4
@ Ph0en1x nigdy nie było w ramach, miałem na myśli, że byłoby to przydatne, gdyby tak było, np. Jak uzyskać nazwę typu CallerMember
stuartd
3
@DiegoDeberdt - Przeczytałem, że korzystanie z tego nie ma negatywnych skutków, ponieważ wykonuje całą pracę w czasie kompilacji. Uważam, że jest dokładne co do tego, co nazywa się tą metodą.
cchamberlain
109

Możesz użyć informacji o dzwoniącym i opcjonalnych parametrów:

public static string WhoseThere([CallerMemberName] string memberName = "")
{
       return memberName;
}

Ten test ilustruje to:

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

Chociaż StackTrace działa dość szybko powyżej i w większości przypadków nie będzie to problem z wydajnością, informacje o dzwoniącym są nadal znacznie szybsze. W próbce 1000 iteracji taktowałem ją 40 razy szybciej.

gołąb
źródło
Dostępne tylko z .Net 4.5
DerApe,
1
Zauważ, że to nie działa, jeśli wywołujący przejdzie agrument: CachingHelpers.WhoseThere("wrong name!");==>, "wrong name!"ponieważ CallerMemberNamejest to tylko podstawia wartość domyślną.
Olivier Jacot-Descombes,
@ OlivierJacot-Descombes nie działa w ten sam sposób, w jaki metoda rozszerzenia nie działałaby, gdyby przekazano mu parametr. możesz pomyśleć o innym parametrze łańcuchowym, którego można użyć. Zauważ też, że resharper dałby ci ostrzeżenie, gdybyś próbował przekazać argument tak jak ty.
gołąb
1
@dove możesz przekazać dowolny jawny thisparametr do metody rozszerzenia. Ponadto Olivier ma rację, można przekazać wartość i [CallerMemberName]nie jest ona stosowana; zamiast tego działa jako nadpisanie, w którym normalnie byłaby używana wartość domyślna. W rzeczywistości, jeśli spojrzymy na IL, zobaczymy, że uzyskana metoda nie różni się od tego, co normalnie byłoby emitowane dla [opt]arg, wstrzyknięcie CallerMemberNamejest zatem zachowaniem CLR. Na koniec dokumenty: „Atrybuty informacji o abonencie wywołującym [...] wpływają na domyślną wartość, która jest przekazywana, gdy argument jest pominięty
Shaun Wilson
2
Jest to idealne i asyncprzyjazne, które ci StackFramenie pomoże. Nie wpływa również na wywołanie z lambda.
Aaron,
65

Szybkie podsumowanie 2 podejść, przy czym ważnym elementem jest porównanie prędkości.

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

Określanie dzwoniącego w czasie kompilacji

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);
}

Określanie dzwoniącego za pomocą stosu

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);
}

Porównanie dwóch podejść

Time for 1,000,000 iterations with Attributes: 196 ms
Time for 1,000,000 iterations with StackTrace: 5096 ms

Widzisz, używanie atrybutów jest znacznie, dużo szybsze! Prawie 25 razy szybszy.

Tikall
źródło
Ta metoda wydaje się lepszym podejściem. Działa również w Xamarin bez problemu, że przestrzenie nazw są niedostępne.
lyndon hughey,
63

Możemy nieco ulepszyć kod pana Assada (obecna akceptowana odpowiedź), tworząc tylko potrzebną ramkę, a nie cały stos:

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

Może to działać nieco lepiej, choć najprawdopodobniej nadal musi użyć pełnego stosu, aby utworzyć tę pojedynczą ramkę. Ponadto nadal ma te same zastrzeżenia, które wskazał Alex Lyman (optymalizator / kod natywny może uszkodzić wyniki). Wreszcie możesz sprawdzić, aby upewnić się, że nie , new StackFrame(1)lub .GetFrame(1)nie powrócić null, tak mało prawdopodobne, jak mogłoby się wydawać.

Zobacz to pokrewne pytanie: czy możesz użyć refleksji, aby znaleźć nazwę aktualnie wykonywanej metody?

Joel Coehoorn
źródło
1
czy to w ogóle możliwe, że new ClassName(…)wynosi zero?
Nazwa wyświetlana
1
Zaletą jest to, że działa to również w .NET Standard 2.0.
srsedate
60

Ogólnie rzecz biorąc, możesz użyć System.Diagnostics.StackTraceklasy, aby uzyskać a System.Diagnostics.StackFrame, a następnie użyć GetMethod()metody, aby uzyskać System.Reflection.MethodBaseobiekt. Istnieją jednak pewne zastrzeżenia dotyczące tego podejścia:

  1. Reprezentuje stos środowiska wykonawczego - optymalizacje mogłyby wstawić metodę i nie zobaczysz tej metody w śladzie stosu.
  2. To będzie nie wykazują żadnych rodzimych ramek, więc jeśli jest nawet szansa metoda jest wywoływana przez native metody, będzie to nie praca, i nie będzie już w rzeczywistości nie ma obecnie dostępny sposób to zrobić.

( UWAGA: Właśnie rozwijam odpowiedź udzieloną przez Firas Assad .)

Alex Lyman
źródło
2
Czy w trybie debugowania z wyłączonymi optymalizacjami można zobaczyć, jaka metoda znajduje się w śladzie stosu?
AttackingHobo
1
@AttackingHobo: Tak - chyba że metoda jest wbudowana (optymalizacje włączone) lub natywna ramka, zobaczysz ją.
Alex Lyman
38

Począwszy od .NET 4.5 można używać atrybutów informacji o dzwoniącym :

  • CallerFilePath - Plik źródłowy, który wywołał funkcję;
  • CallerLineNumber - Wiersz kodu, który wywołał funkcję;
  • CallerMemberName - Członek, który wywołał funkcję.

    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);
    }

 

Ta funkcja jest również dostępna w „.NET Core” i „.NET Standard”.

Bibliografia

  1. Microsoft - informacje o dzwoniącym (C #)
  2. Microsoft - CallerFilePathAttributeklasa
  3. Microsoft - CallerLineNumberAttributeklasa
  4. Microsoft - CallerMemberNameAttributeklasa
Ivan Pinto
źródło
15

Pamiętaj, że takie postępowanie będzie niewiarygodne w kodzie wersji ze względu na optymalizację. Ponadto uruchomienie aplikacji w trybie piaskownicy (udział sieciowy) w ogóle nie pozwala na uchwycenie ramki stosu.

Zastanów się nad programowaniem aspektowym (AOP), takim jak PostSharp , który zamiast być wywoływany z kodu, modyfikuje kod, a tym samym wie, gdzie on jest.

Lasse V. Karlsen
źródło
Masz całkowitą rację, że to nie zadziała w wydaniu. Nie jestem pewien, czy podoba mi się pomysł wprowadzenia kodu, ale w pewnym sensie instrukcja debugowania wymaga modyfikacji kodu, ale nadal. Dlaczego nie wrócić do makr C? To przynajmniej coś, co możesz zobaczyć.
ebyrob,
9

Oczywiście jest to późna odpowiedź, ale mam lepszą opcję, jeśli możesz używać .NET 4.5 lub więcej:

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

Spowoduje to wydrukowanie bieżącej daty i godziny, a następnie „Namespace.ClassName.MethodName” i kończące się na „: text”.
Przykładowe dane wyjściowe:

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

Przykładowe użycie:

Logger.WriteInformation<MainWindow>("MainWindow initialized");
Camilo Terevinto
źródło
8
/// <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;
}
Flandria
źródło
Ups, powinienem był nieco lepiej wyjaśnić param „MethodAfter”. Więc jeśli wywołujesz tę metodę w funkcji typu „log”, będziesz chciał pobrać tę metodę zaraz po funkcji „log”. więc wywołałbyś GetCallingMethod („log”). -Cheers
Flandria
6

Może szukasz czegoś takiego:

StackFrame frame = new StackFrame(1);
frame.GetMethod().Name; //Gets the current method name

MethodBase method = frame.GetMethod();
method.DeclaringType.Name //Gets the current class name
jesal
źródło
4
private static MethodBase GetCallingMethod()
{
  return new StackFrame(2, false).GetMethod();
}

private static Type GetCallingType()
{
  return new StackFrame(2, false).GetMethod().DeclaringType;
}

Fantastyczna klasa jest tutaj: http://www.csharp411.com/c-get-calling-method/

Tebo
źródło
StackFrame nie jest niezawodny. Przechodzenie w górę o „2 klatki” może łatwo cofnąć wywołania metod.
user2864740,
2

Innym podejściem, które zastosowałem, jest dodanie parametru do metody, o której mowa. Na przykład zamiast void Foo()użyj void Foo(string context). Następnie przekaż unikalny ciąg znaków, który wskazuje kontekst wywołania.

Jeśli potrzebujesz tylko dzwoniącego / kontekstu do opracowania, możesz usunąć paramprzed wysyłką.

GregUzelac
źródło
2

Aby uzyskać nazwę metody i nazwę klasy, spróbuj:

    public static void Call()
    {
        StackTrace stackTrace = new StackTrace();

        var methodName = stackTrace.GetFrame(1).GetMethod();
        var className = methodName.DeclaringType.Name.ToString();

        Console.WriteLine(methodName.Name + "*****" + className );
    }
ariański
źródło
1
StackFrame caller = (new System.Diagnostics.StackTrace()).GetFrame(1);
string methodName = caller.GetMethod().Name;

myślę, że wystarczy.

Caner
źródło
1

Spójrz na nazwę metody rejestrowania w .NET . Uwaga na używanie go w kodzie produkcyjnym. StackFrame może nie być wiarygodny ...

Yuval Peled
źródło
5
Miłe byłoby podsumowanie treści.
Peter Mortensen
1

Możemy również użyć lambda do znalezienia dzwoniącego.

Załóżmy, że masz zdefiniowaną przez siebie metodę:

public void MethodA()
    {
        /*
         * Method code here
         */
    }

i chcesz znaleźć dzwoniącego.

1 . Zmień podpis metody, abyśmy mieli parametr typu Action (Func również będzie działał):

public void MethodA(Action helperAction)
        {
            /*
             * Method code here
             */
        }

2 . Nazwy Lambda nie są generowane losowo. Reguła wydaje się wyglądać następująco:> <nazwa_programu_wywołania> __X, gdzie nazwa_wywołania jest zastępowana przez poprzednią funkcję, a X to indeks.

private MethodInfo GetCallingMethodInfo(string funcName)
    {
        return GetType().GetMethod(
              funcName.Substring(1,
                                funcName.IndexOf("&gt;", 1, StringComparison.Ordinal) - 1)
              );
    }

3 . Kiedy wywołujemy MethodA, parametr Action / Func musi zostać wygenerowany przez metodę wywołującą. Przykład:

MethodA(() => {});

4 . Wewnątrz MethodA możemy teraz wywołać funkcję pomocniczą zdefiniowaną powyżej i znaleźć MethodInfo metody wywołującej.

Przykład:

MethodInfo callingMethodInfo = GetCallingMethodInfo(serverCall.Method.Name);
smiron
źródło
0

Dodatkowe informacje do odpowiedzi Firas Assaad.

Użyłem new StackFrame(1).GetMethod().Name;w .net core 2.1 z iniekcją zależności i otrzymuję metodę wywoływania jako „Start”.

Próbowałem z [System.Runtime.CompilerServices.CallerMemberName] string callerName = "" i daje mi to prawidłową metodę wywoływania

cdev
źródło
-1
var callingMethod = new StackFrame(1, true).GetMethod();
string source = callingMethod.ReflectedType.FullName + ": " + callingMethod.Name;
Mauro Sala
źródło
1
Nie głosowałem, ale chciałem zauważyć, że dodanie tekstu wyjaśniającego, dlaczego opublikowałeś bardzo podobne informacje (lata później), może zwiększyć wartość pytania i uniknąć dalszego głosowania w dół.
Shaun Wilson