Visual StudioデバッガーがToStringオーバーライドの評価を停止するのはなぜですか?


221

環境:Visual Studio 2015 RTM。(私は古いバージョンを試していません。)

最近、Noda Timeのコードの一部をデバッグしていて、タイプのローカル変数NodaTime.InstantstructNoda Timeの中心的なタイプの1つ)を取得すると、[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()、野田時間ではかなりの量の作業が行われますが、1秒もかかりません。おそらく、デバッガーがToString()呼び出しの評価をやめる原因となる条件がさらにあると考えられます。そしてもちろん、とにかくそれは構造体です。

スタック制限であるかどうかを確認するために再帰的に試しましたが、そうではないようです。

だから、どうすればVSが完全に評価するのを止めているのかを知ることができInstant.ToString()ますか?以下に示すように、DebuggerDisplayAttribute助けになるように見えますが、理由がわからないので、必要なときと必要としないときを完全に確信することは決してありません。

更新

を使用するとDebuggerDisplayAttribute、状況が変わります。

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

私に与える:

demoClass      Evaluation timed out

野田タイムで適用すると:

[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}

ただし、コメントでのErenの提案で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
Neel、

1
たぶん、DebuggerDisplayAttributeもう少し頑張ろうとするでしょう。
ローリング、2015

1
それが5番目のポイントであることを確認してくださいneelbhatt40.wordpress.com/2015/07/13/…新しいc#6.0の場合は @ 3-14159265358979323846264
ニール

5
@DiomidisSpinellis:まあ私はここでそれを聞いたので、a)以前に同じものを見たことがあるか、VSの内部を知っている人が答えることができます。b)将来同じ問題が発生した場合、誰でもすぐに答えを得ることができます。
Jon Skeet

回答:


193

更新:

このバグはVisual Studio 2015 Update 2で修正されています。Update2以降を使用して構造体の値のToStringを評価する問題が引き続き発生する場合はお知らせください。

元の回答:

Visual Studio 2015で既知のバグ/デザインの制限が発生し、構造体型でToStringを呼び出しています。これは、を処理するときにも確認できSystem.DateTimeSpanます。 System.DateTimeSpan.ToString()Visual Studio 2013の評価ウィンドウで機能しますが、2015で常に機能するとは限りません。

低レベルの詳細に関心がある場合は、次のようになります。

を評価するためToStringに、デバッガは「関数評価」と呼ばれる処理を実行します。簡単に言うと、デバッガーは、現在のスレッドを除くプロセス内のすべてのスレッドを中断し、現在のスレッドのコンテキストをToString関数に変更し、非表示のガードブレークポイントを設定して、プロセスの続行を許可します。ガードブレークポイントに到達すると、デバッガーはプロセスを以前の状態に復元し、関数の戻り値を使用してウィンドウにデータを入力します。

ラムダ式をサポートするには、Visual Studio 2015でCLR式エバリュエーターを完全に書き換える必要がありました。大まかに言うと、実装は次のとおりです。

  1. Roslynは、式/ローカル変数のMSILコードを生成して、さまざまな検査ウィンドウに表示される値を取得します。
  2. デバッガーはILを解釈して結果を取得します。
  3. 「call」命令がある場合、デバッガーは上記の関数評価を実行します。
  4. デバッガー/ roslynはこの結果を受け取り、ユーザーに表示されるツリーのようなビューにフォーマットします。

ILの実行により、デバッガーは常に「実際の」値と「偽の」値の複雑な混合を処理します。実際の値は、デバッグされているプロセスに実際に存在します。偽の値はデバッガプロセスにのみ存在します。適切な構造体のセマンティクスを実装するために、デバッガーは、構造体の値をILスタックにプッシュするときに常に値のコピーを作成する必要があります。コピーされた値はもはや「実際の」値ではなく、デバッガプロセスにのみ存在します。つまり、後での関数評価を実行する必要がある場合ToString、プロセスに値が存在しないため、実行できません。試してみて値を取得するには、ToString方法。一部をエミュレートできますが、多くの制限があります。たとえば、ネイティブコードをエミュレートしたり、「実際の」デリゲート値の呼び出しやリフレクション値の呼び出しを実行したりすることはできません。

これらすべてを念頭に置いて、さまざまな動作が発生している原因は次のとおりです。

  1. デバッガーが評価していないNodaTime.Instant.ToString->これは、それが構造体型であり、ToStringの実装を上記のようにデバッガーでエミュレートできないためです。
  2. Thread.SleepToString構造体で呼び出されたときに時間がかからないようです->これはエミュレータが実行されているためToStringです。Thread.Sleepはネイティブメソッドですが、エミュレータはそれを認識していて、呼び出しを無視します。これは、ユーザーに表示する値を取得するために行います。この場合、遅延は役に立ちません。
  3. DisplayAttibute("ToString()")動作します。->それは混乱しています。暗黙の呼び出しの唯一の違いToStringDebuggerDisplay暗黙的のいずれかのタイムアウトということでToString 評価はすべて、暗黙的な無効になりToString、次のデバッグセッションまで、そのタイプのための評価を。その動作を観察している可能性があります。

設計の問題/バグに関しては、これはVisual Studioの将来のリリースで対処する予定です。

うまくいけば、これで問題が解決します。他にご不明な点がありましたらお知らせください。:-)


1
実装が「文字列リテラルを返す」だけの場合に、Instant.ToStringがどのように機能するか考えていますか?まだ考慮されていない複雑さがいくつかあるようです:)私は本当にその振る舞いを再現できることを確認します...
Jon Skeet

1
@ジョン、私はあなたが何を求めているのかわかりません。デバッガーは、実際の関数評価を行う場合、実装に依存せず、常に最初にこれを試みます。デバッガは、呼び出しをエミュレートする必要がある場合にのみ実装を考慮します。文字列リテラルを返すことがエミュレートする最も簡単なケースです。
Patrick Nelson-MSFT 2015

8
理想的には、CLRがすべてを実行するようにします。これにより、最も正確で信頼性の高い結果が得られます。これが、ToString呼び出しに対して実際の関数評価を行う理由です。これが不可能な場合は、コールのエミュレーションにフォールバックします。つまり、デバッガはメソッドを実行するCLRになりすまします。明らかに、実装が<code> return "Hello" </ code>の場合、これは簡単に実行できます。実装がP-Invokeを実行する場合、それはより困難または不可能です。
Patrick Nelson-MSFT 2015

3
@tzachs、エミュレータは完全にシングルスレッドです。innerResultnullで始まる場合、ループは終了せず、最終的に評価がタイムアウトします。実際、評価ではデフォルトでプロセス内の単一のスレッドしか実行できないため、エミュレーターが使用されているかどうかに関係なく、同じ動作が見られます。
Patrick Nelson-MSFT 2015

2
ところで、評価に複数のスレッドが必要であることがわかっている場合は、Debugger.NotifyOfCrossThreadDependencyを見てください。このメソッドを呼び出すと、評価はすべてのスレッドを実行する必要があることを示すメッセージで評価が中止され、デバッガーはユーザーが評価を強制するために押すことができるボタンを提供します。欠点は、評価中に他のスレッドにヒットしたブレークポイントが無視されることです。
Patrick Nelson-MSFT 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.