Qu'est-ce qui empêche le débogueur Visual Studio d'évaluer une substitution ToString?

221

Environnement: Visual Studio 2015 RTM. (Je n'ai pas essayé d'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 structtypes centraux de Noda Time), les fenêtres "Locals" et "Watch" ne semblent pas appeler son ToString()remplacement. Si j'appelle ToString()explicitement dans la fenêtre de surveillance, je vois la représentation appropriée, mais sinon je vois juste:

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 capable de détecter qu'elle est là - il ne veut tout simplement pas l'utiliser dans son é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, DemoStructc'était une classe et DemoClassn'existait pas du tout - ma faute, mais cela explique certains commentaires qui semblent bizarres 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 Thread.Sleepréduis l' appel de 1 seconde à 900 ms, il y a encore une courte pause, mais je vois Class: Foola valeur. Peu importe la durée de l' Thread.Sleepappel DemoStruct.ToString(), il s'affiche toujours correctement - et le débogueur affiche la valeur avant la fin du sommeil. (C'est comme si elle Thread.Sleepétait désactivée.)

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

J'ai essayé de répéter pour voir s'il s'agit d'une limite de pile, mais cela ne semble pas être le cas.

Alors, comment puis-je déterminer ce qui empêche VS d'évaluer complètement Instant.ToString()? Comme indiqué ci-dessous, DebuggerDisplayAttributesemble aider, mais sans savoir pourquoi , je ne serai jamais entièrement confiant quand j'en ai besoin et quand je n'en ai 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 lorsque je l'applique dans Noda Time:

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

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

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

On peut donc supposer le problème dans le temps Noda est une condition qui DebuggerDisplayAttribute fait la force à travers - même si elle ne force pas par les délais d' attente. (Ce serait conforme à mon attente, qui Instant.ToStringest facilement assez rapide pour éviter un délai d'attente.)

Cela peut être une assez bonne solution - mais j'aimerais toujours savoir ce qui se passe et si je peux changer le code simplement pour éviter d'avoir à mettre l'attribut sur tous les différents types de valeurs dans Noda Time.

Curieux et curieux

Tout ce qui prête à confusion, le débogueur ne fait que le confondre parfois. Créons une classe qui contient un Instantet l'utilise pour sa propre ToString()méthode:

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 d'Eren dans les commentaires, si je change InstantWrapperpour être une structure, j'obtiens:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Donc, il peut évaluer Instant.ToString()- tant qu'il est invoqué par une autre ToStringméthode ... qui est 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.

Comme autre exemple de cela, si nous utilisons:

object boxed = NodaConstants.UnixEpoch;

... alors cela fonctionne très bien, affichant la bonne valeur. Colorie-moi confus.

