Prática recomendada para implementar um inicializador fiável no Swift

Com o código a seguir, tento definir uma class de modelo simples e inicializável, que usa um dictionary (json-) como parâmetro. O inicializador deve retornar nil se o nome do usuário não estiver definido no json original.

1. Por que o código não compila? A mensagem de erro diz:

Todas as propriedades armazenadas de uma instância de class devem ser inicializadas antes de retornar nil de um inicializador.

Isso não faz sentido. Por que devo inicializar essas propriedades quando planejo retornar nil ?

2. A minha abordagem é a certa ou haveria outras ideias ou padrões comuns para alcançar o meu objective?

 class User: NSObject { let userName: String let isSuperUser: Bool = false let someDetails: [String]? init?(dictionary: NSDictionary) { if let value: String = dictionary["user_name"] as? String { userName = value } else { return nil } if let value: Bool = dictionary["super_user"] as? Bool { isSuperUser = value } someDetails = dictionary["some_details"] as? Array super.init() } } 

Atualização: no log de alterações do Swift 2.2 (lançado em 21 de março de 2016):

Inicializadores de class designados declarados como fiável ou jogando agora podem retornar nil ou lançar um erro, respectivamente, antes que o object tenha sido totalmente inicializado.


Para o Swift 2.1 e anterior:

De acordo com a documentação da Apple (e seu erro do compilador), uma class deve inicializar todas as suas propriedades armazenadas antes de retornar nil de um inicializador fiável:

Para classs, no entanto, um inicializador fiável pode triggersr uma falha de boot somente depois que todas as propriedades armazenadas introduzidas por essa class tiverem sido definidas para um valor inicial e qualquer delegação inicializadora tenha ocorrido.

Nota: Ele realmente funciona bem para estruturas e enumerações, apenas não classs.

A maneira sugerida para lidar com propriedades armazenadas que não podem ser inicializadas antes de o inicializador falhar é declará-las como opcionais implicitamente não-desembrulhados.

Exemplo dos documentos:

 class Product { let name: String! init?(name: String) { if name.isEmpty { return nil } self.name = name } } 

No exemplo acima, a propriedade name da class Product é definida como tendo um tipo de string opcional implicitamente não desembrulhado (String!). Por ser de um tipo opcional, isso significa que a propriedade name possui um valor padrão nulo antes de ser atribuído um valor específico durante a boot. Esse valor padrão de zero significa que todas as propriedades introduzidas pela class Product possuem um valor inicial válido. Como resultado, o inicializador fiável do Produto pode triggersr uma falha de boot no início do inicializador, se for passada uma cadeia vazia, antes de atribuir um valor específico à propriedade name dentro do inicializador.

No seu caso, no entanto, simplesmente definindo userName como String! não corrige o erro de compilation porque você ainda precisa se preocupar em inicializar as propriedades em sua class base, NSObject . Felizmente, com userName definido como uma String! , você pode realmente chamar super.init() antes de return nil que irá iniciar sua class base NSObject e corrigir o erro de compilation.

 class User: NSObject { let userName: String! let isSuperUser: Bool = false let someDetails: [String]? init?(dictionary: NSDictionary) { super.init() if let value = dictionary["user_name"] as? String { self.userName = value } else { return nil } if let value: Bool = dictionary["super_user"] as? Bool { self.isSuperUser = value } self.someDetails = dictionary["some_details"] as? Array } } 

Isso não faz sentido. Por que devo inicializar essas propriedades quando planejo retornar nulo?

De acordo com Chris Lattner, isso é um erro. Aqui está o que ele diz:

Esta é uma limitação de implementação no compilador swift 1.1, documentada nas notas de lançamento. O compilador é atualmente incapaz de destruir classs parcialmente inicializadas em todos os casos, por isso não permite a formação de uma situação onde seria necessário. Consideramos isso um bug a ser corrigido em versões futuras, não em um recurso.

Fonte

EDITAR:

Tão rápido agora é open source e de acordo com este changelog ele é corrigido agora em snapshots do swift 2.2

