Apa yang membuat debugger Visual Studio berhenti mengevaluasi penggantian ToString?

221

Lingkungan: Visual Studio 2015 RTM. (Saya belum mencoba versi yang lebih lama.)

Baru-baru ini, saya telah men-debug beberapa kode Waktu Noda saya , dan saya perhatikan bahwa ketika saya mendapatkan variabel jenis lokal NodaTime.Instant(salah satu structjenis utama dalam Waktu Noda), jendela "Lokal" dan "Tonton" tampaknya tidak memanggil ToString()penggantiannya. Jika saya menelepon ToString()secara eksplisit di jendela arloji, saya melihat representasi yang sesuai, tetapi sebaliknya saya hanya melihat:

variableName       {NodaTime.Instant}

yang sangat tidak berguna.

Jika saya mengubah override untuk mengembalikan string konstan, string yang ditampilkan dalam debugger, sehingga jelas mampu mengambil bahwa itu ada - itu hanya tidak ingin menggunakannya dalam state "normal".

Saya memutuskan untuk mereproduksi ini secara lokal di aplikasi demo kecil, dan inilah yang saya buat. (Perhatikan bahwa dalam versi awal posting ini, DemoStructadalah sebuah kelas dan DemoClasstidak ada sama sekali - salahku, tapi itu menjelaskan beberapa komentar yang terlihat aneh sekarang ...)

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();
    }
}

Di debugger, sekarang saya melihat:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

Namun, jika saya mengurangi Thread.Sleeppanggilan turun dari 1 detik menjadi 900 ms, masih ada jeda singkat, tetapi kemudian saya melihat Class: Foosebagai nilainya. Tampaknya tidak masalah berapa lama Thread.Sleeppanggilan itu masuk DemoStruct.ToString(), selalu ditampilkan dengan benar - dan debugger menampilkan nilai sebelum tidur selesai. (Seolah-olah Thread.Sleepdinonaktifkan.)

Sekarang Instant.ToString()di Noda Time melakukan cukup banyak pekerjaan, tetapi tentu saja tidak memakan waktu satu detik - jadi mungkin ada lebih banyak kondisi yang menyebabkan debugger menyerah mengevaluasi ToString()panggilan. Dan tentu saja itu adalah struct.

Saya sudah mencoba berulang-ulang untuk melihat apakah itu batas tumpukan, tetapi tampaknya tidak demikian.

Jadi, bagaimana saya bisa mengetahui apa yang menghentikan VS dari sepenuhnya mengevaluasi Instant.ToString()? Seperti disebutkan di bawah ini, DebuggerDisplayAttributetampaknya membantu, tetapi tanpa tahu mengapa , saya tidak akan pernah sepenuhnya percaya diri ketika saya membutuhkannya dan ketika saya tidak.

Memperbarui

Jika saya gunakan DebuggerDisplayAttribute, semuanya berubah:

// For the sample code in the question...
[DebuggerDisplay("{ToString()}")]
public class DemoClass

memberi saya:

demoClass      Evaluation timed out

Sedangkan ketika saya menerapkannya dalam Noda Time:

[DebuggerDisplay("{ToString()}")]
public struct Instant

aplikasi tes sederhana menunjukkan kepada saya hasil yang tepat:

instant    "1970-01-01T00:00:00Z"

Jadi mungkin masalah di Noda Waktu adalah beberapa kondisi yang DebuggerDisplayAttribute tidak berlaku melalui - meskipun tidak memaksa melalui timeout. (Ini akan sesuai dengan harapan saya yang Instant.ToStringcukup cepat untuk menghindari batas waktu.)

Ini mungkin solusi yang cukup bagus - tetapi saya masih ingin tahu apa yang terjadi, dan apakah saya dapat mengubah kode hanya untuk menghindari keharusan meletakkan atribut pada semua jenis nilai yang berbeda di Noda Time.

Ingin tahu dan ingin tahu

Apa pun yang membingungkan, debugger terkadang hanya membingungkannya. Mari kita membuat sebuah kelas yang memegang sebuah Instantdan menggunakannya untuk sendiri ToString()metode:

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();
    }
}

Sekarang saya akhirnya melihat:

instant    {NodaTime.Instant}
wrapper    {1970-01-01T00:00:00Z}

