Что заставляет отладчик Visual Studio перестать оценивать переопределение ToString?

221

Среда: Visual Studio 2015 RTM. (Я не пробовал старые версии.)

Недавно я отлаживал часть своего кода 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"

Так , предположительно, проблема в Ноде время некоторое условие , которое 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методом ... который находится внутри класса. Часть класса / структуры кажется важной в зависимости от типа отображаемой переменной, а не от того, какой код необходимо выполнить, чтобы получить результат.

В качестве другого примера этого, если мы используем:

object boxed = NodaConstants.UnixEpoch;

... тогда он работает нормально, отображая правильное значение. Цвет меня смутил.

Джон Скит
источник
7
@ То же самое поведение у Джона в VS 2013 (мне пришлось удалить материал c # 6), с дополнительным сообщением: Имя Оценка функции отключена, потому что истекло время предыдущей оценки функции. Вы должны продолжить выполнение, чтобы включить функцию оценки. Строка
ВК 74
1
добро пожаловать в c # 6.0 @ 3-14159265358979323846264
Нил
1
Может быть, это DebuggerDisplayAttributeзаставит его попробовать немного сложнее.
Роулинг
1
видите 5-й балл neelbhatt40.wordpress.com/2015/07/13/… @ 3-14159265358979323846264 для нового c # 6.0
Нил
5
@DiomidisSpinellis: Ну, я спросил об этом здесь, чтобы а) кто-то, кто видел то же самое раньше или знает, что внутри VS, может ответить; б) любой, кто столкнется с той же проблемой в будущем, сможет быстро получить ответ.
Джон Скит

Ответы:

193

Обновить:

Эта ошибка была исправлена ​​в Visual Studio 2015 Update 2. Дайте мне знать, если у вас все еще возникают проблемы с оценкой ToString для значений структуры с использованием Update 2 или более поздней версии.

Оригинальный ответ:

Вы сталкиваетесь с известным ограничением ошибок / дизайна в Visual Studio 2015 и вызываете ToString для структурных типов. Это также можно наблюдать при работе с System.DateTimeSpan. System.DateTimeSpan.ToString()работает в окнах оценки с Visual Studio 2013, но не всегда работает в 2015 году.

Если вы заинтересованы в деталях низкого уровня, вот что происходит:

Для оценки ToStringотладчик выполняет то, что известно как «оценка функции». В упрощенном виде отладчик приостанавливает все потоки в процессе, кроме текущего, изменяет контекст текущего потока на ToStringфункцию, устанавливает скрытую контрольную точку останова и затем позволяет процессу продолжаться. При достижении контрольной точки защиты отладчик восстанавливает процесс до его предыдущего состояния, а возвращаемое значение функции используется для заполнения окна.

Для поддержки лямбда-выражений нам пришлось полностью переписать оценщик выражений CLR в Visual Studio 2015. На высоком уровне реализация:

  1. Roslyn генерирует код MSIL для выражений / локальных переменных, чтобы получить значения, которые будут отображаться в различных окнах проверки.
  2. Отладчик интерпретирует IL для получения результата.
  3. Если есть какие-либо инструкции «вызова», отладчик выполняет оценку функции, как описано выше.
  4. Отладчик / Roslyn берет этот результат и форматирует его в древовидное представление, которое отображается для пользователя.

Из-за выполнения IL отладчик всегда имеет дело со сложным сочетанием «реальных» и «поддельных» значений. Реальные значения действительно существуют в отлаживаемом процессе. Ложные значения существуют только в процессе отладчика. Чтобы реализовать правильную семантику структуры, отладчик всегда должен делать копию значения при отправке значения структуры в стек IL. Скопированное значение больше не является «реальным» значением и теперь существует только в процессе отладчика. Это означает, что если нам позже понадобится выполнить оценку функции ToString, мы не сможем, потому что значение не существует в процессе. Чтобы попытаться получить значение, нам нужно эмулировать выполнениеToStringметод. Хотя мы можем подражать некоторым вещам, есть много ограничений. Например, мы не можем эмулировать нативный код и не можем выполнять вызовы «реальных» значений делегатов или вызовы значений отражения.

Имея это в виду, вот что вызывает различные виды поведения, которые вы видите:

  1. Отладчик не выполняет оценку NodaTime.Instant.ToString-> Это потому, что это структурный тип, и реализация ToString не может эмулироваться отладчиком, как описано выше.
  2. Thread.Sleepкажется, занимает нулевое время при вызове ToStringструктуры -> Это потому, что эмулятор выполняется ToString. Thread.Sleep является нативным методом, но эмулятор знает об этом и просто игнорирует вызов. Мы делаем это, чтобы попытаться получить значение для показа пользователю. Задержка не поможет в этом случае.
  3. DisplayAttibute("ToString()")работает. -> Это сбивает с толку. Единственное различие между неявным вызовом ToStringи DebuggerDisplayсостоит в том, что любые тайм-ауты неявной ToString оценки будут отключать все неявные ToStringоценки для этого типа до следующего сеанса отладки. Вы можете наблюдать это поведение.

С точки зрения проблемы проектирования / ошибки, это то, что мы планируем решить в будущем выпуске Visual Studio.

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

Патрик Нельсон - MSFT
источник
1
Любая идея, как Instant.ToString работает, если реализация просто «вернуть строковый литерал»? Похоже, что некоторые сложности все еще не учтены :) Я проверю, что я действительно могу воспроизвести это поведение ...
Джон Скит
1
@ Джон, я не уверен, что ты спрашиваешь. Отладчик не зависит от реализации, когда выполняет оценку реальной функции, и он всегда пробует это в первую очередь. Отладчик заботится о реализации только тогда, когда ему нужно эмулировать вызов. Возвращение строкового литерала - самый простой случай для эмуляции.
Патрик Нельсон - MSFT
8
В идеале мы хотим, чтобы CLR выполнял все. Это обеспечивает наиболее точные и надежные результаты. Вот почему мы делаем реальную оценку функции для вызовов ToString. Когда это невозможно, мы возвращаемся к эмуляции вызова. Это означает, что отладчик притворяется CLR, выполняющим метод. Ясно, что если реализация <code> возвращает «Hello» </ code>, это легко сделать. Если реализация делает P-Invoke, это более сложно или невозможно.
Патрик Нельсон - MSFT
3
@tzachs, эмулятор полностью однопоточный. Если innerResultначинается с нуля, цикл никогда не прекратится, и в конечном итоге время ожидания истечет. Фактически, оценки позволяют запускать по умолчанию только один поток в процессе, поэтому вы увидите одно и то же поведение независимо от того, используется эмулятор или нет.
Патрик Нельсон - MSFT
2
Кстати, если вы знаете, что ваши оценки требуют нескольких потоков, взгляните на Debugger.NotifyOfCrossThreadDependency . Вызов этого метода прервет оценку с сообщением о том, что оценка требует выполнения всех потоков, а отладчик предоставит кнопку, которую пользователь может нажать, чтобы вызвать оценку. Недостатком является то, что любые точки останова, попавшие в другие потоки во время оценки, будут игнорироваться.
Патрик Нельсон - MSFT