Perché gli oggetti vengono passati per riferimento?


31

Un giovane collaboratore che studiava OO mi ha chiesto perché ogni oggetto è passato per riferimento, che è l'opposto di tipi o strutture primitivi. È una caratteristica comune di linguaggi come Java e C #.

Non sono riuscito a trovare una buona risposta per lui.

Quali sono le motivazioni per questa decisione progettuale? Gli sviluppatori di queste lingue erano stanchi di dover creare puntatori e dattiloscritti ogni volta?


2
Stai chiedendo perché Java e C # hanno passato i parametri per riferimento anziché per valore o per riferimento anziché tramite puntatore?
Robert

@Robert, c'è un alto livello di differenza tra "riferimento anziché puntatore"? Pensi che dovrei cambiare il titolo in qualcosa del tipo "perché gli oggetti fanno sempre riferimento?"?
Gustavo Cardoso,

I riferimenti sono puntatori.
compman

2
@Anto: un riferimento Java è in ogni modo identico a un puntatore C usato correttamente (usato correttamente: non cast di tipo, non impostato su memoria non valida, non impostato da un valore letterale).
Zan Lynx,

5
Anche per essere veramente pedante, il titolo non è corretto (almeno per quanto riguarda .net). Gli oggetti NON vengono passati per riferimento, i riferimenti vengono passati per valore. Quando si passa un oggetto a un metodo, il valore di riferimento viene copiato in un nuovo riferimento all'interno del corpo del metodo. Penso che sia un peccato che "gli oggetti siano passati per riferimento" sia entrato nell'elenco delle citazioni dei programmatori comuni quando è errato e porta a una comprensione più scarsa dei riferimenti per i nuovi programmatori che iniziano.
SecretDeveloper

Risposte:


15

I motivi di base si riducono a questo:

  • I puntatori sono tecnici per funzionare correttamente
  • Sono necessari puntatori per implementare determinate strutture di dati
  • È necessario che i puntatori siano efficienti nell'uso della memoria
  • Non è necessario l'indicizzazione manuale della memoria per funzionare se non si utilizza direttamente l'hardware.

Quindi riferimenti.


32

Risposta semplice:

Ridurre al minimo il consumo di memoria
e il
tempo della CPU nel ricreare e fare una copia profonda di ogni oggetto passato da qualche parte.


Ti concordo, ma penso che ci sia anche qualche motivazione estetica o di design OO lungo questi.
Gustavo Cardoso,

9
@Gustavo Cardoso: "qualche motivazione estetica o di design OO". No. È semplicemente un'ottimizzazione.
S.Lott

6
@ S.Lott: No, ha senso passare in termini OO per riferimento perché, semanticamente, non si desidera creare copie di oggetti. Vuoi passare l'oggetto piuttosto che una sua copia. Se passi per valore, spezza un po 'la metafora OO perché hai tutti questi cloni di oggetti generati in tutto il luogo che non hanno senso a un livello superiore.
intuito l'

@Gustavo: penso che stiamo discutendo lo stesso punto. Citi la semantica di OOP e fai riferimento alla metafora di OOP come ragioni aggiuntive alla mia. Mi sembra che i creatori di OOP siano riusciti a "ridurre al minimo il consumo di memoria" e a "risparmiare tempo CPU"
Tim

14

In C ++, hai due opzioni principali: return by value o return by pointer. Diamo un'occhiata al primo:

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

Supponendo che il tuo compilatore non sia abbastanza intelligente da utilizzare l'ottimizzazione del valore di ritorno, quello che succede qui è questo:

  • newObj è costruito come oggetto temporaneo e posizionato nello stack locale.
  • Viene creata e restituita una copia di newObj.

Abbiamo fatto una copia dell'oggetto inutilmente. Questo è uno spreco di tempo di elaborazione.

Diamo invece un'occhiata a return-by-pointer:

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

Abbiamo eliminato la copia ridondante, ma ora abbiamo introdotto un altro problema: abbiamo creato un oggetto sull'heap che non verrà automaticamente distrutto. Dobbiamo occuparcene da soli:

MyClass someObj = getNewObject();
delete someObj;

Sapere chi è responsabile dell'eliminazione di un oggetto allocato in questo modo è qualcosa che può essere comunicato solo tramite commenti o convenzioni. Porta facilmente a perdite di memoria.

Sono state suggerite molte soluzioni alternative per risolvere questi due problemi: ottimizzazione del valore di ritorno (in cui il compilatore è abbastanza intelligente da non creare la copia ridondante in valore di ritorno), passando un riferimento al metodo (quindi la funzione inserisce in un oggetto esistente anziché crearne uno nuovo), puntatori intelligenti (in modo che la questione della proprietà sia controversa).

