Skip to content

Ruby Шпаргалка

Динамичный, элегантный язык, оптимизированный для удовольствия разработчика.

01

Основы

Переменные и типы

Ruby — язык с динамической типизацией, переменным не нужны объявления типов. Используйте .class для проверки типа и is_a? для проверок типа. Всё является объектом, включая числа и булевы значения.

ruby
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) — неизменяемые, интернированные строки, в памяти существует только одна копия. Используйте их для ключей хешей, имён методов и идентификаторов, где идентичность важнее содержимого. Более эффективно по памяти, чем строки, при повторном использовании.

ruby
status = :active
puts status.class        # Symbol
puts status.to_s         # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status]       # active

Nil и истинность

В Ruby только nil и false являются ложными — всё остальное (включая 0, '' и []) истинно. Это отличается от многих языков, где 0 ложно. Используйте || для значений по умолчанию и nil? для проверки конкретно nil.

ruby
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). Используйте строгое преобразование, когда нужна проверка, мягкое — когда нужна постепенная деградация.

ruby
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-последовательности.

ruby
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
02

Строки

Распространённые строковые методы

Строки Ruby имеют богатые методы. Большинство возвращают новые строки (строки в Ruby изменяемы). Используйте варианты с ! (upcase!, gsub!) для модификации на месте — они изменяют получатель и возвращают nil, если изменений нет.

ruby
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, если изменений не было. Используйте, когда хотите избежать создания копий, но помните о побочных эффектах.

ruby
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-шаблонов или длинных сообщений.

ruby
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 знаками. Полезно для табличного вывода и форматирования фиксированной ширины.

ruby
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 повторному + для производительности.

ruby
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
03

Структуры данных

Массивы

Массивы упорядочены, индексируются с нуля и могут содержать смешанные типы. << и push добавляют в конец. Используйте include? для принадлежности, sum для сумм. Массивы изменяемы — используйте freeze для неизменяемости.

ruby
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 для массовых обновлений.

ruby
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 типом.

ruby
(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 }. Они обеспечивают выразительные конвейеры преобразования данных.

ruby
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'.

ruby
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)
04

Поток управления

If / Elsif / Unless

if/elsif/else — стандартное ветвление. unless — противоположность if (выполняется, когда условие ложно). Форма-модификатор (statement if condition) идиоматична для однострочных проверок — улучшает читаемость для простых случаев.

ruby
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 >= 60

Case (When)

case/when использует === для сопоставления, что допускает диапазоны, классы и регулярные выражения. Несколько значений разделяются запятыми. then допускает однострочные тела. Без цели case действует как более чистая цепочка if/elsif.

ruby
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"
end

While / Until / Loop

while выполняется пока истинно, until выполняется пока ложно (пока не истинно). loop — бесконечен, используйте break для выхода. Предпочитайте итераторы (each, map) циклам while для коллекций — они идиоматичнее и менее подвержены ошибкам.

ruby
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.

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 3

Break / Next / Redo

next пропускает к следующей итерации (как continue), break выходит из цикла раньше. break может вернуть значение из блока. redo перезапускает текущую итерацию без повторной проверки условия — используется редко.

ruby
[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
05

Методы и блоки

Определение метода

Методы используют def/end. Ключевые аргументы (key:) улучшают читаемость для многих параметров. *args собирает лишние позиционные аргументы в массив, **kwargs собирает ключевые аргументы в хеш. Значения по умолчанию используют =.

ruby
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, чтобы сделать методы гибкими — вызывающий предоставляет поведение.

ruby
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 для строгого поведения.

ruby
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 }. Чище и читаемее для простых преобразований.

ruby
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 для ранних выходов. Множественные значения возвращаются как массив и могут быть деструктурированы. Это делает код лаконичным.

ruby
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
06

Классы и ООП

Класс и переменные экземпляра

@var = переменная экземпляра (на объект), @@var = переменная класса (разделяемая). attr_accessor генерирует getter+setter, attr_reader только getter, attr_writer только setter. self.method определяет методы класса.

ruby
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.

ruby
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 и т.д.

ruby
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 для методов, разделяемых между экземплярами одного класса.

ruby
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 method

Self и методы класса

self ссылается на текущий объект. Внутри тела класса self — класс, def self.method определяет методы класса. Методы класса вызываются на классе (Counter.total), методы экземпляра на экземплярах. alias_method создаёт псевдоним метода.

ruby
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
07

Обработка ошибок

Begin / Rescue / Ensure

begin/rescue — это try/catch в Ruby. Спасайте конкретные классы исключений для целевой обработки. => e захватывает объект исключения. ensure выполняется всегда — используйте для очистки (закрытие файлов, освобождение блокировок).

ruby
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/failure

Raise и пользовательские исключения

raise генерирует исключение (raise без аргументов перегенерирует). Пользовательские исключения наследуются от StandardError (или более конкретного класса). Называйте их с суффиксом Error. Спасайте по классу для обработки конкретных режимов неудач.

ruby
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}"
end

Retry

retry перезапускает блок begin с начала. Используйте для временных неудач (сеть, лимиты частоты) со счётчиком для избежания бесконечных циклов. Без ограничения retry может зависить программу — всегда ограничивайте.

ruby
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 и возвращает правую сторону. Используйте для простых случаев — избегайте для сложной логики, так как скрывает ошибки. Отлично для разбора или опциональных операций.

ruby
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 для реальных ошибок.

ruby
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
08

Файловый ввод-вывод

Чтение и запись файлов

File.write/File.read — простые одноразовые методы. File.open с блоком автоматически закрывает файл. File.foreach читает построчно без загрузки всего файла — эффективно по памяти для больших файлов. chomp удаляет завершающий перенос строки.

ruby
# 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.

ruby
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 модифицируют файловую систему. Всегда проверяйте существование перед операциями для избежания ошибок.

ruby
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 сопоставляет шаблоны файлов — * совпадает с любым, ** рекурсивно. Полезно для обнаружения файлов и пакетной обработки.

ruby
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 } # recursive

CSV и JSON

CSV и JSON — в стандартной библиотеке. CSV.foreach потоково читает строки (эффективно по памяти). JSON.parse возвращает хеши/массивы со строковыми ключами. to_json сериализует любой объект. Это необходимо для обмена данными.

ruby
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
09

Дата/Время и регулярные выражения

Time и Date

Time представляет момент (с часовым поясом). Date представляет календарную дату (без времени). Арифметика Time использует секунды. strftime форматирует с %Y (год), %m (месяц), %d (день), %H:%M (время). Требует 'time' для разбора ISO8601.

ruby
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 для временных меток.

ruby
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/.

ruby
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) в строках замены ссылаются на захваченные группы. Мощно для обработки текста.

ruby
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).

ruby
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"
10

Метапрограммирование и параллелизм

Динамическое определение методов

define_method создаёт методы во время выполнения. Используйте для генерации нескольких похожих методов или построения DSL. Это метапрограммирование — код, который пишет код. Мощно, но используйте осмотрительно; может усложнить понимание кода.

ruby
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 methods

method_missing

method_missing перехватывает вызовы неопределённых методов. Используйте для построения гибких API или прокси. Всегда переопределяйте respond_to_missing? тоже, чтобы рефлексия работала. Используйте умеренно — может скрывать ошибки и путать статический анализ.

ruby
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+) или несколько процессов.

ruby
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. В отличие от потоков, они не выполняются параллельно. Используйте для генераторов, ленивых вычислений или приостанавливаемых вычислений. Меньше накладных расходов, чем у потоков.

ruby
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 preemptively

Send и Eval

send вызывает любой метод по имени (включая private). public_send уважает видимость. eval выполняет строку как Ruby-код — крайне мощно, но опасно с ненадёжным вводом (внедрение кода). Используйте send для динамической диспетчеризации, избегайте eval в продакшене.

ruby
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!
11

Блоки, процы и лямбды

Основы блоков

Блоки — самое распространённое замыкание Ruby, анонимный код, передаваемый методам через { } или do...end. yield вызывает блок. |n| объявляет параметры блока. block_given? проверяет, передан ли блок. Блоки не являются объектами (нельзя хранить в переменных) — используйте Proc/Lambda для этого. Каждый метод может принимать неявный блок, делая DSL естественным (Rails активно это использует).

ruby
# 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) — современный лаконичный синтаксис.

ruby
# 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 сохраняют получателя, поэтому они привязаны к объекту.

ruby
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 так мощны для обратных вызовов и итераторов.

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 элегантными.

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) }
12

Enumerable и итераторы

Основные методы Enumerable

