A aula anterior nos deu o mapa da memória: valores simples vivem diretamente na variável (na pilha), objetos vivem no heap, e variáveis de objeto guardam apenas uma referência — um endereço — que aponta para eles. Agora colhemos o fruto desse conhecimento, enfrentando de frente um comportamento que confunde praticamente todo iniciante e que é fonte de bugs sutis: copiar um objeto não é o mesmo que copiar um número. Formalizaremos a distinção entre tipos por valor e tipos por referência, e você entenderá, na raiz, por que atribuir uma variável de objeto a outra faz as duas apontarem para a mesma coisa, por que passar um objeto a um método permite alterá-lo, e por que dois objetos "iguais" às vezes não são considerados o mesmo. É uma das aulas mais esclarecedoras do curso — e ela só faz sentido porque você já sabe onde cada coisa vive.
As duas famílias de tipos
Todo tipo em C# pertence a uma de duas famílias, que correspondem exatamente ao que vimos sobre a memória. Os tipos por valor guardam o dado diretamente na variável — quando você usa a variável, você tem o valor em si. São os números (int, double, decimal), o bool, o char. Os tipos por referência guardam na variável apenas uma referência (um endereço) que aponta para o dado real, que vive no heap. São os objetos que você cria a partir de classes (Pessoa, Produto), as listas (List), e — com uma ressalva que veremos — as strings. A distinção, que na aula anterior era sobre onde os dados vivem, agora ganha um nome e consequências práticas: tipos por valor contêm seu dado; tipos por referência apontam para ele.
A consequência decisiva: o que acontece ao copiar
Toda a importância dessa distinção se revela quando copiamos uma variável — o que acontece ao atribuir uma variável a outra. Comecemos com um tipo por valor, um número:
int a = 10;
int b = a; // copia o VALOR 10 para b; agora a e b têm cada um o seu 10
b = 99; // altero b...
Console.WriteLine(a); // 10 — 'a' NÃO mudou
Console.WriteLine(b); // 99
Com o número, a atribuição b = a copiou o valor 10 de fato: a e b ficaram com cópias independentes. Alterar b não tocou em a, pois cada um tem o seu próprio 10, em sua própria gaveta. Isso é intuitivo e esperado. Agora observe o mesmo padrão com um tipo por referência, um objeto:
class Caixa { public int Valor { get; set; } }
Caixa x = new Caixa();
x.Valor = 10;
Caixa y = x; // copia a REFERÊNCIA, não o objeto! x e y apontam para o MESMO objeto
y.Valor = 99; // altero através de y...
Console.WriteLine(x.Valor); // 99 — 'x' MUDOU também!
Console.WriteLine(y.Valor); // 99
Aqui está a surpresa. A atribuição y = x não copiou o objeto — copiou apenas a referência, o endereço. Lembrando a analogia da aula anterior: x e y viraram dois papéis com o endereço da mesma casa. Quando alteramos y.Valor = 99, fomos até aquela casa (o único objeto) e mudamos algo dentro dela — e como x aponta para a mesma casa, ele "vê" a mudança. Não há dois objetos; há um só objeto e duas variáveis apontando para ele. Este é o comportamento que a aula anterior antecipou, agora confirmado: com tipos por referência, copiar a variável copia o endereço, não o objeto, e as duas variáveis passam a compartilhar a mesma coisa.
Esta é, muito provavelmente, a maior fonte de confusão do iniciante com objetos, e vale gravar a lição: quando você atribui um objeto a outra variável, não cria uma cópia do objeto — cria outra referência ao mesmo objeto. Se você quiser de fato uma cópia independente, precisa criar um novo objeto e copiar os dados, o que é uma operação deliberada, não o que a simples atribuição faz.
O mesmo comportamento ao passar para métodos
Essa distinção reaparece — e causa as mesmas surpresas — quando passamos valores a métodos, pois passar um argumento é, em essência, uma atribuição ao parâmetro. Um tipo por valor chega ao método como cópia; alterá-lo lá dentro não afeta o original. Um tipo por referência chega como cópia do endereço; alterar o objeto apontado afeta o original:
static void MudarNumero(int n) { n = 99; }
static void MudarObjeto(Caixa c) { c.Valor = 99; }
int numero = 10;
MudarNumero(numero);
Console.WriteLine(numero); // 10 — inalterado (o número foi copiado)
Caixa caixa = new Caixa { Valor = 10 };
MudarObjeto(caixa);
Console.WriteLine(caixa.Valor); // 99 — ALTERADO! (o método recebeu a referência)
O método MudarNumero recebeu uma cópia do número, e mexer nela não tocou no original. Já MudarObjeto recebeu uma cópia da referência — que aponta para o mesmo objeto — e, ao alterar c.Valor, alterou o objeto real, visível de fora. Este comportamento é imensamente útil (permite que métodos modifiquem objetos que recebem), mas precisa ser compreendido, ou vira fonte de bugs: um iniciante que passa um objeto a um método esperando que o original fique intacto se surpreenderá. A regra: métodos podem alterar os objetos que recebem (tipos por referência), mas não os números que recebem (tipos por valor), porque uns compartilham o dado real e os outros recebem cópias.
Igualdade: mesmo conteúdo ou mesma coisa?
A distinção afeta ainda o significado de "igual". Para tipos por valor, comparar com == verifica se os conteúdos são iguais: 5 == 5 é verdadeiro porque os valores coincidem. Para tipos por referência, == verifica, por padrão, se as duas variáveis apontam para o mesmo objeto — a mesma casa —, e não se os objetos têm o mesmo conteúdo:
Caixa c1 = new Caixa { Valor = 10 };
Caixa c2 = new Caixa { Valor = 10 }; // outro objeto, mesmo conteúdo
Console.WriteLine(c1 == c2); // False — são objetos DIFERENTES (casas diferentes)
Caixa c3 = c1; // c3 aponta para o mesmo objeto que c1
Console.WriteLine(c1 == c3); // True — é o MESMO objeto
c1 e c2 têm o mesmo conteúdo (ambos com Valor 10), mas são objetos distintos, em endereços diferentes — duas casas idênticas por dentro, mas casas separadas —, então == os considera diferentes. Já c3, que recebeu a referência de c1, aponta para a mesma casa, então c1 == c3 é verdadeiro. Para tipos por referência, portanto, == pergunta "é o mesmo objeto?", não "têm o mesmo conteúdo?". (É possível ensinar uma classe a comparar por conteúdo, e existem tipos especiais que já o fazem, mas isso fica para mais adiante; por ora, saiba que o comportamento padrão de == para objetos é comparar identidade, não conteúdo.)
A exceção conveniente: a string
Vale uma nota sobre a string, que temos usado desde o começo. Tecnicamente, a string é um tipo por referência — mas ela foi projetada com um comportamento especial: o == entre strings compara conteúdo, não identidade. Por isso "oi" == "oi" é verdadeiro, como você esperaria intuitivamente, mesmo sendo a string um tipo por referência. Essa é uma conveniência deliberada, porque comparar textos por conteúdo é quase sempre o que queremos. Mencione a string como o "caso especial que confirma a regra": ela é a exceção projetada para se comportar de forma intuitiva, enquanto os objetos comuns que você cria seguem a regra geral de comparar por identidade. Não se preocupe em decorar todas as sutilezas; guarde que a string "se comporta bem" na comparação, e que seus próprios objetos, por padrão, comparam por identidade.
A divisão mais importante do sistema de tipos do C# ficou clara. Tipos por valor guardam o próprio conteúdo; tipos por referência guardam um endereço. E a consequência decisiva aparece na cópia: copiar um valor produz um duplicado independente, enquanto copiar uma referência produz outro apontador para o mesmo objeto — alterar por um dos nomes altera o que o outro enxerga.
O mesmo vale ao passar argumentos para métodos, e é por isso que um método pode modificar um objeto recebido e a mudança permanecer visível fora dele. A string é a exceção conveniente: tecnicamente um tipo por referência, ela é imutável e, por isso, comporta-se na prática como se fosse um tipo por valor.
Fontes e leituras recomendadas
- Tipos por valor — a referência oficial dos tipos que contêm seu dado diretamente; a base desta aula.
- Tipos por referência — a contraparte: tipos cuja variável aponta para o dado no heap.
- Passagem de argumentos a métodos — como valor e referência se comportam ao entrar num método.
- Comparações de igualdade — identidade versus conteúdo, e por que a
stringé especial. - Igualdade de referências e de valores — o comportamento padrão de
==para objetos.
Exercícios
Exercício 1
Reproduza os dois primeiros exemplos da aula (o do int e o da Caixa) e confirme as saídas. Depois, com suas palavras, explique por que alterar b não afetou a no caso do número, mas alterar y.Valor afetou x.Valor no caso do objeto.
Ver resposta
✓ Resposta: Alterar b não afetou a no caso do número porque int é um tipo por valor: a atribuição b = a copiou o próprio valor 10, dando a a e b gavetas independentes, cada uma com o seu 10; mexer numa não toca na outra. Alterar y.Valor afetou x.Valor no caso do objeto porque Caixa é um tipo por referência: a atribuição y = x copiou apenas a referência (o endereço), de modo que x e y apontam para o mesmo objeto no heap; alterar o objeto através de y altera a única coisa que ambos referenciam, e x "vê" a mudança.
Exercício 2
Crie uma classe Ponto com properties X e Y. Crie um objeto p1, atribua-o a p2 (Ponto p2 = p1;), e altere p2.X. Verifique que p1.X também mudou, e explique por quê, usando a ideia de referência.
Ver resposta
✓ Resposta: Exemplo:
class Ponto { public int X { get; set; } public int Y { get; set; } }
Ponto p1 = new Ponto { X = 1, Y = 2 };
Ponto p2 = p1; // p2 recebe a referência de p1
p2.X = 99;
Console.WriteLine(p1.X); // 99 — mudou também
p1.X mudou porque Ponto p2 = p1 não criou um novo ponto; copiou a referência, fazendo p1 e p2 apontarem para o mesmo objeto. Alterar p2.X alterou o único objeto existente, que p1 também referencia — logo, p1.X reflete a mudança.
Exercício 3
Escreva um método void Zerar(Caixa c) que faça c.Valor = 0, e um método void Zerar(int n) que faça n = 0. Chame ambos e mostre que o objeto foi alterado mas o número não. Explique a diferença com base em tipos por valor e por referência.
Ver resposta
✓ Resposta: Exemplo:
void Zerar(Caixa c) { c.Valor = 0; }
void Zerar(int n) { n = 0; }
Caixa caixa = new Caixa { Valor = 50 };
Zerar(caixa);
Console.WriteLine(caixa.Valor); // 0 — objeto alterado
int numero = 50;
Zerar(numero);
Console.WriteLine(numero); // 50 — número inalterado
A diferença: Zerar(Caixa c) recebeu uma cópia da referência, que aponta para o mesmo objeto; alterar c.Valor alterou o objeto real, visível fora do método. Zerar(int n) recebeu uma cópia do valor; alterar n mexeu só na cópia local, deixando o original intacto. Objetos (tipos por referência) são alteráveis pelos métodos que os recebem; números (tipos por valor) não.
Exercício 4
Crie dois objetos Caixa com o mesmo Valor e compare-os com ==. O resultado é True ou False? Depois, atribua um deles a uma terceira variável e compare essa terceira com o original. Explique os dois resultados em termos de "mesmo objeto" versus "mesmo conteúdo".
Ver resposta
✓ Resposta: Exemplo:
Caixa a = new Caixa { Valor = 10 };
Caixa b = new Caixa { Valor = 10 };
Console.WriteLine(a == b); // False
Caixa c = a;
Console.WriteLine(a == c); // True
a == b é False porque, embora tenham o mesmo conteúdo (Valor 10), são objetos distintos no heap, em endereços diferentes; o == padrão para objetos compara identidade (se é o mesmo objeto), não conteúdo. a == c é True porque c = a fez c apontar para o mesmo objeto que a — é a mesma coisa, então a comparação de identidade dá verdadeiro. Em resumo: a e b são "casas idênticas mas separadas"; a e c são "a mesma casa".
Exercício 5
Sem código: um colega criou um objeto configuracao, fez var backup = configuracao; achando que estava fazendo uma cópia de segurança, e depois alterou configuracao. Ele reclama que o "backup" também mudou. Explique o que realmente aconteceu e o que ele precisaria fazer para obter uma cópia de verdade.
Ver resposta
✓ Resposta: O que realmente aconteceu é que var backup = configuracao; não criou uma cópia do objeto — como configuracao é um tipo por referência, a atribuição copiou apenas a referência, fazendo backup e configuracao apontarem para o mesmo objeto. Por isso, quando ele alterou configuracao, o "backup" pareceu mudar também: não havia dois objetos, apenas duas variáveis referindo-se a um só. Para obter uma cópia de verdade, ele precisaria criar um novo objeto e copiar, um a um, os dados do original para ele (ou usar um mecanismo de clonagem projetado para isso) — uma operação deliberada de duplicação, e não a simples atribuição, que apenas compartilha a referência.