I creatori di Java / C # hanno capito che restituire sempre l'oggetto per riferimento era una soluzione migliore, specialmente se il linguaggio lo supportava in modo nativo. Si collega a molte altre funzionalità delle lingue, come la raccolta dei rifiuti, ecc.


Il ritorno per valore è abbastanza negativo, ma il pass-per-valore è ancora peggio quando si tratta di oggetti, e penso che sia stato il vero problema che stavano cercando di evitare.
Mason Wheeler,

di sicuro hai un punto valido. Ma il problema di progettazione OO che @Mason ha indicato è stata la motivazione finale del cambiamento. Non c'era alcun significato per mantenere la differenza tra riferimento e valore quando si desidera semplicemente utilizzare il riferimento.
Gustavo Cardoso,

10

Molte altre risposte hanno buone informazioni. Vorrei aggiungere un punto importante sulla clonazione che è stato affrontato solo parzialmente.

L'uso dei riferimenti è intelligente. Copiare le cose è pericoloso.

Come altri hanno già detto, in Java non esiste un "clone" naturale. Questa non è solo una caratteristica mancante. Non vuoi mai copiare solo volenti o nolenti (sia superficiali che profondi) ogni proprietà in un oggetto. E se quella proprietà fosse una connessione al database? Non puoi semplicemente "clonare" una connessione al database più di quanto tu possa clonare un essere umano. L'inizializzazione esiste per un motivo.

Le copie in profondità sono un problema tutto loro - quanto in profondità vai davvero ? Non puoi assolutamente copiare nulla di statico (inclusi Classoggetti).

Quindi, per lo stesso motivo per cui non esiste un clone naturale, gli oggetti che vengono passati come copie creerebbero follia . Anche se potessi "clonare" una connessione DB, come assicureresti ora che sia chiusa?


* Vedi i commenti - Con questa affermazione "mai" intendo un clone automatico che clona ogni proprietà. Java non ne ha fornito uno, e probabilmente non è una buona idea per te come utente della lingua crearne uno tuo, per i motivi elencati qui. La clonazione di soli campi non transitori sarebbe un inizio, ma anche in questo caso dovresti essere diligente nel definire transientdove appropriato.


Ho difficoltà a capire il salto dalle buone obiezioni alla clonazione in determinate condizioni all'affermazione che non è mai necessario. E ho incontrato situazioni in cui c'era bisogno di un duplicato esatto, dove non funzioni statiche dove coinvolti, senza IO o connessioni aperte potrebbero essere a tema ... ho capito i rischi di clonazione, ma non riesco a vedere la coperta mai .
Inca,

2
@Inca - Potresti fraintendermi. Implementato intenzionalmente cloneva bene. Con "volenti o nolenti" intendo copiare tutte le proprietà senza pensarci - senza intenzioni intenzionali. I progettisti del linguaggio Java hanno forzato questo intento richiedendo l'implementazione creata dall'utente clone.
Nicole,

L'utilizzo di riferimenti a oggetti immutabili è intelligente. Rendere mutabili valori semplici come Date e quindi non creare riferimenti multipli ad essi.
Kevin Cline,

@NickC: Il motivo principale per cui "clonare cose volenti o nolenti" è pericoloso è che i linguaggi / i framework come Java e .net non hanno alcun mezzo per indicare in modo dichiarativo se un riferimento incapsula lo stato mutevole, l'identità, entrambi o nessuno dei due. Se il campo contiene un riferimento all'oggetto che incapsula lo stato mutabile ma non l'identità, la clonazione dell'oggetto richiede che l'oggetto che contiene lo stato sia duplicato e un riferimento a quel duplicato archiviato nel campo. Se il riferimento incapsula lo stato di identità ma non è modificabile, il campo nella copia deve fare riferimento allo stesso oggetto dell'originale.
supercat

L'aspetto della copia profonda è un punto importante. La copia di oggetti è problematica quando contengono riferimenti ad altri oggetti, in particolare se il grafico degli oggetti contiene oggetti mutabili.
Caleb,

4

Gli oggetti sono sempre referenziati in Java. Non sono mai passati intorno a se stessi.

Un vantaggio è che questo semplifica la lingua. Un oggetto C ++ può essere rappresentato come valore o come riferimento, creando la necessità di utilizzare due diversi operatori per accedere a un membro: .e ->. (Ci sono ragioni per cui questo non può essere consolidato; ad esempio, i puntatori intelligenti sono valori che sono riferimenti e devono mantenerli distinti.) Java ha solo bisogno ..

