2012-04-03 10 views
22

Siempre pensé que un parámetro de método con un tipo de clase se pasa como un parámetro de referencia por defecto. Aparentemente, ese no es siempre el caso. Considere estas pruebas unitarias en C# (usando MSTest).Pasar una clase como un parámetro ref en C# no siempre funciona como se esperaba. ¿Alguien puede explicar?

[TestClass] 
public class Sandbox 
{ 
    private class TestRefClass 
    { 
     public int TestInt { get; set; } 
    } 

    private void TestDefaultMethod(TestRefClass testClass) 
    { 
     testClass.TestInt = 1; 
    } 

    private void TestAssignmentMethod(TestRefClass testClass) 
    { 
     testClass = new TestRefClass() { TestInt = 1 }; 
    } 

    private void TestAssignmentRefMethod(ref TestRefClass testClass) 
    { 
     testClass = new TestRefClass() { TestInt = 1 }; 
    } 

    [TestMethod] 
    public void DefaultTest() 
    { 
     var testObj = new TestRefClass() { TestInt = 0 }; 
     TestDefaultMethod(testObj); 
     Assert.IsTrue(testObj.TestInt == 1); 
    } 

    [TestMethod] 
    public void AssignmentTest() 
    { 
     var testObj = new TestRefClass() { TestInt = 0 }; 
     TestAssignmentMethod(testObj); 
     Assert.IsTrue(testObj.TestInt == 1); 
    } 

    [TestMethod] 
    public void AssignmentRefTest() 
    { 
     var testObj = new TestRefClass() { TestInt = 0 }; 
     TestAssignmentRefMethod(ref testObj); 
     Assert.IsTrue(testObj.TestInt == 1); 
    } 
} 

Los resultados son que AssignmentTest() falla y pasar los otros dos métodos de prueba. Supongo que el problema es que la asignación de una nueva instancia al parámetro testClass rompe la referencia del parámetro, pero de alguna manera se agrega explícitamente la palabra clave ref.

¿Alguien puede dar una buena y detallada explicación de lo que está pasando aquí? Principalmente intento expandir mi conocimiento de C#; No tengo ningún escenario específico que estoy tratando de resolver ...

Respuesta

29

Lo que es casi siempre olvidado es que una clase no se pasa por referencia, la referencia a la clase se pasa por valor.

Esto es importante. En lugar de copiar toda la clase (pasar por valor en el sentido estereotípico), se copia referencia a esa clase (estoy tratando de evitar decir "puntero"). Esto es 4 u 8 bytes; mucho más apetecible que copiar toda la clase y en efecto significa que la clase se pasa "por referencia". En este punto, el método tiene su propia copia de la referencia a la clase. Asignación a esa referencia tiene un alcance dentro del método (el método reasignó solo su propia copia de la referencia).

Rechazar esa referencia (como en, hablar con miembros de la clase) funcionaría como era de esperar: vería la clase subyacente a menos que la cambie para ver una nueva instancia (que es lo que hace en su falla prueba).

El uso de la palabra clave ref efectivamente pasa la referencia por referencia (puntero a un tipo de cosa de puntero).

Como siempre, Jon Skeet ha proporcionado una visión general muy bien escrito:

http://www.yoda.arachsys.com/csharp/parameters.html

Prestar atención a la parte de "parámetros de referencia":

parámetros de referencia no pasa el valores de las variables utilizadas en la invocación del miembro de función - usan las variables mismas.

Si el método asigna algo a una referencia ref, a continuación, copia del llamante también se ve afectada (como habrá observado) porque están buscando en el misma referencia a una instancia en la memoria (en oposición a cada uno tiene su propia copia).

4

La convención predeterminada para los parámetros en C# es pasar por valor. Esto es cierto si el parámetro es class o struct. En el caso class, la referencia se pasa por valor mientras que en el caso struct se pasa una copia superficial del objeto completo.

Al entrar en la TestAssignmentMethod hay 2 referencias a un único objeto: testObj, que vive en AssignmentTest y testClass la que vive en TestAssignmentMethod. Si tuviera que mutar el objeto real a través de testClass o testObj sería visible para ambas referencias, ya que ambas apuntan al mismo objeto. En la primera línea, aunque se ejecuta

testClass = new TestRefClass() { TestInt = 1 } 

Esto crea un nuevo objeto y señala testClass a ella. Esto no altera dónde la referencia testObj apunta de ninguna manera porque testClass es una copia independiente. Ahora hay 2 objetos y 2 referencias que cada referencia apunta a una instancia de objeto diferente.

Si desea pasar por semántica de referencia, necesita usar un parámetro ref.

+1

Respuesta agradable y detallada, pero por alguna razón siempre me resulta irritante cuando alguien dice "los parámetros siempre pasan por valor, incluso si es una referencia que está pasando". No veo cómo ilumina una distinción tan pedante. –

+0

@RobertHarvey Irritante, pero lamentablemente en el caso predeterminado, siempre es verdadero (a menos que, por supuesto, 'ref' o' out' estén presentes). Creo que la mejor explicación que he leído fue de Jon Skeet, en su libro de C#. –

+0

@RobertHarvey la terminología es desafortunadamente confusa :( – JaredPar

2

El AssignmentTest utiliza TestAssignmentMethod que solamente cambia la referencia objeto pasado por valor.

Por lo que el objeto en sí se pasa por referencia, pero la referencia al objeto se pasa por valor. así que cuando lo hace:

testClass = new TestRefClass() { TestInt = 1 }; 

Está cambiando la referencia local de copiado pasado al método de referencia no es el que tiene en la prueba.

Así que aquí:

[TestMethod] 
public void AssignmentTest() 
{ 
    var testObj = new TestRefClass() { TestInt = 0 }; 
    TestAssignmentMethod(testObj); 
    Assert.IsTrue(testObj.TestInt == 1); 
} 

testObj es una variable de referencia. Cuando lo pasa al TestAssignmentMethod(testObj);, la referencia se pasa por valor. por lo tanto, cuando lo cambie en el método, referencia original aún apunta al mismo objeto.

2

Mis 2 centavos

Cuando la clase se pasa a un método que se envía una copia del espacio de direcciones de memoria que (una dirección a la casa que se está enviando). Entonces cualquier operación en esa dirección con efecto en la casa pero no cambiará la dirección en sí misma. (esto es el valor predeterminado)

Pasar la clase (objeto) por referencia tiene el efecto de pasar su dirección real en lugar de copiarla. Esto significa que si asigna un objeto nuevo al argumento pasado por referencia, cambiará la dirección real (similar a la reubicación). : D

Así es como yo lo veo.

Cuestiones relacionadas