Треки
/
Crystal
Crystal
/
Салабус
/
Обʼєднаний тип
Об

Обʼєднаний тип у Crystal

1 вправа

Про концепцію Обʼєднаний тип

У Crystal змінна може складатися з кількох типів. Це називається обʼєднаним типом. У Crystal компілятор часто сам виводить обʼєднаний тип.

Note

Обʼєднаний тип залишається одним типом під час виконання, навіть якщо складається з кількох типів. Це означає, що якщо обʼєднаний тип побудовано з 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? не обмежується одним типом як аргументом, він може приймати й обʼєднаний тип. Його також можна поєднати з && (логічне і) для кількох типів.

Note

Коли метод is_a? використовують разом із керуючим виразом, він не може бути викликаний на змінній екземпляра чи змінній класу. Натомість їх потрібно спершу присвоїти локальній змінній.

as

Один зі способів звести обʼєднаний тип до одного типу - зробити так, щоб у гілку можна було потрапити лише тоді, коли тип є конкретним типом. Інший підхід - скористатися методом as. Він зводить обʼєднаний тип до одного типу, виконуючи перевірку під час виконання. Якщо тип не той, який очікується, буде викинуто виняток.

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

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

Цей підхід призначений лише для випадків, коли ми впевнені, що тип є очікуваним, або коли хочемо викинути виняток, якщо це не так.

Використання цього підходу з неправильним налаштуванням може призвести до неочікуваної поведінки.

as?

as? працює дуже схоже на as, але повертає nil замість того, щоб викинути виняток, якщо тип не є очікуваним. Це означає, що він поверне обʼєднаний тип з очікуваного типу та Nil.

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

foo.as?(Int32) # => nil

Скорочений запис для nilable

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)
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Обʼєднаний тип

Практика заблокована

Розблокуйте ще 1 вправу, щоб практикувати концепцію Обʼєднаний тип