¿Por qué se pasan tantos idiomas por valor?

36

Incluso los lenguajes donde tiene una manipulación explícita del puntero como C siempre se pasa por valor ( puede pasarlos por referencia, pero ese no es el comportamiento predeterminado).

¿Cuál es el beneficio de esto? ¿Por qué se pasan tantos idiomas por valores y por qué otros se pasan por referencia ? (Entiendo que Haskell se pasa por referencia, aunque no estoy seguro).

Mi código no tiene errores
fuente
55
void acceptEntireProgrammingLanguageByValue(C++);
Thomas Eding
44
Podría ser peor. Algunos idiomas antiguos también permitían llamadas por nombre
hugomg
25
En realidad, en C no puedes pasar por referencia. Puede pasar un puntero por valor , que es muy similar a pasar por referencia, pero no es lo mismo. Sin embargo, en C ++, puede pasar por referencia.
Mason Wheeler
@Mason Wheeler puede elaborar más o añadir algún tipo de relación, beacuase su estado de cuenta no está claro para mí, ya que no soy C / C ++ gurú, sólo un programador regular, gracias
Betlista
1
@Betlista: con el pase por referencia, puede escribir una rutina de intercambio que se vea así: temp := a; a := b; b := temp;y cuando regrese, los valores de ay bserán intercambiados. No hay forma de hacer eso en C; tienes que pasar punteros a ay by la swaprutina debe actuar sobre los valores a los que apuntan.
Mason Wheeler el

Respuestas:

58

Pasar por valor es a menudo más seguro que pasar por referencia, porque no puede modificar accidentalmente los parámetros a su método / función. Esto hace que el lenguaje sea más simple de usar, ya que no tiene que preocuparse por las variables que le da a una función. Sabes que no se cambiarán, y esto es a menudo lo que esperas .

Sin embargo, si desea modificar los parámetros, necesita tener alguna operación explícita para aclarar esto (pasar un puntero). Esto obligará a todas las personas que llaman a hacer la llamada de manera ligeramente diferente ( &variable, en C) y esto hace explícito que el parámetro variable puede cambiarse.

Entonces, ahora puede suponer que una función no cambiará su parámetro variable, a menos que esté marcada explícitamente para hacerlo (al requerirle que pase un puntero). Esta es una solución más segura y limpia que la alternativa: suponga que todo puede cambiar sus parámetros, a menos que ellos digan específicamente que no pueden.

