CodeGym /Cursos /C# SELF /Tipos por referência e tipos por valor

Tipos por referência e tipos por valor

C# SELF
Nível 12, Lição 3
Disponível

1. Introdução

Imagina que você tem uma caixa. Se essa caixa tem o objeto dentro dela (tipo uma caixa com uma maçã dentro), isso é parecido com um tipo por valor. Os dados estão direto nessa "caixa" (variável).

Por outro lado, imagina que você tem um cartão de visita com um endereço. O cartão não é a casa, ele só mostra onde achar a casa. Isso é tipo um tipo por referência. A variável, nesse caso, não guarda os dados em si, mas sim o "cartão" – o endereço na memória onde esses dados estão.

Exemplos bem simples:

  • int x = 5; // Tipo por valor: a variável x "guarda" o número 5 direto nela.
  • string name = "Vasya"; // Tipo por referência: a variável name "guarda" uma referência pra string "Vasya", que tá em algum lugar da memória.

Quem é quem nessa história?

Pra facilitar sua vida, aqui vai uma lista geral de quais categorias os principais tipos de dados entram:

Tipos por valor (Value Types):
  • Tipos primitivos: int, double, float, bool, char, byte, short, long, decimal e por aí vai.
  • Structs (struct): Todas as structs que você cria usando a palavra-chave struct.
  • Enums (enum): Tipos que deixam você definir um conjunto de constantes com nome.
Tipos por referência (Reference Types):
  • Strings (string): Mesmo que string tenha umas particularidades (ela é imutável), ela é tipo por referência.
  • Todos os arrays: Tipo int[], string[], YourCustomClass[].
  • Todas as classes (class): Todas as classes que você cria usando class.
  • Delegates (delegate): Tipos que representam referências pra métodos.
  • Interfaces (interface): Mesmo que interface não seja um objeto, uma variável do tipo interface pode guardar uma referência pra um objeto que implementa essa interface.
  • Listas, dicionários e outras coleções: Tipo List<T>, Dictionary<TKey, TValue>.
  • E basicamente, tudo que não é struct ou enum é tipo por referência em C#.

2. Copiando variáveis

É aqui que mora a diferença prática. Quando você atribui uma variável pra outra, o que realmente é copiado?

A) Copiando tipo por valor (Value Type)

Quando você copia um tipo por valor, é criada uma cópia independente dos dados. É tipo tirar uma xerox de um documento: mudar uma cópia não afeta o original nem as outras cópias.


    int a = 10;
int b = a; 					// b agora também é 10, mas é "sua própria cópia"
Console.WriteLine($"Valores iniciais: a = {a}, b = {b}"); // Valores iniciais: a = 10, b = 10

b = 15; 					// Mudando b
Console.WriteLine($"Depois de mudar b: a = {a}, b = {b}"); // Depois de mudar b: a = 10, b = 15

Explicação: A variável b ganhou sua própria cópia do valor 10. Quando b virou 15, a ficou com o valor original 10. Elas são totalmente independentes.

B) Copiando tipo por referência (Reference Type)

Quando você copia um tipo por referência, só a referência é copiada, ou seja, o "endereço" do objeto na memória. As duas variáveis agora apontam pro mesmo objeto. É como se você desse o mesmo cartão de visita pra duas pessoas: os dois sabem o endereço da mesma casa. Se um deles pintar a parede da casa, o outro, quando for lá, vai ver a parede pintada também.

Vamos ver isso com arrays, porque fica bem claro (strings têm uns detalhes próprios).


int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // arr2 e arr1 apontam pro mesmo array na memória!

// Valores iniciais: arr1[0] = 1, arr2[0] = 1
Console.WriteLine($"Valores iniciais: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}"); 

arr2[0] = 42; // Mudando o elemento do array via arr2

// Depois de mudar arr2[0]: arr1[0] = 42, arr2[0] = 42
Console.WriteLine($"Depois de mudar arr2[0]: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}"); 

Explicação: As duas variáveis (arr1 e arr2) têm a referência pro mesmo array na memória. Quando você muda arr2[0], você tá mudando o array que as duas apontam. Por isso arr1[0] também mostra o valor novo.

Detalhe com strings (string)

Strings em C# são tipo por referência, mas se comportam diferente porque são imutáveis (immutable). Isso quer dizer que, depois de criada, a string não muda. Qualquer operação que parece "mudar" a string (tipo concatenação, Replace()), na real cria uma nova string na memória.


string str1 = "Hello";
string str2 = str1; // str2 aponta pro mesmo objeto "Hello" que str1

// str1 = "Hello", str2 = "Hello"
Console.WriteLine($"Valores iniciais: str1 = \"{str1}\", str2 = \"{str2}\""); 

str2 = "Bye"; // Aqui é criado UM NOVO objeto "Bye", e str2 passa a apontar pra ele

