Parâmetros de método (Referência de C#)

No C#, argumentos podem ser passados para parâmetros por valor ou por referência. Lembre-se de que os tipos C# podem ser tipos de referência (class) ou tipos de valor (struct):

  • Passar por valor significa passar uma cópia da variável para o método.
  • Passar por referência significa passar o acesso à variável para o método.
  • Uma variável de um tipo de referência contém uma referência aos seus dados.
  • Uma variável de um tipo de valor contém seus dados diretamente.

Como um struct é um tipo de valor, ao passar um struct por valor a um método, esse método receberá e operará em uma cópia do argumento do struct. O método não tem acesso ao struct original no método de chamada e, portanto, não é possível alterá-lo de forma alguma. O método pode alterar somente a cópia.

Uma instância de classe é um tipo de referência e não é um tipo de valor. Quando um tipo de referência é passado por valor a um método, esse método receberá uma cópia da referência para a instância da classe. Ou seja, o método chamado recebe uma cópia do endereço da instância e o método de chamada retém o endereço original da instância. A instância de classe no método de chamada tem um endereço, o parâmetro do método chamado tem uma cópia do endereço e os dois endereços se referem ao mesmo objeto. Como o parâmetro contém apenas uma cópia do endereço, o método chamado não pode alterar o endereço da instância de classe no método de chamada. No entanto, o método chamado pode usar a cópia do endereço para acessar os membros de classe que o endereço original e a cópia do endereço referenciam. Se o método chamado alterar um membro de classe, a instância da classe original no método de chamada também será alterada.

O resultado do exemplo a seguir ilustra a diferença. O valor do campo willIChange da instância da classe foi alterado pela chamada ao método ClassTaker, pois o método usa o endereço no parâmetro para localizar o campo especificado da instância da classe. O campo willIChange do struct no método de chamada não foi alterado pela chamada ao método StructTaker, pois o valor do argumento é uma cópia do próprio struct e não uma cópia de seu endereço. StructTaker altera a cópia e a cópia será perdida quando a chamada para StructTaker for concluída.

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

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* Output:
    Class field = Changed
    Struct field = Not Changed
*/

Como um argumento é passado e se é um tipo de referência ou tipo de valor controla quais modificações feitas no argumento são visíveis do chamador.

Passar um tipo de valor por valor

Quando você passa um tipo de valorpor valor:

  • Se o método atribuir o parâmetro para se referir a um objeto diferente, essas alterações não serão visíveis do chamador.
  • Se o método modificar o estado do objeto referenciado pelo parâmetro, essas alterações não serão visíveis do chamador.

O exemplo a seguir demonstra a passagem de parâmetros de tipo de valor por valor. A variável n é passada por valor para o método SquareIt. Quaisquer alterações que ocorrem dentro do método não têm efeito sobre o valor original da variável.

int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);

SquareIt(n);  // Passing the variable by value.
System.Console.WriteLine("The value after calling the method: {0}", n);

// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();

static void SquareIt(int x)
// The parameter x is passed by value.
// Changes to x will not affect the original value of x.
{
    x *= x;
    System.Console.WriteLine("The value inside the method: {0}", x);
}
/* Output:
    The value before calling the method: 5
    The value inside the method: 25
    The value after calling the method: 5
*/

A variável n é um tipo de valor. Ela contém seus dados, o valor 5. Quando SquareIt é invocado, o conteúdo de n é copiado para o parâmetro x, que é elevado ao quadrado dentro do método. No entanto, em Main, o valor de n é o mesmo depois de chamar o método SquareIt como era antes. A alteração que ocorre dentro do método afeta apenas a variável local x.

Passar um tipo de valor por referência

Quando você passa um tipo de valorpor referência:

  • Se o método atribuir o parâmetro para se referir a um objeto diferente, essas alterações não serão visíveis do chamador.
  • Se o método modificar o estado do objeto referenciado pelo parâmetro, essas alterações não serão visíveis do chamador.

O exemplo a seguir é o mesmo que o exemplo anterior, exceto que o argumento é passado como um parâmetro ref. O valor do argumento subjacente, n, é alterado quando x é alterado no método.

int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);

SquareIt(ref n);  // Passing the variable by reference.
System.Console.WriteLine("The value after calling the method: {0}", n);

// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();

static void SquareIt(ref int x)
// The parameter x is passed by reference.
// Changes to x will affect the original value of x.
{
    x *= x;
    System.Console.WriteLine("The value inside the method: {0}", x);
}
/* Output:
    The value before calling the method: 5
    The value inside the method: 25
    The value after calling the method: 25
*/

Neste exemplo, não é o valor de n que é passado; em vez disso, é passada uma referência a n. O parâmetro x não é um int; é uma referência a um int, nesse caso, uma referência a n. Portanto, quando x é elevado ao quadrado dentro do método, o que é realmente é elevado ao quadrado é aquilo a que x se refere, n.

Passar um tipo de referência por valor

Quando você passa um tipo de referênciapor valor:

  • Se o método atribuir o parâmetro para se referir a um objeto diferente, essas alterações não serão visíveis do chamador.
  • Se o método modificar o estado do objeto referenciado pelo parâmetro, essas alterações ficarão visíveis do chamador.