Oleksi
fuente
44
Las matrices +1 que se descomponen en punteros presentan una notable excepción, pero la explicación general es buena.
dasblinkenlight
66
@dasblinkenlight: las matrices son un punto doloroso en C :(
Matthieu M.
55

La llamada por valor y la llamada por referencia son técnicas de implementación que se confundieron con los modos de paso de parámetros hace mucho tiempo.

Al principio, estaba FORTRAN. FORTRAN solo tenía llamada por referencia, ya que las subrutinas tenían que poder modificar sus parámetros, y los ciclos de computación eran demasiado caros para permitir múltiples modos de paso de parámetros, además no se sabía lo suficiente sobre la programación cuando se definió por primera vez FORTRAN.

A ALGOL se le ocurrió llamar por nombre y llamar por valor. La llamada por valor era para cosas que no debían cambiarse (parámetros de entrada). La llamada por nombre era para los parámetros de salida. Call-by-name resultó ser un gran problema, y ​​ALGOL 68 lo dejó caer.

PASCAL proporcionó llamada por valor y llamada por referencia. No proporcionó ninguna forma para que el programador le dijera al compilador que estaba pasando un objeto grande (generalmente una matriz) por referencia, para evitar volar la pila de parámetros, pero que el objeto no debería cambiarse.

PASCAL agregó punteros al léxico del diseño del lenguaje.

C proporcionó una llamada por valor y una llamada por referencia simulada definiendo un operador kludge para devolver un puntero a un objeto arbitrario en la memoria.

Los lenguajes posteriores copiaron C, principalmente porque los diseñadores nunca habían visto nada más. Esta es probablemente la razón por la cual la llamada por valor es tan popular.

C ++ agregó un kludge encima del C kludge para proporcionar una llamada por referencia.

Ahora, como resultado directo de call-by-value vs. call-by-reference vs. call-by-pointer-kludge, C y C ++ (programadores) tienen dolores de cabeza horribles con punteros constantes y punteros const (solo lectura) objetos.

Ada logró evitar toda esta pesadilla.

Ada no tiene una llamada explícita por valor frente a una llamada por referencia. Por el contrario, Ada tiene parámetros de entrada (que pueden leerse pero no escritos), parámetros de salida (que DEBEN escribirse antes de que puedan leerse) y parámetros de salida, que pueden leerse y escribirse en cualquier orden. El compilador decide si un parámetro en particular se pasa por valor o por referencia: es transparente para el programador.

John R. Strohm
fuente
1
+1. Esta es la única respuesta que he visto aquí hasta ahora que en realidad responde a la pregunta y tiene sentido y no la usa como una excusa transparente para un discurso pro-FP.
Mason Wheeler
11
+1 para "Los lenguajes posteriores copiaron C, principalmente porque los diseñadores nunca habían visto nada más". Cada vez que veo un nuevo lenguaje con constantes octales prefijadas con 0, muero un poco por dentro.
librik
44
Esta podría ser la primera frase de la Biblia de la programación: In the beginning, there was FORTRAN.
1
Con respecto a ADA, si una variable global se pasa a un parámetro de entrada / salida, ¿impedirá el lenguaje que una llamada anidada la examine o modifique? Si no, pensaría que habría una clara diferencia entre los parámetros de entrada / salida y de referencia. Personalmente, me gustaría ver que los idiomas admitan explícitamente todas las combinaciones de "entrada / salida / entrada + salida" y "por valor / por referencia / no importa", para que los programadores puedan ofrecer la máxima claridad en cuanto a su intención pero optimizadores tendría la máxima flexibilidad en la implementación [siempre y cuando concuerde con la intención del programador].
supercat
2
@Neikos También existe el hecho de que el 0prefijo es inconsistente con cualquier otro prefijo no base diez. Eso puede ser algo excusable en C (y en su mayoría lenguajes compatibles con la fuente, por ejemplo, C ++, Objective C), si octal era la única base alternativa cuando se introdujo, pero cuando los lenguajes más modernos usan ambos 0xy 0desde el principio, solo los hace verse mal pensado
8bittree
13

El pase por referencia permite efectos secundarios no intencionales muy sutiles que son muy difíciles de rastrear cuando comienzan a causar el comportamiento no deseado.

El paso por valor, especialmente final, statico constlos parámetros de entrada hacen que toda esta clase de errores desaparezcan.

Los lenguajes inmutables son aún más deterministas y más fáciles de razonar y comprender qué está entrando y qué se espera que salga de una función.


fuente
77
Es una de esas cosas que descubres después de intentar depurar el código VB 'antiguo' donde todo es por referencia como predeterminado.
jfrankcarr
Tal vez es solo que mi experiencia es diferente, pero no tengo idea de qué tipo de "efectos secundarios involuntarios muy sutiles que son muy difíciles de rastrear" están hablando. Estoy acostumbrado a Pascal, donde el valor por defecto es el predeterminado, pero la referencia puede usarse marcando explícitamente el parámetro (básicamente el mismo modelo que C #) y nunca tuve problemas que me causen problemas. Puedo ver cómo sería problemático en "VB antiguo" donde by-ref es el valor predeterminado, pero cuando by-ref es opt-in, te hace pensar en ello mientras lo estás escribiendo.
Mason Wheeler
44
@MasonWheeler, ¿qué pasa con los novatos que vienen detrás de ti y simplemente copian lo que hiciste sin entenderlo y comienzan a manipular ese estado indirectamente? El mundo real sucede todo el tiempo. Menos indirección es algo bueno. Inmutable es lo mejor.
1
@MasonWheeler Los ejemplos de alias simples, como los Swaphacks que no usan una variable temporal, aunque probablemente no sean un problema en sí mismos, muestran lo difícil que puede ser depurar un ejemplo más complejo.
Mark Hurd
1
@MasonWheeler: El ejemplo clásico en FORTRAN, que llevó al dicho "Las variables no lo harán; las constantes no lo son", pasaría una constante de coma flotante como 3.0 a una función que modifica el parámetro pasado. Para cualquier constante de punto flotante que se pasó a una función, el sistema crearía una variable "oculta" que se inicializó con el valor adecuado y podría pasar a las funciones. Si la función agregó 1.0 a su parámetro, un subconjunto arbitrario de 3.0 en el programa podría convertirse de repente en 4.0.
supercat
8

¿Por qué se pasan tantos idiomas por valor?

El punto de dividir los programas grandes en pequeñas subrutinas es que puede razonar sobre las subrutinas de forma independiente. El paso por referencia rompe esta propiedad. (Al igual que el estado mutable compartido).

Incluso los lenguajes donde tiene una manipulación explícita del puntero como C siempre se pasa por valor ( puede pasarlos por referencia, pero ese no es el comportamiento predeterminado).

En realidad, C siempre es paso por valor, nunca paso por referencia. Puede tomar una dirección de algo y pasar esa dirección, pero la dirección aún se pasará por valor.

¿Cuál es el beneficio de esto? ¿Por qué se pasan tantos idiomas por valores y por qué otros se pasan por referencia ?

Hay dos razones principales para usar el paso por referencia:

  1. simulando múltiples valores de retorno
  2. eficiencia

Personalmente, creo que el n. ° 1 es falso: casi siempre es una excusa para una mala API y / o diseño de lenguaje:

  1. Si necesita múltiples valores de retorno, no los simule, solo use un lenguaje que los admita.
  2. También puede simular múltiples valores de retorno al empaquetarlos en una estructura de datos liviana, como una tupla. Esto funciona particularmente bien si el lenguaje admite la coincidencia de patrones o el enlace de desestructuración. Ej. Ruby:

    def foo
      # This is actually just a single return value, an array: [1, 2, 3]
      return 1, 2, 3
    end
    
    # Ruby supports destructuring bind for arrays: a, b, c = [1, 2, 3]
    one, two, three = foo
    
  3. A menudo, ni siquiera necesita múltiples valores de retorno. Por ejemplo, un patrón popular es que la subrutina devuelve un código de error y el resultado real se vuelve a escribir por referencia. En su lugar, solo debe lanzar una excepción si el error es inesperado o devolver un Either<Exception, T>si se espera el error. Otro patrón es devolver un valor booleano que indica si la operación fue exitosa y devolver el resultado real por referencia. Nuevamente, si la falla es inesperada, debe lanzar una excepción, si se espera la falla, por ejemplo, al buscar un valor en un diccionario, debe devolver un Maybe<T>lugar.

El paso por referencia puede ser más eficiente que el paso por valor, ya que no tiene que copiar el valor.

(Entiendo que Haskell se pasa por referencia, aunque no estoy seguro).

No, Haskell no es de referencia. Tampoco es paso por valor. Pasar por referencia y pasar por valor son estrategias de evaluación estrictas, pero Haskell no es estricto.

De hecho, la Especificación de Haskell no especifica ninguna estrategia de evaluación en particular. La mayoría de las implementaciones de Hakell usan una combinación de llamada por nombre y llamada por necesidad (una variante de llamada por nombre con memorización), pero el estándar no exige esto.

Tenga en cuenta que incluso para lenguajes estrictos, no tiene sentido distinguir entre pasar por referencia o pasar por valor para lenguajes funcionales, ya que la diferencia entre ellos solo se puede observar si muta la referencia. Por lo tanto, el implementador es libre de elegir entre los dos, sin romper la semántica del lenguaje.

Jörg W Mittag
fuente
99
"Si necesita múltiples valores de retorno, no los simule, solo use un lenguaje que los admita". Esto es un poco extraño de decir. Básicamente está diciendo "si su lenguaje puede hacer todo lo que necesita, pero una característica que es probable que tengas en menos del 1% de su código - pero todavía se va a necesitar para ese 1% - no puede ser hecho en de una manera particularmente limpia, entonces tu idioma no es lo suficientemente bueno para tu proyecto y debes reescribir todo en otro idioma ". Lo siento, pero eso es simplemente ridículo.
Mason Wheeler
+1 Estoy totalmente de acuerdo con este punto. El punto de dividir programas grandes en pequeñas subrutinas es que puedes razonar sobre las subrutinas de forma independiente. El paso por referencia rompe esta propiedad. (Como no compartía estado mutable.)
Rémi
3

Hay un comportamiento diferente según el modelo de llamada del lenguaje, el tipo de argumentos y los modelos de memoria del lenguaje.

Con tipos nativos simples, pasar por valor le permite pasar el valor por los registros. Eso puede ser muy rápido ya que el valor no necesita cargarse desde la memoria ni guardarse de nuevo. Una optimización similar también es posible simplemente reutilizando la memoria utilizada por el argumento una vez que la persona que llama ha terminado con él, sin arriesgarse a meterse con la copia del objeto que llama. Si el argumento fuera un objeto temporal, probablemente habría guardado una copia completa al hacerlo (C ++ 11 hace que este tipo de optimización sea aún más obvio con la nueva referencia a la derecha y su movimiento semántico).

En muchos lenguajes OO (C ++ es más una excepción en este contexto), no puede pasar un objeto por valor. Estás obligado a pasarlo por referencia. Esto hace que el código sea polimórfico por defecto y está más en línea con la noción de instancias propias de OO. Además, si desea pasar por valor, debe hacer una copia usted mismo, reconociendo el costo de rendimiento que generó dicha acción. En este caso, el idioma elige para usted el enfoque que es más probable que le brinde el mejor rendimiento.

En el caso de los lenguajes funcionales, supongo que pasar por valor o referencia es simplemente una cuestión de optimización. Dado que las funciones en dicho lenguaje son puras y están libres de efectos secundarios, realmente no hay razón para copiar el valor, excepto la velocidad. Incluso estoy bastante seguro de que dicho lenguaje a menudo comparte la misma copia de objetos con los mismos valores, una posibilidad disponible solo si usó una semántica de referencia (const). Python también usó este truco para enteros y cadenas comunes (como métodos y nombres de clase), explicando por qué los enteros y las cadenas son objetos constantes en Python. Esto también ayuda a optimizar el código nuevamente, permitiendo, por ejemplo, la comparación de punteros en lugar de la comparación de contenido, y haciendo una evaluación diferida de algunos datos internos.

Fabien Ninoles
fuente
2

Puedes pasar una expresión por valor, es natural. Pasar la expresión por referencia (temporal) es ... raro.

herby
fuente
1
Además, pasar la expresión por referencia temporal puede conducir a errores incorrectos (en un lenguaje con estado), cuando lo cambia felizmente (ya que es solo temporal), pero luego falla cuando realmente pasa una variable, y debe idear una solución fea como pasar foo +0 en lugar de foo.
herby
2

Si pasa por referencia, entonces siempre está trabajando efectivamente con valores globales, con todos los problemas de los globales (es decir, alcance y efectos secundarios no deseados).

Las referencias, al igual que los globales, a veces son beneficiosas, pero no deberían ser su primera opción.

jmoreno
fuente
3
¿Supongo que te refieres a pasar por referencia en la primera oración?
jk.