Hashes en Ruby: crear, consultar y recorrer
Un hash es una colección que guarda parejas de clave y valor. Funciona como una cajonera con etiquetas: buscas el cajón por su etiqueta (la clave) y dentro está lo que guarda (el valor), aquí un teléfono. No buscas «el cajón número 3», como harías en un array, sino «el de Alberto».
Crear un hash
La clave y el valor pueden ser de casi cualquier tipo: textos, números o símbolos.
contactos = {
"Alberto" => "600000000",
"Barreto" => "611111111",
159 => "622222222"
}
contactos["Alberto"] # => "600000000"
contactos[159] # => "622222222"
contactos["Nadie"] # => nil (la clave no existe)
Claves con símbolos
Lo más habitual en Ruby es usar símbolos como claves. Hay dos formas de escribirlos, y son equivalentes:
contactos = {
:alberto => "600000000", # forma clásica
dario: "633333333" # forma abreviada, la más usada
}
contactos[:alberto] # => "600000000"
contactos[:dario] # => "633333333"
Claves repetidas
Si repites una clave al escribir el hash, gana el último valor y Ruby avisa con un warning:
contactos = { "Alberto" => "600000000", "Alberto" => "633333333" }
p contactos
dup.rb:1: warning: key "Alberto" is duplicated and overwritten on line 1
{"Alberto" => "633333333"}
El aviso sale aunque no ejecutes con ruby -w, tanto en Ruby 3.4 como en Ruby 4.0.
Añadir y modificar
Con []= añades una clave nueva o cambias el valor de una existente. store hace lo mismo:
contactos = { alberto: "600000000", barreto: "611111111" }
contactos[:alberto] = "644444444" # cambia un valor
contactos[:dario] = "622222222" # añade una clave
contactos.store(:candido, "633333333")
p contactos
# => {alberto: "644444444", barreto: "611111111", dario: "622222222", candido: "633333333"}
Para quitar una pareja, usa delete, que además devuelve el valor borrado: contactos.delete(:candido).
Fusionar con merge
merge no modifica el hash original: devuelve uno nuevo con las parejas de los dos. Si quieres modificar el original, usa merge!.
contactos = { alberto: "600000000", barreto: "611111111" }
nuevos = contactos.merge({ dario: "622222222" })
p contactos # => {alberto: "600000000", barreto: "611111111"}
p nuevos # => {alberto: "600000000", barreto: "611111111", dario: "622222222"}
Consultar
| Quieres saber… | Método | Resultado con { alberto: "600000000", barreto: "611111111" } |
|---|---|---|
| Cuántas parejas hay | length o size |
2 |
| Todas las claves | keys |
[:alberto, :barreto] |
| Todos los valores | values |
["600000000", "611111111"] |
| Si existe una clave | key?(:alberto) |
true |
| Si existe un valor | value?("600000000") |
true |
has_key? y has_value? también funcionan; son nombres antiguos de key? y value?.
fetch: cuando la clave debe existir
contactos[:daniel] devuelve nil en silencio, y ese nil puede dar un error mucho más adelante. fetch falla en el momento o te deja dar un valor por defecto:
# Sigue con contactos = { alberto: "600000000", barreto: "611111111" }
contactos.fetch(:alberto) # => "600000000"
contactos.fetch(:daniel) # KeyError: key not found: :daniel
contactos.fetch(:daniel, "sin teléfono") # => "sin teléfono"
Cómo capturar ese KeyError se explica en Excepciones en Ruby.
dig: hashes dentro de hashes
Un valor puede ser otro hash. dig baja por las claves y, si alguna no existe, devuelve nil en vez de fallar:
usuario = { nombre: "Ana", direccion: { ciudad: "La Laguna", cp: "38200" } }
p usuario.dig(:direccion, :ciudad) # "La Laguna"
p usuario.dig(:trabajo, :ciudad) # nil
Valores por defecto: contar con Hash.new(0)
Un hash creado con Hash.new(0) devuelve 0 para las claves que no tiene. Es el truco clásico para contar:
votos = Hash.new(0)
%w[papas mojo papas gofio papas].each { |tapa| votos[tapa] += 1 }
p votos # {"papas" => 3, "mojo" => 1, "gofio" => 1}
p votos["queso"] # 0
p votos.key?("queso") # false: el 0 es solo lo que devuelve, no lo guarda
Para contar elementos de un array, tally hace todo esto de una vez: %w[papas mojo papas gofio papas].tally da el mismo hash.
Recorrer y transformar un hash
each te da cada clave con su valor:
# Sigue con contactos = { alberto: "600000000", barreto: "611111111" }
contactos.each do |nombre, telefono|
puts "#{nombre}: #{telefono}"
end
# alberto: 600000000
# barreto: 611111111
Los hashes recuerdan el orden en que entraron las claves. Casi todos los métodos para recorrer y buscar que usas con los arrays (los del módulo Enumerable) funcionan también con ellos, recibiendo la clave y el valor. Los que dependen de la posición, como push, << o [0], no: en un hash, [] busca por clave.
precios = { papas: 4.5, mojo: 2.0, gofio: 3.25 }
p precios.select { |_tapa, precio| precio > 3 } # {papas: 4.5, gofio: 3.25}
p precios.transform_values { |precio| precio * 2 } # {papas: 9.0, mojo: 4.0, gofio: 6.5}
p precios.sort_by { |_tapa, precio| precio }.to_h # {mojo: 2.0, gofio: 3.25, papas: 4.5}
p precios.max_by { |_tapa, precio| precio } # [:papas, 4.5]
p precios.sum { |_tapa, precio| precio } # 9.75
p precios.to_a # [[:papas, 4.5], [:mojo, 2.0], [:gofio, 3.25]]
El guion bajo de _tapa indica que ese parámetro no se usa. select y transform_values devuelven un hash; sort_by y max_by devuelven parejas [clave, valor], y to_h las vuelve a convertir en hash.
Errores frecuentes
Las salidas son de Ruby 4.0.9, recortadas a las líneas que importan; en Ruby 3.4.12 salen igual.
1. Buscar con texto una clave que es un símbolo. "alberto" y :alberto son claves distintas, y la búsqueda devuelve nil sin avisar:
contactos = { alberto: "600000000" }
p contactos["alberto"] # nil: la clave es el símbolo :alberto, no el texto "alberto"
p contactos[:alberto] # "600000000"
Pasa mucho con datos que vienen de fuera (un JSON, un formulario), donde las claves llegan como texto. transform_keys(&:to_sym) las convierte en símbolos: { "alberto" => 1 }.transform_keys(&:to_sym) da {alberto: 1}.
2. Equivocarse en una clave con fetch. Aquí fetch ayuda, porque sugiere la clave que querías:
contactos = { alberto: "600000000", barreto: "611111111" }
contactos.fetch(:albert)
fetch_typo.rb:2:in 'Hash#fetch': key not found: :albert (KeyError)
Did you mean? :alberto
Con contactos[:albert] solo habrías recibido nil.
3. Encadenar [] sobre una clave que no existe:
usuario = { nombre: "Ana", direccion: { ciudad: "La Laguna", cp: "38200" } }
p usuario[:trabajo][:ciudad]
dig.rb:2:in '<main>': undefined method '[]' for nil (NoMethodError)
p usuario[:trabajo][:ciudad]
^^^^^^^^^
usuario[:trabajo] es nil, y a nil no se le puede pedir [:ciudad]. Usa dig si la clave puede faltar, o fetch si debe estar.
4. Añadir claves mientras lo recorres. Ruby lo impide:
contactos = { alberto: "600000000", barreto: "611111111" }
contactos.each do |nombre, telefono|
contactos[:"#{nombre}_movil"] = telefono
end
iterar_anadir.rb:3:in 'block in <main>': can't add a new key into hash during iteration (RuntimeError)
Construye un hash nuevo y fusiónalo después, o recorre una copia de las claves (contactos.keys.each).
5. Usar Hash.new([]) para agrupar. Como en el Array.new(3, []) de los arrays, el valor por defecto es un único array compartido por todas las claves, y además no se guarda en el hash:
por_letra = Hash.new([])
por_letra["a"] << "Ana"
por_letra["b"] << "Bea"
p por_letra # {}
p por_letra["z"] # ["Ana", "Bea"]
El hash sigue vacío y los dos nombres han acabado en el array por defecto. Con un bloque, cada clave nueva recibe su propio array y se guarda:
por_letra = Hash.new { |hash, clave| hash[clave] = [] }
por_letra["a"] << "Ana"
por_letra["b"] << "Bea"
p por_letra # {"a" => ["Ana"], "b" => ["Bea"]}
Compruébalo con un test
Una agenda que guarda los nombres en minúsculas y como símbolos, para que «Alberto» y «alberto» sean el mismo contacto. Crea agenda.rb:
class Agenda
def initialize
@contactos = {}
end
def guardar(nombre, telefono)
@contactos[nombre.downcase.to_sym] = telefono
end
def telefono(nombre)
@contactos.fetch(nombre.downcase.to_sym, "sin teléfono")
end
def nombres = @contactos.keys.sort
end
Y agenda_test.rb, con Minitest, que viene con Ruby:
require "minitest/autorun"
require_relative "agenda"
class AgendaTest < Minitest::Test
def setup
@agenda = Agenda.new
@agenda.guardar("Alberto", "600000000")
end
def test_encuentra_sin_importar_mayusculas
assert_equal "600000000", @agenda.telefono("alberto")
assert_equal "600000000", @agenda.telefono("ALBERTO")
end
def test_guardar_de_nuevo_sustituye_el_telefono
@agenda.guardar("alberto", "644444444")
assert_equal "644444444", @agenda.telefono("Alberto")
end
def test_valor_por_defecto_si_no_existe
assert_equal "sin teléfono", @agenda.telefono("Daniel")
end
def test_nombres_ordenados
@agenda.guardar("Barreto", "611111111")
@agenda.guardar("Ana", "622222222")
assert_equal [:alberto, :ana, :barreto], @agenda.nombres
end
end
ruby agenda_test.rb
4 runs, 5 assertions, 0 failures, 0 errors, 0 skips
El segundo test deja claro que una clave solo puede estar una vez: guardar de nuevo sustituye el valor anterior. Si es tu primer test en Ruby, empieza por la introducción al testing en Ruby.
Cuándo no usar un hash
- Si el orden y la posición son lo importante, usa un array. Una cola de turnos es una lista, no una cajonera.
- Si siempre tiene las mismas claves (una persona con
nombre,edadyciudad), un hash no protege de nada:persona[:nombr]devuelvenilsin quejarse. UnStruct, unData(desde Ruby 3.2) o una clase te dan métodos con nombre, y si te equivocas al escribirlo, salta un error. - Si solo quieres saber si algo está, sin valor asociado, usa un
Set: guardartruecomo valor de cada clave es un hash que hace deSet.
Ejercicio de cinco minutos
- Cuenta cuántas veces aparece cada letra en
"papas con mojo"conHash.new(0), y después conchars.tally. Comprueba que dan lo mismo (pista: el espacio también cuenta). - Agrupa
%w[Ana Alberto Bea Barreto Carla]por su inicial conHash.new { |h, k| h[k] = [] }, y luego congroup_by. - Añade a la
Agendaun métodoborrar(nombre)que devuelva el teléfono borrado, y escribe primero su test.
Fuentes
- Hash (Ruby 4.0): creación, valores por defecto con
Hash.new(valor y bloque),fetch,dig,merge,store,delete,transform_keys,transform_valuesy el error al añadir claves durante una iteración. - Enumerable (Ruby 4.0):
tally,sort_by,max_by,sumygroup_by. - Notas de la versión de Ruby 3.4:
Hash#inspectmuestra las claves símbolo como{user: 1}y pone espacios alrededor de=>en las demás. - Los ejemplos del artículo original se ejecutaron con Ruby 4.0.9 (imagen oficial
ruby:latestde Docker) en noviembre de 2026. El 12 de noviembre de 2026 se ha repetido todo, junto con las secciones nuevas (dig, valores por defecto, transformaciones, errores frecuentes y test), con las imágenes oficialesruby:4.0(Ruby 4.0.9, Minitest 6.0.0) yruby:3.4(Ruby 3.4.12, Minitest 5.25.4), con la misma salida. Al repetirlo se ha corregido el artículo: el aviso de clave repetida sale también sinruby -w.