// str1 = "Hello", str2 = "Bye"
Console.WriteLine($"Depois de mudar str2: str1 = \"{str1}\", str2 = \"{str2}\""); 

Explicação: No começo, str1 e str2 apontavam pro mesmo objeto "Hello". Quando str2 virou "Bye", o C# não mudou o objeto "Hello". Ele criou um novo objeto "Bye" na memória, e str2 agora aponta pra esse novo. str1 continua apontando pro antigo "Hello". Isso confunde muita gente que tá começando.

3. Tabela das principais diferenças

Característica Tipo por Valor
(tipo struct, int)
Tipo por Referência
(tipo class, string, array)
O que é copiado O valor em si ("xerox" dos dados) Referência pro objeto ("endereço" na memória)
Ligação entre as cópias Não tem, as cópias são totalmente independentes. Mudar uma não afeta a outra. Sim, todas as referências apontam pro mesmo objeto. Mudança no objeto por uma referência é vista por todas.
Pode ser null? Não (só tipos Nullable, tipo int?). Sempre tem valor. Sim. Pode apontar pra "nada" (null). Tentar acessar um objeto nullNullReferenceException.
Como é declarado struct, todos os tipos primitivos (int, bool etc.), enum class, interface, delegate, array, string, object
Como é limpo É removido da stack automaticamente quando sai do escopo. É limpo pelo Garbage Collector quando ninguém mais aponta pra ele.

4. Exemplo em um app

Imagina que a gente tá fazendo um app de console simples pra um questionário de usuário. Temos uma struct pros pontos do exame e uma classe pro perfil do usuário.

// Tipo por valor: struct pra guardar pontos
struct Score
{
    public int Points;
    public string Grade; // Só pra ilustrar
}

// Tipo por referência: classe pro perfil do usuário
class User
{
    public string Name;
    public Score ExamScore; // Struct aninhada
}

Copiando struct (Score)

Score score1 = new Score { Points = 100, Grade = "A" };
Score score2 = score1; // Todo o conteúdo de score1 é copiado pra score2
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=100, Grade=A

score2.Points = 88;
score2.Grade = "B";

Console.WriteLine("--- Depois de mudar score2 ---");
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A (não mudou!)
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=88, Grade=B

Resultado: score1 ficou igualzinho. Isso porque quando você faz Score score2 = score1;, o conteúdo de score1 (todos os campos) é copiado pra score2. Agora cada variável tem seu próprio conjunto de dados.

Copiando classe (User)

User u1 = new User 
{ 
    Name = "Anna", 
    ExamScore = new Score { Points = 95, Grade = "A" } 
};
User u2 = u1; // Agora u2 e u1 apontam pro MESMO usuário na memória!
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}"); // u1: Name=Anna, Score=95
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}"); // u2: Name=Anna, Score=95

u2.Name = "Ivan"; // Mudando o nome via u2
u2.ExamScore.Points = 60; // Mudando os pontos via u2
u2.ExamScore.Grade = "C";

Console.WriteLine("--- Depois de mudar u2 ---");
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}, Grade={u1.ExamScore.Grade}"); // u1: Name=Ivan, Score=60, Grade=C
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}, Grade={u2.ExamScore.Grade}"); // u2: Name=Ivan, Score=60, Grade=C

Resultado: u1 também mudou! Isso porque u1 e u2 apontam pro mesmo objeto User na memória. Quando a gente muda as propriedades pelo u2 (tipo u2.Name = "Ivan";), a gente tá mudando o objeto em si. Por isso, quando acessa u1.Name, já vê o valor novo. Até a struct aninhada ExamScore mudou pra u1, porque ela faz parte do objeto User que as duas referências apontam.

5. O que rola quando passa pra métodos

Sacar como os tipos são passados pra métodos é essencial pra prever o que seu programa vai fazer.

Passando tipo por valor (Value Type) pra método

Quando você passa um tipo por valor pra um método, por padrão é passagem por valor. Ou seja, o método recebe uma cópia da variável original. Qualquer mudança feita dentro do método nessa cópia não afeta o original fora do método.

void AddTen(int x)
{
    Console.WriteLine($"Dentro do método (antes de mudar): x = {x}"); // Dentro do método (antes de mudar): x = 5
    x = x + 10; // x agora é 15, mas é uma cópia local
    Console.WriteLine($"Dentro do método (depois de mudar): x = {x}"); // Dentro do método (depois de mudar): x = 15
    // Essa cópia local 'x' "morre" quando sai do método.
}

int num = 5;
Console.WriteLine($"Antes de chamar o método: num = {num}"); // Antes de chamar o método: num = 5
AddTen(num);
Console.WriteLine($"Depois de chamar o método: num = {num}"); // Depois de chamar o método: num = 5 (não mudou!)

