อะไรทำให้ดีบักเกอร์ Visual Studio หยุดประเมินการแทนที่ ToString


221

สภาพแวดล้อม: Visual Studio 2015 RTM (ฉันไม่ได้ลองรุ่นที่เก่ากว่า)

เมื่อเร็ว ๆ นี้ฉันได้ทำการดีบักรหัสเวลา Nodaของฉันและฉันสังเกตว่าเมื่อฉันมีตัวแปรประเภทท้องถิ่นNodaTime.Instant(หนึ่งในstructประเภทส่วนกลางในเวลา Noda) หน้าต่าง "คนในท้องถิ่น" และ "ดู" ดูเหมือนจะไม่เรียกใช้การ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 มีปริมาณงานที่พอเหมาะ แต่ไม่ใช้เวลาสักวินาทีดังนั้นจึงมีเงื่อนไขเพิ่มเติมที่ทำให้ debugger ยกเลิกการประเมินผลการ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 เวลาเป็นเงื่อนไขบางอย่างที่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}

อย่างไรก็ตามตามคำแนะนำของ Eren ในความคิดเห็นหากฉันเปลี่ยนInstantWrapperเป็น struct ฉันจะได้รับ:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

ดังนั้นมันจึงสามารถประเมินได้Instant.ToString()- ตราบใดที่มันถูกเรียกโดยToStringวิธีอื่น... ซึ่งอยู่ในชั้นเรียน ส่วนคลาส / โครงสร้างดูเหมือนจะมีความสำคัญโดยขึ้นอยู่กับประเภทของตัวแปรที่จะแสดงไม่ใช่รหัสที่ต้องใช้ในการประมวลผลเพื่อให้ได้ผลลัพธ์

เป็นอีกตัวอย่างหนึ่งของสิ่งนี้ถ้าเราใช้:

object boxed = NodaConstants.UnixEpoch;

... จากนั้นก็ใช้งานได้ดีแสดงค่าที่ถูกต้อง ทำให้ฉันสับสน


7
@John พฤติกรรมแบบเดียวกันใน VS 2013 (ฉันต้องลบเนื้อหา c # 6) โดยมีข้อความเพิ่มเติม: ชื่อการประเมินฟังก์ชั่นปิดใช้งานเนื่องจากการประเมินฟังก์ชั่นก่อนหน้านี้หมดเวลา คุณต้องดำเนินการต่อเพื่อเปิดใช้การประเมินฟังก์ชันอีกครั้ง string
vc 74

1
ยินดีต้อนรับสู่ c # 6.0 @ 3-14159265358979323846264
Neel

1
อาจDebuggerDisplayAttributeจะทำให้มันลองยากขึ้นเล็กน้อย
Rawling

1
เห็นว่ามันเป็นจุดที่ 5 neelbhatt40.wordpress.com/2015/07/13/… @ 3-14159265358979323846264 สำหรับ c # 6.0 ใหม่
Neel

5
@DiomidisSpinellis: ดีฉันได้ถามที่นี่เพื่อให้ก) คนที่เคยเห็นสิ่งเดียวกันก่อนหรือรู้ว่าข้างในของ VS สามารถตอบ; b) ใครก็ตามที่พบเจอปัญหาเดียวกันนี้ในอนาคตจะได้รับคำตอบอย่างรวดเร็ว
Jon Skeet

คำตอบ:


193

ปรับปรุง:

ข้อผิดพลาดนี้ได้รับการแก้ไขใน Visual Studio 2015 Update 2 แจ้งให้เราทราบหากคุณยังพบปัญหาในการประเมิน ToString เกี่ยวกับค่า struct โดยใช้ Update 2 หรือใหม่กว่า

คำตอบเดิม:

คุณกำลังใช้งานข้อผิดพลาด / ข้อ จำกัด การออกแบบที่รู้จักกับ Visual Studio 2015 และเรียก ToString กับประเภทของ struct System.DateTimeSpanนอกจากนี้ยังสามารถสังเกตได้เมื่อต้องรับมือกับ System.DateTimeSpan.ToString()ทำงานในหน้าต่างการประเมินผลด้วย Visual Studio 2013 แต่ไม่สามารถทำงานได้ในปี 2558

หากคุณสนใจในรายละเอียดในระดับต่ำนี่คือสิ่งที่เกิดขึ้น:

