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

126

Esto está permitido:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

Según tengo entendido, la asignación s = ”Hello”;solo debe hacer “Hello”que se asigne s, pero la operación no debe devolver ningún valor. Si eso fuera cierto, entonces ((s = "Hello") != null)produciría un error, ya que nullno se compararía con nada.

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

usuario437291
fuente
44
Como referencia, este comportamiento se define en la especificación de C # : "El resultado de una expresión de asignación simple es el valor asignado al operando izquierdo. El resultado tiene el mismo tipo que el operando izquierdo y siempre se clasifica como un valor".
Michael Petrotta
1
@Skurmedel No soy el votante, estoy de acuerdo contigo. ¿Pero tal vez porque se pensó que era una pregunta retórica?
jsmith
En la asignación pascal / delphi: = no devuelve nada. lo odio.
Andrey
20
Estándar para lenguajes en la rama C. Las asignaciones son expresiones.
Hans Passant
"la asignación ... solo debería hacer que" Hola "se asigne a s, pero la operación no debería devolver ningún valor" - ¿Por qué? "¿Cuál es el razonamiento detrás de permitir que las declaraciones de asignación devuelvan un valor?" - Porque es útil, como lo demuestran tus propios ejemplos. (Bueno, tu segundo ejemplo es tonto, pero while ((s = GetWord()) != null) Process(s);no lo es).
Jim Balter

Respuestas:

160

A mi entender, la asignación s = "Hola"; solo debería hacer que se asigne "Hola" a s, pero la operación no debería devolver ningún valor.

Tu comprensión es 100% incorrecta. ¿Puedes explicar por qué crees esta cosa falsa?

¿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 declaración legal; solo hay un puñado de expresiones que son declaraciones legales en C #: en espera de una expresión, se pueden usar expresiones de construcción de instancias, incremento, decremento, invocación y asignación cuando se espera una declaración.

Solo hay un tipo de expresión en C # que no produce algún tipo de valor, a saber, una invocación de algo que se escribe como vacío. (O, de manera equivalente, esperar una tarea sin ningún valor de resultado asociado). Cualquier otro tipo de expresión produce un valor o variable o referencia o acceso de propiedad o acceso a eventos, y así sucesivamente.

Tenga en cuenta que todas las expresiones que son legales como declaraciones son útiles por sus efectos secundarios . Esa es la idea clave aquí, y creo que quizás la causa de su intuición de que las tareas deben ser declaraciones y no expresiones. Idealmente, tendríamos exactamente un efecto secundario por enunciado y ningún efecto secundario en una expresión. Se es un poco extraño que código-efectuar lado se puede utilizar en un contexto de expresión en absoluto.

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

Uno podría notar que la pregunta ha sido planteada: ¿por qué esto es idiomático en lenguajes tipo C?

Dennis Ritchie ya no está disponible para preguntar, desafortunadamente, pero supongo que una tarea casi siempre deja atrás el valor que se acaba de asignar en un registro. C es un lenguaje muy "cercano a la máquina". Parece plausible y de acuerdo con el diseño de C que haya una función de lenguaje 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; simplemente sigue usando el registro que almacenó el valor asignado.

Eric Lippert
fuente
1
No era consciente de que cualquier cosa que devuelve un valor (incluso la construcción instancia se considera una expresión)
user437291
9
@ user437291: Si la construcción de instancia no fuera una expresión, no podría hacer nada con el objeto construido: no podría asignarlo a algo, no podría pasarlo a un método y no podría llamar a ningún método en eso. Sería bastante inútil, ¿no?
Timwi
1
Cambiar una variable es en realidad "solo" un efecto secundario del operador de asignación, que, creando una expresión, produce un valor como su propósito principal.
mk12
2
"Tu comprensión es 100% incorrecta". - Si bien el uso del inglés por parte del OP no es el mejor, su "comprensión" se trata de lo que debería ser el caso, por lo que es una opinión, por lo que no es el tipo de cosa que puede ser "100% incorrecta" . Algunos diseñadores de idiomas están de acuerdo con el "debería" del OP, y hacen que las tareas no tengan valor o de lo contrario les prohíben ser subexpresiones. Creo que es una reacción exagerada al =/ ==typo, que se soluciona fácilmente al no usar el valor de a =menos que esté entre paréntesis. por ejemplo, if ((x = y))o if ((x = y) == true)está permitido pero if (x = y)no lo está.
Jim Balter
"No sabía que todo lo que devuelve un valor ... se considera una expresión" - Hmm ... todo lo que devuelve un valor se considera una expresión - los dos son prácticamente sinónimos.
Jim Balter
44

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

Un caso común en el que se utiliza esta propiedad del operador de asignación es leer líneas de un archivo ...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
Timwi
fuente
1
+1 Este tiene que ser uno de los usos más prácticos de esta construcción
Mike Burton el
11
Realmente me gusta para los inicializadores de propiedades realmente simples, pero hay que tener cuidado y trazar la línea para una baja "simple" de legibilidad: return _myBackingField ?? (_myBackingField = CalculateBackingField());mucho menos desperdicio que verificar nulo y asignar.
Tom Mayfield
35

