Co powoduje, że debuger programu Visual Studio przestaje oceniać zastąpienie ToString?

221

Środowisko: Visual Studio 2015 RTM. (Nie próbowałem starszych wersji).

Ostatnio debugowałem część mojego kodu Noda Time i zauważyłem, że kiedy mam lokalną zmienną typu NodaTime.Instant(jeden z głównych structtypów w Noda Time), okna „Locals” i „Watch” nie wydaje się nazywać jego ToString()zastąpienia. Jeśli dzwonię ToString()jawnie w oknie zegarka, widzę odpowiednią reprezentację, ale poza tym widzę tylko:

variableName       {NodaTime.Instant}

co nie jest zbyt przydatne.

Jeśli zmienię przesłonięcie, aby zwracać stały ciąg, ciąg jest wyświetlany w debuggerze, więc jest w stanie wyraźnie zauważyć, że tam jest - po prostu nie chce go używać w stanie „normalnym”.

Postanowiłem odtworzyć to lokalnie w małej aplikacji demonstracyjnej i oto, co wymyśliłem. (Zauważ, że we wczesnej wersji tego postu DemoStructbyła klasa i DemoClasswcale nie istniała - moja wina, ale wyjaśnia niektóre komentarze, które wyglądają teraz dziwnie ...)

using System;
using System.Diagnostics;
using System.Threading;

public struct DemoStruct
{
    public string Name { get; }

    public DemoStruct(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        Thread.Sleep(1000); // Vary this to see different results
        return $"Struct: {Name}";
    }
}

public class DemoClass
{
    public string Name { get; }

    public DemoClass(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        Thread.Sleep(1000); // Vary this to see different results
        return $"Class: {Name}";
    }
}

public class Program
{
    static void Main()
    {
        var demoClass = new DemoClass("Foo");
        var demoStruct = new DemoStruct("Bar");
        Debugger.Break();
    }
}

W debuggerze widzę teraz:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

Jeśli jednak zmniejszę Thread.Sleepwywołanie z 1 sekundy do 900 ms, nadal będzie krótka pauza, ale wtedy widzę Class: Foowartość. Wydaje się, że nie ma znaczenia, jak długo trwa Thread.Sleeppołączenie DemoStruct.ToString(), zawsze jest wyświetlane poprawnie - a debugger wyświetla wartość przed zakończeniem snu. (To tak, jakby Thread.Sleepbyło wyłączone.)

Teraz Instant.ToString()w Noda Time wykonuje sporo pracy, ale z pewnością nie zajmuje to całej sekundy - więc przypuszczalnie jest więcej warunków, które powodują, że debugger rezygnuje z oceny ToString()połączenia. I oczywiście jest to i tak struktura.

Próbowałem rekursywnie, aby sprawdzić, czy jest to limit stosu, ale wydaje się, że tak nie jest.

Jak więc ustalić, co powstrzymuje VS przed pełną oceną Instant.ToString()? Jak wspomniano poniżej, DebuggerDisplayAttributewydaje się pomagać, ale nie wiedząc, dlaczego , nigdy nie będę całkowicie pewny, kiedy będę tego potrzebować, a kiedy nie.

Aktualizacja

Jeśli użyję DebuggerDisplayAttribute, rzeczy się zmienią:

// For the sample code in the question...
[DebuggerDisplay("{ToString()}")]
public class DemoClass

daje mi:

demoClass      Evaluation timed out

Podczas gdy stosuję go w Czasie Nody:

[DebuggerDisplay("{ToString()}")]
public struct Instant

prosta aplikacja testowa pokazuje właściwy wynik:

instant    "1970-01-01T00:00:00Z"

Prawdopodobnie problemem w Czasie Nody jest pewien warunek, który DebuggerDisplayAttribute powoduje wymuszenie - nawet jeśli nie wymusza przekroczenia limitu czasu. (Byłoby to zgodne z moimi oczekiwaniami, które Instant.ToStringjest wystarczająco szybkie, aby uniknąć przekroczenia limitu czasu).

To może być wystarczająco dobre rozwiązanie - ale nadal chciałbym wiedzieć, co się dzieje i czy mogę zmienić kod po prostu, aby uniknąć konieczności przypisywania atrybutu do wszystkich różnych typów wartości w Noda Time.

Curiouser i curiouser

Cokolwiek jest mylące, debugger tylko myli je czasami. Stwórzmy klasę, która posiadaInstant i używa go dla własnej ToString()metody:

