Pergunta

Retiro uma exceção com "arremesso", mas o Stacktrace está incorreto:

static void Main(string[] args) {
    try {
        try {
            throw new Exception("Test"); //Line 12
        }
        catch (Exception ex) {
            throw; //Line 15
        }
    }
    catch (Exception ex) {
        System.Diagnostics.Debug.Write(ex.ToString());
    }
    Console.ReadKey();
}

O stacktrace certo deve ser:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 12

Mas eu entendo:

System.Exception: Test
   at ConsoleApplication1.Program.Main(String[] args) in Program.cs:Line 15

Mas a linha 15 é a posição do "arremesso". Eu testei isso com .NET 3.5.

Foi útil?

Solução

Jogar duas vezes no mesmo método é provavelmente um caso especial - não consegui criar um rastreamento de pilha em que diferentes linhas no mesmo método se seguem. Como diz a palavra, um "rastreamento de pilha" mostra os quadros de pilha que uma exceção atravessou. E existe apenas um quadro de pilha por chamada de método!

Se você jogar de outro método, throw; não removerá a entrada para Foo(), como esperado:

  static void Main(string[] args)
  {
     try
     {
        Rethrower();
     }
     catch (Exception ex)
     {
        Console.Write(ex.ToString());
     }
     Console.ReadKey();
  }

  static void Rethrower()
  {
     try
     {
        Foo();
     }
     catch (Exception ex)
     {
        throw;
     }

  }

  static void Foo()
  {
     throw new Exception("Test"); 
  }

Se você modificar Rethrower() e substitua throw; por throw ex;, a Foo() A entrada no rastreamento da pilha desaparece. Novamente, esse é o comportamento esperado.

Outras dicas

É algo que pode ser considerado como esperado. Modificar o rastreamento da pilha é um caso comum se você especificar throw ex;, O FXCOP notificará que a pilha é modificada. Caso você faça throw;, nenhum aviso é gerado, mas ainda assim, o rastreamento será modificado. Infelizmente, por enquanto, é o melhor não pegar o ex ou jogá -lo como interno. Eu acho que deve ser considerado como um Impacto do Windows ou smth assim - Editado. Jeff Richter descreve esta situação com mais detalhes em seu "CLR via C#":

O código a seguir joga o mesmo objeto de exceção que ele capturou e faz com que o CLR redefinirá seu ponto de partida para a exceção:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw e; // CLR thinks this is where exception originated.
    // FxCop reports this as an error
  }
}

Por outro lado, se você re-lança um objeto de exceção usando a palavra-chave Throw por si só, o CLR não redefine o ponto de partida da pilha. O código a seguir recai novamente o mesmo objeto de exceção que ele capturou, fazendo com que o CLR não redefinir seu ponto de partida para a exceção:

private void SomeMethod() {
  try { ... }
  catch (Exception e) {
    ...
    throw; // This has no effect on where the CLR thinks the exception
    // originated. FxCop does NOT report this as an error
  }
}

De fato, a única diferença entre esses dois fragmentos de código é o que o CLR acha que é o local original em que a exceção foi lançada. Infelizmente, quando você joga ou repensa uma exceção, o Windows redefine o ponto de partida da pilha. Portanto, se a exceção se tornar sem tratamento, o local da pilha que é relatado ao relatório de erro do Windows é a localização do último arremesso ou re-arremesso, mesmo que o CLR conheça o local da pilha em que a exceção original foi lançada. Isso é lamentável porque torna os aplicativos de depuração que falharam no campo muito mais difíceis. Alguns desenvolvedores acharam isso tão intolerável que escolheram uma maneira diferente de implementar seu código para garantir que o rastreamento da pilha reflita realmente o local em que uma exceção foi lançada originalmente:

private void SomeMethod() {
  Boolean trySucceeds = false;
  try {
    ...
    trySucceeds = true;
  }
  finally {
    if (!trySucceeds) { /* catch code goes in here */ }
  }
}