Un'altra ragione è che il polimorfismo deve essere fatto per riferimento, non per valore; un oggetto trattato dal valore è proprio lì e ha un tipo fisso. È possibile rovinare tutto in C ++.

Inoltre, Java può cambiare l'assegnazione / copia predefinita / qualunque cosa. In C ++, è una copia più o meno approfondita, mentre in Java è una semplice assegnazione / copia del puntatore / qualunque cosa, con .clone()e simili nel caso sia necessario copiarla.


A volte diventa davvero brutto quando usi '(* oggetto) ->'
Gustavo Cardoso

1
Vale la pena notare che C ++ distingue tra puntatori, riferimenti e valori. SomeClass * è un puntatore a un oggetto. SomeClass & è un riferimento a un oggetto. SomeClass è un tipo di valore.
Ant

Ho già chiesto a @Rober sulla domanda iniziale, ma lo farò anche qui: la differenza tra * e & su C ++ è solo una cosa tecnica di basso livello, no? Sono, ad alto livello, semanticamente sono gli stessi.
Gustavo Cardoso,

3
@Gustavo Cardoso: la differenza è semantica; a livello tecnico basso sono generalmente identici. Un puntatore punta a un oggetto o è NULL (un valore errato definito). A meno che const, il suo valore possa essere modificato per puntare ad altri oggetti. Un riferimento è un altro nome per un oggetto, non può essere NULL e non può essere ripristinato. Generalmente è implementato dal semplice utilizzo di puntatori, ma è un dettaglio di implementazione.
David Thornley,

+1 per "il polimorfismo deve essere fatto per riferimento". Questo è un dettaglio incredibilmente cruciale che la maggior parte delle altre risposte ha ignorato.
Doval,

4

La tua dichiarazione iniziale sugli oggetti C # passati per riferimento non è corretta. In C #, gli oggetti sono tipi di riferimento, ma per impostazione predefinita vengono passati per valore proprio come i tipi di valore. Nel caso di un tipo di riferimento, il "valore" che viene copiato come parametro del metodo pass-by-value è il riferimento stesso, quindi le modifiche alle proprietà all'interno di un metodo verranno riflesse al di fuori dell'ambito del metodo.

Tuttavia, se si dovesse riassegnare la variabile del parametro stesso all'interno di un metodo, vedrai che questa modifica non si riflette al di fuori dell'ambito del metodo. Al contrario, se si passa effettivamente un parametro come riferimento utilizzando la refparola chiave, questo comportamento funziona come previsto.


3

Risposta rapida

I progettisti di Java e di linguaggi simili volevano applicare il concetto di "tutto è un oggetto". E passare i dati come riferimento è molto veloce e non consuma molta memoria.

Ulteriori commenti noiosi estesi

Inoltre, quei linguaggi usano riferimenti a oggetti (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), la verità è che i riferimenti a oggetti sono puntatori a oggetti mascherati. Il valore null, l'allocazione di memoria, la copia di un riferimento senza copiare tutti i dati di un oggetto, tutti sono puntatori di oggetti, non semplici oggetti !!!

