In Crystal kann eine Variable aus mehreren Typen bestehen. Das nennt man einen Union-Typ. In Crystal ist es üblich, dass der Compiler einen Union-Typ ableitet.
Ein Union-Typ ist zur Laufzeit trotzdem ein einzelner Typ, auch wenn er aus mehreren Typen besteht.
Das bedeutet: Wenn ein Union-Typ aus String und Int32 besteht, ist er nicht gleichzeitig beides.
Stattdessen ist er entweder ein String oder ein Int32.
Ein Union-Typ wird deklariert, indem man die Typen durch einen senkrechten Strich (|) trennt.
Sie werden oft in Klammern gesetzt, das ist aber nicht erforderlich.
Der häufigste Union-Typ ist (T | Nil), wobei T ein Typ ist; er kommt in Methoden vor, die Nil zurückgeben können.
Man sagt dann auch, dass eine Variable nilable ist.
foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)
foo : (String | Nil) = "Hello"
foo = nil
Er ist nicht auf zwei Typen beschränkt, sondern kann so viele enthalten, wie du willst.
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof vs Object#class
Es gibt zwei Möglichkeiten, den Typ einer Variablen herauszufinden.
Entweder mit typeof oder mit Object#class.
Der Unterschied ist, dass typeof den Typ einer Variablen zur Compile-Zeit liefert, während Object#class den Typ zur Laufzeit liefert.
Wenn du also zum Beispiel herausfinden willst, ob eine Variable einen Union-Typ hat, kann dir Object#class das nicht sagen, denn es liefert nur den Typ zur Laufzeit, und der ist ein einzelner Typ.
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
Da ein Union-Typ zur Laufzeit ein einzelner Typ ist, funktionieren alle Standardoperationen damit. Beim Kompilieren des Codes muss der Compiler aber wissen, welcher Typ es ist. Deshalb muss der Code so aufgebaut sein, dass er nur einer der Typen sein kann, wenn du typspezifische Operationen verwenden willst.
foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)
Crystal hat eine besondere Methode für Union-Typen: die Methode is_a?, die einen Typ als Argument nimmt und einen booleschen Wert zurückgibt.
Die Methode nil? ist eine Kurzform für is_a?(Nil).
Wenn du die Methode is_a? in einem Kontrollausdruck verwendest, teilst du dem Compiler mit, welcher Typ es ist, und garantierst damit, dass es dieser Typ ist.
Und für einen else-Zweig ist garantiert, dass es nicht dieser Typ ist.
foo : (String | Int32) = "Hello"
if foo.is_a?(String)
typeof(foo) # => String
foo.downcase # => "hello"
end
Dieses is_a? ist nicht auf einen einzelnen Typ als Argument beschränkt, sondern kann auch einen Union-Typ annehmen.
Es kann außerdem mit && für mehrere Typen kombiniert werden.
Die Methode is_a? kann, wenn du sie zusammen mit einem Kontrollausdruck verwendest, keine Instanzvariable und keine Klassenvariable sein.
Diese müssen zuerst einer lokalen Variable zugewiesen werden.
Eine Möglichkeit, einen Union-Typ in einen einzelnen Typ umzuwandeln, besteht darin, einen Zweig nur dann betreten zu können, wenn der Typ ein bestimmter Typ ist.
Ein anderer Ansatz ist die Methode as.
Sie wandelt einen Union-Typ in einen einzelnen Typ um, indem sie eine Prüfung zur Laufzeit durchführt.
Wenn der Typ nicht der erwartete Typ ist, wird eine Exception ausgelöst.
foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"
foo.as(Int32) # Error: can't cast String to Int32
Dieser Ansatz ist nur für den Fall gedacht, dass du sicher bist, dass der Typ der erwartete Typ ist, oder dass du eine Exception auslösen willst, wenn er es nicht ist.
Verwendest du diesen Ansatz bei einem ungeeigneten Aufbau, kann das zu unerwartetem Verhalten führen.
as? funktioniert sehr ähnlich wie as, gibt aber nil zurück, statt eine Exception auszulösen, wenn der Typ nicht der erwartete Typ ist.
Das bedeutet, sie gibt einen Union-Typ aus dem erwarteten Typ und Nil zurück.
foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"
foo.as?(Int32) # => nil
Nilable bedeutet, dass eine Variable entweder ein Typ oder Nil sein kann.
Das kann man als (T | Nil) schreiben.
Da nilable Typen aber relativ häufig vorkommen, gibt es dafür eine Kurzschreibweise: T?.
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
Der Compiler leitet einen Union-Typ ab, wenn er sich nicht sicher ist, welcher Typ es ist.
Im folgenden Code weiß der Compiler zum Beispiel nicht, welchen Typ foo hat, denn es kann entweder ein String oder ein Int32 sein.
if true
foo = 1
else
foo = "Hello"
end
typeof(foo) # => (Int32 | String)
Diese Ableitung geschieht automatisch, und es gibt weitere Szenarien, in denen der Compiler zum Beispiel ableitet, dass eine Methode einen Union-Typ zurückgibt.
character = "Hello world"[10]?
typeof(character) # => (Char | Nil)