Enumerable — самый мощный mixin в Ruby: включите его и определите #each, чтобы получить 50+ методов. map преобразует, select/reject фильтруют, reduce/inject агрегирует, find возвращает первое совпадение, group_by/partition кластеризуют. Сокращения через Symbol (reduce(:+)) идиоматичны. Эти методы работают с массивами, хешами, диапазонами, файлами — всем с #each. Освоение Enumerable — ключ к идиоматичному Ruby.

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 имеет накладные расходы на элемент, поэтому медленнее для маленьких коллекций.

ruby
# 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, который можно компоновать.

ruby
# 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 и определите <=> для естественного упорядочивания ваших объектов.

ruby
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 — освойте это для чистой, выразительной обработки данных.

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"]}
13

Гемы и Bundler

Основы гемов

Гемы — пакеты Ruby (библиотеки). gem install управляет системными гемами. Для проектов используйте Bundler с Gemfile для фиксации версий и управления зависимостями. Спецификаторы версий: '~> 1.4' (пессимистичный, допускает патчи), '>= 5.0' (оптимистичный). Группы (:development, :test, :production) позволяют загружать только нужные гемы на окружение. require: false означает, что Bundler не будет авто-требовать его (требуете вручную при необходимости).

ruby
# 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 делает развёртывания воспроизводимыми.

ruby
# 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 clean

Gemspec (создание гема)

Файл .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/ для тестов.

ruby
# 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].

ruby
# 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'
end

Rbenv и RVM (версии Ruby)

rbenv и RVM управляют несколькими версиями Ruby на одной машине. rbenv лёгкий (shims); RVM тяжелее (переопределяет команды оболочки). Файл .ruby-version (коммитится) гарантирует, что все используют одну версию Ruby. Gemsets (RVM) или bundle config path изолируют гемы проекта. Для продакшена используйте bundle config set path для установки гемов локально в проект, избегая загрязнения системных гемов. Всегда фиксируйте версию Ruby в .ruby-version.

ruby
# 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
14

Основы Rails

Структура MVC

Rails — MVC-фреймворк: Модели (ActiveRecord) обрабатывают данные, Контроллеры (ActionController) обрабатывают HTTP-запросы, Представления (ActionView) рендерят ответы. resources генерирует 7 RESTful-маршрутов автоматически. Маршруты сопоставляют URL с действиями контроллера. Конвенция вместо конфигурации: назовите модель User, контроллер UsersController, и Rails всё соединит. Эта структура — основа каждого Rails-приложения.

ruby
# 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.

ruby
# 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) конвенциональны.

ruby
# 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/) или декораторы.

ruby
<!-- 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 напрямую — используйте миграции. Это делает изменения БД воспроизводимыми между окружениями.

ruby
# 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
15

Тестирование RSpec

Базовый синтаксис (describe, it, expect)

RSpec — доминирующий фреймворк тестирования Ruby. describe группирует связанные тесты; it определяет один тест. expect(...).to / not_to делают утверждения. let определяет ленивые мемоизированные переменные (вычисляются один раз при первом доступе). context — псевдоним describe, используется для ветвей (when...). Гем shoulda-matchers предоставляет однострочный синтаксис для распространённых валидаций/ассоциаций Rails. Тесты идут в spec/, зеркалируя структуру app/.

ruby
# 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 (только в памяти).

ruby
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 для быстрых тестов без БД. Держите фабрики простыми — сложные фабрики указывают на сложные модели.

ruby
# 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 для побочных эффектов, которые должны произойти (логирование, заморозка времени).

ruby
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 управляют браузером.

ruby
# 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
16

Файловые и каталоговые операции

Чтение файлов

File.read загружает весь файл в память — нормально для маленьких файлов. File.foreach читает построчно — необходимо для больших файлов (не переполнит память). File.open с блоком авто-закрывает файл (RAII). readlines возвращает массив строк (с переносами — используйте chomp). binread для бинарных файлов. Всегда проверяйте File.exist? перед чтением, если файл может не существовать, или спасайте Errno::ENOENT.

ruby
# 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). Для логов откройте один раз и пишите несколько раз (буферизовано для производительности). Всегда закрывайте файлы или используйте блочную форму для избежания утечек ресурсов.

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-операциям для кросс-платформенной безопасности.

ruby
require 'fileutils'

# List directory contents
Dir.entries(".")  # [".", "..", "file.txt", ...]
Dir.glob("*.rb")  # ["script.rb", "test.rb"]
Dir.glob("**/*.rb")  # recursive
Dir.glob("src/**/*.{rb,erb}")  # multiple extensions

# Create directories
Dir.mkdir("new_dir")
Dir.mkdir("nested/path") rescue Errno::ENOENT  # fails if parent missing
FileUtils.mkdir_p("nested/path/deep")  # creates all parents

# Remove
Dir.rmdir("empty_dir")  # only works if empty
FileUtils.rm_rf("dir")  # recursive force (dangerous!)

# Copy/Move
FileUtils.cp("a.txt", "b.txt")
FileUtils.mv("old.txt", "new.txt")
FileUtils.cp_r("src_dir", "dest_dir")  # recursive

# Change directory
Dir.chdir("/tmp") { puts Dir.pwd }  # temporarily
Dir.chdir("/tmp")  # permanently for process

# Check if directory
File.directory?("path")
File.exist?("path")

Pathname и Tempfile

Pathname — объектно-ориентированная обёртка файловых путей — чище, чем манипуляция строками. Предоставляет dirname, basename, extname, join и файловые проверки как методы. Tempfile создаёт временные файлы, автоматически удаляемые (используйте блочную форму). Dir.mktmpdir создаёт временные каталоги. Используйте их для чистой, безопасной обработки путей и управления временными файлами. Pathname компонует пути безопасно между платформами (обрабатывает / против \).

ruby
require 'pathname'
require 'tempfile'

# Pathname: object-oriented path manipulation
path = Pathname.new("/home/user/docs/file.txt")
path.dirname   # #<Pathname:/home/user/docs>
path.basename  # #<Pathname:file.txt>
path.extname   # ".txt"
path.parent    # #<Pathname:/home/user/docs>
path.join("sub", "file.rb")  # /home/user/docs/sub/file.rb

path.exist?
path.directory?
path.file?
path.readable?

# Tempfile: auto-deleted file
Tempfile.create("prefix") do |f|
  f.write("temporary data")
  f.rewind
  puts f.read
end  # file deleted after block

# Tempfile without block (must close/unlink manually)
tf = Tempfile.new("prefix")
tf.write("data")
tf.close
tf.unlink  # delete

# Dir.mktmpdir for temp directories
Dir.mktmpdir do |dir|
  # work in dir
end  # dir deleted after

CSV и JSON

CSV и JSON встроены в стандартную библиотеку Ruby. CSV.foreach читает построчно (эффективно по памяти); CSV.read загружает всё. headers: true обрабатывает первую строку как имена колонок. JSON.parse преобразует JSON в Ruby-хеши/массивы; to_json сериализует Ruby-объекты. symbolize_names даёт ключи-символы (чище). Для YAML используйте require 'yaml' и YAML.load_file. Это необходимо для обмена данными в Ruby-скриптах и веб-приложениях.

ruby
require 'csv'
require 'json'

# CSV reading
CSV.foreach("data.csv", headers: true) do |row|
  puts row['name']  # access by header
  puts row[0]       # access by index
end

# CSV reading all at once
rows = CSV.read("data.csv", headers: true)
rows.first['name']  # first row's name

# CSV writing
CSV.open("output.csv", "w") do |csv|
  csv << ["name", "age"]  # header
  csv << ["Alice", 30]
  csv << ["Bob", 25]
end

# JSON
data = { name: "Alice", age: 30 }
json = data.to_json  # '{"name":"Alice","age":30}'
parsed = JSON.parse(json)  # {"name"=>"Alice", "age"=>30}

# JSON with symbols
JSON.parse(json, symbolize_names: true)  # {name: "Alice", age: 30}

# Read/write JSON files
File.write("data.json", data.to_json)
loaded = JSON.parse(File.read("data.json"))
17

Кодировки и внутренности строк

Кодировки строк

Строки Ruby хранят свою кодировку (обычно UTF-8). force_encoding интерпретирует байты как другую кодировку (без преобразования — используйте, когда знаете, что байты уже в этой кодировке). encode фактически преобразует между кодировками. valid_encoding? проверяет, валидны ли байты для кодировки строки. Проблемы кодировки вызывают страшную Encoding::CompatibilityError. Всегда знайте кодировку ваших данных; по умолчанию UTF-8.

ruby
# Ruby strings have an encoding
"hello".encoding  # #<Encoding:UTF-8>
"café".encoding   # #<Encoding:UTF-8>

