Desenvolvimento

19 ago, 2015

O curioso caso do nil em Swift

Publicidade

Vou confessar: apesar de já ter palestrado sobre o assunto (infelizmente tiveram problemas com o áudio e não publicaram o vídeo), demorei a me aprofundar novamente nos Opcionais em Swift. Mas recentemente, em uma pequena pausa entre duas fases de um projeto, resolvi estudar Swift e seu lado funcional e esbarrei em coisas bacanas. Então, decidi explorar aqui um pouco de suas características.

Historicamente, a representação de ausência de valor é resolvida de formas diferentes em diferentes linguagens, mas nem sempre de forma satisfatória. Como representar que um tipo booleano não tem valor? Em C, por exemplo, o inteiro 0 é associado ao valor falso, e qualquer coisa diferente de 0 é considerada verdadeira. Então, uma variável booleana sempre tem valor, o que pode gerar erros inesperados em tempo de execução. Como saber se uma variável inteira que contêm o valor 0 teve seu valor atribuído ou se este é um valor padrão? Um valor padrão pode não ser sempre o desejado, e saber se o valor já foi atribuído pode ser bastante útil. Além do problema com o valor atribuído a uma variável, temos também o problema de ponteiros nulo em diversas linguagens.

Tony Hoare, um dos criadores da linguagem ALGOL W (linguagem de programação de 1966, variação do ALGOL, que deu origem a C, C++, Objective-C, LISP…), foi quem primeiro introduziu o conceito de ponteiro nulo em uma linguagem de programação ao trabalhar no sistema de tipos da linguagem. Anos depois, declarou em uma conferência que aquele era seu “Billion-Dollar Mistake”. Em linguagens imperativas, tipos referenciais (como os ponteiros para objetos em Objective-C) podem potencialmente ser nulos, o que pode gerar interrupções anormais. Em Objective-C, por exemplo, é possível enviar uma mensagem para uma variável que representa um objeto, mas que contém o valor nil (neste caso, um valor especial que representa que o objeto não foi alocado/inicializado), porém adicionar um ponteiro com o valor nil em um array, por exemplo, causa um crash no aplicativo.

Swift é uma linguagem de tipagem estática, isto é, variáveis sempre têm tipo definido via código ou, no caso do Swift, também por inferência em alguns casos, e o compilador verifica seu uso detectando erros em tempo de compilação que poderiam ser detectados só em tempo de execução. Ela também não contém ponteiros, mas contém a palavra reservada nil. O que ela faz? Pra que ela serve?

nil não contém um tipo e nem um valor. Assim, se o verificador de tipos do compilador não consegue definir o tipo de uma variável quando você tenta atribuir ela para nil , é gerado um erro de compilação. Ou seja:

<span class="crayon-e">let </span><span class="crayon-v">variable</span> <span class="crayon-o">=</span> <span class="crayon-v">nil</span>
<span class="crayon-e">let </span><span class="crayon-v">variable</span><span class="crayon-o">:</span> <span class="crayon-t">Int</span><span class="crayon-sy">?</span> <span class="crayon-o">=</span> <span class="crayon-v">nil</span>

compila sem problemas. Qual a diferença? Durante a compilação, quando o verificador de tipos consegue inferir corretamente o tipo da variável, ele substitui o “valor” de nil. E na verdade, o tipo é substituído magicamente pelo tipo Optional, que nada mais é que uma enumeração, possivelmente com valor atribuído. De forma bem simples, ficaria assim:

enum Optional<T> {
	case None
	case Some(T)
}

Assim, a variável desse tipo pode não conter um valor ou conter algum valor do tipo T, que possivelmente foi inferido durante a compilação, como no caso let variable: Int? = nil, no qual T é inferido como Int.

Vamos exemplificar um pouco mais criando algumas variáveis. O comentário do código diz qual é o real valor da variável criada:

let a:Int? = nil // a = Optional<Int>.None
let b:String? = nil // b = Optional<String>.None
let c:Bool? = nil // c = Optional<Bool>.None

Para essa mágica acontecer, tudo o que é necessário pelo compilador é o tipo obedecer ao protocolo NilLiteralConvertible exibido abaixo:

protocol NilLiteralConvertible {
    init(nilLiteral: ())
}

Assim, um tipo que obedece ao protocolo NilLiteralConvertible deve implementar um método init, que recebe um único parâmetro, uma tupla vazia que não fornece nenhum valor relevante, e produzir um valor padrão para o tipo. Por exemplo, o tipo Optional poderia obedecer ao protocolo NilLiteralConvertible, inicializando o Optional com o valor None com a implementação abaixo:

enum Optional<T> : NilLiteralConvertible {
    init(nilLiteral: ()) {
        self = None
    }
}

Como os tipos comuns (Int, String, etc) não obedecem a esse protocolo, é impossível inicializar direto com nil. Por exemplo:

let b: Bool = nil

gera o erro “*Error* Type ‘Int’ does not conform to protocol ‘NilLiteralConvertible’”.

Então, se Optional é um enum, podemos inicializar direto como um? Sim!

let a:Int? = .None // let a:Optional<Int> = .None

E a comparação com nil?

Se um tipo opcional declarado com o sinal de interrogação é automaticamente convertido para um tipo Optional, o que acontece no caso abaixo?

let d:Int? = 1
d != nil

O resultado é claramente true, mas o que acontece por baixo dos panos? Como vimos, o tipo de d é na verdade Optional<Int>. O código poderia então ser reescrito como:

let d:Optional<Int> = 1
d != nil //prefira fazer o desenmpacotamento com a sintaxe "if let..."

