C# Valor × Referência
Faça um teste: guarde um número numa variável, copie para outra e mude a cópia. A original não muda. Agora faça o mesmo com um objeto: mude a "cópia" e a original muda junto! Não é defeito. O C# tem dois tipos de tipo: os tipos de valor, em que a variável guarda o próprio dado, e os tipos de referência, em que a variável guarda o endereço de um objeto. Entender essa diferença evita uma família inteira de bugs.
- O que é um tipo de valor e um tipo de referência, e quais tipos são de cada família.
- O que acontece numa atribuição (
b = a) em cada caso. - O que acontece quando você passa cada um para um método.
- Por que os arrays e as listas se comportam como objetos, e por que o
stringparece um valor. - Quem pode ser
null(o erroCS0037). - Boxing e unboxing: guardar um valor dentro de um
object, e aInvalidCastException.
Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia: a fotocópia e o endereço
A Ana tem uma receita de bolo e quer passá-la para a Bia. Ela pode fazer de dois jeitos:
- Tirar uma fotocópia e dar a cópia. Se a Bia rabiscar a cópia dela, a receita da Ana continua limpa. Cada uma tem a sua. Isso é um tipo de valor.
- Passar o endereço do blog onde a receita está publicada. Agora as duas olham a mesma receita. Se a Bia (que também tem a senha) editar a página, a Ana vê a mudança. Isso é um tipo de referência: a variável guarda o "endereço" do objeto, não o objeto.
| Tipos de valor (a variável guarda o dado) | Tipos de referência (a variável guarda o endereço) |
|---|---|
int, long, double, decimal, bool, char | Toda class (as suas e as do .NET) |
struct (como DateTime) | string |
enum | Arrays (int[]), List<T>, Dictionary |
record struct, tuplas | interfaces, record, object, delegates |
Uma regra fácil para o começo: class é referência; números, bool, char, struct e enum são valor. Dá para perguntar ao .NET:
Console.WriteLine(typeof(int).IsValueType); Console.WriteLine(typeof(decimal).IsValueType); Console.WriteLine(typeof(DateTime).IsValueType); Console.WriteLine(typeof(string).IsValueType); Console.WriteLine(typeof(int[]).IsValueType); Console.WriteLine(typeof(List<int>).IsValueType);
$ dotnet run tipos.cs True « int True « decimal True « DateTime é uma struct False « string False « array False « List
Copiar um valor: cada um com o seu
int a = 5;
int b = a;
b = 10;
Console.WriteLine($"a = {a}, b = {b}");
- int a = 5;
- A variável
aguarda o número 5 dentro dela. - int b = a;
- O 5 é copiado para dentro de
b. A partir daqui,aebnão têm nada a ver uma com a outra. - b = 10;
- Muda só o
b.
$ dotnet run valor.cs a = 5, b = 10
Nada de surpresa: é o que você esperava desde a aula de Variáveis.
Copiar uma referência: os dois apontam para o mesmo objeto
Caderno c1 = new Caderno();
c1.Dono = "Ana";
Caderno c2 = c1;
c2.Dono = "Bia";
Console.WriteLine($"c1: {c1.Dono}, c2: {c2.Dono}");
class Caderno
{
public string Dono { get; set; } = "";
}
- Caderno c1 = new Caderno();
- O
newcria um objetoCadernona memória. A variávelc1não guarda o caderno; guarda o endereço dele (a "referência"). - Caderno c2 = c1;
- Copia o endereço, não o caderno. Agora
c1ec2são dois nomes para o mesmo objeto. Repare: não houvenew, então não nasceu caderno nenhum. - c2.Dono = "Bia";
- "Vá até o objeto para onde
c2aponta e troque o dono." É o mesmo objeto dec1.
$ dotnet run referencia.cs c1: Bia, c2: Bia
Um desenho ajuda:
variáveis objetos na memória
┌──────────┐
│ c1 ●────┼──────┐
└──────────┘ ▼
┌──────────┐ ┌────────────────┐
│ c2 ●────┼──▶│ Caderno │
└──────────┘ │ Dono = "Bia" │
└────────────────┘
┌──────────┐
│ a 5 │ « um int guarda o próprio número
└──────────┘
Em livros e entrevistas você vai ouvir que "tipos de valor ficam na pilha (stack) e objetos ficam no heap". É uma boa imagem para começar: as variáveis locais ficam numa área rápida e organizada (a pilha), e os objetos criados com new ficam numa área maior, cuidada pelo coletor de lixo (o heap). Mas não é a regra inteira: um int que é campo de uma classe mora dentro do objeto, no heap. O que define a família é o comportamento na cópia, não o lugar.
Missão: a Ana quer que a Bia tenha um caderno dela. Troque Caderno c2 = c1; por um caderno novo (new Caderno()), para a saída ficar c1: Ana, c2: Bia.
Caderno c1 = new Caderno();
c1.Dono = "Ana";
Caderno c2 = c1;
c2.Dono = "Bia";
Console.WriteLine($"c1: {c1.Dono}, c2: {c2.Dono}");
class Caderno
{
public string Dono { get; set; } = "";
}
Caderno c1 = new Caderno();
c1.Dono = "Ana";
Caderno c2 = new Caderno();
c2.Dono = "Bia";
Console.WriteLine($"c1: {c1.Dono}, c2: {c2.Dono}");
class Caderno
{
public string Dono { get; set; } = "";
}
Passar para um método
Passar um argumento para um método é uma atribuição disfarçada: o parâmetro recebe uma cópia do que está na variável. Para um valor, é a cópia do dado. Para uma referência, é a cópia do endereço.
int pontos = 10;
DobrarNumero(pontos);
Console.WriteLine($"pontos = {pontos}");
Placar placar = new Placar { Pontos = 10 };
DobrarPlacar(placar);
Console.WriteLine($"placar.Pontos = {placar.Pontos}");
void DobrarNumero(int numero)
{
numero = numero * 2;
}
void DobrarPlacar(Placar p)
{
p.Pontos = p.Pontos * 2;
}
class Placar
{
public int Pontos { get; set; }
}
$ dotnet run metodos.cs pontos = 10 « o método dobrou a cópia placar.Pontos = 20 « o método mexeu no mesmo objeto
- DobrarNumero(pontos)
- O parâmetro
numerorecebe uma cópia do 10. O método dobra a cópia, e ela some quando o método termina. A variávelpontosnem fica sabendo. - DobrarPlacar(placar)
- O parâmetro
precebe uma cópia do endereço.peplacarapontam para o mesmo objeto, entãop.Pontos = …muda o placar de verdade.
Mas trocar o objeto inteiro não funciona
O método recebeu uma cópia do endereço. Se ele fizer o parâmetro apontar para outro objeto, a variável de fora continua apontando para o antigo:
Placar placar = new Placar { Pontos = 10 };
Zerar(placar);
Console.WriteLine($"placar.Pontos = {placar.Pontos}");
void Zerar(Placar p)
{
p = new Placar();
p.Pontos = 0;
}
class Placar
{
public int Pontos { get; set; }
}
$ dotnet run trocar.cs placar.Pontos = 10 « o Zerar mexeu num placar novo, que ninguém viu
O p = new Placar(); fez só o p apontar para um placar novo. Se você precisa que um método troque a variável de quem chamou, existe o ref (veja ref, out e in). Mas, quase sempre, o melhor é o método devolver o objeto novo com return.
Arrays e listas são referências
Um array é um objeto, mesmo que seja de int. Copiar a variável copia o endereço:
int[] notas = [7, 8, 9];
int[] copia = notas;
copia[0] = 10;
Console.WriteLine(string.Join(", ", notas));
int[] deVerdade = (int[])notas.Clone();
deVerdade[1] = 0;
Console.WriteLine(string.Join(", ", notas));
Console.WriteLine(string.Join(", ", deVerdade));
$ dotnet run arrays.cs 10, 8, 9 « mudar a "copia" mudou as notas 10, 8, 9 « o Clone criou um array novo: as notas ficaram 10, 0, 9
- int[] copia = notas;
- Não copia nada:
copiaé outro nome para o mesmo array. - (int[])notas.Clone()
- O
Clone()cria um array novo, com os mesmos elementos. Ele devolveobject, por isso o(int[])na frente (veja Conversão de Tipos). Com listas, usenew List<int>(lista)oulista.ToList().
E o string?
O string é um tipo de referência, mas parece um valor:
string s1 = "Ana";
string s2 = s1;
s2 = s2 + " Souza";
Console.WriteLine($"s1 = {s1}, s2 = {s2}");
$ dotnet run texto.cs s1 = Ana, s2 = Ana Souza
O segredo é que um string é imutável: nenhum método muda o texto que já existe (veja Strings). O s2 + " Souza" cria um texto novo e faz o s2 apontar para ele, igual ao p = new Placar() de antes. O s1 continua apontando para o "Ana" antigo. Como ninguém consegue mudar um texto "por dentro", compartilhar a referência nunca causa surpresas. E o == do string compara o conteúdo, não o endereço.
Quem pode ser null
null quer dizer "não aponta para nada". Só faz sentido para referências: um endereço pode estar vazio, mas um int sempre guarda um número.
int idade = null;
$ dotnet run nulo.cs /home/ana/cs/nulo.cs(1,13): error CS0037: Não é possível converter o valor nulo em 'int' porque ele não é um tipo de valor não anulável Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
"Porque ele não é um tipo de valor não anulável" parece dizer o contrário do que deveria. O original é Cannot convert null to 'int' because it is a non-nullable value type: "porque int é um tipo de valor que não aceita null". Para um valor que pode faltar, use int? (veja null e Tipos Anuláveis).
Boxing: um valor dentro de uma caixa
A variável object é uma referência: ela guarda um endereço. Então, como object caixa = 42; funciona, se o 42 é um valor? O C# faz um truque chamado boxing ("encaixotar"): cria um objeto no heap, coloca uma cópia do 42 dentro e guarda o endereço da caixa. O caminho de volta, tirar o valor da caixa, é o unboxing.
int n = 42;
object caixa = n;
n = 7;
Console.WriteLine($"n = {n}, caixa = {caixa}");
int deVolta = (int)caixa;
Console.WriteLine(deVolta + 1);
- object caixa = n;
- Boxing: a caixa recebe uma cópia do 42. Acontece sozinho, sem você escrever nada.
- n = 7;
- Muda o
n. A caixa tem a sua própria cópia e continua com 42. - (int)caixa
- Unboxing: tira o valor da caixa. Precisa do cast
(int), porque o compilador só sabe que ali há "umobject".
$ dotnet run boxing.cs n = 7, caixa = 42 43
Sem o cast, o compilador recusa:
$ dotnet run semcast.cs /home/ana/cs/semcast.cs(2,9): error CS0266: Não é possível converter implicitamente tipo "object" em "int". Existe uma conversão explícita (há uma conversão ausente?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O unboxing tem de usar o tipo exato
Uma caixa com um int só abre como int. Nem um long, que cabe um int com folga, serve:
object caixa = 42; long grande = (long)caixa; Console.WriteLine(grande);
$ dotnet run unbox.cs Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Int64'. at Program.<Main>$(String[] args) in /home/ana/cs/unbox.cs:line 2 $ echo $? 134
Desta vez compilou, e o erro só apareceu rodando: uma exceção (em inglês, como toda exceção do .NET no Linux). "Não é possível converter um objeto do tipo Int32 (o int) para o tipo Int64 (o long)." O conserto é abrir a caixa com o tipo certo e, só depois, converter: long grande = (int)caixa;. Se você não sabe o que há na caixa, pergunte com is: if (caixa is int numero) { … }.
Por que evitar
Cada boxing cria um objeto novo na memória, e isso custa tempo. Num laço com milhões de voltas, faz diferença. Por isso o C# moderno usa generics: uma List<int> guarda os int direto, sem caixas, enquanto a antiga ArrayList (de antes dos generics) guardava tudo como object. Regra prática: use o tipo certo, não object, e o boxing quase nunca aparece.
Experimente: rode e veja a InvalidCastException e o código de saída 134. Depois, conserte de dois jeitos: com (int)caixa e com if (caixa is int numero). Teste também object caixa = 42L; (o L faz o número ser long). No editor do guia, a mensagem da exceção sai mais curta, Specified cast is not valid. ("a conversão indicada não é válida"); o tipo da exceção, a linha e o 134 são os mesmos do dotnet.
object caixa = 42; long grande = (long)caixa; Console.WriteLine(grande);
Quando dá errado
O que aconteceu: não havia cópia. b = a com um objeto, um array ou uma lista só copia o endereço.
Como resolver: crie um objeto novo de propósito: new Caderno { Dono = c1.Dono }, (int[])notas.Clone(), new List<string>(lista). Para classes suas, um método Copiar() ajuda (é a missão final). Ou use um record, que tem o with para fazer cópias com mudanças.
O que aconteceu: o método recebeu uma cópia: do número (tipo de valor) ou do endereço (e trocou o parâmetro por outro objeto com new).
Como resolver: faça o método devolver o resultado (pontos = Dobrar(pontos);) ou, se for mesmo necessário, use ref.
$ dotnet run nulo.cs /home/ana/cs/nulo.cs(1,13): error CS0037: Não é possível converter o valor nulo em 'int' porque ele não é um tipo de valor não anulável
Como resolver: tipos de valor não aceitam null. Use int? se o valor pode faltar.
System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Int64'.
O que aconteceu: um unboxing com o tipo errado.
Como resolver: abra a caixa com o tipo exato ((int)caixa) ou teste com is. Melhor ainda: evite guardar valores em object.
Referência
| Tipo de valor | Tipo de referência | |
|---|---|---|
| Exemplos | int, double, decimal, bool, char, struct, enum | class, string, arrays, List, object, record |
| A variável guarda | O próprio dado | O endereço de um objeto |
b = a; | Copia o dado: dois independentes | Copia o endereço: o mesmo objeto |
| Passar para um método | O método recebe uma cópia do dado | O método recebe uma cópia do endereço e pode mudar o objeto |
Pode ser null? | Não (CS0037); use int? | Sim (string? com referências anuláveis) |
Guardado em object | Boxing: vira uma cópia numa caixa | Só o endereço é guardado |
| Perguntar | typeof(T).IsValueType | |
Exercícios
1. Valor
int x = 3; int y = x; y = 8; Quanto vale x?
2. Referência
int[] a = [1, 2]; int[] b = a; b[0] = 9; Quanto vale a[0]?
3. Abrir a caixa
Complete para tirar o número de object caixa = 10;:
4. Qual destes é um tipo de referência?
5. void Zerar(Placar p) { p = new Placar(); } é chamado com Zerar(meuPlacar);. O que acontece com meuPlacar?
6. object caixa = 42; long g = (long)caixa; O que acontece?
Missão final: a Bia quis começar o carrinho dela a partir do da Ana e acrescentar um chá. Mas o método Copiar() devolve this (o próprio objeto), e o chá foi parar no carrinho da Ana também. Rode e veja. Conserte o Copiar(): ele deve criar um new Carrinho(), colocar nele os itens deste carrinho (novo.Itens.AddRange(Itens);) e devolver o novo. A saída deve ser Ana: Café e Bia: Café, Chá.
Carrinho daAna = new Carrinho();
daAna.Itens.Add("Café");
Carrinho daBia = daAna.Copiar();
daBia.Itens.Add("Chá");
Console.WriteLine($"Ana: {string.Join(", ", daAna.Itens)}");
Console.WriteLine($"Bia: {string.Join(", ", daBia.Itens)}");
class Carrinho
{
public List<string> Itens { get; } = [];
public Carrinho Copiar()
{
return this;
}
}
Carrinho daAna = new Carrinho();
daAna.Itens.Add("Café");
Carrinho daBia = daAna.Copiar();
daBia.Itens.Add("Chá");
Console.WriteLine($"Ana: {string.Join(", ", daAna.Itens)}");
Console.WriteLine($"Bia: {string.Join(", ", daBia.Itens)}");
class Carrinho
{
public List<string> Itens { get; } = [];
public Carrinho Copiar()
{
Carrinho novo = new Carrinho();
novo.Itens.AddRange(Itens);
return novo;
}
}
Pense no que aconteceria se o Copiar() fizesse novo.Itens = Itens; (se a propriedade tivesse set): os dois carrinhos teriam objetos diferentes, mas a mesma lista lá dentro, e o problema voltaria. Copiar o objeto de fora sem copiar as referências de dentro é chamado de cópia rasa; copiar tudo, até o fundo, é a cópia profunda.
Resumo
- Tipo de valor (
int,decimal,bool,struct,enum): a variável guarda o dado;b = afaz uma cópia independente. - Tipo de referência (
class,string, arrays, listas): a variável guarda o endereço;b = afaz dois nomes para o mesmo objeto. - Métodos recebem cópias: do dado ou do endereço. Pelo endereço, o método pode mudar o objeto, mas não trocar a variável de quem chamou.
- O
stringé referência, mas imutável, e por isso se comporta como valor. Tipos de valor não aceitamnull(CS0037); useint?. - Boxing põe uma cópia de um valor numa caixa
object; o unboxing exige o tipo exato (InvalidCastException). Prefira generics aobject. Próxima aula: Structs.