Перейти к основному содержимому

Типы данных в Ruby

Разработчику Архитектору

О чём эта статья

В Ruby тип живёт у значения, а не у имени переменной. Префиксы @, @@, $, константы, nil/false, массивы и хэши, проверки class / is_a?.

Дальше: итераторы и case · Справочник Ruby · таблицы Операции для Array / Hashниже


Переменные в Ruby

Переменная в Ruby — это именованная ссылка на объект в памяти. Переменная не содержит значение напрямую: она хранит ссылку (reference) на объект. Это принципиально важно, поскольку все операции с переменными — это операции с ссылками на объекты, а не с самими объектами (за исключением случаев, когда объект является непосредственно значением примитива, но даже тогда в Ruby примитивы инкапсулированы в объекты).

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

Имя переменной в Ruby — это последовательность символов, начинающаяся с определённой сигнатуры, которая определяет её область видимости:

  • Локальные переменные начинаются со строчной буквы или символа подчёркивания — counter, _temp, value. Их область видимости ограничена текущим блоком, методом или классом (в зависимости от контекста объявления). Локальные переменные не видны за пределами своей лексической области.
  • Глобальные переменные начинаются со знака доллара: $stdout, $LOAD_PATH. Такие переменные доступны из любого места программы, включая другие классы и модули. Их использование считается нежелательным в большинстве случаев, поскольку нарушает инкапсуляцию и затрудняет отладку.
  • Переменные экземпляра начинаются с одного символа @: @name, @balance. Принадлежат конкретному объекту (экземпляру класса) и сохраняют своё значение между вызовами методов этого объекта. Не видны вне объекта, если не предоставлены соответствующие методы доступа (attr_reader, attr_accessor и т.п.).
  • Переменные класса начинаются с двух символов @@: @@counter, @@default_options. Принадлежат классу как объекту, а не его экземплярам. Все экземпляры класса и его подклассов разделяют одну и ту же переменную класса. Поведение при наследовании требует осторожности: изменение переменной класса в подклассе может повлиять на родительский класс в некоторых реализациях, хотя современные версии Ruby стараются изолировать состояния.

Важно подчеркнуть: все перечисленные сущности — переменные — являются ссылками. При присваивании переменной нового значения предыдущая ссылка удаляется (и, при отсутствии других ссылок на тот же объект, объект может быть удалён сборщиком мусора), а переменная начинает ссылаться на новый объект. При копировании переменной (a = b) создаётся новая ссылка на тот же самый объект, а не копия объекта. Это ключевой момент, влияющий на поведение при изменении изменяемых объектов (например, массивов или хэшей).


Константы в Ruby

Константа — это именованная ссылка на объект, предназначенная для хранения неизменяемого (по замыслу) значения. Имя константы начинается с прописной буквы — MAX_RETRY_COUNT, DEFAULT_TIMEOUT, HTTP_CODES. По соглашению, имена констант пишутся заглавными буквами с подчёркиваниями (SCREAMING_SNAKE_CASE), хотя формально это не требуется — достаточно первой заглавной буквы (Foo, BarBaz — тоже константы).

Важно: в Ruby константы не являются неизменяемыми в смысле языков с const или final. Присваивание нового значения константе, уже инициализированной, вызывает предупреждение уровня warning: already initialized constant, но не приводит к ошибке времени выполнения. Это — особенность, которую необходимо учитывать: константы в Ruby — это скорее соглашение о неизменности, чем языковая гарантия.

Константы имеют лексическую область видимости, как и локальные переменные, но с дополнительной иерархией: они могут быть определены на уровне класса (class A; PI = 3.14159; end), модуля (module Math; E = 2.71828; end) или даже в глобальном пространстве имён (хотя это не рекомендуется). Обращение к константе может быть квалифицировано: Math::PI, Kernel::ARGV.

