Nivel inicial9 min de lectura · Publicado el

Hashes en Ruby: crear, consultar y recorrer

Hashes en Ruby: una cajonera con etiquetas; contactos[:alberto] abre el cajón :alberto, la clave, y dentro está 600000000, el valor. Por clave, no por posiciónrubentejera.com "600000000":alberto:dariocontactos[:alberto]clave: la etiquetavalor: lo de dentro
Cada cajón es una clave con su etiqueta, como :alberto, y lo que guarda dentro es el valor, "600000000". contactos[:alberto] abre ese cajón; si la etiqueta no existe, devuelve nil.

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, edad y ciudad), un hash no protege de nada: persona[:nombr] devuelve nil sin quejarse. Un Struct, un Data (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: guardar true como valor de cada clave es un hash que hace de Set.

Ejercicio de cinco minutos

  1. Cuenta cuántas veces aparece cada letra en "papas con mojo" con Hash.new(0), y después con chars.tally. Comprueba que dan lo mismo (pista: el espacio también cuenta).
  2. Agrupa %w[Ana Alberto Bea Barreto Carla] por su inicial con Hash.new { |h, k| h[k] = [] }, y luego con group_by.
  3. Añade a la Agenda un método borrar(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_values y el error al añadir claves durante una iteración.
  • Enumerable (Ruby 4.0): tally, sort_by, max_by, sum y group_by.
  • Notas de la versión de Ruby 3.4: Hash#inspect muestra 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:latest de 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 oficiales ruby:4.0 (Ruby 4.0.9, Minitest 6.0.0) y ruby: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 sin ruby -w.