Crystal дозволяє, щоб змінна складалася з кількох типів. Це називають обʼєднаним типом. У Crystal обʼєднаний тип часто виводить компілятор.
Під час виконання обʼєднаний тип усе одно залишається одним типом, навіть якщо складається з кількох типів.
Це означає, що якщо обʼєднаний тип складено з String й Int32, він не буде обома водночас.
Натомість він буде або String, або Int32.
Обʼєднаний тип оголошують, розділяючи типи вертикальною рискою (|).
Їх часто беруть у дужки, але це не обовʼязково.
Найпоширеніший обʼєднаний тип: (T | Nil), де замість T стоїть якийсь тип. Він трапляється в методах, які можуть повернути Nil.
Також кажуть, що така змінна допускає Nil.
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
«Допускає Nil» означає, що змінна може бути або якогось типу, або Nil.
Це можна записати як (T | Nil).
Але оскільки типи, що допускають Nil, досить поширені, для них є скорочений запис: T?.
# This:
foo : (String | Nil) = "Hello"
foo = nil
# Is the same as:
foo : String? = "Hello"
foo = nil
Компанія, у якій ми працюємо, ось-ось випустить свій цілком новий смартфон під назвою Smarty 5. У ньому є цілком нова система камер, новий дизайн і революційно новий процесор, що працює на новій операційній системі smartyOS.
Новий процесор здатен працювати з безпечнішими паролями, ніж будь-коли раніше, і компанія вирішила скористатися цією перевагою. Нас попросили реалізувати для телефона систему блокування паролем, яка дозволить користувачеві встановити пароль, а потім перевіряти, чи правильний заданий пароль.
Система паролів, яку ми розглядаємо в цій вправі, не є безпечною і використовується лише з освітньою метою. Її НЕ слід застосовувати в жодних реальних застосунках.
Телефон дозволяє користувачам використовувати різні типи паролів: від простого цифрового пароля до безпечнішого буквено-цифрового і навіть пароля, який зберігає відбиток пальця.
Ці різні типи паролів використовують різні типи даних: цифровий пароль використовує Int32, буквено-цифровий - String, а пароль із відбитком пальця - Float64.
Реалізуйте клас PasswordLock з ініціалізатором, який приймає будь-який із трьох типів паролів як аргумент і зберігає його у змінній екземпляра @password.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
Компанія вирішила шифрувати пароль, щоб він не зберігався у відкритому вигляді, і попросила нас реалізувати метод для його шифрування.
Кожен тип пароля має власний спосіб шифрування:
Int32: пароль ділиться на два й округлюється до найближчого цілого.String: пароль записується у зворотному порядку.Float64: пароль множиться на чотири.Реалізуйте метод екземпляра encrypt, який не приймає аргументів і змінює змінну екземпляра @password так, щоб вона була зашифрована.
password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>
password_lock.encrypt
# => #<PasswordLock:0x7f8e1b8c0b80 @password=617>
Компанія також попросила нас реалізувати метод, який перевіряє, чи правильний заданий пароль.
Вони хочуть, щоб метод повертав nil, якщо пароль неправильний, і "Unlocked", якщо пароль правильний.
Реалізуйте метод екземпляра unlock?, який приймає як аргумент пароль для перевірки.
Метод має повертати nil, якщо пароль неправильний, і "Unlocked", якщо пароль правильний.
password_lock = PasswordLock.new(1234)
password_lock.encrypt
password_lock.unlock?(1234)
# => "Unlocked"
Зареєструйтеся на Exercism, щоб вивчати й опановувати Crystal, а також 26 концепцій142 вправи та справжнє наставництво від людей, і все це безкоштовно.