Основы
Переменные и типы
Ruby — язык с динамической типизацией, переменным не нужны объявления типов. Используйте .class для проверки типа и is_a? для проверок типа. Всё является объектом, включая числа и булевы значения.
name = "Alice" # string
age = 30 # integer
pi = 3.14 # float
is_dev = true # boolean
nums = [1, 2, 3] # array
puts name.class # String
puts age.is_a?(Integer) # trueСимволы
Символы (:name) — неизменяемые, интернированные строки, в памяти существует только одна копия. Используйте их для ключей хешей, имён методов и ид ентификаторов, где идентичность важнее содержимого. Более эффективно по памяти, чем строки, при повторном использовании.
status = :active
puts status.class # Symbol
puts status.to_s # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status] # activeNil и истинность
В Ruby только nil и false являются ложными — всё остальное (включая 0, '' и []) истинно. Это отличается от многих языков, где 0 ложно. Используйте || для значений по умолчанию и nil? для проверки конкретно nil.
x = nil
puts x.nil? # true
puts x || "default" # default
# Only nil and false are falsy; 0 and "" are truthy!
puts 0 ? "truthy" : "falsy" # truthy
puts "" ? "truthy" : "falsy" # truthy
puts nil ? "truthy" : "falsy" # falsyПреобразование типов
to_i/to_s/to_f — мягкие преобразования (возвращают 0 при неудаче). Integer()/Float() — строгие (вызывают ArgumentError). Используйте строгое преобразование, когда нужна проверка, мягкое — когда нужна постепенная деградация.
puts "42".to_i # 42
puts 42.to_s # "42"
puts "3.14".to_f # 3.14
puts 3.14.to_i # 3
puts "abc".to_i # 0 (no error)
puts Integer("42") # 42 (raises if invalid)Интерполяция строк
Строки в двойных кавычках поддерживают интерполяцию #{expr} — любое Ruby-выражение внутри. Строки в одинарных кавычках буквальны (без интерполяции или экранирования, кроме \\ и \'). Используйте двойные кавычки, когда нужна интерполяция или escape-последовательности.
name = "Alice"
age = 30
puts "Name: #{name}, Age: #{age}"
puts "5 + 3 = #{5 + 3}"
puts "Upper: #{name.upcase}"
# Single quotes don't interpolate
puts 'No #{name} here' # No #{name} hereСтроки
Распространённые с троковые методы
Строки Ruby имеют богатые методы. Большинство возвращают новые строки (строки в Ruby изменяемы). Используйте варианты с ! (upcase!, gsub!) для модификации на месте — они изменяют получатель и возвращают nil, если изменений нет.
s = "Hello, World"
puts s.length # 12
puts s.upcase # HELLO, WORLD
puts s.downcase # hello, world
puts s.reverse # dlroW ,olleH
puts s.split(", ") # ["Hello", "World"]
puts s.gsub("o", "0") # Hell0, W0rldМутация строк (методы с !)
Методы, оканчивающиеся на !, изменяют объект на месте и часто более эффективны. Они могут возвращать nil, если изменений не было. Используйте, когда хотите избежать создания копий, но помните о побочных эффектах.
s = "hello"
s.upcase! # s is now "HELLO"
s.gsub!(/L/, "1") # s is now "HE11O"
puts s # HE11O
# Bang methods modify in-place; use with caution
arr = [3, 1, 2]
arr.sort! # arr is now [1, 2, 3]Heredoc и многострочность
Heredoc (<<TEXT ... TEXT) создают многострочные строки. <<~ (волнистый heredoc) удаляет общий ведущий пробел для чистого кода. Полезно для SQL-запросов, HTML-шаблонов или длинных сообщений.
text = <<~HEREDOC
Hello,
World!
Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentationФорматирование (sprintf)
Используйте оператор % или format/sprintf для C-подобного форматирования строк. %-10s выравнивает по левому краю в 10 символов, %08.2f заполняет нулями до 8 символов с 2 знаками. Полезно для табличного вывода и форматирования фиксированной ширины.
puts sprintf("%s is %d", "Alice", 30)
puts "%-10s|%5d" % ["Name", 42]
puts format("%.2f", 3.14159) # 3.14
puts "%08.2f" % 3.14 # 00003.14
# %s string, %d integer, %f floatКонкатенация строк
+ создаёт новую строку, << добавляет н а месте (эффективнее для построения строк). * повторяет строку. join объединяет элементы массива с разделителем. Предпочитайте << или join повторному + для производительности.
s1 = "Hello" + ", " + "World"
s2 = ["a", "b", "c"].join("-") # a-b-c
s3 = "Hello"
s3 << " " << "World" # in-place append
s4 = "x" * 3 # xxx (repetition)
puts s1, s2, s3, s4Структуры данных
Массивы
Массивы упорядочены, индексируются с нуля и могут содержать смешанные типы. << и push добавляют в конец. Используйте include? для принадлежности, sum для сумм. Массивы изменяемы — используйте freeze для неизменяемости.
nums = [1, 2, 3, 4, 5]
nums.push(6) # [1,2,3,4,5,6]
nums << 7 # same as push
nums[0] = 0
puts nums.first # 0
puts nums.length # 7
puts nums.sum # 28
puts nums.include?(3) # trueХеши
Хеши — словари ключ-значение. Ключи-символы (name:) идиоматичны и эффективны. Используйте key? для существования, fetch для безопасного доступа (вызывает ошибку при отсутствии), transform_values для массовых обновлений.
user = { name: "Alice", age: 30 }
user[:email] = "[email protected]"
puts user[:name] # Alice
puts user.key?(:name) # true
user.each { |k, v| puts "#{k}: #{v}" }
puts user.values # ["Alice", 30, "[email protected]"]
puts user.transform_values(&:to_s)Диапазоны
'..' включает конец, '...' исключает. Диапазоны ленивы и эффективны по памяти для больших последовательностей. Полезны для итерации, срезов и генерации последовательностей. Могут испо льзоваться с любым Comparable типом.
(1..5).each { |n| puts n } # 1 2 3 4 5
puts (1...5).to_a # [1, 2, 3, 4]
puts (1..10).select(&:even?) # [2, 4, 6, 8, 10]
puts ('a'..'e').include?('c') # true
puts (1..5).map { |n| n ** 2 } # [1, 4, 9, 16, 25]Методы Enumerable
Enumerable — самый мощный mixin в Ruby: map, select, reject, reduce, find, group_by, sort_by и т.д. Используйте &:method как сокращение для { |x| x.method }. Они обеспечивают выразительные конвейеры преобразования данных.
nums = [1, 2, 3, 4, 5]
puts nums.map { |n| n * 2 }.inspect # [2,4,6,8,10]
puts nums.select(&:even?).inspect # [2, 4]
puts nums.reject(&:odd?).inspect # [2, 4]
puts nums.reduce(0) { |sum, n| sum + n } # 15
puts nums.find { |n| n > 3 } # 4
puts nums.group_by(&:even?).inspect # {false=>[1,3,5], true=>[2,4]}Множества
Set (из библиотеки 'set') хранит уникальные элементы с поиском O(1). Используйте для дедупликации и операций над множествами (объединение, пересечение, разность). Преобразуйте в to_a, когда нужен массив. Требует require 'set'.
require 'set'
a = Set.new([1, 2, 3])
b = Set.new([3, 4, 5])
puts a.union(b).to_a.inspect # [1,2,3,4,5]
puts a.intersection(b).to_a.inspect # [3]
puts a.subtract([1]).to_a.inspect # [2, 3]
puts a.subset?(Set.new([1,2,3,4])) # true
a.add(6)Поток управления
If / Elsif / Unless
if/elsif/else — стандартное ветвление. unless — противоположность if (выполняется, когда условие ложно). Форма-модификатор (statement if condition) идиоматична для однострочных проверок — улучшает читаемость для простых случаев.
score = 85
if score >= 90
puts "A"
elsif score >= 80
puts "B"
else
puts "C"
end
# Modifier form
puts "Pass" if score >= 60
puts "Fail" unless score >= 60Case (When)
case/when использует === для сопоставления, что допускает диапазоны, классы и регуляр ные выражения. Несколько значений разделяются запятыми. then допускает однострочные тела. Без цели case действует как более чистая цепочка if/elsif.
grade = "B"
case grade
when "A" then puts "Excellent"
when "B", "C" then puts "Good"
when "D".."F" then puts "Poor"
else puts "Unknown"
end
# Case without value = multi-condition if
case
when score > 90 then puts "Top"
when score > 60 then puts "Pass"
endWhile / Until / Loop
while выполняется пока истинно, until выполняется пока ложно (пока не истинно). loop — бесконечен, используйте break для выхода. Предпочитайте итераторы (each, map) циклам while для коллекций — они идиоматичнее и менее подвержены ошибкам.
count = 0
while count < 3
puts count
count += 1
end
# until = opposite of while
n = 3
until n == 0
puts n
n -= 1
end
# Infinite loop with break
loop do
puts "forever"
break if rand > 0.8
endИтераторы (Each/Times/Upto)
Итераторы Ruby более идиоматичны, чем циклы for. times для счёта, upto/downto для диапазонов, each_with_index для индекс+значение. step управляет шагом. Это основа итерации в Ruby.
3.times { |i| puts i } # 0 1 2
1.upto(3) { |n| puts n } # 1 2 3
3.downto(1) { |n| puts n } # 3 2 1
[1,2,3].each_with_index do |n, i|
puts "#{i}: #{n}"
end
(1..3).step(1) { |n| puts n } # 1 2 3Break / Next / Redo
next пропускает к следующей итерации (как continue), break выходит из цикла раньше. break может вернуть значение из блока. redo перезапускает текущую итерацию без повторной проверки условия — используется редко.
[1, 2, 3, 4, 5].each do |n|
next if n.even? # skip even
break if n > 4 # stop at 5
puts n # prints 1, 3
end
# break value returns from the block
result = [1,2,3].each { |n| break n * 10 if n == 2 }
puts result # 20Методы и блоки
Определение метода
Мет оды используют def/end. Ключевые аргументы (key:) улучшают читаемость для многих параметров. *args собирает лишние позиционные аргументы в массив, **kwargs собирает ключевые аргументы в хеш. Значения по умолчанию используют =.
def greet(name, greeting: "Hello")
"#{greeting}, #{name}!"
end
puts greet("Alice") # Hello, Alice!
puts greet("Bob", greeting: "Hi") # Hi, Bob!
def add(*nums) # splat (variadic)
nums.sum
end
puts add(1, 2, 3, 4) # 10Блоки и Yield
Блоки — анонимные куски кода, передаваемые методам. yield вызывает блок. Блоки повсюду в Ruby (each, map и т.д.). Используйте yield, чтобы сделать методы гибкими — вызывающий предоставляет поведение.
def repeat(n)
n.times { yield }
end
repeat(3) { puts "hi" } # prints hi 3 times
def with_result
yield(5)
end
puts with_result { |x| x * 2 } # 10Процы и лямбды
Процы и лямбды — переиспользуемые блоки, хранимые в переменных. Ключевое отличие: лямбды проверяют количество аргументов (вызывают ошибку при несоответствии) и возвращаются только из себя; процы мягче и возвращаются из охватывающего метода. Предпочитайте lambda для строгого поведения.
square = proc { |x| x * x }
puts square.call(5) # 25
puts square.(5) # 25 (shortcut)
double = lambda { |x| x * 2 }
puts double.call(5) # 10
# Lambda checks arity; Proc doesn't
# Lambda returns from itself; Proc returns from enclosing methodОбъект Method (&)
&:method преобразует имя метода в Proc. Это идиоматичное сокращение для простых одно-методных блоков: map(&:to_i) вместо map { |s| s.to_i }. Чище и читаемее для простых преобразований.
nums = ["1", "2", "3"]
ints = nums.map(&:to_i) # [1, 2, 3]
puts ints.inspect
# &:to_i is shorthand for { |s| s.to_i }
names = ["alice", "bob"]
up = names.map(&:upcase) # ["ALICE", "BOB"]
puts up.inspectВозвращаемые значения
Методы Ruby неявно возвращают последнее вычисленное выражение — не нужен явн ый return. Используйте return для ранних выходов. Множественные значения возвращаются как массив и могут быть деструктурированы. Это делает код лаконичным.
def status(ok)
return "error" unless ok
"ok" # implicit return of last expression
end
puts status(true) # ok
puts status(false) # error
# Multiple assignment
def coords
return 1, 2 # returns [1, 2]
end
x, y = coordsКлассы и ООП
Класс и переменные экземпляра
@var = переменная экземпляра (на объект), @@var = переменная класса (разделяемая). attr_accessor генерирует getter+setter, attr_reader только getter, attr_writer только setter. self.method определяет методы класса.
class Person
attr_accessor :name, :age
attr_reader :id
@@count = 0 # class variable
def initialize(name, age)
@name = name # instance variable
@age = age
@@count += 1
end
def self.count; @@count; end
end
p = Person.new("Alice", 30)
puts p.name, Person.countНаследование и Super
< обозначает наследование (только одиночное). super вызывает версию родителя текущего метода. Используйте super (со скобками для передачи аргументов, без — те же аргументы) для расширения поведения родителя. Ruby использует одиночное наследование + mixins.
class Animal
def initialize(name); @name = name; end
def speak; "..."; end
end
class Dog < Animal
def speak; "#{@name}: Woof!"; end
end
class Puppy < Dog
def speak; "#{super} (small)"; end
end
puts Puppy.new("Rex").speak # Rex: Woof! (small)Модули (Mixins)
Модули группируют переиспользуемые методы. include добавляет методы экземпляра, extend добавляет методы класса. Это решение Ruby для множественного наследования. Enumerable — известный mixin: включите его и определите each, чтобы получить map, select и т.д.
module Walkable
def walk; "#{@name} is walking"; end
end
module Swimmable
def swim; "#{@name} is swimming"; end
end
class Duck
include Walkable # instance methods
extend Swimmable # class methods
def initialize(n); @name = n; end
end
puts Duck.new("Donald").walk # Donald is walkingУправление доступом
public (по умолчанию), private (только без явного получателя), protected (вызываемый в иерархии классов). Используйте private для внутренних помощников, protected для методов, разделяемых между экземплярами одного класса.
class BankAccount
def initialize(bal); @balance = bal; end
def deposit(amt); @balance += amt; end
def balance; @balance; end
private
def audit; "auditing..."; end
protected
def compare(other); @balance > other.balance; end
end
a = BankAccount.new(100)
puts a.balance # 100
# a.audit # Error: private methodSelf и методы класса
self ссылается на текущий объект. Внутри тела класса self — класс, def self.method определяет методы класса. Методы класса вызываются на классе (Counter.total), методы экземпляра на экземплярах. alias_method создаёт псевдоним метода.
class Counter
@@total = 0
def initialize; @@total += 1; end
def self.total; @@total; end
def self.reset!; @@total = 0; end
def instance_method; "I'm an instance"; end
alias_method :count, :instance_method
end
Counter.new; Counter.new
puts Counter.total # 2Обработка ошибок
Begin / Rescue / Ensure
begin/rescue — это try/catch в Ruby. Спасайте конкретные классы исключений для целевой обработки. => e захватывает объект исключения. ensure выполняется всегда — используйте для очистки (закрытие файлов, освобождение блокировок).
begin
result = 10 / 0
rescue ZeroDivisionError => e
puts "Caught: #{e.message}"
rescue => e
puts "Other error: #{e.class}"
ensure
puts "Always runs (cleanup)"
end
# ensure runs regardless of success/failureRaise и пользов ательские исключения
raise генерирует исключение (raise без аргументов перегенерирует). Пользовательские исключения наследуются от StandardError (или более конкретного класса). Называйте их с суффиксом Error. Спасайте по классу для обработки конкретных режимов неудач.
class InvalidAgeError < StandardError; end
def set_age(age)
raise InvalidAgeError, "Age cannot be negative" if age < 0
raise ArgumentError, "Must be Integer" unless age.is_a?(Integer)
@age = age
end
begin
set_age(-5)
rescue InvalidAgeError => e
puts "Custom: #{e.message}"
endRetry
retry перезапускает блок begin с начала. Используйте для временных неудач (сеть, лимиты частоты) со счётчиком для избежания бесконечных циклов. Без ограничения retry может зависить программу — всегда ограничивайте.
attempts = 0
begin
attempts += 1
fetch_data # might fail
rescue NetworkError
retry if attempts < 3
puts "Failed after 3 attempts"
end
# retry re-runs the begin block from the topМодификатор Rescue
rescue как модификатор — лаконичный способ предоставить резервное значение. Перехватывает StandardError и возвращает правую сторону. Используйте для простых случаев — избегайте для сложной логики, так как скрывает ошибки. Отлично для разбора или опциональных операций.
result = risky_operation rescue "default"
puts result
# Same as:
# result = begin; risky_operation; rescue; "default"; end
# Catches StandardError only
json = JSON.parse(str) rescue nil
puts "Invalid JSON" if json.nil?Throw / Catch (поток управления)
throw/catch — НЕ обработка исключений, это механизм потока управления для раннего выхода из глубокой вложенности (в отличие от других языков). throw :symbol прыгает к соответствующему catch. Используйте для выхода из вложенных циклов; используйте begin/rescue для реальных ошибок.
catch(:done) do
[1, 2, 3, 4, 5].each do |n|
throw :done, n if n > 3
puts n
end
end
# Prints 1, 2, 3 and returns 4
# Not for errors—use for early exit from nested loopsФайловый ввод-вывод
Чтение и запись файлов
File.write/File.read — простые одноразовые методы. File.open с блоком автоматически закрывает файл. File.foreach читает построчно без загрузки всего файла — эффективно по памяти для больших файлов. chomp удаляет завершающий перенос строки.
# Write
File.write("test.txt", "Hello, File!")
# Read
content = File.read("test.txt")
puts content # Hello, File!
# Append
File.open("log.txt", "a") { |f| f.puts "new line" }
# Read line by line
File.foreach("test.txt") { |line| puts line.chomp }Блок File (авто-закрытие)
Всегда используйте File.open с блоком — гарантирует закрытие файла даже при ошибке. Блочная форма — идиоматичный, безопасный способ работы с файлами. Без блока нужно вручную вызывать close.
File.open("data.txt", "w") do |f|
f.puts "Line 1"
f.puts "Line 2"
f.write("No newline")
end # file auto-closed here
File.open("data.txt", "r") do |f|
f.each_line.with_index { |line, i| puts "#{i}: #{line}" }
endСуществование и информация о файлах
Класс File предоставляет запросы к файловой системе. file?/directory? различают типы. mtime/ctime/atime дают временные метки. rename/delete модифицируют файловую систему. Всегда проверяйте существование перед операциями для избежания ошибок.
puts File.exist?("test.txt") # true
puts File.file?("test.txt") # true (regular file)
puts File.directory?(".") # true
puts File.size("test.txt") # bytes
puts File.mtime("test.txt") # modification time
File.rename("old.txt", "new.txt")
File.delete("new.txt") if File.exist?("new.txt")Операции с каталогами
Dir управляет каталогами. mkdir создаёт, chdir меняет (блочная форма восстанавливает после). glob сопоставляет шаблоны файлов — * совпадает с любым, ** рекурсивно. Полезно для обнаружения файлов и пакетной обработки.
Dir.mkdir("test_dir") unless Dir.exist?("test_dir")
Dir.chdir("test_dir") do
File.write("a.txt", "a")
puts Dir.pwd # current path
end
Dir.glob("*.txt") { |f| puts f } # list .txt files
Dir.glob("**/*.rb") { |f| puts f } # recursiveCSV и JSON
CSV и JSON — в стандартной библиотеке. CSV.foreach потоково читает строки (эффективно по памяти). JSON.parse возвращает хеши/массивы со строковыми ключами. to_json сериализует любой объект. Это необходимо для обмена данными.
require 'csv'
require 'json'
CSV.write("data.csv", [["a", 1], ["b", 2]])
CSV.foreach("data.csv") { |row| puts row.inspect }
data = { name: "Alice", age: 30 }
File.write("data.json", data.to_json)
parsed = JSON.parse(File.read("data.json"))
puts parsed["name"] # AliceДата/Время и регулярные выражения
Time и Date
Time представляет момент (с часовым поясом). Date представляет календарную дату (без времени). Арифметика Time использует секунды. strftime форматирует с %Y (год), %m (месяц), %d (день), %H:%M (время). Требует 'time' для разбора ISO8601.
require 'time'
now = Time.now
puts now # 2024-01-15 14:30:00 +0800
puts now.strftime("%Y-%m-%d %H:%M") # 2024-01-15 14:30
tomorrow = now + 86400 # +1 day (seconds)
puts tomorrow.strftime("%A") # weekday name
date = Date.today
puts date.next_day # tomorrowРазбор дат и арифметика
Арифметика дат работает естественно — добавление целых чисел добавляет дни. next_month/prev_month обрабатывают границы месяцев. Date - Date возвращает Rational (дни). upto/downto перебирают диапазоны дат. Используйте Date для календарной логики, Time для временных меток.
require 'date'
d = Date.parse("2024-01-15")
puts d.year # 2024
puts d + 7 # 2024-01-22 (+7 days)
puts d.next_month # 2024-02-15
diff = (Date.today - d).to_i
puts "#{diff} days since"
puts Date.today.upto(Date.today + 6).map(&:wday)Сопоставление регулярных выражений
=~ возвращает позицию совпадения или nil. $1, $2 содержат захваченные группы после совпадения. .match возвращает объект MatchData для большего detail. Используйте =~ для простых проверок, .match для извлечения захватов. Литералы регулярных выражений используют /pattern/.
s = "Phone: 123-4567"
if s =~ /(\d+)-(\d+)/
puts "Area: #{$1}, Number: #{$2}"
end
m = /\w+@(\w+)/.match("[email protected]")
puts m[1] # example
puts "abc123" =~ /\d/ ? "has digit" : "no digit"Подстановка в регулярных выражениях
gsub заменяет все совпадения (sub заменяет первое). Передайте блок для динамической замены. scan извлекает все совпадения в массив. Обратные ссылки (\1, \2) в строках замены ссылаются на захваченные группы. Мощно для обработки текста.
s = "Hello, World"
puts s.gsub(/o/, "0") # Hell0, W0rld
puts s.gsub(/\w+/) { |w| w.capitalize }
puts s.scan(/\w+/).inspect # ["Hello", "World"]
puts "2024-01-15".gsub(/(\d+)-(\d+)-(\d+)/, '\3/\2/\1')
# => 15/01/2024Опции регулярных выражений
Опции регулярных выражений: i (без учёта регистра), m (многострочный — точка совпадает с переносом строки), x (расширенный — допускает пробелы/комментарии в шаблоне). %r{} — альтернативный разделитель, полезен когда шаблон содержит слэши (как URL).
puts /hello/i =~ "HELLO" # 0 (case-insensitive)
puts /line/m =~ "a\nb" # 0 (multiline: . matches \n)
puts /x.y/x =~ "x y" # 0 (extended: ignore whitespace)
# Common patterns
email = /[\w.]+@[\w]+\.[a-z]+/
url = %r{https?://[\w./]+}
puts email.match("[email protected]") ? "valid" : "invalid"Метапрограммирование и параллелизм
Динамическое определение методов
define_method создаёт методы во время выполнения. Используйте для генерации нескольких похожих методов или построения DSL. Это метапрограммирование — код, который пишет код. Мощно, но используйте осмотрительно; может усложнить понимание кода.
class Dynamic
[:foo, :bar, :baz].each do |name|
define_method(name) { puts "Called #{name}" }
end
end
d = Dynamic.new
d.foo # Called foo
d.bar # Called bar
# Useful for generating similar methodsmethod_missing
method_missing перехватывает вызовы неопределённых методов. Используйте для построения гибких API или прокси. Всегда переопределяйте respond_to_missing? тоже, чтобы рефлексия работала. Используйте умеренно — может скрывать ошибки и путать статический анализ.
class Proxy
def method_missing(name, *args)
puts "Called: #{name} with #{args.inspect}"
end
def respond_to_missing?(name, include_private = false)
true
end
end
p = Proxy.new
p.anything(1, 2) # Called: anything with [1, 2]Потоки
Потоки выполняют код параллельно. join ждёт завершения. MRI (стандартный Ruby) имеет GIL, поэтому CPU-bound потоки не выполняются в истинном параллелизме — I/O-bound потоки выполняются. Для истинного параллелизма используйте Ractor (Ruby 3.0+) или несколько процессов.
threads = [1, 2, 3].map do |n|
Thread.new { puts "Thread #{n}: #{n * n}" }
end
threads.each(&:join) # wait for all
# MRI has GIL—threads don't run truly parallel for CPU work
# Use for I/O concurrency (network, file)Файберы (кооперативные)
Файберы — лёгкая, кооперативная параллельность — они приостанавливаются и возобновляются вручную через yield/resume. В отличие от потоков, они не выполняются параллельно. Используйте для генераторов, ленивых вычислений или приостанавливаемых вычислений. Меньше накладных расходов, чем у потоков.
fiber = Fiber.new do
Fiber.yield 1
Fiber.yield 2
3
end
puts fiber.resume # 1
puts fiber.resume # 2
puts fiber.resume # 3
# Fibers yield control cooperatively, not preemptivelySend и Eval
send вызывает любой метод по имени (включая private). public_send уважает видимость. eval выполняет строку как Ruby-код — крайне мощно, но опасно с ненадёжным вводом (внедрение кода). Используйте send для динамической диспетчеризации, избегайте eval в продакшене.
class Obj
def secret; "hidden"; end
end
o = Obj.new
puts o.send(:secret) # hidden (calls private too)
puts o.public_send(:secret) # hidden (respects visibility)
x = 5
result = eval("x * 2")
puts result # 10
# eval runs a string as code—avoid with untrusted input!Блоки, процы и лямбды
Основы блоков
Блоки — самое распространённое замыкание Ruby, анонимный код, передаваемый методам через { } или do...end. yield вызывает блок. |n| объявляет параметры блока. block_given? проверяет, передан ли блок. Блоки не являются объектами (нельзя хранить в переменных) — используйте Proc/Lambda для этого. Каждый метод может принимать неявный блок, делая DSL естественным (Rails активно это использует).
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n } # do-end for multi-line
[1, 2, 3].each do |n|
puts n * 2
end
# yield: call the block from a method
def greet
puts "before"
yield # invokes the block
puts "after"
end
greet { puts "in block" }
# before / in block / after
# yield with arguments
def compute
result = yield(10, 20)
puts "got: #{result}"
end
compute { |a, b| a + b } # got: 30
# block_given? checks if a block was passed
def maybe_yield
return "no block" unless block_given?
yield
endПроцы vs Лямбды
Процы и лямбды — оба вызываемые объекты (блоки, превращённые в объекты). Процы мягкие: лишние аргументы становятся nil, отсутствующие — nil, и 'return' выходит из охватывающего метода. Лямбды строгие: проверяют количество аргументов и 'return' выходит только из лямбды. Используйте лямбды, когда нужно методо-подобное поведение; процы — когда нужно блоко-подобное. -> (stabby lambda) — современный лаконичный синтаксис.
# Proc: a block stored as an object
p = Proc.new { |x| puts x * 2 }
p.call(5) # 10
p.(5) # 10 (shorthand)
p[5] # 10 (another shorthand)
# Lambda: stricter Proc
l = lambda { |x| puts x * 2 }
l = ->(x) { puts x * 2 } # stabby lambda syntax
l.call(5)
# Key differences:
# 1. Argument checking
p = Proc.new { |a, b| puts a }
p.call(1) # 1 (b is nil, no error)
l = ->(a, b) { puts a }
l.call(1) # ArgumentError (wrong number of args)
# 2. return behavior
def proc_test
p = Proc.new { return 1 }
p.call
return 2 # never reached—Proc return exits method
end
proc_test # 1
def lambda_test
l = lambda { return 1 }
l.call # returns from lambda, not method
return 2
end
lambda_test # 2Объекты Method
method(:name) извлекает метод как объект Method (вызываемый как Proc). &:symbol преобразует символ в proc, отправляющий этот метод — крайне распространённая идиома: array.map(&:to_s). Префикс & преобразует Proc в блок (и наоборот). Это позволяет элегантно передавать методы как аргументы. Объекты Method сохраняют получателя, поэтому они привязаны к объекту.
class Calculator
def add(a, b) a + b end
end
calc = Calculator.new
# method(:name) gets a Method object
m = calc.method(:add)
m.call(2, 3) # 5
m.(2, 3) # 5
m[2, 3] # 5
# Convert method to Proc
p = m.to_proc
p.call(4, 5) # 9
# &:method shorthand (common in map, each)
["a", "b", "c"].map(&:upcase) # ["A", "B", "C"]
# Equivalent to: .map { |s| s.upcase }
# Symbol#to_proc
:upcase.to_proc.call("hello") # "HELLOW"
# Useful for passing methods as blocks
[1, 2, 3].each(&method(:puts)) # prints 1, 2, 3Замыкания и Binding
Замыкания (блоки, процы, лямбды) захватывают переменные по ссылке — видят обновления захваченных переменных. Это обеспечивает состояние-хранящие замыкания (счётчики, аккумуляторы). Proc#binding даёт доступ к окружению замыкания (для продвинутого метапрограммирования). Метод Kernel#binding захватывает текущий контекст выполнения для eval. Замыкания — причина, п о которой блоки Ruby так мощны для обратных вызовов и итераторов.
# Closures capture surrounding variables
counter = 0
increment = lambda { counter += 1 }
increment.call # 1
increment.call # 2
puts counter # 2 (closure modified it)
# Multiple closures share the same variables
x = 10
add = lambda { |n| x += n }
get = lambda { x }
add.call(5)
get.call # 15
# Proc#binding: access the closure's environment
def make_counter
count = 0
lambda { count += 1 }
end
c = make_counter
c.call # 1
# c.binding.eval("count") # 1 (peek at captured var)
# Binding for eval in a specific context
b = binding
x = 42
eval("x", b) # 42Пользовательские итераторы с блоками
Включение Enumerable и определение #each даёт вам map, select, reduce и 50+ методов бесплатно — это протокол итератора Ruby. each_with_object чище inject для построения аккумуляторов. tap вставляет побочные эффекты в цепочки методов (отлично для отладки). Пользовательские методы с yield позволяют создавать собственные DSL (with_timing, with_database и т.д.). Блоки делают шаблоны итерации и обратных вызовов Ruby элегантными.
# Define your own iterator using yield
class LinkedList
include Enumerable # gets map, select, etc. for free
def each
node = @head
while node
yield node.value
node = node.next
end
end
end
# each_with_object (memo pattern)
result = [1, 2, 3].each_with_object({}) do |n, hash|
hash[n] = n * n
end
# {1=>1, 2=>4, 3=>9}
# tap (for debugging chains)
[1, 2, 3].map { |n| n * 2 }
.tap { |arr| puts "after map: #{arr}" }
.select { |n| n > 2 }
# Custom method with block
def with_timing
start = Time.now
yield
puts "took #{Time.now - start}s"
end
with_timing { sleep(1) }Enumerable и итераторы
Основные методы Enumerable
Enumerable — самый мощный mixin в Ruby: включите его и определите #each, чтобы получить 50+ методов. map преобразует, select/reject фильтруют, reduce/inject агрегирует, find возвращает первое совпадение, group_by/partition кластеризуют. Сокращения через Symbol (reduce(:+)) идиоматичны. Эти методы работают с массивами, хешами, диапазонами, файлами — всем с #each. Освоение Enumerable — ключ к идиоматичному Ruby.
nums = [1, 2, 3, 4, 5, 6]
# map/collect: transform
nums.map { |n| n * 2 } # [2, 4, 6, 8, 10, 12]
# select/filter: keep matching
nums.select { |n| n.even? } # [2, 4, 6]
nums.reject { |n| n.even? } # [1, 3, 5]
# reduce/inject: accumulate
nums.reduce(0) { |sum, n| sum + n } # 21
nums.reduce(:+) # 21 (symbol shorthand)
nums.reduce(:*) # 720
# find/detect: first match
nums.find { |n| n > 3 } # 4
# group_by: cluster
nums.group_by { |n| n.even? } # {false=>[1,3,5], true=>[2,4,6]}
# partition: split into two
nums.partition { |n| n.even? } # [[2,4,6], [1,3,5]]
# chunk: consecutive grouping
[1,1,2,2,3].chunk { |n| n }.to_aЛенивые вычисления
lazy преобразует enumerable в ленивый — значения вычисляются только при необходимости. Это позволяет бесконечные последовательности и избегает вычисления всей коллекции для цепочек, которым нужно мало результатов. Без lazy map/select создают промежуточные массивы. С lazy конвейер вытягивает значения по одному. Используйте lazy для больших/бесконечных наборов данных или дорогих преобразований. Компромисс: lazy имеет накладные расходы на элемент, поэтому медленнее для маленьких коллекций.
# Lazy: evaluate on demand (infinite sequences possible)
require 'prime'
# Without lazy: infinite loop!
# primes = (1..Float::INFINITY).select(&:prime?).first(5)
# With lazy: works!
primes = (1..Float::INFINITY).lazy.select(&:prime?).first(5)
# [2, 3, 5, 7, 11]
# Chain without intermediate arrays
(1..1_000_000).lazy
.map { |n| n * 2 }
.select { |n| n > 1_000_000 }
.first(10) # only computes what's needed
# Build a lazy enumerator
enum = Enumerator::Lazy.new(1..Float::INFINITY) do |yielder, n|
yielder << n if n.prime?
end
enum.first(5) # [2, 3, 5, 7, 11]Enumerator и внешняя итерация
Enumerator оборачивает итерацию как объект — можно вызывать .next вручную (внешняя итерация) вместо использования блока (внутренняя итерация). Это позволяет приостанавливать/возобновлять итерацию, заглядывать вперёд и создавать бесконечные последовательности. Enumerator.new с блоком позволяет строить пользовательские итераторы (генераторы). Большинство методов Enumerable возвращают Enumerator при вызове без блока: [1,2,3].map возвращает Enumerator, который можно компоновать.
# Enumerator: an iterator object you can control
enum = [10, 20, 30].each
enum.next # 10
enum.next # 20
enum.next # 30
enum.next # StopIteration
# External iteration with loop (handles StopIteration)
enum = [1, 2, 3].each
loop do
puts enum.next
end
# Create custom Enumerator
fib = Enumerator.new do |yielder|
a, b = 0, 1
loop do
yielder << a
a, b = b, a + b
end
end
fib.first(10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Convert to array
fib.take(10).to_aСортировка и сравнение
sort принимает блок, сравнивающий два элемента (возвращает -1, 0 или 1). sort_by эффективнее — вычисляет ключ сортировки один раз на элемент (преобразование Шварца) вместо каждого сравнения. Используйте sort_by для сложных ключей. Оператор <=> (spaceship) возвращает -1/0/1 и является основой сортировки Ruby. min/max/min_by/max_by находят экстремумы. Включите Comparable и определите <=> для естественного упорядочивания ваших объектов.
arr = [3, 1, 4, 1, 5, 9, 2, 6]
# sort: returns new array
arr.sort # [1, 1, 2, 3, 4, 5, 6, 9]
arr.sort { |a, b| b <=> a } # descending (9, 6, 5, ...)
# sort_by: more efficient (Schwartzian transform)
arr.sort_by { |n| -n } # descending
words = ["banana", "apple", "cherry"]
words.sort_by { |w| w.length } # ["apple", "banana", "cherry"]
# min/max
arr.min # 1
arr.max # 9
arr.minmax # [1, 9]
arr.min_by { |n| n.abs } # 1
# min/max with block
people.max_by { |p| p[:age] }
# <=> (spaceship): -1, 0, 1
1 <=> 2 # -1
2 <=> 2 # 0
3 <=> 2 # 1Итерация и преобразование хешей
Хеши тоже Enumerable. each/each_pair перебирают пары ключ-значение. transform_keys/transform_values (Ruby 2.5+) создают новые хеши с изменёнными ключами/значениями. select/reject фильтруют в новый хеш. merge объединяет хеши (блок разрешает конфликты). group_by строит хеш из массива. Итерация хеша — основа обработки данных в Ruby — освойте это для чистой, выразительной обработки данных.
hash = { a: 1, b: 2, c: 3 }
# Iterate
hash.each { |k, v| puts "#{k}=#{v}" }
hash.each_key { |k| puts k }
hash.each_value { |v| puts v }
hash.each_pair { |k, v| puts "#{k}: #{v}" }
# Transform keys/values
hash.transform_keys { |k| k.to_s } # {"a"=>1, "b"=>2, "c"=>3}
hash.transform_values { |v| v * 10 } # {a:10, b:20, c:30}
# Filter
hash.select { |k, v| v > 1 } # {b:2, c:3}
hash.reject { |k, v| v > 1 } # {a:1}
# Merge
{ a: 1 }.merge({ b: 2 }) # {a:1, b:2}
{ a: 1 }.merge({ a: 2 }) { |k, old, new| old + new } # {a:3}
# Invert (values become keys)
{ a: 1, b: 2 }.invert # {1=>:a, 2=>:b}
# Group arrays into hash
%w[apple apricot banana].group_by { |w| w[0] }
# {"a"=>["apple", "apricot"], "b"=>["banana"]}Гемы и Bundler
Основы гемов
Гемы — пакеты Ruby (библиотеки). gem install управляет системными гемами. Для проектов используйте Bundler с Gemfile для фиксации версий и управления зависимостями. Спецификаторы версий: '~> 1.4' (пессимистичный, допускает патчи), '>= 5.0' (оптимистичный). Группы (:development, :test, :production) позволяют загружать только нужные гемы на окружение. require: false означает, что Bundler не будет авто-требовать его (требуете вручную при необходимости).
# Install a gem
$ gem install rails
$ gem install rails -v 7.0.0
$ gem install rails --pre # pre-release
# List installed gems
$ gem list
$ gem list rails
# Uninstall
$ gem uninstall rails
# Gemfile: project dependencies
# Gemfile
source 'https://rubygems.org'
gem 'rails', '7.0.0'
gem 'pg', '~> 1.4' # pessimistic: >= 1.4, < 2.0
gem 'puma', '>= 5.0' # optimistic: >= 5.0
gem 'rspec', group: :test # group-specific
gem 'pry', require: false # don't auto-require
# Groups
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
end
group :production do
gem 'newrelic_rpm'
endКоманды Bundler
Bundler гарантирует, что проект использует точные версии гемов. bundle install читает Gemfile и пишет Gemfile.lock (точные версии для воспроизводимости — коммитьте это!). bundle exec запускает команды с правильными версиями гемов (избегает конфликтов). bundle update меняет версии (будьте осторожны — может сломать). Всегда используйте bundle exec для rake/rspec/rails, чтобы обеспечить загрузку правильных гемов. Gemfile.lock делает развёртывания воспроизводимыми.
# Install all gems from Gemfile
$ bundle install
$ bundle install --without production # skip group
# Update gems
$ bundle update # update all
$ bundle update rails # update specific gem
$ bundle outdated # show outdated gems
# Run commands in bundle context
$ bundle exec rails server
$ bundle exec rspec
$ bundle exec rake db:migrate
# Check for dependency issues
$ bundle check
$ bundle doctor
# Lock file: Gemfile.lock
# Records exact versions installed (for reproducibility)
# Commit Gemfile.lock to version control!
# Add/remove gems
$ bundle add rspec
$ bundle remove rspec
# Clean old gems
$ bundle cleanGemspec (создание гема)
Файл .gemspec определяет метаданные и зависимости гема. spec.files перечисляет включённые файлы; require_paths сообщает Ruby, где их найти. add_dependency для зависимостей времени выполнения, add_development_dependency для тестов/сборки. required_ruby_version требует версию Ruby. gem build создаёт пакет .gem; gem install устанавливает локально. Публикуйте на rubygems.org через gem push. Структура: lib/ для кода, spec/ для тестов.
# my_gem.gemspec
Gem::Specification.new do |spec|
spec.name = "my_gem"
spec.version = "0.1.0"
spec.summary = "A useful Ruby gem"
spec.description = "Longer description..."
spec.authors = ["Alice"]
spec.email = ["[email protected]"]
spec.homepage = "https://github.com/alice/my_gem"
spec.license = "MIT"
spec.files = Dir["lib/**/*.rb"] + ["README.md"]
spec.require_paths = ["lib"]
spec.required_ruby_version = ">= 3.0"
spec.add_dependency "httparty", "~> 0.21"
spec.add_development_dependency "rspec", "~> 3.12"
end
# Directory structure:
# my_gem/
# lib/my_gem.rb (main file)
# lib/my_gem/version.rb
# my_gem.gemspec
# Gemfile
# spec/ (tests)
# Build and install
$ gem build my_gem.gemspec # creates my_gem-0.1.0.gem
$ gem install ./my_gem-0.1.0.gemЗадачи Rake
Rake — это make для Ruby, исполнитель задач. Определите задачи через task :name do ... end. Пространства имён группируют связанные задачи. Файловые задачи имеют зависимости (перестройка при изменении источника). sh запускает команды оболочки. Rake используется для тестов, сборок, развёртываний и задач БД (Rails активно использует). Запускайте через rake task_name. Задача default выполняется при вводе просто rake. Передавайте аргументы через rake greet[Alice].
# Rakefile
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task default: :spec
# Custom task
task :greet, [:name] do |t, args|
puts "Hello, #{args.name}!"
end
# $ rake greet[Alice]
# Namespace
namespace :db do
task :migrate do
puts "migrating..."
end
task :seed do
puts "seeding..."
end
end
# $ rake db:migrate
# File tasks (build dependencies)
file 'output.txt' => 'input.txt' do |t|
sh "cp #{t.source} #{t.name}"
end
# Multi-line task
task :deploy do
sh 'git push heroku main'
sh 'heroku run rails db:migrate'
endRbenv и RVM (версии Ruby)
rbenv и RVM управляют несколькими версиями Ruby на одной машине. rbenv лёгкий (shims); RVM тяжелее (переопределяет команды оболочки). Файл .ruby-version (коммитится) гарантирует, что все используют одну версию Ruby. Gemsets (RVM) или bundle config path изолируют гемы проекта. Для продакшена используйте bundle config set path для установки гемов локально в проект, избегая загрязнения системных гемов. Всегда фиксируйте версию Ruby в .ruby-version.
# rbenv: lightweight Ruby version manager
$ rbenv install 3.2.0 # install a version
$ rbenv global 3.2.0 # set global version
$ rbenv local 3.1.0 # set per-project (.ruby-version)
$ rbenv versions # list installed
# .ruby-version file (committed to project)
# 3.2.0
# RVM: alternative version manager
$ rvm install 3.2.0
$ rvm use 3.2.0
$ rvm gemset create myapp # isolated gem sets
$ rvm use 3.2.0@myapp
# Bundler config
$ bundle config set path 'vendor/bundle' # install locally
$ bundle config set without 'development test' # for production
# Check versions
$ ruby -v
$ gem -v
$ bundle -vОсновы Rails
Структура MVC
Rails — MVC-фреймворк: Модели (ActiveRecord) обрабатывают данные, Контроллеры (ActionController) обрабатывают HTTP-запросы, Представления (ActionView) рендерят ответы. resources генерирует 7 RESTful-маршрутов автоматически. Маршруты сопоставляют URL с действиями контроллера. Конвенция вместо конфигурации: назовите модель User, контроллер UsersController, и Rails всё соединит. Эта структура — основа каждого Rails-приложения.
# Rails follows Model-View-Controller
# app/
# models/ (ActiveRecord: data + business logic)
# controllers/ (ActionController: handle requests)
# views/ (ActionView: render responses)
# helpers/ (view helpers)
# config/routes.rb (URL routing)
# Route → Controller → Model → View
# GET /users → UsersController#index → User.all → index.html.erb
# config/routes.rb
Rails.application.routes.draw do
resources :users # generates 7 RESTful routes
# GET /users → index
# GET /users/new → new
# POST /users → create
# GET /users/:id → show
# GET /users/:id/edit → edit
# PATCH /users/:id → update
# DELETE /users/:id → destroy
root 'pages#home' # root path
get 'about', to: 'pages#about' # custom route
endМодели ActiveRecord
ActiveRecord — ORM Rails: модели сопоставляются с таблицами БД. validates обеспечивает целостность данных. Ассоциации (has_many, belongs_to, has_one) определяют отношения. Колбэки (before_save, after_create) встраиваются в жизненный цикл. Скоупы — переиспользуемые фрагменты запросов. ActiveRecord использует конвенции: модель User → таблица users, колонки created_at/updated_at. Это сердце Rails — освоите для эффективной разработки на Rails.
# app/models/user.rb
class User < ApplicationRecord
# Validations
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
# Associations
has_many :posts, dependent: :destroy
has_one :profile
belongs_to :company
has_and_belongs_to_many :tags
# Callbacks
before_save :normalize_email
after_create :send_welcome_email
# Scopes (reusable queries)
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
private
def normalize_email
self.email = email.downcase
end
end
# Usage
User.create(name: "Alice", email: "[email protected]")
User.active.recent
User.find(1)
User.where("age > ?", 18)Контроллеры и сильные параметры
Контроллеры обрабатывают HTTP-запросы и координируют модели/представления. before_action запускает фильтры (аутентификация, загрузка ресурсов). Сильные параметры (permit) предотвращают уязвимости массового присваивания — только whitelist-поля могут быть установлены. redirect_to отправляет пользователя в другое место; render показывает представление. @instance-переменные доступны в представлениях. RESTful-действия (index, show, new, create, edit, update, destroy) конвенциональны.
# app/controllers/users_controller.rb
class UsersController < ApplicationController
before_action :set_user, only: %i[show edit update destroy]
def index
@users = User.all
end
def show; end # @user set by before_action
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: 'User created'
else
render :new, status: :unprocessable_entity
end
end
private
def set_user
@user = User.find(params[:id])
end
# Strong parameters: whitelist allowed fields
def user_params
params.require(:user).permit(:name, :email, :age)
end
endПредставления и помощники
ERB (Embedded Ruby) — шаблонизатор Rails по умолчанию: <%= %> выводит, <% %> выполняет. link_to/button_to генерируют HTML-ссылки/формы. form_with строит формы, привязанные к моделям. Партиалы (_form.html.erb) — переиспользуемые фрагменты представлений, рендерятся через render. Помощники путей (new_user_path, user_path(user)) генерируют URL из маршрутов. Помощники сохраняют представления чистыми. Для сложной логики используйте помощники (app/helpers/) или декораторы.
<!-- app/views/users/index.html.erb -->
<h1>Users</h1>
<%= link_to "New User", new_user_path %>
<ul>
<% @users.each do |user| %>
<li>
<%= link_to user.name, user_path(user) %>
(<%= user.email %>)
<%= link_to "Edit", edit_user_path(user) %>
<%= button_to "Delete", user, method: :delete %>
</li>
<% end %>
</ul>
<!-- Partials: reusable view fragments -->
<!-- app/views/users/_form.html.erb -->
<%= form_with model: user do |form| %>
<% if user.errors.any? %>
<div class="errors">
<% user.errors.full_messages.each do |msg| %>
<p><%= msg %></p>
<% end %>
</div>
<% end %>
<%= form.text_field :name %>
<%= form.email_field :email %>
<%= form.submit %>
<% end %>
<!-- Render partial -->
<%= render "form", user: @user %>Миграции и база данных
Миграции развивают схему БД со временем, под контролем версий. create_table определяет таблицы; add_column/remove_column модифицируют их. t.references создаёт внешние ключи. t.timestamps добавляет created_at/updated_at. db:migrate применяет ожидающие миграции; db:rollback отменяет последнюю. Файл schema.rb — авторитетный источник текущей схемы (автогенерируется). Никогда не редактируйте schema.rb напрямую — используйте миграции. Это делает изменения БД воспроизводимыми между окружениями.
# Generate a migration
$ rails generate migration CreateUsers name:string email:string
# db/migrate/20240101_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false
t.string :email, null: false, index: { unique: true }
t.integer :age, default: 0
t.text :bio
t.references :company, foreign_key: true
t.timestamps # created_at, updated_at
end
end
end
# Run migrations
$ rails db:migrate
$ rails db:rollback # undo last migration
$ rails db:seed # load seed data
$ rails db:reset # drop + create + migrate + seed
# Schema file (don't edit manually)
# db/schema.rb reflects current database structure
# Add a column later
$ rails generate migration AddAgeToUsers age:integer
# creates: add_column :users, :age, :integerТестирование RSpec
Базовый синтаксис (describe, it, expect)
RSpec — доминирующий фреймворк тестирования Ruby. describe группирует связанные тесты; it определяет один тест. expect(...).to / not_to делают утверждения. let определяет ленивые мемоизированные переменные (вычисляются один раз при первом доступе). context — псевдоним describe, используется для ветвей (when...). Гем shoulda-matchers предоставляет однострочный синтаксис для распространённых валидаций/ассоциаций Rails. Тесты идут в spec/, зеркалируя структуру app/.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Setup with let (lazy, memoized)
let(:user) { User.new(name: "Alice", email: "[email protected]") }
describe '#name' do
it 'returns the name' do
expect(user.name).to eq("Alice")
end
# Multiple expectations with context
context 'when name is blank' do
let(:user) { User.new(name: "") }
it 'is invalid' do
expect(user).not_to be_valid
end
end
end
# One-liner syntax
it { should validate_presence_of(:email) }
it { should have_many(:posts) }
endМоки и заглушки
Заглушки (allow) заменяют возвращаемые значения методов; моки (expect) проверяют, что метод был вызван. Двойники — фейковые объекты для тестирования (быстрее реальных). Используйте заглушки для изоляции тестируемого кода от внешних зависимостей (API, БД). Используйте моки для проверки взаимодействий. Избыточное мокирование делает тесты хрупкими — предпочитайте реальные объекты, когда достаточно быстры. FactoryBot создаёт тестовые данные; используйте create (сохраняет в БД) или build (только в памяти).
RSpec.describe PaymentService do
let(:user) { create(:user) }
it 'charges the card' do
# Stub: replace a method's return value
allow(user).to receive(:premium?).and_return(true)
# Mock: expect a method to be called
expect(Stripe::Charge).to receive(:create).with(
amount: 1000, currency: 'usd'
).and_return(double(id: 'ch_123'))
service = PaymentService.new(user)
result = service.charge(10)
expect(result).to eq('ch_123')
end
it 'handles failure' do
# Stub to raise an error
allow(Stripe::Charge).to receive(:create)
.and_raise(Stripe::StripeError.new("card declined"))
service = PaymentService.new(user)
expect { service.charge(10) }.to raise_error(PaymentError)
end
end
# Double: test stand-in for an object
fake_card = double('Card', last4: '4242', brand: 'Visa')FactoryBot и фикстуры
FactoryBot создаёт тестовые объекты с разумными значениями по умолчанию. Traits создают вариации (:admin, :inactive). Sequences генерируют уникальные значения (email). create персистит в БД; build не делает. create_list создаёт несколько. Переопределяйте любой атрибут, передав его. Фабрики гибче фикстур (YAML), но медленнее (запись в БД). Используйте build_stubbed для быстрых тестов без БД. Держите фабрики простыми — сложные фабрики указывают на сложные модели.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { "Alice" }
email { "[email protected]" }
age { 30 }
active { true }
# Traits: variations
trait :admin do
role { "admin" }
end
trait :inactive do
active { false }
end
# Associations
company { association(:company) }
# Sequences for unique values
sequence(:email) { |n| "user#{n}@example.com" }
end
end
# Usage in specs
let(:user) { create(:user) } # saved to DB
let(:admin) { create(:user, :admin) } # with trait
let(:user) { build(:user) } # not saved
let(:users) { create_list(:user, 5) } # 5 users
# Override attributes
let(:alice) { create(:user, name: "Alice") }Хуки Before/After
хуки before запускают код настройки: before(:each) (наиболее частый) перед каждым тестом, before(:all) один раз на группу. хуки after убирают. DatabaseCleaner управляет состоянием тестовой БД (транзакция для скорости, усечение для тщательности). Предпочитайте let перед before(:each) — let лениво (вычисляется только при использовании) и мемоизировано, тогда как before выполняется, даже если тест не нуждается в нём. Используйте before для побочных эффектов, которые должны произойти (логирование, заморозка времени).
RSpec.describe User do
before(:all) do
# Runs once before all tests in this group
@shared_data = load_expensive_data
end
before(:each) do # or just 'before'
# Runs before each test
@user = User.create(name: "test")
end
after(:each) do
# Runs after each test (cleanup)
User.delete_all
end
after(:all) do
# Runs once after all tests
@shared_data = nil
end
# Database cleaner for transactional tests
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
end
# Use before for setup, after for cleanup
# Prefer let over before(:each) for lazy evaluationИнтеграционные и системные тесты
Request-спеки тестируют весь стек (маршрутизация → контроллер → модель → представление) через HTTP. System-спеки (Capybara) управляют реальным браузером — заполнение форм, клики, проверка содержимого страницы. Модульные тесты (model-спеки) быстры и изолированы; интеграционные/системные медленнее, но ловят ошибки связывания. Используйте пирамиду тестирования: много быстрых модульных, меньше интеграционных, минимум системных. have_http_status проверяет коды ответа; visit/fill_in/click_button управляют браузером.
# spec/requests/users_spec.rb (integration)
require 'rails_helper'
RSpec.describe 'Users API', type: :request do
describe 'GET /users' do
before { create_list(:user, 3) }
it 'returns all users' do
get '/users'
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).size).to eq(3)
end
end
describe 'POST /users' do
it 'creates a user' do
post '/users', params: { user: { name: 'Bob' } }
expect(response).to have_http_status(:created)
end
end
end
# spec/system/login_spec.rb (browser tests)
require 'rails_helper'
RSpec.describe 'Login', type: :system do
it 'logs in a user' do
user = create(:user, password: 'secret')
visit login_path
fill_in 'Email', with: user.email
fill_in 'Password', with: 'secret'
click_button 'Log in'
expect(page).to have_content('Welcome')
end
endФайловые и каталоговые операции
Чтение файлов
File.read загружает весь файл в память — нормально для маленьких файлов. File.foreach читает построчно — необходимо для больших файлов (не переполнит память). File.open с блоком авто-закрывает файл (RAII). readlines возвращает массив строк (с переносами — используйте chomp). binread для бинарных файлов. Всегда проверяйте File.exist? перед чтением, если файл может не существовать, или спасайте Errno::ENOENT.
# Read entire file
content = File.read("data.txt")
# Read line by line (memory efficient)
File.foreach("large.txt") do |line|
puts line.chomp # chomp removes trailing newline
end
# Read all lines into array
lines = File.readlines("data.txt")
# With a block (auto-closes)
File.open("data.txt") do |f|
f.each_line { |line| puts line }
end
# Read with options
File.read("data.txt", encoding: "utf-8")
File.binread("image.png") # binary mode
# Check existence
File.exist?("data.txt")
File.file?("data.txt") # is it a regular file?
File.directory?("path") # is it a directory?Запись файлов
File.write — простейший способ записи файла (перезаписывает по умолчанию). Используйте mode: 'a' для добавления. File.open с блоком гарантирует закрытие файла даже при исключении. puts добавляет перенос строки; write не делает. << — псевдоним для write (часто в Ruby). Для логов откройте один раз и пишите несколько раз (буферизовано для производительности). Всегда закрывайте файлы или используйте блочную форму для избежания утечек ресурсов.
# Write (overwrites)
File.write("output.txt", "Hello, World!")
# Append
File.write("output.txt", "More text\n", mode: "a")
# With a block (buffered, auto-closes)
File.open("log.txt", "w") do |f|
f.puts "Line 1"
f.puts "Line 2"
f.write("No newline")
f << "appended" # << is an alias for write
end
# Binary write
File.binwrite("data.bin", binary_data)
# Modes: r (read), w (write/truncate), a (append),
# r+ (read/write), w+ (read/write/truncate),
# b (binary, Windows)
# Flush buffer
f = File.open("log.txt", "w")
f.write("data")
f.flush # write without closing
f.closeОперации с каталогами
Dir.glob с шаблонами находит файлы (** для рекурсивности). FileUtils предоставляет надёжные файловые операции: mkdir_p создаёт вложенные каталоги, cp_r копирует рекурсивно, rm_rf удаляет принудительно (будьте осторожны!). Dir.chdir меняет рабочий каталог (используйте блочную форму для временного изменения). Dir.entries включает . и ..; glob не делает. Предпочитайте FileUtils ручным File-операциям для кросс-платформенной безопасности.