Generics e Variance em Kotlin (in, out, T)

Neste artigo, nosso dev Android Thiago Pereira aborda o conceito de Generics e Variance em Kotlin e faz uma comparação com o Java.

Primeiro: o que é Generics?

Em java, o conceito de Generics foi introduzido em 2004. Ele foi desenhado para estender o sistema de Tipos do Java e permitir “um tipo ou método de operar em objetos de vários tipos provendo type safety em tempo de compilação”.

Mas o que isso significa? Vamos supor o seguinte trecho de código:

Em tempo de compilação, nenhum erro vai ser identificado no trecho acima, mas ao tentar executar, isso acontece na linha 9:

Exception in thread “main” java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

Isso acontece porque sem o uso de Generics, o compilador não consegue identificar em tempo de compilação que o que está sendo atribuído a uma variável do tipo Integer não é um inteiro.

Com o uso de Generics, isso seria identificado:

Perceba o erro em tempo de compilação na linha 9 novamente.

Segundo: o que é Variance?

Existem duas definições que eu particularmente gosto:

“Variance refere-se a como a subtipagem entre tipos mais complexos está relacionada à subtipagem entre seus componentes.”
“O conceito de Variance descreve como tipos como o mesmo tipo base e diferentes argumentos se relacionam entre si.”

Há três termos que precisamos ter em mente: Invariance, Covariance e Contravariance.

Vamos à prática pra entender melhor.

Variance na prática (um comparativo entre Java e Kotlin)

Antes de tudo, vamos abstrair um pouco o conceito:

PECS: “Producer Extends, Consumer Super” (vamos entender melhor mais a frente).

Covariance

Covariance implica que uma relação de subtipagem de tipos simples é preservada para os tipos complexos. Isso nos permite garantir o seguinte:

Passaro e Arara são subclasses de Ave, então nós podemos atribuir uma Arara ou um Passaro para uma Ave. Isso é covariance.

Contravariance

Contravariance é o exato oposto de Covariance. Contravariance implica que uma relação de subtipagem de tipos simples é invertida. Vamos entender melhor usando o exemplo anterior:

A atribuição acima apenas é permitida usando contravariance, onde a subtipagem é invertida. Nesse caso, agora Ave é um subtipo de Arara e Passaro.

Invariance

Por último, o mais simples, mas não menos importante. Invariance ignora subtipo e supertipo, o que significa que dado um tipo, apenas aquele tipo poderá ser consumido ou produzido. Vamos a um exemplo:

Dado um tipo T, tanto o input quanto o output apenas poderá ser T. Usando esse conceito, nós podemos ter o método acima both(t: T): T, que é tanto um consumer quanto um producer.

Declaration-site e Use-site Variance

Tá, mas finalmente, como podemos fazer uso de generics variance?

use-site variance

Em java, variance apenas é permitido através de wildcards types (use-site variance). Os generics types precisam ter sua variance manipulada cada vez que um tipo especifico precisa ser usado. Vamos a um exemplo pra entender melhor:

Perceba que com o uso do wildcard “? extends Number” podemos tornar List que é invariant em covariant. Isso nos permite fazer as atribuições acima de forma segura em tempo de compilação.

Dessa forma, podemos ler Number dessas listas de forma segura, porém não podemos escrever nada porque não conseguimos saber qual tipo esta lista está apontando. Então, você não consegue garantir se o que está tentando adicionar é permitido nessa lista.

Vamos ver o que acontece se definirmos a nossa List sem uso de variance:

Podemos ver que as linhas 10, 11, 15 e 16 nem sequer compilam mais. O motivo é que sem o uso de covariance, nossa List apenas aceita o próprio Number.

declaration-site variance

Em Kotlin, não existe “wildcards types”. A linguagem nos oferece uma anotação a nível de declaração para trabalhar com variance (in e out). Vamos novamente a um exemplo:

Podemos anotar o parâmetro de tipo T de Source para garantir que ele seja retornado (produzido) somente de membros de Source <T> e nunca consumido. Para fazer isso, usamos o modificador de saída out. A variance é definida a nível declaração da classe, por isso o nome declaration-site.

Um exemplo com a anotação de saída in:

Além de out, Kotlin fornece uma anotação de variance complementar: in. Ela faz um parâmetro de tipo contravariante: ele só pode ser consumido e nunca produzido.

Exemplo do equivalente de in em Java:

