Che cosa impedisce al debugger di Visual Studio di interrompere la valutazione di una sostituzione ToString?


221

Ambiente: Visual Studio 2015 RTM. (Non ho provato le versioni precedenti.)

Di recente, ho eseguito il debug di alcuni dei miei codici Noda Time e ho notato che quando ho una variabile locale di tipo NodaTime.Instant(uno dei structtipi centrali in Noda Time), le finestre "Locals" e "Watch" non sembra chiamare la sua ToString()sostituzione. Se chiamo ToString()esplicitamente nella finestra di controllo, vedo la rappresentazione appropriata, ma altrimenti vedo solo:

variableName       {NodaTime.Instant}

che non è molto utile.

Se cambio l'override per restituire una stringa costante, la stringa viene visualizzata nel debugger, quindi è chiaramente in grado di rilevare che è lì - semplicemente non vuole usarlo nel suo stato "normale".

Ho deciso di riprodurlo localmente in una piccola app demo, ed ecco cosa ho pensato. (Nota che in una prima versione di questo post, DemoStructera una classe e DemoClassnon esisteva affatto - colpa mia, ma spiega alcuni commenti che sembrano strani adesso ...)

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

Nel debugger ora vedo:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

Tuttavia, se Thread.Sleepriduco la chiamata da 1 secondo a 900 ms, c'è ancora una breve pausa, ma poi vedo Class: Foocome valore. Non sembra importare per quanto tempo dura la Thread.Sleepchiamata DemoStruct.ToString(), viene sempre visualizzata correttamente - e il debugger visualizza il valore prima che la sospensione fosse completata. (È come se Thread.Sleepfosse disabilitato.)

Ora Instant.ToString()in Noda Time fa una buona dose di lavoro, ma sicuramente non ci vuole un secondo intero - quindi presumibilmente ci sono più condizioni che fanno sì che il debugger smetta di valutare una ToString()chiamata. E ovviamente è comunque una struttura.

Ho provato a ricorrere per vedere se si tratta di un limite dello stack, ma sembra non essere così.

Quindi, come posso capire cosa impedisce a VS di valutare completamente Instant.ToString()? Come indicato di seguito, DebuggerDisplayAttributesembra aiutare, ma senza sapere il perché , non sarò mai completamente fiducioso quando ne avrò bisogno e quando non lo farò.

Aggiornare

Se uso DebuggerDisplayAttribute, le cose cambiano:

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

mi da:

demoClass      Evaluation timed out

Mentre quando lo applico in Noda Time:

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

una semplice app di prova mi mostra il risultato giusto:

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

Quindi, presumibilmente il problema in Noda Il tempo è una condizione che DebuggerDisplayAttribute fa forza attraverso - anche se non forza attraverso timeout. (Questo sarebbe in linea con le mie aspettative che Instant.ToStringè abbastanza veloce abbastanza da evitare un timeout.)

Questa potrebbe essere una soluzione abbastanza buona, ma mi piacerebbe comunque sapere cosa sta succedendo e se posso cambiare il codice semplicemente per evitare di dover mettere l'attributo su tutti i vari tipi di valore in Noda Time.

Curioso e curioso

Tutto ciò che confonde il debugger lo confonde solo a volte. Creiamo una classe che detiene una Instante lo utilizza per il proprio ToString()metodo:

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

Ora finisco per vedere:

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

Tuttavia, su suggerimento di Eren nei commenti, se cambio InstantWrappera essere una struttura, ottengo:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Quindi può valutare Instant.ToString()- purché sia ​​invocato da un altro ToStringmetodo ... che è all'interno di una classe. La parte class / struct sembra essere importante in base al tipo di variabile visualizzata, non al codice che deve essere eseguito per ottenere il risultato.

Come altro esempio di ciò, se utilizziamo:

object boxed = NodaConstants.UnixEpoch;

... allora funziona benissimo, mostrando il giusto valore. Colorami confuso.


7
@John stesso comportamento in VS 2013 (ho dovuto rimuovere la roba c # 6), con un messaggio aggiuntivo: Nome Valutazione della funzione disabilitata a causa del timeout di una precedente valutazione della funzione. È necessario continuare l'esecuzione per riattivare la valutazione della funzione. string
vc 74

1
benvenuto in c # 6.0 @ 3-14159265358979323846264
Neel

1
Forse a lo DebuggerDisplayAttributefarebbe provare un po 'più difficile.
Rawling,

1
vedi il 5 ° punto neelbhatt40.wordpress.com/2015/07/13/… @ 3-14159265358979323846264 per il nuovo c # 6.0
Neel

5
@DiomidisSpinellis: Beh, l'ho chiesto qui in modo che a) qualcuno che abbia già visto la stessa cosa prima o sappia che l'interno di VS possa rispondere; b) chiunque incontrerà lo stesso problema in futuro può ottenere rapidamente la risposta.
Jon Skeet,

Risposte:


193

Aggiornare:

Questo errore è stato corretto nell'aggiornamento 2 di Visual Studio 2015. Fammi sapere se stai ancora riscontrando problemi nella valutazione di ToString sui valori di struttura utilizzando l'aggiornamento 2 o successivo.

Risposta originale:

Stai riscontrando una limitazione di bug / progettazione nota con Visual Studio 2015 e stai chiamando ToString su tipi di struttura. Questo può essere osservato anche quando si ha a che fare con System.DateTimeSpan. System.DateTimeSpan.ToString()funziona nelle finestre di valutazione con Visual Studio 2013, ma non funziona sempre nel 2015.

