Sintaxe Swift do-try-catch

162

Eu tentei entender o novo problema de manipulação de erros no swift 2. Aqui está o que eu fiz: declarei primeiro um enum de erro:

enum SandwichError: ErrorType {
    case NotMe
    case DoItYourself
}

E então eu declarei um método que gera um erro (não pessoal de exceção. É um erro). Aqui está esse método:

func makeMeSandwich(names: [String: String]) throws -> String {
    guard let sandwich = names["sandwich"] else {
        throw SandwichError.NotMe
    }

    return sandwich
}

O problema é do lado de chamada. Aqui está o código que chama esse método:

let kitchen = ["sandwich": "ready", "breakfeast": "not ready"]

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
}

Após o docompilador de linha diz Errors thrown from here are not handled because the enclosing catch is not exhaustive. Mas, na minha opinião, é exaustivo porque há apenas dois casos em SandwichErrorenum.

Para declarações regulares de troca, o swift pode entender que é exaustivo quando todos os casos são tratados.

mustafa
fonte
3
Você não especificar o tipo de erro que você joga, então Swift não pode determinar todas as opções possíveis
Farlei Heinen
Existe uma maneira de especificar o tipo de erro?
Mustafa
Eu não consigo encontrar nada na nova versão do livro Swift - apenas o lança palavra-chave agora
Farlei Heinen
Funciona para mim em um playground sem erros ou avisos.
Fogmeister
2
Os playgrounds parecem permitir doblocos no nível superior que não são exaustivos - se você envolver o fazer em uma função de não arremessar, o erro será gerado.
Sam

Respostas:

267

Existem dois pontos importantes no modelo de tratamento de erros do Swift 2: exaustividade e resiliência. Juntos, eles se resumem à sua declaração do/ catch, precisando detectar todos os erros possíveis, não apenas aqueles que você sabe que pode lançar.

Observe que você não declara quais tipos de erros uma função pode gerar, apenas se ela gera. É um problema do tipo zero-um-infinito: como alguém que define uma função para os outros (incluindo o seu futuro eu), você não precisa fazer com que cada cliente da sua função se adapte a todas as mudanças na implementação do seu função, incluindo quais erros ela pode gerar. Você deseja que o código que chama sua função seja resistente a essa alteração.

Como sua função não pode dizer que tipo de erros ela gera (ou pode gerar no futuro), os catchblocos que os capturam não sabem que tipos de erros podem ser lançados. Portanto, além de lidar com os tipos de erro que você conhece, você precisa lidar com os que não conhece com uma catchinstrução universal - dessa forma, se sua função alterar o conjunto de erros que ela lança no futuro, os chamadores ainda perceberão seu problema. erros.

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
} catch let error {
    print(error.localizedDescription)
}

Mas não vamos parar por aí. Pense mais nessa idéia de resiliência. Do jeito que você projetou seu sanduíche, você deve descrever os erros em todos os lugares em que os usar. Isso significa que sempre que você altera o conjunto de casos de erro, é necessário alterar todos os locais que os usam ... não é muito divertido.

A idéia por trás da definição de seus próprios tipos de erro é permitir que você centralize coisas assim. Você pode definir um descriptionmétodo para seus erros:

extension SandwichError: CustomStringConvertible {
    var description: String {
        switch self {
            case NotMe: return "Not me error"
            case DoItYourself: return "Try sudo"
        }
    }
}

E então seu código de tratamento de erros pode solicitar que seu tipo de erro se descreva - agora todos os lugares em que você lida com erros podem usar o mesmo código e também com possíveis casos de erros futuros.

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch let error as SandwichError {
    print(error.description)
} catch {
    print("i dunno")
}

Isso também abre caminho para os tipos de erro (ou extensões neles) oferecerem suporte a outras formas de relatar erros - por exemplo, você pode ter uma extensão no seu tipo de erro que saiba como apresentar um UIAlertControllerpara relatar o erro a um usuário do iOS.

