Como symbols diferem de strings em Ruby
Tags: ruby, performance, beginners, optimization
Published: 2021-08-21 21:18:18

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](https://nandovieira.com.br/o-que-mudou-no-ruby-dois-ponto-tres), é 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.
<br/>  
## 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.
<br/>
Digamos que temos o seguinte código:

```
foo.add(bar)
```
<br/>
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.
<br/>
Digamos agora, que você rode o código dessa maneira:

```
method = :add
foo.send(method, bar)
```

> <sub> 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)** </sub>

<br/>
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**.
<br/> 
Agora, digamos que você rode esse código da seguinte maneira:

```
method = "add"
foo.send(method, bar)
```
<br/> 
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.
<br/>  
## 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.
<br/> 
Em métodos como **downcase**, **upcase**, **captilize**, **[]**, **size**, onde, por exemplo, em uma chamada de método:

```
:foo.upcase
# :FOO
```
<br/>
Seria necessário ao Ruby um processo similar à este para chegar ao resultado esperado:

```
:foo.to_s.upcase.to_sym
# :FOO
```
<br/> 
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.
<br/> 
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
```
<br/> 
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
```