C# Memória e Coletor de Lixo
Todo new que você escreve pede um pedaço da memória do computador: uma lista de compras, um texto, um objeto Conta. Em linguagens como o C, o programador tem de devolver cada pedaço com free, e esquecer disso é um dos erros mais comuns. Em C# quem devolve é o coletor de lixo (em inglês, garbage collector, ou GC). Nesta aula você vai ver onde as variáveis moram, quando um objeto vira "lixo", como o coletor trabalha e por que, mesmo com ele, um programa C# ainda pode gastar memória demais.
- A diferença entre a pilha (stack) e o heap.
- Quando um objeto vira lixo: quando nenhuma variável chega mais até ele.
- Como o coletor trabalha e o que são as gerações 0, 1 e 2.
- Como medir a memória com
GC.GetTotalMemoryeGC.GetAllocatedBytesForCurrentThread. - Os dois "vazamentos" mais comuns em C#: a lista
staticque só cresce e o evento sem-=. - Por que o coletor não fecha arquivos (e o
usingfecha). - Os erros
Stack overflow.eOut of memory., com o conserto.
Os programas desta aula rodaram na máquina da Ana com o .NET 10, cada um num arquivo solto em ~/cs, com dotnet run arquivo.cs (veja Rodar com dotnet run). Para entender a aula, ajuda ter visto Valor × Referência e Classes e Objetos.
A ideia: a mesa, o depósito e o faxineiro
Imagine a lojinha da Ana. Ela trabalha numa mesa pequena, onde ficam só as coisas da tarefa do momento: a calculadora, um bloquinho. Quando a tarefa acaba, a mesa é limpa de uma vez. As caixas grandes ficam num depósito nos fundos, e na mesa ela guarda só um papelzinho com o endereço da caixa ("prateleira 7"). De vez em quando passa um faxineiro pelo depósito: toda caixa cujo endereço não está anotado em nenhum papel é jogada fora, porque ninguém mais consegue achá-la.
| Na lojinha | No programa | O que fica lá |
|---|---|---|
| A mesa | A pilha (stack) | As variáveis locais de cada método que está rodando. Quando o método termina, a parte dele some de uma vez. |
| O depósito | O heap (em inglês, "monte") | Os objetos criados com new: listas, arrays, strings, objetos das suas classes. |
| O papel com o endereço | A referência | O que uma variável de classe guarda: não o objeto, mas onde ele está. |
| O faxineiro | O coletor de lixo (GC) | Passa de vez em quando e libera os objetos que nenhuma referência alcança. |
Os tipos de valor (int, double, decimal, bool, structs) guardam o próprio valor na variável. Os tipos de referência (string, arrays, List<T>, as suas classes) guardam um endereço, e o objeto fica no heap.
Um int que é variável local fica na pilha. Mas um int que é campo de uma classe fica dentro do objeto, no heap. A regra certa é: o valor fica onde a variável está. Para quem está começando, "valores na mesa, objetos no depósito" basta.
1. Uma cópia × o mesmo objeto
Este programa mostra as duas coisas lado a lado. Copiar um int copia o número. Copiar uma lista copia só o endereço: as duas variáveis passam a apontar para a mesma lista no heap.
int idade = 31;
int copia = idade;
copia = 40;
Console.WriteLine($"idade = {idade}, copia = {copia}");
var compras = new List<string> { "arroz" };
var mesmaLista = compras;
mesmaLista.Add("feijão");
Console.WriteLine($"compras tem {compras.Count} itens: {string.Join(", ", compras)}");
Console.WriteLine(ReferenceEquals(compras, mesmaLista));
| Linha | O que acontece na memória |
|---|---|
int copia = idade; | Na pilha, a variável copia recebe uma cópia do número 31. Mudar copia não mexe em idade. |
new List<string> { "arroz" } | O new cria a lista no heap. A variável compras, na pilha, guarda o endereço dela. |
var mesmaLista = compras; | Copia o endereço. Agora há dois papéis com o mesmo endereço, e uma lista só. |
mesmaLista.Add("feijão"); | Mexe na lista do heap. Quem olhar por compras vê a mudança. |
ReferenceEquals(compras, mesmaLista) | "São o mesmo objeto?" Compara os endereços. |
$ dotnet run ref.cs idade = 31, copia = 40 compras tem 2 itens: arroz, feijão True
2. Quando um objeto vira lixo
O coletor não olha se o objeto "ainda vai ser usado" (isso ele não tem como saber). Ele olha se o objeto ainda é alcançável: se dá para chegar até ele partindo das variáveis locais dos métodos que estão rodando e dos campos static. Esses pontos de partida se chamam raízes. O que não é alcançável é lixo.
Para ver isso acontecer, usamos uma WeakReference ("referência fraca"): ela observa um objeto sem segurá-lo. Se só ela aponta para o objeto, o coletor pode levá-lo, e a propriedade IsAlive ("está vivo") passa a dar false.
using System.Runtime.CompilerServices;
WeakReference observador = CriarRelatorio();
Console.WriteLine($"Antes da coleta, vivo? {observador.IsAlive}");
GC.Collect();
Console.WriteLine($"Depois da coleta, vivo? {observador.IsAlive}");
[MethodImpl(MethodImplOptions.NoInlining)]
static WeakReference CriarRelatorio()
{
var relatorio = new byte[1_000_000];
return new WeakReference(relatorio);
}
- static WeakReference CriarRelatorio()
- Um método local que cria um array de 1 milhão de bytes (1 MB) e devolve só um observador dele. A variável
relatorioé local: quando o método termina, ela some da pilha, e nenhuma referência "forte" ao array sobra. - [MethodImpl(MethodImplOptions.NoInlining)]
- Um atributo que pede ao .NET para não "colar" o método dentro de quem o chama (inlining). Assim temos certeza de que
relatoriomorre quando o método volta. Num programa normal você não precisa disso; aqui é só para o experimento dar sempre o mesmo resultado. - GC.Collect()
- Manda o coletor passar agora. Só usamos para aprender: num programa de verdade, deixe o .NET decidir (veja a dica mais abaixo).
- observador.IsAlive
trueenquanto o objeto existir.
$ dotnet run lixo.cs Antes da coleta, vivo? True Depois da coleta, vivo? False
Antes da coleta o array ainda existia (virou lixo, mas o faxineiro não tinha passado). Depois, o coletor viu que ninguém o alcançava e liberou o 1 MB.
null e ele continua vivo!"
A Ana tentou o mesmo experimento sem o método, direto no programa:
byte[]? relatorio = new byte[1_000_000];
var observador = new WeakReference(relatorio);
relatorio = null;
GC.Collect();
Console.WriteLine($"Ainda vivo? {observador.IsAlive}");
$ dotnet run fraca.cs Ainda vivo? True
O que aconteceu: o dotnet run compila em modo Debug (para depurar), e nesse modo o .NET pode guardar cópias escondidas das referências na pilha até o fim do método, para o depurador conseguir mostrá-las. O array ainda estava "alcançável" por uma dessas cópias. Não é um erro: o coletor só promete liberar o que não é alcançável em algum momento, nunca num momento exato.
A lição: nunca escreva um programa que dependa de quando o coletor passa. Para o experimento, criar o objeto dentro de um método (como em lixo.cs) resolve.
3. Gerações: o coletor olha primeiro para os novatos
Varrer o depósito inteiro toda vez seria lento. O coletor do .NET aposta numa coisa que quase sempre é verdade: a maioria dos objetos morre jovem (um texto montado para um WriteLine, uma lista temporária). Por isso ele separa o heap em gerações:
| Geração | Quem fica lá | Quando é varrida |
|---|---|---|
| 0 | Os objetos recém-criados. | Muitas vezes: é pequena e a varredura é rápida. |
| 1 | Os que sobreviveram a uma coleta. | Menos vezes. É um "meio-termo". |
| 2 | Os que sobreviveram a duas ou mais: os objetos de vida longa (configurações, caches). Arrays grandes, de 85 mil bytes ou mais, já nascem num lugar especial ligado à geração 2 (o Large Object Heap). | Raramente. É a coleta completa, a mais cara. |
var compras = new List<string> { "arroz", "feijão" };
Console.WriteLine($"Recém-criada: geração {GC.GetGeneration(compras)}");
GC.Collect();
Console.WriteLine($"Sobreviveu a 1 coleta: geração {GC.GetGeneration(compras)}");
GC.Collect();
Console.WriteLine($"Sobreviveu a 2 coletas: geração {GC.GetGeneration(compras)}");
Console.WriteLine($"Maior geração: {GC.MaxGeneration}");
Console.WriteLine($"Coletas da geração 0 até agora: {GC.CollectionCount(0)}");
- GC.GetGeneration(compras)
- Em que geração o objeto está agora.
- GC.Collect()
- Uma coleta completa. A lista ainda é usada (a variável
comprasa alcança), então sobrevive e é promovida para a geração seguinte. - GC.MaxGeneration
- O número da última geração.
- GC.CollectionCount(0)
- Quantas vezes a geração 0 foi varrida desde que o programa começou. Toda coleta completa também conta como coleta da geração 0.
$ dotnet run geracoes.cs Recém-criada: geração 0 Sobreviveu a 1 coleta: geração 1 Sobreviveu a 2 coletas: geração 2 Maior geração: 2 Coletas da geração 0 até agora: 2
Os números acima são os do dotnet no Linux. O editor do guia roda o .NET em WebAssembly, dentro do navegador, e o coletor de lá é mais simples: o mesmo programa mostra a lista na geração 0 nas três linhas, Maior geração: 1 e 0 coletas. As ideias da aula (alcançável, lixo, promoção) são as mesmas; os números de gerações e de memória, não. Para ver as gerações de verdade, rode no terminal.
4. Medir a memória
A classe GC tem dois métodos para espiar a memória:
GC.GetTotalMemory(true): quantos bytes os objetos vivos ocupam agora no heap. Otruepede uma coleta antes de medir, para não contar lixo.GC.GetAllocatedBytesForCurrentThread(): quantos bytes este programa já pediu desde o começo, contando o que já virou lixo. Serve para comparar duas formas de fazer a mesma coisa.
Um exemplo clássico: montar um texto de 1000 letras com += e com StringBuilder.
using System.Text;
long antes = GC.GetAllocatedBytesForCurrentThread();
string texto = "";
for (int i = 0; i < 1000; i++)
{
texto += "x";
}
long depois = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"Com +=: {depois - antes:N0} bytes");
antes = GC.GetAllocatedBytesForCurrentThread();
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append('x');
}
string pronto = sb.ToString();
depois = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"Com StringBuilder: {depois - antes:N0} bytes");
Console.WriteLine(texto == pronto);
$ dotnet run aloc.cs Com +=: 1.025.976 bytes Com StringBuilder: 4.576 bytes True
Rode e compare os dois números. No editor eles saem um pouco diferentes dos do terminal (o .NET do navegador guarda os objetos de outro jeito), mas a diferença entre += e StringBuilder é a mesma. Depois, troque o 1000 dos dois laços por 2000: o += dobra ou quadruplica?
using System.Text;
long antes = GC.GetAllocatedBytesForCurrentThread();
string texto = "";
for (int i = 0; i < 1000; i++)
{
texto += "x";
}
long depois = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"Com +=: {depois - antes:N0} bytes");
antes = GC.GetAllocatedBytesForCurrentThread();
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append('x');
}
string pronto = sb.ToString();
depois = GC.GetAllocatedBytesForCurrentThread();
Console.WriteLine($"Com StringBuilder: {depois - antes:N0} bytes");
Console.WriteLine(texto == pronto);
Os dois textos são iguais (True), mas o += pediu mais de 1 milhão de bytes, e o StringBuilder, menos de 5 mil. Por quê? Uma string nunca muda (ela é imutável). Cada texto += "x" cria uma string nova, uma letra maior, e a anterior vira lixo: 1000 strings de 1, 2, 3… até 1000 letras, cada letra com 2 bytes. O StringBuilder vai enchendo uma área que cresce aos poucos e só cria a string no fim.
O megabyte do += não "vazou": o coletor recolhe tudo nas próximas passadas. O problema é o trabalho: muito lixo faz o coletor passar mais vezes, e cada passada custa tempo. Num laço com milhares de voltas, prefira o StringBuilder. Para juntar três ou quatro pedaços, o + e a interpolação são ótimos.
5. "Vazamento" de memória em C#
Com o coletor, você nunca esquece de liberar um objeto que ninguém usa. Mas existe o problema contrário: um objeto que você não usa mais, mas que continua alcançável. O coletor não pode levá-lo. Se isso se repete, a memória só cresce: é o que se chama de vazamento em C#.
Uma lista static que só cresce
Campos static são raízes: vivem enquanto o programa viver. A Ana guardou os relatórios de cada dia num "cache" (uma lista para reaproveitar depois), mas nunca tirou nada de lá:
for (int dia = 1; dia <= 5; dia++)
{
Relatorios.Gerar(dia);
}
Console.WriteLine($"Relatórios guardados: {Relatorios.Cache.Count}");
Console.WriteLine($"Memória: {GC.GetTotalMemory(true) / 1_000_000} MB");
static class Relatorios
{
public static List<byte[]> Cache { get; } = new();
public static void Gerar(int dia)
{
byte[] relatorio = new byte[10_000_000]; // 10 MB
Cache.Add(relatorio);
Console.WriteLine($"Dia {dia}: relatório gerado");
}
}
$ dotnet run cache.cs Dia 1: relatório gerado Dia 2: relatório gerado Dia 3: relatório gerado Dia 4: relatório gerado Dia 5: relatório gerado Relatórios guardados: 5 Memória: 50 MB
A variável relatorio morre no fim de cada Gerar, mas a lista Cache continua segurando cada array. Cinco dias, 50 MB. Num programa que fica ligado o mês inteiro, seria 300 MB, depois 1 GB… O conserto é decidir quanto guardar: só os últimos dias, por exemplo.
Missão: o cache da Ana guarda os 5 relatórios. Faça ele guardar só os 3 últimos: em Gerar, depois do Add, se Cache.Count passar de 3, tire o mais antigo com Cache.RemoveAt(0). (Aqui cada relatório tem 1 MB, para não pesar no navegador.)
for (int dia = 1; dia <= 5; dia++)
{
Relatorios.Gerar(dia);
}
Console.WriteLine($"Relatórios guardados: {Relatorios.Cache.Count}");
static class Relatorios
{
public static List<byte[]> Cache { get; } = new();
public static void Gerar(int dia)
{
byte[] relatorio = new byte[1_000_000]; // 1 MB
Cache.Add(relatorio);
// se o cache passou de 3, tire o mais antigo
Console.WriteLine($"Dia {dia}: relatório gerado");
}
}
for (int dia = 1; dia <= 5; dia++)
{
Relatorios.Gerar(dia);
}
Console.WriteLine($"Relatórios guardados: {Relatorios.Cache.Count}");
static class Relatorios
{
public static List<byte[]> Cache { get; } = new();
public static void Gerar(int dia)
{
byte[] relatorio = new byte[1_000_000]; // 1 MB
Cache.Add(relatorio);
if (Cache.Count > 3)
{
Cache.RemoveAt(0);
}
Console.WriteLine($"Dia {dia}: relatório gerado");
}
}
Um evento sem -=
Quando um objeto se inscreve num evento com +=, o dono do evento passa a guardar uma referência a ele. Se ninguém fizer -=, o inscrito fica vivo enquanto o dono do evento viver, mesmo que mais ninguém o use. No exemplo abaixo, cada Tela é usada uma vez só, mas o Relogio continua chamando todas:
var relogio = new Relogio();
for (int i = 1; i <= 3; i++)
{
var tela = new Tela($"Tela {i}");
relogio.Tique += tela.Mostrar;
relogio.Bater();
}
Console.WriteLine($"Inscritos no fim: {relogio.Inscritos}");
class Relogio
{
public event Action? Tique;
public int Inscritos => Tique?.GetInvocationList().Length ?? 0;
public void Bater() => Tique?.Invoke();
}
class Tela(string nome)
{
public void Mostrar() => Console.WriteLine($"{nome}: tique!");
}
$ dotnet run evento.cs Tela 1: tique! Tela 1: tique! Tela 2: tique! Tela 1: tique! Tela 2: tique! Tela 3: tique! Inscritos no fim: 3
- relogio.Tique += tela.Mostrar;
- Inscreve o método
Mostrardaquela tela. O evento guarda o método e o objetotela. - Tique?.GetInvocationList().Length
- Quantos métodos estão inscritos no evento.
- class Tela(string nome)
- Uma classe com construtor primário C# 12: o
nomefica disponível dentro dela.
A "Tela 1" foi chamada três vezes, embora o laço já tenha passado dela: o relógio ainda a segura. Num programa com janelas, cada janela fechada continuaria na memória.
6. O coletor cuida da memória, não dos arquivos
Um objeto que abriu um arquivo ou uma conexão segura algo que não é memória: o arquivo fica aberto no sistema. O coletor não sabe disso e só passa quando acha que falta memória, o que pode demorar. Existe um jeito de rodar código quando o coletor leva um objeto, o finalizador (~NomeDaClasse()), mas você não controla quando ele roda:
using System.Runtime.CompilerServices;
UsarArquivo();
Console.WriteLine("Chamando o coletor...");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Fim do programa");
[MethodImpl(MethodImplOptions.NoInlining)]
static void UsarArquivo()
{
var r = new Recurso("notas.txt");
Console.WriteLine($"Usando {r.Nome}");
}
class Recurso
{
public string Nome { get; }
public Recurso(string nome) => Nome = nome;
~Recurso() => Console.WriteLine($"Finalizador: liberando {Nome}");
}
$ dotnet run final.cs Usando notas.txt Chamando o coletor... Finalizador: liberando notas.txt Fim do programa
O UsarArquivo terminou bem antes, mas o "arquivo" só foi liberado quando chamamos o coletor à força e esperamos os finalizadores (GC.WaitForPendingFinalizers). Sem essas duas linhas, ele poderia ficar aberto até o fim do programa. No editor do guia é ainda pior: lá o finalizador não chegou a rodar, e a linha Finalizador: … nem aparece. Por isso a regra do C#:
using.
Classes que seguram arquivos, conexões ou outros recursos do sistema implementam IDisposable, e você as usa com using: o recurso é liberado na hora em que o bloco termina, sempre. A aula using e IDisposable mostra como. Escrever finalizadores é raro: as classes do .NET que precisam deles já os têm.
Qual usar: quando se preocupar com memória
| Situação | O que fazer |
|---|---|
| Quase todo programa | Nada. Crie os objetos com new à vontade; o coletor é muito bom no que faz. |
| Montar texto num laço grande | StringBuilder em vez de +=. |
Arquivos, conexões, StreamReader | using (veja using e IDisposable). |
Listas e dicionários static, caches | Decida um limite e tire os itens antigos. |
| Eventos de objetos de vida curta | Faça -= quando o objeto não for mais usado. |
| Muitos dados pequenos, como pontos de um gráfico | Uma struct pode evitar milhões de objetos no heap. Veja também Span<T>. |
| O programa ficou lento ou pesado | Meça antes de mudar (GC.GetAllocatedBytesForCurrentThread, ou ferramentas como o dotnet-counters). Não adivinhe. |
GC.Collect() pelo programa
Nesta aula usamos GC.Collect() para ver o coletor trabalhar. Num programa de verdade, ele quase sempre atrapalha: força uma coleta completa (a mais cara) e promove para a geração 2 objetos que iam morrer logo, o que deixa as próximas coletas mais caras. O .NET escolhe a hora certa melhor do que nós.
Quando dá errado
Stack overflow.: a recursão que nunca para
A pilha é pequena (no Linux, 8 MB por padrão). Cada chamada de método ocupa um pedaço dela, e o pedaço só é devolvido quando o método termina. Um método que chama a si mesmo sem parar (recursão sem caso base) enche a pilha:
int Contar(int n)
{
return Contar(n + 1);
}
Console.WriteLine("Contando...");
Console.WriteLine(Contar(1));
$ dotnet run pilha.cs Contando... Stack overflow. Repeated 261736 times: « o número muda um pouco a cada execução -------------------------------- at Program.<<Main>$>g__Contar|0_0(Int32) -------------------------------- at Program.<Main>$(System.String[]) $ echo $? 134
O que significa: "estouro da pilha". O método Contar (o nome estranho <<Main>$>g__Contar|0_0 é como o compilador batiza uma função local) foi chamado mais de 260 mil vezes, uma dentro da outra, até a pilha acabar. Esse erro não pode ser pego com try/catch: o .NET encerra o programa na hora, com o código 134. No editor do guia, o estouro aparece do mesmo jeito (Stack overflow., o bloco repetido e o código 134), só que com uma pilha menor: Repeated 999 times.
Como consertar: toda recursão precisa de um caso base, uma condição em que o método para de se chamar: if (n >= 10) return n;. Se a recursão é muito funda de propósito (milhões de níveis), troque por um laço while.
Out of memory.: um pedido grande demais
A Ana quis um array com o maior tamanho possível:
Console.WriteLine("Criando um array gigante...");
int[] numeros = new int[int.MaxValue];
Console.WriteLine(numeros.Length);
$ dotnet run oom.cs Criando um array gigante... Out of memory. $ echo $? 134
O que significa: int.MaxValue são 2.147.483.647 posições de 4 bytes: 8 GB num array só, além do tamanho máximo que um array do .NET aceita. O new lançou uma OutOfMemoryException ("sem memória"), ninguém a pegou, e o .NET encerrou o programa com uma mensagem curta e o código 134. Pegando a exceção, dá para ler a mensagem completa:
try
{
int[] numeros = new int[int.MaxValue];
Console.WriteLine(numeros.Length);
}
catch (OutOfMemoryException e)
{
Console.WriteLine(e.Message);
}
$ dotnet run oom2.cs Array dimensions exceeded supported range.
"As dimensões do array passaram do limite aceito". (No editor do guia, o oom.cs mostra o mesmo Out of memory. e o código 134; a mensagem lida no catch é outra: Out of memory.) Como consertar: peça só o que vai usar. Para muitos dados, use uma List<T> que cresce aos poucos, ou leia um arquivo linha por linha (veja StreamReader) em vez de carregá-lo inteiro. Se a memória cresce sem parar num programa que fica ligado, procure um dos vazamentos da seção 5.
Referência
| Membro | O que faz |
|---|---|
GC.Collect() | Força uma coleta completa agora. Para testes e aulas, quase nunca em programas. |
GC.WaitForPendingFinalizers() | Espera os finalizadores dos objetos coletados terminarem. |
GC.GetGeneration(obj) | A geração em que o objeto está (0, 1 ou 2). |
GC.MaxGeneration | O número da última geração. |
GC.CollectionCount(n) | Quantas coletas da geração n já aconteceram. |
GC.GetTotalMemory(true) | Bytes ocupados pelos objetos vivos (com true, coleta antes de medir). |
GC.GetAllocatedBytesForCurrentThread() | Bytes já pedidos por esta thread desde o começo, contando o lixo. |
new WeakReference(obj) / .IsAlive | Observa um objeto sem impedir que ele seja coletado. |
ReferenceEquals(a, b) | true se as duas variáveis apontam para o mesmo objeto. |
~Classe() { … } | Finalizador: roda quando o coletor leva o objeto, sem hora marcada. |
Exercícios
1. Quando o coletor de lixo pode liberar um objeto?
2. Onde fica a lista criada por var compras = new List<string>(); dentro de um método?
3. Um objeto acabou de ser criado com new. Em que geração ele está?
4. Um programa que fica ligado o dia inteiro guarda cada pedido numa static List<Pedido> e nunca remove nada. O que acontece?
5. Qual é o jeito certo de garantir que um arquivo seja fechado na hora?
6. Complete o nome do método que força uma coleta agora:
7. Complete o operador que desinscreve a tela do evento:
Missão final: conserte o vazamento do relógio. Dentro do laço, depois do relogio.Bater();, desinscreva a tela com relogio.Tique -= tela.Mostrar;. Cada tela deve dizer "tique!" uma vez só, e no fim não deve sobrar nenhum inscrito.
var relogio = new Relogio();
for (int i = 1; i <= 3; i++)
{
var tela = new Tela($"Tela {i}");
relogio.Tique += tela.Mostrar;
relogio.Bater();
}
Console.WriteLine($"Inscritos no fim: {relogio.Inscritos}");
class Relogio
{
public event Action? Tique;
public int Inscritos => Tique?.GetInvocationList().Length ?? 0;
public void Bater() => Tique?.Invoke();
}
class Tela(string nome)
{
public void Mostrar() => Console.WriteLine($"{nome}: tique!");
}
var relogio = new Relogio();
for (int i = 1; i <= 3; i++)
{
var tela = new Tela($"Tela {i}");
relogio.Tique += tela.Mostrar;
relogio.Bater();
relogio.Tique -= tela.Mostrar;
}
Console.WriteLine($"Inscritos no fim: {relogio.Inscritos}");
class Relogio
{
public event Action? Tique;
public int Inscritos => Tique?.GetInvocationList().Length ?? 0;
public void Bater() => Tique?.Invoke();
}
class Tela(string nome)
{
public void Mostrar() => Console.WriteLine($"{nome}: tique!");
}
$ dotnet run evento.cs « com o -= Tela 1: tique! Tela 2: tique! Tela 3: tique! Inscritos no fim: 0
Resumo
- Variáveis locais ficam na pilha e somem quando o método termina; objetos criados com
newficam no heap. - Uma variável de tipo de referência guarda o endereço do objeto; copiar a variável copia o endereço.
- O coletor de lixo libera os objetos que nenhuma raiz alcança, numa hora que ele escolhe.
- O heap tem gerações: 0 (novos), 1 e 2 (vida longa). A maioria dos objetos morre na geração 0, onde a coleta é barata.
+=emstringnum laço gera muito lixo;StringBuilderresolve.- Vazamento em C# é objeto inútil mas alcançável: listas
staticsem limite e eventos sem-=. - O coletor cuida só da memória; arquivos e conexões se fecham com
using. Stack overflow.é recursão sem fim;Out of memory.é um pedido grande demais. Os dois encerram o programa com o código 134.
Próxima aula: Boas Práticas.