С точки зрения объектной модели, константа — это способ связывания имени с объектом, при котором интерпретатор отслеживает факт повторной инициализации. Сам объект, на который ссылается константа, может быть изменяемым (DEFAULT_SETTINGS = { timeout: 5 }), и его содержимое можно изменять без предупреждений — предупреждение будет только при замене самой ссылки.


Объявление и присваивание переменных

В Ruby отсутствует отдельная операция объявления переменной. Переменная считается объявленной в тот момент, когда ей впервые присваивается значение. До этого момента попытка чтения неинициализированной локальной переменной приведёт к ошибке NameError (если имя начинается с @, @@ или $, поведение отличается — см. ниже).

Присваивание выполняется оператором =. Это привязка имени к объекту. Пример:

x = 42
y = x

Разбор:

  • x = 42 привязывает имя x к объекту Integer.
  • y = x копирует ссылку на тот же объект, а не делает глубокую копию.
  • Для неизменяемых чисел это обычно безопасно, но для изменяемых структур важно помнить про общее состояние.

Здесь x и y — две разные локальные переменные, но они ссылаются на один и тот же объект — целое число 42. Изменение y (например, y = 100) не повлияет на x, поскольку операция присваивания лишь заменяет ссылку в y, оставляя x указывать на прежний объект.

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

  • Неинициализированная переменная экземпляра возвращает nil.
  • Неинициализированная переменная класса также возвращает nil.
  • Неинициализированная глобальная переменная возвращает nil.

Так сделано для удобства, чтобы не требовать обязательной инициализации в каждом конструкторе. В рабочих проектах лучше инициализировать поля явно — так код читается быстрее и ведёт себя предсказуемее.

Ruby поддерживает множественное присваивание, позволяющее инициализировать несколько переменных одновременно:

a, b = 1, 2
c, d, e = [3, 4, 5]
name, age = "Alice", 30

Разбор:

  • Это параллельное присваивание: несколько переменных инициализируются за одну операцию.
  • Правая часть вычисляется полностью до начала присваивания.
  • Ruby распаковывает массив или список значений в переменные слева по порядку.
  • Если значений меньше, недостающие переменные получают nil; лишние значения отбрасываются.

Здесь справа от = может находиться массив, диапазон, или даже результат вызова метода, возвращающего несколько значений. При несоответствии количества элементов "лишние" переменные получат значение nil, "лишние" значения — будут проигнорированы. Также поддерживается синтаксис параллельного присваивания (например, a, b = b, a для обмена значениями), которое гарантирует атомарность: правая часть полностью вычисляется до присваивания левой.


Типы данных

В Ruby отсутствует деление на примитивные и ссылочные типы в том виде, в каком оно принято, например, в Java или C#. Вместо этого — единая объектная модель — каждое значение есть объект, принадлежащий определённому классу, и каждый класс, в свою очередь, наследуется (прямо или косвенно) от корневого класса Object, а тот — от BasicObject (минимальный набор методов, введён для изоляции при создании DSL и метапрограммирования).

Тип значения в Ruby определяется динамически — во время выполнения — и может быть установлен с помощью метода #class, #is_a?, #kind_of? или #instance_of?. Однако это — запрос к объекту: какому классу ты принадлежишь?, являешься ли ты экземпляром данного класса или его подкласса? и так далее. Таким образом, "тип данных" в Ruby — это, строго говоря, класс объекта.

Все встроенные типы данных в Ruby реализованы как классы в стандартной библиотеке. Ниже рассмотрены основные категории типов, их назначение, поведение и отличительные особенности.


Числовые типы