Poderíamos também usar o método init do tipo Optional, poupando a necessidade de declarar o tipo:

let d = Optional(1)
d != nil

Então, implicitamente, o que está sendo comparado na verdade é o valor .None da enumeração.

d != Optional<Int>.None //falso

Agora, uma surpresa:

nil < 0 //true

Se nil não tem valor, como pode ser menor do que zero? Na verdade, não é. Lembrando que nil na verdade é um Optional sem valor, então o que temos inferido pelo verificador de tipos do compilador é:

Optional<Int>.None < 0 //também é verdadeiro

Então estamos comparando um opcional com um valor inteiro. Mas se olharmos a definição da comparação no arquivo Swift (para visualizá-lo, adicione no seu código Swift a linha importSwift e, segurando o botão Cmd, clique no nome Swift), na linha 907 (isso com o Xcode 6.3.1 e o Swift 1.2), teremos:

func <<T : _Comparable>(lhs: T?, rhs: T?) -> Bool

Ou seja, a comparação é entre dois tipos idênticos. Como comparar um Optional com um inteiro?

Como vimos anteriormente, podemos declarar um tipo opcional e atribuir um valor:

let v: Int? = 0 //equivalente a Optional<Int>.Some(0)

Então, o que temos de verdade na comparação é o verificador de tipos inferindo:

Optional<Int>.None < Optional<Int>.Some(0) //agora claramente verdade

Isso é válido para todos os tipos que obedecem ao protocolo Comparable, por exemplo:

Optional<String>.None < Optional<String>.Some("a") //true
"a" > nil //true
//A comparação inversa causa um erro, pois String não pode ser convertida automaticamente para Optional
nil < "a" //'(NillLiteralConvertible, UnicodeScalarConvertible)' is not convertible to 'NilLiteralConvertible'
nil < .Some("a") //true

E com Objective-C?

Uma das vantagens do Swift é sua interoperabilidade com Objective-C, uma linguagem imperativa, com ponteiros nulos e os problemas inerentes ao Billion-Dollar Mistake (apesar de aceitar envio de mensagens para variáveis com valor nil). Apesar da interoperabilidade, existe uma distinção dos tipos: por exemplo, enquanto um objeto pode ser do tipo UIView * em Objective-C, como Swift não possui ponteiros nem valores nil, ele deve ser do tipo UIView ou UIView? (como vimos, são tipos distintos). E como Swift não sabe qual tipo deve usar, então é convertido de Objective-C em Swift como um UIView!, ou seja, um tipo opcional implicitamente desempacotado.

Os frameworks da Apple permitem isso por padrão, mas como fazer para suas classes? A partir do Xcode 6.3, com as chamadas anotações de nulabilidade.

Existem dois tipos de anotações: __nullable __nonnull. Como esperado, a primeira permite ter um valor nil, enquanto a segunda não. O compilador avisa caso a regra seja quebrada. Você pode usá-los em qualquer lugar que aceite const, mas a Apple foi um pouco mais generosa e criou versões mais legíveis, mas que podem ser usadas apenas após a abertura de parênteses na declaração de métodos (no retorno ou nos parâmetros) ou na declaração de properties, também dentro da lista de atributos:

- (nullable CSCollection *)insertItem:(nonnull CSItem *)item;
 
@property (nonatomic, strong, nullable) NSString *name;

Apesar de mais legível que as versões __nullable e __nonnull, devemos adicionar essas palavras chave para cada método/parâmetro/property da nossa classe escrita em Objective-C, que será acessada a partir de um código Swift. Para facilitar o processo de auditoria dos headers e verificação da compatibilidade do seu código Objective-C com Swift por meio do uso dessas anotações, a Apple adicionou macros de Objective-C para marcar regiões do header como auditadas.

NS_ASSUME_NONNULL_BEGIN
@interface CSCollection : NSObject
- (nullable CSItem *)itemWithKey:(NSString *)key;
 
@property (readonly) NSArray *allItems;
NS_ASSUME_NONNULL_END

Nesse caso, por exemplo, itemWithKey: pode retornar nil, mas não pode ser passado nil como parâmetro para o método (o compilador gera um warning).

Além disso, para compatibilidade, se distribuirmos esse código na forma de um framework, apps de terceiros já compilados com versões antigas rodam sem problema. Mas ao compilar um código antigo com a nova versão do nosso framework, por segurança, o Xcode irá gerar warnings para o usuário da nova versão.

A recomendação da Apple para a forma de tratar nullable a nonnull é a seguinte: “In general, you should look at nullable and nonnull roughly the way you currently use assertions or exceptions: violating the contract is a programmer error” (tradução livre: “Em geral, você deveria olhar para nullable e nunnull de grosso modo da forma como atualmente usa asserções e exceções: violar o contrato é um erro do programador”.

Ao gerar nosso framework e acessá-lo em Swift, veríamos assim:

class CSCollection : NSObject {
	func itemWithKey(key: String) -> CSItem?
	
	var allItems: [AnyObject]! { get }
}

ao invés de vermos assim:

class CSCollection : NSObject {
	func itemWithKey(key: String!) -> CSItem!
	
	var allItems: [AnyObject] { get }
}

A mudança aqui não é significativa, mas deixa o código Swift um pouco mais limpo e mais “Swift-like”.

Em breve, escreverei um pouco mais sobre Optionals, sua relação com linguagens funcionais e o poder que esse tipo de dados nos permite.

Ficou alguma dúvida ou tem alguma coisa a adicionar? Aproveite os campos abaixo e deixe seu comentário!