Esta é uma limitação bem conhecida na versão do Windows do CLR. Ele usa o suporte interno do Windows para manuseio de exceção (SEH). O problema é que é baseado em quadros de pilha e um método tem apenas um quadro de pilha. Você pode resolver facilmente o problema movendo o bloco de tentativa/captura interno para outro método auxiliar, criando assim outro quadro de pilha. Outra conseqüência dessa limitação é que o compilador JIT não incluirá nenhum método que contenha uma instrução TRY.

Como posso preservar o verdadeiro Stacktrace?

Você joga uma nova exceção e inclui a exceção original como a exceção interna.

Mas isso é feio ... mais ... faz você escolher a exceção de Rigth para jogar ....

Você está errado sobre o feio Mas bem sobre os outros dois pontos. A regra geral é: não pegue, a menos que você faça algo com ela, como embrulhá -lo, modifique, engula ou registre -o. Se você decidir catch e depois throw Novamente, verifique se você está fazendo algo com ele; caso contrário, deixe -o borbulhar.

Você também pode ser tentado a fazer uma captura simplesmente para poder de intervalo na captura, mas o depurador do Visual Studio tem opções suficientes para tornar essa prática desnecessária, tente usar exceções de primeira chance ou pontos de interrupção condicional.

Editar/substituir

O comportamento é realmente diferente, mas sutilmente assim. Quanto a Por quê O comportamento, se diferente, precisarei adiar para um especialista em CLR.

EDITAR: Resposta de Alexd parece indicar que isso é por design.

Jogar a exceção no mesmo método que o pega confunde um pouco a situação, então vamos dar uma exceção de outro método:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Throw();
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    public static void Throw()
    {
        int a = 0;
        int b = 10 / a;
    }
}

Se throw; é usado, a pilha de chamadas é (números de linha substituídos pelo código):

at Throw():line (int b = 10 / a;)
at Main():line (throw;) // This has been modified

Se throw ex; é usado, a pilha de call é:

at Main():line (throw ex;)

Se a exceção não for capturada, a pilha de chamadas é:

at Throw():line (int b = 10 / a;)
at Main():line (Throw())

Testado no .net 4 / vs 2010

Há uma pergunta duplicada aqui.

Pelo que entendi - jogue; é compilado em Instrução MSIL 'Rethrow' e modifica o último quadro do rastreamento da pilha.

Eu esperaria que ele mantenha o rastreamento original e adicione a linha onde ela foi re-arruinada, mas aparentemente Só pode haver um quadro de pilha por chamada de método.

Conclusão: Evite usar arremesso; E embrulhe sua exceção em um novo no re -arremesso - não é feio, é a melhor prática.

Você pode preservar o rastreamento da pilha usando

ExceptionDispatchInfo.Capture(ex);

Aqui está o exemplo de código:

    static void CallAndThrow()
    {
        throw new ApplicationException("Test app ex", new Exception("Test inner ex"));
    }

    static void Main(string[] args)
    {
        try
        {
            try
            {
                try
                {
                    CallAndThrow();
                }
                catch (Exception ex)
                {
                    var dispatchException = ExceptionDispatchInfo.Capture(ex);

                    // rollback tran, etc

                    dispatchException.Throw();
                }
            }
            catch (Exception ex)
            {
                var dispatchException = ExceptionDispatchInfo.Capture(ex);

                // other rollbacks

                dispatchException.Throw();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.InnerException.Message);
            Console.WriteLine(ex.StackTrace);
        }

        Console.ReadLine();
    }

A saída será algo como:

Test app ex
Test inner ex
   at TestApp.Program.CallAndThrow() in D:\Projects\TestApp\TestApp\Program.cs:line 19
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 30
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 38
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at TestApp.Program.Main(String[] args) in D:\Projects\TestApp\TestApp\Program.cs:line 47

OK, parece haver um bug na estrutura .NET, se você lançar uma exceção e repensar -o no mesmo método, o número da linha original será perdido (será a última linha do método).

Fortunatelly, um cara inteligente chamado Fabrice Marguerie encontrou uma solução para este bug. Abaixo está minha versão, que você pode testar em Este .Net Fiddle.

