Wodurch stoppt der Visual Studio-Debugger die Auswertung einer ToString-Überschreibung?

221

Umgebung: Visual Studio 2015 RTM. (Ich habe ältere Versionen nicht ausprobiert.)

Vor kurzem habe ich einen Teil meines Noda-Zeitcodes debuggt und festgestellt, dass die Fenster "Locals" und "Watch" angezeigt werden, wenn ich eine lokale Variable vom Typ NodaTime.Instant(einer der zentralen structTypen in Noda Time) habe scheint seine ToString()Überschreibung nicht zu nennen . Wenn ich ToString()explizit im Überwachungsfenster anrufe, sehe ich die entsprechende Darstellung, aber ansonsten sehe ich nur:

variableName       {NodaTime.Instant}

das ist nicht sehr nützlich.

Wenn ich die Überschreibung ändern , um eine konstante String zurück, die Zeichenfolge wird im Debugger angezeigt, so dass es eindeutig in der Lage ist zu holen , dass es da ist - es will einfach nicht , es benutzen in seinem „normalen“ Zustand.

Ich habe beschlossen, dies lokal in einer kleinen Demo-App zu reproduzieren, und hier ist, was ich mir ausgedacht habe. (Beachten Sie, dass in einer frühen Version dieses Beitrags DemoStructeine Klasse war und DemoClassüberhaupt nicht existierte - meine Schuld, aber es erklärt einige Kommentare, die jetzt seltsam aussehen ...)

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

Im Debugger sehe ich jetzt:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

Wenn ich den Thread.SleepAnruf von 1 Sekunde auf 900 ms reduziere, gibt es immer noch eine kurze Pause, aber dann sehe ich Class: Fooals Wert. Es scheint keine Rolle zu spielen, wie lange der Thread.SleepAnruf dauert DemoStruct.ToString(), er wird immer richtig angezeigt - und der Debugger zeigt den Wert an, bevor der Ruhezustand abgeschlossen wäre. (Es ist, als wäre Thread.Sleepes deaktiviert.)

Jetzt Instant.ToString()in Noda macht Time eine ganze Menge Arbeit, aber es dauert sicherlich keine ganze Sekunde - also gibt es vermutlich mehr Bedingungen, die den Debugger dazu veranlassen, die Auswertung eines ToString()Anrufs aufzugeben . Und natürlich ist es sowieso eine Struktur.

Ich habe versucht, rekursiv zu prüfen, ob es sich um ein Stapellimit handelt, aber das scheint nicht der Fall zu sein.

Wie kann ich also herausfinden, was VS davon abhält, vollständig zu evaluieren Instant.ToString()? Wie unten erwähnt, DebuggerDisplayAttributescheint es zu helfen, aber ohne zu wissen warum , werde ich nie ganz sicher sein, wann ich es brauche und wann nicht.

Aktualisieren

Wenn ich benutze DebuggerDisplayAttribute, ändern sich die Dinge:

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

gibt mir:

demoClass      Evaluation timed out

Wenn ich es in Noda Time anwende:

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

Eine einfache Test-App zeigt mir das richtige Ergebnis:

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

Vermutlich ist das Problem in der Noda-Zeit eine Bedingung, die DebuggerDisplayAttribute sich durchsetzt - obwohl sie keine Zeitüberschreitungen erzwingt. (Dies würde meiner Erwartung entsprechen, Instant.ToStringdie leicht schnell genug ist, um eine Zeitüberschreitung zu vermeiden.)

Dies mag eine gute Lösung sein - aber ich würde immer noch gerne wissen, was los ist und ob ich den Code einfach ändern kann, um zu vermeiden, dass das Attribut auf alle verschiedenen Werttypen in Noda Time gesetzt werden muss.

Neugieriger und neugieriger

Was den Debugger verwirrt, verwirrt ihn nur manchmal. Erstellen wir eine Klasse, die eine enthält Instantund diese für ihre eigene ToString()Methode verwendet:

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

Jetzt sehe ich:

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

Auf Vorschlag von Eren in Kommentaren InstantWrappererhalte ich jedoch Folgendes, wenn ich mich in eine Struktur verwandle:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Es kann also ausgewertet werden Instant.ToString()- solange dies von einer anderen ToStringMethode aufgerufen wird ... die sich innerhalb einer Klasse befindet. Der Klassen- / Strukturteil scheint wichtig zu sein, basierend auf dem Typ der angezeigten Variablen und nicht darauf, welcher Code ausgeführt werden muss, um das Ergebnis zu erhalten.

