Điều gì làm cho trình gỡ lỗi Visual Studio ngừng đánh giá ghi đè ToString?


221

Môi trường: Visual Studio 2015 RTM. (Tôi chưa thử các phiên bản cũ hơn.)

Gần đây, tôi đã gỡ lỗi một số mã Thời gian Noda của mình và tôi nhận thấy rằng khi tôi có một loại biến cục bộ NodaTime.Instant(một trong những structloại trung tâm trong Giờ Noda), cửa sổ "Địa phương" và "Xem" không xuất hiện để gọi ToString()ghi đè của nó . Nếu tôi gọi ToString()một cách rõ ràng trong cửa sổ đồng hồ, tôi thấy đại diện thích hợp, nhưng nếu không thì tôi chỉ thấy:

variableName       {NodaTime.Instant}

Điều này không hữu ích lắm.

Nếu tôi thay đổi ghi đè để trả về một chuỗi không đổi, chuỗi đó sẽ được hiển thị trong trình gỡ lỗi, vì vậy rõ ràng có thể chọn nó ở đó - nó chỉ không muốn sử dụng nó ở trạng thái "bình thường".

Tôi đã quyết định sao chép nó cục bộ trong một ứng dụng demo nhỏ và đây là những gì tôi nghĩ ra. (Lưu ý rằng trong phiên bản đầu của bài đăng này, DemoStructlà một lớp và DemoClasshoàn toàn không tồn tại - lỗi của tôi, nhưng nó giải thích một số nhận xét trông kỳ lạ bây giờ ...)

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

Trong trình gỡ lỗi, bây giờ tôi thấy:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

Tuy nhiên, nếu tôi giảm Thread.Sleepcuộc gọi xuống từ 1 giây xuống còn 900ms, vẫn có một khoảng dừng ngắn, nhưng sau đó tôi thấy đó Class: Foolà giá trị. Nó dường như không quan trọng Thread.Sleepcuộc gọi kéo dài bao lâu DemoStruct.ToString(), nó luôn được hiển thị đúng - và trình gỡ lỗi hiển thị giá trị trước khi giấc ngủ hoàn thành. (Như thể Thread.Sleeplà bị vô hiệu hóa.)

Bây giờ Instant.ToString()trong Noda Time thực hiện một số lượng công việc khá lớn, nhưng chắc chắn nó không mất cả giây - vì vậy có lẽ có nhiều điều kiện khiến trình gỡ lỗi từ bỏ việc đánh giá ToString()cuộc gọi. Và tất nhiên đó là một cấu trúc nào.

Tôi đã thử đệ quy để xem liệu đó có phải là giới hạn ngăn xếp hay không, nhưng dường như không phải vậy.

Vì vậy, làm thế nào tôi có thể tìm ra những gì ngăn chặn VS đánh giá đầy đủ Instant.ToString()? Như đã lưu ý dưới đây, có DebuggerDisplayAttributevẻ hữu ích, nhưng không hiểu tại sao , tôi sẽ không bao giờ hoàn toàn tự tin khi tôi cần và khi tôi không.

Cập nhật

Nếu tôi sử dụng DebuggerDisplayAttribute, mọi thứ sẽ thay đổi:

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

đưa cho tôi:

demoClass      Evaluation timed out

Trong khi tôi áp dụng nó trong Noda Time:

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

một ứng dụng thử nghiệm đơn giản cho tôi thấy kết quả đúng:

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

Vì vậy, có lẽ vấn đề trong Noda Thời gian là một số điều kiện là DebuggerDisplayAttribute không có hiệu lực thông qua - mặc dù nó không buộc qua timeout. (Điều này sẽ phù hợp với kỳ vọng của tôi, Instant.ToStringđủ nhanh để tránh thời gian chờ.)

Đây có thể là một giải pháp đủ tốt - nhưng tôi vẫn muốn biết chuyện gì đang xảy ra và liệu tôi có thể thay đổi mã đơn giản để tránh phải đặt thuộc tính trên tất cả các loại giá trị khác nhau trong Noda Time hay không.

Tò mò và tò mò

Bất cứ điều gì gây nhầm lẫn trình gỡ lỗi chỉ đôi khi nhầm lẫn nó. Hãy tạo ra một lớp mà giữ một Instantvà sử dụng nó cho riêng mình ToString()phương pháp:

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

Bây giờ tôi cuối cùng thấy:

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

Tuy nhiên, theo gợi ý của Eren trong các bình luận, nếu tôi thay đổi InstantWrapperthành một cấu trúc, tôi nhận được:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Vì vậy, nó có thể đánh giá Instant.ToString()- miễn là điều đó được gọi bằng một ToStringphương thức khác ... nằm trong một lớp. Phần lớp / struct dường như rất quan trọng dựa trên loại biến được hiển thị, chứ không phải mã nào cần được thực thi để có kết quả.

