Existem motivos legítimos para retornar objetos de exceção em vez de lançá-los?

44

Esta questão destina-se a ser aplicada a qualquer linguagem de programação OO que suporte o tratamento de exceções; Estou usando o C # apenas para fins ilustrativos.

Exceções são geralmente planejadas para serem levantadas quando surge um problema que o código não pode manipular imediatamente e, em seguida, ser capturado em uma cláusula catch em um local diferente (geralmente um quadro de pilha externa).

P: Existem situações legítimas em que as exceções não são lançadas e capturadas, mas simplesmente retornadas de um método e então passadas como objetos de erro?

Essa pergunta surgiu porque o System.IObserver<T>.OnError method do .NET 4 sugere exatamente isso : exceções sendo passadas como objetos de erro.

Vamos analisar outro cenário, validação. Digamos que eu esteja seguindo a sabedoria convencional e que, portanto, eu esteja distinguindo entre um tipo de objeto de erro IValidationError e um tipo de exceção separado ValidationException que é usado para relatar erros inesperados:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

(O System.Component.DataAnnotations namespace faz algo bastante semelhante.)

Esses tipos podem ser empregados da seguinte forma:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Agora eu estou querendo saber, não poderia simplificar o acima para isso:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

Q: Quais são as vantagens e desvantagens dessas duas abordagens diferentes?

    
por stakx 05.08.2013 / 14:56
fonte

8 respostas

31

Are there any legitimate situations where exceptions are not thrown and caught, but simply returned from a method and then passed around as error objects?

Se nunca for lançado, não é uma exceção. É um objeto derived de uma classe Exception, embora não siga o comportamento. Chamar isso de exceção é puramente semântica neste momento, mas não vejo nada errado em não jogá-lo. Do meu ponto de vista, não lançar uma exceção é exatamente a mesma coisa que uma função interna capturá-la e impedir que ela se propague.

É válido para uma função retornar objetos de exceção?

Absolutamente. Aqui está uma pequena lista de exemplos em que isso pode ser apropriado:

  • Uma fábrica de exceções.
  • Um objeto de contexto que informa se houve um erro anterior como uma exceção pronta para uso.
  • Uma função que mantém uma exceção capturada anteriormente.
  • Uma API de terceiros que cria uma exceção de um tipo interno.

Não está jogando mal?

Fazer uma exceção é mais ou menos como: "Vá para a cadeia. Não passe para o Go!" no jogo de tabuleiro Monopoly. Ele diz ao compilador para pular todo o código-fonte até a captura sem executar qualquer código-fonte. Não tem nada a ver com erros, relatórios ou interrupção de bugs. Eu gosto de pensar em lançar uma exceção como uma declaração de "super retorno" para uma função. Ele retorna a execução do código para algum lugar muito maior que o chamador original.

O importante aqui é entender que o verdadeiro valor das exceções está no padrão try/catch e não na instanciação do objeto de exceção. O objeto de exceção é apenas uma mensagem de estado.

Em sua pergunta, você parece confundir o uso dessas duas coisas: um salto para um manipulador da exceção e o estado de erro que a exceção representa. Só porque você assumiu um estado de erro e o envolveu em uma exceção, não significa que esteja seguindo o padrão try/catch ou seus benefícios.

    
por 05.08.2013 / 17:06
fonte
51

Retornar exceções ao invés de lançá-las pode fazer sentido semântico quando você tem um método auxiliar para analisar a situação e retornar uma exceção apropriada que é então lançada pelo chamador (você poderia chamar isso de "fábrica de exceção"). Lançar uma exceção nessa função do analisador de erros significaria que algo deu errado durante a análise em si, enquanto o retorno de uma exceção significa que o tipo de erro foi analisado com sucesso.

Um possível caso de uso pode ser uma função que transforma os códigos de resposta HTTP em exceções:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Note que lançar uma exceção significa que o método foi usado errado ou teve um erro interno, enquanto retornando uma exceção significa que o código de erro foi identificado com sucesso.

    
por 05.08.2013 / 16:39
fonte
5

Conceitualmente, se o objeto de exceção for o resultado esperado da operação, então sim. Os casos em que posso pensar, no entanto, sempre envolvem captura em algum ponto:

  • Variações no padrão "Try" (em que um método encapsula um segundo método de lançamento de exceção, mas captura a exceção e, em vez disso, retorna um booleano indicando sucesso). Você poderia, em vez de retornar um Booleano, retornar qualquer exceção lançada (nulo se tivesse êxito), permitindo ao usuário final mais informações, mantendo a indicação de sucesso ou falha sem pegar.

  • Processamento de erros de fluxo de trabalho. Semelhante na prática ao encapsulamento do método "tentar padrão", você pode ter uma etapa do fluxo de trabalho abstraída em um padrão de cadeia de comando. Se um link na cadeia lançar uma exceção, geralmente é mais limpo capturar essa exceção no objeto de abstração e, em seguida, retorná-la como resultado da operação para que o mecanismo do fluxo de trabalho e o código real sejam executados como uma etapa do fluxo de trabalho -catch lógica própria.

por 05.08.2013 / 16:59
fonte
5

Digamos que você tenha inserido alguma tarefa para ser executada em algum pool de threads. Se esta tarefa lançar exceção, é um thread diferente para que você não a capture. Tópico onde foi executado apenas morrer.

Agora considere que algo (seja o código nessa tarefa ou a implementação do conjunto de encadeamentos) detecta essa exceção armazenando-a junto com a tarefa e considera a tarefa concluída (sem sucesso). Agora você pode perguntar se a tarefa está terminada (e quer perguntar se ele jogou, ou pode ser jogado novamente em seu segmento (ou melhor, nova exceção com original como causa)).

Se você fizer isso manualmente, você poderá notar que está criando uma nova exceção, lançando-a e capturando-a, depois armazenando-a, em diferentes segmentos recuperando o lançamento, capturando e reagindo nela. Por isso, pode fazer sentido pular jogar e pegar e apenas armazená-lo e terminar a tarefa e, em seguida, basta perguntar se há exceção e reagir a ela. Mas isso pode levar a um código mais complicado se, no mesmo local, puderem ser realmente lançadas exceções.

PS: isso é escrito com experiência com Java, onde as informações de rastreio de pilha são criadas quando uma exceção é criada (ao contrário do C # onde é criada ao ser lançada). Portanto, a abordagem de exceção não lançada em Java será mais lenta que em C # (a menos que seja pré-criada e reutilizada), mas terá informações de rastreio de pilha disponíveis.

Em geral, eu ficaria longe de criar uma exceção e nunca lançá-la (a menos que a otimização do desempenho após a criação de perfil aponte como gargalo). Pelo menos em java, onde a criação de exceções é cara (rastreamento de pilha). Em C # é a possibilidade, mas o IMO é surpreendente e, portanto, deve ser evitado.

    
por 05.08.2013 / 17:47
fonte
1

Depende do seu design, normalmente eu não retornaria exceções para um chamador, em vez disso, eu jogaria e pegaria e deixaria assim. Normalmente, o código é escrito para falhar cedo e rapidamente. Por exemplo, considere o caso de abrir um arquivo e processá-lo (This is C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

Nesse caso, erraríamos e interromperíamos o processamento do arquivo assim que ele encontrasse um registro incorreto.

Mas, digamos que o requisito é continuarmos processando o arquivo, mesmo que uma ou mais linhas tenham um erro. O código pode começar a se parecer com isso:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Portanto, neste caso, retornamos a exceção ao chamador, embora eu provavelmente não retorne uma exceção, mas sim algum tipo de objeto de erro, já que a exceção foi capturada e já tratada. Isso não é prejudicial ao retornar uma exceção de volta, mas outros chamadores podem voltar a lançar a exceção que pode não ser desejável, uma vez que o objeto de exceção tem esse comportamento. Se você quiser que os chamadores tenham a capacidade de reproduzir novamente, retorne uma exceção, caso contrário, retire as informações do objeto de exceção e construa um objeto de peso leve menor e retorne-o. A falha rápida é geralmente um código mais limpo.

Para o seu exemplo de validação, eu provavelmente não herdaria de uma classe de exceção ou lançaria exceções porque os erros de validação poderiam ser bastante comuns. Seria menos sobrecarga para retornar um objeto em vez de lançar uma exceção se 50% dos meus usuários não preencherem um formulário corretamente na primeira tentativa.

    
por 05.08.2013 / 22:01
fonte
1

Q: Are there any legitimate situations where exceptions are not thrown and caught, but simply returned from a method and then passed around as error objects?

Sim. Por exemplo, eu encontrei recentemente uma situação onde eu descobri que as exceções lançadas em um Serviço Web ASMX não incluem um elemento na mensagem SOAP resultante, então eu tive que gerá-lo.

Código ilustrativo:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function
    
por 06.08.2013 / 00:09
fonte
1

Na maioria das linguagens (afaik), as exceções vêm com alguns extras adicionados. Principalmente, um instantâneo do rastreamento de pilha atual é armazenado no objeto. Isso é bom para depuração, mas também pode ter implicações de memória.

Como o @ThinkingMedia já disse, você está realmente usando a exceção como um objeto de erro.

Em seu exemplo de código, parece que você faz isso principalmente para reutilização de código e para evitar composição. Eu pessoalmente não acho que isso seja uma boa razão para fazer isso.

Outro motivo possível seria enganar o idioma para fornecer um objeto de erro com um rastreamento de pilha. Isso fornece informações contextuais adicionais ao seu código de tratamento de erros.

Por outro lado, podemos supor que manter o rastreio de pilha tem um custo de memória. Por exemplo. Se você começar a coletar esses objetos em algum lugar, poderá ver um impacto desagradável na memória. É claro que isso depende muito de como os rastreamentos de pilha de exceção são implementados no respectivo idioma / mecanismo.

Então, é uma boa ideia?

Até agora eu não vi isso sendo usado ou recomendado. Mas isso não significa muito.

A pergunta é: o rastreamento de pilha é realmente útil para o tratamento de erros? Ou, mais geralmente, quais informações você deseja fornecer ao desenvolvedor ou administrador?

O típico padrão throw / try / catch faz com que seja necessário ter um rastreamento de pilha, porque senão você não tem idéia de onde ele está vindo. Para um objeto retornado, ele sempre vem da função chamada. O rastreamento de pilha pode conter todas as informações de que o desenvolvedor precisa, mas talvez algo menos pesado e mais específico seja suficiente.

    
por 20.12.2016 / 07:17
fonte
-4

A resposta é sim. Consulte o link e abra a seta para o guia de estilo do C ++ do Google sobre exceções. Você pode ver lá os argumentos que eles usaram para decidir, mas dizer que se eles tivessem que fazer isso de novo, eles provavelmente teriam decidido.

No entanto, é importante notar que a linguagem Go também não utiliza linguisticamente exceções, por motivos semelhantes.

    
por 05.08.2013 / 18:40
fonte