In Object Pascal (non Delphi), anc C ++ (non Java, non C #), un oggetto può essere dichiarato come variabile allocata statica, e anche con una variabile allocata dinamica, attraverso l'uso di un puntatore ("riferimento oggetto" senza " sintassi dello zucchero "). Ogni caso usa una certa sintassi e non c'è modo di confondersi come in Java "e amici". In quelle lingue, un oggetto può essere passato sia come valore che come riferimento.

Il programmatore sa quando è richiesta una sintassi del puntatore e quando non è richiesta, ma in Java e in linguaggi simili, questo è confuso.

Prima che Java esistesse o diventasse mainstream, molti programmatori imparavano OO in C ++ senza puntatori, passando per valore o per riferimento quando richiesto. Quando passano dall'apprendimento alle app aziendali, quindi usano comunemente puntatori a oggetti. La libreria QT ne è un buon esempio.

Quando ho imparato Java, ho cercato di seguire tutto ciò che è un concetto di oggetto, ma mi sono confuso con la programmazione. Alla fine, ho detto "ok, questi sono oggetti allocati dinamicamente con un puntatore con la sintassi di un oggetto allocato staticamente", e non ho avuto problemi a codificare, di nuovo.


3

Java e C # prendono il controllo della memoria di basso livello da te. Il "mucchio" in cui risiedono gli oggetti che crei vive la sua stessa vita; ad esempio, Garbage Collector raccoglie oggetti ogni volta che lo preferisce.

Dato che esiste un livello separato di indiretto tra il tuo programma e quello "heap", i due modi per fare riferimento a un oggetto, per valore e per puntatore (come in C ++), diventano indistinguibili : fai sempre riferimento a oggetti "per puntatore" per da qualche parte nel mucchio. Ecco perché tale approccio progettuale rende il riferimento pass-by la semantica predefinita dell'assegnazione. Java, C #, Ruby, eccetera.

Quanto sopra riguarda solo le lingue imperative. Nelle lingue di cui sopra il controllo sulla memoria viene passato al runtime, ma la progettazione del linguaggio dice anche "ehi, ma in realtà, non v'è la memoria, e ci sono gli oggetti, ed essi fanno occupare la memoria". I linguaggi funzionali si astraggono ulteriormente, escludendo il concetto di "memoria" dalla loro definizione. Ecco perché il pass-by-reference non si applica necessariamente a tutte le lingue in cui non si controlla la memoria di basso livello.


2

Mi vengono in mente alcuni motivi:

  • Copiare i tipi primitivi è banale, di solito si traduce in un'istruzione di macchina.

  • Copiare oggetti non è banale, l'oggetto può contenere membri che sono oggetti stessi. La copia di oggetti è costosa in termini di tempo e memoria della CPU. Esistono anche diversi modi per copiare un oggetto a seconda del contesto.

  • Il passaggio di oggetti per riferimento è economico e risulta utile anche quando si desidera condividere / aggiornare le informazioni sull'oggetto tra più client dell'oggetto.

  • Strutture di dati complesse (specialmente quelle ricorsive) richiedono puntatori. Passare oggetti per riferimento è solo un modo più sicuro di passare i puntatori.


1

Perché altrimenti, la funzione dovrebbe essere in grado di creare automaticamente una copia (ovviamente profonda) di qualsiasi tipo di oggetto che gli viene passato. E di solito non riesce a indovinarlo. Quindi dovresti definire l'implementazione del metodo copia / clone per tutti i tuoi oggetti / classi.


Potrebbe semplicemente fare una copia superficiale e mantenere i valori effettivi e i puntatori ad altri oggetti?
Gustavo Cardoso,

#Gustavo Cardoso, quindi è possibile modificare altri oggetti attraverso questo, è quello che ti aspetteresti da un oggetto NON passato come riferimento?
David

0

Perché Java è stato progettato come un C ++ migliore e C # è stato progettato come un Java migliore e gli sviluppatori di questi linguaggi erano stanchi del modello di oggetti C ++ fondamentalmente rotto, in cui gli oggetti sono tipi di valore.

Due dei tre principi fondamentali della programmazione orientata agli oggetti sono ereditarietà e polimorfismo e trattare gli oggetti come tipi di valore anziché tipi di riferimento provoca il caos di entrambi. Quando si passa un oggetto a una funzione come parametro, il compilatore deve sapere quanti byte passare. Quando il tuo oggetto è un tipo di riferimento, la risposta è semplice: la dimensione di un puntatore, uguale per tutti gli oggetti. Ma quando il tuo oggetto è un tipo di valore, deve passare la dimensione effettiva del valore. Poiché una classe derivata può aggiungere nuovi campi, questo significa sizeof (derivato)! = Sizeof (base) e il polimorfismo esce dalla finestra.

Ecco un banale programma C ++ che dimostra il problema:

#include <iostream> 
class Parent 
{ 
public: 
   int a;
   int b;
   int c;
   Parent(int ia, int ib, int ic) { 
      a = ia; b = ib; c = ic;
   };
   virtual void doSomething(void) { 
      std::cout << "Parent doSomething" << std::endl;
   }
};

class Child : public Parent {
public:
   int d;
   int e;
   Child(int id, int ie) : Parent(1,2,3) { 
      d = id; e = ie;
   };
   virtual void doSomething(void) {
      std::cout << "Child doSomething : D = " << d << std::endl;
   }
};

void foo(Parent a) {
   a.doSomething();
}

int main(void)
{
   Child c(4, 5);
   foo(c);
   return 0;
}

L'output di questo programma non è quello che sarebbe per un programma equivalente in qualsiasi linguaggio OO sano, perché non è possibile passare un oggetto derivato per valore a una funzione in attesa di un oggetto base, quindi il compilatore crea un costruttore di copie nascosto e passa una copia della parte Parent dell'oggetto Child , invece di passare l'oggetto Child come gli hai detto di fare. I gotcha semantici nascosti come questo sono il motivo per cui passare oggetti per valore dovrebbe essere evitato in C ++ e non è possibile in quasi tutti gli altri linguaggi OO.


Ottimo punto Tuttavia, mi sono concentrato sui problemi di ritorno poiché aggirarli richiede un po 'di sforzo; questo programma può essere risolto con l'aggiunta di una singola e commerciale: void foo (Parent & a)
Ant

OO in realtà non funziona bene senza puntatori
Gustavo Cardoso

-1.000000000000
P Shved

5
È importante ricordare che Java è pass-by-value (passa i riferimenti agli oggetti per valore, mentre le primitive vengono passate semplicemente per valore).
Nicole,

3
@Pavel Shved - "due è meglio di uno!" O, in altre parole, più corda con cui appenderti.
Nicole,

0

Perché altrimenti non ci sarebbe polimorfismo.

Nella programmazione OO, è possibile creare una Derivedclasse più grande da una Basee quindi passarla a funzioni in attesa di una Base. Piuttosto banale eh?

Solo che la dimensione dell'argomento di una funzione è fissa e determinata in fase di compilazione. Puoi argomentare tutto ciò che vuoi, il codice eseguibile è così e le lingue devono essere eseguite in un punto o nell'altro (le lingue interpretate in modo puro non sono limitate da questo ...)

Ora, esiste un dato ben definito su un computer: l'indirizzo di una cella di memoria, generalmente espresso come una o due "parole". È visibile come puntatore o come riferimento nei linguaggi di programmazione.

Quindi, per passare oggetti di lunghezza arbitraria, la cosa più semplice da fare è passare un puntatore / riferimento a questo oggetto.

Questa è una limitazione tecnica della programmazione OO.

Ma poiché per i tipi di grandi dimensioni, in genere preferisci comunque passare i riferimenti per evitare la copia, non è generalmente considerato un duro colpo :)

