Что делает отладчик 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.

Надеюсь, это очистит все. Дайте мне знать, если у вас появятся дополнительные вопросы.: -)