1 1 1 1 1 1 1 1 1 1 Rating 0.00 (0 Votes)

Bekanntlich sind die einfachen Datentypen (int, double, ...) sog. Werttypen, das heißt, ihre Inhalte werden direkt auf dem Stack gespeichert und bei Zuweisungen z.B. bei Methodenaufrufen als Argumente für die Methoden-Parameter in die Ziel-Variable kopiert. Hier gilt also das Prinzip "call by value".

Alle komplexen Typen (object, eigene Klassen) werden als sog. Referenztypen (oder Verweistypen) dagegen im Heap und diese Heap-Speicheradresse dann auf dem Stack gespeichert. Bei Zuweisungen wird dann vom Stack nur die dort gespeicherte Heap-Adresse (der Verweis/die Referenz) "by reference" übergeben bzw. kopiert. Dadurch werden Änderungen z.B. in einer Methode an einer Eigenschaft eines Parameter-Objektes im Argument-Objekt vorgenommen.

(Ausnahme hierbei ist der komplexe Typ string, der eigentlich ein Referenztyp ist, aber intern wie ein Werttyp behandelt wird.)

 

Um nun Werttypen wie ein Referenztyp an eine Methode zu übergeben (um Änderungen an der Variable im Argument zu erhalten), muss dieses Werttyp-Argument explizit referenziert übergeben werden. Dafür kennen wir in C# ref und out, die genau das ermöglichen ("call by reference").

Aber das ist ja jetzt nicht das Thema :)

Vielmehr ist interessant, warum auch Referenztypen noch zusätzlich mit ref an Methoden übergeben werden können. Das hört sich im ersten Moment "doppelt gemoppelt" an, hat aber einen konkreten Hintergrund, den ich hier an einem kleinen Beispiel erläutern möchte.

Für dieses Beispiel benutze ich einen ganz einfachen komplexen Typ mit nur einer Eigenschaft:

    class Klasse
    {
        public int Wert { get; set; }
    }

Um das Prinzip "call by reference" nochmal zu verdeutlichen, sollen zuerst nur zwei Variablen mit derselben Instanz erzeugt werden und eine davon an eine Methode übergeben werden, in der dann die Eigenschaft geändert wird.

    class Program
    {
        static void Main(string[] args)
        {
            Klasse k1 = new Klasse();
            Klasse k2 = k1;             // k1 und k2 referenzieren dasselbe Objekt
            k1.Wert = 5;                // auch in k2 ändert sich Wert

            Console.WriteLine("Wert vor Standard-Methodenaufruf:      k1={0,3}  k2={1,3}", k1.Wert, k2.Wert);
            MethodeStandard(k1);
            Console.WriteLine("Wert nach Standard-Methodenaufruf:     k1={0,3}  k2={1,3}\n\n", k1.Wert, k2.Wert);

            Console.ReadKey();
        }

        static void MethodeStandard(Klasse klasse)      // klasse erhält Referenz von k1
        {
                                                        // klasse, k1 und k2 sind dasselbe Objekt
            klasse.Wert = 10;                           // Wert wird für klasse, k1 und k2 geändert!
        }
    }

Nach der Instanziierung von k1 wird der zweiten Objektvariablen k2 die Referenz der Variablen k1 zugewiesen, somit haben wir ein Objekt, welches durch zwei Variablen referenziert wird.

In der vereinfachten schematischen Darstellung ergibt sich also folgendes Bild:

nach Instanziierung

Bild 1: nach Instanziierung

k1 instanziiert ein Objekt vom Typ Klasse, für dessen Inhalte (Objekt-Konstanten und Felder) auf dem Heap Speicher reserviert ("alloziiert") wird, hier exemplarisch ab Adresse 40000. Der einzige Inhalt ist die Eigenschaft Wert, dessen "backing field" anfangs mit 0 initialisiert wird. Der Einfachheit halber nehmen wir im weiteren Verlauf an, dass der Speicher des backing fields mit der Startadresse des für das Objekt alloziierten Speicherbereichs identisch ist. Die dem Objekt zugewiesene Adresse (40000) wird für die Variable auf dem Stack abgelegt (hier die angenommene Stack-Adresse 10000) und die Stackadresse der Variablen zugewiesen.

k2 erhält auch einen Speicherplatz auf dem Stack (10004). Dieser Speicherplatz erhält durch die Zuweisung der Variablen k1 den Stackinhalt von k1, also ebenfalls die Objekt-Referenzadresse 40000.

Nun wird der Objekt-Eigenschaft Wert über die Variable k1 der Wert 5 zugewiesen.

