Что делает отладчик Visual Studio прекратить оценку переопределения ToString?
Окружающая среда: окончательная первоначальная версия Visual Studio 2015. (Я не пробовал более старые версии.)
Недавно я отлаживал часть своего кода Noda Time, и я заметил, что когда у меня есть локальная переменная типа NodaTime.Instant
(один из центральных типов struct
в Noda Time), окна "Locals" и "Watch" не отображают его переопределение ToString()
. Если я вызываю ToString()
явно в окне просмотра, я вижу соответствующее представление, но в остальном я просто вижу:
variableName {NodaTime.Instant}
что не очень полезно.
Если я изменил переопределение, чтобы возвращать константную строку, строка отображается в отладчике, поэтому она явно может поднять ее там - она просто не хочет использовать ее в своем "нормальном" состоянии.
Я решил воспроизвести это локально в небольшом демонстрационном приложении, и вот то, что я придумал. (Обратите внимание, что в ранней версии этого сообщения DemoStruct
был классом, а DemoClass
вообще не существовал - моя ошибка, но он объясняет некоторые комментарии, которые сейчас выглядят странно...)
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();
}
}
В отладчике теперь я вижу:
demoClass {DemoClass}
demoStruct {Struct: Bar}
Однако, если я уменьшу вызов Thread.Sleep
вниз с 1 секунды до 900 мс, все еще короткая пауза, но тогда я вижу Class: Foo
как значение. Кажется, не имеет значения, как долго вызов Thread.Sleep
находится в DemoStruct.ToString()
, он всегда отображается правильно - и отладчик отображает значение до завершения сна. (Он как будто Thread.Sleep
отключен.)
Теперь Instant.ToString()
в Noda Time выполняет довольно много работы, но это, конечно, не занимает целую секунду, поэтому, по-видимому, есть больше условий, которые заставляют отладчика отказаться от оценки вызова ToString()
. И, конечно же, это структура.
Я пробовал рекурсировать, чтобы увидеть, является ли ограничение на стек, но это не так.
Итак, как я могу решить, что остановить VS от полной оценки Instant.ToString()
? Как отмечено ниже, DebuggerDisplayAttribute
, похоже, помогает, но, не зная почему, я никогда не буду полностью уверен в том, когда мне это нужно, а когда нет.
Обновление
Если я использую DebuggerDisplayAttribute
, все меняется:
// For the sample code in the question...
[DebuggerDisplay("{ToString()}")]
public class DemoClass
дает мне:
demoClass Evaluation timed out
Если я применяю его в Noda Time:
[DebuggerDisplay("{ToString()}")]
public struct Instant
простое тестовое приложение показывает мне правильный результат:
instant "1970-01-01T00:00:00Z"
Таким образом, предположительно проблема в Noda Time - это некоторое условие, которое DebuggerDisplayAttribute
проскальзывает - даже если оно не затягивает таймауты. (Это соответствовало бы моему ожиданию, что Instant.ToString
будет достаточно быстрым, чтобы избежать таймаута.)
Это может быть достаточно хорошее решение, но я все равно хотел бы знать, что происходит, и могу ли я изменить код просто, чтобы избежать необходимости добавлять атрибут во все типы значений в Noda Time.
Любопытный и любопытный
Все, что сбивает с толку, отладчик только иногда путает его. Позвольте создать класс, который содержит Instant
и использует его для собственного метода 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();
}
}
Теперь я вижу:
instant {NodaTime.Instant}
wrapper {1970-01-01T00:00:00Z}
Однако, по предложению Эрена в комментариях, если я изменяю InstantWrapper
как структуру, я получаю:
instant {NodaTime.Instant}
wrapper {InstantWrapper}
Таким образом, он может оценить Instant.ToString()
- до тех пор, пока он вызван другим методом ToString
... который находится внутри класса. Элемент class/struct кажется важным, основываясь на типе отображаемой переменной, а не на том, что нужно коду
для выполнения результата.
В качестве еще одного примера этого, если мы используем:
object boxed = NodaConstants.UnixEpoch;
... тогда он отлично работает, отображая правильное значение. Цвет меня смущает.
Ответы
Ответ 1
Update:
Эта ошибка была исправлена в обновлении Visual Studio 2015 2. Сообщите мне, если вы все еще сталкиваетесь с проблемами, оценивающими ToString для значений структуры с помощью обновления 2 или более поздней версии.
Исходный ответ:
В Visual Studio 2015 вы используете известное ограничение ошибок/дизайна и вызываете ToString для типов struct. Это также можно наблюдать при работе с System.DateTimeSpan
. System.DateTimeSpan.ToString()
работает в окнах оценки с Visual Studio 2013, но не всегда работает в 2015 году.
Если вас интересуют детали низкого уровня, вот что происходит:
Чтобы оценить ToString
, отладчик выполняет так называемую "оценку функции". В очень упрощенных терминах отладчик приостанавливает все потоки в процессе, кроме текущего потока, изменяет контекст текущего потока на функцию ToString
, устанавливает скрытую контрольную точку останова, а затем позволяет продолжить процесс. Когда ударяется контрольная точка останова, отладчик восстанавливает процесс до своего предыдущего состояния, и для заполнения окна используется возвращаемое значение функции.
Для поддержки лямбда-выражений нам пришлось полностью переписать Оценщик выражений CLR в Visual Studio 2015. На высоком уровне реализация такова:
- Roslyn генерирует код MSIL для выражений/локальных переменных, чтобы получить значения, отображаемые в различных окнах проверки.
- Отладчик интерпретирует IL для получения результата.
- Если есть какие-либо инструкции "вызова", отладчик выполняет
как описано выше.
- Отладчик /roslyn принимает этот результат и форматирует его в
подобный дереву, который отображается пользователю.
Из-за выполнения IL, отладчик всегда имеет дело со сложным сочетанием "реальных" и "поддельных" значений. Фактические значения фактически существуют в процессе отладки. Поддельные значения существуют только в процессе отладчика. Для реализации правильной семантики структуры отладчик всегда должен делать копию значения при нажатии значения структуры в стек IL. Скопированное значение больше не является "реальным" значением и теперь существует только в процессе отладчика. Это означает, что, если позже нам нужно выполнить оценку функции ToString
, мы не можем, потому что это значение не существует в этом процессе. Чтобы попытаться получить значение, нам нужно эмулировать выполнение метода ToString
. Хотя мы можем подражать некоторым вещам, существует множество ограничений. Например, мы не можем эмулировать собственный код, и мы не можем выполнять вызовы на "реальные" значения делегата или вызовы значений отражения.
Со всем этим в виду, вот что вызывает различные виды поведения, которые вы видите:
- Отладчик не оценивает
NodaTime.Instant.ToString
→ Это
потому что это тип структуры, и реализация ToString не может
эмулироваться отладчиком, как описано выше.
-
Thread.Sleep
, кажется, принимает нулевое время при вызове ToString
на
struct → Это потому, что эмулятор выполняет ToString
.
Thread.Sleep - это собственный метод, но эмулятор знает
и просто игнорирует вызов. Мы делаем это, чтобы попытаться получить ценность
для показа пользователю. Задержка не была бы полезной в этом случае.
-
DisplayAttibute("ToString()")
работает. → Это сбивает с толку. Единственный
разница между неявным вызовом ToString
и
DebuggerDisplay
заключается в том, что любые таймауты неявного ToString
оценка отключит все неявные оценки ToString
для этого
введите до следующего сеанса отладки. Вы можете наблюдать, что
поведение.
С точки зрения проблемы с дизайном/ошибкой, это то, что мы планируем рассмотреть в будущей версии Visual Studio.
Надеюсь, это очистит все. Дайте мне знать, если у вас появятся дополнительные вопросы.: -)