O processamento de erros irrecuperáveis ​​no Swift

Prefácio


Este artigo é um exemplo de como podemos fazer pesquisas sobre o comportamento das funções da Swift Standard Library, construindo nosso conhecimento não apenas na documentação da Biblioteca, mas também em seu código-fonte.


Erros irrecuperáveis


Todos os eventos que os programadores chamam de "erros" podem ser separados em dois tipos.


  • Eventos causados ​​por fatores externos, como uma falha na conexão de rede.
  • Eventos causados ​​por erro de um programador, como chegar a um caso de operador de switch que deve estar inacessível.

Os eventos do primeiro tipo são processados ​​em um fluxo de controle regular. Por exemplo, reagimos à falha de rede mostrando uma mensagem para um usuário e configurando um aplicativo para aguardar a recuperação da conexão de rede.


Tentamos descobrir e eliminar eventos do segundo tipo o mais cedo possível antes que o código seja produzido. Uma das abordagens aqui é executar algumas verificações em tempo de execução, encerrando a execução do programa em um estado depurável e imprimir uma mensagem com uma indicação de onde no erro ocorreu o código.


Por exemplo, um programador pode encerrar a execução se o inicializador necessário não foi fornecido, mas foi chamado. Isso será notado e corrigido invariavelmente durante a primeira execução de teste.


required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } 

Outro exemplo é a alternância entre índices (vamos supor que, por algum motivo, você não possa usar a enumeração).


 switch index { case 0: // something is done here case 1: // other thing is done here case 2: // and other thing is done here default: assertionFailure("Impossible index") } 

Novamente, um programador causará falha durante a depuração aqui, a fim de observar inevitavelmente um erro na indexação.


Existem cinco funções de terminação na Swift Standard Library (como no Swift 4.2).


 func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func preconditionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never 

 func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func assertionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) 

 func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never 

Qual das cinco funções de terminação devemos preferir?


Código-fonte vs documentação


Vamos dar uma olhada no código fonte . Podemos ver o seguinte imediatamente:


  1. Cada uma dessas cinco funções finaliza a execução do programa ou não faz nada.
  2. Possível encerramento ocorre de duas maneiras.
    • Com a impressão de uma mensagem de depuração conveniente chamando _assertionFailure(_:_:file:line:flags:) .
    • Sem a mensagem de depuração, chamando Builtin.condfail(error._value) ou Builtin.int_trap() .
  3. A diferença entre as cinco funções de terminação está nas condições sob as quais todas as opções acima acontecem.
  4. fatalError(_:file:line) chama _assertionFailure(_:_:file:line:flags:) incondicionalmente.
  5. As outras quatro funções finais avaliam as condições chamando as seguintes funções de avaliação da configuração. (Eles começam com um sublinhado, o que significa que são internos e não devem ser chamados diretamente por um programador que usa a Swift Standard Library).
    • _isReleaseAssertConfiguration()
    • _isDebugAssertConfiguration()
    • _isFastAssertConfiguration()

Agora vamos olhar a documentação . Podemos ver o seguinte imediatamente.


  1. fatalError(_:file:line) imprime incondicionalmente uma determinada mensagem e interrompe a execução .
  2. Os efeitos das outras quatro funções de terminação variam dependendo do sinalizador de compilação usado: -Onone , -O , -Ounchecked . Por exemplo, consulte a documentação preconditionFailure(_:file:line:) .
  3. Podemos definir esses sinalizadores de compilação no Xcode através da SWIFT_OPTIMIZATION_LEVEL compilação do SWIFT_OPTIMIZATION_LEVEL .
  4. Também sabemos na documentação do Xcode 10 que mais um sinalizador de otimização - -Osize - é introduzido.
  5. Portanto, temos os quatro sinalizadores de otimização a serem considerados.
    • -Onone (não otimize)
    • -O (otimizar velocidade)
    • -Osize (otimizar para tamanho)
    • -Ounchecked (desative muitas verificações do compilador)

Podemos concluir que a configuração avaliada nas quatro funções de terminação é definida por esses sinalizadores de construção.


Executando funções de avaliação de configuração


Embora as funções de avaliação de configuração sejam projetadas para uso interno, algumas delas são públicas para fins de teste , e podemos testá-las através da CLI, fornecendo os seguintes comandos no Bash.


 $ echo 'print(_isFastAssertConfiguration())' >conf.swift $ swift conf.swift false $ swift -Onone conf.swift false $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift true 

 $ echo 'print(_isDebugAssertConfiguration())' >conf.swift $ swift conf.swift true $ swift -Onone conf.swift true $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift false 

Esses testes e a inspeção do código fonte nos levam às seguintes conclusões aproximadas.


Existem três configurações mutuamente exclusivas.


  • A configuração da liberação é definida fornecendo um -Osize construção -O ou -Osize .
  • A configuração de depuração é definida fornecendo um -Onone construção -Onone ou nenhum sinalizador de otimização.
  • _isFastAssertConfiguration() é avaliado como true se um -Ounchecked construção -Ounchecked estiver definido. Embora essa função tenha uma palavra "rápido" em seu nome, ela não tem nada a ver com a otimização do sinalizador de velocidade-O build.