Một ví dụ khác về điều này, nếu chúng ta sử dụng:

object boxed = NodaConstants.UnixEpoch;

... Sau đó, nó hoạt động tốt, hiển thị đúng giá trị. Màu tôi bối rối.


7
@John hành vi tương tự trong VS 2013 (Tôi đã phải xóa nội dung c # 6), với một thông báo bổ sung: Tên Đánh giá chức năng bị vô hiệu hóa do đánh giá chức năng trước đó đã hết thời gian. Bạn phải tiếp tục thực hiện để đánh giá chức năng reenable. chuỗi
vc 74

1
chào mừng bạn đến c # 6.0 @ 3-14159265358979323846264
Neel

1
Có lẽ a DebuggerDisplayAttributesẽ khiến nó cố gắng hơn một chút.
Rawling

1
xem đó là điểm thứ 5 neelbhatt40.wordpress.com/2015/07/13/iêu @ 3-14159265358979323846264 cho c # 6.0 mới
Neel

5
@DiomidisSpinellis: Vâng, tôi đã hỏi nó ở đây để a) ai đó đã từng nhìn thấy điều tương tự trước đó hoặc biết bên trong VS có thể trả lời; b) bất cứ ai gặp phải vấn đề tương tự trong tương lai đều có thể nhận được câu trả lời nhanh chóng.
Jon Skeet

Câu trả lời:


193

Cập nhật:

Lỗi này đã được sửa trong Visual Studio 2015 Update 2. Hãy cho tôi biết nếu bạn vẫn đang gặp vấn đề khi đánh giá ToString trên các giá trị cấu trúc bằng cách sử dụng Cập nhật 2 trở lên.

Câu trả lời gốc:

Bạn đang gặp phải một giới hạn lỗi / thiết kế đã biết với Visual Studio 2015 và gọi ToString theo các kiểu cấu trúc. Điều này cũng có thể được quan sát khi giao dịch System.DateTimeSpan. System.DateTimeSpan.ToString()hoạt động trong các cửa sổ đánh giá với Visual Studio 2013, nhưng không phải lúc nào cũng hoạt động trong năm 2015.

Nếu bạn quan tâm đến các chi tiết cấp thấp, đây là những gì đang diễn ra:

Để đánh giá ToString, trình gỡ lỗi thực hiện cái được gọi là "đánh giá chức năng". Theo các thuật ngữ được đơn giản hóa rất nhiều, trình gỡ lỗi tạm ngưng tất cả các luồng trong quy trình ngoại trừ luồng hiện tại, thay đổi ngữ cảnh của luồng hiện tại thành ToStringhàm, đặt điểm dừng bảo vệ ẩn, sau đó cho phép quá trình tiếp tục. Khi điểm dừng bảo vệ bị tấn công, trình gỡ lỗi khôi phục quy trình về trạng thái trước đó và giá trị trả về của hàm được sử dụng để điền vào cửa sổ.

Để hỗ trợ các biểu thức lambda, chúng tôi đã phải viết lại hoàn toàn Trình đánh giá biểu thức CLR trong Visual Studio 2015. Ở mức cao, việc triển khai là:

  1. Roslyn tạo mã MSIL cho các biểu thức / biến cục bộ để có được các giá trị được hiển thị trong các cửa sổ kiểm tra khác nhau.
  2. Trình gỡ lỗi diễn giải IL để nhận kết quả.
  3. Nếu có bất kỳ hướng dẫn "gọi" nào, trình gỡ lỗi sẽ thực hiện đánh giá chức năng như được mô tả ở trên.
  4. Trình gỡ lỗi / roslyn lấy kết quả này và định dạng nó vào dạng xem giống như cây được hiển thị cho người dùng.

Do việc thực thi IL, trình gỡ lỗi luôn xử lý một hỗn hợp phức tạp của các giá trị "thực" và "giả". Giá trị thực sự tồn tại trong quá trình được gỡ lỗi. Giá trị giả chỉ tồn tại trong quá trình gỡ lỗi. Để triển khai ngữ nghĩa cấu trúc phù hợp, trình gỡ lỗi luôn cần tạo một bản sao của giá trị khi đẩy một giá trị cấu trúc vào ngăn xếp IL. Giá trị được sao chép không còn là giá trị "thực" và hiện chỉ tồn tại trong quy trình gỡ lỗi. Điều đó có nghĩa là nếu sau này chúng ta cần thực hiện đánh giá chức năng ToString, chúng ta không thể vì giá trị không tồn tại trong quy trình. Để thử và nhận giá trị, chúng ta cần mô phỏng thực thiToStringphương pháp. Trong khi chúng ta có thể mô phỏng một số thứ, có nhiều hạn chế. Ví dụ: chúng tôi không thể mô phỏng mã gốc và chúng tôi không thể thực hiện các lệnh gọi đến các giá trị ủy nhiệm "thực" hoặc các giá trị phản ánh.

Với tất cả những điều đó, đây là những gì gây ra các hành vi khác nhau mà bạn đang thấy:

  1. Trình gỡ lỗi không đánh giá NodaTime.Instant.ToString-> Điều này là do đây là loại cấu trúc và việc triển khai ToString không thể được trình gỡ lỗi mô phỏng như mô tả ở trên.
  2. Thread.Sleepdường như mất không thời gian khi được gọi bởi ToStringtrên một cấu trúc -> Điều này là do trình giả lập đang thực thi ToString. Thread.S ngủ là một phương thức riêng, nhưng trình giả lập nhận thức được nó và chỉ bỏ qua cuộc gọi. Chúng tôi làm điều này để thử và nhận được một giá trị để hiển thị cho người dùng. Một sự chậm trễ sẽ không hữu ích trong trường hợp này.
  3. DisplayAttibute("ToString()")làm. -> Điều đó thật khó hiểu. Sự khác biệt duy nhất giữa việc gọi ngầm ToStringDebuggerDisplaylà bất kỳ thời gian chờ nào của ToString đánh giá ngầm sẽ vô hiệu hóa tất cả các ToStringđánh giá ngầm cho loại đó cho đến phiên gỡ lỗi tiếp theo. Bạn có thể đang quan sát hành vi đó.

Xét về vấn đề / lỗi thiết kế, đây là điều chúng tôi dự định sẽ giải quyết trong phiên bản Visual Studio trong tương lai.

Hy vọng rằng sẽ xóa mọi thứ. Hãy cho tôi biết nếu bạn có thêm câu hỏi. :-)


1
Bất kỳ ý tưởng nào về cách Instant.ToString hoạt động nếu việc triển khai chỉ là "trả về một chuỗi ký tự"? Có vẻ như có một số phức tạp vẫn chưa được giải quyết :) Tôi sẽ kiểm tra xem tôi thực sự có thể tái tạo hành vi đó không ...
Jon Skeet

1
@Jon, tôi không chắc bạn đang hỏi gì Trình gỡ lỗi không tin vào việc thực hiện khi thực hiện đánh giá chức năng thực và nó luôn thử điều này trước tiên. Trình gỡ lỗi chỉ quan tâm đến việc triển khai khi nó cần mô phỏng cuộc gọi - Trả lại một chuỗi ký tự là trường hợp đơn giản nhất để mô phỏng.
Patrick Nelson - MSFT

8
Lý tưởng nhất là chúng tôi muốn CLR thực thi mọi thứ. Điều này cung cấp kết quả chính xác và đáng tin cậy nhất. Đó là lý do tại sao chúng tôi thực hiện đánh giá chức năng thực sự cho các cuộc gọi ToString. Khi điều này là không thể, chúng tôi quay lại mô phỏng cuộc gọi. Điều đó có nghĩa là trình gỡ lỗi giả vờ là CLR thực thi phương thức. Rõ ràng nếu việc triển khai là <code> trả về "Xin chào" </ code>, điều này rất dễ thực hiện. Nếu việc triển khai thực hiện P-Gọi thì khó khăn hơn hoặc không thể.
Patrick Nelson - MSFT

3
@tzachs, Trình giả lập hoàn toàn đơn luồng. Nếu innerResultbắt đầu là null, vòng lặp sẽ không bao giờ chấm dứt và cuối cùng việc đánh giá sẽ hết thời gian. Trong thực tế, các đánh giá chỉ cho phép một luồng trong quy trình chạy theo mặc định, vì vậy bạn sẽ thấy hành vi tương tự bất kể trình giả lập có được sử dụng hay không.
Patrick Nelson - MSFT

2
BTW, nếu bạn biết các đánh giá của mình yêu cầu nhiều luồng, hãy xem Debugger.NotifyOfCrossThreadDependency . Gọi phương thức này sẽ hủy bỏ việc đánh giá bằng một thông báo cho biết việc đánh giá yêu cầu tất cả các luồng phải chạy và trình gỡ lỗi sẽ cung cấp một nút mà người dùng có thể ấn để buộc đánh giá. Nhược điểm là bất kỳ điểm dừng nào đánh vào các luồng khác trong quá trình đánh giá sẽ bị bỏ qua.
Patrick Nelson - MSFT
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.