Perché tante lingue vengono passate per valore?


36

Anche i linguaggi in cui hai una manipolazione esplicita del puntatore come C viene sempre passato per valore ( puoi passarli per riferimento ma non è il comportamento predefinito).

Qual è il vantaggio di questo, perché tante lingue sono passate per valori e perché altre sono passate per riferimento ? (Capisco che Haskell è passato per riferimento anche se non ne sono sicuro).


5
void acceptEntireProgrammingLanguageByValue(C++);
Thomas Eding,

4
Potrebbe essere peggio. Alcune vecchie lingue consentivano anche di chiamare per nome
hugomg

25
In realtà, in C non puoi passare per riferimento. Puoi passare un puntatore per valore , che è molto simile al pass-by-reference, ma non è la stessa cosa. In C ++, tuttavia, puoi passare per riferimento.
Mason Wheeler,

@Mason Wheeler puoi approfondire o aggiungere qualche link, perché la tua affermazione non mi è chiara in quanto non sono un guru C / C ++, solo un programmatore regolare, grazie
Betlista,

1
@Betlista: con pass per riferimento, puoi scrivere una routine di scambio che assomigli a questa: temp := a; a := b; b := temp;E quando ritorna, i valori di ae bverranno scambiati. Non c'è modo di farlo in C; si deve passare puntatori a aed be la swaproutine deve agire sui valori a cui puntano.
Mason Wheeler,

Risposte:


58

Il passaggio per valore è spesso più sicuro del passaggio per riferimento, poiché non è possibile modificare accidentalmente i parametri nel metodo / funzione. Questo rende il linguaggio più semplice da usare, poiché non devi preoccuparti delle variabili che dai a una funzione. Sai che non saranno cambiati, e questo è spesso quello che ti aspetti .

Tuttavia, se si desidera modificare i parametri, è necessario disporre di alcune operazioni esplicite per chiarire questo aspetto (passare un puntatore). Ciò costringerà tutti i chiamanti a rendere la chiamata in modo leggermente diverso ( &variable, in C) e ciò rende esplicito che il parametro variabile può essere modificato.

Quindi ora puoi presumere che una funzione non cambierà il tuo parametro variabile, a meno che non sia esplicitamente contrassegnato per farlo (richiedendo di passare un puntatore). Questa è una soluzione più sicura e più pulita dell'alternativa: supponiamo che tutto possa cambiare i tuoi parametri, a meno che non lo affermino specificamente.


4
+1 Le matrici in decomposizione verso i puntatori rappresentano una notevole eccezione, ma la spiegazione generale è buona.
dasblinkenlight,

