Cuándo usar ref y cuándo no es necesario en C #

104

Tengo un objeto que está en mi estado de memoria del programa y también tengo algunas otras funciones de trabajo a las que paso el objeto para modificar el estado. Lo he estado pasando por referencia a las funciones de trabajador. Sin embargo, encontré la siguiente función.

byte[] received_s = new byte[2048];
IPEndPoint tmpIpEndPoint = new IPEndPoint(IPAddress.Any, UdpPort_msg);
EndPoint remoteEP = (tmpIpEndPoint);

int sz = soUdp_msg.ReceiveFrom(received_s, ref remoteEP); 

Me confunde porque ambos received_sy remoteEPestán devolviendo cosas de la función. ¿Por qué remoteEPnecesita un refy received_sno?

También soy programador de corriente alterna, así que tengo problemas para sacarme los punteros de la cabeza.

Editar: Parece que los objetos en C # son punteros al objeto debajo del capó. Entonces, cuando pasa un objeto a una función, puede modificar el contenido del objeto a través del puntero y lo único que se pasa a la función es el puntero al objeto para que el objeto en sí no se esté copiando. Use ref o out si desea poder cambiar o crear un nuevo objeto en la función que es como un puntero doble.

Rex Logan
fuente

Respuestas:

216

Respuesta corta: lea mi artículo sobre el paso de argumentos .

Respuesta larga: cuando un parámetro de tipo de referencia se pasa por valor, solo se pasa la referencia, no una copia del objeto. Esto es como pasar un puntero (por valor) en C o C ++. Los cambios en el valor del parámetro en sí no serán vistos por la persona que llama, pero los cambios en el objeto que los puntos de referencia a la va a ser visto.

Cuando un parámetro (de cualquier tipo) se pasa por referencia, eso significa que la persona que llama ve cualquier cambio en el parámetro; los cambios en el parámetro son cambios en la variable.

El artículo explica todo esto con más detalle, por supuesto :)

Respuesta útil: casi nunca es necesario usar ref / out . Básicamente, es una forma de obtener otro valor de retorno y, por lo general, debe evitarse precisamente porque significa que el método probablemente esté tratando de hacer demasiado. Ese no es siempre el caso ( TryParseetc son los ejemplos canónicos de uso razonable de out) pero el uso de ref / out debería ser una rareza relativa.

Jon Skeet
fuente
38
Creo que ha mezclado su respuesta corta y su respuesta larga; ¡Ese es un gran artículo!
Programador fuera de la ley
23
@Outlaw: Sí, pero la respuesta corta en sí, la directiva para leer el artículo, tiene solo 6 palabras :)
Jon Skeet
27
¡Una referencia! :)
gonzobrains
5
@Liam Usar ref como lo hace puede parecer más explícito para usted, pero en realidad podría ser confuso para otros programadores (aquellos que saben lo que hace esa palabra clave de todos modos), porque esencialmente le está diciendo a los posibles llamantes: "Puedo modificar la variable que utilizado en el método de llamada, es decir, reasignarlo a un objeto diferente (o incluso a un valor nulo, si es posible), así que no se aferre a él, o asegúrese de validarlo cuando haya terminado ". Eso es bastante fuerte y completamente diferente de "este objeto se puede modificar", que siempre es el caso cada vez que pasa una referencia de objeto como parámetro.
mbargiel
1
@ M.Mimpen: C # 7 permitirá (con suerte)if (int.TryParse(text, out int value)) { ... use value here ... }
Jon Skeet
26

Piense en un parámetro no ref como un puntero y un parámetro ref como un puntero doble. Esto me ayudó más.

Casi nunca debería pasar valores por ref. Sospecho que si no fuera por problemas de interoperabilidad, el equipo de .Net nunca lo habría incluido en la especificación original. La forma OO de lidiar con la mayoría de los problemas que resuelven los parámetros de referencia es:

Para múltiples valores de retorno

  • Cree estructuras que representen los múltiples valores devueltos

Para primitivas que cambian en un método como resultado de la llamada al método (el método tiene efectos secundarios en los parámetros primitivos)

  • Implementar el método en un objeto como método de instancia y manipular el estado del objeto (no los parámetros) como parte de la llamada al método
  • Utilice la solución de valor de retorno múltiple y combine los valores de retorno con su estado
  • Cree un objeto que contenga un estado que pueda ser manipulado por un método y pase ese objeto como parámetro, y no las primitivas mismas.
