Um animal interessante, que aparece em linguagens como Java e Python, é o objeto imutável. Como tais linguagens, em particular Java, encorajam o programador a construir suas próprias classes de objetos, é importante entender o porquê de sua existência.
Como já foi abordado em outro artigo, algumas linguagens, em particular o C++, permitem ao programador criar seus próprios tipos. O tipo é criado como uma classe; é a forma de usar a classe que "converte-a" num tipo. Exemplo:
Classe a; // variáveis são objetos Classe b; b = a; // valor de "a" é copiado em "b", porém // continuam sendo variáveis independentes -- Classe* a; // variáveis são ponteiros para objetos Classe* b; a = new Classe; // "a" aponta agora para objeto em heap b = a; // "b" aponta para o mesmo objeto que "a", // portanto não é mais independente
Creio que C++ é a linguagem que oferece a ferramenta mais poderosa para criação de tipos. É realmente o Latim das linguagens: incrivelmente completo, e incrivelmente complexo...
Em outras linguagens, como Java, Object Pascal, Python, C#, etc. não existem tipos criados pelo usuário; apenas classes e seus objetos.
Exceto pela sintaxe de ponteiros, que não transparece nessas outras linguagens, a semântica de objetos é exatamente igual ao segundo exemplo em C++. Segue um exemplo em Python:
class qualquer: pass a = qualquer() b = a // "b" aponta para a mesma coisa que "a" a.modifica() // o estado do objeto apontado por "a" muda, // e portanto também o objeto apontado por "b".A semântica acima não seria muito popular se aplicada a objetos tipo string:
class string_quebrada: pass a = "rato" b = a a[3] = "a" // muda "rato" para "rata", mas a variável "b" // também será alterada pois aponta para o mesmo // objeto!
A priori, isso significa que, nestas linguagens, os únicos tipos existentes são os primitivos. Mas existe um truque que permite simular um tipo criado pelo usuário, e usando apenas objetos. Este truque é o objeto imutável.
Objeto imutável é o que não pode sofrer qualquer modificação em seu estado, depois de sua construção. A única forma de modificar um objeto contido por uma variável, é criar um novo e colocar no lugar do velho.
Vejamos o que acontece quando o objeto é imutável:
a = qualquer() b = a c = b // neste ponto, as 3 variáveis apontam para o // mesmo objeto. a = a.modifica() // métodos que antes modificavam o objeto agora // retornam um novo objeto. // Agora, "a" aponta para um objeto novo, // que é independente de "b" e "c" b = b.modifica() // "b" também é desvinculado do objeto original // apenas "c" ainda aponta para o original.
Com objetos imutáveis, temos um comportamento muito mais intuitivo, semelhante ao de um tipo primitivo. Esse mecanismo permite que Python e Java implementem strings como classes sem modificar a linguagem. Object Pascal preferiu oferecer um tipo String primitivo.
Idealmente, toda classe deve produzir objetos imutáveis, pois são mais seguros. Mas em alguns casos, isso não faz muito sentido. Exemplo: quando o objeto representa um recurso do sistema, como um arquivo aberto. Seria ruim que esse objeto se "multiplicasse". Esse tipo de objeto deve permanecer único, e ser mutável, mesmo que referenciado por diversas variáveis diferentes.
Um possível aspecto negativo de objetos imutáveis é a performance. Veja o que acontece com objetos string em C++ e Python:
// C++
std::string a;
for(int x = 0; x < 100000; ++x) {
a += "x";
}
O objeto-tipo "a" só precisa ser criado uma vez. As chamadas a "operator +=" apenas atualizam o objeto existente. Uma classe C++ bem-feita pode comportar-se como um tipo com máxima performance.
O mesmo acontece com Object Pascal onde string é um tipo primitivo. O compilador é esperto o suficiente para evitar a criação de novas strings.
// Python string a; for i in range(0,100000): a += "x"Como o objeto "a" é imutável, a operação
a += "x"é transformada internamente em
a = string.__add__(a, "x")
O método __add__ retorna uma nova string, que é a concatenação do objeto "a" e da string "x". Portanto, haverá pelo menos 100000 criações de objetos novos nesse loop. Nesse benchmark em particular, Java e Python perdem muito feio para C++.
Se, em C++, eu substituísse
a += "x"por
a = a + "x"a performance fica grandemente prejudicada, pois o "operator +" tem obrigação de retornar um objeto novo sem tocar nos originais. Com essa modificação, C++ praticamente empata com Python na performance (e C++ é compilado!).
Filosofando um pouco agora, podemos dizer que todos os tipos primitivos (inteiros, floats) são "imutáveis", mesmo em C++. Observe:
a = 5
Podemos dizer que o número 5 como um objeto imutável fornecido pelo compilador. Ao fazermos
a = a + 1não estaremos reformando 5, e sim somando 5 + 1, e atribuindo um novo objeto imutável que será o valor 6.
O compilador garante que os objetos imutáveis literais são "read-only", de modo que escrever
5 = anão seja aceitável.
Se fizermos o mesmo loop com o tipo Inteiro:
a = 0 for i in range(0, 100000): a = a + 1
O trabalho imposto sobre a CPU é o mesmo que no caso das strings; há 100000 criações de novos objetos inteiros. Mas por que, quando utilizamos variáveis inteiras, isso é tão rápido?
A resposta é simples: inteiros, caracteres e floats são pequenos e simples de criar. Também são suportados diretamente pela CPU, o que é muito mais rápido que suporte por software... Em linguagens compiladas, a = a + 1 vira uma única instrução de CPU.
Object Pascal e Clipper utilizam uma técnica de contagem de referências para otimizar o seu tipo primitivo string. As variáveis são, internamente, ponteiros, tais como as variáveis de objetos.
let a := "rato" let b := a; // aponta para a mesma string que "a" let c := ""; // otimização: string vazia é internamente // um ponteiro nulo! delete(a,4,1); // apaga 4o caractere // como a string tem 2 variáveis penduradas, // é necesário criar outra nova, e atribuir // para a variável "a" // neste ponto, a = "rat", b = "rato" Let a := a + "a"; // como a contagem de referência é igual a 1, // a soma pode atuar sobre "rat" diretamente // neste ponto, a = "rata", b = "rato", // e "rat" é liberado da memória. delete(b,4,1); // apaga 4o caractere // atua sobre a própria string "rato" pois // só existe uma referência para ela Let b := b + "os"; Let c := b; // Super rápido pois c era um ponteiro nulo // String "ratos" volta a ter 2 referências
É uma otimização possível para objetos imutáveis, pelo menos em linguagens compiladas como Pascal e C++, bem como código nativo para Java ou Python.
No exemplo, a função "delete" tem de ter comportamento duplo, conforme a contagem de referência da string. Isso pode ser trabalhoso e não valer a pena, se o objeto for pouco usado. Apenas objetos como strings geralmente valem tais cuidados com performance.