Jon Skeet
la source
7
@John même comportement dans VS 2013 (j'ai dû supprimer le truc c # 6), avec un message supplémentaire: Nom L'évaluation de la fonction est désactivée car une évaluation de la fonction précédente a expiré. Vous devez continuer l'exécution pour réactiver l'évaluation de la fonction. string
vc 74
1
bienvenue à c # 6.0 @ 3-14159265358979323846264
Neel
1
Peut-être que cela le DebuggerDisplayAttributeferait essayer un peu plus.
Rawling
1
voir c'est le 5ème point neelbhatt40.wordpress.com/2015/07/13/… @ 3-14159265358979323846264 pour le nouveau c # 6.0
Neel
5
@DiomidisSpinellis: Eh bien, je l'ai demandé ici pour que a) quelqu'un qui a déjà vu la même chose ou qui connaît l'intérieur de VS puisse répondre; b) toute personne rencontrant le même problème à l'avenir peut obtenir la réponse rapidement.
Jon Skeet du

Réponses:

193

Mettre à jour:

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

Réponse originale:

Vous rencontrez une limitation de conception / bogue connue avec Visual Studio 2015 et appelez ToString sur les types de structure. Cela peut également être observé lors du traitement 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 fait ce qu'on appelle «l'évaluation de fonction». En termes très simplifiés, le débogueur suspend tous les threads du processus à l'exception du thread actuel, modifie le contexte du thread actuel en ToStringfonction, définit un point d'arrêt de garde masqué, puis permet au processus de continuer. Lorsque le point d'arrêt de garde est atteint, le débogueur restaure 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:

  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 "d'appel", 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 qui est montrée à l'utilisateur.

En raison de l'exécution d'IL, le débogueur est toujours confronté à un mélange compliqué de valeurs "réelles" et "fausses". Des 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 une sémantique de structure appropriée, le débogueur doit toujours faire une copie de la valeur lors de la transmission d'une valeur de structure à la pile IL. La valeur copiée n'est plus une valeur "réelle" et n'existe désormais que dans le processus de débogage. Cela signifie que si nous devons ultérieurement effectuer une évaluation de la fonction de ToString, nous ne pouvons pas, car la valeur n'existe pas dans le processus. Pour essayer d'obtenir la valeur dont nous avons besoin pour émuler l'exécution de laToStringméthode. Bien que nous puissions émuler certaines choses, il existe de nombreuses limitations. Par exemple, nous ne pouvons pas émuler de code natif et nous ne pouvons pas exécuter des appels à des valeurs de délégué "réelles" ou des appels à des valeurs de réflexion.

Avec tout cela à l'esprit, voici ce qui cause les divers comportements que vous voyez:

  1. Le débogueur n'évalue pas NodaTime.Instant.ToString-> C'est parce qu'il est de type struct et l'implémentation de ToString ne peut pas être émulée par le débogueur comme décrit ci-dessus.
  2. Thread.Sleepsemble ne prendre aucun temps lorsqu'il est appelé par ToStringune structure -> C'est parce que l'émulateur est en cours d'exécution 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 retard ne serait pas utile dans ce cas.
  3. DisplayAttibute("ToString()")travaux. -> C'est déroutant. La seule différence entre l'appel implicite de ToStringet DebuggerDisplayest que tout délai d'expiration de l' ToString évaluation implicite désactivera toutes les ToStringévaluations implicites pour ce type jusqu'à la prochaine session de débogage. Vous observez peut-être ce comportement.

En ce qui concerne le problème / bug de conception, c'est quelque chose que nous prévoyons de résoudre dans une future version de Visual Studio.

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

Patrick Nelson - MSFT
la source
1
Une idée du fonctionnement d'Instant.ToString si l'implémentation consiste simplement à "renvoyer un littéral chaîne"? On dirait qu'il y a encore des complexités non expliquées :) Je vais vérifier que je peux vraiment reproduire ce comportement ...
Jon Skeet
1
@Jon, je ne suis pas sûr de ce que vous demandez. Le débogueur est indépendant de l'implémentation lors de l'évaluation de fonction réelle et il essaie toujours ceci en premier. Le débogueur ne se soucie de l'implémentation que lorsqu'il doit émuler l'appel - Le retour d'un littéral de chaîne est le cas le plus simple à émuler.
Patrick Nelson - MSFT
8
Idéalement, nous voulons que le CLR exécute tout. Cela fournit les résultats les plus précis et les plus fiables. C'est pourquoi nous faisons une véritable évaluation des fonctions pour les appels ToString. Lorsque cela n'est pas possible, nous retombons sur l'émulation de l'appel. Cela signifie que le débogueur prétend être le CLR exécutant la méthode. De toute évidence, si l'implémentation est <code> return "Hello" </code>, c'est facile à faire. Si l'implémentation fait un P-Invoke, c'est plus difficile ou impossible.
Patrick Nelson - MSFT
3
@tzachs, L'émulateur est entièrement monothread. Si innerResultdémarre comme nul, la boucle ne se terminera jamais et finalement l'évaluation expirera. En fait, les évaluations n'autorisent qu'un seul thread dans le processus à s'exécuter par défaut, vous verrez donc le même comportement, que l'émulateur soit utilisé ou non.
Patrick Nelson - MSFT
2
BTW, si vous savez que vos évaluations nécessitent plusieurs threads, jetez un œil à Debugger.NotifyOfCrossThreadDependency . L'appel de cette méthode annulera l'évaluation avec un message indiquant que l'évaluation nécessite l'exécution de tous les threads et le débogueur fournira un bouton que l'utilisateur peut appuyer pour forcer l'évaluation. L'inconvénient est que tout point d'arrêt atteint sur d'autres threads pendant l'évaluation sera ignoré.
Patrick Nelson - MSFT