using NodaTime;
using System.Diagnostics;

public class InstantWrapper
{
    private readonly Instant instant;

    public InstantWrapper(Instant instant)
    {
        this.instant = instant;
    }

    public override string ToString() => instant.ToString();
}

public class Program
{
    static void Main()
    {
        var instant = NodaConstants.UnixEpoch;
        var wrapper = new InstantWrapper(instant);

        Debugger.Break();
    }
}

Teraz widzę:

instant    {NodaTime.Instant}
wrapper    {1970-01-01T00:00:00Z}

Jednak na sugestię Erena w komentarzach, jeśli zmienię InstantWrappersię na struct, otrzymuję:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Więc można ocenić Instant.ToString()- tak długo, jak to jest wywoływana przez inną ToStringmetodą ... co jest w klasie. Wydaje się, że część class / struct jest ważna na podstawie typu wyświetlanej zmiennej, a nie tego, jaki kod należy wykonać, aby uzyskać wynik.

Jako kolejny przykład tego, jeśli użyjemy:

object boxed = NodaConstants.UnixEpoch;

... to działa dobrze, wyświetlając właściwą wartość. Kolor mnie zmieszany.

Jon Skeet
źródło
7
@John to samo zachowanie w VS 2013 (musiałem usunąć c # 6), z dodatkowym komunikatem: Nazwa Ocena funkcji wyłączona, ponieważ upłynął limit czasu poprzedniej oceny funkcji. Musisz kontynuować wykonywanie, aby ponownie włączyć ocenę funkcji. string
werset 74
1
witamy w c # 6.0 @ 3-14159265358979323846264
Neel
1
Może DebuggerDisplayAttributesprawiłoby to, że spróbowałoby trochę trudniej.
Rawling,
1
zobacz, że to piąty punkt neelbhatt40.wordpress.com/2015/07/13/… @ 3-14159265358979323846264 dla nowego c # 6.0
Neel
5
@DiomidisSpinellis: Cóż, poprosiłem o to tutaj, aby: a) ktoś, kto albo widział to samo wcześniej lub zna wnętrze VS, może odpowiedzieć; b) każdy, kto napotka ten sam problem w przyszłości, może szybko uzyskać odpowiedź.
Jon Skeet

Odpowiedzi:

193

Aktualizacja:

Ten błąd został naprawiony w Visual Studio 2015 Update 2. Daj mi znać, jeśli nadal masz problemy z oceną ToString na wartościach struktur przy użyciu aktualizacji 2 lub nowszej.

Oryginalna odpowiedź:

Napotykasz znane ograniczenie błędów / projektu w programie Visual Studio 2015 i wywołujesz ToString na typach struktur. Można to również zaobserwować w kontaktach System.DateTimeSpan. System.DateTimeSpan.ToString()działa w oknach oceny w programie Visual Studio 2013, ale nie zawsze działa w 2015 r.

Jeśli interesują Cię szczegóły niskiego poziomu, oto co się dzieje:

Aby ocenić ToString, debugger wykonuje tak zwaną „ocenę funkcji”. W znacznie uproszczonych terminach debuger zawiesza wszystkie wątki w procesie oprócz bieżącego wątku, zmienia kontekst bieżącego wątku na ToStringfunkcję, ustawia ukryty punkt przerwania osłony, a następnie umożliwia kontynuowanie procesu. Po osiągnięciu punktu przerwania osłony debugger przywraca proces do poprzedniego stanu, a wartość zwracana przez funkcję jest wykorzystywana do wypełnienia okna.

Aby obsługiwać wyrażenia lambda, musieliśmy całkowicie przepisać narzędzie CLR Expression Evaluator w programie Visual Studio 2015. Na wysokim poziomie implementacja jest następująca:

  1. Roslyn generuje kod MSIL dla wyrażeń / zmiennych lokalnych, aby uzyskać wartości do wyświetlenia w różnych oknach kontroli.
  2. Debuger interpretuje IL, aby uzyskać wynik.
  3. Jeśli są jakieś instrukcje „wywołania”, debugger wykonuje ocenę funkcji, jak opisano powyżej.
  4. Debugger / roslyn bierze ten wynik i formatuje go w drzewiastym widoku, który jest pokazywany użytkownikowi.

