web-dev-qa-db-fra.com

Pourquoi le débogueur Visual Studio arrête-t-il l'évaluation d'un remplacement ToString?

Environnement: Visual Studio 2015 RTM. (Je n'ai pas essayé les anciennes versions.)

Récemment, j'ai débogué une partie de mon code Noda Time , et j'ai remarqué que lorsque j'ai une variable locale de type NodaTime.Instant (L'un des centraux struct dans Noda Time), les fenêtres "Locals" et "Watch" ne semblent pas appeler sa substitution ToString(). Si j'appelle explicitement ToString() dans la fenêtre de surveillance, je vois la représentation appropriée, mais sinon, je ne vois que:

variableName       {NodaTime.Instant}

ce qui n'est pas très utile.

Si je modifie le remplacement pour renvoyer une chaîne constante, la chaîne est affichée dans le débogueur. Il est donc clairement possible de détecter le fait qu'elle est là. Elle ne veut tout simplement pas l'utiliser dans sa "état normal.

J'ai décidé de reproduire cela localement dans une petite application de démonstration, et voici ce que j'ai trouvé. (Notez que dans une première version de ce post, DemoStruct était une classe et DemoClass n'existait pas du tout - de ma faute, mais il explique certains commentaires qui paraissent étranges maintenant ...)

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

Dans le débogueur, je vois maintenant:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

Cependant, si je réduis l'appel de Thread.Sleep De 1 seconde à 900 ms, il reste une courte pause, mais je vois alors Class: Foo Comme valeur. La durée d'affichage de Thread.Sleep Dans DemoStruct.ToString() n'a pas d'importance, elle est toujours affichée correctement - et le débogueur affiche la valeur avant la fin du sommeil. (C'est comme si Thread.Sleep Était désactivé.)

Maintenant, Instant.ToString() dans Noda Time fait beaucoup de travail, mais cela ne prend certainement pas une seconde entière - donc, il y a probablement plus de conditions qui poussent le débogueur à abandonner l'évaluation d'un appel ToString() . Et bien sûr, c'est une structure quand même.

J'ai essayé récursif pour voir si c'est une limite de pile, mais cela ne semble pas être le cas.

Alors, comment puis-je déterminer ce qui empêche VS de bien évaluer pleinement Instant.ToString()? Comme indiqué ci-dessous, DebuggerDisplayAttribute semble aider, mais sans savoir pourquoi, je ne serai jamais totalement confiant si j'en ai besoin ou pas.

Mettre à jour

Si j'utilise DebuggerDisplayAttribute , les choses changent:

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

donne moi:

demoClass      Evaluation timed out

Alors que quand je l'applique dans Noda Time:

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

une application de test simple me montre le bon résultat:

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

Donc, vraisemblablement, le problème dans Noda Time est une condition que DebuggerDisplayAttribute le fait forcer - même si cela ne force pas les timeouts. (Cela correspondrait à mon attente que Instant.ToString Soit assez rapide pour éviter un délai d'attente.)

Ceci peut-être est une bonne solution - mais j'aimerais quand même savoir ce qui se passe et savoir si je peux changer le code simplement pour éviter de devoir attribuer l'attribut à tous les types de valeur à l'heure de Noda.

Curieux et curieux

Tout ce qui déroute le débogueur ne le confond que parfois. Créons une classe qui détient un Instant et l’utilise pour sa propre méthode ToString():

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

Maintenant je finis par voir:

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

Cependant, à la suggestion de Eren dans les commentaires, si je change InstantWrapper en struct, je reçois:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Donc, il peut évaluer Instant.ToString() - tant que cette méthode est invoquée par une autre méthode ToString ... qui se trouve dans une classe. La partie class/struct semble être importante en fonction du type de la variable affichée et non du code à exécuter pour obtenir le résultat.

Autre exemple, si nous utilisons:

object boxed = NodaConstants.UnixEpoch;

... alors cela fonctionne bien, affichant la bonne valeur. Couleur moi confus.

218
Jon Skeet

Mise à jour:

Ce bogue a été corrigé dans Visual Studio 2015 Update 2. Indiquez-moi si vous rencontrez toujours des problèmes pour évaluer ToString sur des valeurs de structure à l'aide de Update 2 ou version ultérieure.

Réponse originale:

Vous rencontrez une limitation de bogue/conception connue avec Visual Studio 2015 et appelez ToString sur des types de structure. Ceci peut également être observé lorsque vous utilisez System.DateTimeSpan. System.DateTimeSpan.ToString() fonctionne dans les fenêtres d'évaluation avec Visual Studio 2013, mais ne fonctionne pas toujours en 2015.

Si vous êtes intéressé par les détails de bas niveau, voici ce qui se passe:

Pour évaluer ToString, le débogueur effectue ce qu'on appelle "l'évaluation de la fonction". En termes très simplifiés, le débogueur suspend tous les threads du processus sauf le thread actuel, modifie le contexte du thread actuel en fonction ToString, définit un point d'arrêt de garde masqué, puis autorise la poursuite du processus. Lorsque le point d'arrêt de garde est atteint, le débogueur rétablit le processus à son état précédent et la valeur de retour de la fonction est utilisée pour remplir la fenêtre.

Pour prendre en charge les expressions lambda, nous avons dû réécrire complètement l’évaluateur d’expression CLR dans Visual Studio 2015. À un niveau élevé, l’implémentation est la suivante:

  1. Roslyn génère du code MSIL pour les expressions/variables locales afin d’obtenir les valeurs à afficher dans les différentes fenêtres d’inspection.
  2. Le débogueur interprète l'IL pour obtenir le résultat.
  3. S'il existe des instructions "call", le débogueur exécute une évaluation de fonction comme décrit ci-dessus.
  4. Le débogueur/roslyn prend ce résultat et le formate dans la vue arborescente présentée à l'utilisateur.

En raison de l'exécution de IL, le débogueur traite toujours un mélange compliqué de valeurs "réelles" et "fausses". Les valeurs réelles existent réellement dans le processus en cours de débogage. Les fausses valeurs n'existent que dans le processus de débogage. Pour implémenter la sémantique correcte de la structure, le débogueur doit toujours effectuer une copie de la valeur lors de l'insertion d'une valeur de structure dans la pile IL. La valeur copiée n'est plus une valeur "réelle" et n'existe plus que dans le processus de débogage. Cela signifie que si nous devons ultérieurement évaluer la fonction de ToString, nous ne le pouvons pas car la valeur n’existe pas dans le processus. Pour essayer d'obtenir la valeur, nous devons émuler l'exécution de la méthode ToString. Bien que nous puissions imiter certaines choses, il existe de nombreuses limites. Par exemple, nous ne pouvons pas émuler de code natif ni exécuter des appels à de "vraies" valeurs déléguées ou des appels à des valeurs de réflexion.

Avec tout cela à l'esprit, voici ce qui cause les différents comportements que vous observez:

  1. Le débogueur n'évalue pas NodaTime.Instant.ToString -> En effet, il s'agit d'un type de structure et l'implémentation de ToString ne peut pas être émulée par le débogueur comme décrit ci-dessus.
  2. Thread.Sleep Semble ne prendre aucun temps lorsqu'il est appelé par ToString sur un struct -> C'est parce que l'émulateur exécute ToString. Thread.Sleep est une méthode native, mais l'émulateur en est conscient et ignore simplement l'appel. Nous faisons cela pour essayer d'obtenir une valeur à montrer à l'utilisateur. Un délai ne serait pas utile dans ce cas.
  3. DisplayAttibute("ToString()") fonctionne. -> C'est déroutant. La seule différence entre l'appel implicite de ToString et DebuggerDisplay est que tout dépassement de délai de l'évaluation implicite ToString désactivera toutes les évaluations implicites ToString pour cette tapez jusqu'à la prochaine session de débogage. Vous observez peut-être ce comportement.

En ce qui concerne le problème/bogue de conception, nous prévoyons d’y remédier dans une future version de Visual Studio.

Espérons que cela clarifie les choses. Dites moi si vous avez d'autres questions. :-)

191