nach Zuweisung der Eigenschaft

Bild 2: nach Zuweisung der Eigenschaft

Vereinfacht ausgedrückt, wird über den Bezeichner k1 die Stackadresse ermittelt (10000). Da es sich mit dem Datentyp Klasse um einen komplexen Typen, also einen Referenztypen handelt, kann auf dem Stack nur die Heap-Adresse des Objektes stehen (40000), über die dann der Speicher der Eigenschaft Wert ermittelt wird (hier vereinfacht ebenfalls 40000). Und hier wird nun der neue Wert 5 gespeichert.

Da die Variable k2 in ihrer Stackadresse ebenfalls die Objekt-Adresse 40000 enthält, zeigen also nach der Wert-Zuweisung beide Variablen denselben Wert, da sie dasselbe Objekt referenzieren.

Durch den Methodenaufruf wird der Inhalt der k1-Stackadresse 10000, also die Objekt-Referenzadresse 40000 als Inhalt für die Parameter-Variable klasse übergeben, so dass nun drei Variablen dasselbe Objekt referenzieren. Und über die dritte Variable (klasse) wird nun die Eigenschaft Wert geändert.

Änderung der Eigenschaft

Bild 3: Änderung der Eigenschaft

Hier passiert natürlich dasselbe, wie auch schon vorher. Die Parameter-Variable klasse erhält einen eigenen Stack-Speicherplatz, in der der Inhalt der Argument-Variablen k1, also die Heap-Adresse 40000 gespeichert wird. Und durch die Zuweisung ändert sich dann der Inhalt der Adresse 40000.

Das hat zur Folge, dass jetzt natürlich die Änderung der Eigenschaft Wert in allen drei Variablen sichtbar ist, was die Ausgabe der beiden Argument-Variablen beweist.

 

Im nächsten Schritt soll die Parameter-Variable klasse eine neue Instanz der Klasse erhalten.

    class Program
    {
        static void Main(string[] args)
        {
            Klasse k1 = new Klasse();
            Klasse k2 = k1;             // k1 und k2 sind dasselbe Objekt
            k1.Wert = 5;                // auch in k2 ändert sich Wert

            Console.WriteLine("Wert vor Standard-Methodenaufruf:      k1={0,3}  k2={1,3}", k1.Wert, k2.Wert);
            MethodeStandard(k1);
            Console.WriteLine("Wert nach Standard-Methodenaufruf:     k1={0,3}  k2={1,3}\n\n", k1.Wert, k2.Wert);

            Console.WriteLine("Wert vor NewKlasse-Methodenaufruf:     k1={0,3}  k2={1,3}", k1.Wert, k2.Wert);
            MethodeNew(k1);         // Parameter wird neu instanziiert; Argument wird dadurch nicht mehr geändert
            Console.WriteLine("Wert nach NewKlasse-Methodenaufruf:    k1={0,3}  k2={1,3}\n\n", k1.Wert, k2.Wert);

            Console.ReadKey();
        }

        static void MethodeStandard(Klasse klasse)      // klasse erhält Referenz von k1
        {
                                                        // klasse, k1 und k2 sind dasselbe Objekt
            klasse.Wert = 10;                           // Wert wird für klasse, k1 und k2 geändert!
        }

        static void MethodeNew(Klasse klasse)           // klasse erhält Referenz von k1
        {
            klasse = new Klasse();                      // klasse erhält neue Referenz, klasse und k1/k2 sind unterschiedliche Objekte
            klasse.Wert = 77;                           // Wert für klasse wird geändert, nicht aber für k1/k2
        }
    }

 In der Methode wird nun die Parameter-Variable erneut instanziiert. Das Ergebnis sieht wie folgt aus:

Instanziierung der Parameter-Variablen

Bild 4: Instanziierung der Parameter-Variablen

Durch die Instanziierung wird ein neues Objekt erzeugt, für das auf dem Heap ein neuer Speicherbereich alloziiert wird (hier: Startadresse 50000). Diese Startadresse wird nun in die Stack-Adresse (10008) der Parameter-Variablen klasse geschrieben. Durch die Zuweisung des Wertes 77 an die Eigenschaft Wert des neuen Objektes ändert sich also der Inhalt der Heap-Adresse 50000

Da die Argument-Variablen k1 und k2 nach wie vor ein Objekt ab Heap-Adresse 40000 referenzieren, ändert sich dessen Wert also nicht.

 

