Por padrão, C# passa todo argumento por valor: o método recebe uma cópia, e atribuir ao parâmetro não afeta quem chamou. Os modificadores ref, out e in passam a própria variável de quem chamou.
Saída:
by value: 50
by ref: 60
A palavra-chave aparece duas vezes: na lista de parâmetros (ref int score) e no ponto da chamada (ref score). Escrevê-la na chamada é obrigatório, e é de propósito: quem lê AddPoints(ref score) sabe que score pode mudar. Esqueça-a e o compilador reporta CS1620, Argument 1 must be passed with the 'ref' keyword.
ref: ler e escrever a variável de quem chama
Um parâmetro ref é um apelido para a variável de quem chama. O método pode ler o valor atual e substituí-lo. O exemplo clássico é uma troca, que é impossível com parâmetros comuns:
Saída:
Linus Ada
volume 100
Regras do ref:
- O argumento precisa ser uma variável (uma local, um parâmetro, um campo ou um elemento de array), não um literal nem uma expressão. Uma propriedade também não serve:
Clamp(ref player.Volume, 0, 100)é o erro CS0206,A non ref-returning property or indexer may not be used as an out or ref value. Copie a propriedade para uma variável local, passe essa variável e atribua de volta. - A variável precisa estar definitivamente atribuída antes da chamada.
int n; Clamp(ref n, 0, 10);é CS0165,Use of unassigned local variable 'n'.
out: resultados extras
out é para valores que o método produz. Quem chama não precisa inicializar a variável, e o método precisa atribuí-la em todos os caminhos antes de retornar:
static void Split(string fullName, out string first, out string last)
{
// error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
first = fullName.Split(' ')[0];
}
Dentro do método, um parâmetro out conta como não atribuído até você escrever nele, então não dá para ler o valor antigo de quem chamou. Essa é a diferença para o ref: ref é de mão dupla, out é de mão única, do método para quem chama.
O padrão TryParse
O uso mais conhecido do out é o padrão Try da biblioteca base: retornar bool para indicar sucesso e colocar o resultado em um parâmetro out, para que uma entrada inválida nunca lance exceção:
Saída:
"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized
out var quantity (C# 7.0) declara a variável direto na chamada; o tipo dela vem do parâmetro. Em caso de falha, TryParse a define como 0 e retorna false. Quando você só quer saber se a conversão funciona, descarte o valor com out _: bool isNumber = int.TryParse(input, out _);.
Repare como TryGetDiscount atribui rate nos dois caminhos: uma vez explicitamente e outra repassando-a para Dictionary.TryGetValue, que também é uma chamada com out.
out versus retornar uma tupla
Antes do C# 7, out era a principal forma de retornar vários valores. Hoje as tuplas fazem isso de forma mais direta:
// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);
// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);
Use uma tupla (ou uma classe pequena) para vários resultados simples. Deixe o out para o padrão Try, em que o retorno bool permite colocar a chamada dentro de uma condição if.
Tipos de referência: mudar o objeto versus substituí-lo
Passar uma instância de classe por valor já permite que o método mude o objeto, porque o valor copiado é uma referência para o mesmo objeto. ref acrescenta uma capacidade: substituir o objeto para o qual a variável de quem chamou aponta.
Saída:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
ResetByValue fez a sua própria cópia da referência apontar para uma lista nova; quem chamou nunca viu isso. ResetByRef mudou a própria variável de quem chamou. No dia a dia você raramente precisa de ref em um parâmetro de classe: retorne o novo objeto.
Para structs, a diferença é maior, já que um argumento struct é copiado campo a campo. Um método que precisa modificar uma struct de quem chama (um Point, uma struct grande de configurações) precisa de ref.
in: somente leitura por referência
O C# 7.2 adicionou o in, que passa por referência mas proíbe atribuição. O objetivo é desempenho: uma struct grande não é copiada a cada chamada, e quem chama tem a garantia de que ela não vai mudar.
// C# 7.2 and later
struct Matrix4
{
public double M11, M12, M13, M14, M21, M22, M23, M24,
M31, M32, M33, M34, M41, M42, M43, M44;
}
static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;
static void Reset(in Matrix4 m)
{
m.M11 = 0;
// error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}
No ponto da chamada o in é opcional: Trace(matrix) e Trace(in matrix) funcionam. Para int, double, DateTime e outros tipos pequenos, in não ganha nada e pode ser um pouco mais lento; use-o para structs de vários campos que você passa com frequência.
Restrições
- Parâmetros
refeoutnão podem ser opcionais (CS1741). Um parâmetroinpode:static void Connect(in int retries = 3)é válido. - Sobrecargas não podem diferir só entre
refeout(ouin), porque no nível do runtime são a mesma coisa.F(int x)eF(ref int x)podem coexistir. - Métodos
asynce iteradores (métodos que usamyield return) não podem ter parâmetrosref,outouin. Nesses casos, retorne uma tupla ou um objeto de resultado. - Uma lambda ou função local não pode capturar um parâmetro
ref,outouindo método que a contém; copie-o antes para uma variável local.
ref, out e in comparados
| Quem chama precisa atribuir antes | O método precisa atribuir | O método pode ler | Palavra-chave na chamada | |
|---|---|---|---|---|
| (nenhum) | sim | não | sim (uma cópia) | nenhuma |
ref | sim | não | sim | obrigatória |
out | não | sim, em todos os caminhos | só depois de atribuir | obrigatória |
in | sim | não permitido | sim | opcional |
Perguntas frequentes
Qual a diferença entre ref e out em C#?
Os dois passam a própria variável de quem chama em vez de uma cópia. Com ref, a variável precisa estar atribuída antes da chamada e o método pode lê-la e alterá-la. Com out, a variável não precisa estar atribuída antes, o método não pode lê-la antes de escrever e precisa atribuí-la antes de retornar (senão é CS0177). out significa "este é um resultado extra".
O que significa out var em C#?
Desde o C# 7.0 você pode declarar a variável out dentro da chamada: if (int.TryParse(text, out var number)). O tipo da variável é inferido do parâmetro, e ela continua em escopo depois da instrução. out _ descarta um resultado de que você não precisa.
Objetos são passados por referência em C#?
Não, todo argumento é passado por valor a menos que você escreva ref, out ou in. Para uma classe, o valor copiado é uma referência, então o método pode mudar os campos do objeto e quem chamou vê a mudança. O que ele não pode fazer sem ref é fazer a variável de quem chamou apontar para outro objeto.
O que é o modificador de parâmetro in em C#?
in (C# 7.2 em diante) passa um argumento por referência, mas somente leitura: o método não pode atribuir a ele. Ele existe para evitar copiar structs grandes a cada chamada. Para tipos pequenos como int ou DateTime não traz nenhum benefício.
Por que recebo "Argument 1 must be passed with the 'ref' keyword"?
O método declara um parâmetro ref e a chamada omitiu a palavra-chave. C# exige ref (e out) também no ponto da chamada, para que quem lê veja que a variável pode mudar: Increment(ref count);, não Increment(count);.