Środowisko: Visual Studio 2015 RTM. (Nie próbowałem starszych wersji).
Ostatnio debugowałem część mojego kodu Noda Time i zauważyłem, że kiedy mam lokalną zmienną typu NodaTime.Instant
(jeden z głównych struct
typów w Noda Time), okna „Locals” i „Watch” nie wydaje się nazywać jego ToString()
zastąpienia. Jeśli dzwonię ToString()
jawnie w oknie zegarka, widzę odpowiednią reprezentację, ale poza tym widzę tylko:
variableName {NodaTime.Instant}
co nie jest zbyt przydatne.
Jeśli zmienię przesłonięcie, aby zwracać stały ciąg, ciąg jest wyświetlany w debuggerze, więc jest w stanie wyraźnie zauważyć, że tam jest - po prostu nie chce go używać w stanie „normalnym”.
Postanowiłem odtworzyć to lokalnie w małej aplikacji demonstracyjnej i oto, co wymyśliłem. (Zauważ, że we wczesnej wersji tego postu DemoStruct
była klasa i DemoClass
wcale nie istniała - moja wina, ale wyjaśnia niektóre komentarze, które wyglądają teraz dziwnie ...)
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();
}
}
W debuggerze widzę teraz:
demoClass {DemoClass}
demoStruct {Struct: Bar}
Jeśli jednak zmniejszę Thread.Sleep
wywołanie z 1 sekundy do 900 ms, nadal będzie krótka pauza, ale wtedy widzę Class: Foo
wartość. Wydaje się, że nie ma znaczenia, jak długo trwa Thread.Sleep
połączenie DemoStruct.ToString()
, zawsze jest wyświetlane poprawnie - a debugger wyświetla wartość przed zakończeniem snu. (To tak, jakby Thread.Sleep
było wyłączone.)
Teraz Instant.ToString()
w Noda Time wykonuje sporo pracy, ale z pewnością nie zajmuje to całej sekundy - więc przypuszczalnie jest więcej warunków, które powodują, że debugger rezygnuje z oceny ToString()
połączenia. I oczywiście jest to i tak struktura.
Próbowałem rekursywnie, aby sprawdzić, czy jest to limit stosu, ale wydaje się, że tak nie jest.
Jak więc ustalić, co powstrzymuje VS przed pełną oceną Instant.ToString()
? Jak wspomniano poniżej, DebuggerDisplayAttribute
wydaje się pomagać, ale nie wiedząc, dlaczego , nigdy nie będę całkowicie pewny, kiedy będę tego potrzebować, a kiedy nie.
Aktualizacja
Jeśli użyję DebuggerDisplayAttribute
, rzeczy się zmienią:
// For the sample code in the question...
[DebuggerDisplay("{ToString()}")]
public class DemoClass
daje mi:
demoClass Evaluation timed out
Podczas gdy stosuję go w Czasie Nody:
[DebuggerDisplay("{ToString()}")]
public struct Instant
prosta aplikacja testowa pokazuje właściwy wynik:
instant "1970-01-01T00:00:00Z"
Prawdopodobnie problemem w Czasie Nody jest pewien warunek, który DebuggerDisplayAttribute
powoduje wymuszenie - nawet jeśli nie wymusza przekroczenia limitu czasu. (Byłoby to zgodne z moimi oczekiwaniami, które Instant.ToString
jest wystarczająco szybkie, aby uniknąć przekroczenia limitu czasu).
To może być wystarczająco dobre rozwiązanie - ale nadal chciałbym wiedzieć, co się dzieje i czy mogę zmienić kod po prostu, aby uniknąć konieczności przypisywania atrybutu do wszystkich różnych typów wartości w Noda Time.
Curiouser i curiouser
Cokolwiek jest mylące, debugger tylko myli je czasami. Stwórzmy klasę, która posiadaInstant
i używa go dla własnej ToString()
metody:
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();
}
}
Teraz widzę:
instant {NodaTime.Instant}
wrapper {1970-01-01T00:00:00Z}
Jednak na sugestię Erena w komentarzach, jeśli zmienię InstantWrapper
się na struct, otrzymuję:
instant {NodaTime.Instant}
wrapper {InstantWrapper}
Więc można ocenić Instant.ToString()
- tak długo, jak to jest wywoływana przez inną ToString
metodą ... co jest w klasie. Wydaje się, że część class / struct jest ważna na podstawie typu wyświetlanej zmiennej, a nie tego, jaki kod należy wykonać, aby uzyskać wynik.
Jako kolejny przykład tego, jeśli użyjemy:
object boxed = NodaConstants.UnixEpoch;
... to działa dobrze, wyświetlając właściwą wartość. Kolor mnie zmieszany.
źródło
DebuggerDisplayAttribute
sprawiłoby to, że spróbowałoby trochę trudniej.Odpowiedzi:
Aktualizacja:
Ten błąd został naprawiony w Visual Studio 2015 Update 2. Daj mi znać, jeśli nadal masz problemy z oceną ToString na wartościach struktur przy użyciu aktualizacji 2 lub nowszej.
Oryginalna odpowiedź:
Napotykasz znane ograniczenie błędów / projektu w programie Visual Studio 2015 i wywołujesz ToString na typach struktur. Można to również zaobserwować w kontaktach
System.DateTimeSpan
.System.DateTimeSpan.ToString()
działa w oknach oceny w programie Visual Studio 2013, ale nie zawsze działa w 2015 r.Jeśli interesują Cię szczegóły niskiego poziomu, oto co się dzieje:
Aby ocenić
ToString
, debugger wykonuje tak zwaną „ocenę funkcji”. W znacznie uproszczonych terminach debuger zawiesza wszystkie wątki w procesie oprócz bieżącego wątku, zmienia kontekst bieżącego wątku naToString
funkcję, ustawia ukryty punkt przerwania osłony, a następnie umożliwia kontynuowanie procesu. Po osiągnięciu punktu przerwania osłony debugger przywraca proces do poprzedniego stanu, a wartość zwracana przez funkcję jest wykorzystywana do wypełnienia okna.Aby obsługiwać wyrażenia lambda, musieliśmy całkowicie przepisać narzędzie CLR Expression Evaluator w programie Visual Studio 2015. Na wysokim poziomie implementacja jest następująca:
Z powodu wykonania IL debugger zawsze ma do czynienia ze skomplikowaną mieszanką „rzeczywistych” i „fałszywych” wartości. Rzeczywiste wartości faktycznie istnieją w procesie debugowania. Fałszywe wartości istnieją tylko w procesie debugowania. Aby zaimplementować poprawną semantykę struktury, debugger zawsze musi wykonać kopię wartości podczas wypychania wartości struktury do stosu IL. Skopiowana wartość nie jest już „rzeczywistą” wartością i teraz istnieje tylko w procesie debugowania. Oznacza to, że jeśli później będziemy musieli dokonać oceny funkcji
ToString
, nie możemy, ponieważ wartość nie istnieje w procesie. Aby spróbować uzyskać wartość, musimy emulować wykonanieToString
metoda. Chociaż możemy naśladować pewne rzeczy, istnieje wiele ograniczeń. Na przykład nie możemy emulować kodu natywnego i nie możemy wykonywać wywołań „rzeczywistych” wartości delegowanych lub wywołań wartości odbicia.Mając to na uwadze, oto co powoduje różne zachowania, które widzisz:
NodaTime.Instant.ToString
-> Jest tak, ponieważ jest to typ struktury i implementacja ToString nie może być emulowana przez debugger, jak opisano powyżej.Thread.Sleep
wydaje się, że zajmuje zero czasu, gdy jest wywoływany przezToString
struct -> Jest tak, ponieważ wykonuje się emulatorToString
. Thread.Sleep jest rodzimą metodą, ale emulator jest tego świadomy i po prostu ignoruje wywołanie. Robimy to, aby uzyskać wartość do pokazania użytkownikowi. W tym przypadku opóźnienie nie byłoby pomocne.DisplayAttibute("ToString()")
Pracuje. -> To jest mylące. Jedyną różnicą między niejawnym wywołaniemToString
iDebuggerDisplay
jest to, że wszelkie przekroczenia limitu czasu niejawnejToString
oceny wyłączą wszystkie niejawneToString
oceny dla tego typu do następnej sesji debugowania. Być może obserwujesz to zachowanie.Jeśli chodzi o problem / błąd w projekcie, planujemy rozwiązać ten problem w przyszłej wersji programu Visual Studio.
Mam nadzieję, że to wszystko wyjaśni. Daj mi znać, jeśli masz więcej pytań. :-)
źródło
innerResult
rozpocznie się od zera, pętla nigdy się nie zakończy, a ocena zostanie zakończona. W rzeczywistości oceny pozwalają domyślnie na uruchomienie tylko jednego wątku w procesie, więc zobaczysz to samo zachowanie, niezależnie od tego, czy emulator jest używany, czy nie.