lunedì 28 febbraio 2011

Passaggio di parametri ai metodi in Java e C#


Diamo oggi un’occhiata ad un argomento che ho notato solleva dubbi ai programmatori principianti: come vengono trattati gli oggetti e le variabili passati come argomenti ai metodi, per riferimento o per valore e quali sono le differenze tra i due linguaggi C# e Java.
Iniziamo col dire cosa significa passaggio “per valore” e passaggio “per riferimento” (indipendentemente dal linguaggio utilizzato):
  • Valore: viene creata una copia di una variabile al momento della chiamata, per cui il metodo non agisce direttamente sulla variabile originale ma solo sulla sua copia, quindi all’uscita di tale metodo la variabile originale rimarrà immutata
  • Riferimento: viene passato un riferimento (indirizzo) alla variabile per cui il metodo chiamato agirà direttamente sulla variabile originale tramite il suo riferimento.
Anche le variabili si suddividono principalmente in due tipologie:

  • ValueTypes:  sono i cosiddetti tipi primitivi. Vengono usualmente allocati nello stack. Esempi di tipi primitivi sono int, double, float e le strutture (p. es. la struttura Int32 per .NET)
  • ReferenceTypes: sono i cosiddetti tipi complessi.  Sono tutti gli oggetti definiti tramite una “class”. Usualmente tali tipi vengono allocati nel Managed Heap tramite una new. Esempi sono le classi che noi creiamo, oppure quelle preesistenti nel framework (p.es. la classe Integer in Java è un Reference Type)


In Java e C# tutte le variabili (ValueTypes o ReferenceTypes) vengono passate ai metodi, di default, per valore. Sfatiamo il mito che dice che in Java le variabili primitive sono passate per valore e gli oggetti per riferimento: è corretto invece dire che in Java (come in C#), vengono passati i riferimenti agli oggetti per valore,  così come confermato dallo stesso James Gosling, uno degli inventori di Java:

“Some people will say incorrectly that objects are passed “by reference.” In programming language design, the term pass by reference properly means that when an argument is passed to a function, the invoked function gets a reference to the original value, not a copy of its value. If the function modifies its parameter, the value in the calling code will be changed because the argument and parameter use the same slot in memory…. The Java programming language does not pass objects by reference; it passes object references by value. Because two copies of the same reference refer to the same actual object, changes made through one reference variable are visible through the other. There is exactly one parameter passing mode — pass by value — and that helps keep things simple.”
James Gosling – The Java Programming Language, 4th Edition

Java e C# hanno quindi lo stesso comportamento. Vediamo un esempio in Java sui tipi primitivi:

public class A{
  ...
   void A_method(int k){
     int i=10*k; //questa i è locale al metodo e non ha niente a che fare con quella esterna
     k=5; //qui si modifica il valore di una variabile locale al metodo (k)
     ...
   }
 }

...  
int i=2;
A a;
a.A_method(i);

int j=i;//qui si avrà j=i=2 e non 5!
...

La chiamata al metodo passando per valore la variabile i, non sortisce su questa alcun effetto anche se A_method copia il valore di i su un’altra variabile k. In C# il discorso è analogo.
Il discorso per i ReferenceTypes è invece differente e merita un pò di attenzione.
Soprattutto in C#, il passaggio per valore di un oggetto (che è quello di default), può creare qualche confusione iniziale e qualche sorpresa.  Abbiamo visto che passare un oggetto per valore significa che verrà creata una copia non dell’oggetto stesso, ma del suo riferimento. Ciò significa che se io chiamo un metodo e gli passo un oggetto, verrà creata una copia del riferimento e cioè tale copia del riferimento punterà all’oggetto stesso: il risultato è che le modifiche apportate ad un oggetto dentro al metodo in C# saranno persistenti.
Si considerino le seguenti classi C#:
  class A
  {
      public int val {get;set;}
  }

  class Class1
  { 
     public void A_byVal1(A k)
     {
       k.val = 10; //qui i riferimenti di k e ag sono gli stessi e l’istruzione agisce su ag
       A b = new A(); //questo new crea un nuovo riferimento
       b.val = 100;
       k = b; //questo copia il riferimento di b in k
       k.val = 200; //qui k.val e b.val valgono entrambi 200, ma ag.val vale sempre 10!
     }
  }

con le seguenti istruzioni:

Class1 c1 = new Class1();
A ag1 = new A();
ag1.val = 1;
c1.A_byVal1(ag1); //all’uscita ag1.val vale 10: non 1, non 100 e nemmeno 200!

All’uscita del metodo A_byVal1, ag1.val  vale 10 perchè al momento della chiamata di A_byVal1 viene creata una copia della reference di ag1 (ref_ag1), chiamiamola ag1_ref_copy: ma allora ref_ag1 e ag1_ref_copy puntano entrambe ad ag1 e da ciò si capisce come k.val = 10; vada a modificare il reale valore dell’oggetto ag1 originale. La new dentro il metodo crea un altro oggetto b nel Managed Heap, ne inizializza il valore a 100 e copia il riferimento dell’oggetto b in k: ciò significa che l’oggetto originale ag non viene più modificato poichè il riferimento di k punta ad una variabile locale b, che verrà segnata per l’eliminazione all’uscita del metodo dal GC.
Il meccanismo per passare variabili (primitive e non) ad un metodo per riferimento in C# prevede l’utilizzo della parola chiave ref (tale metodologia ha origini probabilmente dal C++ in cui si usava il carattere & per indicare il passaggio per riferimento di una variabile):