# Default external encoding (for file I/O)
Encoding.default_external  # #<Encoding:UTF-8>

# Force an encoding (reinterprets bytes, doesn't convert)
bytes = "café".bytes  # [99, 97, 102, 195, 169]
latin1 = bytes.pack("C*").force_encoding("ISO-8859-1")
# "caf" + é (as single byte)

# Convert encoding (transcodes)
utf8 = "café"
latin1 = utf8.encode("ISO-8859-1")  # converts
back = latin1.encode("UTF-8")        # converts back

# Check if valid
"abc".valid_encoding?  # true
"\xff".valid_encoding?  # false (invalid byte)

# Common encodings: UTF-8, ASCII, ISO-8859-1, Windows-1252
# Always use UTF-8 unless you have a specific reason

Преобразование кодировок и ввод-вывод

Файловый ввод-вывод использует Encoding.default_external для чтения. Укажите кодировку для каждого файла через опцию encoding:. Синтаксис 'source:target' (ISO-8859-1:UTF-8) читает в исходной кодировке и преобразует в целевую. Установка default_internal заставляет Ruby авто-конвертировать все прочитанные строки в эту кодировку. Для веб-приложений всё должно быть UTF-8. При обработке унаследованных данных явно указывайте кодировки для избежания повреждения.

ruby
# Read a file with specific encoding
content = File.read("data.txt", encoding: "ISO-8859-1")
# content.encoding is ISO-8859-1

# Read and convert to UTF-8
content = File.read("data.txt", encoding: "ISO-8859-1:UTF-8")
# The "source:target" syntax converts while reading

# Write with specific encoding
File.write("output.txt", "café", encoding: "UTF-8")

# Open with encoding
File.open("data.txt", "r:ISO-8859-1") do |f|
  f.read  # ISO-8859-1 encoded string
end

# Convert while opening
File.open("data.txt", "r:ISO-8859-1:UTF-8") do |f|
  f.read  # UTF-8 encoded string
end

# Set default encodings
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8

Строковые методы: глубокое погружение

Строки Ruby имеют богатые методы. Инспекция (length, include?, start_with?) проверяет свойства. Методы преобразования возвращают новые строки (строки изменяемы в Ruby, но эти не мутируют). Доступ к подстрокам через [start, length] или диапазоны. sub заменяет первое совпадение; gsub заменяет все (поддерживает регулярные выражения и блоки). split/join преобразуют между строками и массивами. Примечание: прагма frozen_string_literal в Ruby 3.0+ делает строки неизменяемыми для производительности — используйте << или + для построения.

ruby
s = "Hello, World"

# Inspection
s.length    # 12
s.empty?    # false
s.include?("World")  # true
s.start_with?("Hello")  # true
s.end_with?("World")  # true

# Transformation (return new string)
s.upcase     # "HELLO, WORLD"
s.downcase   # "hello, world"
s.capitalize # "Hello, world"
s.swapcase   # "hELLO, wORLD"
s.reverse    # "dlroW ,olleH"
s.strip      # remove leading/trailing whitespace
s.chomp(",") # remove trailing substring
s.chop       # remove last char

# Substrings
s[0, 5]      # "Hello" (start, length)
s[7..11]     # "World" (range)
s[-5..]      # "World" (negative index)

# Replace
s.sub("World", "Ruby")    # first match
s.gsub("o", "0")          # all matches
s.gsub(/\w+/) { |w| w.upcase }  # with block

# Split/Join
"a,b,c".split(",")  # ["a", "b", "c"]
["a", "b"].join("-")  # "a-b"

Замороженные строки и производительность

frozen_string_literal: true (магический комментарий в начале файла) делает все строковые литералы неизменяемыми — это оптимизация производительности (замороженные строки могут разделять память) и предотвращает случайные ошибки мутации. Для построения строк используйте << (добавление на месте, O(n)) не += (создаёт новую строку каждый раз, O(n²)). join — самый чистый для массивов. StringIO действует как файл, но пишет в строку — полезно для построения сложного вывода. Ruby 3.x поощряет замороженные строки по умолчанию.

ruby
# frozen_string_literal: true  (at top of file)
# Makes all string literals in the file frozen (immutable)

# Mutable vs frozen
s = "hello"
s << " world"  # "hello world" (mutates)

f = "hello".freeze
# f << " world"  # Error: can't modify frozen String

# Why freeze? Performance: frozen strings share memory
# "abc".freeze is the same object everywhere
"a".freeze.equal?("a".freeze)  # true (same object)

# Building strings efficiently
# BAD: creates many intermediate strings
result = ""
items.each { |i| result += i.to_s }  # O(n²)

# GOOD: use << or join
result = ""
items.each { |i| result << i.to_s }  # O(n)
result = items.map(&:to_s).join  # cleanest

# StringIO for buffered building
require 'stringio'
io = StringIO.new
io << "line 1\n"
io << "line 2\n"
result = io.string

Символы vs строки

Символы (:name) — неизменяемые, singleton-идентификаторы, только один :foo существует в памяти. Строки — изменяемые текстовые данные с несколькими экземплярами. Используйте символы для ключей хешей (быстрее проверки равенства), имён методов и enum-подобных значений. Используйте строки для реального текста. Символы немного быстрее для ключей хешей и сравнения. В современном Ruby (2.2+) символы могут быть собраны сборщиком мусора, поэтому старая проблема 'утечки памяти символами' ушла. Rails активно использует символы для ключей и значений статуса.

ruby
# Symbol: immutable, reusable identifier
:hello
:world
status = :active

# String: mutable, can have many instances
"hello"
"world"
status = "active"

# Key difference: identity
"hello".equal?("hello")  # false (different objects)
:hello.equal?(:hello)    # true (same object, singleton)

# Memory: symbols are singletons
1000.times { :foo }  # one :foo object
1000.times { "foo" }  # 1000 "foo" objects (unless frozen)

# Use symbols for:
# - Hash keys (faster comparison)
# - Method names (send(:method_name))
# - Enum-like values (:active, :pending, :closed)
# - Identifiers (not text data)

# Use strings for:
# - Text data (names, content)
# - Things that change

# Conversion
:hello.to_s  # "hello"
"hello".to_sym  # :hello
"hello world".to_sym  # :"hello world" (valid but ugly)
18

Модули и Mixins

Основы модулей

Модули служат двум целям: пространство имён (группировка связанного кода, предотвращение конфликтов имён) и mixins (разделение поведения без наследования). Методы модуля (def self.method) вызываются на модуле. Методы экземпляра (def method) — для смешивания в классы. Модули нельзя инстанцировать. Используйте модули для пространства имён классов (MyApp::User) и организации констант и утилитарных функций. Это альтернатива Ruby множественному наследованию.

ruby
# Module: a namespace + collection of methods
module MathUtils
  PI = 3.14159

  def self.circle_area(radius)  # module method
    PI * radius ** 2
  end

  def square(x)  # instance method (for mixins)
    x * x
  end
end

# Access constants and module methods
MathUtils::PI  # 3.14159
MathUtils.circle_area(5)  # 78.54

# Namespacing classes
module MyApp
  class User
    # MyApp::User
  end
end

# Prevent instantiation (modules can't be instantiated)
# MathUtils.new  # NoMethodError

Include vs Extend vs Prepend

Три способа смешать модули: include (добавляет методы экземпляра, идёт ниже класса в поиске), extend (добавляет методы класса), prepend (добавляет методы экземпляра, идёт выше класса — может обернуть/переопределить). prepend мощен для хуков before/after (вызов super для оригинала). Поиск методов: prepend → класс → include → суперкласс. Используйте include для обычных mixins, prepend когда нужно обернуть существующие методы, extend для функциональности уровня класса.

ruby
module Greetable
  def greet
    "Hello from #{self.class}"
  end
end

class User
  include Greetable  # adds as instance methods
end
User.new.greet  # "Hello from User"

class Service
  extend Greetable  # adds as class methods
end
Service.greet  # "Hello from Service"

# Method lookup order:
# prepend → class → include → super
module Logging
  def save
    puts "before save"
    super  # calls the original save
    puts "after save"
  end
end

class Record
  prepend Logging  # Logging#save runs first
  def save; puts "saving"; end
end
Record.new.save
# before save / saving / after save

Mixin Enumerable

Включение Enumerable и определение #each даёт вам map, select, reduce, sort, min, max и 40+ методов — это протокол итератора Ruby. Включение Comparable и определение <=> даёт вам <, >, ==, between?, clamp и поддержку сортировки. Эти mixins — причина, по которой коллекции Ruby так мощны. Любой класс, представляющий коллекцию или имеющий естественное упорядочивание, должен их включать. Это композиция вместо наследования.

