¿Por qué las declaraciones de asignación devuelven un valor?

Esto está permitido:

int a, b, c; a = b = c = 16; string s = null; while ((s = "Hello") != null) ; 

A mi entender, asignación s = ”Hello”; solo debería asignar “Hello” a s , pero la operación no debería devolver ningún valor. Si eso fuera cierto, entonces ((s = "Hello") != null) produciría un error, ya que null se compararía con nada.

¿Cuál es el razonamiento detrás de permitir que las declaraciones de asignación devuelvan un valor?

A mi entender, asignación s = “Hola”; solo debería asignar “Hello” a s, pero la operación no debería devolver ningún valor.

Tu comprensión es 100% incorrecta. ¿Puedes explicar por qué crees esto falso?

¿Cuál es el razonamiento detrás de permitir que las declaraciones de asignación devuelvan un valor?

En primer lugar, las declaraciones de asignación no producen un valor. Las expresiones de asignación producen un valor. Una expresión de asignación es una statement legal; solo hay un puñado de expresiones que son declaraciones legales en C #: las expresiones esperadas de una expresión, creación de instancia, incremento, decremento, invocación y asignación pueden usarse donde se espera una statement.

Solo hay un tipo de expresión en C # que no produce ningún tipo de valor, es decir, una invocación de algo que se escribe como return void. (O, de forma equivalente, una espera de una tarea sin valor de resultado asociado). Cualquier otro tipo de expresión produce un valor o una variable o referencia o acceso de propiedad o evento, y así sucesivamente.

Tenga en cuenta que todas las expresiones que son legales como declaraciones son útiles para sus efectos secundarios . Esa es la idea clave aquí, y creo que tal vez la causa de su intuición es que las asignaciones deben ser declaraciones y no expresiones. Idealmente, tendríamos exactamente un efecto secundario por afirmación y ningún efecto secundario en una expresión. Es un poco extraño que el código de efecto lateral se pueda usar en un contexto de expresión.

El razonamiento detrás de permitir esta característica es porque (1) es frecuentemente conveniente y (2) es idiomática en los lenguajes tipo C.

Uno podría notar que la pregunta ha sido suplicada: ¿por qué esto es idiomático en las lenguas C-like?

Dennis Ritchie ya no está disponible para preguntar, desafortunadamente, pero creo que una tarea casi siempre deja atrás el valor que se acaba de asignar en un registro. C es un tipo de lenguaje muy “cercano a la máquina”. Parece plausible y de acuerdo con el diseño de C que haya una función de idioma que básicamente significa “seguir usando el valor que acabo de asignar”. Es muy fácil escribir un generador de código para esta característica; solo sigue usando el registro que almacena el valor asignado.

¿No has proporcionado la respuesta? Es para permitir exactamente los tipos de construcciones que ha mencionado.

Un caso común donde se usa esta propiedad del operador de asignación es leer líneas de un archivo …

 string line; while ((line = streamReader.ReadLine()) != null) // ... 

Mi uso favorito de las expresiones de asignación es para propiedades inicializadas de forma lenta.

 private string _name; public string Name { get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); } } 

Por un lado, te permite encadenar tus tareas, como en tu ejemplo:

 a = b = c = 16; 

Por otro, le permite asignar y verificar un resultado en una sola expresión:

 while ((s = foo.getSomeString()) != null) { /* ... */ } 

Ambos son motivos posiblemente dudosos, pero definitivamente hay personas a quienes les gustan estos constructos.

Además de los motivos ya mencionados (encadenamiento de asignaciones, establecer y probar dentro de ciclos while, etc.), para usar correctamente la instrucción using , necesita esta característica:

 using (Font font3 = new Font("Arial", 10.0f)) { // Use font3. } 

MSDN no recomienda declarar el objeto desechable fuera de la instrucción de uso, ya que permanecerá en el ámbito incluso después de que se haya eliminado (consulte el artículo de MSDN que he vinculado).

Me gustaría explayarme sobre un punto específico que Eric Lippert hizo en su respuesta y poner el centro de atención en una ocasión particular que nadie ha tocado en absoluto. Eric dijo:

[…] una tarea casi siempre deja atrás el valor que se acaba de asignar en un registro.

Me gustaría decir que la tarea siempre dejará atrás el valor que tratamos de asignar a nuestro operando izquierdo. No solo “casi siempre”. Pero no sé porque no he encontrado este problema comentado en la documentación. En teoría, podría ser un procedimiento implementado muy eficaz para “dejar atrás” y no reevaluar el operando de la izquierda, pero ¿es eficiente?

‘Eficiente’ sí para todos los ejemplos construidos hasta ahora en las respuestas de este hilo. ¿Pero eficiente en el caso de las propiedades y los indexadores que usan accesadores get y set? De ningún modo. Considera este código:

 class Test { public bool MyProperty { get { return true; } set { ; } } } 

Aquí tenemos una propiedad, que ni siquiera es un contenedor para una variable privada. Siempre que se le solicite, volverá verdadero, cada vez que uno trate de establecer su valor no hará nada. Por lo tanto, cada vez que se evalúa esta propiedad, él será sincero. Veamos qué pasa:

 Test test = new Test(); if ((test.MyProperty = false) == true) Console.WriteLine("Please print this text."); else Console.WriteLine("Unexpected!!"); 

¿Adivina qué imprime? ¡Imprime Unexpected!! . Como resultado, el acceso de conjunto se llama, de hecho, que no hace nada. Pero a partir de entonces, el acceso de obtención nunca se llama en absoluto. La tarea simplemente deja atrás el valor false que intentamos asignar a nuestra propiedad. Y este valor false es lo que la statement if evalúa.

Terminaré con un ejemplo del mundo real que me hizo investigar este tema. Hice un indexador que era un contenedor conveniente para una colección ( List ) que una clase mía tenía como variable privada.

El parámetro enviado al indexador era una cadena, que debía tratarse como un valor en mi colección. El acceso de obtención simplemente devolvería verdadero o falso si ese valor existía en la lista o no. Por lo tanto, el acceso de obtención fue otra forma de utilizar el método List.Contains .

Si se llamaba al descriptor de acceso del indexador con una cadena como argumento y el operando derecho era un bool true , él agregaría ese parámetro a la lista. Pero si el mismo parámetro se envió al descriptor de acceso y el operando de la derecha era un bool false , en su lugar, eliminaría el elemento de la lista. Por lo tanto, el acceso del conjunto se usó como una alternativa conveniente para List.Add y List.Remove .

Pensé que tenía una “API” limpia y compacta que englobaba la lista con mi propia lógica implementada como puerta de enlace. Solo con la ayuda de un indexador podía hacer muchas cosas con unas pocas teclas. Por ejemplo, ¿cómo puedo tratar de agregar un valor a mi lista y verificar que esté ahí? Pensé que esta era la única línea de código necesaria:

 if (myObject["stringValue"] = true) ; // Set operation succeeded..! 

Pero como lo demostró en mi ejemplo anterior, ni siquiera se llamó al acceso de obtención que se supone debe ver si el valor realmente está en la lista. El true valor siempre se dejó atrás, destruyendo efectivamente cualquier lógica que hubiera implementado en mi acceso de obtención.

Si la asignación no devolvió un valor, la línea a = b = c = 16 tampoco funcionaría.

También ser capaz de escribir cosas como while ((s = readLine()) != null) puede ser útil a veces.

Entonces, la razón detrás de dejar que la asignación devuelva el valor asignado es permitirle hacer esas cosas.

Creo que estás entendiendo mal cómo el analizador interpretará esa syntax. La asignación se evaluará primero , y el resultado se comparará con NULL, es decir, la statement es equivalente a:

 s = "Hello"; //s now contains the value "Hello" (s != null) //returns true. 

Como otros han señalado, el resultado de una asignación es el valor asignado. Me resulta difícil imaginar la ventaja de tener

((s = "Hello") != null)

y

 s = "Hello"; s != null; 

no ser equivalente …

Creo que la razón principal es la similitud (intencional) con C ++ y C. Hacer que el operador de asignación (y muchos otros constructos de lenguaje) se comporten como sus contrapartes de C ++ simplemente sigue el principio de la menor sorpresa, y cualquier progtwigdor que venga de otro rizado el lenguaje de corchete puede usarlos sin pensarlo mucho. Ser fácil de elegir para los progtwigdores de C ++ fue uno de los principales objectives de diseño de C #.

Por las dos razones que incluyes en tu publicación
1) para que pueda hacer a = b = c = 16
2) para que pueda probar si una asignación tuvo éxito if ((s = openSomeHandle()) != null)

El hecho de que ‘a ++’ o ‘printf (‘ foo ‘)’ pueda ser útil como una statement independiente o como parte de una expresión más grande significa que C tiene que permitir la posibilidad de que los resultados de la expresión puedan o no ser usado. Dado eso, hay una noción general de que las expresiones que podrían ‘devolver’ un valor útil también pueden hacerlo. El encadenamiento de asignaciones puede ser ligeramente “interesante” en C, e incluso más interesante en C ++, si todas las variables en cuestión no tienen exactamente el mismo tipo. Tales usos probablemente sean mejor evitados.

Una ventaja adicional que no veo dado en las respuestas aquí, es que la syntax para la asignación se basa en la aritmética.

Ahora x = y = b = c = 2 + 3 significa algo diferente en aritmética que un lenguaje de estilo C; en aritmética es una afirmación, afirmamos que x es igual a y, y en un lenguaje de estilo C, es una instrucción que hace que x sea igual a y, etc., después de que se ejecuta.

Dicho esto, todavía hay suficiente relación entre la aritmética y el código que no tiene sentido negar lo que es natural en la aritmética a menos que haya una buena razón. (La otra cosa que los lenguajes de estilo C tomaron del uso del símbolo de igualdad es el uso de == para la comparación de igualdad. Sin embargo, aquí porque el más a la derecha == devuelve un valor, este tipo de encadenamiento sería imposible).

Otro gran ejemplo de caso de uso, lo uso todo el tiempo:

 var x = _myVariable ?? (_myVariable = GetVariable()); //for example: when used inside a loop, "GetVariable" will be called only once