Namun, atas saran Eren dalam komentar, jika saya berubah InstantWrappermenjadi seorang struct, saya mendapatkan:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Sehingga dapat mengevaluasi Instant.ToString()- selama itu dipanggil oleh ToStringmetode lain ... yang ada di dalam kelas. Bagian kelas / struct tampaknya penting berdasarkan pada jenis variabel yang ditampilkan, bukan kode apa yang perlu dieksekusi untuk mendapatkan hasilnya.

Sebagai contoh lain dari ini, jika kita menggunakan:

object boxed = NodaConstants.UnixEpoch;

... lalu berfungsi dengan baik, menampilkan nilai yang tepat. Warna saya bingung.

Jon Skeet
sumber
7
@John perilaku yang sama di VS 2013 (saya harus menghapus hal-hal c # 6), dengan pesan tambahan: Nama Evaluasi fungsi dinonaktifkan karena evaluasi fungsi sebelumnya habis. Anda harus melanjutkan eksekusi untuk mengaktifkan kembali evaluasi fungsi. string
vc 74
1
selamat datang di c # 6.0 @ 3-14159265358979323846264
Neel
1
Mungkin DebuggerDisplayAttributeakan menyebabkannya mencoba sedikit lebih keras.
Rawling
1
lihat ini poin ke-5 neelbhatt40.wordpress.com/2015/07/13/... @ 3-14159265358979323846264 untuk c # 6.0
Neel
5
@DiomidisSpinellis: Ya saya sudah bertanya di sini sehingga a) seseorang yang pernah melihat hal yang sama sebelumnya atau mengetahui bagian dalam VS dapat menjawab; b) siapa pun yang mengalami masalah yang sama di masa depan bisa mendapatkan jawabannya dengan cepat.
Jon Skeet

Jawaban:

193

Memperbarui:

Bug ini telah diperbaiki di Visual Studio 2015 Pembaruan 2. Beritahu saya jika Anda masih mengalami masalah mengevaluasi ToString pada nilai struct menggunakan Pembaruan 2 atau lebih baru.

Jawaban asli:

Anda mengalami keterbatasan bug / desain yang diketahui dengan Visual Studio 2015 dan memanggil ToString pada tipe struct. Ini juga bisa diamati ketika berhadapan dengan System.DateTimeSpan. System.DateTimeSpan.ToString()bekerja di jendela evaluasi dengan Visual Studio 2013, tetapi tidak selalu berfungsi pada tahun 2015.

Jika Anda tertarik dengan detail level rendah, inilah yang terjadi:

Untuk mengevaluasi ToString, debugger melakukan apa yang dikenal sebagai "evaluasi fungsi". Dalam istilah yang sangat disederhanakan, debugger menangguhkan semua utas dalam proses kecuali utas saat ini, mengubah konteks utas saat ini ke ToStringfungsi, menetapkan breakpoint pelindung tersembunyi, kemudian memungkinkan proses untuk melanjutkan. Ketika guard breakpoint terkena, debugger mengembalikan proses ke keadaan sebelumnya dan nilai kembali fungsi digunakan untuk mengisi jendela.

Untuk mendukung ekspresi lambda, kami harus menulis ulang sepenuhnya CLR Expression Evaluator di Visual Studio 2015. Pada level tinggi, implementasinya adalah:

  1. Roslyn menghasilkan kode MSIL untuk ekspresi / variabel lokal untuk mendapatkan nilai yang akan ditampilkan di berbagai jendela inspeksi.
  2. Debugger menginterpretasikan IL untuk mendapatkan hasilnya.
  3. Jika ada instruksi "panggilan", debugger menjalankan evaluasi fungsi seperti dijelaskan di atas.
  4. Debugger / roslyn mengambil hasil ini dan memformatnya menjadi tampilan seperti pohon yang ditampilkan kepada pengguna.

Karena eksekusi IL, debugger selalu berurusan dengan campuran rumit dari nilai "nyata" dan "palsu". Nilai nyata sebenarnya ada dalam proses yang sedang di-debug. Nilai-nilai palsu hanya ada dalam proses debugger. Untuk menerapkan semantik struct yang tepat, debugger selalu perlu membuat salinan nilai ketika mendorong nilai struct ke tumpukan IL. Nilai yang disalin tidak lagi menjadi nilai "nyata" dan sekarang hanya ada dalam proses debugger. Itu berarti jika nanti kita perlu melakukan evaluasi fungsi ToString, kita tidak bisa karena nilainya tidak ada dalam proses. Untuk mencoba dan mendapatkan nilai, kita perlu meniru pelaksanaanToStringmetode. Meskipun kita dapat meniru beberapa hal, ada banyak keterbatasan. Misalnya, kami tidak dapat meniru kode asli dan kami tidak dapat melakukan panggilan ke nilai delegasi "nyata" atau panggilan pada nilai refleksi.