ruby
# Include Enumerable + define #each = 50+ methods free
class Playlist
  include Enumerable

  def initialize(songs)
    @songs = songs
  end

  def each
    @songs.each { |song| yield song }
  end
end

playlist = Playlist.new(["Song A", "Song B", "Song C"])
playlist.map { |s| s.upcase }   # ["SONG A", "SONG B", "SONG C"]
playlist.select { |s| s.include?("A") }  # ["Song A"]
playlist.reduce(:+)            # "Song ASong BSong C"
playlist.sort                  # ["Song A", "Song B", "Song C"]
playlist.first(2)              # ["Song A", "Song B"]
playlist.include?("Song B")    # true
playlist.count                 # 3

# Comparable: define <=> for natural ordering
class Temperature
  include Comparable
  attr_reader :celsius

  def initialize(c) @celsius = c end
  def <=>(other) celsius <=> other.celsius end
end

t1 = Temperature.new(20)
t2 = Temperature.new(30)
t1 < t2   # true (uses <=>)
t1 == t2  # false

Singleton-методы и методы класса

Singleton-методы принадлежат одному конкретному объекту. Методы класса — просто singleton-методы на объекте класса. 'class << self' открывает singleton-класс (eigenclass) для чистого определения нескольких методов класса. Паттерн singleton использует переменную класса для хранения одного экземпляра. Понимание singleton-классов — ключ к объектной модели Ruby — у каждого объекта есть singleton-класс, хранящий его уникальные методы. Это позволяет настройку на уровне объекта и метапрограммирование.

ruby
# Singleton method: defined on one object only
str = "hello"
def str.shout
  upcase + "!"
end
str.shout  # "HELLO!"
# "world".shout  # NoMethodError (only str has it)

# Class methods are singleton methods on the class
class Calculator
  def self.add(a, b)  # singleton method on Calculator
    a + b
  end
end
Calculator.add(1, 2)  # 3

# Singleton class (eigenclass): where singleton methods live
class Calculator
  class << self  # opens the singleton class
    def multiply(a, b) a * b end
    def divide(a, b) a / b end
  end
end
Calculator.multiply(2, 3)  # 6

# Singleton pattern
class Logger
  @instance = Logger.new
  class << self
    attr_reader :instance
  end
end
Logger.instance.equal?(Logger.instance)  # true

Refinements (областное monkey patching)

Refinements (Ruby 2.1+) позволяют областное monkey patching — добавляйте методы к существующим классам, но только там, где вы явно 'using' refinement. Это безопаснее глобального monkey patching (которое может сломать другой код). Refinements активируются по области (файл, класс, метод). Полезны для добавления удобных методов без загрязнения глобального пространства имён. Менее распространены, чем должны быть, из-за производительности и некоторых особенностей области, но это 'правильный' способ расширения базовых классов.

ruby
# Refinement: limited monkey patching
module StringRefinements
  refine String do
    def shout
      upcase + "!!!"
    end

    def palindrome?
      downcase == downcase.reverse
    end
  end
end

# Without using: shout is undefined
# "hello".shout  # NoMethodError

# With using: refinement is active in this scope
using StringRefinements
"hello".shout        # "HELLO!!!"
"racecar".palindrome?  # true

# Scoped to file or module
class App
  using StringRefinements
  def greet
    "hi".shout  # works here
  end
end
# "hi".shout  # NoMethodError (outside App)

# Safer than global monkey patching
# Refinements only affect code that opts in
19

Блоки, процы, лямбды: глубокое погружение

Основы блоков

Блоки — анонимные замыкания, передаваемые методам через { } или do...end. yield вызывает блок изнутри метода. Блоки могут принимать параметры через |var|. Метод может проверить, передан ли блок, через block_given?. Блоки — основа паттерна итераторов и DSL Ruby. Они захватывают переменные из охватывающей области (замыкания).

ruby
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n }      # Single-line
[1, 2, 3].each do |n|              # Multi-line
  puts n
end

# yield invokes the block
def greet
  print "Hello, "
  yield          # Calls the block
  puts "!"
end

greet { print "World" }   # Hello, World!

# yield with arguments
def calculate(a, b)
  yield(a + b)
  yield(a * b)
end

calculate(3, 4) { |result| puts "Result: #{result}" }

Процы и лямбды

Процы и лямбды — объекты, оборачивающие блоки, позволяющие хранить в переменных и передавать. Процы имеют мягкую проверку аргументов (лишние игнорируются) и возвращаются из охватывающего метода. Лямбды имеют строгую проверку аргументов и возвращаются только из себя. Используйте процы для гибкости (как методы, принимающие блоки), лямбды для анонимных функций с предсказуемым поведением. Синтаксис ->() {} (stabby lambda) лаконичен для однострочников.

ruby
# Proc: object wrapping a block
my_proc = Proc.new { |x| puts x * 2 }
my_proc.call(5)         # 10
my_proc.(5)             # 10 (shorthand)
my_proc[5]              # 10 (another shorthand)

# Lambda: stricter Proc
my_lambda = lambda { |x| puts x * 2 }
my_lambda = ->(x) { puts x * 2 }   # Stabby lambda

# Difference 1: argument checking
my_proc.call(1, 2, 3)    # OK (ignores extra args)
my_lambda.call(1, 2, 3)  # ArgumentError (wrong number of args)

# Difference 2: return behavior
def proc_test
  p = Proc.new { return 1 }
  p.call
  return 2  # Never reached
end

def lambda_test
  l = lambda { return 1 }
  l.call    # Returns 1 to the lambda, not the method
  return 2  # Reached
end

Объекты Method

Метод method() возвращает объект Method, оборачивающий существующий метод. Оператор & преобразует Method или Proc в блок (и наоборот). Symbol#to_proc преобразует :upcase в { |x| x.upcase }, обеспечивая лаконичный синтаксис &:symbol. Это идиоматичный Ruby для коротких блочных операций. Объекты Method привязаны к получателю, поэтому помнят self при передаче.

ruby
# Convert method to Proc with method()
class Calculator
  def add(a, b) a + b end
  def multiply(a, b) a * b end
end

calc = Calculator.new
add_proc = calc.method(:add)
puts add_proc.call(2, 3)   # 5

# Pass method as block with & operator
def apply_op(a, b, op)
  op.call(a, b)
end

puts apply_op(2, 3, calc.method(:add))       # 5
puts apply_op(2, 3, calc.method(:multiply))  # 6

# Symbol#to_proc
words = ["hello", "world"]
upcased = words.map(&:upcase)   # ["HELLO", "WORLD"]
# Equivalent to: words.map { |w| w.upcase }

# Common with &:map(&:to_i), select(&:positive?), sort_by(&:length)

Замыкания и переменные

Замыкания захватывают переменные по ссылке, не по значению. Несколько замыканий могут разделять состояние (пример счётчика). Блоко-локальные переменные (объявлены после ; в параметрах) затеняют внешние переменные без их изменения. Это обеспечивает функциональные паттерны вроде аккумуляторов, генераторов и мемоизации. Будьте осторожны: замыкания, удерживающие ссылки, могут вызвать утечки памяти, если не освобождены. Используйте этот паттерн для инкапсуляции приватного состояния.

ruby
# Blocks/Procs/Lambdas capture variables (closures)
counter = 0
increment = lambda { counter += 1 }

increment.call
increment.call
puts counter   # 2

# Multiple closures sharing state
def make_counters
  count = 0
  [
    lambda { count += 1 },
    lambda { count },
    lambda { count = 0 }
  ]
end

inc, get, reset = make_counters
inc.call; inc.call
puts get.call   # 2
reset.call
puts get.call   # 0

# Block-local variables (shadow outer)
x = 10
[1, 2, 3].each do |y; x|   # x is block-local
  x = y * 2
end
puts x   # 10 (unchanged)

Пользовательские итераторы

Включение Enumerable и реализация each даёт вашему классу все методы итератора (map, select, reduce, sort и т.д.) бесплатно. Передайте блок через &block и вызывайте его, или используйте yield. Оператор & преобразует блок в Proc и обратно. Это идиоматичный способ сделать пользовательские коллекции итерируемыми. Реализуйте each для прямой итерации; добавьте reverse_each для двунаправленной.

ruby
class Tree
  include Enumerable

  def initialize(value, children = [])
    @value = value
    @children = children
  end

  def each(&block)
    block.call(@value)
    @children.each { |child| child.each(&block) }
  end
end

