Diferença entre objetos mutáveis e imutáveis (usando symbols de Ruby)

Fala pessoal, tudo bem?

Hoje vou trazer um post mais curtinho, mas com uma pegada bem legal!

Vamos explicar a diferença entre objetos mutáveis e imutáveis na prática, usando como exemplo os Symbols da linguagem de programação Ruby. Esse post poderia chamar “Diferença entre symbol e string literal em Ruby”, mas você não precisa saber Ruby para entender.

O que é um Symbol?

Quem tem alguma experiência com Ruby, já está familiarizado com a utilização de Symbols, então se não quiser uma explicação básica sobre o que são e como funcionam os Symbols, pode pular esse tópico :)

Um Symbol em Ruby é algo parecido com uma string, mas uma string imutável, isto é, que não pode ser modificado. Os Symbols são mais utilizados como identificadores, seja de métodos, atributos ou keys numa hash. Por exemplo:

hash = {}
hash[:foo] = 1
puts hash[:foo]
# => 1

Você não utiliza um Symbol pra guardar informações, como por exemplo um nome ou um endereço, pra isso nós temos as strings. Por outro lado, podemos usar uma string no lugar de um symbol na maioria das situações. Inclusive, é possível obter o mesmo resultado do código anterior usando uma string:

hash = {}
hash['foo'] = 1
puts hash['foo']
# => 1

Agora talvez você esteja se perguntando, qual a utilidade desses Symbols, sendo que eu posso fazer a mesma coisa com a boa e velha string?

Diferenças entre Symbol (imutável) e String (mutável)

Existe mais de um motivo para usarmos o Symbol, acho que pra todo programador Ruby, o primeiro motivo que vem na cabeça é a beleza. Sim, a beleza hehe. Com os symbols, o código fica mais organizado, quando você vê um symbol, você sabe que aquilo é um identificador pra algo do seu código. Por outro lado, ao se deparar com uma string, o programador sabe que aquilo trata-se de uma informação, algo que provavelmente vai ser visível para o usuário do programa. Vejamos um código simples que valida um campo e adiciona uma mensagem de erro numa hash (se necessário):

if campo_inteiro > 10_000
  erros[:campo_inteiro] = 'Campo inteiro não pode ser maior que 10.000!'
end

Aqui fica bem claro o uso do Symbol pra melhorar a legibilidade, especialmente em editores que destacam strings em cores diferentes do código comum. O Symbol é utilizado pra identificar o nome do campo na hash de erros. Já a string é utilizada pra criar a mensagem que vai aparecer para o usuário.

Outra vantagem dos Symbols é sua performance. Por conta dos Symbols serem imutáveis por padrão, o interpretador do Ruby faz caches dos Symbols e então só instancia um Symbol uma única vez, isto é, o objeto :campo_inteiro vai ser sempre o mesmo. Dá pra comprovar isso de forma bem simples:

:campo_inteiro.object_id
# => 200
:campo_inteiro.object_id
# => 200

Viu que o ID do objeto não mudou (esse ID é da instância do objeto, nada tem a ver com banco de dados)?

Já as strings não tem esse mesmo comportamento, cada vez que você cria uma string no código, o interpretador criar uma nova instância do objeto, mesmo que a string seja exatamente igual:

'campo_inteiro'.object_id
# => 200
'campo_inteiro'.object_id
# => 220

Reparou que o ID mudou, né?

Mas e por que o interpretador Ruby não trata essas strings da mesma forma que trata os Symbols? Ou seja, por que o interpretador não cria uma única instância de uma string, daí quando ela repetir não vai ser necessário criar uma nova instância?

A resposta é simples e já foi dita nesse post: strings são mutáveis em Ruby. Mas o que isso quer dizer na prática? Vamos pegar esse código de exemplo pra entender melhor:

foo = 'campo_inteiro'
bar = 'campo_inteiro'
puts foo.gsub('campo', 'atributo') # Substituir campo por atributo e depois imprimir o resultado
# => "atributo_inteiro"
puts bar # imprimir o conteúdo de bar
# => "campo_inteiro"

foo e bar receberam a mesma string, mas lembre-se, pra cada string o interpretador cria um objeto novo, então foo.object_id é diferente de bar.object_id. Agora vamos criar um cenário hipotético, onde o interpretador Ruby não cria uma nova instância quando a string é igual:

foo = 'campo_inteiro'
bar = 'campo_inteiro'
puts foo.gsub('campo', 'atributo') # Substituir campo por atributo e depois imprimir o resultado
# => "atributo_inteiro"
puts bar # imprimir o conteúdo de bar
# => "atributo_inteiro"

O resultado é ligeiramente diferente, mas o impacto pode ser grandioso. Aqui é um exemplo bobo, onde a string ‘campo_inteiro’ aparece duas vezes seguidas, mas imagine uma base código que tem a string ‘campo_inteiro’ espalhada por diversas classes e módulos e daí em algum momento essa string é alterada (como nesse código de exemplo), consegue perceber como isso poderia afetar todo o funcionamento do sistema?

Além disso, ainda existe a diferença de performance. No próximo post, nós vamos ver uma comparação na utilização de objetos mutáveis vs imutáveis.

Gostou do post? Conta aqui nos comentários pra gente. Fique a vontade para tirar dúvidas também, ficaremos felizes em responder!