Mi uso favorito de expresiones de asignación es para propiedades perezosamente inicializadas.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
Nathan Baulch
fuente
55
Es por eso que tanta gente ha sugerido la ??=sintaxis.
configurador
27

Por un lado, le permite encadenar sus tareas, como en su ejemplo:

a = b = c = 16;

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

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

Ambas son posiblemente razones dudosas, pero definitivamente hay personas a las que les gustan estas construcciones.

Michael Burr
fuente
55
El encadenamiento de +1 no es una característica crítica, pero es bueno ver que "simplemente funciona" cuando tantas cosas no funcionan.
Mike Burton
+1 para encadenar también; Siempre pensé en eso como un caso especial manejado por el lenguaje en lugar del efecto secundario natural de los "valores de retorno de expresiones".
Caleb Bell
2
También le permite utilizar la asignación en devoluciones:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz
14

Además de las razones ya mencionadas (encadenamiento de tareas, establecer y probar dentro de los bucles while, etc.), para usar correctamente la usingdeclaración, necesita esta función:

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

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

Martin Törnwall
fuente
44
No creo que esto sea realmente un encadenamiento de tareas per se.
kenny
1
@kenny: Uh ... No, pero tampoco dije que lo fuera. Como dije, aparte de las razones ya mencionadas, que incluyen el encadenamiento de asignaciones, la declaración de uso sería mucho más difícil de usar si no fuera por el hecho de que el operador de asignación devuelve el resultado de la operación de asignación. Esto no está realmente relacionado con el encadenamiento de tareas.
Martin Törnwall
1
Pero como Eric señaló anteriormente, "las declaraciones de asignación no devuelven un valor". En realidad, esto es solo la sintaxis del lenguaje de la declaración de uso, prueba de ello es que no se puede usar una "expresión de asignación" en una declaración de uso.
kenny
@kenny: Disculpas, mi terminología estaba claramente equivocada. Me doy cuenta de que la declaración de uso podría implementarse sin que el lenguaje tenga soporte general para las expresiones de asignación que devuelven un valor. Sin embargo, eso sería, en mi opinión, terriblemente inconsistente.
Martin Törnwall
2
@kenny: En realidad, uno puede usar una declaración de definición y asignación, o cualquier expresión en using. Todos estos son legales: using (X x = new X()), using (x = new X()), using (x). Sin embargo, en este ejemplo, el contenido de la instrucción using es una sintaxis especial que no depende en absoluto de que la asignación devuelva un valor; Font font3 = new Font("Arial", 10.0f)no es una expresión y no es válida en ningún lugar que espere expresiones.
configurador
10

Me gustaría dar más detalles sobre un punto específico que Eric Lippert hizo en su respuesta y poner el foco en una ocasión particular que nadie más ha tocado. Eric dijo:

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

Me gustaría decir que la asignación siempre dejará atrás el valor que intentamos asignar a nuestro operando izquierdo. No solo "casi siempre". Pero no lo sé porque no he encontrado este problema comentado en la documentación. Teóricamente podría ser un procedimiento implementado muy efectivo para "dejar atrás" y no reevaluar el operando izquierdo, pero ¿es eficiente?

'Eficiente' sí para todos los ejemplos hasta ahora construidos en las respuestas de este hilo. ¿Pero eficiente en el caso de las propiedades e indexadores que usan get y set accessors? 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 intente establecer su valor, no hará nada. Por lo tanto, siempre que se evalúe 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 lo que imprime? Se imprime Unexpected!!. Resulta que, de hecho, se llama al descriptor de acceso, que no hace nada. Pero a partir de entonces, el get getor nunca se llama en absoluto. La asignación simplemente deja atrás el falsevalor que intentamos asignar a nuestra propiedad. Y este falsevalor es lo que evalúa la instrucción if.

Terminaré con un ejemplo del mundo real que me hizo investigar este problema. Hice un indexador que era un contenedor conveniente para una colección ( List<string>) 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 get getor simplemente devolvería verdadero o falso si ese valor existiera en la lista o no. Por lo tanto, el get getor era otra forma de usar el List<T>.Containsmétodo.

Si se llama al conjunto de acceso del indexador con una cadena como argumento y el operando correcto es un bool true, agregaría ese parámetro a la lista. Pero si el mismo parámetro se envió al descriptor de acceso y el operando correcto era un bool false, en su lugar eliminaría el elemento de la lista. Por lo tanto, el conjunto de acceso se utilizó como una alternativa conveniente para ambos List<T>.Addy List<T>.Remove.

Pensé que tenía una "API" ordenada y compacta que envolvía la lista con mi propia lógica implementada como puerta de enlace. Con la ayuda de un indexador solo, podría hacer muchas cosas con unas pocas pulsaciones de teclas. Por ejemplo, ¿cómo puedo intentar agregar un valor a mi lista y verificar que esté allí? Pensé que esta era la única línea de código necesaria:

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

Pero como mostró mi ejemplo anterior, ni siquiera se llamó al get getor que se supone que ve si el valor realmente está en la lista. El truevalor siempre se dejó atrás destruyendo efectivamente cualquier lógica que haya implementado en mi get accessor.

Martin Andersson
fuente
Muy interesante respuesta. Y me ha hecho pensar positivamente sobre el desarrollo basado en pruebas.
Elliot
1
Si bien esto es interesante, es un comentario sobre la respuesta de Eric, no una respuesta a la pregunta del OP.
Jim Balter
No esperaría que un intento de expresión de asignación obtenga primero el valor de la propiedad de destino. Si la variable es mutable y los tipos coinciden, ¿por qué importaría cuál era el valor anterior? Simplemente establezca el nuevo valor. Sin embargo, no soy fanático de la asignación en las pruebas IF. ¿Devuelve verdadero porque la asignación fue exitosa, o tiene algún valor, por ejemplo, un entero positivo que se convierte en verdadero, o porque está asignando un valor booleano? Independientemente de la implementación, la lógica es ambigua.
Davos
6

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

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

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

sepp2k
fuente
Las desventajas mencionadas por nadie: llegué aquí preguntándome por qué las tareas no podían ser nulas, para que la asignación común versus el error de comparación 'if (something = 1) {}' no se compilara en lugar de devolver siempre verdadero. Ahora veo que eso crearía ... ¡otros problemas!
Jon
1
@Jon ese error podría prevenirse fácilmente rechazando o advirtiendo que =ocurra en una expresión que no está entre paréntesis (sin contar los parentes de la declaración if / while). gcc da tales advertencias y, por lo tanto, esencialmente ha eliminado esta clase de errores de los programas C / C ++ que se compilan con él. Es una pena que otros escritores de compiladores hayan prestado tan poca atención a esta y otras buenas ideas en gcc.
Jim Balter
4

Creo que está malentendido cómo el analizador interpretará esa sintaxis. La asignación se evaluará primero y el resultado se comparará con NULL, es decir, la declaración 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 ...

Dan J
fuente
Si bien tiene razón desde el punto de vista práctico de que sus dos líneas son equivalentes, su afirmación de que la asignación no devuelve un valor no es técnicamente verdadera. Tengo entendido que el resultado de una asignación es un valor (según la especificación de C #), y en ese sentido no es diferente de decir el operador de suma en una expresión como 1 + 2 + 3
Steve Ellinger
3

Creo que la razón principal es la similitud (intencional) con C ++ y C. Hacer que el operador de asignación (y muchas otras construcciones de lenguaje) se comporten como sus contrapartes de C ++ solo sigue el principio de la menor sorpresa, y cualquier programador que venga de otro rizado. el lenguaje de soporte puede usarlos sin tener que pensar mucho. Ser fácil de aprender para los programadores de C ++ fue uno de los principales objetivos de diseño para C #.

tdammers
fuente
2

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

JamesMLV
fuente
1

El hecho de que 'a ++' o 'printf ("foo")' pueda ser útil como una declaración autónoma 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, existe una noción general de que las expresiones que podrían 'devolver' un valor de manera útil también podrían 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. Es probable que sea mejor evitar estos usos.

Super gato
fuente
1

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
jazzcat
fuente
0

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

Ahora x = y = b = c = 2 + 3significa algo diferente en aritmética que un lenguaje de estilo C; en la aritmética de su afirmación, que afirmar que x es igual ay, etc., y en un lenguaje de estilo C Es una instrucción que las marcas x igual a y etc. después de que se ejecute.

Dicho esto, todavía hay suficiente relación entre la aritmética y el código que no tiene sentido rechazar 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).

Jon Hanna
fuente
@JimBalter Me pregunto si, dentro de cinco años, podrías argumentar en contra.
Jon Hanna
@JimBalter no, tu segundo comentario es en realidad un argumento contrario, y uno sólido. Mi respuesta de pensamiento fue porque su primer comentario no fue. Sin embargo, cuando el aprendizaje de la aritmética aprendemos a = b = cmedios que ae by cson la misma cosa. Cuando aprendemos un lenguaje de estilo C, aprendemos eso después a = b = c, ay bsomos lo mismo que c. Ciertamente, hay una diferencia en la semántica, como dice mi propia respuesta, pero aun así, cuando era niño, aprendí a programar en un idioma que sí utilizaba =para la asignación pero no permitía que a = b = cesto me pareciera irrazonable ...
Jon Hanna
... inevitable porque ese lenguaje también se usaba =para la comparación de igualdad, por lo que a = b = ctendría que significar lo que a = b == csignifica en los lenguajes de estilo C. Encontré que el encadenamiento permitido en C era mucho más intuitivo porque podía dibujar una analogía con la aritmética.
Jon Hanna
0

Me gusta usar el valor de retorno de la asignación cuando necesito actualizar un montón de cosas y devolver si hubo cambios o no:

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

Pero ten cuidado. Puede pensar que puede acortarlo a esto:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

Pero esto en realidad dejará de evaluar las declaraciones o después de que encuentre la primera verdad. En este caso, eso significa que deja de asignar valores posteriores una vez que asigna el primer valor que es diferente.

Ver https://dotnetfiddle.net/e05Rh8 para jugar con esto

Luke Kubat
fuente