rickster
fonte
1
@ Rickster: Você realmente pode reproduzir o erro do compilador? O código original é compilado sem erros ou avisos para mim. E se uma exceção não corrigida for lançada, o programa será interrompido com error caught in main().- Portanto, enquanto tudo o que você disse parece sensato, não posso reproduzir esse comportamento.
Martin R
5
Adore como você separou as mensagens de erro em uma extensão. Uma ótima maneira de manter seu código limpo! Ótimo exemplo!
Konrad77
É altamente recomendável que você evite usar o forçado - tryexpressão no código de produção, uma vez que pode causar um erro de execução e fazer com que seu aplicativo falhar
Otar
@ Otar é um bom pensamento em geral, mas é um pouco fora de tópico - a resposta não se refere ao uso (ou não) try!. Além disso, é possível argumentar que existem casos de uso "seguros" válidos para as várias operações de "força" no Swift (desembrulhar, tentar etc.), mesmo para o código de produção - se por pré-condição ou configuração você tiver eliminado com segurança a possibilidade de falha, isso poderá ser mais razoável causar um curto-circuito à falha instantânea do que escrever um código de tratamento de erros que não pode ser testado.
Rickster 23/01/19
Se tudo que você precisa é exibir a mensagem de erro, colocar essa lógica dentro da SandwichErrorclasse faz sentido. No entanto, suspeito que para a maioria dos erros, a lógica de manipulação de erros não pode ser tão encapsulada. Isso ocorre porque geralmente requer o conhecimento do contexto do chamador (se deseja recuperar, tentar novamente ou relatar uma falha a montante, etc.). Em outras palavras, eu suspeito que o padrão mais comum teria que ser o de comparar com tipos de erro específicos de qualquer maneira.
Max
29

Eu suspeito que isso ainda não foi implementado corretamente ainda. O Guia de Programação Swift definitivamente parece sugerir que o compilador pode inferir correspondências exaustivas 'como uma declaração de chave'. Não menciona a necessidade de um generalcatch para ser exaustivo.

Você também notará que o erro está na trylinha, não no final do bloco, ou seja, em algum momento o compilador poderá identificar qual tryinstrução no bloco não manipulou tipos de exceção.

A documentação é um pouco ambígua. Examinei o vídeo 'What's new in Swift' e não consegui encontrar nenhuma pista; Vou continuar tentando.

Atualizar:

Agora estamos no Beta 3 sem nenhuma dica de inferência ErrorType. Agora acredito que, se isso já foi planejado (e ainda acho que foi em algum momento), o despacho dinâmico nas extensões de protocolo provavelmente o matou.

Atualização Beta 4:

O Xcode 7b4 adicionou suporte para comentários de documentos Throws:, que "devem ser usados ​​para documentar quais erros podem ser gerados e por quê". Eu acho que isso pelo menos fornece algum mecanismo para comunicar erros aos consumidores da API. Quem precisa de um sistema de tipos quando você tem documentação!

Outra atualização:

Depois de passar algum tempo esperando automática ErrorTypeinferência, e trabalhar para fora o que as limitações seria desse modelo, eu mudei de idéia - isso é o que eu espero implementos da Apple em seu lugar. Essencialmente:

// allow us to do this:
func myFunction() throws -> Int

// or this:
func myFunction() throws CustomError -> Int

// but not this:
func myFunction() throws CustomErrorOne, CustomErrorTwo -> Int

Mais uma atualização

A lógica de tratamento de erros da Apple já está disponível aqui . Também houve algumas discussões interessantes na lista de discussão de rápida evolução . Essencialmente, John McCall se opõe aos erros de digitação porque ele acredita que a maioria das bibliotecas acabará incluindo um caso de erro genérico de qualquer maneira, e é improvável que esses erros adicionem muito ao código além do padrão (ele usou o termo 'blefe aspiracional'). Chris Lattner disse que está aberto a erros de digitação no Swift 3, se puder trabalhar com o modelo de resiliência.

Sam
fonte
Obrigado pelos links. John não se convenceu: "muitas bibliotecas incluem 'outro tipo de erro'" não significa que todo mundo precisa de um tipo de "outro erro".
Franklin Yu
O contador óbvio é que não há uma maneira simples de saber que tipo de erro uma função lançará, até que isso aconteça, forçando o desenvolvedor a pegar todos os erros e tentar lidar com eles da melhor maneira possível. É um pouco chato, para ser franco.
William T Froggard
4

Swift teme que sua declaração de caso não cubra todos os casos. Para corrigi-lo, você precisa criar um caso padrão:

do {
    let sandwich = try makeMeSandwich(kitchen)
    print("i eat it \(sandwich)")
} catch SandwichError.NotMe {
    print("Not me error")
} catch SandwichError.DoItYourself {
    print("do it error")
} catch Default {
    print("Another Error")
}
Icaro
fonte
2
Mas isso não é estranho? Eu só tenho dois casos e todos eles listados em catchdeclarações.
Mustafa
2
Agora é um momento bom para uma solicitação de melhoria que adiciona func method() throws(YourErrorEnum), ou mesmo throws(YourEnum.Error1, .Error2, .Error3)para que você saiba o que pode ser jogado
Matthias Bauch
8
A equipe do compilador Swift em uma das sessões da WWDC deixou claro que não queria listas pedantes de todos os erros possíveis 'como Java'.
Sam
4
Não há erro padrão / padrão; basta deixar um prendedor em branco {} como outros cartazes têm apontado
Opus1217
1
@ Ícaro Isso não me torna seguro; se eu "adicionar uma nova entrada na matriz" no futuro, o compilador deve gritar comigo por não atualizar todas as cláusulas de captura afetadas.
Franklin Yu
3

