Parámetros de métodos

De manera predeterminada, los argumentos de C# se pasan a funciones por valor. Esto significa que se pasa una copia de la variable al método. Para los tipos de valor (struct), se pasa una copia del valor al método. Para los tipos de referencia (class), se pasa una copia de la referencia al método. Los modificadores de parámetro permiten pasar argumentos por referencia. Los conceptos siguientes le ayudarán a comprender estas diferencias y a usar los modificadores de parámetro:

  • Pasar por valor significa pasar una copia de la variable al método.
  • Pasar por referencia significa pasar el acceso a la variable al método.
  • Una variable de un tipo de referencia contiene una referencia a sus datos.
  • Una variable de un tipo de valor contiene sus datos directamente.

Dado que una estructura es un tipo de valor, el método recibe y opera en una copia del argumento de estructura cuando se pasa una estructura por valor a un método. El método no tiene acceso al struct original en el método de llamada y, por lo tanto, no puede cambiarlo de ninguna manera. El método solo puede cambiar la copia.

Una instancia de una clase es un tipo de referencia, no un tipo de valor. Cuando un tipo de referencia se pasa mediante valor a un método, el método recibe una copia de la referencia a la instancia de clase. Ambas variables hacen referencia al mismo objeto. El parámetro es una copia de la referencia. El método llamado no puede reasignar la instancia en el método autor de la llamada. Sin embargo, el método llamado puede usar la copia de la referencia para acceder a los miembros de la instancia. Si el método llamado cambia un miembro de la instancia, el método autor de la llamada también ve esos cambios, ya que hace referencia a la misma instancia.

En el resultado del ejemplo siguiente se ilustra la diferencia. El método ClassTaker cambia el valor del campo willIChange porque el método usa la dirección en el parámetro para buscar el campo especificado de la instancia de la clase. El campo willIChange de la estructura en el método autor de la llamada no se cambia mediante la llamada al método StructTaker porque el valor del argumento es una copia de la propia estructura, no una copia de su dirección. StructTaker cambia la copia, y la copia se pierde cuando se completa la llamada a StructTaker.

class TheClass
{
    public string? willIChange;
}

struct TheStruct
{
    public string willIChange;
}

class TestClassAndStruct
{
    static void ClassTaker(TheClass c)
    {
        c.willIChange = "Changed";
    }

    static void StructTaker(TheStruct s)
    {
        s.willIChange = "Changed";
    }

    public static void Main()
    {
        TheClass testClass = new TheClass();
        TheStruct testStruct = new TheStruct();

        testClass.willIChange = "Not Changed";
        testStruct.willIChange = "Not Changed";

        ClassTaker(testClass);
        StructTaker(testStruct);

        Console.WriteLine("Class field = {0}", testClass.willIChange);
        Console.WriteLine("Struct field = {0}", testStruct.willIChange);
    }
}
/* Output:
    Class field = Changed
    Struct field = Not Changed
*/

Combinaciones de tipo de parámetro y modo de argumento

Cómo se pasa un argumento y, si es un tipo de referencia o un tipo de valor, controla qué modificaciones realizadas en el argumento son visibles desde el autor de la llamada:

  • Cuando se pasa un tipo de valorpor valor:
    • Si el método asigna el parámetro para hacer referencia a otro objeto, esos cambios no son visibles desde el autor de la llamada.
    • Si el método modifica el estado del objeto al que hace referencia el parámetro, esos cambios no son visibles desde el autor de la llamada.
  • Cuando se pasa un tipo de referenciapor valor:
    • Si el método asigna el parámetro para hacer referencia a otro objeto, esos cambios no son visibles desde el autor de la llamada.
    • Si el método modifica el estado del objeto al que hace referencia el parámetro, esos cambios son visibles desde el autor de la llamada.
  • Cuando se pasa un tipo de valorpor referencia:
    • Si el método asigna el parámetro para hacer referencia a otro objeto, esos cambios no son visibles desde el autor de la llamada.
    • Si el método modifica el estado del objeto al que hace referencia el parámetro, esos cambios son visibles desde el autor de la llamada.
  • Cuando se pasa un tipo de referenciapor referencia:
    • Si el método asigna el parámetro para hacer referencia a otro objeto, esos cambios son visibles desde el autor de la llamada.
    • Si el método modifica el estado del objeto al que hace referencia el parámetro, esos cambios son visibles desde el autor de la llamada.

Pasar un tipo de referencia por referencia permite que el método llamado pueda reemplazar el objeto al que hace referencia el parámetro de referencia en el autor de la llamada. La ubicación de almacenamiento del objeto se pasa al método como el valor del parámetro de referencia. Si cambia el valor de la ubicación de almacenamiento del parámetro (para que apunte a un nuevo objeto), también debe cambiar la ubicación de almacenamiento a la que se refiere el autor de la llamada. En el ejemplo siguiente se pasa una instancia de un tipo de referencia como un parámetro ref.