Nota: essas conclusões não são a definição estrita de quando as compilações de depuração ou compilação ocorrem. É uma questão mais complexa. Mas essas conclusões estão corretas para o contexto de finalização do uso de funções.


Simplificando a imagem


-Ounchecked


Não vamos ver para que -Ounchecked sinalizador -Ounchecked (é irrelevante aqui), mas qual é o seu papel no contexto de finalização do uso de funções.


  • A documentação para a precondition(_:_:file:line:) - precondition(_:_:file:line:) e assert(_:_:file:line:) diz: "Nas compilações -Ounchecked , a condição não é avaliada, mas o otimizador pode assumir que sempre avalia como verdadeiro. Não atender a essa suposição é um erro de programação grave ".
  • A documentação para preconditionFailure(_:file:line) e assertionFailure(_:file:line:) diz: "Nas compilações -Ounchecked , o otimizador pode assumir que essa função nunca é chamada. Falha em satisfazer essa suposição é um erro de programação grave. "
  • Podemos ver no código fonte que a avaliação de _isFastAssertConfiguration() como true não deve ocorrer . (Se isso acontecer, estranho _conditionallyUnreachable() é chamado. Consulte as linhas 136 e _conditionallyUnreachable() )

Falando mais diretamente, você não deve permitir a acessibilidade das quatro funções de encerramento a seguir com o -Ounchecked build -Ounchecked definido para o seu programa.


  • precondition(_:_:file:line:)
  • preconditionFailure(_:file:line)
  • assert(_:_:file:line:)
  • assertionFailure(_:file:line:)

Use apenas fatalError(_:file:line) ao aplicar -Ounchecked e ao mesmo tempo permitindo que o ponto do seu programa com a fatalError(_:file:line) possa ser alcançada.


O papel de uma verificação de condição


Duas das funções de terminação permitem verificar condições. A inspeção do código-fonte nos permite ver que, se a condição falhar, o comportamento da função é o mesmo que o de seu respectivo primo:


  • precondition(_:_:file:line:) se torna preconditionFailure(_:file:line) ,
  • assert(_:_:file:line:) torna-se assertionFailure(_:file:line:) .

Esse conhecimento facilita análises adicionais.


Configurações de versão versus depuração


Eventualmente, mais documentação e inspeção do código-fonte nos permitem formular a tabela a seguir.


Terminando funções


Agora está claro que a escolha mais importante para um programador é como deve ser o comportamento do programa no lançamento, se uma verificação em tempo de execução revelar um erro.


O principal argumento aqui é que assert(_:_:file:line:) e assertionFailure(_:file:line:) tornam o impacto da falha do programa menos grave. Por exemplo, um aplicativo iOS pode ter corrompido a interface do usuário (desde que algumas verificações importantes de tempo de execução falharam), mas não falha.


Mas esse cenário pode não ser o desejado. Você tem uma escolha.


Never retornar tipo


Never é usado como um tipo de função de retorno que lança incondicionalmente um erro, interceptações ou não termina normalmente. Na verdade, esses tipos de funções não retornam, nunca retornam.


Entre as cinco funções de terminação, apenas preconditionFailure(_:file:line) e fatalError(_:file:line) retornam Never porque apenas essas duas funções interrompem incondicionalmente as execuções de programas e, portanto, nunca retornam.


Aqui está um bom exemplo de como usar Never digite um aplicativo de linha de comando. (Embora este exemplo não use as funções de encerramento da Swift Standard Library, mas a função C exit() padrão).


 func printUsagePromptAndExit() -> Never { print("Usage: command directory") exit(1) } guard CommandLine.argc == 2 else { printUsagePromptAndExit() } // ... 

Se printUsagePromptAndExit() retornar Void vez de Never , você receberá um erro de compilação com a mensagem " o corpo do 'guard' não deve cair, considere usar um 'return' ou 'throw' para sair do escopo ". Ao usar Never você está dizendo antecipadamente que nunca sai do escopo e, portanto, o compilador não fornecerá um erro no momento da criação. Caso contrário, você deve adicionar return no final do bloco do código de guarda, o que não parece agradável.


Para viagem


  • Não importa qual função de término usar se você tiver certeza de que todas as suas verificações de tempo de execução são relevantes apenas para a configuração de Depuração .
  • Use apenas fatalError(_:file:line) ao aplicar -Ounchecked e ao mesmo tempo permitindo que o ponto do seu programa com a fatalError(_:file:line) possa ser alcançada.
  • Use assert(_:_:file:line:) e assertionFailure(_:file:line:) se você estiver preocupado que as verificações de tempo de execução possam falhar de alguma forma no lançamento. Pelo menos seu aplicativo não trava.
  • Use Never para tornar seu código organizado.


Source: https://habr.com/ru/post/pt440884/


All Articles