Michael Meadows
fuente
8
Dios bueno. Tendría que leer esto 20x para entenderlo. Me parece un montón de trabajo extra solo para hacer algo simple.
PositiveGuy
El marco .NET no sigue su regla # 1. ('Para múltiples valores de retorno, cree estructuras'). Tomemos por ejemplo IPAddress.TryParse(string, out IPAddress).
Swen Kooij
@SwenKooij Tienes razón. Supongo que en la mayoría de los lugares donde usan los parámetros, o (a) están ajustando una API de Win32, o (b) eran los primeros días, y los programadores de C ++ estaban tomando decisiones.
Michael Meadows
@SwenKooij Sé que esta respuesta tiene muchos años de retraso, pero lo hace. Estamos acostumbrados a TryParse, pero eso no significa que sea bueno. Hubiera sido mejor si en lugar de tenerlo if (int.TryParse("123", out var theInt) { /* use theInt */ }tuviéramos var candidate = int.TrialParse("123"); if (candidate.Parsed) { /* do something with candidate.Value */ }más código, pero es mucho más consistente con el diseño del lenguaje C #.
Michael Meadows
9

Probablemente podría escribir una aplicación C # completa y nunca pasar ningún objeto / estructura por ref.

Tuve un profesor que me dijo esto:

El único lugar donde usarías refs es donde tú:

  1. Quiere pasar un objeto grande (es decir, los objetos / estructura tiene objetos / estructuras dentro de él a múltiples niveles) y copiarlo sería costoso y
  2. Está llamando a un Framework, API de Windows u otra API que lo requiera.

No lo hagas solo porque puedes. Puede ser mordido por algunos errores desagradables si comienza a cambiar los valores en un parámetro y no está prestando atención.

Estoy de acuerdo con su consejo, y en mis más de cinco años desde la escuela, nunca lo he necesitado más allá de llamar al Framework o la API de Windows.

Chris
fuente
3
Si planea implementar "Swap", pasar por ref podría ser útil.
Brian
@Chris, ¿habrá algún problema si uso la palabra clave ref para objetos pequeños?
ManirajSS
@TechnikEmpire "Pero, las alteraciones en el objeto dentro del alcance de la función llamada no se reflejan en la persona que llama". Eso está mal. Si paso una Persona a SetName(person, "John Doe"), la propiedad del nombre cambiará y ese cambio se reflejará en la persona que llama.
M. Mimpen
@ M.Mimpen Comentario eliminado. Apenas había aprendido C # en ese momento y claramente estaba hablando de mi * $$. Gracias por avisarme.
@Chris - Estoy bastante seguro de que pasar un objeto "grande" no es caro. Si simplemente lo pasa por valor, todavía está pasando un solo puntero, ¿verdad?
Ian
3

Dado queived_s es una matriz, está pasando un puntero a esa matriz. La función manipula los datos existentes en su lugar, sin cambiar la ubicación o el puntero subyacentes. La palabra clave ref significa que está pasando el puntero real a la ubicación y actualizando ese puntero en la función exterior, por lo que el valor en la función exterior cambiará.

Por ejemplo, la matriz de bytes es un puntero a la misma memoria antes y después, la memoria se acaba de actualizar.

La referencia de punto final en realidad está actualizando el puntero al punto final en la función exterior a una nueva instancia generada dentro de la función.

Chris Hynes
fuente
3

Piense en una referencia como lo que significa que está pasando un puntero por referencia. No usar una referencia significa que está pasando un puntero por valor.

Mejor aún, ignore lo que acabo de decir (probablemente sea engañoso, especialmente con los tipos de valor) y lea esta página de MSDN .

Brian
fuente
De hecho, no es cierto. Al menos la segunda parte. Cualquier tipo de referencia siempre se pasará por referencia, ya sea que use ref o no.
Erik Funkenbusch
En realidad, reflexionando más, eso no salió bien. La referencia se pasa realmente por valor sin el tipo de ref. Es decir, cambiar los valores apuntados por la referencia cambia los datos originales, pero cambiar la referencia en sí no cambia la referencia original.
Erik Funkenbusch
2
Un tipo de referencia sin referencia no se pasa por referencia. Una referencia al tipo de referencia se pasa por valor. Pero si quiere pensar en una referencia como un puntero a algo a lo que se hace referencia, lo que dije tiene sentido (pero pensar de esa manera puede ser engañoso). De ahí mi advertencia.
Brian
El enlace está muerto; pruebe este en su lugar: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
GIVE-ME-CHICKEN
0

Tengo entendido que todos los objetos derivados de la clase Object se pasan como punteros, mientras que los tipos ordinarios (int, struct) no se pasan como punteros y requieren ref. No estoy seguro de la cadena (¿se deriva en última instancia de la clase Object?)

Lucas
fuente
Esto podría necesitar alguna aclaración. ¿Cuál es la diferencia entre tyoes de valor y de referencia? Respuesta ¿Por qué es tan relevante el uso de la palabra clave ref en un parámetro?
oɔɯǝɹ
En .net, todo excepto los punteros, los parámetros de tipo y las interfaces se deriva de Object. Es importante comprender que los tipos "ordinarios" (correctamente llamados "tipos de valor") también heredan del objeto. De ahí en adelante: los tipos de valor (por defecto) se pasan por valor, mientras que los tipos de referencia se pasan por referencia. Si desea modificar un tipo de valor, en lugar de devolver uno nuevo (manejarlo como un tipo de referencia dentro de un método), tendría que usar la palabra clave ref. Pero eso es de mal estilo y no deberías hacerlo a menos que estés absolutamente seguro de que tienes que hacerlo :)
buddybubble
1
para responder a su pregunta: la cadena se deriva del objeto.Es un tipo de referencia que se comporta como un tipo de valor (por razones lógicas y de rendimiento)
buddybubble
0