O exemplo a seguir demonstra a passagem de um parâmetro de tipo de referência, arr, por valor, para um método, Change. Como o parâmetro é uma referência a arr, é possível alterar os valores dos elementos da matriz. No entanto, a tentativa de reatribuir o parâmetro para um local diferente de memória só funciona dentro do método e não afeta a variável original, arr.

int[] arr = { 1, 4, 5 };
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr[0]);

Change(arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr[0]);

static void Change(int[] pArray)
{
    pArray[0] = 888;  // This change affects the original element.
    pArray = new int[5] { -3, -1, -2, -3, -4 };   // This change is local.
    System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
/* Output:
    Inside Main, before calling the method, the first element is: 1
    Inside the method, the first element is: -3
    Inside Main, after calling the method, the first element is: 888
*/

No exemplo anterior, a matriz, arr, que é um tipo de referência, é passada para o método sem o parâmetro ref. Nesse caso, uma cópia da referência, que aponta para arr, é passada para o método. A saída mostra que é possível para o método alterar o conteúdo de um elemento de matriz, nesse caso de 1 para 888. No entanto, alocar uma nova parte da memória usando o operador new dentro do método Change faz a variável pArray referenciar uma nova matriz. Portanto, quaisquer alterações realizadas depois disso não afetarão a matriz original, arr, criada dentro de Main. Na verdade, duas matrizes são criadas neste exemplo, uma dentro de Main e outra dentro do método Change.

Passar um tipo de referência por referência

Quando você passa um tipo de referênciapor referência:

  • Se o método atribuir o parâmetro para se referir a um objeto diferente, essas alterações ficarão visíveis do chamador.
  • Se o método modificar o estado do objeto referenciado pelo parâmetro, essas alterações ficarão visíveis do chamador.

O exemplo a seguir é o mesmo que o exemplo anterior, exceto que a palavra-chave ref é adicionada ao cabeçalho e à chamada do método. Quaisquer alterações que ocorrem no método afetam a variável original no programa de chamada.

int[] arr = { 1, 4, 5 };
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr[0]);

Change(ref arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr[0]);

static void Change(ref int[] pArray)
{
    // Both of the following changes will affect the original variables:
    pArray[0] = 888;
    pArray = new int[5] { -3, -1, -2, -3, -4 };
    System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
/* Output:
    Inside Main, before calling the method, the first element is: 1
    Inside the method, the first element is: -3
    Inside Main, after calling the method, the first element is: -3
*/

Todas as alterações que ocorrem dentro do método afetam a matriz original em Main. Na verdade, a matriz original é realocada usando o operador new. Portanto, depois de chamar o método Change, qualquer referência a arr aponta para a matriz de cinco elementos, criada no método Change.

Escopo de referências e valores

Os métodos podem armazenar os valores dos parâmetros em campos. Quando os parâmetros são passados por valor, isso é sempre seguro. Os valores são copiados e os tipos de referência podem ser acessados quando armazenados em um campo. Passar parâmetros por referência com segurança requer que o compilador defina quando é seguro atribuir uma referência a uma nova variável. Para cada expressão, o compilador define um escopo que vincula o acesso a uma expressão ou variável. O compilador usa dois escopos: safe_to_escape e ref_safe_to_escape.

  • O escopo safe_to_escape define o escopo em que qualquer expressão pode ser acessada com segurança.
  • O escopo ref_safe_to_escape define o escopo em que uma referência a qualquer expressão pode ser acessada ou modificada com segurança

Informalmente, você pode pensar nesses escopos como o mecanismo para garantir que seu código nunca acesse ou modifique uma referência que não seja mais válida. Uma referência é válida desde que se refira a um objeto ou struct válido. O escopo safe_to_escape define quando uma variável pode ser atribuída ou reatribuída. O escopo ref_safe_to_escape define quando uma variável pode reenquadrar atribuída ou reatribuída. A atribuição atribui uma variável a um novo valor. A atribuição ref atribui a variável para se referir a um local de armazenamento diferente.

Modificadores

Os parâmetros declarados para um método sem in, ref nem out são passados para o método chamado pelo valor. Os refmodificadores e out os inmodificadores diferem nas regras de atribuição:

  • O argumento para um ref parâmetro deve ser definitivamente atribuído. O método chamado pode reatribuir esse parâmetro.
  • O argumento para um in parâmetro deve ser atribuído definitivamente. O método chamado não pode reatribuir esse parâmetro.
  • O argumento para um out parâmetro não precisa ser atribuído definitivamente. O método chamado deve atribuir o parâmetro.

Esta seção descreve as palavras-chave que podem ser usadas ao declarar parâmetros de método:

  • params especifica que esse parâmetro pode receber um número variável de argumentos.
  • in especifica que esse parâmetro é passado por referência, mas é lido apenas pelo método chamado.
  • ref especifica que esse parâmetro é passado por referência e pode ser lido ou gravado pelo método chamado.
  • out especifica que esse parâmetro é passado por referência e é gravado pelo método chamado.

Confira também