Desenvolvimento

9 mar, 2016

Em Swift, nil é diferente de zero

Publicidade

Objective-C possui uma característica muito interessante e polêmica, enviar mensagens para nil é válido e não lança exceção como em outras linguagens. Não vou discutir se isso é bom ou ruim, mas é algo que gosto e uso frequentemente quando programo nessa linguagem.

Um uso comum seria quando é necessário testar se um array é vazio:

if(array.count == 0) {
    NSLog(@"empty array");
}

Isso funciona porque o retorno de qualquer mensagem enviada para nil é 0, nil ou NULL, dependendo do tipo de retorno da mensagem, semanticamente diferentes mas tecnicamente iguais a ZERO.

Então se array for nil, array.count retorna 0, mais uma vitória do bem e menos código escrito ?.

Sem pensar muito, podemos escrever o equivalente em Swift:

if array.count == 0 {
    print("empty array")
}

Se array for um opcional, novamente sem pensar muito, poderíamos fazer um optional chaining, e só colocar um ?:

if array?.count == 0 {
    print("empty array")
}

NÃO!! Quando array = nil, array?.count == 0 é falso, mas por quê?

A resposta está no funcionamento do optional chaining: ?, que tem um comportamento semelhante ao ! (force unwrapping), com a diferença de que se o valor opcional for nil, não é lançado um erro de runtime. Como não ocorre a interrupção do programa, a fim de evitar inconsistências, o resultado de chamadas de métodos, propriedades e subscripts sempre vai retornar um opcional, mesmo que o tipo original não seja. Por exemplo:

var array: [Int]?
let count = array?.count // count is Int? not Int

Isso significa que quando array = nil, count = nil que não é igual a 0 e, por isso, o teste anterior falha! Ou seja em Swift, nil != 0!

Para escrever o teste de maneira que funciona, temos várias opções.

Definir que quando o resultado do count for nil o resultado esperado é 0:

if (array?.count ?? 0) == 0 {
    print("empty array")
}

Testar o nil e fazer force unwrapping. Não me agrada o force unwrapping, sei que nesse caso nunca aconteceria um erro de runtime, mas prefiro evitar ao máximo o !:

if array == nil || array!.count == 0 {
    print("empty array")
}

Por algum motivo ainda desconhecido para mim, nil é menor que qualquer Int. Então, temos uma opção que eu não recomendo, ¯\_(ツ)_/¯:

if !(array?.count > 0) {
    print("empty array")
}

Com certeza deve haver mais uma dezena de maneiras de escrever, mas acho que já deu para ter uma ideia. Provavelmente o erro desse caso é transpor exatamente a mesma lógica do Objective-C para Swift.

Update: O Fabri e o Koga lembraram que o Array adota o protocolo CollectionType e, portanto, o isEmpty seria mais adequado:

if array?.isEmpty ?? true {
    print("empty array")
}

O Fabri pensou mais no assunto e propôs uma extensão para o Optional de IntegerType que evitaria o problema apresentado:

extension Optional where Wrapped: IntegerType {
    var valueOrZero: Wrapped {
        return self ?? 0
    }
}

Assim o teste ficaria:

if (array?.count).valueOrZero == 0 {
    print("empty")
}

Um bom exercício para evitar o erro, mas acho que a versão com o isEmpty ainda fica mais legível.

Criticas, sugestões e comentários são sempre bem-vindos, é só me pingar no @diogot ou no slack do iOS Dev BR.