Site menu Objetos imutáveis
e-mail icon
Site menu

Objetos imutáveis

e-mail icon

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.

Algumas linguagens, em particular o C++, permitem ao programador criar seus próprios tipos. O tipo é implementado como uma classe. A forma de usar a classe é que realmente faz dela um tipo. Exemplo:

// Usando Classe como tipo
Classe a;
Classe b;

b = a;		// valor de "a" é copiado para "b",
		// a e b são objetos independentes

// usando Classe como classe
Classe* a;
Classe* b;


a = new Classe();
b = a;		// "b" aponta para o mesmo objeto que "a"

C++ é uma linguagem particularmente poderosa para criar novos tipos. É possível inclusive forçar o uso da classe de um jeito ou de outro, basta tornar privados os construtores certos. Por exemplo, se o operador new for "privatizado", ele não pode mais ser usado e assim tangemos os usuários para a primeira forma.

Em quase todas as outras linguagens em uso corrente, como Python, Java, Object Pascal, C#, Go, Javascript, etc. as instâncias de uma classe não podem ser forçadas a funcionar como tipos nativos. Ainda que e.g. Python, Java e Javascript não explicitem ponteiros, a semântica dos objetos é exatamente igual ao segundo exemplo em C++, onde os objetos são manipulados como ponteiros.

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".

Esta semântica 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

O que parece, é que tais linguagens não podem ter seu conjunto de tipos estendido. Mas na verdade podem, mediante um truque: tornar imutáveis os objetos da classe-tipo.

Objeto imutável é o que não pode sofrer qualquer modificação em seu estado depois da construção. A única forma de modificar um objeto contido por uma variável é criar um novo.

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 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.

c.modifica()     // não faz nada útil porque o objeto original
		 // permanece em "c" e o objeto modificado foi
		 // retornado mas não foi armazenado.

Com objetos imutáveis, temos um comportamento muito mais intuitivo, semelhante ao de um tipo primitivo. Esse mecanismo permite que e.g. Python implemente strings como classes sem modificar a linguagem. Na verdade, Python implementa todos os tipos primitivos como classes!

É claro, outros recursos de linguagem são necessárias para que classes imutáveis tenham a mesma ergonomia dos tipos primitivos. O recurso mais importante é a sobrecarga de operadores. C++ e Python têm, porém Java e Javascript não têm, e a ergonomia dos tipos não-primitivos é bem pior em Java e Javascript.

Se a implementação de uma classe pode escolher entre produzir objetos imutáveis ou mutáveis, sempre deveria adotar a primeira opção, pois é muito mais segura e ergonômica. Nem sempre isso faz sentido, é claro. Quando o objeto representa um recurso do sistema, como um arquivo aberto, não faz sentido ele ser imutável, e sempre deve ser copiado por referência.

Um possível aspecto negativo de objetos imutáveis é a performance. Observe o seguinte código em C++:

std::string a;

for(int x = 0; x < 100000; ++x) {
	a = a + "x";
}

O código acima é extremamente ineficiente porque cria 100 mil objetos, desperdiçando 99999 deles já que apenas o último nos interessa. Tem como fazer melhor?

for(int x = 0; x < 100000; ++x) {
	a += "x";
}

A eficiência do código acima vai depender da implementação da classe. Se ela sobrecarrega apenas o operador "+", será tão ineficiente quanto o caso anterior. Se a classe sobrecarrega o operador "+=", pode reaproveitar sempre o mesmo objeto.

Operador infix é um caso onde podemos quebrar a regra da imutabilidade com segurança. Além de C++, Python também oferece a opção de sobrecarregar os operadores + e += separadamente.

Como disse Bjarne Stroustroup, criador da linguagem C++, "faça como os números fazem". Ou seja, quando o desenvolvedor está implementando uma classe-tipo, o objetivo deve ser fazer com que esse tipo pareça com um tipo numérico, tanto quanto possível.

A imutabilidade é um aspecto importante dessa parecença: o número 5 é um objeto imutável. Se eu fizer 5+1, o resultado não é um 5 modificado, mas sim outro objeto: o 6. No caso específico dos tipos numéricos, os operadores "+" e "+=" tendem a possuir a mesma implementação, porque e.g. somar dois números é tarefa executada diretamente pela CPU.

(Stroustroup também tinha em mente os abusos da sobrecarga de operador. Por exemplo, faz sentido o meu tipo implementar "soma" e "subtração"? Se for um tipo numérico complexo, tipo quaternions, sem dúvida faz sentido. Se for string, "soma" pode ser concatenação, mas "subtração" de strings já seria um abuso de metáfora.)

e-mail icon