6
@dasblinkenlight: le matrici sono un punto dolente in C :(
Matthieu M.

55

Call-by-value e call-by-reference sono tecniche di implementazione che sono state confuse molto tempo fa con le modalità di passaggio dei parametri.

All'inizio c'era FORTRAN. FORTRAN aveva solo una chiamata per riferimento, poiché le subroutine dovevano essere in grado di modificare i loro parametri e i cicli di calcolo erano troppo costosi per consentire più modalità di passaggio dei parametri, inoltre non si sapeva abbastanza sulla programmazione quando FORTRAN fu definito per la prima volta.

ALGOL ha inventato il call-by-name e il call-by-value. La chiamata per valore era per cose che non dovevano essere modificate (parametri di input). La chiamata per nome era per i parametri di output. Il call-by-name si rivelò essere un grande idiota e ALGOL 68 lo lasciò cadere.

PASCAL ha fornito call-by-value e call-by-reference. Non ha fornito al programmatore alcun modo per dire al compilatore che stava passando un oggetto di grandi dimensioni (di solito un array) per riferimento, per evitare di far saltare lo stack dei parametri, ma che l'oggetto non doveva essere modificato.

PASCAL ha aggiunto degli indicatori al lessico del design linguistico.

C ha fornito una chiamata per valore e ha simulato una chiamata per riferimento definendo un operatore kludge per restituire un puntatore a un oggetto arbitrario in memoria.

Le lingue successive copiarono C, soprattutto perché i progettisti non avevano mai visto nient'altro. Questo è probabilmente il motivo per cui la chiamata per valore è così popolare.

C ++ ha aggiunto un kludge in cima a C kludge per fornire chiamate per riferimento.

Ora, come risultato diretto di call-by-value vs. call-by-reference vs. call-by-pointer-kludge, C e C ++ (programmatori) hanno orribili mal di testa con puntatori const e puntatori a const (sola lettura) oggetti.

Ada è riuscita a evitare questo intero incubo.

Ada non ha esplicito call-by-value vs. call-by-reference. Piuttosto, Ada ha parametri (che possono essere letti ma non scritti), parametri out (che DEVONO essere scritti prima di poter essere letti) e parametri out, che possono essere letti e scritti in qualsiasi ordine. Il compilatore decide se un determinato parametro viene passato per valore o per riferimento: è trasparente per il programmatore.


1
+1. Questa è l'unica risposta che ho visto qui finora che in realtà risponde alla domanda e ha un senso e non la usa come una scusa trasparente per un rant pro-FP.
Mason Wheeler,

11
+1 per "Le lingue successive hanno copiato C, soprattutto perché i designer non avevano mai visto nient'altro". Ogni volta che vedo una nuova lingua con costanti ottali con prefisso 0 muoio un po 'dentro.
librik,

4
Questa potrebbe essere la prima frase della Bibbia di programmazione: In the beginning, there was FORTRAN.

1
Per quanto riguarda ADA, se una variabile globale viene passata a un parametro in / out, la lingua impedirà a una chiamata nidificata di esaminarla o modificarla? Altrimenti, penso che ci sarebbe una chiara differenza tra i parametri in / out e ref. Personalmente, mi piacerebbe vedere le lingue supportare esplicitamente tutte le combinazioni di "in / out / in + out" e "per valore / per ref / non-care", così i programmatori potrebbero offrire la massima chiarezza sulla loro intenzione ma ottimizzatori avrebbe la massima flessibilità nell'attuazione [purché distorta con l'intenzione del programmatore].
supercat

2
@Neikos C'è anche il fatto che il 0prefisso non è coerente con tutti gli altri dieci prefissi non di base. Ciò può essere alquanto scusabile in C (e soprattutto in linguaggi compatibili con la fonte, ad esempio C ++, Obiettivo C), se l'ottale era l' unica base alternativa quando introdotta, ma quando i linguaggi più moderni usano entrambi 0xe 0dall'inizio, li fa apparire male pensato.
settembre

13

Il passaggio per riferimento consente effetti collaterali non intenzionali molto difficili, difficili da rintracciare quando iniziano a causare comportamenti indesiderati.

Passa per valore, in particolare final, staticoppure i constparametri di input fanno scomparire l'intera classe di bug.

Le lingue immutabili sono ancora più deterministiche e più facili da ragionare e capire cosa sta succedendo e cosa ci si aspetta che esca da una funzione.


7
È una di quelle cose che ti viene in mente dopo aver tentato di eseguire il debug del codice VB "antico" in cui tutto è di default come riferimento.
jfrankcarr,

Forse è solo che il mio background è diverso, ma non ho idea di che tipo di "effetti collaterali involontari molto sottili che sono molto difficili da raggiungere quasi impossibili da rintracciare" di cui stai parlando. Sono abituato a Pascal, dove per valore è l'impostazione predefinita, ma per riferimento può essere utilizzato contrassegnando esplicitamente il parametro (fondamentalmente lo stesso modello di C #) e non ho mai avuto problemi di questo tipo. Posso vedere come sarebbe problematico in "Ancient VB" dove by-ref è l'impostazione predefinita, ma quando by-ref è opt-in, ti fa riflettere mentre lo stai scrivendo.
Mason Wheeler,