public void A_byRef2(ref A k)
{
    //qui k e ag puntano alla stessa locazione (ag.val = 0)
    A b = new A(); //questo new crea un nuovo riferimento
    b.val = 10;
    k = b; //questo copia il riferimento di b in k, ma quindi anche ag punta a b
}

Class1 c1 = new Class1();
A ag1 = new A();
ag1.val = 0;
c1.A_byRef2(ref ag); //all’uscita ag1.val vale 10, non 0!

Come si nota, pur creando un riferimento locale, all’uscita del metodo  ag.val  varrà 10: ciò avviene perchè al momento della chiamata non viene fatta una copia del riferimento di ag, ma viene usata proprio quella locazione: per cui quando copio b in k, sto copiando proprio sulla locazione di memoria puntata da ag. A differenza del caso del passaggio per valore (metodo A_byVal1), all’uscita del metodo la locazione creata nello heap con la new (b), non verrà segnata per la distruzione dal GC perchè puntata ancora da ag. Qualcuno potrebbe chiedersi come mai l’operatore di assegnamento delle variabili ReferenceType agisce sui riferimenti e non sui valori: il motivo è che quando usiamo gli operatori noi passiamo le variabili per valore e se usiamo i ReferenceTypes passeremo per valore i riferimenti alle variabili contenenti i valori. In .NET e Java i vari operatori (=,+=,-= ecc.), come tutti i metodi che ricevono ReferenceTypes, agiscono sull'indirizzo di memoria dell'oggetto, ovvero l'indirizzo che fa riferimento all'oggetto. Come noto invece, i ValueTypes contengono invece direttamente il valore e non sono sotto il controllo del GC. La differenza tra ValueTypes e ReferenceTypes spesso viene mascherata dal meccanismo di boxingunboxing (wrapping – unwrapping in Java) che consiste nell’incapsulare il ValueType dentro un ReferenceType. Proviamo a sottoporre alle stesse operazioni due variabili, una ValueType e una ReferenceType.
Data la classe A vista prima, si consideri il seguente esempio in C#:

A d_ref1 = new A();  // Allocato nell'heap
double d_val1;      // Allocato nello stack
d_ref1.val = 10;    // Cambia il riferimento a cui punta
d_val1 = 10;        // Cambiato il valore nello stack
A d_ref2 = d_ref1;     // Copia il solo riferimento (puntatore)
double d_val2 = d_val1;         // Alloca nello stack e copia il valore
d_ref1.val = 20;
d_val1 = 20;
Console.WriteLine("d_ref1.val = {0}, d_ref2 = {1}", d_ref1.val, d_ref2.val);
Console.WriteLine("d_val1 = {0}, d_val2 = {1}", d_val1, d_val2);

Eseguendo questo codice si otterrà il seguente output:

d_ref1.val = 20, d_ref2.val = 20
d_val1 = 20, d_val2 = 10

Ossia quando si applica l’operatore di assegnamento (cfr. metodo) ai ValueTypes e ai ReferenceTypes si ha un comportamento differente perchè l’operatore di assegnamento agisce direttamente sui valori dei primi e sugli indirizzi dei secondi (d_val1 = 20 non interferisce con d_val2, mentre d_ref1.val = 20; fa in modo che pure d_ref2.val punti alla locazione contenente 20. Nota: in questo esempio C# non potevamo usare Double al posto di A perchè essa, pur effettuando il boxing di double, è una structure che comunque viene allocata nello stack di default.

Anche in Java gli oggetti Reference sono passati ai metodi per valore dei riferimenti, perciò l'oggetto referenziato può essere modificato dal metodo. Il comportamento è del tutto simile al C#. Per verificarlo facciamo un esempio.
Supponiamo di avere le seguenti  classi Java:

public class A {
  int val=0;
   ...
}
public class Class1
{
   void A_byRef(A k) {
     k.val=20; //qui i riferimenti di k e ag sono gli stessi e l’istruzione agisce su ag
     ...
     k= new A(); //qui viene creato un nuovo riferimento per k
     k.val=100; //questa istruzione agisce solo su k locale
   }
}

Effettuiamo una chiamata al metodo A_byRef:

  Class1 c1 = new Class1();
  A ag = new A();
  ag.val = 5;  
  c1.A_byRef(ag);

  int i = ag.val; //i vale 20 non 5 e nemmeno 100

Il caso di Java è del tutto simile a quello di C#: il riferimento di ag viene passato per valore cosicchè alla chiamata viene fatta una copia di ref_ag, ref_ag_copy. A questo punto l’istruzione (e solo quella) k.val=20; agirà sull’istanza di ag originale, mentre la copia creata successivamente con la new crea un nuovo riferimento ad un’altra istanza di oggetto nello heap di tipo A, per cui k.val=100 non ha ripercussioni su ag.
A questo punto nasce una domanda spontanea: si possono passare variabili in Java per riferimento come in C#? La risposta è no! Non esiste in Java l’equivalente della parola chiave ref e ciò spiega come mai non potremmo mai scambiare due variabili int dentro un metodo Java senza l’utilizzo della tecnica del boxing!

public void badSwapInJava(int var1, int var2)
{
  int temp = var1;
  var1 = var2;
  var2 = temp;
}

Il metodo badSwapInJava() non altera le variabili passate. Facendo il boxing (alias wrapping) otteniamo il risultato voluto:

public void boxingSwapInJava(MyInteger rWrap, MyInteger sWrap) {
      int t = rWrap.getValue();
      rWrap.setValue(sWrap.getValue());
      sWrap.setValue(t);
}

1 commenti:

Anonimo ha detto...

Grazie mi hai chiarito le idee..

Posta un commento

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | cna certification