Perché gli oggetti vengono passati per riferimento?

31

Un giovane collega che stava studiando 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 di progettazione? Gli sviluppatori di queste lingue erano stanchi di dover creare puntatori e typedef ogni volta?

    
posta Gustavo Cardoso 11.03.2011 - 16:22
fonte

14 risposte

15

I motivi di base si riducono a questo:

  • I puntatori sono tecnici per avere ragione
  • Hai bisogno di indicatori per implementare determinate strutture dati
  • Sono necessari indicatori per essere efficienti nell'uso della memoria
  • Non hai bisogno dell'indicizzazione manuale della memoria per lavorare se non stai utilizzando l'hardware direttamente.

Quindi, riferimenti.

    
risposta data 11.03.2011 - 17:38
fonte
31

Risposta semplice:

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

    
risposta data 11.03.2011 - 16:56
fonte
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 usare l'ottimizzazione del valore di ritorno, ciò che accade qui è questo:

  • newObj viene creato come oggetto temporaneo e inserito nello stack locale.
  • Viene creata e restituita una copia di newObj.

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

Diamo un'occhiata invece al 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 affrontarlo da soli:

MyClass someObj = getNewObject();
delete someObj;

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

È stato suggerito un sacco di soluzioni per risolvere questi due problemi: l'ottimizzazione del valore di ritorno (in cui il compilatore è abbastanza intelligente da non creare la copia ridondante in return-by-value), passando un riferimento al metodo (quindi la funzione inietta in un oggetto esistente piuttosto che crearne uno nuovo), puntatori intelligenti (in modo che la questione della proprietà sia discutibile).

I creatori di Java / C # si sono resi conto che restituire sempre oggetto per riferimento era una soluzione migliore, soprattutto se il linguaggio lo supportava in modo nativo. Si lega a molte altre funzionalità delle lingue, come la garbage collection, ecc.

    
risposta data 11.03.2011 - 16:31
fonte
9

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

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

Come altri hanno già detto, in Java non esiste un "clone" naturale. Questa non è solo una funzionalità mancante. mai vuoi solo copiare * volutamente (sia in profondità che in profondità) ogni proprietà in un oggetto. Cosa accadrebbe se quella proprietà fosse una connessione al database? Non si può semplicemente "clonare" una connessione al database più di quanto si possa clonare un umano. Inizializzazione esiste per un motivo.

Le copie profonde sono un problema a parte loro: in che misura veramente vai? Non puoi assolutamente copiare nulla che sia statico (compresi tutti gli oggetti Class ).

Quindi per lo stesso motivo per cui non esiste un clone naturale, gli oggetti che vengono passati come copie creano follia . Anche se fosse possibile "clonare" una connessione DB, come faresti adesso a essere 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 per 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 sulla definizione di transient laddove appropriato.

    
risposta data 11.03.2011 - 18:20
fonte
4

Gli oggetti sono sempre referenziati in Java. Non vengono mai ignorati.

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

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

Inoltre, Java può cambiare l'assegnazione / copia / qualsiasi cosa di default. In C ++, è una copia più o meno profonda, mentre in Java è un semplice assegnamento / copia / qualsiasi puntatore, con .clone() e nel caso sia necessario copiare.

    
risposta data 11.03.2011 - 16:50
fonte
4

La tua affermazione 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, pertanto le modifiche alle proprietà all'interno di un metodo verranno riflesse all'esterno dell'ambito del metodo.

Tuttavia, se si dovesse riassegnare la variabile parametro stessa all'interno di un metodo, si noterà che questa modifica non viene riflessa al di fuori dell'ambito del metodo. Al contrario, se in realtà passi un parametro per riferimento utilizzando la parola chiave ref , questo comportamento funziona come previsto.

    
risposta data 11.03.2011 - 21:39
fonte
3

Risposta rapida

I designer di Java e lingue simili volevano applicare il concetto "tutto è un oggetto". E il passaggio dei dati come riferimento è molto veloce e non consuma molta memoria.

Ulteriori commenti noiosi estesi

Altougth, quelle lingue usano riferimenti a oggetti (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), la verità è che i riferimenti a oggetti sono puntatori di oggetti mascherati. Il valore nullo, l'allocazione di memoria, la copia di un riferimento senza copiare l'intero dato di un oggetto, sono tutti puntatori di oggetti, non oggetti semplici !!!