tree = Tree.new(1, [
  Tree.new(2, [Tree.new(4), Tree.new(5)]),
  Tree.new(3)
])

tree.each { |v| puts v }            # 1 2 4 5 3
puts tree.map { |v| v * 2 }.inspect # [2, 4, 8, 10, 6]
puts tree.select(&:even?).inspect   # [2, 4]
puts tree.reduce(:+)                # 15

# Including Enumerable gives you map, select, reduce, etc.
20

Метапрограммирование

Динамические методы

method_missing перехватывает вызовы неопределённых методов, обеспечивая динамическую диспетчеризацию. Всегда переопределяйте respond_to_missing? соответственно. define_method создаёт методы во время выполнения, полезно для генерации похожих методов (как find_by_* в ActiveRecord). instance_variable_get/set обращаются к переменным экземпляра по имени. Используйте метапрограммирование умеренно — усложняет понимание и отладку кода. Предпочитайте явные определения, когда возможно.

ruby
class Person
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  # Method missing: catch undefined method calls
  def method_missing(name, *args, &block)
    if name.to_s =~ /^(.*)_with_prefix$/
      attr_name = $1
      "PREFIX_#{send(attr_name)}"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s =~ /^(.*)_with_prefix$/ || super
  end
end

p = Person.new("Alice", 30)
puts p.name_with_prefix   # PREFIX_Alice

# Define methods dynamically
class Person
  [:home, :work, :mobile].each do |type|
    define_method("#{type}_phone") do
      instance_variable_get("@#{type}_phone")
    end

    define_method("#{type}_phone=") do |value|
      instance_variable_set("@#{type}_phone", value)
    end
  end
end

Открытые классы и monkey patching

Классы Ruby открыты: можно добавлять методы к любому классу, включая встроенные вроде String. Это мощно, но опасно (monkey patching может сломать другой код). Refinements (Ruby 2.1+) обеспечивают областные monkey patches: применяются только в файлах/классах, использующих модуль. Предпочитайте refinements глобальным monkey patches для более безопасного метапрограммирования. Документируйте патчи чётко и избегайте изменения базового поведения.

ruby
# Reopen existing classes (monkey patching)
class String
  def shout
    upcase + "!"
  end

  def word_count
    split.size
  end
end

puts "hello world".shout       # HELLO WORLD!
puts "one two three".word_count # 3

# Refinements: scoped monkey patches
module ShoutRefinement
  refine String do
    def shout
      upcase + "!"
    end
  end
end

class MyClass
  using ShoutRefinement

  def greet(name)
    "hello #{name}".shout   # Works here
  end
end

# "test".shout  # NoMethodError (refinement not active)

Хуки и колбэки

Ruby предоставляет хуки жизненного цикла: inherited (создан подкласс), included (модуль включён), prepended (модуль prepend-нут), method_added (метод определён), method_removed, method_undefined. Они позволяют фреймворкам реагировать на изменения класса автоматически. ActiveRecord использует их для отслеживания атрибутов, Rails — для маршрутизации. Переопределяйте хуки как методы класса (self.inherited) для событий класса, методы экземпляра для событий методов.

ruby
class Observable
  # Hook: called when class is subclassed
  def self.inherited(subclass)
    puts "#{subclass} inherits from #{self}"
  end

  # Hook: called when a module is included
  def self.included(base)
    puts "#{self} included in #{base}"
  end

  # Hook: called when a method is added
  def method_added(name)
    puts "Added method: #{name}"
  end

  # Hook: called when method is undefined
  def method_removed(name)
    puts "Removed method: #{name}"
  end
end

class Child < Observable
  def my_method; end
end
# Output:
# Child inherits from Observable
# Added method: my_method

# Other hooks: method_undefined, extended, prepended

eval и Binding

eval выполняет строку как Ruby-код, опасно, если ввод ненадёжный (внедрение кода). binding захватывает текущий контекст выполнения (переменные, self) для последующего eval. class_eval выполняет код в контексте класса (определяет методы). instance_eval меняет self на получателя. Используйте для DSL и генерации кода, но избегайте eval пользовательского ввода. Предпочитайте блоки и define_method для динамического кода. Всегда очищайте ввод, если eval неизбежен.

ruby
# eval: execute a string as Ruby code
result = eval("1 + 2 * 3")
puts result   # 7

# eval with binding (context)
def eval_in_context(code)
  x = 10
  binding   # Returns a Binding object capturing local variables
end

b = eval_in_context("binding")
puts eval("x * 2", b)   # 20

# class_eval: execute code in class context
String.class_eval do
  def palindrome?
    self == reverse
  end
end

puts "racecar".palindrome?   # true

# instance_eval: execute in instance context
"hello".instance_eval do
  puts length   # 5 (self is the string)
end

# NEVER eval user input (security risk)

Рефлексия и интроспекция

Ruby предоставляет богатую рефлексию: methods перечисляет все методы, instance_variables перечисляет переменные экземпляра, ancestors показывает цепочку наследования. parameters раскрывает имена и типы параметров метода. respond_to? проверяет, отвечает ли объект на метод. Используйте рефлексию для отладки, сериализации (инспекция атрибутов) и построения универсальных инструментов (как ORM). Избегайте избыточной рефлексии — обходит типобезопасность и усложняет понимание кода.

ruby
class User
  attr_accessor :name, :email

  def initialize(name, email)
    @name = name
    @email = email
  end

  def save; end
  def valid?; true; end
end

u = User.new("Alice", "[email protected]")

# Inspect methods
puts u.methods - Object.methods    # [:name, :email, :save, :valid?, ...]
puts u.public_methods(false)       # Only this class's methods
puts User.instance_methods(false)  # [:save, :valid?, ...]

# Inspect variables
puts u.instance_variables.inspect   # [:@name, :@email]
puts u.instance_variable_get(:@name) # Alice

# Inspect class
puts User.ancestors.inspect
puts User.instance_method(:save).parameters

# Check if responds to method
puts u.respond_to?(:save)   # true
puts u.is_a?(User)          # true
21

Тестирование (RSpec/Minitest)

Основы RSpec

RSpec — BDD-фреймворк тестирования. describe группирует связанные тесты, context группирует по условию, it определяет один тест. let создаёт ленивые мемоизированные переменные. expect(...).to matcher — синтаксис утверждений. Распространённые матчеры: eq, be_valid, include, raise_error. subject + is_expected уменьшает шаблонность. Запуск через rspec (все) или rspec path/to/spec (конкретный). Используйте фабрики (FactoryBot) вместо фикстур для тестовых данных.

ruby
# spec/spec_helper.rb
require 'rspec'
require_relative '../lib/my_app'

# spec/models/user_spec.rb
require 'spec_helper'

RSpec.describe User 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
  end

  describe '#valid?' do
    context 'with valid attributes' do
      it 'is valid' do
        expect(user).to be_valid
      end
    end

    context 'without name' do
      let(:user) { User.new(email: '[email protected]') }
      it 'is invalid' do
        expect(user).not_to be_valid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end
  end

  # Multiple examples
  describe '#age' do
    subject { user.age }
    it { is_expected.to be >= 0 }
  end
end

# Run: rspec spec/models/user_spec.rb
# Run all: rspec

Моки и заглушки RSpec

Заглушки (allow) заменяют возвращаемые значения методов; моки (expect) проверяют, что метод был вызван. double создаёт тестовый двойник (фейковый объект). and_return устанавливает возвращаемые значения, and_raise симулирует ошибки, with устанавливает ожидаемые аргументы. Используйте моки умеренно — избыточное мокирование делает тесты хрупкими. Тестируйте поведение, а не реализацию. Предпочитайте реальные объекты, когда возможно; используйте моки для внешних сервисов (API, email, платёжные шлюзы).

ruby
RSpec.describe PaymentProcessor do
  let(:gateway) { double('PaymentGateway') }
  let(:processor) { PaymentProcessor.new(gateway) }

  describe '#charge' do
    it 'charges the gateway' do
      # Stub: replace method return value
      allow(gateway).to receive(:charge).and_return(success: true)

      result = processor.charge(100)
      expect(result[:success]).to be true
    end

    it 'raises on gateway error' do
      allow(gateway).to receive(:charge)
        .and_raise(GatewayError, 'Network failure')

      expect { processor.charge(100) }
        .to raise_error(GatewayError, /Network/)
    end

    it 'verifies the gateway was called' do
      # Mock: expect method to be called
      expect(gateway).to receive(:charge).with(100)
      processor.charge(100)
    end

    it 'receives multiple calls' do
      allow(gateway).to receive(:charge).and_return(true, false, true)
      expect(gateway.charge(1)).to be true
      expect(gateway.charge(2)).to be false
    end
  end