Vi è una conseguenza importante, tuttavia, in Java o C #, quando si passa un oggetto a un metodo, non si ha idea se l'oggetto verrà modificato dal metodo o meno. Rende il debug / il parallelismo più difficile, e questo è il problema che i linguaggi funzionali e il riferimento trasparente stanno cercando di affrontare -> la copia non è poi così male (quando ha senso).


-1

La risposta è nel nome (beh quasi comunque). Un riferimento (come un indirizzo) si riferisce solo a qualcos'altro, un valore è un'altra copia di qualcos'altro. Sono sicuro che qualcuno abbia probabilmente menzionato qualcosa al seguente effetto, ma ci saranno circostanze in cui uno e non l'altro è adatto (sicurezza della memoria vs efficienza della memoria). Si tratta di gestire la memoria, la memoria, la memoria ...... MEMORIA! : D


-2

Bene, quindi non sto dicendo che questo è esattamente il motivo per cui gli oggetti sono tipi di riferimento o passati per riferimento, ma posso darti un esempio del perché questa è un'ottima idea a lungo termine.

Se non sbaglio, quando erediti una classe in C ++, tutti i metodi e le proprietà di quella classe vengono copiati fisicamente nella classe figlio. Sarebbe come scrivere di nuovo il contenuto di quella classe all'interno della classe figlio.

Ciò significa che la dimensione totale dei dati nella classe figlio è una combinazione di elementi nella classe padre e nella classe derivata.

Ad esempio: #include

class Top 
{   
    int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Middle : Top 
{   
    int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Bottom : Middle
{   
    int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

int main()
{   
    using namespace std;

    int arr[20];
    cout << "Size of array of 20 ints: " << sizeof(arr) << endl;

    Top top;
    Middle middle;
    Bottom bottom;

    cout << "Size of Top Class: " << sizeof(top) << endl;
    cout << "Size of middle Class: " << sizeof(middle) << endl;
    cout << "Size of bottom Class: " << sizeof(bottom) << endl;

}   

Che ti mostrerebbe:

Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240

Ciò significa che se si dispone di una grande gerarchia di più classi, la dimensione totale dell'oggetto, come dichiarato qui, sarebbe la combinazione di tutte quelle classi. Ovviamente, questi oggetti sarebbero considerevolmente grandi in molti casi.

La soluzione, credo, è crearlo sull'heap e usare i puntatori. Ciò significa che, in un certo senso, la dimensione degli oggetti delle classi con più genitori sarebbe gestibile.

Questo è il motivo per cui l'uso dei riferimenti sarebbe un metodo più preferibile per farlo.


1
questo non sembra offrire nulla di sostanziale rispetto ai punti sollevati e spiegati in 13 risposte precedenti
moscerino
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.