Se sei interessato ai dettagli di basso livello, ecco cosa sta succedendo:

Per valutare ToString, il debugger fa ciò che è noto come "valutazione delle funzioni". In termini molto semplificati, il debugger sospende tutti i thread nel processo tranne il thread corrente, modifica il contesto del thread corrente nella ToStringfunzione, imposta un breakpoint di protezione nascosto, quindi consente al processo di continuare. Quando viene raggiunto il punto di interruzione della protezione, il debugger ripristina il processo allo stato precedente e il valore restituito della funzione viene utilizzato per popolare la finestra.

Per supportare le espressioni lambda, abbiamo dovuto riscrivere completamente il CLR Expression Evaluator in Visual Studio 2015. Ad un livello elevato, l'implementazione è:

  1. Roslyn genera codice MSIL per espressioni / variabili locali per ottenere i valori da visualizzare nelle varie finestre di ispezione.
  2. Il debugger interpreta l'IL per ottenere il risultato.
  3. Se sono presenti istruzioni di "chiamata", il debugger esegue una valutazione della funzione come descritto sopra.
  4. Il debugger / roslyn prende questo risultato e lo formatta nella vista ad albero mostrata all'utente.

A causa dell'esecuzione di IL, il debugger ha sempre a che fare con un complicato mix di valori "reali" e "falsi". Valori reali esistono effettivamente nel processo in fase di debug. I valori falsi esistono solo nel processo di debugger. Per implementare la semantica strutt corretta, il debugger deve sempre creare una copia del valore quando si spinge un valore struct nello stack IL. Il valore copiato non è più un valore "reale" e ora esiste solo nel processo di debugger. Ciò significa che se in seguito avremo bisogno di eseguire la valutazione delle funzioni ToString, non potremo perché il valore non esiste nel processo. Per cercare di ottenere il valore dobbiamo emulare l'esecuzione diToStringmetodo. Mentre possiamo emulare alcune cose, ci sono molte limitazioni. Ad esempio, non possiamo emulare il codice nativo e non possiamo eseguire chiamate a valori delegati "reali" o chiamate su valori di riflessione.

Con tutto ciò in mente, ecco cosa sta causando i vari comportamenti che stai vedendo:

  1. Il debugger non sta valutando NodaTime.Instant.ToString-> Questo perché è di tipo struct e l'implementazione di ToString non può essere emulata dal debugger come descritto sopra.
  2. Thread.Sleepsembra richiedere zero tempo quando viene chiamato da ToStringsu una struttura -> Questo perché l'emulatore è in esecuzione ToString. Thread.Sleep è un metodo nativo, ma l'emulatore ne è consapevole e ignora semplicemente la chiamata. Facciamo questo per cercare di ottenere un valore da mostrare all'utente. Un ritardo non sarebbe utile in questo caso.
  3. DisplayAttibute("ToString()")lavori. -> Questo è confuso. L'unica differenza tra la chiamata implicita di ToStringe DebuggerDisplayè che qualsiasi timeout della ToString valutazione implicita disabiliterà tutte le ToStringvalutazioni implicite per quel tipo fino alla prossima sessione di debug. Potresti osservare quel comportamento.

In termini di problema / bug di progettazione, questo è qualcosa che stiamo pianificando di affrontare in una versione futura di Visual Studio.

Speriamo che chiarisca le cose. Fammi sapere se hai altre domande. :-)


1
Hai idea di come funziona Instant.ToString se l'implementazione è "restituisce una stringa letterale"? Sembra che ci siano alcune complessità ancora irrisolte :) Verificherò che posso davvero riprodurre quel comportamento ...
Jon Skeet,

1
@Jon, non sono sicuro di quello che stai chiedendo. Il debugger è agnostico dell'implementazione quando esegue una valutazione delle funzioni reali e prova sempre prima questo. Il debugger si preoccupa dell'implementazione solo quando deve emulare la chiamata: restituire una stringa letterale è il caso più semplice da emulare.
Patrick Nelson - MSFT,

8
Idealmente, vogliamo che il CLR esegua tutto. Ciò fornisce i risultati più accurati e affidabili. Ecco perché eseguiamo una vera valutazione delle funzioni per le chiamate ToString. Quando ciò non è possibile, torniamo all'emulazione della chiamata. Ciò significa che il debugger finge di essere il CLR che esegue il metodo. Chiaramente se l'implementazione è <code> return "Hello" </code>, questo è facile da fare. Se l'implementazione esegue un P-Invoke è più difficile o impossibile.
Patrick Nelson - MSFT,

3
@tzachs, L'emulatore è completamente a thread singolo. Se innerResultinizia come nullo, il ciclo non terminerà mai e alla fine la valutazione scadrà. In effetti, le valutazioni consentono di eseguire un singolo thread nel processo per impostazione predefinita, quindi vedrai lo stesso comportamento indipendentemente dal fatto che l'emulatore venga utilizzato o meno.
Patrick Nelson - MSFT,

2
A proposito, se sai che le tue valutazioni richiedono più thread, dai un'occhiata a Debugger.NotifyOfCrossThreadDependency . La chiamata a questo metodo interromperà la valutazione con un messaggio che indica che la valutazione richiede l'esecuzione di tutti i thread e il debugger fornirà un pulsante che l'utente può premere per forzare la valutazione. Lo svantaggio è che eventuali punti di interruzione colpiti su altri thread durante la valutazione verranno ignorati.
Patrick Nelson - MSFT,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.