end

Minitest

Minitest — библиотека тестирования Ruby по умолчанию (поставляется с Ruby). Поддерживает как Unit-стиль (assert/refute), так и Spec-стиль (must_equal). setup запускается перед каждым тестом. Утверждения: assert_equal, assert_nil, assert_raises, assert_includes. Minitest быстрее RSpec и имеет меньше зависимостей. Используйте Minitest::Mock для мокирования или гем Mocha для большего функционала. Rails использует Minitest по умолчанию (ActiveSupport::TestCase).

ruby
# test/test_helper.rb
require 'minitest/autorun'
require_relative '../lib/my_app'

# test/models/user_test.rb
require 'test_helper'

class UserTest < Minitest::Test
  def setup
    @user = User.new(name: 'Alice', email: '[email protected]')
  end

  def test_name
    assert_equal 'Alice', @user.name
  end

  def test_valid_with_attributes
    assert @user.valid?
  end

  def test_invalid_without_name
    @user.name = nil
    refute @user.valid?
    assert_includes @user.errors[:name], "can't be blank"
  end

  def test_raises_on_error
    assert_raises(ArgumentError) { User.new! }
  end
end

# Spec-style Minitest
class UserSpec < Minitest::Spec
  let(:user) { User.new(name: 'Alice') }

  it 'has a name' do
    _(user.name).must_equal 'Alice'
  end
end

# Run: rake test
# Run specific: ruby -Itest test/models/user_test.rb

Тестовые данные и фабрики

FactoryBot генерирует тестовые объекты со значениями по умолчанию, traits для вариаций и ассоциаций. create персистит в БД; build не делает. Traits компонуются (factory :admin_with_posts, traits: [:admin, :with_posts]). Фабрики гибче фикстур, но медленнее. Фикстуры (YAML) быстрее, но менее гибки. Выбирайте по потребностям: фикстуры для простых данных, фабрики для сложных связей. Избегайте фабрик со слишком многими traits — держите их сфокусированными.

ruby
# FactoryBot (gem 'factory_bot')
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'Alice' }
    email { '[email protected]' }
    age { 30 }

    trait :admin do
      role { 'admin' }
    end

    trait :with_posts do
      after(:create) do |user|
        create_list(:post, 3, user: user)
      end
    end

    factory :admin_user, traits: [:admin]
    factory :user_with_posts, traits: [:with_posts]
  end
end

# Usage in specs
let(:user) { create(:user) }            # Saved to DB
let(:admin) { create(:admin_user) }
let(:user_with_posts) { create(:user_with_posts) }
let(:unsaved_user) { build(:user) }     # Not saved

# Fixtures (alternative)
# test/fixtures/users.yml
# alice:
#   name: Alice
#   email: [email protected]

# In tests: users(:alice)

Интеграционные и системные тесты

Request-спеки тестируют весь стек (маршрутизация, контроллеры, модели) через HTTP. System-спеки (Capybara) управляют реальным браузером, тестируя JavaScript-взаимодействия. Используйте request-спеки для API-эндпоинтов, system-спеки для пользовательских потоков. have_http_status проверяет коды ответа. visit/fill_in/click_button симулируют действия пользователя. Системные тесты медленнее, но ловят интеграционные ошибки. Запускайте системные тесты с js: true для JavaScript-управляемых страниц.

ruby
# spec/requests/users_spec.rb (integration)
require 'spec_helper'

RSpec.describe 'Users API', type: :request do
  describe 'GET /users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/users', as: :json
      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', email: '[email protected]' } }
      expect(response).to have_http_status(201)
      expect(User.last.name).to eq('Bob')
    end
  end
end

# System tests (browser automation)
# spec/system/login_spec.rb
require 'spec_helper'

RSpec.describe 'Login', type: :system do
  it 'logs in a user' do
    user = create(:user, password: 'password123')
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password123'
    click_button 'Log In'
    expect(page).to have_content('Welcome')
  end
end
22

Потоки и файберы

Основы потоков

Потоки в Ruby (MRI) — зелёные потоки, планируемые VM — они не выполняются в истинном параллелизме из-за GIL (Global Interpreter Lock). Однако потоки полезны для I/O-параллелизма (сеть, файловые операции). join ждёт завершения потока. value извлекает возвращаемое значение. Для CPU-bound параллелизма используйте несколько процессов (fork, Sidekiq) или JRuby (без GIL). Всегда присоединяйте потоки, чтобы избежать их удаления при выходе основного потока.

ruby
# Create and run threads
threads = (1..3).map do |i|
  Thread.new(i) do |n|
    puts "Thread #{n} started"
    sleep(1)
    puts "Thread #{n} finished"
  end
end

# Wait for all threads
threads.each(&:join)
puts "All done"

# Thread with return value
thread = Thread.new { 1 + 2 }
result = thread.value   # 3 (waits for completion)

# Thread status
thread = Thread.new { sleep(1) }
puts thread.status   # 'run', 'sleep', false (finished), nil (error)
thread.join

# Current thread
Thread.current
Thread.list   # All threads
Thread.main   # Main thread

Синхронизация потоков

Mutex.synchronize гарантирует, что только один поток выполняет блок за раз, предотвращая гонки. Queue — потокобезопасная FIFO: производители push, потребители pop (блокируется, если пусто). ConditionVariable координирует потоки: wait освобождает mutex и спит, signal/broadcast будит ожидающие потоки. Всегда используйте синхронизацию для разделяемого изменяемого состояния. Взаимоблокировки возникают, когда потоки ждут друг друга — приобретайте блокировки в согласованном порядке.

ruby
# Mutex: mutual exclusion
counter = 0
mutex = Mutex.new

threads = 10.times.map do
  Thread.new do
    1000.times do
      mutex.synchronize { counter += 1 }
    end
  end
end
threads.each(&:join)
puts counter   # 10000 (without mutex, would be less)

# Queue: thread-safe FIFO
require 'thread'
queue = Queue.new

producer = Thread.new do
  5.times { |i| queue << "item #{i}" }
  queue << :done
end

consumer = Thread.new do
  loop do
    item = queue.pop
    break if item == :done
    puts "Processed: #{item}"
  end
end

[producer, consumer].each(&:join)

# ConditionVariable: signal between threads
mutex = Mutex.new
cv = ConditionVariable.new

Файберы

Файберы — кооперативные лёгкие потоки: они уступают управление вручную вместо вытеснения. resume запускает/возобновляет файбер; Fiber.yield приостанавливает его и возвращает значение. Файберы полезны для генераторов, ленивых вычислений и конечных автоматов разбора. В отличие от потоков, только один файбер выполняется за раз, поэтому синхронизация не нужна. Файберы дешевле потоков, но не могут использовать несколько ядер. Используйте их для I/O-мультиплексирования (EventMachine, Async).

ruby
# Fiber: cooperative concurrency (manual scheduling)
fiber = Fiber.new do
  puts "Fiber started"
  Fiber.yield "first yield"
  puts "Fiber resumed"
  Fiber.yield "second yield"
  puts "Fiber ending"
  "fiber done"
end

puts fiber.resume   # "Fiber started", returns "first yield"
puts fiber.resume   # "Fiber resumed", returns "second yield"
puts fiber.resume   # "Fiber ending", returns "fiber done"

# Generator pattern with Fiber
def fibonacci
  Fiber.new do
    a, b = 0, 1
    loop do
      Fiber.yield a
      a, b = b, a + b
    end
  end
end

fib = fibonacci
10.times { print fib.resume, " " }
# 0 1 1 2 3 5 8 13 21 34

Async и параллельный Ruby

Гем async предоставляет современный асинхронный I/O, используя файберы под капотом, обеспечивая высоко-конкурентный сетевой код. concurrent-ruby предоставляет потокобезопасные абстракции: Future (асинхронный результат), Promise (компонуемый), пулы потоков и атомарные переменные. Используйте async для I/O-bound работы (HTTP, БД), пулы потоков для CPU-bound работы. Всегда останавливайте пулы во избежание зависания. Эти гемы обходят GIL MRI для практического параллелизма.

ruby
# Async gem for modern async I/O
# gem install async
require 'async'
require 'async/http/internet'

Async do
  internet = Async::HTTP::Internet.new

  # Run requests concurrently
  tasks = 3.times.map do |i|
    Async do
      response = internet.get("https://httpbin.org/delay/#{i}")
      puts "Request #{i}: #{response.status}"
    end
  end

  tasks.each(&:wait)
end

# Concurrent Ruby (gem 'concurrent-ruby')
require 'concurrent'

