C# ref, out e in

Em Parâmetros você viu que o método recebe uma cópia do valor: se ele mudar o parâmetro, a variável de quem chamou não muda. Quase sempre é isso que você quer. Às vezes, porém, o método precisa mexer na própria variável, ou entregar mais de um resultado de uma vez. Para isso o C# tem três palavras que vão antes do parâmetro: ref, out e in. Você já usou uma delas sem saber: o out do int.TryParse(texto, out int numero).

Você vai aprender
  • A diferença entre passar uma cópia e passar a própria variável (por referência).
  • ref: o método lê e muda a variável de quem chamou.
  • out: o método é obrigado a dar um valor à variável; serve para devolver mais de um resultado.
  • O padrão TryParse: um bool de retorno e um out com o resultado. E as formas out int x, out var x e out _.
  • in: passa a variável só para leitura.
  • Quando usar cada um, e quando é melhor um return ou uma tupla.
  • Os erros CS1620 (com uma tradução que engana), CS1510, CS0165, CS0177, CS0269 e CS8331.

Todos os programas desta aula rodaram na máquina fictícia da Ana, na pasta ~/cs, com o .NET 10 e o comando dotnet run arquivo.cs.


A ideia: a fotocópia e o caderno original

A secretaria da escola precisa anotar a nota de recuperação no boletim da Ana. Há dois jeitos de entregar o boletim:

JeitoO que a secretaria recebeSe ela escrever no papel…Em C#
Entregar uma fotocópiaUma cópiaO boletim da Ana não mudaParâmetro comum (passagem por valor)
Entregar o boletim originalO próprio boletimA nota aparece no boletim da Anaref (passagem por referência)
Entregar uma folha em branco: "preencha e me devolva"Um lugar vazio para escreverEla tem de escrever algoout
Mostrar o original pelo vidro do balcãoO próprio boletim, mas sem canetaEla só pode lerin

Nos três últimos casos, o método não recebe um valor copiado: recebe um apelido para a variável de quem chamou. Tudo o que ele faz com o parâmetro acontece direto na variável original.


1. ref: mudar a variável de quem chamou

Este é o programa da aula de parâmetros, com uma diferença: a palavra ref em dois lugares.

int pontos = 10;
Console.WriteLine($"Antes:  pontos = {pontos}");
Dobrar(ref pontos);
Console.WriteLine($"Depois: pontos = {pontos}");

void Dobrar(ref int valor)
{
    valor = valor * 2;
    Console.WriteLine($"Dentro: valor  = {valor}");
}
$ dotnet run ref1.cs
Antes:  pontos = 10
Dentro: valor  = 20
Depois: pontos = 20                  « sem o ref, continuava 10
void Dobrar(ref int valor)
Na declaração, ref antes do tipo diz: "este parâmetro não é uma cópia; é a própria variável que vier na chamada". valor passa a ser outro nome para pontos.
Dobrar(ref pontos);
Na chamada, o ref também é obrigatório. É um aviso para quem lê: "cuidado, esta linha pode mudar pontos".
valor = valor * 2;
Como valor é um apelido de pontos, quem vira 20 é o próprio pontos.

O exemplo clássico de ref é trocar o conteúdo de duas variáveis, algo que um método comum não consegue, porque só teria as cópias:

int a = 3;
int b = 8;
Console.WriteLine($"a = {a}, b = {b}");
Trocar(ref a, ref b);
Console.WriteLine($"a = {a}, b = {b}");

void Trocar(ref int x, ref int y)
{
    int guarda = x;
    x = y;
    y = guarda;
}
$ dotnet run trocar.cs
a = 3, b = 8
a = 8, b = 3

A variável guarda segura o valor de x (3) enquanto x recebe o de y (8); depois y recebe o 3 guardado. Como x e y são apelidos de a e b, a troca acontece nas variáveis do programa.

Regras do ref

O ref precisa de uma variável que já tenha valor, porque o método pode ler o que está lá. E ele é exigido dos dois lados. Os três erros que aparecem quando uma regra é quebrada:

int pontos = 10;
Dobrar(pontos);

void Dobrar(ref int valor)
{
    valor = valor * 2;
}
$ dotnet run semref.cs
/home/ana/cs/semref.cs(2,8): error CS1620: Argumento 1 não deve ser transmitido com a palavra-chave "ref"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Atenção: a tradução está errada

A mensagem em português diz "não deve ser transmitido com a palavra-chave ref", mas o problema é o contrário: faltou o ref. O original em inglês é Argument 1 must be passed with the 'ref' keyword, "o argumento 1 deve ser passado com a palavra-chave ref". Quando uma mensagem parecer não fazer sentido, procure o código (CS1620) e leia a versão em inglês. Conserto: Dobrar(ref pontos);.

Dobrar(ref 5);

void Dobrar(ref int valor)
{
    valor = valor * 2;
}
$ dotnet run reflit.cs
/home/ana/cs/reflit.cs(1,12): error CS1510: Um valor ref ou out deve ser uma variável que possa ser atribuída

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O método mudaria o 5 para 10, mas um número escrito no código não é uma variável: não há onde guardar o resultado. Com ref e out, o argumento tem de ser uma variável (ou um elemento de array, como ref notas[0]).

int pontos;
Dobrar(ref pontos);
Console.WriteLine(pontos);

void Dobrar(ref int valor)
{
    valor = valor * 2;
}
$ dotnet run refsemvalor.cs
/home/ana/cs/refsemvalor.cs(2,12): error CS0165: Uso de variável local não atribuída "pontos"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

pontos foi declarada sem valor, e o método ia ler esse valor (valor * 2). Conserto: int pontos = 10;. Se o método só vai escrever na variável, sem ler, o certo é out, a próxima seção.

Missão: o método Repor deveria somar 5 ao estoque, mas o programa mostra Estoque: 10. Use ref (na declaração e na chamada) para que ele mude a variável estoque e o programa mostre Estoque: 15.

int estoque = 10;
Repor(estoque);
Console.WriteLine($"Estoque: {estoque}");

void Repor(int quantidade)
{
    quantidade = quantidade + 5;
}
int estoque = 10;
Repor(ref estoque);
Console.WriteLine($"Estoque: {estoque}");

void Repor(ref int quantidade)
{
    quantidade = quantidade + 5;
}

2. out: "preencha e me devolva"

Um método só tem um valor de retorno. E se ele precisar entregar dois resultados, como o menor e o maior número de uma lista? Uma saída é o out ("saída"): o parâmetro chega vazio, e o método é obrigado a colocar um valor nele antes de terminar.

int menor;
int maior;
MenorEMaior(new int[] { 7, 3, 9, 5 }, out menor, out maior);
Console.WriteLine($"Menor: {menor}, maior: {maior}");

void MenorEMaior(int[] numeros, out int min, out int max)
{
    min = numeros[0];
    max = numeros[0];
    foreach (int n in numeros)
    {
        if (n < min) min = n;
        if (n > max) max = n;
    }
}
$ dotnet run out1.cs
Menor: 3, maior: 9
int menor; int maior;
As variáveis foram declaradas sem valor. Com ref, isso daria CS0165; com out, pode, porque o método promete preenchê-las.
out int min, out int max
Dois parâmetros de saída. Dentro do método, eles começam "vazios": o método tem de dar um valor a cada um antes de sair.
min = numeros[0];
A primeira coisa que o método faz é preencher os dois, com o primeiro número. Depois o foreach procura valores menores e maiores.
out menor, out maior
Na chamada, o out é obrigatório, como o ref. Quando o método termina, menor vale 3 e maior vale 9.

Declarar na própria chamada: out int, out var e out _

Declarar a variável numa linha e passá-la na outra é comprido. Desde o C# 7, você pode declarar a variável dentro da chamada:

MenorEMaior(new int[] { 7, 3, 9, 5 }, out int menor, out int maior);
Console.WriteLine($"Menor: {menor}, maior: {maior}");

MenorEMaior(new int[] { 4, 1 }, out var m1, out _);
Console.WriteLine($"Só o menor: {m1}");

void MenorEMaior(int[] numeros, out int min, out int max)
{
    min = numeros[0];
    max = numeros[0];
    foreach (int n in numeros)
    {
        if (n < min) min = n;
        if (n > max) max = n;
    }
}
$ dotnet run outvar.cs
Menor: 3, maior: 9
Só o menor: 1
FormaO que faz
out menorUsa uma variável declarada antes
out int menorDeclara a variável ali mesmo; ela continua existindo nas linhas seguintes
out var m1O mesmo, deixando o compilador descobrir o tipo (int)
out _"Não quero esse resultado": o _ (descarte) joga o valor fora

O padrão TryParse

O uso mais famoso do out você já conhece de Parse e TryParse. O int.TryParse devolve duas respostas: pelo return, um bool ("deu certo?"); pelo out, o número.

string[] digitados = { "42", "abc", "7" };
foreach (string texto in digitados)
{
    if (int.TryParse(texto, out int numero))
    {
        Console.WriteLine($"\"{texto}\" virou o número {numero}");
    }
    else
    {
        Console.WriteLine($"\"{texto}\" não é um número");
    }
}
$ dotnet run tryparse.cs
"42" virou o número 42
"abc" não é um número
"7" virou o número 7

Agora você consegue escrever um método nesse mesmo estilo. O nome começa com Tentar (em inglês, Try), o retorno diz se deu certo e o out traz o resultado:

Mostrar(10, 2);
Mostrar(10, 0);

void Mostrar(int a, int b)
{
    if (TentarDividir(a, b, out int resultado))
    {
        Console.WriteLine($"{a} / {b} = {resultado}");
    }
    else
    {
        Console.WriteLine($"{a} / {b}: não dá para dividir por zero");
    }
}

bool TentarDividir(int a, int b, out int resultado)
{
    if (b == 0)
    {
        resultado = 0;
        return false;
    }
    resultado = a / b;
    return true;
}
$ dotnet run tentar.cs
10 / 2 = 5
10 / 0: não dá para dividir por zero

Repare no resultado = 0; antes do return false;. Mesmo quando falha, o método tem de preencher o out. Sem essa linha:

$ dotnet run outnaoatrib.cs
/home/ana/cs/outnaoatrib.cs(7,9): error CS0177: O parâmetro out "resultado" deve ser atribuído antes que o controle saia do método atual

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Deve ser atribuído antes que o controle saia do método (must be assigned to before control leaves the current method): o return false; da linha 7 sairia do método com o resultado vazio. É a promessa do out: quem chamou pode usar a variável logo depois, então ela precisa ter algum valor. O int.TryParse faz o mesmo: quando falha, coloca 0 no out.


3. in: só para ler

O in C# 7.2 também passa a própria variável (e não uma cópia), mas proíbe o método de mudá-la:

decimal preco = 12.50m;
Mostrar(in preco);
Mostrar(preco);

void Mostrar(in decimal valor)
{
    Console.WriteLine(valor.ToString("C"));
}
$ dotnet run in1.cs
R$ 12,50
R$ 12,50

Diferente de ref e out, o in na chamada é opcional: as duas linhas funcionam. Se o método tentar mudar o parâmetro:

$ dotnet run inmuda.cs                « com valor = valor * 2; dentro do Mostrar
/home/ana/cs/inmuda.cs(6,5): error CS8331: Não é possível atribuir a variável 'valor' ou usá-lo como o lado direito de uma atribuição ref porque é uma variável somente leitura

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Para que serve, se uma cópia também protege a variável? Para economizar a cópia de valores muito grandes, como uma struct com muitos campos: o método lê direto do original, sem copiar nada, e ainda tem a garantia de não estragá-lo. Com int, decimal e outros tipos pequenos não há ganho nenhum, e você raramente vai escrever in. Vale reconhecê-lo quando aparecer em código alheio.


Formas diferentes de devolver mais de um valor

O out não é o único jeito de um método entregar dois resultados. Compare com uma tupla (aula Tuplas), que junta dois valores num só retorno:

var (menor, maior) = MenorEMaior(new int[] { 7, 3, 9, 5 });
Console.WriteLine($"Menor: {menor}, maior: {maior}");

(int, int) MenorEMaior(int[] numeros)
{
    return (numeros.Min(), numeros.Max());
}
$ dotnet run tupla.cs
Menor: 3, maior: 9

E, como os métodos no fim do arquivo são funções locais, eles enxergam as variáveis do programa e podem mudá-las direto, sem ref:

int pontos = 10;
DobrarPontos();
Console.WriteLine($"pontos = {pontos}");

void DobrarPontos()
{
    pontos = pontos * 2;
}
$ dotnet run captura.cs
pontos = 20

Funciona, mas esconde o que muda: quem lê DobrarPontos(); não sabe que pontos foi alterada, e o método só serve para essa variável. Com ref, a chamada mostra o que muda (Dobrar(ref pontos)) e o método serve para qualquer variável. A aula Escopo volta a esse assunto.

Qual usar?

SituaçãoUse
O método calcula um valor novoreturn, e quem chamou guarda: pontos = Dobrar(pontos); (Valor de Retorno)
O método calcula vários valoresUma tupla (ou um record); ou out
"Tente fazer; diga se deu certo e, se deu, o resultado"bool TentarAlgo(…, out T resultado), como o TryParse
O método precisa mesmo mudar variáveis de quem chamou (trocar duas, ajustar um contador)ref
Passar uma struct grande sem copiar, só para leiturain
Regra do guia

Prefira return. Use out no padrão Tentar…/Try… e ref quando não houver jeito melhor. Um programa cheio de ref fica difícil de seguir, porque qualquer chamada pode mudar qualquer variável.

comumrefoutin
O método recebeuma cópiaa variávela variávela variável
A variável precisa ter valor antes?simsimnãosim
O método pode ler?simsimsó depois de preenchersim
O método pode mudar?só a cópiasimé obrigadonão
Palavra na chamadanenhumaobrigatóriaobrigatóriaopcional
Argumento pode ser um valor (5)?simnãonãosim

Quando dá errado

error CS1620: Argumento 1 não deve ser transmitido com a palavra-chave "ref"

O que significa: apesar da tradução, faltou o ref (ou o out) na chamada. Em inglês: Argument 1 must be passed with the 'ref' keyword.

Como consertar: escreva a palavra também na chamada: Dobrar(ref pontos);, int.TryParse(texto, out int n). Esquecer o out do TryParse dá uma cascata de erros, e o CS1620 é o que importa:

$ dotnet run outsemmarca2.cs          « int.TryParse("42", numero);
/home/ana/cs/outsemmarca2.cs(2,14): error CS1503: Argumento 1: não é possível converter de "string" para "System.ReadOnlySpan<byte>"
/home/ana/cs/outsemmarca2.cs(2,20): error CS1620: Argumento 2 não deve ser transmitido com a palavra-chave "out"
/home/ana/cs/outsemmarca2.cs(2,20): error CS0165: Uso de variável local não atribuída "numero"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O primeiro erro, com ReadOnlySpan<byte>, assusta, mas é efeito colateral: sem o out, o compilador procurou outra versão do TryParse e não achou nenhuma que servisse. Ponha o out e os três somem.

error CS1510: Um valor ref ou out deve ser uma variável que possa ser atribuída

O que significa: você passou um valor (ref 5) ou uma conta (ref (a + 1)) onde o método precisa de uma variável.

Como consertar: guarde o valor numa variável e passe a variável: int x = 5; Dobrar(ref x);.

error CS0165: Uso de variável local não atribuída "pontos"

O que significa: a variável passada com ref foi declarada sem valor.

Como consertar: dê um valor inicial (int pontos = 0;) ou, se o método só preenche a variável, troque ref por out.

error CS0177: O parâmetro out "resultado" deve ser atribuído antes que o controle saia do método atual

O que significa: há um caminho (em geral, um return dentro de um if) em que o método sai sem preencher o out.

Como consertar: preencha o out em todos os caminhos. Um truque: atribua um valor padrão logo na primeira linha do método (resultado = 0;).

error CS0269: Uso do parâmetro out não atribuído "total"
$ dotnet run outler.cs
/home/ana/cs/outler.cs(5,13): error CS0269: Uso do parâmetro out não atribuído "total"

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que significa: o método void Somar(out int total) tenta ler o parâmetro (total = total + 1;) antes de dar um valor a ele. Um out chega vazio: não há o que somar.

Como consertar: se o método precisa do valor que já estava lá, o parâmetro é ref, não out.

error CS8331: Não é possível atribuir a variável 'valor'… porque é uma variável somente leitura

O que significa: o parâmetro é in, e o método tentou mudá-lo.

Como consertar: use uma variável local para a conta (decimal dobro = valor * 2;) ou, se o método precisa mesmo mudar o original, troque in por ref.


Referência rápida

EscritaO que faz
void M(ref int x) e M(ref v);Passa a própria variável v; o método lê e muda
void M(out int x) e M(out v);O método é obrigado a dar valor a v; v pode chegar sem valor
M(out int v), M(out var v)Declara a variável na própria chamada
M(out _)Descarta o resultado
void M(in decimal x) e M(v);Passa a variável só para leitura (o in na chamada é opcional)
bool TentarAlgo(…, out T r)Padrão Try: o bool diz se deu certo, o out traz o resultado

Exercícios

1. Na chamada

O método é void Zerar(ref int x). Complete a chamada para zerar a variável saldo:

Zerar();

2. Declarar na chamada

Complete para converter o texto e criar a variável idade ali mesmo:

int.TryParse(texto, )

3. Quanto vale?

Com void Somar5(ref int n) { n = n + 5; }, quanto vale x depois de int x = 2; Somar5(ref x); Somar5(ref x);?

4. Qual das palavras permite passar uma variável declarada sem valor, como int r;?

5. A mensagem CS1620: Argumento 1 não deve ser transmitido com a palavra-chave "ref" aparece. O que fazer?

6. Um método bool TentarLer(string t, out int n) tem um return false; dentro de um if, sem preencher n antes. O que acontece?

Missão final: pagar e receber o troco

Missão: complete o método TentarPagar, no padrão Try. Se o dinheiro der para pagar o preco (maior ou igual), ele coloca a diferença em troco e devolve true; senão, coloca 0 em troco e devolve false. Do jeito que está, o programa nem compila: dá CS0177, porque o out não foi preenchido. A saída deve ser:

Pago! Troco: R$ 12,50
Dinheiro insuficiente
Pago! Troco: R$ 0,00
Pagar(50m, 37.50m);
Pagar(20m, 37.50m);
Pagar(10m, 10m);

void Pagar(decimal dinheiro, decimal preco)
{
    if (TentarPagar(dinheiro, preco, out decimal troco))
    {
        Console.WriteLine($"Pago! Troco: {troco:C}");
    }
    else
    {
        Console.WriteLine("Dinheiro insuficiente");
    }
}

bool TentarPagar(decimal dinheiro, decimal preco, out decimal troco)
{
    // complete aqui
    return false;
}
Pagar(50m, 37.50m);
Pagar(20m, 37.50m);
Pagar(10m, 10m);

void Pagar(decimal dinheiro, decimal preco)
{
    if (TentarPagar(dinheiro, preco, out decimal troco))
    {
        Console.WriteLine($"Pago! Troco: {troco:C}");
    }
    else
    {
        Console.WriteLine("Dinheiro insuficiente");
    }
}

bool TentarPagar(decimal dinheiro, decimal preco, out decimal troco)
{
    if (dinheiro >= preco)
    {
        troco = dinheiro - preco;
        return true;
    }
    troco = 0;
    return false;
}

Resumo

  • Sem palavra nenhuma, o método recebe uma cópia. Com ref, out ou in, recebe a própria variável (passagem por referência).
  • ref: lê e muda a variável de quem chamou; ela precisa ter valor, e a palavra vai na declaração e na chamada.
  • out: a variável pode chegar vazia, e o método é obrigado a preenchê-la (CS0177). Dá para declarar na chamada (out int n, out var n) ou descartar (out _). É a base do padrão TryParse.
  • in: só leitura, sem cópia; útil para structs grandes, raro no dia a dia.
  • Prefira return (ou uma tupla); use out no padrão Try e ref quando não houver jeito melhor.
  • A mensagem em português do CS1620 diz "não deve", mas significa "deve ser passado com ref/out": faltou a palavra na chamada.