Por defecto, C# pasa cada argumento por valor: el método recibe una copia, y asignar al parámetro no afecta a quien llama. Los modificadores ref, out e in pasan en su lugar la propia variable de quien llama.
Salida:
by value: 50
by ref: 60
La palabra clave aparece dos veces: en la lista de parámetros (ref int score) y en el punto de llamada (ref score). Escribirla en la llamada es obligatorio, y es a propósito: cualquiera que lea AddPoints(ref score) sabe que score puede cambiar. Si la olvidas, el compilador da CS1620, Argument 1 must be passed with the 'ref' keyword.
ref: leer y escribir la variable de quien llama
Un parámetro ref es un alias de la variable de quien llama. El método puede leer el valor actual y sustituirlo. El ejemplo clásico es un intercambio, imposible con parámetros normales:
Salida:
Linus Ada
volume 100
Reglas de ref:
- El argumento debe ser una variable (una variable local, un parámetro, un campo o un elemento de un array), no un literal ni una expresión. Una propiedad tampoco vale:
Clamp(ref player.Volume, 0, 100)es el error CS0206,A non ref-returning property or indexer may not be used as an out or ref value. Copia la propiedad en una variable local, pasa esa y vuelve a asignarla después. - La variable debe estar definitivamente asignada antes de la llamada.
int n; Clamp(ref n, 0, 10);es CS0165,Use of unassigned local variable 'n'.
out: resultados extra
out es para valores que produce el método. Quien llama no necesita inicializar la variable, y el método debe asignarla en todos los caminos antes de volver:
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 del método, un parámetro out cuenta como no asignado hasta que escribes en él, así que no puedes leer el valor antiguo de quien llama. Esa es la diferencia con ref: ref va en los dos sentidos, out va en un solo sentido, del método a quien llama.
El patrón TryParse
El uso más conocido de out es el patrón Try de la biblioteca base: devolver bool para indicar el éxito y poner el resultado en un parámetro out, de modo que una entrada incorrecta nunca lance una excepción:
Salida:
"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 la variable directamente en la llamada; su tipo viene del parámetro. Si falla, TryParse la pone a 0 y devuelve false. Cuando solo te importa si el parseo funciona, descarta el valor con out _: bool isNumber = int.TryParse(input, out _);.
Fíjate en que TryGetDiscount asigna rate en los dos caminos: una vez de forma explícita y otra reenviándolo a Dictionary.TryGetValue, que es a su vez una llamada con out.
out frente a devolver una tupla
Antes de C# 7, out era la forma principal de devolver varios valores. Ahora las tuplas lo hacen de forma más directa:
// 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);
Usa una tupla (o una clase pequeña) para varios resultados sin más. Reserva out para el patrón Try, donde el retorno bool permite poner la llamada en la condición de un if.
Tipos de referencia: cambiar el objeto frente a sustituirlo
Pasar por valor una instancia de clase ya permite al método cambiar el objeto, porque el valor copiado es una referencia al mismo objeto. ref añade una capacidad más: sustituir el objeto al que apunta la variable de quien llama.
Salida:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
ResetByValue hizo que su propia copia de la referencia apuntara a una lista nueva; quien llama nunca lo vio. ResetByRef cambió la propia variable de quien llama. En el código del día a día rara vez necesitas ref en un parámetro de clase: devuelve el objeto nuevo en su lugar.
Con los structs la diferencia es mayor, porque un argumento struct se copia campo a campo. Un método que debe modificar el struct de quien llama (un Point, un struct grande de configuración) necesita ref.
in: solo lectura por referencia
C# 7.2 añadió in, que pasa por referencia pero prohíbe la asignación. Su objetivo es el rendimiento: un struct grande no se copia en cada llamada, y quien llama tiene la garantía de que no cambiará.
// 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
}
En el punto de llamada in es opcional: Trace(matrix) y Trace(in matrix) funcionan las dos. Con int, double, DateTime y otros tipos pequeños, in no gana nada y puede ser algo más lento; úsalo para structs de varios campos que pasas a menudo.
Restricciones
- Los parámetros
refyoutno pueden ser opcionales (CS1741). Un parámetroinsí puede:static void Connect(in int retries = 3)es válido. - Las sobrecargas no pueden diferenciarse solo entre
refyout(oin), porque a nivel del runtime son lo mismo.F(int x)yF(ref int x)pueden coexistir. - Los métodos
asyncy los iteradores (métodos que usanyield return) no pueden tener parámetrosref,outniin. En esos casos, devuelve una tupla o un objeto de resultado. - Una lambda o una función local no puede capturar un parámetro
ref,outoindel método que la contiene; cópialo antes en una variable local.
ref, out e in comparados
| Quien llama debe asignar antes | El método debe asignar | El método puede leer | Palabra clave en la llamada | |
|---|---|---|---|---|
| (ninguno) | sí | no | sí (una copia) | ninguna |
ref | sí | no | sí | obligatoria |
out | no | sí, en todos los caminos | solo después de asignar | obligatoria |
in | sí | no permitido | sí | opcional |
Preguntas frecuentes
¿Qué diferencia hay entre ref y out en C#?
Los dos pasan la propia variable de quien llama en lugar de una copia. Con ref, la variable debe estar asignada antes de la llamada y el método puede leerla y cambiarla. Con out, la variable no necesita asignarse antes, el método no puede leerla antes de escribirla y debe asignarla antes de volver (si no, CS0177). out significa "esto es un resultado extra".
¿Qué significa out var en C#?
Desde C# 7.0 puedes declarar la variable out dentro de la llamada: if (int.TryParse(text, out var number)). El tipo de la variable se deduce del parámetro, y la variable sigue en ámbito después de la sentencia. out _ descarta un resultado que no necesitas.
¿Se pasan los objetos por referencia en C#?
No, todo argumento se pasa por valor salvo que escribas ref, out o in. En una clase, el valor que se copia es una referencia, así que el método puede cambiar los campos del objeto y quien llama lo ve. Lo que no puede hacer sin ref es que la variable de quien llama apunte a otro objeto.
¿Qué es el modificador de parámetro in en C#?
in (C# 7.2 y posteriores) pasa un argumento por referencia pero de solo lectura: el método no puede asignarlo. Existe para evitar copiar structs grandes en cada llamada. Con tipos pequeños como int o DateTime no aporta nada.
¿Por qué obtengo "Argument 1 must be passed with the 'ref' keyword"?
El método declara un parámetro ref y en la llamada falta la palabra clave. C# exige ref (y out) también en el punto de llamada, para que los lectores vean que la variable puede cambiar: Increment(ref count);, no Increment(count);.