4
@MasonWheeler che ne pensi dei principianti che ti arrivano dietro e copia quello che hai fatto senza capirlo e inizia a manipolare indirettamente quello stato dappertutto, il mondo reale accade sempre. Meno indirette è una buona cosa. Immutabile è il migliore.

1
@MasonWheeler I semplici esempi di alias, come gli Swaphack che non usano una variabile temporanea, sebbene non possano costituire un problema, mostrano quanto sia difficile eseguire il debug di un esempio più complesso.
Mark Hurd,

1
@MasonWheeler: il classico esempio in FORTRAN, che ha portato al detto "Variabili no; le costanti non lo sono", passerebbe una costante a virgola mobile come 3.0 a una funzione che modifica il parametro passato. Per qualsiasi costante a virgola mobile che è stata passata a una funzione, il sistema creerebbe una variabile "nascosta" inizializzata con il valore corretto e che potrebbe essere passata a funzioni. Se la funzione aggiungesse 1.0 al suo parametro, un sottoinsieme arbitrario di 3.0 nel programma potrebbe improvvisamente diventare 4.0.
supercat

8

Perché tante lingue vengono passate per valore?

Il punto di suddividere grandi programmi in piccole subroutine è che puoi ragionare in modo indipendente sulle subroutine. Passa per riferimento rompe questa proprietà. (Come lo stato mutabile condiviso.)

Anche i linguaggi in cui hai una manipolazione esplicita del puntatore come C viene sempre passato per valore ( puoi passarli per riferimento ma non è il comportamento predefinito).

In realtà, C è sempre pass-by-value, mai pass-by-reference. Puoi prendere un indirizzo di qualcosa e passare quell'indirizzo, ma l'indirizzo verrà comunque passato per valore.

Qual è il vantaggio di questo, perché tante lingue sono passate per valori e perché altre sono passate per riferimento ?

Esistono due motivi principali per l'utilizzo del riferimento pass-by:

  1. simulando più valori di ritorno
  2. efficienza

Personalmente, penso che il n. 1 sia falso: è quasi sempre una scusa per cattive API e / o design del linguaggio:

  1. Se hai bisogno di più valori di ritorno, non simularli, usa solo una lingua che li supporti.
  2. Puoi anche simulare più valori di ritorno impacchettandoli in una struttura dati leggera come una tupla. Funziona particolarmente bene se il linguaggio supporta il pattern matching o il binding destrutturante. Ad esempio 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. Spesso, non hai nemmeno bisogno di più valori di ritorno. Ad esempio, un modello comune è che la subroutine restituisce un codice di errore e il risultato effettivo viene riscritto per riferimento. Invece, dovresti semplicemente lanciare un'eccezione se l'errore è imprevisto o restituire un Either<Exception, T>errore se previsto. Un altro modello è quello di restituire un valore booleano che indica se l'operazione ha avuto esito positivo e restituire il risultato effettivo come riferimento. Ancora una volta, se l'errore è inatteso, è consigliabile generare un'eccezione, se è previsto l'errore, ad esempio quando si cerca un valore in un dizionario, è necessario restituire Maybe<T>invece un .

Il pass-by-reference può essere più efficiente del pass-by-value, perché non è necessario copiare il valore.

(Capisco che Haskell è passato per riferimento anche se non ne sono sicuro).

No, Haskell non è un riferimento pass-by. Né è pass-by-value. Pass-by-reference e pass-by-value sono entrambe strategie di valutazione rigorose, ma Haskell non è rigorosa.

In effetti, la specifica Haskell non specifica alcuna particolare strategia di valutazione. La maggior parte delle implementazioni di Hakell usa una combinazione di chiamata per nome e chiamata per necessità (una variante di chiamata per nome con memoizzazione), ma lo standard non lo impone.

Si noti che anche per i linguaggi rigidi, non ha senso distinguere tra pass-by-reference o pass-by-value per i linguaggi funzionali poiché la differenza tra loro può essere osservata solo se si modifica il riferimento. Pertanto, l'implementatore è libero di scegliere tra i due, senza interrompere la semantica della lingua.


