У Crystal змінна може складатися з кількох типів. Це називається обʼєднаним типом. У Crystal компілятор часто сам виводить обʼєднаний тип.
Обʼєднаний тип залишається одним типом під час виконання, навіть якщо складається з кількох типів.
Це означає, що якщо обʼєднаний тип побудовано з String і Int32, він не буде обома одночасно.
Натомість він буде або String, або Int32.
Обʼєднаний тип оголошують, розділяючи типи вертикальною рискою (|).
Їх часто беруть у дужки, але це не обовʼязково.
Найпоширеніший обʼєднаний тип - (T | Nil), де T - це тип; він трапляється в методах, які можуть повертати Nil.
Про таку змінну також кажуть, що вона nilable.
foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)
foo : (String | Nil) = "Hello"
foo = nil
Він не обмежується лише двома типами, їх може бути скільки завгодно.
foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0
typeof проти Object#class
Є два способи дізнатися тип змінної.
Або скористатися typeof, або скористатися Object#class.
Різниця в тому, що typeof повертає тип змінної під час компіляції, а Object#class повертає тип під час виконання.
Це означає, що коли ми хочемо перевірити, чи є змінна обʼєднаним типом, Object#class цього не покаже, адже він повертає тип лише під час виконання, а той завжди один.
foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String
Оскільки під час виконання обʼєднаний тип поводиться як один тип, усі стандартні операції з ним працюють. Але під час компіляції коду компілятору потрібно знати, який це саме тип. Тож код потрібно побудувати так, щоб він міг бути лише одним із типів, коли ми хочемо застосувати операції, властиві конкретному типу.
foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)
У Crystal є особливий метод для обʼєднаних типів: метод is_a?, який приймає тип як аргумент і повертає булеве значення (англ. Boolean).
Метод nil? - це скорочення для is_a?(Nil).
Якщо поставити метод is_a? в керуючий вираз, він підкаже компілятору, який це тип, і тим самим гарантує, що це саме той тип.
А для гілки else буде гарантовано, що це не той тип.
foo : (String | Int32) = "Hello"
if foo.is_a?(String)
typeof(foo) # => String
foo.downcase # => "hello"
end
Цей is_a? не обмежується одним типом як аргументом, він може приймати й обʼєднаний тип.
Його також можна поєднати з && (логічне і) для кількох типів.
Коли метод is_a? використовують разом із керуючим виразом, він не може бути викликаний на змінній екземпляра чи змінній класу.
Натомість їх потрібно спершу присвоїти локальній змінній.
Один зі способів звести обʼєднаний тип до одного типу - зробити так, щоб у гілку можна було потрапити лише тоді, коли тип є конкретним типом.
Інший підхід - скористатися методом as.
Він зводить обʼєднаний тип до одного типу, виконуючи перевірку під час виконання.
Якщо тип не той, який очікується, буде викинуто виняток.
foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"
foo.as(Int32) # Error: can't cast String to Int32
Цей підхід призначений лише для випадків, коли ми впевнені, що тип є очікуваним, або коли хочемо викинути виняток, якщо це не так.
Використання цього підходу з неправильним налаштуванням може призвести до неочікуваної поведінки.
as? працює дуже схоже на as, але повертає nil замість того, щоб викинути виняток, якщо тип не є очікуваним.
Це означає, що він поверне обʼєднаний тип з очікуваного типу та Nil.
foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"
foo.as?(Int32) # => nil
Nilable означає, що змінна може бути або якимось типом, або Nil.
Це можна записати як (T | Nil).
Але оскільки nilable-типи досить поширені, для них є скорочений запис: T?.
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
Компілятор виведе обʼєднаний тип, якщо не впевнений, який це тип.
Наприклад, у наведеному коді компілятор не знатиме, якого типу foo, оскільки це може бути і String, і Int32.
if true
foo = 1
else
foo = "Hello"
end
typeof(foo) # => (Int32 | String)
Це виведення відбувається автоматично, і є й інші випадки, коли, наприклад, компілятор виводить, що метод повертає обʼєднаний тип.
character = "Hello world"[10]?
typeof(character) # => (Char | Nil)