O que faz com que o depurador do Visual Studio pare de avaliar uma substituição do ToString?

221

Ambiente: Visual Studio 2015 RTM. (Eu não tentei versões mais antigas.)

Recentemente, depurei parte do meu código Noda Time e notei que, quando tenho uma variável local do tipo NodaTime.Instant(um dos structtipos centrais no Noda Time), as janelas "Locals" e "Watch" parece não chamar sua ToString()substituição. Se eu ligar ToString()explicitamente na janela de inspeção, vejo a representação apropriada, mas caso contrário, vejo:

variableName       {NodaTime.Instant}

o que não é muito útil.

Se eu alterar a substituição para retornar uma string constante, a string será exibida no depurador, para que seja claramente possível perceber que ela está lá - ela simplesmente não deseja usá-la em seu estado "normal".

Decidi reproduzir isso localmente em um pequeno aplicativo de demonstração, e aqui está o que eu criei. (Observe que em uma versão anterior deste post, DemoStructhavia uma classe e DemoClassnão existia - minha culpa, mas explica alguns comentários que parecem estranhos agora ...)

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

No depurador, agora vejo:

demoClass    {DemoClass}
demoStruct   {Struct: Bar}

No entanto, se eu reduzir a Thread.Sleepchamada de 1 segundo para 900ms, ainda haverá uma breve pausa, mas vejo Class: Foocomo o valor. Parece que não importa quanto tempo a Thread.Sleepchamada está DemoStruct.ToString(), ela sempre é exibida corretamente - e o depurador exibe o valor antes que a suspensão tenha sido concluída. (É como se estivesse Thread.Sleepdesativado.)

Agora, Instant.ToString()em Noda Time, trabalha bastante, mas certamente não leva um segundo inteiro - portanto, presumivelmente, há mais condições que fazem com que o depurador desista de avaliar uma ToString()chamada. E é claro que é uma estrutura de qualquer maneira.

Eu tentei repetir para ver se é um limite de pilha, mas isso parece não ser o caso.

Então, como posso descobrir o que está impedindo a avaliação completa do VS Instant.ToString()? Como observado abaixo, DebuggerDisplayAttributeparece ajudar, mas sem saber o porquê , nunca ficarei totalmente confiante quando precisar e quando não precisar.

Atualizar

Se eu usar DebuggerDisplayAttribute, as coisas mudam:

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

me dá:

demoClass      Evaluation timed out

Enquanto que quando eu o aplico no Noda Time:

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

um aplicativo de teste simples mostra o resultado certo:

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

Portanto, presumivelmente, o problema no Noda Time é uma condição que se DebuggerDisplayAttribute impõe - mesmo que não exista nos tempos limite. (Isso estaria de acordo com minha expectativa de que Instant.ToStringé rápida o suficiente para evitar um tempo limite.)

Essa pode ser uma solução boa o suficiente - mas eu ainda gostaria de saber o que está acontecendo e se posso alterar o código simplesmente para evitar ter que colocar o atributo em todos os vários tipos de valor no Noda Time.

Curioso e curioso

O que quer que esteja confundindo, o depurador só o confunde às vezes. Vamos criar uma classe que detém um Instante usa-lo para seu próprio ToString()método:

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

Agora acabo vendo:

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

No entanto, por sugestão de Eren nos comentários, se eu mudar InstantWrapperpara ser uma estrutura, recebo:

instant    {NodaTime.Instant}
wrapper    {InstantWrapper}

Portanto, ele pode avaliar Instant.ToString()- desde que seja invocado por outro ToStringmétodo ... que esteja dentro de uma classe. A parte da classe / estrutura parece ser importante com base no tipo da variável que está sendo exibida, não no código que precisa ser executado para obter o resultado.

Como outro exemplo disso, se usarmos:

object boxed = NodaConstants.UnixEpoch;

... então funciona bem, exibindo o valor certo. Cor me confuso.

Jon Skeet
fonte
7
@ John mesmo comportamento no VS 2013 (eu tive que remover o material c # 6), com uma mensagem adicional: Nome Avaliação da função desativada porque uma avaliação anterior da função expirou. Você deve continuar a execução para reativar a avaliação da função. string
vc 74
1
Bem-vindo ao c # 6.0 @ 3-14159265358979323846264
Neel
1
Talvez um DebuggerDisplayAttributefaça com que se esforce um pouco mais.
Rawling
1
vê-lo do 5º ponto neelbhatt40.wordpress.com/2015/07/13/... @ 3-14159265358979323846264 para nova c # 6.0
Neel
5
@DiomidisSpinellis: Bem, eu pedi aqui para que a) alguém que já tenha visto a mesma coisa antes ou saiba o interior do VS possa responder; b) quem encontrar o mesmo problema no futuro poderá obter a resposta rapidamente.
22815 Jon Skeet

Respostas:

193

Atualizar:

Esse bug foi corrigido no Visual Studio 2015, atualização 2. Avise-me se você ainda estiver com problemas para avaliar o ToString nos valores de estrutura usando a Atualização 2 ou posterior.

Resposta original:

Você está enfrentando uma limitação conhecida de bug / design com o Visual Studio 2015 e chamando ToString em tipos de estrutura. Isso também pode ser observado quando se lida com System.DateTimeSpan. System.DateTimeSpan.ToString()funciona nas janelas de avaliação com o Visual Studio 2013, mas nem sempre funciona em 2015.