private static void RethrowExceptionButPreserveStackTrace(Exception exception)
{
    System.Reflection.MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
      System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
    preserveStackTrace.Invoke(exception, null);
      throw exception;
}

Agora pegue a exceção como normalmente, mas em vez de jogar; Basta chamar esse método, e pronto, o número da linha original será preservado!

Não tenho certeza se isso é por design, mas acho que sempre foi assim.

Se a nova exceção de arremesso original estiver em um método separado, o resultado do arremesso deve ter o nome do método original e o número da linha e, em seguida, o número da linha em Main, onde a exceção é re-arruinada.

Se você usar o Throw EX, o resultado será apenas a linha em Main, onde a exceção é o RETHROW.

Em outras palavras, Throw Ex perde tudo o sacktrace, enquanto o arremesso preserva o rastreamento da pilha história (ou seja, detalhes dos métodos de nível inferior). Mas se a sua exceção for gerada pelo mesmo método que o seu RETHROW, você poderá perder algumas informações.

Nb. Se você escrever um programa de teste muito simples e pequeno, a estrutura às vezes poderá otimizar as coisas e alterar um método para ser um código embutido, o que significa que os resultados podem diferir de um programa "real".

Você quer o seu número de linha certa? Apenas use 1 tente/capturar por método. Em sistemas, bem ... apenas na camada da interface do usuário, não na lógica ou no acesso a dados, isso é muito irritante, porque se você precisar de transações de banco de dados, bem, elas não devem estar na camada da interface do usuário e você não terá O número da linha certa, mas se você não precisar deles, não se retire nem sem exceção na captura ...

Código de amostra de 5 minutos:

Cardápio Arquivo -> Novo projeto, coloque três botões e chame o seguinte código em cada um:

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithoutTC();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button2_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC1();
    }
    catch (Exception ex)
    {
            MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

private void button3_Click(object sender, EventArgs e)
{
    try
    {
        Class1.testWithTC2();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace + Environment.NewLine + Environment.NewLine + "In. Ex.: " + ex.InnerException);
    }
}

Agora, crie uma nova classe:

class Class1
{
    public int a;
    public static void testWithoutTC()
    {
        Class1 obj = null;
        obj.a = 1;
    }
    public static void testWithTC1()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch
        {
            throw;
        }
    }
    public static void testWithTC2()
    {
        try
        {
            Class1 obj = null;
            obj.a = 1;
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

Corra ... o primeiro botão é lindo!

Eu acho que isso é menos um caso de trace de pilhas mudando e mais a ver com a maneira como o número da linha para o rastreamento da pilha é determinado. Experimentando no Visual Studio 2010, o comportamento é semelhante ao que você esperaria da documentação do MSDN: "Throw Ex"; Reconstrua o rastreamento da pilha a partir do ponto desta afirmação, "Throw"; Deixa o rastreamento da pilha como, exceto que, onde quer que a exceção seja Rethrown, o número da linha é a localização do RETHROW e não a chamada que a exceção ocorreu.

Então, com "arremesso"; A árvore de chamada de método é inalterada, mas os números da linha podem mudar.

Eu me deparei com isso algumas vezes, e pode ser por design e simplesmente não documentado completamente. Eu posso entender por que eles podem ter feito isso, pois a localização do Rethrow é muito útil para saber, e se seus métodos são simples o suficiente, a fonte original geralmente seria óbvia de qualquer maneira.

Como muitas outras pessoas disseram, geralmente é melhor não pegar a exceção, a menos que você realmente precise, e/ou você vai lidar com isso naquele momento.

Nota lateral interessante: o Visual Studio 2010 nem me permite construir o código conforme apresentado na pergunta, pois ele recebe a divisão por um erro zero no tempo de compilação.

Isso porque você pegou o Exception a partir de Linha 12 e tenha repensado Linha 15, então o rastreamento da pilha leva como dinheiro, que o Exception foi jogado a partir daí.

Para lidar melhor com exceções, você deve simplesmente usar try...finally, e deixe os não atendidos Exception borbulhar.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top