Inicializadores de class designados declarados como fiável ou jogando agora podem retornar nil ou lançar um erro, respectivamente, antes que o object tenha sido totalmente inicializado.

Aceito que a resposta de Mike S seja a recomendação da Apple, mas não acho que seja uma boa prática. O ponto principal de um sistema de tipos forte é mover os erros de tempo de execução para o tempo de compilation. Esta “solução” derrota esse propósito. IMHO, melhor seria ir em frente e inicializar o nome de usuário para "" e, em seguida, verificá-lo após o super.init (). Se userNames em branco forem permitidos, defina um sinalizador.

 class User: NSObject { let userName: String = "" let isSuperUser: Bool = false let someDetails: [String]? init?(dictionary: [String: AnyObject]) { if let user_name = dictionary["user_name"] as? String { userName = user_name } if let value: Bool = dictionary["super_user"] as? Bool { isSuperUser = value } someDetails = dictionary["some_details"] as? Array super.init() if userName.isEmpty { return nil } } } 

Outra maneira de contornar a limitação é trabalhar com uma class de funções para fazer a boot. Você pode até querer mover essa function para uma extensão:

 class User: NSObject { let username: String let isSuperUser: Bool let someDetails: [String]? init(userName: String, isSuperUser: Bool, someDetails: [String]?) { self.userName = userName self.isSuperUser = isSuperUser self.someDetails = someDetails super.init() } } extension User { class func fromDictionary(dictionary: NSDictionary) -> User? { if let username: String = dictionary["user_name"] as? String { let isSuperUser = (dictionary["super_user"] as? Bool) ?? false let someDetails = dictionary["some_details"] as? [String] return User(username: username, isSuperUser: isSuperUser, someDetails: someDetails) } return nil } } 

Usá-lo seria:

 if let user = User.fromDictionary(someDict) { // Party hard } 

Embora o Swift 2.2 tenha sido lançado e você não precise mais inicializar totalmente o object antes de falhar no inicializador, você precisa manter seus cavalos até que https://bugs.swift.org/browse/SR-704 seja corrigido.

Eu descobri que isso pode ser feito no Swift 1.2

Existem algumas condições:

  • As propriedades necessárias devem ser declaradas como opcionais implicitamente não desembrulhados
  • Atribuir um valor às suas propriedades obrigatórias exatamente uma vez. Este valor pode ser nulo.
  • Em seguida, chame super.init () se sua class estiver herdando de outra class.
  • Depois que todas as propriedades necessárias tiverem sido atribuídas a um valor, verifique se o valor delas é o esperado. Se não, retorne zero.

Exemplo:

 class ClassName: NSObject { let property: String! init?(propertyValue: String?) { self.property = propertyValue super.init() if self.property == nil { return nil } } } 

Um inicializador disponível para um tipo de valor (isto é, uma estrutura ou enumeração) pode triggersr uma falha de boot em qualquer ponto dentro de sua implementação de inicializador

Para classs, no entanto, um inicializador fiável pode triggersr uma falha de boot somente depois que todas as propriedades armazenadas introduzidas por essa class tiverem sido definidas para um valor inicial e qualquer delegação inicializadora tenha ocorrido.

Trecho de: Apple Inc. “ The Swift Programming Language. “IBooks. https://itun.es/sg/jEUH0.l

Você pode usar o init de conveniência :

 class User: NSObject { let userName: String let isSuperUser: Bool = false let someDetails: [String]? init(userName: String, isSuperUser: Bool, someDetails: [String]?) { self.userName = userName self.isSuperUser = isSuperUser self.someDetails = someDetails } convenience init? (dict: NSDictionary) { guard let userName = dictionary["user_name"] as? String else { return nil } guard let isSuperUser = dictionary["super_user"] as? Bool else { return nil } guard let someDetails = dictionary["some_details"] as? [String] else { return nil } self.init(userName: userName, isSuperUser: isSuperUser, someDetails: someDetails) } }