class Product
{
    public Product(string name, int newID)
    {
        ItemName = name;
        ItemID = newID;
    }

    public string ItemName { get; set; }
    public int ItemID { get; set; }
}

private static void ChangeByReference(ref Product itemRef)
{
    // Change the address that is stored in the itemRef parameter.
    itemRef = new Product("Stapler", 12345);
}

private static void ModifyProductsByReference()
{
    // Declare an instance of Product and display its initial values.
    Product item = new Product("Fasteners", 54321);
    System.Console.WriteLine("Original values in Main.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);

    // Pass the product instance to ChangeByReference.
    ChangeByReference(ref item);
    System.Console.WriteLine("Calling method.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);
}

// This method displays the following output:
// Original values in Main.  Name: Fasteners, ID: 54321
// Calling method.  Name: Stapler, ID: 12345

Contexto seguro de referencias y valores

Los métodos pueden almacenar los valores de los parámetros en campos. Cuando los parámetros se pasan por valor, normalmente es seguro. Los valores se copian y se puede acceder a los tipos de referencia cuando se almacenan en un campo. Pasar parámetros por referencia de forma segura requiere que el compilador defina cuándo es seguro asignar una referencia a una nueva variable. Para cada expresión, el compilador define un contexto seguro que enlaza el acceso a una expresión o una variable. El compilador usa dos ámbitos: safe-context y ref-safe-context.

  • El ámbito safe-context define el ámbito en el que se puede acceder de forma segura a cualquier expresión.
  • El ámbito ref-safe-context define el ámbito en el que se puede acceder o modificar de forma segura una referencia a cualquier expresión.

Informalmente, puede considerar estos ámbitos como un mecanismo para asegurarse de que el código nunca accede o modifica una referencia que ya no es válida. Una referencia es válida siempre que haga referencia a un objeto o estructura válidos. El ámbito safe-context define cuándo se puede asignar o reasignar una variable. El ámbito ref-safe-context define cuándo una variable se puede asignar por referencia o reasignar por referencia. La asignación asigna una variable a un nuevo valor; la asignación por referencia asigna la variable para hacer referencia a otra ubicación de almacenamiento.

Parámetros de referencia

Se aplica uno de los modificadores siguientes a una declaración de parámetro para pasar argumentos por referencia en lugar de por valor:

  • ref: el argumento se debe inicializar antes de llamar al método. El método puede asignar un nuevo valor al parámetro, pero no es necesario hacerlo.
  • out: el método autor de la llamada no tiene que inicializar el argumento antes de llamar al método. El método debe asignar un valor al parámetro.
  • readonly ref: el argumento se debe inicializar antes de llamar al método. El método no puede asignar un nuevo valor al parámetro.
  • in: el argumento se debe inicializar antes de llamar al método. El método no puede asignar un nuevo valor al parámetro. El compilador puede crear una variable temporal para contener una copia del argumento en los parámetros in.

Los miembros de una clase no pueden tener signaturas que se diferencien solo por ref, ref readonly, in o out. Si la única diferencia entre dos miembros de un tipo es que uno de ellos tiene un parámetro ref y el otro tiene un parámetro out, ref readonly o in, se produce un error de compilador. En cambio, los métodos se pueden sobrecargar cuando un método tiene un parámetro ref, ref readonly, in o out, y el otro tiene un parámetro que se pasa por valor, como se muestra en el ejemplo siguiente. En otras situaciones que requieran signatura coincidente, como ocultar o reemplazar, in, ref, ref readonly y out forman parte de la signatura y no coinciden entre sí.

Cuando un parámetro tiene uno de los modificadores anteriores, el argumento correspondiente puede tener un modificador compatible:

  • Un argumento de un parámetro ref debe incluir el modificador ref.
  • Un argumento de un parámetro out debe incluir el modificador out.
  • Un argumento de un parámetro in puede incluir opcionalmente el modificador in. Si, en su lugar, se usa en el argumento el modificador ref, el compilador emite una advertencia.
  • Un argumento de un parámetro ref readonly debe incluir los modificadores in o ref, pero no ambos. Si no se incluye ninguno de los modificadores, el compilador emite una advertencia.

Al usar estos modificadores, describen cómo se usa el argumento:

  • ref significa que el método puede leer o escribir el valor del argumento.
  • out significa que el método establece el valor del argumento.
  • ref readonly significa que el método lee, pero no puede escribir, el valor del argumento. El argumento se debe pasar por referencia.
  • in significa que el método lee, pero no puede escribir, el valor del argumento. El argumento se pasará por referencia o mediante una variable temporal.

Las propiedades no son variables. Son métodos y no se pueden pasar a parámetros ref. No puede usar los modificadores de parámetros anteriores en los siguientes tipos de métodos:

  • Métodos asincrónicos, que se definen mediante el uso del modificador async.
  • Métodos de iterador, que incluyen una instrucción yield return o yield break.

Los métodos de extensión también tienen restricciones en el uso de estas palabras clave en los argumentos:

  • No se puede usar la palabra clave out en el primer argumento de un método de extensión.
  • No se puede usar la palabra clave ref en el primer argumento de un método de extensión cuando el argumento no es de tipo struct ni un tipo genérico no restringido para ser de tipo struct.
  • No se pueden usar las palabras claves ref readonly e in a menos que el primer argumento sea de tipo struct.
  • No se pueden usar las palabras clave ref readonly e in en ningún tipo genérico, incluso cuando está restringido para ser de tipo struct.

ref (modificador de parámetro)

Para usar un parámetro ref, la definición de método y el método de llamada deben utilizar explícitamente la palabra clave ref, como se muestra en el ejemplo siguiente. (Salvo que el método de llamada puede omitir ref al realizar una llamada COM).

void Method(ref int refArgument)
{
    refArgument = refArgument + 44;
}

int number = 1;
Method(ref number);
Console.WriteLine(number);
// Output: 45

Un argumento que se pasa a un parámetro ref se debe inicializar antes de pasarlo.

out (modificador de parámetro)

Para usar un parámetro out, tanto la definición de método como el método de llamada deben utilizar explícitamente la palabra clave out. Por ejemplo:

int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod);     // value is now 44

void OutArgExample(out int number)
{
    number = 44;
}

Las variables que se han pasado como argumentos out no tienen que inicializarse antes de pasarse en una llamada al método. En cambio, se necesita el método que se ha llamado para asignar un valor antes de que el método se devuelva.

Los métodos de deconstrucción declaran sus parámetros con el modificador out para devolver varios valores. Otros métodos pueden devolver tuplas de valores para varios valores devueltos.

Puede declarar una variable en una instrucción independiente antes de pasarla como un argumento out. También puede declarar la variable out en la lista de argumentos de la llamada de método, en lugar de en una declaración de variable independiente. Las declaraciones de variables out generan un código legible más compacto y, además, evitan que asigne un valor a la variable antes de la llamada al método de manera involuntaria. El ejemplo siguiente define la variable number en la llamada al método Int32.TryParse.

string numberAsString = "1640";

if (Int32.TryParse(numberAsString, out int number))
    Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
    Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
//       Converted '1640' to 1640

También puede declarar una variable local con tipo implícito.

Modificador ref readonly

El modificador ref readonly debe estar presente en la declaración del método. Un modificador en el sitio de llamada es opcional. Se puede usar el modificador in o ref. El modificador ref readonly no es válido en el sitio de llamada. El modificador que se usa en el sitio de llamada puede ayudar a describir las características del argumento. Solo puede usar ref si el argumento es una variable y se puede escribir. Solo puede usar in cuando el argumento es una variable. Puede ser de escritura o de solo lectura. No se puede agregar ningún modificador si el argumento no es una variable, sino una expresión. En los ejemplos siguientes, se muestran estas condiciones. El método siguiente usa el modificador ref readonly para indicar que una estructura grande se debe pasar por referencia por motivos de rendimiento:

public static void ForceByRef(ref readonly OptionStruct thing)
{
    // elided
}

Puede llamar al método mediante el modificador ref o in. Si omite el modificador, el compilador emite una advertencia. Cuando el argumento es una expresión, no una variable, no se pueden agregar los modificadores in ni ref, por lo que debe suprimir la advertencia:

ForceByRef(in options);
ForceByRef(ref options);
ForceByRef(options); // Warning! variable should be passed with `ref` or `in`
ForceByRef(new OptionStruct()); // Warning, but an expression, so no variable to reference

Si la variable es una variable readonly, debe usar el modificador in. El compilador emite un error si usa el modificador ref en su lugar.

El modificador ref readonly indica que el método espera que el argumento sea una variable en lugar de una expresión que no sea una variable. Algunos ejemplos de expresiones que no son variables son las constantes, los valores devueltos de un método y las propiedades. Si el argumento no es una variable, el compilador emite una advertencia.

in (modificador de parámetro)

El modificador in es necesario en la declaración del método, pero no es necesario en el sitio de llamada.

int readonlyArgument = 44;
InArgExample(readonlyArgument);
Console.WriteLine(readonlyArgument);     // value is still 44

void InArgExample(in int number)
{
    // Uncomment the following line to see error CS8331
    //number = 19;
}

El modificador in permite al compilador crear una variable temporal para el argumento y pasar una referencia de solo lectura a ese argumento. El compilador siempre crea una variable temporal cuando se debe convertir el argumento, cuando hay una conversión implícita del tipo de argumento o cuando el argumento es un valor que no es una variable. Por ejemplo, cuando el argumento es un valor literal o el valor devuelto desde el descriptor de acceso de una propiedad. Cuando la API requiera que el argumento se pase por referencia, elija el modificador ref readonly en lugar del modificador in.

Los métodos definidos mediante parámetros in pueden obtener una optimización del rendimiento. Algunos argumentos de tipo struct pueden tener un gran tamaño y, cuando se llama a métodos en bucles de pequeñas dimensiones o rutas de acceso de código crítico, el costo de copiar esas estructuras resulta importante. Los métodos declaran parámetros in para especificar qué argumentos se pueden pasar por referencia sin ningún riesgo porque el método llamado no modifica el estado de ese argumento. Al pasar esos argumentos por referencia se evita la copia (potencialmente) costosa. Se agrega explícitamente el modificador in en el sitio de llamada para garantizar que el argumento se pasa por referencia, y no por valor. Usar explícitamente in tiene los dos efectos siguientes:

  • Al especificar in en el sitio de llamada, se fuerza al compilador a seleccionar un método definido con un parámetro in coincidente. En caso contrario, cuando dos métodos se diferencian solo en presencia de in, la sobrecarga por valor es una coincidencia mejor.
  • Al especificar in, declara su intención de pasar un argumento por referencia. Los argumentos usados con in deben representar una ubicación a la que se pueda hacer referencia directamente. Se aplican las mismas reglas generales para los argumentos out y ref: no se pueden usar constantes, propiedades normales u otras expresiones que produzcan valores. En caso contrario, si se omite in en el sitio de llamada, se informa al compilador de que le permitirá crear una variable temporal para pasar por referencia de solo lectura al método. El compilador crea una variable temporal para superar varias restricciones con argumentos in:
    • Una variable temporal permite constantes en tiempo de compilación como parámetros in.
    • Una variable temporal permite propiedades u otras expresiones para parámetros in.
    • Una variable temporal permite argumentos en los que hay una conversión implícita desde el tipo de argumento hacia el tipo de parámetro.

En todas las instancias anteriores, el compilador crea una variable temporal que almacena el valor de la constante, la propiedad u otra expresión.

Estas reglas se muestran en este código:

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // OK, temporary variable created.
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // OK, temporary int created with the value 0
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // passed by readonly reference
Method(in i); // passed by readonly reference, explicitly using `in`

Supongamos ahora que hay disponible otro método que usa argumentos por valor. Los resultados cambian como se muestra en este código:

static void Method(int argument)
{
    // implementation removed
}

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // Calls overload passed by value
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // Calls overload passed by value.
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // Calls overload passed by value
Method(in i); // passed by readonly reference, explicitly using `in`

La única llamada de método donde se pasa el argumento por referencia es la última.

Nota:

El código anterior usa int como el tipo de argumento para simplificar el trabajo. Como int no es más grande que una referencia en la mayoría de máquinas modernas, no supone ninguna ventaja pasar un único int como una referencia de solo lectura.

Modificador params

No se permiten otros parámetros después de la palabra clave params en una declaración de método, y solo se permite una palabra clave params en una declaración de método.

Si el tipo declarado del parámetro params no es una matriz unidimensional, se produce el error CS0225 del compilador.

Cuando se llama a un método con un parámetro params, se puede pasar:

  • Una lista separada por comas de argumentos del tipo de los elementos de la matriz.
  • Una matriz de argumentos del tipo especificado.
  • Sin argumentos. Si no envía ningún argumento, la longitud de la lista params es cero.

En el ejemplo siguiente se muestran varias maneras de enviar argumentos a un parámetro params.

public class MyClass
{
    public static void UseParams(params int[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    public static void UseParams2(params object[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        // You can send a comma-separated list of arguments of the
        // specified type.
        UseParams(1, 2, 3, 4);
        UseParams2(1, 'a', "test");

        // A params parameter accepts zero or more arguments.
        // The following calling statement displays only a blank line.
        UseParams2();

        // An array argument can be passed, as long as the array
        // type matches the parameter type of the method being called.
        int[] myIntArray = { 5, 6, 7, 8, 9 };
        UseParams(myIntArray);

        object[] myObjArray = { 2, 'b', "test", "again" };
        UseParams2(myObjArray);

        // The following call causes a compiler error because the object
        // array cannot be converted into an integer array.
        //UseParams(myObjArray);

        // The following call does not cause an error, but the entire
        // integer array becomes the first element of the params array.
        UseParams2(myIntArray);
    }
}
/*
Output:
    1 2 3 4
    1 a test

    5 6 7 8 9
    2 b test again
    System.Int32[]
*/