Ganz anders sieht es aus, wenn der Referenztyp-Parameter zusätzlich referenziert wird!

    class Program
    {
        static void Main(string[] args)
        {
            Klasse k1 = new Klasse();
            Klasse k2 = k1;             // k1 und k2 sind dasselbe Objekt
            k1.Wert = 5;                // auch in k2 ändert sich Wert

            Console.WriteLine("Wert vor Standard-Methodenaufruf:      k1={0,3}  k2={1,3}", k1.Wert, k2.Wert);
            MethodeStandard(k1);
            Console.WriteLine("Wert nach Standard-Methodenaufruf:     k1={0,3}  k2={1,3}\n\n", k1.Wert, k2.Wert);

            Console.WriteLine("Wert vor NewKlasse-Methodenaufruf:     k1={0,3}  k2={1,3}", k1.Wert, k2.Wert);
            MethodeNew(k1);         // Parameter wird neu instanziiert; Argument wird dadurch nicht mehr geändert
            Console.WriteLine("Wert nach NewKlasse-Methodenaufruf:    k1={0,3}  k2={1,3}\n\n", k1.Wert, k2.Wert);

            Console.WriteLine("Wert vor NewRefKlasse-Methodenaufruf:  k1={0,3}  k2={1,3}", k1.Wert, k2.Wert);
            MethodeNewRef(ref k1);  // ref-Parameter wird neu instanziiert, dadurch wird auch Argument k1 geändert, aber nicht k2!
            Console.WriteLine("Wert nach NewRefKlasse-Methodenaufruf: k1={0,3}  k2={1,3}\n\n", k1.Wert, k2.Wert);

            Console.ReadKey();
        }

        static void MethodeStandard(Klasse klasse)      // klasse erhält Referenz von k1
        {
                                                        // klasse, k1 und k2 sind dasselbe Objekt
            klasse.Wert = 10;                           // Wert wird für klasse, k1 und k2 geändert!
        }

        static void MethodeNew(Klasse klasse)           // klasse erhält Referenz von k1
        {
            klasse = new Klasse();                      // klasse erhält neue Referenz, klasse und k1/k2 sind unterschiedliche Objekte
            klasse.Wert = 77;                           // Wert für klasse wird geändert, nicht aber für k1/k2
        }

        static void MethodeNewRef(ref Klasse klasse)    // klasse erhält Referenz der Referenz von k1
        {
            klasse = new Klasse();                      // klasse erhält neue Referenz, damit auch k1 und beide sind dasselbe neue Objekt; k2 ist aber immer noch das "alte" Objekt
            klasse.Wert = 999;                          // Wert wird für klasse und k1 geändert, nicht aber für k2
        }
    }

 In der Methode wird ebenfalls ein neues Objekt für die Parameter-Variable instanziiert und ihr ein Wert zugewiesen. Aber mit Konsequenzen:

Instanziierung der referenzierten Parameter-Variablen

Bild 5: Instanziierung der referenzierten Parameter-Variablen

Durch die Referenzierung mit ref (übrigens ebenso wie mit out) erhält die Parameter-Variable klasse nicht den Inhalt der Argument-Variablen k1 (das wäre 40000), sondern die Adresse der Variablen, nämlich die Stackadresse 10000. Und diese Adresse wird für klasse auf im Stack gespeichert. Für die Objekt-Instanziierung wird erneut Speicher im Heap alloziiert (jetzt: Adresse 60000). Diese Adresse wird allerdings nicht im Stack der Parameter-Variablen gespeichert. Veranlasst durch ref wird die Referenzadresse (10000) aus klasse vom Stack geholt und in diese Referenzadresse dann die neue Objekt-Referenzadresse gespeichert.

Die Argument-Variable k1 referenziert nun also ein neues Objekt mit der Referenzadresse 60000, die Variable k2 aber nach wie vor das "alte" Objekt mit der Referenzadresse 40000, was uns die Ausgabe auch bestätigt.

 

Fazit:

Um also einer Argument-Variablen in einem Methoden-Aufruf ein neues Objekt zuzuweisen, reicht es nicht aus, sie als normales "Referenzobjekt"-Argument zu übergeben, sondern sie muss als "referenzierte Referenz" verarbeitet werden.

 

Hätten Sie's gewusst?

Angemeldete User können Kommentare verfolgen und bei Antworten auf Kommentare per Email benachrichtigt werden.

Wussten Sie's schon?

Für Kommentare und Fehlerhinweise bin ich Ihnen immer dankbar. Benutzen Sie hierfür bitte das Kontaktformular oder die Kommentar-Funktion des jeweiligen Beitrags - eine Anmeldung ist hierfür nicht erforderlich. Wollen Sie jedoch über Reaktionen zu Kommentaren per Email informiert werden, dann melden Sie sich bitte an.