DEV Community

BrunoSDias
BrunoSDias

Posted on

Como symbols diferem de strings em Ruby

Em muitos casos, Ruby permite que realizemos operações voltadas à strings (string-like operations), tanto em um dado no formato String, quanto em um dado no formato Symbol, e apesar disso funcionar em ambos os casos, o processo para chegar a esse resultado é diferente.

Uma string é uma série de caracteres ou bytes, usada para armazenar textos ou dados binários, e, à não ser que seja uma frozen string, é possível modificá-lo.

Um symbol é um número com um identificador anexado, onde esse identificador é uma série de caracteres ou bytes (ex: :add).

Esses symbols são object wrappers (Classes que 'envolvem' outras afim de executar funções que não teriam em seu formato nativo) para um tipo interno do ruby chamado ID. A razão para symbols serem um object wrapper de ID se da pelo fato de que ID é do tipo integer e, para o computador, é muito mais rápido lhe dar com números inteiros ao invés de ter que lhe dar com uma série de caracteres ou bytes.

De String para Symbol

Ruby utiliza ID para referenciar variáveis locais, variáveis de instância, variáveis de classe, constantes e nomes de método.


Digamos que temos o seguinte código:

foo.add(bar)
Enter fullscreen mode Exit fullscreen mode



Ruby compilará este código, e para, foo, add e bar, verificará se já existe um ID associado (nesses casos foo, add e bar são os identificadores). Se já existir um ID, ele usará, se não, irá criar um novo ID e associará ao seu identificador.

Isso acontece durante o processo de compilação e esses IDs são embutidos diretamente (hardcoded) nas instruções da VM.


Digamos agora, que você rode o código dessa maneira:

method = :add
foo.send(method, bar)
Enter fullscreen mode Exit fullscreen mode

O método send chama o método identificado pelo symbol, então nesse caso o resultado seria o mesmo que chamar diretamente foo.add(bar)



Ruby compilará esse código, e para method, add, foo, send e bar também verificará se já existe um ID associado ou criará um novo ID se não existir.
Essa abordagem é um pouco mais lenta, já que Ruby terá que criar uma variável local e há também uma chamada indireta já que send terá que buscar o método para chamá-lo dinamicamente. No entanto, não há chamadas em tempo de execução para buscar por um ID.


Agora, digamos que você rode esse código da seguinte maneira:

method = "add"
foo.send(method, bar)
Enter fullscreen mode Exit fullscreen mode



Ruby compilará esse código, e para method, foo, send e bar também verificará se já existe um ID associado ou criará um novo ID se não existir. No entanto, durante a compilação, Ruby não criou um ID para add porque é uma string e não um symbol, e o método send está esperando um symbol como parâmetro.

Apesar disso, antes de Ruby disparar uma exceção (NoMethodError), ele tentará converter essa string a symbol, buscar um identificador, e criar caso não exista, e isso acontecerá toda vez que esse método for chamado, tornando assim o processo mais lento.

Isso não afeta apenas a Kernel#send, mas sim, a maior parte dos métodos onde os identificadores são passados dinamicamente, como por exemplo:
Module#define_method, Kernel#instance_variable_get e Module#const_get.

Como principio de boas práticas, em métodos como esse, é indicado passar symbols a eles, já que resultará em uma melhor performance.

De Symbol para String

Haverá casos também em que o programa esperará uma string e receberá um symbol, onde também será necessário converte-ló, e haverá perca de performance.


Em métodos como downcase, upcase, captilize, [], size, onde, por exemplo, em uma chamada de método:

:foo.upcase
# :FOO
Enter fullscreen mode Exit fullscreen mode



Seria necessário ao Ruby um processo similar à este para chegar ao resultado esperado:

:foo.to_s.upcase.to_sym
# :FOO
Enter fullscreen mode Exit fullscreen mode



Então, como saber quando usar cada um deles?

Como principío geral, use symbols quando você precisa de um identificador em seu código e use strings quando você precisa de textos ou dados.


Por exemplo, vamos supor que você precise selecionar uma configuração baseada em um case, nesse caso um symbol teria uma resposta melhor:

def switch(value)
case value
when :foo
# foo
when :bar
# bar
when :baz
# baz
end
Enter fullscreen mode Exit fullscreen mode



Já nesse outro caso, onde você está realizando uma formatação em uma entrada de dados, uma string seria o mais viável:

def append2(value)
  value.gsub(/foo/, "bar")
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)