# Future: async computation
future = Concurrent::Future.execute { sleep(1); 42 }
puts future.value   # 42 (blocks until ready)

# Promise chain
Concurrent::Promise.execute { 1 }
  .then { |v| v + 1 }
  .then { |v| v * 2 }
  .then { |v| puts v }   # 4

# Thread pool
pool = Concurrent::FixedThreadPool.new(4)
10.times do |i|
  pool.post { puts "Task #{i} on #{Thread.current.object_id}" }
end
pool.shutdown

Ractors (Ruby 3.0+)

Ractors (Ruby 3.0+) обеспечивают истинный параллелизм, избегая GIL. Каждый Ractor имеет свою кучу, поэтому нет гонок данных. Общение через сообщения (send/receive, take). Отправляемые объекты копируются (или перемещаются через Ractor.move) для поддержания изоляции. Замороженные объекты можно разделять. Ractors — будущее параллельного Ruby, но имеют ограничения: большинство гемов ещё не Ractor-безопасны. Используйте их для CPU-bound параллельных вычислений, где изоляция приемлема.

ruby
# Ractor: true parallelism (no GIL)
# Ruby 3.0+ feature for thread-safe parallel execution

ractor = Ractor.new do
  value = receive    # Receive from main
  value * 2
end

ractor.send(21)
puts ractor.take     # 42

# Parallel computation
ractors = (1..4).map do |i|
  Ractor.new(i) do |n|
    sleep(1)
    n * n
  end
end

results = Ractor.select(*ractors)
# Or: ractors.map(&:take)  # [1, 4, 9, 16]

# Ractor restrictions:
# - Cannot share mutable objects
# - Communication only via messages
# - Copy or move semantics for sending

# Shared frozen objects are OK
SHARED = Ractor.new { [1, 2, 3].freeze }
# Multiple ractors can read SHARED safely
23

Кодировки и IO

Кодировка строк

У каждой Ruby-строки есть кодировка (по умолчанию UTF-8). bytesize — количество байтов; length — количество символов (отличается для многобайтовых кодировок). force_encoding меняет тег кодировки без преобразования байтов (используйте, когда знаете фактическую кодировку). encode преобразует между кодировками. Всегда проверяйте valid_encoding? перед обработкой внешних данных. Установите кодировку исходного файла магическим комментарием # encoding: utf-8 (хотя UTF-8 по умолчанию в Ruby 2.0+).

ruby
# Ruby strings have encodings
s = "hello"
puts s.encoding              # <Encoding:UTF-8>
puts s.bytesize              # 5
puts s.length               # 5

# Multibyte characters
japanese = "こんにちは"
puts japanese.encoding       # UTF-8
puts japanese.bytesize       # 15 (3 bytes per char)
puts japanese.length         # 5 (5 characters)

# Force encoding (interpret bytes differently)
bytes = "caf\u00e9".force_encoding('ASCII-8BIT')
puts bytes.encoding          # ASCII-8BIT

# Encode (convert to different encoding)
utf8 = "café"
latin1 = utf8.encode('ISO-8859-1')
puts latin1.encoding         # ISO-8859-1

# Invalid bytes
bad = "\xFF\xFE".force_encoding('UTF-8')
puts bad.valid_encoding?     # false
fixed = bad.encode('UTF-8', invalid: :replace, replace: '?')

Файловый IO

File.read загружает весь файл в память; File.foreach читает построчно (эффективно по памяти для больших файлов). File.write перезаписывает; режим 'a' добавляет; 'r+' читает и пишет. Бинарный режим ('rb', 'wb') предотвращает преобразование кодировки. Всегда используйте блочную форму (File.open), чтобы гарантировать закрытие файла. FileUtils предоставляет операции более высокого уровня (cp, mv, mkdir_p). Проверяйте существование через File.exist? перед чтением.

ruby
# Read entire file
content = File.read('data.txt')

# Read line by line (memory efficient)
File.foreach('large.log') do |line|
  puts line.chomp
end

# Write to file
File.write('output.txt', 'Hello, World!')

# Append
File.open('log.txt', 'a') do |f|
  f.puts "New log entry"
end

# Read/Write modes
File.open('file.txt', 'r+') do |f|
  content = f.read
  f.rewind
  f.write("Updated: " + content)
end

# Binary mode
File.open('image.png', 'rb') do |f|
  bytes = f.read
  puts bytes.bytesize
end

# File operations
File.exist?('file.txt')
File.size('file.txt')
File.mtime('file.txt')
File.delete('file.txt')
File.rename('old.txt', 'new.txt')
FileUtils.cp('a.txt', 'b.txt')   # require 'fileutils'

StringIO и Tempfile

StringIO оборачивает строку в IO-подобный интерфейс, полезно для тестирования файловых операций без диска. Tempfile создаёт временные файлы, автоматически удаляемые при выходе из блока (Tempfile.create) или при unlink (Tempfile.new). Используйте Tempfile для больших данных, не помещающихся в память, или для передачи внешним программам через путь. Всегда обеспечивайте очистку через блоки ensure. StringIO отлично подходит для модульных тестов IO-кода.

ruby
require 'stringio'
require 'tempfile'

# StringIO: in-memory file-like object
io = StringIO.new
io.puts "Hello"
io.puts "World"
io.rewind
puts io.read   # "Hello\nWorld\n"

# Use like a file
io = StringIO.new("line1\nline2\nline3")
io.each_line { |line| puts line.chomp }

# Tempfile: auto-deleted temporary file
Tempfile.create('myapp') do |f|
  f.write('temporary data')
  f.rewind
  puts f.read
end   # File automatically deleted

# Tempfile with explicit cleanup
temp = Tempfile.new('myapp')
begin
  temp.write('data')
  temp.rewind
  # Use temp.path to pass to external programs
ensure
  temp.close
  temp.unlink
end

Сетевой IO

TCPSocket/TCPServer предоставляют низкоуровневый TCP-доступ. Net::HTTP — стандартный HTTP-клиент (встроен). Для сложных HTTP-нужд (сессии, cookies, повторы) используйте гемы httparty или faraday. Всегда устанавливайте use_ssl = true для HTTPS. Для высокопроизводительного HTTP рассмотрите async-http или typhoeus. URI безопасно разбирает URL. Обрабатывайте Net::ReadTimeout и Errno::ECONNREFUSED для надёжного сетевого кода.

ruby
require 'socket'
require 'net/http'
require 'uri'

# TCP Client
TCPSocket.open('example.com', 80) do |socket|
  socket.write("GET / HTTP/1.0\r\n\r\n")
  puts socket.read
end

# TCP Server
server = TCPServer.new(2000)
loop do
  client = server.accept
  client.puts "Hello from server"
  client.close
end

# HTTP Client
uri = URI('https://api.example.com/data')
response = Net::HTTP.get_response(uri)
puts response.code          # 200
puts response.body

# HTTP POST
response = Net::HTTP.post_form(uri, key: 'value')

# HTTP with custom headers
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer token'
response = http.request(request)

STDIO и конвейеры

$stdout, $stderr, $stdin — глобальные IO-объекты (используйте константы STDOUT/STDERR/STDIN для оригиналов). Перенаправляйте через переназначение глобалей. StringIO захватывает вывод для тестирования. Open3.popen3 даёт полный контроль над stdin/stdout/stderr подпроцесса. Open3.pipeline компонует команды как shell-пайп. Capture3 возвращает stdout, stderr и статус. Всегда закрывайте stdin для сигнализации EOF подпроцессу. Используйте wait.value для получения статуса выхода.

ruby
# Standard IO
$stdout.puts "To stdout"
$stderr.puts "To stderr"
input = $stdin.gets   # Read from stdin

# Redirect IO
$stdout = File.open('log.txt', 'w')
puts "This goes to file"
$stdout = $stdout   # Restore (or use STDOUT constant)

# Capture output
captured = StringIO.new
original = $stdout
$stdout = captured
puts "Captured"
$stdout = original
puts captured.string   # "Captured\n"

# Open3: run commands with full IO control
require 'open3'

Open3.popen3('grep hello') do |stdin, stdout, stderr, wait|
  stdin.puts "hello world"
  stdin.puts "goodbye"
  stdin.close
  puts stdout.read      # "hello world"
  puts wait.value.exitstatus
end

# Pipeline
output = Open3.pipeline('ls', 'grep .rb', 'wc -l')
24

Сопоставление шаблонов (3.0+)

Базовое сопоставление шаблонов

