Ti

Tipo união em Crystal

1 exercício

Sobre Tipo união

O Crystal permite que uma variável seja composta por vários tipos. A isto chama-se tipo de união. No Crystal, é comum o compilador inferir um tipo de união.

Note

Um tipo de união continua a ser um único tipo em tempo de execução, mesmo que seja composto por vários tipos. Isto significa que, se um tipo de união for composto por String e Int32, não será ambos ao mesmo tempo. Em vez disso, será ou uma String ou um Int32.

Declara-se um tipo de união separando os tipos com uma barra vertical (|). Aparecem frequentemente entre parênteses, mas isso não é obrigatório. O tipo de união mais comum é (T | Nil), em que T é um tipo, que pode ocorrer em métodos que podem devolver Nil. Também se diz, neste caso, que a variável é nilable.

foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)

foo : (String | Nil) = "Hello"
foo = nil

Não está limitado a apenas dois tipos, pode ter tantos quantos quiseres.

foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0

typeof vs Object#class

Há duas formas de obter o tipo de uma variável. Ou com o typeof, ou com o Object#class. A diferença é que o typeof devolve o tipo de uma variável em tempo de compilação, enquanto o Object#class devolve o tipo em tempo de execução. Isto significa que, se quiseres ver, por exemplo, se uma variável é um tipo de união, o Object#class não te conseguirá dizer isso, porque só devolve o tipo em tempo de execução, que é um único tipo.

foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String

Operações sobre tipos de união

Como um tipo de união é um único tipo em tempo de execução, todas as operações normais funcionam sobre ele. Mas, ao compilar o código, o compilador precisa de saber de que tipo se trata. Por isso, o código tem de estar organizado de forma a que só possa ser um dos tipos quando se querem usar operações específicas de cada tipo.

foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)

O Crystal tem um método específico para os tipos de união: o método is_a?, que recebe um tipo como argumento e devolve um boolean. O método nil? é um atalho para is_a?(Nil). Colocar o método is_a? numa expressão de controlo indica ao compilador de que tipo se trata e, assim, garante que é esse tipo. E, no caso de um ramo else, garante que não é esse tipo.

foo : (String | Int32) = "Hello"
if foo.is_a?(String)
  typeof(foo)  # => String
  foo.downcase # => "hello"
end

Este is_a? não está limitado a ter um único tipo como argumento, também pode receber um tipo de união. Também pode ser combinado com && para vários tipos.

Note

O método is_a?, quando usado em conjunto com uma expressão de controlo, não pode ser uma variável de instância nem uma variável de classe. Em vez disso, estas têm de ser atribuídas primeiro a uma variável local.

as

Uma forma de transformar um tipo de união num único tipo é fazer com que um ramo só possa ser executado se o tipo for um tipo específico. Outra abordagem é usar o método as. Este transforma um tipo de união num único tipo fazendo uma verificação em tempo de execução. É lançada uma exceção se o tipo não for o esperado.

foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"

foo.as(Int32) # Error: can't cast String to Int32
Caution

Esta abordagem destina-se apenas a quando tens a certeza de que o tipo é o esperado, ou se quiseres lançar uma exceção quando não é.

Usar esta abordagem com uma configuração inadequada pode levar a comportamentos inesperados.

as?

as? funciona de forma muito semelhante ao as, mas devolve nil em vez de lançar uma exceção se o tipo não for o esperado. Isto significa que devolve um tipo de união do tipo esperado e Nil.

foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"

foo.as?(Int32) # => nil

Forma abreviada de nilable

Nilable significa que uma variável pode ser um tipo ou Nil. Pode escrever-se como (T | Nil). Mas, como os tipos nilable são relativamente comuns, há uma forma abreviada: T?.

# This:
foo : (String | Nil) = "Hello"
foo = nil

# Is the same as:
foo : String? = "Hello"
foo = nil

Inferência de tipos de união

O compilador infere um tipo de união quando não tem a certeza de que tipo se trata. Por exemplo, no código seguinte o compilador não sabe de que tipo é foo, uma vez que pode ser uma String ou um Int32.

if true
  foo = 1
else
  foo = "Hello"
end

typeof(foo) # => (Int32 | String)

Esta inferência acontece automaticamente e há outros cenários em que, por exemplo, o compilador infere que um método devolve um tipo de união.

character = "Hello world"[10]?
typeof(character) # => (Char | Nil)
Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Tipo união

A prática está bloqueada

Desbloqueia mais 1 exercício para praticares Tipo união