Що змушує відладчик програми Visual Studio перестати оцінювати заміщення ToString?


221

Навколишнє середовище: Visual Studio 2015 RTM. (Я не пробував старих версій.)

Нещодавно я налагоджував частину свого коду Noda Time , і я помітив, що коли я отримую локальну змінну типу NodaTime.Instant(один із центральних structтипів у Noda Time), вікна "Місцеві жителі" та "Дивитися" не здається, щоб викликати її 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()дзвінка. І звичайно, це все-таки структура.

Я намагався повторювати, щоб побачити, чи це обмеження стека, але, здається, це не так.

Отже, як я можу розробити те, що зупиняє ВС від повної оцінки 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.

Curiouser і curiouser

Що б не плутало налагоджувач, лише його іноді плутає. Давайте створимо клас , який тримає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
@John така ж поведінка у VS 2013 (мені довелося видалити c # 6 речі), з додатковим повідомленням: Оцінка функції імені вимкнена, оскільки минула оцінка попередньої функції. Ви повинні продовжити виконання, щоб повторно оцінити функцію. рядок
vc 74

1
Ласкаво просимо на c # 6.0 @ 3-14159265358979323846264
Ніл

1
Можливо, a DebuggerDisplayAttributeспричинить це спробувати трохи складніше.
Раулінг

1
дивіться, що це 5-та точка neelbhatt40.wordpress.com/2015/07/13/… @ 3-14159265358979323846264 для нового c # 6.0
Neel

5
@DiomidisSpinellis: Ну, я тут це запитав, щоб а) хтось, хто або бачив те саме, і раніше, або знає внутрішню частину VS, може відповісти; б) кожен, хто в майбутньому зіткнеться з тим самим питанням, може швидко отримати відповідь.
Джон Скіт

Відповіді:


193

Оновлення:

Ця помилка була виправлена ​​у версії 2. Visual Studio 2015 Update 2. Повідомте мене, якщо ви все ще стикаєтеся з проблемами оцінки ToString щодо структурних значень за допомогою оновлення 2 або пізнішої версії.

Оригінальний відповідь:

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

Якщо вас цікавлять деталі низького рівня, ось що відбувається:

Для оцінки ToStringналагоджувач виконує те, що відомо як "оцінка функції". У значно спрощених термінах відладчик призупиняє всі потоки в процесі, крім поточного потоку, змінює контекст поточного потоку на ToStringфункцію, встановлює приховану точку розриву захисту, а потім дозволяє продовжувати процес. Коли буде досягнуто точку розриву захисту, налагоджувач відновить процес до його попереднього стану, а повернене значення функції використовується для заповнення вікна.

Щоб підтримати лямбда-вирази, нам довелося повністю переписати оцінювач виразів CLR у Visual Studio 2015. На високому рівні реалізація:

  1. Roslyn генерує код MSIL для виразів / локальних змінних, щоб отримати значення, що відображаються у різних вікнах перевірки.
  2. Відладчик інтерпретує ІЛ, щоб отримати результат.
  3. Якщо є якісь інструкції "виклику", налагоджувач виконує оцінку функції, як описано вище.
  4. Відладчик / рослін отримує цей результат і форматує його у вигляді дерева, показаному користувачеві.

Через виконання ІЛ відладчик завжди має справу зі складним поєднанням "реальних" та "підроблених" значень. Реальні значення фактично існують у процесі налагодження. Підроблені значення існують лише в процесі налагодження. Щоб реалізувати належну структуру семантики, налагоджувачу завжди потрібно зробити копію значення при натисканні значення структури на стек IL. Скопійоване значення більше не є "реальним" значенням і тепер існує лише у процесі налагодження. Це означає, що якщо нам пізніше потрібно буде виконати оцінку функції ToString, ми не можемо, оскільки значення не існує в процесі. Щоб спробувати отримати значення, нам потрібно наслідувати виконанняToStringметод. Хоча ми можемо наслідувати деякі речі, існує багато обмежень. Наприклад, ми не можемо емулювати нативний код і не можемо виконувати дзвінки до "реальних" значень делегування або виклики за значеннями відображення.

З огляду на це, ось що викликає різні поведінки, які ви бачите:

  1. Налагоджувач не оцінює NodaTime.Instant.ToString.
  2. Thread.SleepСхоже, це займає нульовий час, коли викликається ToStringна структуру -> Це тому, що емулятор виконує ToString. Thread.Sleep - це власний метод, але емулятор знає про це і просто ігнорує виклик. Ми робимо це, щоб спробувати отримати значення, яке потрібно показати користувачеві. Затримка не допоможе в цьому випадку.
  3. DisplayAttibute("ToString()")працює. -> Це заплутано. Єдина відмінність між неявним викликом ToStringі DebuggerDisplayполягає в тому, що будь-які тайм-аути неявної ToString оцінки відключать усі неявні ToStringоцінки для цього типу до наступного сеансу налагодження. Ви можете спостерігати за такою поведінкою.

Що стосується проблеми дизайну / помилки, саме це ми плануємо вирішити у майбутньому випуску Visual Studio.

Сподіваємось, що це все прояснить. Повідомте мене, якщо у вас є додаткові запитання. :-)


1
Будь-яка ідея, як працює Instant.ToString, якщо реалізація є просто "поверненням рядкового букваря"? Здається, що деякі складності досі не враховані :) Я перевірю, чи справді я можу відтворити таку поведінку ...
Джон Скіт

1
@Jon, я не впевнений, що ти просиш. Відладчик є агностиком реалізації, коли робить реальну оцінку функції, і він завжди намагається зробити це першим. Відладчик піклується про реалізацію лише тоді, коли йому потрібно емулювати виклик - Повернення рядкового літералу - найпростіший випадок для емуляції.
Патрік Нельсон - MSFT

8
В ідеалі ми хочемо, щоб CLR виконував усе. Це забезпечує найбільш точні та надійні результати. Ось чому ми робимо реальну оцінку функції для дзвінків ToString. Коли це неможливо, ми переходимо до емуляції виклику. Це означає, що налагоджувач видає себе за CLR, який виконує метод. Зрозуміло, що якщо реалізація <code> повертає "Hello" </code>, це легко зробити. Якщо реалізація робить P-Invoke, це складніше або неможливіше.
Патрік Нельсон - MSFT

3
@tzachs, Емулятор повністю однопоточний. Якщо він innerResultпочинається як нульовий, цикл ніколи не припиняється, і з часом оцінка закінчиться. Насправді, оцінки дозволяють запускати лише один потік у процесі за замовчуванням, тому ви побачите таку саму поведінку незалежно від того, використовується емулятор чи ні.
Патрік Нельсон - MSFT

2
BTW, якщо вам відомо, що для ваших оцінок потрібні кілька потоків, подивіться на Debugger.NotifyOfCrossThreadDependency . Виклик цього методу скасує оцінку повідомленням про те, що оцінка вимагає запуску всіх потоків, і налагоджувач надасть кнопку, яку користувач може натиснути, щоб примусити оцінку. Недоліком є ​​те, що будь-які точки перерви, які потрапляють на інші потоки, під час оцінювання будуть ігноровані.
Патрік Нельсон - MSFT
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.