ในการประเมินผลToStringดีบักเกอร์ทำสิ่งที่เรียกว่า "การประเมินผลฟังก์ชั่น" ในเงื่อนไขที่ง่ายขึ้นมากดีบักเกอร์จะหยุดเธรดทั้งหมดในกระบวนการยกเว้นเธรดปัจจุบันเปลี่ยนบริบทของเธรดปัจจุบันเป็นToStringฟังก์ชันตั้งค่าเบรกพอยต์ป้องกันที่ซ่อนอยู่จากนั้นอนุญาตให้กระบวนการดำเนินการต่อ เมื่อเบรกพอยต์ยามถูกกดจุดดีบั๊กจะกู้คืนกระบวนการกลับสู่สถานะก่อนหน้าและใช้ค่าส่งคืนของฟังก์ชันเพื่อเติมข้อมูลในหน้าต่าง

เพื่อสนับสนุนการแสดงออกแลมบ์ดาเราต้องเขียน CLR Expression Evaluator ใหม่ใน Visual Studio 2015 โดยสมบูรณ์ในระดับสูงการใช้งานคือ:

  1. Roslyn สร้างรหัส MSIL สำหรับการแสดงออก / ตัวแปรท้องถิ่นเพื่อรับค่าที่จะแสดงในหน้าต่างการตรวจสอบต่างๆ
  2. ตัวดีบักตีความ IL เพื่อให้ได้ผลลัพธ์
  3. หากมีคำสั่ง "การโทร" ใด ๆ ดีบักเกอร์จะดำเนินการประเมินฟังก์ชันตามที่อธิบายไว้ข้างต้น
  4. ตัวดีบักเกอร์ / โรสลินนำผลลัพธ์นี้ไปใช้และจัดรูปแบบลงในมุมมองแบบต้นไม้ที่แสดงให้ผู้ใช้เห็น

เนื่องจากการเรียกใช้งาน IL โปรแกรมดีบั๊กจะจัดการกับการผสมผสานที่ซับซ้อนของค่า "ของจริง" และ "ปลอม" ค่าจริงมีอยู่จริงในกระบวนการที่กำลังดีบั๊ก ค่าปลอมมีอยู่ในกระบวนการดีบักเกอร์เท่านั้น ในการใช้ซีแมนทิกส์ของ struct ที่เหมาะสมนั้น debugger จะต้องทำสำเนาของค่าเมื่อทำการผลักดันค่า struct ไปยัง IL stack ค่าที่คัดลอกจะไม่เป็นค่า "ของจริง" อีกต่อไปและจะมีอยู่ในกระบวนการดีบักเกอร์เท่านั้น ซึ่งหมายความว่าหากเราจำเป็นต้องทำการประเมินฟังก์ชั่นในภายหลังToStringเราไม่สามารถทำได้เพราะไม่มีค่าในกระบวนการ ในการลองและรับค่าเราจำเป็นต้องเลียนแบบการดำเนินการของToStringวิธี. ในขณะที่เราสามารถเลียนแบบบางสิ่งมีข้อ จำกัด มากมาย ตัวอย่างเช่นเราไม่สามารถเลียนแบบโค้ดเนทีฟและเราไม่สามารถดำเนินการเรียกไปยังค่าผู้แทน "ของจริง" หรือเรียกค่าสะท้อนกลับ

ในใจทั้งหมดนี่คือสิ่งที่ทำให้เกิดพฤติกรรมต่าง ๆ ที่คุณเห็น:

  1. ดีบักเกอร์ไม่ได้ประเมินNodaTime.Instant.ToString-> นี่เป็นเพราะมันเป็นประเภท struct และการใช้งานของ ToString ไม่สามารถเลียนแบบโดยดีบักเกอร์ดังอธิบายข้างต้น
  2. Thread.Sleepดูเหมือนว่าจะใช้เวลาในศูนย์เมื่อเรียกโดยToStringใน struct -> ToStringเพราะนี่คือการจำลองรัน Thread.Sleep เป็นวิธีเนทีฟ แต่อีมูเลเตอร์รับรู้และไม่สนใจการโทร เราทำเช่นนี้เพื่อลองและรับค่าที่จะแสดงต่อผู้ใช้ ความล่าช้าจะไม่เป็นประโยชน์ในกรณีนี้
  3. DisplayAttibute("ToString()")โรงงาน -> นั่นทำให้สับสน ความแตกต่างเพียงอย่างเดียวระหว่างการเรียกโดยปริยายของToStringและ DebuggerDisplayก็คือการที่ToString การประเมินโดยนัยใด ๆ ออกนอกเวลาจะปิดใช้งานToStringการประเมินโดยนัยทั้งหมดสำหรับประเภทนั้นจนกว่าเซสชันการดีบักถัดไป คุณอาจสังเกตพฤติกรรมนั้น

