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.
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.
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:
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:
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.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.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. :-)