9
"Se hai bisogno di più valori di ritorno, non simularli, usa solo una lingua che li supporti." Questa è una cosa un po 'strana da dire. E 'fondamentalmente dicendo "se la lingua può fare tutto il necessario, ma una caratteristica che probabilmente avrete bisogno in meno dell'1% del codice - ma è ancora avrà bisogno per questo 1% - non può essere fatto in un modo particolarmente pulito, quindi la tua lingua non è abbastanza buona per il tuo progetto e dovresti riscrivere il tutto in un'altra lingua ". Mi dispiace, ma è semplicemente ridicolo.
Mason Wheeler,

+1 Concordo pienamente con questo punto Il punto di suddividere grandi programmi in piccole subroutine è che puoi ragionare in modo indipendente sulle subroutine. Passa per riferimento rompe questa proprietà. (Come lo stato mutevole condiviso.)
Rémi,

3

Esistono comportamenti diversi a seconda del modello chiamante della lingua, del tipo di argomenti e dei modelli di memoria della lingua.

Con semplici tipi nativi, il passaggio per valore consente di passare il valore dai registri. Questo può essere molto veloce poiché non è necessario caricare il valore dalla memoria né salvare. Una simile ottimizzazione è anche possibile semplicemente riutilizzando la memoria utilizzata dall'argomento una volta terminata la chiamata, senza rischiare di incasinare la copia del chiamante dell'oggetto. Se l'argomento fosse un oggetto temporaneo, probabilmente avresti salvato una copia completa in tal modo (C ++ 11 rende questo tipo di ottimizzazione ancora più evidente con il nuovo riferimento a destra e la sua semantica di spostamento).

In molti linguaggi OO (C ++ è più un'eccezione in questo contesto), non è possibile passare un oggetto per valore. Sei costretto a passarlo per riferimento. Questo rende il codice polimorfico per impostazione predefinita e sono più in linea con la nozione di istanze proprie di OO. Inoltre, se si desidera passare per valore, è necessario crearne una copia, riconoscendo il costo della prestazione che ha generato tale azione. In questo caso, la lingua sceglie per te l'approccio che è più probabile che ti dia le migliori prestazioni.

Nel caso di linguaggi funzionali, suppongo che passare per valore o riferimento sia semplicemente una questione di ottimizzazione. Poiché le funzioni in tale linguaggio sono pure e prive di effetti collaterali, non c'è davvero motivo di copiare il valore se non per la velocità. Sono anche abbastanza sicuro che un tale linguaggio spesso condividesse la stessa copia di oggetti con gli stessi valori, una possibilità disponibile solo se hai usato un semantico di riferimento passante (const). Python ha anche usato questo trucco per stringhe intere e comuni (come metodi e nomi di classe), spiegando perché interi e stringhe sono oggetti costanti in Python. Ciò aiuta anche a ottimizzare nuovamente il codice, consentendo ad esempio il confronto dei puntatori anziché il confronto dei contenuti e facendo una valutazione pigra di alcuni dati interni.


2

Puoi passare un'espressione per valore, è naturale. Passare l'espressione per riferimento (temporaneo) è ... strano.


1
Inoltre, passare l'espressione per riferimento temporaneo può portare a cattivi bug (in un linguaggio di stato), quando lo cambi felicemente (poiché è solo temporaneo), ma poi si ritorcerà contro quando passi effettivamente una variabile e devi escogitare una brutta soluzione come passare il foo +0 invece di foo.
lei,

2

Se passi per riferimento, allora stai effettivamente lavorando sempre con valori globali, con tutti i problemi dei globuli (ovvero portata ed effetti collaterali non intenzionali).

I riferimenti, proprio come i globali, a volte sono utili, ma non dovrebbero essere la tua prima scelta.


3
Presumo che intendevi passare per riferimento nella prima frase?
jk.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.