Perceba que com o uso do wildcard “? super Integer” nossa lista se torna contravariant, em outras palavras: sua subtipagem é invertida. O que nos permite atribuir qualquer supertipo de Integer a uma lista de Integer.

Mais uma vez, o que aconteceria se não usássemos variance? Vejamos:

As linhas 9 e 10 agora não compilam mais, já que Number e Object é um supertipo de Integer. e sem uso do wildcard, nossa lista é invariant e só aceita Integer.

Isso nos garante que podemos ler uma instance de Object ou uma subclasse de Object, mas não é possível saber qual subclasse. Você consegue inserir nessa lista Integer ou qualquer subclasse de Integer, já que uma instância de uma subclasse de Integer é permitida é qualquer das listas acimas. Você não consegue inserir Number, Object ou Double, já que a lista pode estar apontando para uma lista de Integer.

Como nem sempre é possível usar declaration-site até mesmo no Kotlin, a linguagem também permite o uso de use-site variance. Por exemplo:

Agora podemos entender melhor o conceito de PECS apresentado acima: “Producer Extends, Consumer Super”.

  • “Producer Extends” —se você precisa de uma lista que produza valores do tipo T (você quer ler T’s da lista), você precisa declará-la com <out T> (em java: <? extends T>). Mas não é possível adicionar nada à lista;
  • “Consumer Super” — se você precisa de uma lista que consuma valores do tipo T (você quer escrever T’s nessa lista), você precisa declará-la com <in T> (em java: <? super T>). Mas não há garantia de qual tipo de objeto você lerá dessa lista.
  • Se você precisa ler e escrever em uma lista, você precisa declará-la exatamente sem wildcards types no Java ou anotação de parâmetro no Kotlin, e.g. List<Integer>.

Real life cases

Vamos explorar agora alguns casos reais de uso de variance na própria stdlib do Kotlin. O conceito é bastante fundamental na API de Collections.

List (covariant)

Vamos dar uma olhada na interface de List:

Perceba que List usa declaration-site variance para se tornar covariant em E. Dessa forma apenas métodos de leitura são permitidos na lista. Lembre do conceito citado acima, Producer Extends.

Mas você deve estar se perguntando: como é permitido existir os métodos contains, containsAll e indexOf?

Originalmente, esse comportamento de consumer não deveria ser permitido por definição, mas dada a necessidade do comportamento, a solução é usar a anotação @UnsafeVariance. O comportamento dessa anotação é bem simples: ela simplesmente suprime qualquer erro de conflito de variance.

Se removermos a anotação, agora temos um erro em tempo de compilação:

Mas porque nos é permitido fazer isso, já que de certa forma estamos “quebrando o conceito de variance”? Basicamente, o conceito de variance tem objetivo de te proteger de usuários externos, não de você mesmo. Vejamos o seguinte trecho de código:

Perceba que field1 não compila, já que val possui um método get e GenericClass é contravariant em T.

Agora vamos analisar nosso código novamente:

Podemos perceber que field1 agora é compilado sem erro. Por quê? Como dito anteriormente, o conceito de variance é para te proteger de usuários externos, dado que agora seu field1 é private, é como se você estivesse dizendo ao compilador “eu sei o que estou fazendo aqui”.

MutableList (invariant)

Vamos mais uma vez dar uma olhada em uma interface presente em Collections:

Como podemos ver, MutableList é invariant em E. O motivo pra isso é que essa lista precisa ser tanto consumer quanto producer em E. Se MutableList fosse covariant em E, por exemplo, os métodos add, remove, addAll não seriam possíveis.

Conclusão

Nesse artigo, abordei o complexo conceito de variance em generics. Usei Java para demonstrar o conceito de generics e variance, assim como sua motivação. Logo depois, apresentei como o conceito é abordado em Kotlin e comparei com a abordagem do Java. Diferencei o que seria use-site e declaration-site variance e pudemos entender a implicação de variance através de exemplos e casos de uso reais.

O conceito é bem útil no dia a dia, porém bem desconhecido, aparentemente. Comentei com colegas que conheciam bem pouco sobre o assunto, o que me motivou a escrever este artigo e disseminar esse conhecimento.


Referências

Generic Types
Generics: in, out, where - Kotlin Programming Language
SOLID Class Design: The Liskov Substitution Principle


Se encontrou algum problema, deseja discutir sobre o assunto, opinar, contribuir etc… Pode me procurar em uma das opções abaixo:

Twitter: litjc13

Linkedin: Thiago Pereira