LEARN X · ЗА 15 МИН
Crystal
Crystal за 15 минут: синтаксис как у Ruby, скорость как у C. Типы, union-типы и nil, классы, дженерики, исключения — весь язык в комментариях кода.
Crystal — это компилируемый язык со скоростью C и синтаксисом, почти неотличимым от Ruby. Статическая типизация с мощным выводом типов: типы редко пишут руками, но компилятор знает их все. Главная фишка — union-типы и обязательная проверка nil на этапе компиляции. Весь язык ниже — в комментариях к рабочему коду.
Вывод и комментарии
# Это однострочный комментарий — начинается с #
puts "Привет, Crystal!" # puts печатает строку и перенос строки
print "без переноса" # print — без \n в конце
print "\n"
p "строка" # p печатает inspect-представление: с кавычками -> "строка"
p 42 # для чисел p и puts выглядят одинаково -> 42
p [1, 2, 3] # p удобен для отладки -> [1, 2, 3]
pp 2 + 2 # pp печатает И выражение, И результат -> 2 + 2 # => 4
Переменные и типы
Crystal статически типизирован, но тип почти всегда выводится автоматически.
x = 42 # переменные не объявляют через var — просто присваивание
y = 3.14
name = "Аня"
# .class возвращает тип значения в рантайме
puts 42.class # => Int32 (целое по умолчанию 32 бита)
puts 3.14.class # => Float64 (дробное по умолчанию)
puts true.class # => Bool
puts "hi".class # => String
puts 'A'.class # => Char (одиночный символ в одинарных кавычках)
puts :ok.class # => Symbol (символ — лёгкая неизменяемая метка)
# Явные типы целых: Int8/Int16/Int32/Int64, беззнаковые UInt8..UInt64
big = 9_000_000_000_i64 # суффикс _i64 -> Int64
byte = 255_u8 # суффикс _u8 -> UInt8
puts big.class # => Int64
# Подчёркивания в числах — для читаемости
million = 1_000_000
# Константы — с Большой Буквы
PI = 3.14159
Строки
name = "Crystal"
# Интерполяция через #{...} — внутри любое выражение
puts "Язык: #{name}, длина имени: #{name.size}"
# Конкатенация и повтор
puts "abc" + "def" # => abcdef
puts "ха" * 3 # => хахаха
# Полезные методы строк
s = " Hello World "
puts s.strip # убрать пробелы по краям -> "Hello World"
puts s.upcase # ВЕРХНИЙ регистр
puts s.downcase # нижний регистр
puts "Hello".reverse # => olleH
puts "a,b,c".split(",") # => ["a", "b", "c"]
puts "Hello".includes?("ell") # => true
puts "Hello"[1] # индексация -> 'e' (Char)
puts "Hello"[1..3] # срез по диапазону -> "ell"
# Многострочный текст (heredoc)
text = <<-TEXT
Первая строка
Вторая строка
TEXT
puts text
Union-типы и nil
Фишка Crystal: nil — это отдельный тип, и компилятор заставляет его обрабатывать. Переменная может иметь union-тип вроде String | Nil.
# Тип со знаком вопроса: String? — это сокращение для String | Nil
name : String? = "Аня"
name = nil # допустимо: тип union включает Nil
# Union возникает сам, когда ветки дают разные типы
value = rand < 0.5 ? "строка" : 100
puts value.class # либо String, либо Int32 — тип: (String | Int32)
# nil нельзя использовать как обычное значение без проверки.
# Компилятор НЕ даст вызвать метод на возможном nil:
maybe : String? = "hi"
# Проверка через if сужает тип (type narrowing):
if maybe
puts maybe.upcase # здесь компилятор знает: maybe — это String
end
# .try выполняет блок только если значение не nil
puts maybe.try &.size # => 2 (или nil, если maybe был nil)
# .not_nil! утверждает «точно не nil» (упадёт в рантайме, если nil)
puts maybe.not_nil!.upcase
# Оператор || для значения по умолчанию
greeting = maybe || "гость"
puts greeting
Операторы и условия
a = 10
b = 3
# Арифметика
puts a + b # 13
puts a - b # 7
puts a * b # 30
puts a / b # 3.333... (деление даёт Float)
puts a // b # 3 (целочисленное деление)
puts a % b # 1 (остаток)
puts a ** b # 1000 (степень)
# Сравнения возвращают Bool
puts a > b # true
puts a < b # false
puts a == b # false
puts a != b # true
# Логические: && (и), || (или), ! (не)
puts (a > 5) && (b < 5) # true
puts (a > 100) || (b == 3) # true
# if / elsif / else
if a > b
puts "a больше"
elsif a == b
puts "равны"
else
puts "b больше"
end
# unless — это «если НЕ»
unless a == b
puts "a и b различны"
end
# Постфиксная форма — компактно
puts "чётное" if a % 2 == 0
# case — мощный switch, ветка вычисляется и возвращает значение
grade = case a
when 0..4 then "плохо"
when 5..7 then "норм"
when 8..10 then "отлично"
else "вне диапазона"
end
puts grade
Циклы
i = 0
while i < 3 # while — пока условие истинно
puts "while #{i}"
i += 1
end
j = 0
until j == 3 # until — пока условие ЛОЖНО (обратный while)
puts "until #{j}"
j += 1
end
# times — повторить N раз
3.times { |n| puts "шаг #{n}" } # n: 0,1,2
# Диапазоны: .. включает конец, ... исключает
(1..3).each { |n| puts n } # 1, 2, 3
(1...3).each { |n| puts n } # 1, 2
# each по коллекции
["a", "b", "c"].each { |x| puts x }
# break и next работают как обычно
(1..10).each do |n|
next if n.even? # пропустить чётные
break if n > 5 # выйти после 5
puts n # 1, 3, 5
end
Массивы и хеши
# Массив — Array(T), тип элементов выводится из литерала
nums = [1, 2, 3, 4, 5] # Array(Int32)
puts nums.class # => Array(Int32)
# Пустой массив требует явного типа
empty = [] of String # Array(String)
empty << "первый" # << добавляет в конец
puts nums[0] # 1 (индекс с нуля)
puts nums[-1] # 5 (отрицательный — с конца)
puts nums.size # 5
puts nums.first # 1
puts nums.last # 5
puts nums.sum # 15
# Функциональные методы возвращают новые коллекции
puts nums.map { |n| n * 2 } # => [2, 4, 6, 8, 10]
puts nums.select { |n| n.even? } # оставить чётные -> [2, 4]
puts nums.reject { |n| n.even? } # убрать чётные -> [1, 3, 5]
puts nums.reduce { |acc, n| acc + n } # свёртка -> 15
# Hash(K, V) — словарь ключ→значение
ages = { "Аня" => 25, "Петя" => 30 } # Hash(String, Int32)
puts ages["Аня"] # 25
ages["Лена"] = 22 # добавить пару
puts ages.keys # => ["Аня", "Петя", "Лена"]
puts ages.values # => [25, 30, 22]
puts ages.has_key?("Петя") # => true
# []? возвращает nil вместо ошибки, если ключа нет
puts ages["Кто-то"]? # => (пусто, nil)
ages.each do |name, age|
puts "#{name}: #{age}"
end
Методы
# def — определение метода. Тип возврата выводится автоматически.
def greet(name)
"Привет, #{name}!" # последнее выражение — возвращаемое значение
end
puts greet("Crystal")
# Аргументы по умолчанию и именованные
def power(base, exp = 2)
base ** exp
end
puts power(3) # => 9 (exp по умолчанию 2)
puts power(2, 10) # => 1024
puts power(base: 5, exp: 3) # именованные аргументы -> 125
# Можно указывать типы и тип возврата явно
def add(a : Int32, b : Int32) : Int32
a + b
end
puts add(2, 3)
# Метод с вопросительным знаком по соглашению возвращает Bool
def adult?(age)
age >= 18
end
puts adult?(20) # => true
# Блоки: yield вызывает переданный блок
def twice
yield 1
yield 2
end
twice { |n| puts "блок получил #{n}" }
# Захват блока в переменную через &block
def apply(x, &block : Int32 -> Int32)
block.call(x)
end
puts apply(5) { |n| n * 10 } # => 50
# Splat: *args собирает переменное число аргументов в Tuple
def sum_all(*nums)
nums.sum
end
puts sum_all(1, 2, 3, 4) # => 10
Классы и ООП
class Animal
# property создаёт и геттер, и сеттер для @name
property name : String
# getter — только чтение
getter species : String
# initialize — конструктор; @name — это поле объекта (instance variable)
def initialize(@name : String, @species : String)
end
def speak
"#{@name} издаёт звук"
end
end
a = Animal.new("Рекс", "собака")
puts a.name # геттер от property -> Рекс
a.name = "Барон" # сеттер от property
puts a.speak
puts a.species # только чтение (getter)
# Наследование через < (символ «меньше»)
class Dog < Animal
def initialize(name : String)
super(name, "собака") # вызвать конструктор родителя
end
# переопределение метода
def speak
"#{@name}: Гав!"
end
end
puts Dog.new("Шарик").speak # => Шарик: Гав!
Структуры и модули
struct — значимый тип (копируется при передаче), module — для примесей и пространств имён.
# struct похож на class, но передаётся по значению (как Int)
struct Point
getter x : Int32
getter y : Int32
def initialize(@x, @y)
end
def +(other : Point)
Point.new(@x + other.x, @y + other.y)
end
end
p1 = Point.new(1, 2)
p2 = Point.new(3, 4)
sum = p1 + p2 # вызовет наш оператор +
puts "#{sum.x}, #{sum.y}" # => 4, 6
# module как примесь (mixin): include добавляет методы в класс
module Greetable
def greet
"Привет от #{name}" # модуль рассчитывает, что у класса есть name
end
end
class User
getter name : String
include Greetable # подмешать методы модуля
def initialize(@name)
end
end
puts User.new("Аня").greet
# module как пространство имён
module Math2
TAU = 6.283
def self.double(x) # self. — метод на самом модуле
x * 2
end
end
puts Math2::TAU # доступ через ::
puts Math2.double(21) # => 42
Типизация и дженерики
# Дженерик-класс: T — параметр типа, подставляется при создании
class Box(T)
getter value : T
def initialize(@value : T)
end
end
int_box = Box(Int32).new(42)
str_box = Box(String).new("hi")
puts int_box.value # 42
puts str_box.value # hi
puts int_box.class # => Box(Int32)
# Дженерик-метод: тип выводится из аргумента
def first_of(arr : Array(T)) forall T
arr.first
end
puts first_of([10, 20, 30]) # => 10
# Тип-ограничение в union: метод примет и то, и другое
def describe(x : Int32 | String)
"значение #{x} типа #{x.class}"
end
puts describe(5)
puts describe("текст")
# as приводит тип (когда вы знаете больше компилятора)
values = [1, "two", 3] of Int32 | String
n = values[0].as(Int32)
puts n + 1 # => 2
Обработка исключений
# begin / rescue / else / ensure
begin
raise "что-то сломалось" # raise возбуждает исключение
rescue ex : Exception
puts "Поймали: #{ex.message}" # доступ к сообщению
else
puts "выполнится, если ошибок не было"
ensure
puts "ensure выполнится ВСЕГДА"
end
# Свой класс исключения
class MyError < Exception
end
def risky(n)
raise MyError.new("число слишком мало") if n < 10
n * 2
end
begin
risky(5)
rescue ex : MyError
puts "MyError: #{ex.message}"
end
# rescue прямо в методе — без begin
def safe_divide(a, b)
a // b
rescue DivisionByZeroError
0 # вернуть 0 при делении на ноль
end
puts safe_divide(10, 0) # => 0
Особенности языка
# 1) КОМПИЛЯЦИЯ. Crystal компилируется в нативный бинарник:
# crystal run app.cr — скомпилировать и запустить
# crystal build app.cr — собрать исполняемый файл
# crystal build --release app.cr — с оптимизацией для прода
# 2) ВЫВОД ТИПОВ. Типы почти не пишут руками, но проверка строгая
# на этапе компиляции — ошибки типов ловятся ДО запуска.
# 3) NIL-БЕЗОПАСНОСТЬ. Невозможно вызвать метод на возможном nil —
# компилятор требует явной проверки. Меньше runtime-падений.
# 4) Кортежи (Tuple) — неизменяемые, фиксированной длины, разных типов
tuple = {1, "два", 3.0}
puts tuple[0] # 1
puts tuple[1] # два
# 5) Named Tuple — кортеж с именованными полями
person = {name: "Аня", age: 25}
puts person[:name] # => Аня
# 6) Макросы выполняются на этапе компиляции (метапрограммирование)
macro define_const(name, value)
{{name.id}} = {{value}}
end
define_const(ANSWER, 42)
puts ANSWER # => 42
# 7) Совместимость с C: можно вызывать C-библиотеки через lib
# Это даёт доступ к огромной экосистеме нативных библиотек.
# Итог: Ruby-синтаксис + статическая типизация + скорость C + nil-безопасность.