ในแง่ของปัญหาการออกแบบ / บั๊กนี่คือสิ่งที่เราวางแผนที่จะจัดการกับ Visual Studio ในอนาคต

หวังว่าจะช่วยล้างสิ่งต่างๆ แจ้งให้เราทราบหากคุณมีคำถามเพิ่มเติม :-)


1
มีความคิดใดที่ Instant.ToString ทำงานอย่างไรหากการใช้งานเป็นเพียง "คืนค่าตัวอักษร"? ดูเหมือนว่ายังมีความซับซ้อนบางอย่างที่ยังไม่ได้รับการอธิบาย :) ฉันจะตรวจสอบว่าฉันสามารถสร้างพฤติกรรมนั้นได้จริงๆ ...
Jon Skeet

1
@ จอนฉันไม่แน่ใจว่าสิ่งที่คุณถาม ตัวดีบักนั้นไม่เชื่อเรื่องการนำไปปฏิบัติเมื่อทำการประเมินฟังก์ชั่นจริงและจะพยายามทำสิ่งนี้ก่อนเสมอ โปรแกรมดีบั๊กจะให้ความสำคัญกับการนำไปใช้เมื่อต้องการเลียนแบบการโทรเท่านั้น - การส่งคืนสตริงตัวอักษรเป็นกรณีที่ง่ายที่สุดในการเลียนแบบ
Patrick Nelson - MSFT

8
โดยหลักการแล้วเราต้องการให้ CLR ดำเนินการทุกอย่าง สิ่งนี้ให้ผลลัพธ์ที่แม่นยำและน่าเชื่อถือที่สุด นั่นเป็นเหตุผลที่เราทำการประเมินฟังก์ชั่นจริงสำหรับการโทร ToString เมื่อสิ่งนี้เป็นไปไม่ได้เราจะกลับไปเลียนแบบการโทร นั่นหมายความว่าตัวดีบักอ้างว่าเป็น CLR ที่เรียกใช้งานเมธอด เห็นได้ชัดว่าหากการใช้งานคือ <code> ส่งคืน "Hello" </code> สิ่งนี้จะทำง่าย หากการใช้งานแบบ P-Invoke นั้นยากหรือเป็นไปไม่ได้
Patrick Nelson - MSFT

3
@tzachs อีมูเลเตอร์เป็นเธรดเดี่ยวทั้งหมด หากinnerResultเริ่มต้นเป็นโมฆะการวนซ้ำจะไม่สิ้นสุดและการประเมินจะหมดเวลาในที่สุด ในความเป็นจริงการประเมินผลอนุญาตให้ใช้เธรดเดียวเท่านั้นในกระบวนการเพื่อให้ทำงานตามค่าเริ่มต้นดังนั้นคุณจะเห็นลักษณะการทำงานเดียวกันโดยไม่คำนึงว่าอีมูเลเตอร์ถูกใช้หรือไม่
Patrick Nelson - MSFT

2
BTW ถ้าคุณรู้ว่าการประเมินผลของคุณจำเป็นต้องมีหลายหัวข้อใช้เวลาดูที่Debugger.NotifyOfCrossThreadDependency การเรียกวิธีการนี้จะยกเลิกการประเมินผลด้วยข้อความที่ระบุว่าการประเมินผลจำเป็นต้องใช้เธรดทั้งหมดเพื่อเรียกใช้และตัวดีบักจะให้ปุ่มที่ผู้ใช้สามารถกดเพื่อบังคับการประเมินผล ข้อเสียคือเบรกพอยต์ใด ๆ ที่กระทบกับเธรดอื่น ๆ ในระหว่างการประเมินผลจะถูกละเว้น
Patrick Nelson - MSFT
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.