Z powodu wykonania IL debugger zawsze ma do czynienia ze skomplikowaną mieszanką „rzeczywistych” i „fałszywych” wartości. Rzeczywiste wartości faktycznie istnieją w procesie debugowania. Fałszywe wartości istnieją tylko w procesie debugowania. Aby zaimplementować poprawną semantykę struktury, debugger zawsze musi wykonać kopię wartości podczas wypychania wartości struktury do stosu IL. Skopiowana wartość nie jest już „rzeczywistą” wartością i teraz istnieje tylko w procesie debugowania. Oznacza to, że jeśli później będziemy musieli dokonać oceny funkcji ToString, nie możemy, ponieważ wartość nie istnieje w procesie. Aby spróbować uzyskać wartość, musimy emulować wykonanieToStringmetoda. Chociaż możemy naśladować pewne rzeczy, istnieje wiele ograniczeń. Na przykład nie możemy emulować kodu natywnego i nie możemy wykonywać wywołań „rzeczywistych” wartości delegowanych lub wywołań wartości odbicia.

Mając to na uwadze, oto co powoduje różne zachowania, które widzisz:

  1. Debuger nie ocenia NodaTime.Instant.ToString-> Jest tak, ponieważ jest to typ struktury i implementacja ToString nie może być emulowana przez debugger, jak opisano powyżej.
  2. Thread.Sleepwydaje się, że zajmuje zero czasu, gdy jest wywoływany przez ToStringstruct -> Jest tak, ponieważ wykonuje się emulator ToString. Thread.Sleep jest rodzimą metodą, ale emulator jest tego świadomy i po prostu ignoruje wywołanie. Robimy to, aby uzyskać wartość do pokazania użytkownikowi. W tym przypadku opóźnienie nie byłoby pomocne.
  3. DisplayAttibute("ToString()")Pracuje. -> To jest mylące. Jedyną różnicą między niejawnym wywołaniem ToStringi DebuggerDisplayjest to, że wszelkie przekroczenia limitu czasu niejawnej ToString oceny wyłączą wszystkie niejawne ToStringoceny dla tego typu do następnej sesji debugowania. Być może obserwujesz to zachowanie.

Jeśli chodzi o problem / błąd w projekcie, planujemy rozwiązać ten problem w przyszłej wersji programu Visual Studio.

Mam nadzieję, że to wszystko wyjaśni. Daj mi znać, jeśli masz więcej pytań. :-)

Patrick Nelson - MSFT
źródło
1
Każdy pomysł, jak działa Instant.ToString, jeśli implementacja jest po prostu „zwraca literał ciągu”? Wygląda na to, że wciąż nie uwzględniono pewnych zawiłości :) Sprawdzę, czy naprawdę potrafię odtworzyć to zachowanie ...
Jon Skeet
1
@Jon, nie jestem pewien, o co pytasz. Debuger jest niezależny od implementacji podczas oceny rzeczywistej funkcji i zawsze próbuje tego najpierw. Debuger dba o implementację tylko wtedy, gdy musi emulować wywołanie - Zwrócenie literału ciągu jest najprostszym przypadkiem do emulacji.
Patrick Nelson - MSFT,
8
Idealnie chcemy, aby CLR wykonał wszystko. Zapewnia to najbardziej dokładne i wiarygodne wyniki. Dlatego dokonujemy prawdziwej oceny funkcji dla wywołań ToString. Kiedy nie jest to możliwe, wracamy do emulacji połączenia. Oznacza to, że debuger udaje, że CLR wykonuje tę metodę. Oczywiście, jeśli implementacja to <code> return „Hello” </code>, jest to łatwe do zrobienia. Jeśli implementacja wykonuje P-Invoke, jest to trudniejsze lub niemożliwe.
Patrick Nelson - MSFT
3
@tzachs, Emulator jest całkowicie jednowątkowy. Jeśli innerResultrozpocznie się od zera, pętla nigdy się nie zakończy, a ocena zostanie zakończona. W rzeczywistości oceny pozwalają domyślnie na uruchomienie tylko jednego wątku w procesie, więc zobaczysz to samo zachowanie, niezależnie od tego, czy emulator jest używany, czy nie.
Patrick Nelson - MSFT,
2
BTW, jeśli wiesz, że twoje oceny wymagają wielu wątków, spójrz na Debugger.NotifyOfCrossThreadDependency . Wywołanie tej metody przerwie ocenę z komunikatem, że ocena wymaga uruchomienia wszystkich wątków, a debuger zapewni przycisk, który użytkownik może nacisnąć, aby wymusić ocenę. Wadą jest to, że punkty przerwania trafione w inne wątki podczas oceny zostaną zignorowane.
Patrick Nelson - MSFT,