Resultado: A variável num ficou igual (5). x dentro do AddTen é outra variável, inicializada com uma cópia do valor de num.

Passando tipo por referência (Reference Type) pra método

Quando você passa um tipo por referência pra um método, também é passagem por valor, mas o que é copiado é o valor da referência, não o objeto em si. Ou seja, dentro do método você tem uma cópia do "cartão de visita" (endereço) pro objeto original. As duas referências (a original e a do método) apontam pro mesmo objeto na memória.

void RenameUser(User u)
{
    // Dentro do método (antes de mudar): u.Name = "Olga"
    Console.WriteLine($"Dentro do método (antes de mudar): u.Name = \"{u.Name}\""); 
    u.Name = "Novo nome"; // Mudando a propriedade do objeto que 'u' aponta
    // Dentro do método (depois de mudar): u.Name = "Novo nome"
    Console.WriteLine($"Dentro do método (depois de mudar): u.Name = \"{u.Name}\""); 
}

User user = new User { Name = "Olga" };
// Antes de chamar o método: user.Name = "Olga"
Console.WriteLine($"Antes de chamar o método: user.Name = \"{user.Name}\""); 
RenameUser(user); 
// Depois de chamar o método: user.Name = "Novo nome"
Console.WriteLine($"Depois de chamar o método: user.Name = \"{user.Name}\""); 

Resultado: O nome do usuário virou "Novo nome". O método RenameUser recebeu uma cópia da referência pro objeto user. Com essa cópia, o método conseguiu acessar o objeto original na heap e mudar a propriedade Name.

Detalhe importante: O que acontece se dentro do método a gente atribuir um novo objeto pra variável do tipo por referência?

void ReassignUser(User u)
{
    u = new User { Name = "Usuário totalmente novo" }; // 'u' agora aponta pra um novo objeto
    Console.WriteLine($"Dentro do método (depois de reatribuir): u.Name = \"{u.Name}\"");
}

User originalUser = new User { Name = "Usuário original" };
Console.WriteLine($"Antes de chamar ReassignUser: originalUser.Name = \"{originalUser.Name}\"");
ReassignUser(originalUser); 
Console.WriteLine($"Depois de chamar ReassignUser: originalUser.Name = \"{originalUser.Name}\""); // "Usuário original" - não mudou!

Resultado: originalUser não mudou! Isso porque ReassignUser recebeu uma cópia da referência. Quando u = new User(...) rolou dentro do método, a variável local u passou a apontar pra um objeto novo. A referência original originalUser continua apontando pro mesmo objeto de antes. Isso é muito importante!

6. "Lágrimas de iniciante": erros típicos e por quê

Entender tipos por referência e por valor pode ser complicado pra quem tá começando. Aqui vão alguns erros e confusões comuns:

Confusão ao copiar arrays: Muita gente acha que ao fazer arr2 = arr1; vai criar uma cópia independente do array. Na real, são só duas referências pro mesmo array. É tipo dois controles pro mesmo videogame: qualquer botão que você aperte em um, afeta o jogo, e o outro controle vê isso também. Pra criar uma cópia independente, tem que clonar explicitamente (tipo int[] arr2 = (int[])arr1.Clone(); ou usar métodos Copy).

Achar que string muda "por referência": Como string é tipo por referência, às vezes a galera acha que ela vai se comportar como array na hora de mudar. Mas, por causa da imutabilidade das strings, qualquer operação que parece mudar, na real cria um novo objeto string. Isso pode dar resultados inesperados e ser ineficiente se você faz muitas operações com string em loop (pra isso, melhor usar StringBuilder).

Esquecer do null: Tipos por valor, tirando os nullable (int?, bool?), sempre têm algum valor e nunca podem ser null. Tipos por referência podem ser null, ou seja, não apontar pra nenhum objeto. Tentar acessar um membro de um objeto que é null vai dar o famoso NullReferenceException. Sempre cheque variáveis de referência pra null antes de usar, se existe a chance de estarem indefinidas.

Usar classes em vez de structs pra dados pequenos: Às vezes, por costume, todo mundo faz tudo como classe. Pra dados pequenos e simples, que representam uma coisa só (tipo um ponto Point { X, Y }, cor Color { R, G, B }), structs podem ser mais eficientes, porque ficam na stack e são copiados por valor, o que reduz o trabalho do garbage collector. Mas structs devem ser imutáveis, pequenas e não devem ter tipos por referência que podem ser null ou mudar de estado.

2
Tarefa
C# SELF, nível 12, lição 3
Bloqueado
Comportamento de arrays como tipo de referência
Comportamento de arrays como tipo de referência
2
Tarefa
C# SELF, nível 12, lição 3
Bloqueado
Interação com métodos para tipos valor e tipos referência
Interação com métodos para tipos valor e tipos referência
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION