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).
- 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: umboolde retorno e umoutcom o resultado. E as formasout int x,out var xeout _. in: passa a variável só para leitura.- Quando usar cada um, e quando é melhor um
returnou uma tupla. - Os erros
CS1620(com uma tradução que engana),CS1510,CS0165,CS0177,CS0269eCS8331.
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:
| Jeito | O que a secretaria recebe | Se ela escrever no papel… | Em C# |
|---|---|---|---|
| Entregar uma fotocópia | Uma cópia | O boletim da Ana não muda | Parâmetro comum (passagem por valor) |
| Entregar o boletim original | O próprio boletim | A nota aparece no boletim da Ana | ref (passagem por referência) |
| Entregar uma folha em branco: "preencha e me devolva" | Um lugar vazio para escrever | Ela tem de escrever algo | out |
| Mostrar o original pelo vidro do balcão | O próprio boletim, mas sem caneta | Ela só pode ler | in |
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,
refantes do tipo diz: "este parâmetro não é uma cópia; é a própria variável que vier na chamada".valorpassa a ser outro nome parapontos. - Dobrar(ref pontos);
- Na chamada, o
reftambém é obrigatório. É um aviso para quem lê: "cuidado, esta linha pode mudarpontos". - valor = valor * 2;
- Como
valoré um apelido depontos, quem vira 20 é o própriopontos.
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.
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 dariaCS0165; comout, 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
foreachprocura valores menores e maiores. - out menor, out maior
- Na chamada, o
outé obrigatório, como oref. Quando o método termina,menorvale 3 emaiorvale 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
| Forma | O que faz |
|---|---|
out menor | Usa uma variável declarada antes |
out int menor | Declara a variável ali mesmo; ela continua existindo nas linhas seguintes |
out var m1 | O 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ção | Use |
|---|---|
| O método calcula um valor novo | return, e quem chamou guarda: pontos = Dobrar(pontos); (Valor de Retorno) |
| O método calcula vários valores | Uma 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 leitura | in |
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.
| comum | ref | out | in | |
|---|---|---|---|---|
| O método recebe | uma cópia | a variável | a variável | a variável |
| A variável precisa ter valor antes? | sim | sim | não | sim |
| O método pode ler? | sim | sim | só depois de preencher | sim |
| O método pode mudar? | só a cópia | sim | é obrigado | não |
| Palavra na chamada | nenhuma | obrigatória | obrigatória | opcional |
Argumento pode ser um valor (5)? | sim | não | não | sim |
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
| Escrita | O 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:
2. Declarar na chamada
Complete para converter o texto e criar a variável idade ali mesmo:
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,outouin, 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ãoTryParse.in: só leitura, sem cópia; útil para structs grandes, raro no dia a dia.- Prefira
return(ou uma tupla); useoutno padrão Try erefquando não houver jeito melhor. - A mensagem em português do
CS1620diz "não deve", mas significa "deve ser passado comref/out": faltou a palavra na chamada.