Числовые данные в Ruby организованы в иерархию, корнем которой является класс Numeric. От него наследуются:

  • Integer — целые числа произвольной точности (ранее делился на Fixnum и Bignum, но с Ruby 2.4 объединён в единый класс Integer). Это означает, что ограничения вроде -2^31…2^31−1 отсутствуют: целое число может иметь сколь угодно много разрядов, ограниченное лишь доступной памятью. Арифметические операции (+, -, *, /, %, **) для целых чисел возвращают целый результат (деление / усекает к нулю: 7 / 3 == 2, -7 / 3 == -2). Деление с плавающей точкой требует явного приведения хотя бы одного операнда к Float.

  • Float — числа с плавающей точкой двойной точности (64-битный IEEE 754). Представляет приближённые вещественные значения. Обладает всеми стандартными ограничениями, присущими формату — конечная точность, наличие специальных значений Infinity, -Infinity, NaN. Проверка равенства (==) с NaN всегда возвращает false, даже NaN == NaN ложно; для проверки используется nan?.

  • Rational — рациональные числа (дроби вида p/q, где p и q — целые, q ≠ 0). Конструируется либо через литерал с суффиксом r (1/3r), либо через метод Rational(1, 3). Арифметические операции над рациональными числами выполняются точно, без потери точности — Rational(1, 3) + Rational(1, 6) == Rational(1, 2).

  • Complex — комплексные числа (a + bi). Создаётся через литерал (1+2i) или Complex(1, 2). Поддерживает полный набор арифметических операций, включая возведение в степень и извлечение корня.

Особо отметим: все числовые типы являются неизменяемыми (immutable). Любая операция, изменяющая значение (например, x += 1), создаёт новый объект и связывает переменную с ним; исходный объект остаётся неизменным. Это гарантирует потокобезопасность и предсказуемость при передаче чисел в методы.


Строки (String)

Класс String представляет изменяемую последовательность символов в кодировке UTF-8 (по умолчанию, хотя возможна работа с другими кодировками через Encoding). Строки в Ruby — изменяемые, что отличает их от строк в Java или Python (где строки неизменяемы). Это позволяет эффективно выполнять конкатенацию, вставку, замену частей строки in-place с помощью методов, заканчивающихся на ! ("деструктивных") — gsub!, capitalize!, << (конкатенация в конец).