Se você estiver interessado nos detalhes de baixo nível, aqui está o que está acontecendo:

Para avaliar ToString, o depurador faz o que é conhecido como "avaliação de função". Em termos bastante simplificados, o depurador suspende todos os threads no processo, exceto o thread atual, altera o contexto do thread atual para a ToStringfunção, define um ponto de interrupção de proteção oculto e permite que o processo continue. Quando o ponto de interrupção da proteção é atingido, o depurador restaura o processo ao seu estado anterior e o valor de retorno da função é usado para preencher a janela.

Para oferecer suporte a expressões lambda, tivemos que reescrever completamente o CLR Expression Evaluator no Visual Studio 2015. Em um nível alto, a implementação é:

  1. Roslyn gera código MSIL para expressões / variáveis ​​locais para obter os valores a serem exibidos nas várias janelas de inspeção.
  2. O depurador interpreta o IL para obter o resultado.
  3. Se houver instruções de "chamada", o depurador executará uma avaliação da função conforme descrito acima.
  4. O depurador / roslyn pega esse resultado e o formata na exibição em forma de árvore que é mostrada ao usuário.

Por causa da execução de IL, o depurador está sempre lidando com uma mistura complicada de valores "reais" e "falsos". Valores reais realmente existem no processo que está sendo depurado. Valores falsos existem apenas no processo do depurador. Para implementar a semântica de estrutura adequada, o depurador sempre precisa fazer uma cópia do valor ao enviar um valor de estrutura para a pilha IL. O valor copiado não é mais um valor "real" e agora existe apenas no processo do depurador. Isso significa que, se precisarmos executar posteriormente a avaliação da função ToString, não poderemos, porque o valor não existe no processo. Para tentar obter o valor, precisamos emular a execução doToStringmétodo. Embora possamos imitar algumas coisas, há muitas limitações. Por exemplo, não podemos emular código nativo e não podemos executar chamadas para delegar valores "reais" ou chamadas para valores de reflexão.

Com tudo isso em mente, aqui está o que está causando os vários comportamentos que você está vendo:

  1. O depurador não está avaliando NodaTime.Instant.ToString-> Isso ocorre porque é do tipo struct e a implementação do ToString não pode ser emulada pelo depurador, conforme descrito acima.
  2. Thread.Sleepparece levar tempo zero quando chamado por ToStringuma estrutura -> Isso ocorre porque o emulador está sendo executado ToString. Thread.Sleep é um método nativo, mas o emulador está ciente disso e simplesmente ignora a chamada. Fazemos isso para tentar obter um valor para mostrar ao usuário. Um atraso não seria útil neste caso.
  3. DisplayAttibute("ToString()")trabalho. -> Isso é confuso. A única diferença entre a chamada implícita de ToStringe DebuggerDisplayé que qualquer tempo limite da ToString avaliação implícita desabilitará todas as ToStringavaliações implícitas desse tipo até a próxima sessão de depuração. Você pode estar observando esse comportamento.

Em termos de problema / bug de design, isso é algo que planejamos abordar em uma versão futura do Visual Studio.

Espero que isso esclareça as coisas. Deixe-me saber se você tem mais perguntas. :-)

Patrick Nelson - MSFT
fonte
1
Alguma idéia de como o Instant.ToString funciona se a implementação é apenas "retornar uma string literal"? Parece que ainda existem algumas complexidades não explicadas :) Vou verificar se realmente consigo reproduzir esse comportamento ...
Jon Skeet
1
@ Jon, não tenho certeza do que você está perguntando. O depurador é independente da implementação ao fazer uma avaliação real da função e sempre tenta isso primeiro. O depurador se preocupa apenas com a implementação quando precisa emular a chamada - Retornar um literal de cadeia de caracteres é o caso mais simples de emular.
Patrick Nelson - MSFT
8
Idealmente, queremos que o CLR execute tudo. Isso fornece os resultados mais precisos e confiáveis. É por isso que fazemos uma avaliação real das funções para chamadas ToString. Quando isso não é possível, voltamos a emular a chamada. Isso significa que o depurador finge ser o CLR executando o método. Claramente, se a implementação for <code> retornar "Hello" </code>, isso é fácil de fazer. Se a implementação faz um P-Invoke, é mais difícil ou impossível.
Patrick Nelson - MSFT
3
@ tzachs, O emulador é completamente único. Se innerResultiniciar como nulo, o loop nunca será encerrado e, eventualmente, a avaliação expirará. De fato, as avaliações permitem apenas que um único encadeamento no processo seja executado por padrão; portanto, você verá o mesmo comportamento, independentemente de o emulador ser usado ou não.
Patrick Nelson - MSFT
2
BTW, se você sabe que suas avaliações requerem vários encadeamentos, consulte Debugger.NotifyOfCrossThreadDependency . A chamada desse método interrompe a avaliação com uma mensagem informando que a avaliação requer que todos os threads sejam executados e o depurador fornecerá um botão que o usuário pode pressionar para forçar a avaliação. A desvantagem é que quaisquer pontos de interrupção atingidos em outros threads durante a avaliação serão ignorados.
Patrick Nelson - MSFT