Dengan semua itu dalam pikiran, di sini adalah apa yang menyebabkan berbagai perilaku yang Anda lihat:

  1. Debugger tidak mengevaluasi NodaTime.Instant.ToString-> Ini karena ini adalah tipe struct dan implementasi ToString tidak dapat ditiru oleh debugger seperti dijelaskan di atas.
  2. Thread.Sleeptampaknya tidak membutuhkan waktu ketika dipanggil oleh ToStringstruct -> Ini karena emulator mengeksekusi ToString. Thread.Sleep adalah metode asli, tetapi emulator menyadarinya dan mengabaikan panggilan. Kami melakukan ini untuk mencoba dan mendapatkan nilai untuk ditampilkan kepada pengguna. Penundaan tidak akan membantu dalam kasus ini.
  3. DisplayAttibute("ToString()")bekerja. -> Itu membingungkan. Satu-satunya perbedaan antara panggilan implisit dari ToStringdan DebuggerDisplayadalah bahwa setiap time-out dari ToString evaluasi implisit akan menonaktifkan semua ToStringevaluasi implisit untuk tipe itu sampai sesi debug berikutnya. Anda mungkin mengamati perilaku itu.

Dalam hal masalah desain / bug, ini adalah sesuatu yang kami rencanakan untuk ditangani dalam rilis Visual Studio mendatang.

Semoga itu beres. Beritahu saya jika Anda memiliki pertanyaan lebih lanjut. :-)

Patrick Nelson - MSFT
sumber
1
Adakah yang tahu bagaimana Instant.ToString bekerja jika implementasinya hanya "mengembalikan string literal"? Kedengarannya ada beberapa kerumitan yang masih belum ditemukan :) Saya akan memeriksa apakah saya benar-benar dapat mereproduksi perilaku itu ...
Jon Skeet
1
@ Jon, saya tidak yakin apa yang Anda minta. Debugger adalah agnostik dari implementasi ketika melakukan evaluasi fungsi nyata dan selalu mencoba ini terlebih dahulu. Debugger hanya peduli tentang implementasi ketika perlu meniru panggilan - Mengembalikan string literal adalah kasus paling sederhana untuk ditiru.
Patrick Nelson - MSFT
8
Idealnya kami ingin CLR untuk mengeksekusi semuanya. Ini memberikan hasil yang paling akurat dan dapat diandalkan. Itu sebabnya kami melakukan evaluasi fungsi nyata untuk panggilan ToString. Ketika ini tidak memungkinkan, kami kembali meniru panggilan. Itu berarti debugger berpura-pura menjadi CLR yang mengeksekusi metode. Jelas jika implementasinya <code> mengembalikan "Halo" </code>, ini mudah dilakukan. Jika implementasi melakukan P-Invoke, itu lebih sulit atau tidak mungkin.
Patrick Nelson - MSFT
3
@tzachs, Emulator ini sepenuhnya berulir tunggal. Jika innerResultdimulai sebagai nol, loop tidak akan pernah berhenti dan akhirnya evaluasi akan habis. Bahkan, evaluasi hanya memungkinkan satu utas dalam proses dijalankan secara default, jadi Anda akan melihat perilaku yang sama terlepas dari apakah emulator digunakan atau tidak.
Patrick Nelson - MSFT
2
BTW, jika Anda tahu evaluasi Anda membutuhkan banyak utas, lihat Debugger . Beri tahuOfCrossThreadDependency . Memanggil metode ini akan membatalkan evaluasi dengan pesan yang menyatakan bahwa evaluasi memerlukan semua utas untuk dijalankan dan debugger akan memberikan tombol yang dapat ditekan pengguna untuk memaksa evaluasi. Kerugiannya adalah setiap breakpoints mengenai thread lain selama evaluasi akan diabaikan.
Patrick Nelson - MSFT