Литералы строк задаются в одинарных ('text') или двойных кавычках ("text"). В одинарных кавычках интерполяция и большинство escape-последовательностей отключены; в двойных — разрешены интерполяция ("x = #{x}") и escape-коды (\n, \t, \" и т.д.).

Важно: при присваивании строки другой переменной (s2 = s1) создаётся новая ссылка на тот же объект. Изменение s2 через деструктивный метод (s2 << "!") повлияет и на s1. Для создания независимой копии используется s2 = s1.dup (поверхностная копия) или s2 = s1.clone (с сохранением frozen-состояния и singleton-методов).

Строка может быть "заморожена" (freeze), после чего любая попытка её изменить вызовет RuntimeError. Это рекомендуется для строк-констант, используемых как ключи, идентификаторы или части DSL.


Символы (Symbol)

Символ — это неизменяемый, интернированный идентификатор, обозначаемый префиксом двоеточия — :name, :status, :"key with spaces". Главное свойство символа — уникальность в рамках процесса: два символа с одинаковым именем всегда ссылаются на один и тот же объект в памяти. Это достигается за счёт интернирования (interning) — механизма, при котором при первом создании символа он сохраняется в глобальном пуле, и последующие обращения к тому же имени возвращают ссылку на существующий объект.

Преимущества символов перед строками:

  • Экономия памяти при многократном использовании одних и тех же ключей (например, в хэшах: { :id => 1, :name => "Alice" }).
  • Быстрое сравнение по ссылке (identity comparison), а не по содержимому.
  • Неизменяемость "из коробки" — символы всегда frozen.

Символы широко используются как ключи в хэшах, имена методов при метапрограммировании (send(:method_name)), опции в DSL (render partial: 'header'). Однако не следует создавать символы из ненадёжных источников (например, из пользовательского ввода) — поскольку символы никогда не удаляются сборщиком мусора (в классических реализациях Ruby, таких как MRI), это может привести к утечке памяти.


Логические значения и nil

Ruby, как и многие динамические языки, использует логический контекст при вычислении условий (if, while, &&, || и др.). В таком контексте только два значения считаются "ложными": false и nil. Все остальные значения — в том числе 0, пустая строка "", пустой массив [] — истинны.

  • TrueClass — класс, чей единственный экземпляр — true.
  • FalseClass — класс, чей единственный экземпляр — false.
  • NilClass — класс, чей единственный экземпляр — nil. Семантически nil означает отсутствие значения — не инициализированная переменная, отсутствующий ключ в хэше, результат метода без явного return, возврат из итератора при отсутствии совпадений (find без условия).

Несмотря на то, что nil ведёт себя как false в условиях, он является объектом со своим набором методов (например, nil? возвращает true), и его можно передавать как аргумент. Частая практика — использовать nil для обозначения неопределённого или несуществующего состояния, но в современных подходах предпочтение отдаётся явным типам-обёрткам (например, Maybe/Option из функциональных языков), которых в стандартной библиотеке Ruby нет.


Массивы (Array)

Интерактивные виджеты ниже показывают те же структуры на JavaScript / Python / C#; синтаксис Ruby — в примерах текста главы.

Play ITЗагрузка интерактивного демо…

Play ITЗагрузка интерактивного демо…

Сначала — теория (раздел 3 "Данные")

Структуры данныхпсевдокод. Ниже — Array и Hash в Ruby.

массив << x # в конец
элемент := массив[i]

хеш[ключ] = значение
значение := хеш[ключ]

Play ITЗагрузка интерактивного демо…

Array — упорядоченная, индексируемая с нуля коллекция объектов произвольных типов. Массивы в Ruby гетерогенны — один массив может содержать числа, строки, другие массивы, хэши и т.д.

Литералы — [1, "two", :three], %w[a b c] (массив строк без кавычек), %i[a b c] (массив символов).

Массивы изменяемы. Операции Array:

ДействиеСинтаксис
Добавить в конец<< value, push(value)
Добавить в началоunshift(value)
Вставить по индексуinsert(index, value)
Прочитатьarr[index]
Заменитьarr[index] = value
Удалить по индексуdelete_at(index)
Удалить с конца / началаpop, shift
Удалить по значениюdelete(value)

Плюс сортировка (sort!, sort), фильтрация (select, reject) и функциональные методы (map, reduce, each).

Операции Array:

ДействиеСинтаксис
Добавить в конец<< value, push(value)
Добавить в началоunshift(value)
Вставить по индексуinsert(index, value)
Прочитатьarr[index]
Заменитьarr[index] = value
Удалить по индексуdelete_at(index)
Удалить с конца / началаpop, shift
Удалить по значениюdelete(value)

Индексы могут быть отрицательными: -1 — последний элемент, -2 — предпоследний и т.д. Попытка доступа к несуществующему индексу возвращает nil (не вызывает исключение).

Методы, возвращающие новый массив (например, map, select), не изменяют исходный; методы с суффиксом ! (map!, select!) — изменяют на месте. Важно различать эти варианты, чтобы избежать побочных эффектов.


Хэши (Hash)

Play ITЗагрузка интерактивного демо…

Hash — коллекция пар ключ → значение. Ключами могут быть любые объекты, для которых определены методы #eql? и #hash (это условие обеспечивает корректную работу хэш-таблицы). По умолчанию ключи сравниваются по значению (eql?), а не по идентичности (equal?).

Литералы:

  • { :name => "Alice", :age => 30 } (традиционный синтаксис),
  • { name — "Alice", age: 30 } (новый синтаксис для символьных ключей, появился в Ruby 1.9).

Хэши изменяемы. Операции Hash:

ДействиеСинтаксис
Добавить или заменитьhash[key] = value
Прочитатьhash[key]
Безопасное чтениеhash.fetch(key, default)
Удалитьdelete(key)
Проверить ключkey?(key), has_key?(key)

Также — merge, merge!, each, each_key, each_value.

Операции Hash:

ДействиеСинтаксис
Добавить или заменитьhash[key] = value
Прочитатьhash[key]
Безопасное чтениеhash.fetch(key, default)
Удалитьdelete(key)
Проверить ключkey?(key), has_key?(key)

Начиная с Ruby 1.9, хэши сохраняют порядок вставки ключей — важное изменение по сравнению с версиями до 1.9, где порядок был неопределён.

По умолчанию при обращении к отсутствующему ключу возвращается nil. Однако можно задать значение по умолчанию при создании хэша:

  • Hash.new(0) — возвращает 0 для любого отсутствующего ключа,
  • Hash.new { |hash, key| hash[key] = [] } — создаёт новый пустой массив при первом обращении к новому ключу ("хэш с отложенной инициализацией").

Диапазоны (Range)

Диапазон — это упорядоченная последовательность значений между двумя границами. Создаётся с помощью операторов .. (включая правую границу) и ... (исключая правую границу):

  • (1..5)1, 2, 3, 4, 5,
  • ('a'...'d')'a', 'b', 'c'.

Диапазоны не обязательно материализуются в памяти как массив: они ленивы. Метод #to_a преобразует диапазон в массив, но большинство операций (include?, cover?, each) работают без создания промежуточной коллекции.

Диапазоны могут использоваться с числами, символами, и с любыми объектами, реализующими методы #<=> (сравнение) и #succ (следующее значение): например, с датами ((Date.new(2025,1,1)..Date.new(2025,1,10))).


Регулярные выражения (Regexp)

Регулярные выражения в Ruby — это объекты класса Regexp, создаваемые либо литералами в косых чертах (/pattern/), либо через Regexp.new("pattern"). Поддерживают флаги — /i (регистронезависимость), /m (многострочный режим), /x (расширенный синтаксис с комментариями и пробелами).

Основные операции:

  • =~ — проверка совпадения (возвращает позицию первого совпадения или nil),
  • match — возвращает объект MatchData с деталями совпадения или nil,
  • scan — возвращает все неперекрывающиеся совпадения в виде массива,
  • gsub — глобальная замена по шаблону.

Группы захвата доступны через $1, $2, … или через MatchData#[].


Работа с типами данных

В условиях динамической типизации Ruby предоставляет богатый набор инструментов для запроса, проверки и преобразования типов во время выполнения. Эти механизмы позволяют писать гибкий, адаптивный код, сохраняя при этом контроль над поведением программы. Ключевой принцип — типы проверяются по объектам, а не по переменным, и любая проверка — это вызов метода у объекта.


Проверка типов

Ruby предлагает несколько уровней проверки принадлежности объекта к тому или иному типу, различающихся по строгости и назначению.

  • #class — возвращает точный класс объекта. Например, 42.classInteger, "hello".classString. Этот метод полезен, когда требуется идентифицировать конкретную реализацию, а не иерархию. Однако прямое сравнение через == (obj.class == String) считается избыточным и неидиоматичным; предпочтительнее использовать проверки на принадлежность иерархии.

  • #is_a?(klass) и синоним #kind_of?(klass) — возвращают true, если объект является экземпляром указанного класса или любого из его подклассов. Это метод, реализованный в Object, и он учитывает всю цепочку наследования. Пример: 42.is_a?(Numeric)true, "text".is_a?(Object)true. Это — стандартный способ проверки "может ли объект вести себя как X?".

  • #instance_of?(klass) — более строгая проверка: возвращает true только если объект создан непосредственно классом klass, без учёта наследования. Пример: 42.instance_of?(Integer)true, но 42.instance_of?(Numeric)false, поскольку Integer — подкласс Numeric. Такая проверка редко требуется в практике, так как нарушает принцип подстановки Барбары Лисков и ограничивает расширяемость.

  • #respond_to?(:method_name) — проверяет, поддерживает ли объект данный метод. Это — ключевой приём "утиной типизации" (duck typing): "если существо крякает как утка и плавает как утка — будем считать, что это утка". Вместо проверки is_a?(String), часто достаточно obj.respond_to?(:length) и obj.respond_to?(:gsub), чтобы убедиться, что объект ведёт себя как строка. Это повышает полиморфизм и совместимость с прокси, декораторами и пользовательскими классами, имитирующими стандартные интерфейсы.

  • Модуль Comparable и метод #<=> (spaceship operator) — не проверка типа как таковая, но важный механизм для определения упорядочиваемости. Любой класс, реализующий #<=> и подключающий include Comparable, автоматически получает методы ==, <, >, <=, >=, between?. Проверка obj.respond_to?(:<=>) часто используется для определения, можно ли сравнивать объекты.

Для повышения надёжности при работе с внешними данными (например, из API или пользовательского ввода) рекомендуется комбинировать проверки — сначала nil?, затем is_a? или respond_to?, и только после этого — операции.


Приведение типов (явное)

Ruby поддерживает явное приведение типа через методы-конструкторы и методы-преобразователи. Важно различать:

  1. Конструкторы классовString(x), Integer(x), Float(x), Array(x), Hash(x). Эти методы являются глобальными функциями, определёнными в Kernel. Они вызывают у аргумента x метод #to_str, #to_int, #to_ary, #to_hash соответственно — но только если такие методы определены. Если соответствующий to_*-метод отсутствует, вызывается #to_s, #to_i, #to_f и т.д., в зависимости от контекста. Например, String(42) вызывает 42.to_s, возвращая "42".

  2. Методы экземпляров#to_s, #to_i, #to_f, #to_a, #to_h, #to_sym, #to_proc. Каждый стандартный класс реализует набор таких методов для преобразования в другие типы.

    • #to_s — строковое представление (используется при интерполяции и выводе),
    • #to_i — преобразование в целое число (игнорирует нецифровые символы после начала, возвращает 0 при неудаче),
    • #to_f — в число с плавающей точкой (аналогично),
    • #to_a — в массив (String#to_a возвращает массив символов с Ruby 2.4+),
    • #to_h — в хэш (Array#to_h требует массива пар [key, value]).

    Эти методы не вызывают исключений при неудаче — они возвращают "безопасные" значения по умолчанию (чаще всего 0, "", [], {} или nil). Это соответствует философии "программа должна продолжать работу", но требует внимания при валидации.

  3. Более строгие методы#to_str, #to_int, #to_ary, #to_hash. Отличаются от #to_s и #to_i тем, что должны быть реализованы только классами, которые семантически являются строкой, целым и т.д. Например, Pathname реализует #to_path, но не #to_str, поскольку путь — не строка, хотя может быть в неё преобразован. Методы вроде String(x) и операторы (например, + для строк) вызывают именно #to_str, а не #to_s, если хотят убедиться в строкоподобности, а не просто получить текстовое представление. Если #to_str не определён — будет ошибка TypeError. Это — механизм обеспечения типовой дисциплины в ключевых операциях.

Пример различия:

class PhoneNumber
def initialize(num); @num = num; end
def to_s; "+7 (#{@num})"; end
# to_str НЕ определён
end

pn = PhoneNumber.new("999-123-45-67")
puts pn.to_s # "+7 (999-123-45-67)"
String(pn) # вызывает to_s → "+7 (999-123-45-67)"
"Call: " + pn # TypeError: no implicit conversion of PhoneNumber into String
# потому что + вызывает to_str, которого нет

Разбор:

  • to_s даёт строковое представление объекта и используется для явного форматирования.
  • String(pn) выполняет мягкое преобразование и может опираться на to_s.
  • Оператор + у строк требует более строгий протокол to_str, то есть "объект должен быть строкоподобным".
  • Отсутствие to_str вызывает TypeError, что защищает от неявных неоднозначных преобразований.
  • Этот пример показывает различие между "можно показать как строку" и "можно использовать как строку в API".

Чтобы исправить — нужно реализовать def to_str; to_s; end, но только если семантически номер является строкой (что спорно).


Неявные преобразования

Неявные преобразования в Ruby происходят в строго определённых контекстах и инициируются операторами или встроенными методами. Они вызывают соответствующие to_*-методы без участия программиста. Основные случаи:

  • Арифметические операции между разными числовыми типами:
    Integer + FloatFloat,
    Float + RationalFloat,
    Integer + ComplexComplex.
    Правило: если хотя бы один операнд "шире" другого (в порядке Integer < Rational < Float < Complex), результат приводится к более широкому типу.

  • Строковая интерполяция ("x = #{x}") вызывает x.to_s.

  • Оператор + для строк требует, чтобы правый операнд отвечал на #to_str (не #to_s!).

  • Оператор * для строк ("a" * 3) требует, чтобы правый операнд отвечал на #to_int.

  • Логический контекст (if, while, &&, ||, ?:) интерпретирует только false и nil как ложные — никаких неявных преобразований в true/false не происходит. Это — важное отличие от языков вроде JavaScript.

  • Хэш как именованный аргумент (since Ruby 2.0) — при вызове method(a — 1, b: 2) создается хэш {:a => 1, :b => 2}. Если последний аргумент — хэш без фигурных скобок, он автоматически "распаковывается" в именованные параметры. Это синтаксическое преобразование, а не типовое.

Ruby избегает неявных преобразований там, где они могут привести к неоднозначности. Например, 1 + "2" вызывает TypeError, а не пытается преобразовать строку в число. Это — сознательный выбор в пользу явности и предсказуемости.


Типобезопасность в динамической среде

Динамическая типизация означает перенос проверок с этапа компиляции на этап выполнения. Ruby поддерживает типобезопасность через:

  • Исключения при нарушении контрактовTypeError, ArgumentError, NoMethodError возникают точно в момент попытки некорректной операции, что облегчает локализацию проблемы.
  • Соглашения об интерфейсах: duck typing (respond_to?) позволяет работать с объектами по их поведению, а не по иерархии.
  • Инструменты статического анализа# typed — strong в Sorbet, TypeProf, RBS — позволяют добавлять необязательные аннотации типов для документирования и проверки на этапе разработки, не нарушая динамической природы языка.
  • Методы-гаранты#freeze, #dup, private, protected, attr_reader/attr_writer помогают управлять изменяемостью и инкапсуляцией, что косвенно влияет на типовую стабильность.

Ошибки типов в Ruby — всегда ошибки времени выполнения. Поэтому покрытие кода тестами (особенно интеграционными и end-to-end) — необходимое условие надёжности. Статическая проверка типов может дополнить, но не заменить тестирование.


Практика — как выбирать структуру данных под задачу

Новичкам в Ruby помогает простое правило выбора:

  • Array подходит для "списка по порядку".
  • Hash подходит для "поиска по ключу".
  • Set подходит для "уникальных элементов и быстрых проверок наличия".
  • Queue и Array (как стек) подходят для очередей задач и LIFO-сценариев.

Пример выбора в реальном коде:

users = [{ id: 1, name: "Ada" }, { id: 2, name: "Matz" }]
users_by_id = users.to_h { |u| [u[:id], u] }

puts users[0][:name]
puts users_by_id[2][:name]

Разбор:

  • users хранит список хэшей в виде массива, что удобно для последовательного обхода.
  • to_h { |u| [u[:id], u] } строит индексированный словарь id -> объект пользователя.
  • После преобразования доступ users_by_id[2] работает за константное время и удобен для точечных выборок.
  • puts users[0][:name] и puts users_by_id[2][:name] демонстрируют два способа доступа: по позиции и по ключу.

Мини-сниппет — безопасная работа с nil и типом

value = params[:age]

age = if value.is_a?(String) && value.match?(/\A\d+\z/)
value.to_i
else
18
end

puts age

Разбор:

  • is_a?(String) проверяет, что значение действительно строка.
  • match?(/\A\d+\z/) убеждается, что строка состоит только из цифр.
  • to_i выполняется только после валидации формата.
  • Ветка else задаёт безопасное значение по умолчанию.
  • Сниппет показывает практику динамической типизации: сначала проверка, потом преобразование.

Мини-сниппет — отличие dup от общей ссылки

original = ["ruby", "oop"]
shared = original
copied = original.dup

shared << "metaprogramming"
copied << "types"

p original
p copied

Разбор:

  • shared = original создаёт вторую ссылку на тот же массив.
  • dup создаёт отдельный объект-массив с копией элементов первого уровня.
  • Изменение shared влияет на original, потому что объект общий.
  • Изменение copied не меняет original, так как это уже независимый массив.
  • Пример закрепляет базовую модель Ruby: переменные хранят ссылки на объекты.

Сводка операций по типам коллекций

Операции Array (упорядоченная изменяемая коллекция)
ДействиеСинтаксис
Добавить в конецarr << value, arr.push(value)
Вставить по индексуarr.insert(index, value)
Прочитатьarr[index]
Заменитьarr[index] = value
Удалитьarr.delete_at(index), arr.delete(value), arr.pop, arr.shift

Операции Hash (словарь)
ДействиеСинтаксис
Добавить или заменитьhash[key] = value
Прочитатьhash[key], hash.fetch(key, default)
Удалитьhash.delete(key)

Операции Set (множество)

Для Set нужна стандартная библиотека:

require "set"

Разбор:

  • require "set" подключает класс Set из стандартной библиотеки.
  • По умолчанию Set не загружается автоматически, в отличие от Array и Hash.
  • Подключение нужно сделать один раз до первого использования множества.
ДействиеСинтаксис
Добавитьset.add(value)
Удалитьset.delete(value)
Проверить наличиеset.include?(value)

Очередь и стек

Queue (FIFO) из стандартной библиотеки thread:

q = Queue.new
q << "job-1"
q << "job-2"
q.pop
q.first

Разбор:

  • Queue.new создаёт потокобезопасную очередь FIFO из стандартной библиотеки.
  • Оператор << добавляет элементы в хвост очереди.
  • pop извлекает элемент с головы в порядке поступления.
  • first возвращает следующий элемент без удаления, если такая операция поддерживается реализацией.

Stack (LIFO) обычно делают на Array:

stack = []
stack.push("a")
stack.push("b")
stack.pop
stack.last

Разбор:

  • stack = [] использует обычный массив как стек.
  • push кладёт элементы на вершину.
  • pop снимает последний добавленный элемент (LIFO).
  • last показывает текущую вершину без удаления.
  • Это простой и распространённый шаблон для локальных алгоритмов.

Связанные статьи энциклопедии


Прочие важные типы

Кратко упомянем ещё несколько фундаментальных типов, не вошедших в предыдущие разделы:

  • Proc и Lambda — объекты-замыкания, инкапсулирующие блок кода и его окружение. Различаются в проверке арности и поведении return. Создаются через Proc.new, lambda, -> {}.

  • Method — объект, представляющий привязанный метод (например, obj.method(:to_s)). Позволяет передавать методы как данные.

  • Class и Module — тоже объекты. Любой класс — экземпляр класса Class, который, в свою очередь, наследуется от Module. Это позволяет динамически создавать и модифицировать классы во время выполнения (метапрограммирование).

  • File, Dir, IO — объекты, представляющие ресурсы операционной системы. Их корректное управление (особенно освобождение через close или ensure) критично для стабильности.

  • Struct — лёгкий способ создания класса-контейнера для фиксированного набора атрибутов: Point = Struct.new(:x, :y).