O que é um Hash em Ruby
Um Hash é uma coleção de pares chave → valor. Diferente de um array (indexado por posição), o hash é indexado por uma chave, que costuma ser um símbolo (ex.: :host, :port) por ser leve, imutável e muito comum em configurações e dados estruturados.
config = { host: "localhost", port: 5432, ssl: false }Você acessa valores usando a chave:
config[:host] # => "localhost"Símbolos como chaves (padrão idiomático)
Em Ruby, é comum usar a sintaxe abreviada de símbolos:
user = { name: "Ana", role: "admin" } # equivalente a { :name => "Ana", :role => "admin" }Boa prática: escolha um padrão e mantenha consistência. Se você usar símbolos como chaves, evite misturar com strings (ex.: "name") no mesmo hash, pois :name e "name" são chaves diferentes.
h = { name: "Ana" } h["name"] # => nil (chave diferente)Acesso a valores: [] vs fetch (acesso seguro)
Usando [] (retorna nil quando não existe)
O operador [] é direto, mas quando a chave não existe ele retorna nil, o que pode mascarar erros.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
config = { host: "localhost" } config[:port] # => nilIsso pode virar um problema se você assumir que sempre haverá valor:
config[:port] + 1 # TypeError (nil + 1)Usando fetch (fallback e/ou erro explícito)
fetch é a forma idiomática para exigir a presença da chave ou definir um fallback.
1) fetch com fallback (valor padrão pontual)
config = { host: "localhost" } port = config.fetch(:port, 5432) # => 54322) fetch com bloco (fallback calculado)
env = :dev config = { host: "localhost" } port = config.fetch(:port) { env == :prod ? 5432 : 15432 }3) fetch sem fallback (falha rápido com KeyError)
config = { host: "localhost" } config.fetch(:port) # KeyError: key not found: :portEsse comportamento é útil quando a ausência da chave é um erro de programação ou de configuração.
Tratamento de erro quando apropriado
Em cenários como leitura de configurações, você pode capturar o erro e gerar uma mensagem clara:
begin config = { host: "localhost" } port = config.fetch(:port) rescue KeyError => e raise "Configuração inválida: porta ausente (#{e.message})" endValores padrão do Hash (default)
Você pode definir um valor padrão para chaves ausentes. Isso é útil, mas exige cuidado.
Default simples
h = Hash.new(0) h[:a] # => 0 h[:a] += 1 h # => { :a => 1 }Esse padrão é excelente para contagens.
Cuidado: default com objeto mutável compartilhado
Se você usar um array como default, ele será o mesmo objeto para todas as chaves ausentes.
h = Hash.new([]) h[:a] << 1 h[:b] << 2 h[:a] # => [1, 2] (surpresa!)Forma correta: use bloco para criar um novo objeto por chave.
h = Hash.new { |hash, key| hash[key] = [] } h[:a] << 1 h[:b] << 2 h # => { :a => [1], :b => [2] }Iteração: each_pair (e padrões comuns)
Para percorrer chaves e valores, use each_pair (ou each, que é equivalente). each_pair deixa explícito que você quer pares.
config = { host: "localhost", port: 5432, ssl: false } config.each_pair do |key, value| puts "#{key} => #{value}" endVocê também pode iterar só chaves ou só valores:
config.keys # => [:host, :port, :ssl] config.values # => ["localhost", 5432, false]Verificação de existência: key? e value?
key? (ou has_key?)
Use key? quando você precisa distinguir entre “chave ausente” e “chave presente com valor nil”.
h = { token: nil } h[:token] # => nil h.key?(:token) # => true h.key?(:missing) # => falsevalue? (ou has_value?)
value? verifica se algum valor existe no hash. Pode ser útil, mas costuma ser menos eficiente (precisa procurar nos valores).
h = { a: 1, b: 2 } h.value?(2) # => trueMerge: combinando hashes de forma idiomática
merge combina hashes e, quando há conflito de chave, o valor do hash da direita vence.
defaults = { host: "localhost", port: 5432, ssl: false } override = { ssl: true } config = defaults.merge(override) # => { host: "localhost", port: 5432, ssl: true }Merge com bloco (resolver conflitos)
Quando você quer controlar como lidar com conflitos:
a = { retries: 2, tags: ["core"] } b = { retries: 5, tags: ["urgent"] } merged = a.merge(b) do |key, old_val, new_val| case key when :retries then [old_val, new_val].max when :tags then old_val + new_val else new_val end end # => { retries: 5, tags: ["core", "urgent"] }Boa prática: prefira merge para criar um novo hash e manter imutabilidade local. Use merge! apenas quando a mutação for intencional e clara.
Transformação: map, transform_keys e transform_values
Hashes frequentemente precisam ser transformados: normalizar chaves, ajustar valores, preparar dados para saída.
transform_keys (normalizar chaves)
raw = { "Host" => "localhost", "PORT" => "5432" } normalized = raw.transform_keys { |k| k.downcase.to_sym } # => { host: "localhost", port: "5432" }transform_values (converter valores)
normalized = { host: "localhost", port: "5432" } typed = normalized.transform_values do |v| v.match?(/\A\d+\z/) ? v.to_i : v end # => { host: "localhost", port: 5432 }map para gerar outro formato
map em hash retorna um array. É útil para gerar linhas, logs ou estruturas derivadas.
config = { host: "localhost", port: 5432 } lines = config.map { |k, v| "#{k}=#{v}" } # => ["host=localhost", "port=5432"]Cenário prático 1: Configurações com defaults, override e validação
Objetivo: construir uma configuração final com valores padrão, permitir override e garantir que chaves obrigatórias existam.
Passo a passo
1) Defina defaults
defaults = { host: "localhost", port: 5432, ssl: false, timeout: 5 }2) Receba overrides (ex.: do usuário)
overrides = { ssl: true, timeout: 10 }3) Faça merge
config = defaults.merge(overrides)4) Exija chaves obrigatórias com fetch
host = config.fetch(:host) port = config.fetch(:port)5) Use fallback pontual quando fizer sentido
retries = config.fetch(:retries, 3)6) Valide tipos/intervalos (exemplo simples)
timeout = config.fetch(:timeout, 5) raise "timeout deve ser > 0" unless timeout.is_a?(Integer) && timeout > 0Cenário prático 2: Contagens por categoria (Hash.new(0))
Objetivo: contar ocorrências por categoria de forma idiomática.
items = [ { category: :food, name: "banana" }, { category: :tech, name: "mouse" }, { category: :food, name: "arroz" } ] counts = Hash.new(0) items.each do |item| cat = item.fetch(:category) counts[cat] += 1 end counts # => { :food => 2, :tech => 1 }Boa prática: use fetch(:category) se a categoria for obrigatória; assim você descobre dados inválidos cedo.
Cenário prático 3: Agrupamentos (Hash com default por bloco)
Objetivo: agrupar registros por uma chave (ex.: status, categoria, mês).
orders = [ { id: 1, status: :paid }, { id: 2, status: :pending }, { id: 3, status: :paid } ] grouped = Hash.new { |h, k| h[k] = [] } orders.each do |order| status = order.fetch(:status) grouped[status] << order end grouped # => { :paid => [{...}, {...}], :pending => [{...}] }Boas práticas para lidar com chaves ausentes
- Use
fetchpara chaves obrigatórias (falha rápido) ou com fallback quando houver um padrão aceitável. - Use
key?quando precisar diferenciar “ausente” de “presente com nil”. - Defina defaults com cuidado: para objetos mutáveis (arrays/hashes), prefira
Hash.new { |h, k| h[k] = ... }. - Evite misturar tipos de chave (símbolos e strings) no mesmo hash; normalize com
transform_keysquando necessário. - Prefira
mergeamerge!para evitar efeitos colaterais, a menos que a mutação seja intencional.
Tabela rápida: métodos essenciais de Hash
| Método | Uso | Quando usar |
|---|---|---|
h[:k] | Acesso simples | Quando nil é aceitável para ausente |
h.fetch(:k) | Acesso estrito | Quando a chave é obrigatória |
h.fetch(:k, v) | Acesso com fallback | Quando há um padrão aceitável |
h.key?(:k) | Checar existência de chave | Distinguir ausente vs presente com nil |
h.value?(v) | Checar existência de valor | Consultas pontuais em valores |
h.each_pair | Iterar pares | Processar chaves e valores |
h.merge(other) | Combinar hashes | Defaults + overrides, composição |
h.transform_keys | Transformar chaves | Normalização (string → símbolo) |
h.transform_values | Transformar valores | Conversões e ajustes de tipos |