In Object Pascal (non in Delphi), anc C ++ (non in Java, non in C #), un oggetto può essere dichiarato come variabile allocata statica, e anche con una variabile allocata dinamica, tramite 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 sia come riferimento.

Il programmatore sa quando è richiesta una sintassi del puntatore e quando non è richiesto, ma in Java e lingue simili, questo è fonte di confusione.

Prima che Java esistesse o diventasse mainstream, molti programmatori impararono O.O. in C ++ senza puntatori, passando per valore o per riferimento quando richiesto. Quando passano dall'app di apprendimento alle app aziendali, quindi, comunemente usano puntatori di oggetti. Il Q.T. la biblioteca ne è un buon esempio.

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

    
risposta data 11.03.2011 - 18:13
fonte
3

Java e C # prendono il controllo della memoria di basso livello da te. L '"ammasso" in cui gli oggetti creati crea vita propria; ad esempio, il garbage collector raccoglie oggetti ogni volta che preferisce.

Poiché esiste un livello separato di riferimento indiretto tra il tuo programma e quel "mucchio", i due modi per fare riferimento a un oggetto, per valore e per puntatore (come in C ++), diventano indistinguibili : fai sempre riferimento agli oggetti "per puntatore" a qualche parte nell'heap. Ecco perché questo approccio alla progettazione rende pass-per-referenziare la semantica predefinita dell'assegnazione. Java, C #, Ruby, eccetera.

Quanto sopra riguarda solo le lingue imperative. Nei linguaggi menzionati sopra il controllo della memoria viene passato al runtime, ma il linguaggio design dice anche "hey, ma in realtà, è la memoria, e là sono gli oggetti e fanno occupano 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 controlli la memoria di basso livello.

    
risposta data 11.03.2011 - 21:12
fonte
2

Posso pensare ad alcuni motivi:

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

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

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

  • Le strutture dati complesse (specialmente quelle che sono ricorsive) richiedono dei puntatori. Passare oggetti per riferimento è solo un modo più sicuro per passare i puntatori.

risposta data 11.03.2011 - 19:27
fonte
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 può indovinare per farlo. Quindi dovresti definire l'implementazione del metodo copia / clone per tutti i tuoi oggetti / classi.

    
risposta data 11.03.2011 - 16:29
fonte
0

Poiché 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 l'ereditarietà e il polimorfismo, e il trattamento di oggetti come tipi di valore invece di tipi di riferimento causa scompiglio con entrambi. Quando si passa un oggetto a una funzione come parametro, il compilatore deve sapere quanti byte devono 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 che si aspetta un oggetto base, quindi il compilatore crea una copia nascosta costruttore e passa una copia della parte padre dell'oggetto Child , invece di passare l'oggetto Child come gli hai detto di fare. I trucchi semantici nascosti come questo sono il motivo per cui il passaggio di oggetti in base al valore deve essere evitato in C ++ e non è possibile in quasi tutti gli altri linguaggi OO.

    
risposta data 11.03.2011 - 16:47
fonte
0

Perché altrimenti non ci sarebbe il polimorfismo.

In Programmazione OO, puoi creare una classe Derived maggiore da una Base una, e quindi passarla a funzioni che prevedono una Base una. Piuttosto banale eh?

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

Ora, c'è un pezzo di dati che è ben definito su un computer: l'indirizzo di una cella di memoria, solitamente espressa come una o due "parole". È visibile come puntatori o riferimenti 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 dato che per i tipi grandi, generalmente preferisci passare i riferimenti comunque per evitare di copiare, in genere non è considerato un grosso problema:)

C'è una conseguenza importante però, in Java o C #, quando si passa un oggetto a un metodo, non si ha idea se il proprio oggetto verrà modificato dal metodo o meno. Rende più difficile il debugging / parallelization, e questo è il problema che Functional Languages e Transparential Referency stanno cercando di risolvere - > la copia non è poi così male (quando ha senso).

    
risposta data 11.03.2011 - 20:36
fonte
-1

La risposta è nel nome (beh quasi sempre). 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 rispetto all'efficienza della memoria). Si tratta di gestire la memoria, la memoria, la memoria ...... MEMORIA! : D

    
risposta data 11.03.2011 - 18:26
fonte
-2

Bene, 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 è una buona 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 figlia.

Quindi questo significa che la dimensione totale dei dati nella classe figlia è una combinazione delle cose nella classe genitore e nella classe derivata.

EG:     #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 hai 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, è crearla sull'heap e usare i puntatori. Ciò significa che la dimensione degli oggetti delle classi con più genitori sarebbe gestibile, in un certo senso.

Ecco perché usare i riferimenti sarebbe un metodo più preferibile per farlo.

    
risposta data 01.09.2016 - 15:53
fonte

Leggi altre domande sui tag