Als weiteres Beispiel hierfür verwenden wir:

object boxed = NodaConstants.UnixEpoch;

... dann funktioniert es einwandfrei und zeigt den richtigen Wert an. Färbe mich verwirrt.

Jon Skeet
quelle
7
@John dasselbe Verhalten in VS 2013 (ich musste das c # 6-Zeug entfernen), mit einer zusätzlichen Meldung: Name Funktionsbewertung deaktiviert, da eine vorherige Funktionsbewertung abgelaufen ist. Sie müssen die Ausführung fortsetzen, um die Funktionsbewertung wieder zu aktivieren. string
vc 74
1
Willkommen bei c # 6.0 @ 3-14159265358979323846264
Neel
1
Vielleicht DebuggerDisplayAttributewürde es dazu führen, dass es etwas härter wird.
Rawling
1
siehe es ist 5. Punkt neelbhatt40.wordpress.com/2015/07/13/… @ 3-14159265358979323846264 für neue c # 6.0
Neel
5
@DiomidisSpinellis: Nun, ich habe es hier gefragt, damit a) jemand antworten kann, der entweder dasselbe schon einmal gesehen hat oder das Innere von VS kennt; b) Jeder, der in Zukunft auf dasselbe Problem stößt, kann die Antwort schnell erhalten.
Jon Skeet

Antworten:

193

Aktualisieren:

Dieser Fehler wurde in Visual Studio 2015 Update 2 behoben. Lassen Sie mich wissen, wenn Sie weiterhin Probleme beim Auswerten von ToString für Strukturwerte mit Update 2 oder höher haben.

Ursprüngliche Antwort:

Sie stoßen mit Visual Studio 2015 auf eine bekannte Fehler- / Entwurfsbeschränkung und rufen ToString für Strukturtypen auf. Dies kann auch beim Umgang mit beobachtet werden System.DateTimeSpan. System.DateTimeSpan.ToString()Funktioniert in den Evaluierungsfenstern mit Visual Studio 2013, funktioniert jedoch nicht immer in 2015.

Wenn Sie an den Details auf niedriger Ebene interessiert sind, gehen Sie wie folgt vor:

Zur Auswertung ToStringführt der Debugger eine sogenannte "Funktionsauswertung" durch. In stark vereinfachter Form hält der Debugger alle Threads im Prozess außer dem aktuellen Thread an, ändert den Kontext des aktuellen Threads in die ToStringFunktion, legt einen versteckten Schutz-Haltepunkt fest und lässt den Prozess dann fortfahren. Wenn der Guard-Haltepunkt erreicht wird, stellt der Debugger den Prozess auf seinen vorherigen Zustand zurück und der Rückgabewert der Funktion wird zum Auffüllen des Fensters verwendet.

Um Lambda-Ausdrücke zu unterstützen, mussten wir den CLR Expression Evaluator in Visual Studio 2015 komplett neu schreiben. Auf hoher Ebene lautet die Implementierung:

  1. Roslyn generiert MSIL-Code für Ausdrücke / lokale Variablen, um die Werte zu erhalten, die in den verschiedenen Inspektionsfenstern angezeigt werden sollen.
  2. Der Debugger interpretiert die IL, um das Ergebnis zu erhalten.
  3. Wenn es "Aufruf" -Anweisungen gibt, führt der Debugger eine Funktionsbewertung wie oben beschrieben aus.
  4. Der Debugger / Roslyn nimmt dieses Ergebnis und formatiert es in die baumartige Ansicht, die dem Benutzer angezeigt wird.

Aufgrund der Ausführung von IL hat der Debugger immer mit einer komplizierten Mischung aus "echten" und "falschen" Werten zu tun. Während des Debuggens existieren tatsächlich reale Werte. Gefälschte Werte existieren nur im Debugger-Prozess. Um eine ordnungsgemäße Struktursemantik zu implementieren, muss der Debugger immer eine Kopie des Werts erstellen, wenn ein Strukturwert auf den IL-Stapel übertragen wird. Der kopierte Wert ist kein "realer" Wert mehr und existiert nur noch im Debugger-Prozess. Das heißt, wenn wir später eine Funktionsbewertung von durchführen müssen ToString, können wir dies nicht, da der Wert im Prozess nicht vorhanden ist. Um zu versuchen, den Wert zu erhalten, müssen wir die Ausführung von emulierenToStringMethode. Während wir einige Dinge emulieren können, gibt es viele Einschränkungen. Beispielsweise können wir keinen nativen Code emulieren und keine Aufrufe an "echte" Delegatwerte oder Aufrufe von Reflexionswerten ausführen.