Сопоставление шаблонов (ключевое слово in) деструктурирует массивы и хеши, связывая переменные. => связывает совпавшие значения с переменными. *rest захватывает оставшиеся элементы массива. Сопоставление хешей частичное: лишние ключи игнорируются. | совпадает с несколькими шаблонами. case/in — основная форма; однострочная форма — expression in pattern. Сопоставление шаблонов особенно мощно для разбора структурированных данных (JSON, AST) и замены сложных if-else цепочек.

ruby
# in: pattern matching (Ruby 2.7+, stable in 3.0)
case [1, 2, 3]
in [1, *rest]
  puts "Starts with 1, rest: #{rest}"
end

case {name: 'Alice', age: 30}
in {name: String => name, age: Integer => age}
  puts "#{name} is #{age}"
end

# Multiple patterns with |
case status
in :success | :ok
  puts "Good"
else
  puts "Other"
end

# Array patterns
case [1, 2, 3, 4]
in [_, _, *rest]
  puts "Rest: #{rest}"   # [3, 4]
end

# Hash patterns (partial match by default)
case {a: 1, b: 2, c: 3}
in {a: Integer}
  puts "Has a"   # Matches (ignores b, c)
end

Привязка переменных и Guards

=> связывает совпавшие значения с переменными. Оператор pin (^var) совпадает с текущим значением переменной (не связывание). Guards (if/unless) добавляют условия. Шаблоны массивов поддерживают * для splat в любой позиции. Find-шаблон [*, target, *] ищет элемент в массиве. Сопоставление шаблонов декларативно и лаконично для сложного извлечения данных. Переменные, связанные в шаблонах, доступны после оператора case.

ruby
user = {name: 'Alice', age: 30, role: :admin}

case user
in {name: String => name, age: Integer => age, role: :admin} if age > 18
  puts "#{name} is an adult admin"
end

# Pin operator (^): match against variable value
expected = 'Alice'
case user
in {name: ^expected}
  puts "Matched expected name"
end

# Array destructuring with binding
case [1, 2, 3]
in [first, *middle, last]
  puts "First: #{first}, Middle: #{middle}, Last: #{last}"
end

# Find pattern (search within array)
case [1, 2, 3, 4, 5]
in [*, 3 => three, *]
  puts "Found 3: #{three}"
end

Шаблоны классов и типов

Шаблоны классов совпадают по типу. Пользовательские классы поддерживают деструктуризацию через реализацию deconstruct (шаблон массива) и deconstruct_keys (шаблон хеша). Шаблон Class[args] использует deconstruct; Class(key:) использует deconstruct_keys. Многие встроенные классы (Time, Date, MatchData) поддерживают деструктуризацию. Это обеспечивает выразительное сопоставление доменных объектов. Реализуйте эти методы, чтобы сделать ваши классы pattern-matchable.

ruby
# Match by class
case value
in Integer
  puts "Integer"
in String
  puts "String"
in Array
  puts "Array"
end

# Class with destructuring
class Point
  attr_reader :x, :y
  def initialize(x, y) @x, @y = x, y end
  def deconstruct = [@x, @y]              # Array pattern
  def deconstruct_keys(keys) = {x: @x, y: @y}  # Hash pattern
end

p = Point.new(1, 2)
case p
in Point[x, y]
  puts "Point at (#{x}, #{y})"
end

case p
in Point(x:, y:)
  puts "X: #{x}, Y: #{y}"
end

# Built-in classes support destructuring
case Time.now
in Time(hour: h) if h < 12
  puts "Morning"
end

Однострочное сопоставление шаблонов

Однострочная форма (expr in pattern) возвращает true/false вместо вызова NoMatchingPatternError. Она связывает переменные при успешном совпадении. Полезно для guard-условий и условного извлечения. Правая форма (pattern => var) связывает всё совпадение. Однострочное сопоставление лаконично для простых случаев; используйте case/in для сложной много-ветвной логики. Переменные, связанные в неудачных совпадениях, остаются nil.

ruby
# One-line form (Ruby 3.0+)
{status: :ok, data: 42} in {status: :ok, data: Integer => data}
puts data   # 42

# Returns true/false (does not raise)
[1, 2, 3] in [Integer, Integer, Integer]   # true
[1, 2, 3] in [String, *, *]                # false

# Useful for conditionals
if user in {role: :admin}
  puts "Admin access"
end

# Guard clauses
def process(data)
  return unless data in {type: String, value: Integer}
  # ...
end

# Extracting from JSON
json = JSON.parse('{"user":{"name":"Alice","age":30}}')
json in {"user" => {"name" => String => name, "age" => Integer => age}}
puts "#{name}, #{age}"   # Alice, 30

Практические случаи использования

Сопоставление шаблонов отлично в: разборе структурированных данных (JSON, XML), конечных автоматах, обходе AST и диспетчеризации по форме данных. Оно заменяет многословные if-else цепочки декларативными шаблонами. Предложение else обрабатывает неожиданные форматы. Комбинируйте с guards для сложных условий. Сопоставление шаблонов делает код более читаемым и поддерживаемым для сложной обработки данных. Это одна из самых мощных функций Ruby 3 для чистого, выразительного кода.

ruby
# 1. Parse JSON API responses
def handle_response(response)
  case JSON.parse(response)
  in {"status" => "success", "data" => Array => items}
    items.each { |item| process(item) }
  in {"status" => "error", "message" => String => msg}
    raise "API error: #{msg}"
  in {"status" => "error", "code" => Integer => code} if code >= 500
    retry_request
  else
    raise "Unknown response format"
  end
end

# 2. State machines
case state
in :idle, event: :start
  transition_to :running
in :running, event: :pause
  transition_to :paused
in :paused, event: :resume
  transition_to :running
in :running | :paused, event: :stop
  transition_to :idle
end

# 3. AST traversal
def evaluate(node)
  case node
  in {type: 'number', value: n}
    n
  in {type: 'add', left:, right:}
    evaluate(left) + evaluate(right)
  in {type: 'mul', left:, right:}
    evaluate(left) * evaluate(right)
  end
end
25

Метапрограммирование

send и define_method

define_method создаёт методы динамически. send вызывает методы по имени (даже private). public_send уважает видимость. Полезно для DSL и уменьшения шаблонности. Будьте осторожны с пользовательским вводом для избежания проблем безопасности. Метапрограммирование мощно, но может усложнить понимание кода.

ruby
class Foo
  define_method(:greet) do |name|
    "Hello, #{name}"
  end
end
Foo.new.greet("Alice")  # "Hello, Alice"
# Dynamic method call
Foo.new.send(:greet, "Bob")  # "Hello, Bob"
Foo.new.public_send(:greet, "Bob")  # Respects visibility

method_missing

method_missing перехватывает вызовы неопределённых методов. Полезно для динамической диспетчеризации и DSL. Всегда переопределяйте respond_to_missing? соответственно. Может быть медленным и скрывать ошибки. Предпочитайте define_method, когда множество методов известно. ActiveRecord использует это для аксессоров атрибутов.

ruby
class DynamicHash
  def method_missing(name, *args)
    if name.to_s.end_with?('=')
      self[name.to_s.chomp('=')] = args.first
    else
      self[name.to_s]
    end
  end
  def respond_to_missing?(name, include_private = false)
    true
  end
end

eval

eval выполняет строку как Ruby-код. Крайне мощно, но опасно. Никогда не eval-ите ненадёжный ввод (внедрение кода). Используйте binding.eval для конкретного контекста. Для безопасного вычисления используйте парсер вроде Ripper или песочницу. У большинства случаев есть более безопасные альтернативы.

ruby
# Execute a string as Ruby code
result = eval("1 + 2 * 3")  # 7
# Dynamic method definition
eval("def dynamic_method; 42; end")
dynamic_method  # 42
# WARNING: never eval untrusted input!

Открытые классы

Классы Ruby открыты: можно добавлять методы к любому классу, включая встроенные. Это называется monkey patching. Мощно для быстрых исправлений, но может вызвать конфликты и путаницу. Используйте refinements для областных модификаций. Документируйте изменения чётко. Предпочитайте композицию monkey patching-у.

ruby
class String
  def shout
    upcase + "!"
  end
end
"hello".shout  # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflicts

Макросы классов

Макросы классов — методы класса, определяющие другие методы. has_many, belongs_to, attr_accessor — примеры. Они используют define_method внутренне. Так Rails создаёт динамические методы. DSL вроде RSpec и Sinatra используют этот паттерн. Делает код декларативным и читаемым.

ruby
class ActiveRecord::Base
  def self.has_many(name)
    define_method(name) { [] }
  end
end
class Post < ActiveRecord::Base
  has_many :comments
end
Post.new.comments  # []

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.