Também fiquei desapontado com a falta de tipo que uma função pode lançar, mas agora a recebo graças ao @rickster e vou resumir da seguinte forma: digamos que poderíamos especificar o tipo que uma função lança, teríamos algo assim:

enum MyError: ErrorType { case ErrorA, ErrorB }

func myFunctionThatThrows() throws MyError { ...throw .ErrorA...throw .ErrorB... }

do {
    try myFunctionThatThrows()
}
case .ErrorA { ... }
case .ErrorB { ... }

O problema é que, mesmo que não alteremos nada no myFunctionThatThrows, se apenas adicionarmos um caso de erro ao MyError:

enum MyError: ErrorType { case ErrorA, ErrorB, ErrorC }

estamos ferrados porque nosso do / try / catch não é mais exaustivo, assim como em qualquer outro lugar onde chamamos funções que lançam MyError

greg3z
fonte
3
Não sei se entendi por que você está ferrado. Você receberia um erro do compilador, o que você deseja, certo? É o que acontece com as instruções de troca se você adicionar um caso de enum.
Sam
Em certo sentido, parecia-me mais provável que isso acontecesse com enumerações de erro / caso do caso, mas é exatamente como aconteceria com enumerações / alternância, você está certo. Ainda estou tentando me convencer de que a escolha da Apple de não digitar o que jogamos é a boa, mas você não me ajuda nisso! ^^
greg3z 15/06
Digitar manualmente erros gerados acabaria como uma grande bagunça em casos não triviais. Os tipos são uma união de todos os erros possíveis de todas as instruções throw e try dentro da função. Se você estiver mantendo manualmente as enumerações de erros, isso será doloroso. A catch {}na parte inferior de cada bloco é indiscutivelmente pior. Espero que o compilador acabe por inferir os tipos de erro automaticamente onde puder, mas não consegui confirmar.
Sam
Concordo que o compilador deve, teoricamente, ser capaz de inferir os tipos de erro que uma função lança. Mas acho que faz sentido também para o desenvolvedor anotá-las explicitamente para maior clareza. Nos casos não triviais de que você está falando, listar os diferentes tipos de erro parece-me aceitável: func f () lança ErrorTypeA, ErrorTypeB {}
greg3z
Definitivamente, existe uma grande parte que falta, pois não há mecanismo para comunicar tipos de erro (além dos comentários dos documentos). No entanto, a equipe Swift disse que não deseja listas explícitas de tipos de erro. Estou certo que a maioria pessoas que lidei com Java exceções verificadas no passado concordaria 😀
Sam
1
enum NumberError: Error {
  case NegativeNumber(number: Int)
  case ZeroNumber
  case OddNumber(number: Int)
}

extension NumberError: CustomStringConvertible {
         var description: String {
         switch self {
             case .NegativeNumber(let number):
                 return "Negative number \(number) is Passed."
             case .OddNumber(let number):
                return "Odd number \(number) is Passed."
             case .ZeroNumber:
                return "Zero is Passed."
      }
   }
}

 func validateEvenNumber(_ number: Int) throws ->Int {
     if number == 0 {
        throw NumberError.ZeroNumber
     } else if number < 0 {
        throw NumberError.NegativeNumber(number: number)
     } else if number % 2 == 1 {
         throw NumberError.OddNumber(number: number)
     }
    return number
}

Agora valide o número:

 do {
     let number = try validateEvenNumber(0)
     print("Valid Even Number: \(number)")
  } catch let error as NumberError {
     print(error.description)
  }
Yogendra Singh
fonte
-2

Crie enum como este:

//Error Handling in swift
enum spendingError : Error{
case minus
case limit
}

Crie um método como:

 func calculateSpending(morningSpending:Double,eveningSpending:Double) throws ->Double{
if morningSpending < 0 || eveningSpending < 0{
    throw spendingError.minus
}
if (morningSpending + eveningSpending) > 100{
    throw spendingError.limit
}
return morningSpending + eveningSpending
}

Agora verifique se o erro existe ou não e lide com isso:

do{
try calculateSpending(morningSpending: 60, eveningSpending: 50)
} catch spendingError.minus{
print("This is not possible...")
} catch spendingError.limit{
print("Limit reached...")
}
Mr.Javed Multani
fonte
Perto, mas sem charuto. Tente fixar o espaçamento e criar a caixa de enum camel.
Alec