Invariante   Algumas coisas nunca mudam

Em Swift, nil é diferente de Zero

Objective-C possui uma característica muito interessante e polêmica, enviar mensagens para nil é válido e não lança excessã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 o 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 porque?

A resposta está em no funcionamento do optional chaining: ? tem um comportamento semelhante ao ! (force unwrapping), com a diferença que se o valor opcional for nil não é lançado um erro de runtime. Como não ocorre a interrupção do programa e afim de evitar inconsistências o resultado de chamadas de métodos, propriedades e subscripts sempre vão 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 devem haver mais uma dezena de maneiras de escrever mas acho que já deu para ter uma idéia. Provavelmente o erro desse caso é transpor exatamente a mesma lógica do Objective-C para Swift.


Update 2016/02/16 - 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 assunto1 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.


Diogo Tridapalli
@diogot

  1. Assumindo que eu seja um cara teimoso e me recuse a usar o isEmpty