Menu

Parâmetros ref, out e in em C#: passagem por referência explicada

Como os parâmetros ref, out e in funcionam em C#: passar a variável em vez de uma cópia, out para resultados extras e o padrão TryParse, out var, parâmetros in somente leitura, as regras de atribuição definida e o que muda quando o argumento é um tipo de referência.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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 ref e out não podem ser opcionais (CS1741). Um parâmetro in pode: static void Connect(in int retries = 3) é válido.
  • Sobrecargas não podem diferir só entre ref e out (ou in), porque no nível do runtime são a mesma coisa. F(int x) e F(ref int x) podem coexistir.
  • Métodos async e iteradores (métodos que usam yield return) não podem ter parâmetros ref, out ou in. Nesses casos, retorne uma tupla ou um objeto de resultado.
  • Uma lambda ou função local não pode capturar um parâmetro ref, out ou in do método que a contém; copie-o antes para uma variável local.

ref, out e in comparados

Quem chama precisa atribuir antesO método precisa atribuirO método pode lerPalavra-chave na chamada
(nenhum)simnãosim (uma cópia)nenhuma
refsimnãosimobrigatória
outnãosim, em todos os caminhossó depois de atribuirobrigatória
insimnão permitidosimopcional

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);.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR