Melhor prática para implementar um inicializador failable em Swift

100

Com o código a seguir, tento definir uma classe de modelo simples e seu inicializador failable, que leva um dicionário (json-) como parâmetro. O inicializador deve retornar nilse 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 classe devem ser inicializadas antes de retornar nil de um inicializador.

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

2. Minha abordagem é a correta ou haveria outras idéias ou padrões comuns para atingir meu objetivo?

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()
    }
}
Kai Huppmann
fonte
Eu tive um problema semelhante, com o meu concluí que cada valor do dicionário deveria ser esperado e então forço o desembrulhar os valores. Se a propriedade não estiver lá, poderei pegar o bug. Além disso, adicionei um canSetCalculablePropertiesparâmetro booleano que permite ao meu inicializador calcular propriedades que podem ou não ser criadas instantaneamente. Por exemplo, se uma dateCreatedchave estiver faltando e eu puder definir a propriedade imediatamente porque o canSetCalculablePropertiesparâmetro é verdadeiro, apenas defini-lo para a data atual.
Adam Carter

Respostas:

71

Atualização: Do Swift 2.2 Change Log (lançado em 21 de março de 2016):

Inicializadores de classe designados declarados como failable ou throwing podem agora retornar nil ou lançar um erro, respectivamente, antes que o objeto tenha sido totalmente inicializado.


Para Swift 2.1 e anterior:

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

Para classes, entretanto, um inicializador failable pode acionar uma falha de inicialização somente depois que todas as propriedades armazenadas introduzidas por essa classe foram definidas com um valor inicial e qualquer delegação de inicializador ocorreu.

Nota: Na verdade, funciona bem para estruturas e enumerações, mas não para classes.

A maneira sugerida de lidar com propriedades armazenadas que não podem ser inicializadas antes que o inicializador falhe é declará-las como opcionais implicitamente 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 classe Product é definida como tendo um tipo de string opcional desdobrado implicitamente (String!). Por ser de um tipo opcional, isso significa que a propriedade name tem um valor padrão de nil antes de receber um valor específico durante a inicialização. Esse valor padrão de nil, por sua vez, significa que todas as propriedades introduzidas pela classe Product têm um valor inicial válido. Como resultado, o inicializador failable para Product pode acionar uma falha de inicialização no início do inicializador se for passada uma string vazia, antes de atribuir um valor específico à propriedade name dentro do inicializador.

No seu caso, entretanto, simplesmente definir userNamecomo um String!não corrige o erro de compilação porque você ainda precisa se preocupar em inicializar as propriedades em sua classe base NSObject,. Felizmente, com userNamedefinido como a String!, você pode realmente chamar super.init()antes de return nilque irá iniciar sua NSObjectclasse base e corrigir o erro de compilação.

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
    }
}
Mike S
fonte
1
Muito obrigado não apenas certo, mas também bem explicado
Kai Huppmann
9
em swift1.2, o exemplo dos documentos apresenta um erro "Todas as propriedades armazenadas de uma instância de classe devem ser inicializadas antes de retornar nil de um inicializador"
jeffrey
2
@jeffrey Correto, o exemplo da documentação ( Productclasse) não pode acionar uma falha de inicialização antes de atribuir um valor específico, embora a documentação diga que pode. Os documentos não estão sincronizados com a versão Swift mais recente. É aconselhável torná-lo um varpor agora let. fonte: Chris Lattner .
Arjan
1
A documentação tem esse trecho de código um pouco diferente: primeiro você configura a propriedade e depois verifica se ela está presente. Consulte “Inicializadores disponíveis para classes”, “A linguagem de programação Swift”. `` `class Product {let name: String! init? (nome: String) {self.name = name if name.isEmpty {return nil}}} `` `
Misha Karpenko
Também li isso nos documentos da Apple, mas não consigo ver por que isso seria necessário. Uma falha significaria retornar nil de qualquer maneira. O que importa então se as propriedades foram inicializadas?
Alper
132

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

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

Esta é uma limitação da implementação no compilador swift 1.1, documentada nas notas de lançamento. O compilador é atualmente incapaz de destruir classes parcialmente inicializadas em todos os casos, portanto, ele não permite a formação de uma situação em que deveria. Consideramos isso um bug a ser corrigido em versões futuras, não um recurso.

Fonte

EDITAR:

Então o swift agora é de código aberto e de acordo com este changelog ele está corrigido agora em instantâneos do swift 2.2

Inicializadores de classe designados declarados como failable ou throwing podem agora retornar nil ou lançar um erro, respectivamente, antes que o objeto tenha sido totalmente inicializado.

mustafa
fonte
2
Obrigado por abordar meu ponto de que a ideia de inicializar propriedades que não serão mais necessárias não parece muito razoável. E +1 por compartilhar uma fonte, o que prova que Chris Lattner se sente como eu;).
Kai Huppmann,
22
FYI: "Certamente. Isso ainda é algo que gostaríamos de melhorar, mas não foi aprovado para o Swift 1.2". - Chris Lattner 10. fevereiro 2015
dreamlab
14
Para sua informação: No Swift 2.0 beta 2, isso ainda é um problema, e também é um problema com um inicializador que lança.
aranasaurus
7

Aceito que a resposta de Mike S seja uma recomendação da Apple, mas não acho que seja a melhor prática. O objetivo de um sistema de tipo forte é mover os erros de tempo de execução para o tempo de compilação. Essa "solução" anula 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 nomes de usuário 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
        }
    }
}
Daniel T.
fonte
Obrigado, mas não vejo como as idéias de sistemas de tipo forte são corrompidas pela resposta de Mike. Em suma, você apresenta a mesma solução com a diferença de que o valor inicial é definido como "" em vez de nulo. Além disso, seu código leva embora para usar "" como nome de usuário (o que pode parecer bastante academicamente, mas pelo menos é diferente de não estar definido no json / dicionário)
Kai Huppmann
2
Após revisão, vejo que você está certo, mas apenas porque userName é uma constante. Se fosse uma variável, a resposta aceita seria pior do que a minha porque userName poderia ser posteriormente definido como nulo.
Daniel T.
Eu gosto dessa resposta. @KaiHuppmann, se você quiser permitir nomes de usuário vazios, você também pode ter um simples Bool needsReturnNil. Se o valor não existir no dicionário, defina needsReturnNil como true e defina userName como qualquer. Depois de super.init (), verifique needReturnNil e retorne nil se necessário.
Richard Venable,
6

Outra maneira de contornar a limitação é trabalhar com funções de classe para fazer a inicialização. Você pode até querer mover essa função 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
}
Kevin R
fonte
1
Eu gosto disso; Prefiro que os construtores sejam transparentes sobre o que desejam, e passar um dicionário é muito opaco.
Ben Leggiero
3

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

sssilver
fonte
1

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 desembrulhados
  • Atribua um valor às propriedades necessárias exatamente uma vez. Este valor pode ser nulo.
  • Em seguida, chame super.init () se sua classe estiver herdando de outra classe.
  • Depois de atribuir um valor a todas as propriedades necessárias, verifique se o valor está conforme o esperado. Se não, retorne nil.

Exemplo:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
Pim
fonte
0

Um inicializador failable para um tipo de valor (ou seja, uma estrutura ou enumeração) pode acionar uma falha de inicialização em qualquer ponto dentro de sua implementação de inicializador

Para classes, entretanto, um inicializador failable pode acionar uma falha de inicialização somente depois que todas as propriedades armazenadas introduzidas por essa classe foram definidas com um valor inicial e qualquer delegação de inicializador ocorreu.

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

user1046037
fonte
0

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)
    } 
}
Максим Петров
fonte