Si bien estoy de acuerdo con la respuesta de Jon Skeet en general y algunas de las otras respuestas, hay un caso de uso para usar ref, y es para ajustar las optimizaciones de rendimiento. Se ha observado durante la elaboración de perfiles de rendimiento que establecer el valor de retorno de un método tiene ligeras implicaciones de rendimiento, mientras que el uso refcomo argumento mediante el cual el valor de retorno se completa en ese parámetro da como resultado la eliminación de este pequeño cuello de botella.

Esto realmente solo es útil cuando los esfuerzos de optimización se llevan a niveles extremos, sacrificando la legibilidad y tal vez la capacidad de prueba y el mantenimiento para ahorrar milisegundos o quizás milisegundos divididos.

Jon Davis
fuente
-1

Primero, la regla de la zona cero, las primitivas se pasan por valor (pila) y las no primitivas por referencia (montón) en el contexto de los TIPOS involucrados.

Los parámetros involucrados se pasan por Valor por defecto. Buen post que explica las cosas en detalle. http://yoda.arachsys.com/csharp/parameters.html

Student myStudent = new Student {Name="A",RollNo=1};

ChangeName(myStudent);

static void ChangeName(Student s1)
{
  s1.Name = "Z"; // myStudent.Name will also change from A to Z
                // {AS s1 and myStudent both refers to same Heap(Memory)
                //Student being the non-Primitive type
}

ChangeNameVersion2(ref myStudent);
static void ChangeNameVersion2(ref Student s1)
{
  s1.Name = "Z"; // Not any difference {same as **ChangeName**}
}

static void ChangeNameVersion3(ref Student s1)
{
    s1 = new Student{Name="Champ"};

    // reference(myStudent) will also point toward this new Object having new memory
    // previous mystudent memory will be released as it is not pointed by any object
}

Podemos decir (con advertencia) Los tipos no primitivos no son más que punteros Y cuando los pasamos por ref podemos decir que estamos pasando Double Pointer

Surender Singh Malik
fuente
Todos los parámetros se pasan por valor, de forma predeterminada, en C #. Cualquier parámetro puede pasarse por referencia. Para los tipos de referencia, el valor que se pasa (ya sea por referencia o por valor) es en sí mismo una referencia. Eso es completamente independiente de cómo se transmite.
Servicio
¡De acuerdo @Servy! "Cuando escuchamos las palabras" referencia "o" valor "debemos tener muy claro si nos referimos a que un parámetro es una referencia o un parámetro de valor, o si queremos decir que el tipo involucrado es una referencia o un tipo de valor 'Little Confusión por mi parte, cuando dije primero la regla de Zona cero, las primitivas se pasan por valor (pila) y las no primitivas por referencia (montón) Me refiero a los TIPOS involucrados, ¡no al parámetro! Cuando hablamos de parámetros, usted es correcto , Todos los parámetros se pasan por valor, por defecto en C #.
Surender Singh Malik
Decir que un parámetro es una referencia o un parámetro de valor no es un término estándar y es realmente ambiguo en cuanto a lo que quiere decir. El tipo de parámetro puede ser un tipo de referencia o un tipo de valor. Cualquier parámetro puede pasarse por valor o por referencia. Estos son conceptos ortogonales para cualquier parámetro dado. Tu publicación describe esto incorrectamente.
Servicio
1
Pasas los parámetros por referencia o por valor. Los términos "por referencia" y "por valor" no se utilizan para describir si un tipo es un tipo de valor o un tipo de referencia.
Servicio
1
No, es no una cuestión de percepción. Cuando usa el término incorrecto para referirse a algo, esa declaración se vuelve incorrecta . Es importante que las declaraciones correctas utilicen la terminología correcta para referirse a conceptos.
Servicio