In Anbetracht dessen ist hier, was die verschiedenen Verhaltensweisen verursacht, die Sie sehen:

  1. Der Debugger wertet nicht aus NodaTime.Instant.ToString-> Dies liegt daran, dass es sich um einen Strukturtyp handelt und die Implementierung von ToString vom Debugger nicht wie oben beschrieben emuliert werden kann.
  2. Thread.Sleepscheint keine Zeit in ToStringAnspruch zu nehmen, wenn von einer Struktur aufgerufen wird -> Dies liegt daran, dass der Emulator ausgeführt wird ToString. Thread.Sleep ist eine native Methode, aber der Emulator ist sich dessen bewusst und ignoriert den Aufruf einfach. Wir tun dies, um zu versuchen, einen Wert zu erhalten, der dem Benutzer angezeigt wird. Eine Verzögerung wäre in diesem Fall nicht hilfreich.
  3. DisplayAttibute("ToString()")funktioniert. -> Das ist verwirrend. Der einzige Unterschied zwischen dem impliziten Aufruf von ToStringund DebuggerDisplaybesteht darin, dass alle Zeitüberschreitungen der impliziten ToString Auswertung alle impliziten ToStringAuswertungen für diesen Typ bis zur nächsten Debug-Sitzung deaktivieren . Möglicherweise beobachten Sie dieses Verhalten.

In Bezug auf das Designproblem / den Designfehler planen wir, dies in einer zukünftigen Version von Visual Studio zu beheben.

Hoffentlich klärt das die Dinge auf. Lassen Sie mich wissen, wenn Sie weitere Fragen haben. :-)

Patrick Nelson - MSFT
quelle
1
Irgendeine Idee, wie Instant.ToString funktioniert, wenn die Implementierung nur "ein String-Literal zurückgeben" ist? Es hört sich so an, als ob einige Komplexitäten noch nicht berücksichtigt wurden :) Ich werde überprüfen, ob ich dieses Verhalten wirklich reproduzieren kann ...
Jon Skeet
1
@ Jon, ich bin mir nicht sicher, was du fragst. Der Debugger ist unabhängig von der Implementierung, wenn er eine echte Funktionsbewertung durchführt, und versucht dies immer zuerst. Der Debugger kümmert sich nur dann um die Implementierung, wenn er den Aufruf emulieren muss. Die Rückgabe eines String-Literal ist der einfachste Fall, den es zu emulieren gilt.
Patrick Nelson - MSFT
8
Idealerweise soll die CLR alles ausführen. Dies liefert die genauesten und zuverlässigsten Ergebnisse. Aus diesem Grund führen wir eine echte Funktionsbewertung für ToString-Aufrufe durch. Wenn dies nicht möglich ist, greifen wir auf die Emulation des Anrufs zurück. Das heißt, der Debugger gibt vor, die CLR zu sein, die die Methode ausführt. Wenn die Implementierung <code> lautet und "Hallo" </ code> zurückgibt, ist dies ganz einfach. Wenn die Implementierung einen P-Aufruf ausführt, ist dies schwieriger oder unmöglich.
Patrick Nelson - MSFT
3
@tzachs, Der Emulator ist komplett Single-Threaded. Wenn innerResultder Start mit null beginnt, wird die Schleife niemals beendet und die Auswertung läuft schließlich ab. Bei Auswertungen kann standardmäßig nur ein einzelner Thread im Prozess ausgeführt werden, sodass dasselbe Verhalten angezeigt wird, unabhängig davon, ob der Emulator verwendet wird oder nicht.
Patrick Nelson - MSFT
2
Übrigens, wenn Sie wissen, dass Ihre Auswertungen mehrere Threads erfordern, werfen Sie einen Blick auf Debugger.NotifyOfCrossThreadDependency . Durch Aufrufen dieser Methode wird die Auswertung mit einer Meldung abgebrochen, die besagt, dass für die Auswertung alle Threads ausgeführt werden müssen, und der Debugger stellt eine Schaltfläche bereit, die der Benutzer drücken kann, um die Auswertung zu erzwingen. Der Nachteil ist, dass alle Haltepunkte, die während der Auswertung auf andere Threads treffen, ignoriert werden.
Patrick Nelson - MSFT