CodeGym /Kurse /C# SELF /Referenztypen und Werttypen

Referenztypen und Werttypen

C# SELF
Level 12, Lektion 3
Verfügbar

1. Einführung

Stell dir vor, du hast eine Box. Wenn diese Box den Gegenstand selbst enthält (zum Beispiel eine Box mit einem Apfel darin), dann ist das wie ein Werttyp. Die Daten liegen direkt in dieser "Box" (Variable).

Andererseits stell dir vor, du hast eine Visitenkarte mit einer Adresse. Die Visitenkarte selbst ist kein Haus, sie zeigt nur, wo das Haus zu finden ist. Das ist wie ein Referenztyp. Die Variable enthält in diesem Fall nicht die Daten selbst, sondern die "Visitenkarte" – die Adresse im Speicher, wo diese Daten liegen.

Ganz einfache Beispiele:

  • int x = 5; // Werttyp: Die Variable x "speichert" die Zahl 5 direkt.
  • string name = "Wally"; // Referenztyp: Die Variable name "speichert" eine Referenz auf den String "Wally", der irgendwo im Speicher liegt.

Wer ist wer im Leben?

Damit du dich besser zurechtfindest, hier eine Übersicht, zu welchen Kategorien die wichtigsten Datentypen gehören:

Werttypen (Value Types):
  • Primitive Typen: int, double, float, bool, char, byte, short, long, decimal usw.
  • Strukturen (struct): Alle eigenen Strukturen, die du mit dem Schlüsselwort struct deklarierst.
  • Aufzählungen (enum): Typen, mit denen du eine Menge benannter Konstanten definieren kannst.
Referenztypen (Reference Types):
  • Strings (string): Auch wenn Strings ein paar Besonderheiten haben (sie sind immutable), sind sie Referenztypen.
  • Alle Arrays: Zum Beispiel int[], string[], YourCustomClass[].
  • Alle Klassen (class): Alle eigenen Klassen, die du mit dem Schlüsselwort class deklarierst.
  • Delegates (delegate): Typen, die Referenzen auf Methoden darstellen.
  • Interfaces (interface): Auch wenn Interfaces selbst keine Objekte sind, kann eine Variable vom Typ Interface eine Referenz auf ein Objekt speichern, das dieses Interface implementiert.
  • Listen, Dictionaries und andere Collections: Zum Beispiel List<T>, Dictionary<TKey, TValue>.
  • Und überhaupt, alles, was kein struct oder enum ist, ist standardmäßig ein Referenztyp in C#.

2. Variablen kopieren

Hier liegt der wichtigste praktische Unterschied. Wenn du eine Variable einer anderen zuweist, was wird eigentlich kopiert?

A) Kopieren eines Werttyps (Value Type)

Wenn du einen Werttyp kopierst, wird eine vollständige, unabhängige Kopie der Daten erstellt. Das ist wie eine Kopie eines Dokuments: Änderungen an einer Kopie beeinflussen das Original oder andere Kopien nicht.


    int a = 10;
int b = a; 					// b ist jetzt auch 10, aber das ist "seine eigene Kopie"
Console.WriteLine($"Anfangswerte: a = {a}, b = {b}"); // Anfangswerte: a = 10, b = 10

b = 15; 					// Wir ändern b
Console.WriteLine($"Nach Änderung von b: a = {a}, b = {b}"); // Nach Änderung von b: a = 10, b = 15

Erklärung: Die Variable b hat ihre eigene Kopie des Werts 10 bekommen. Als b auf 15 geändert wurde, blieb a mit seinem ursprünglichen Wert 10. Sie sind komplett unabhängig.

B) Kopieren eines Referenztyps (Reference Type)

Beim Kopieren eines Referenztyps wird nur die Referenz kopiert, also die "Adresse" des Objekts im Speicher. Beide Variablen zeigen jetzt auf dasselbe Objekt. Das ist so, als würdest du zwei Leuten dieselbe Visitenkarte geben: Beide kennen die Adresse desselben Hauses. Wenn einer etwas im Haus ändert (z.B. die Wand streicht), sieht der andere diese Änderung auch, wenn er zur Adresse geht.

Schauen wir uns das mal mit Arrays an, weil sie die "Magie" der Referenzen sehr anschaulich zeigen (anders als Strings, die ihre eigenen Besonderheiten haben).


int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // arr2 und arr1 zeigen auf dasselbe Array im Speicher!

// Anfangswerte: arr1[0] = 1, arr2[0] = 1
Console.WriteLine($"Anfangswerte: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}"); 

arr2[0] = 42; // Wir ändern ein Element im Array über arr2

// Nach Änderung von arr2[0]: arr1[0] = 42, arr2[0] = 42
Console.WriteLine($"Nach Änderung von arr2[0]: arr1[0] = {arr1[0]}, arr2[0] = {arr2[0]}"); 

Erklärung: Beide Variablen (arr1 und arr2) enthalten eine Referenz auf dasselbe Array im Speicher. Wenn du das Element arr2[0] änderst, änderst du tatsächlich genau das Array, auf das beide Variablen zeigen. Deshalb zeigt arr1[0] auch den neuen Wert.

Nuance bei Strings (string)

Strings in C# sind Referenztypen, verhalten sich aber wegen ihrer Unveränderlichkeit (immutability) etwas anders. Das bedeutet, dass ein String nach seiner Erstellung nicht mehr verändert werden kann. Jede Operation, die scheinbar den String "ändert" (z.B. Konkatenation, Replace()), erzeugt in Wirklichkeit einen neuen String im Speicher.


string str1 = "Hello";
string str2 = str1; // str2 zeigt auf dasselbe Objekt "Hello" wie str1

// str1 = "Hello", str2 = "Hello"
Console.WriteLine($"Anfangswerte: str1 = \"{str1}\", str2 = \"{str2}\""); 

str2 = "Bye"; // Hier wird ein NEUES Objekt "Bye" erstellt, und str2 zeigt jetzt darauf

// str1 = "Hello", str2 = "Bye"
Console.WriteLine($"Nach Änderung von str2: str1 = \"{str1}\", str2 = \"{str2}\""); 

Erklärung: Ursprünglich zeigten str1 und str2 auf dasselbe Objekt "Hello". Als str2 "Bye" zugewiesen wurde, hat C# das bestehende Objekt "Hello" nicht verändert. Stattdessen wurde ein neues Objekt "Bye" im Speicher erstellt, und str2 zeigt jetzt darauf. str1 zeigt weiterhin auf das alte Objekt "Hello". Das ist ein wichtiger Unterschied, der Anfänger oft verwirrt.

3. Tabelle der wichtigsten Unterschiede

Eigenschaft Werttyp
(z.B. struct, int)
Referenztyp
(z.B. class, string, Array)
Was wird kopiert Der Wert selbst ("Kopie" der Daten) Die Referenz auf das Objekt ("Adresse" im Speicher)
Beziehung zwischen Kopien Keine, Kopien sind komplett unabhängig. Änderung einer Kopie beeinflusst die andere nicht. Ja, alle Referenzen zeigen auf dasselbe Objekt. Änderungen am Objekt über eine Referenz sind für alle sichtbar.
Kann null sein? Nein (außer Nullable-Typen wie int?). Hat immer einen Wert. Ja. Kann auf "nichts" zeigen (null). Zugriff auf ein null-Objekt löst NullReferenceException aus.
Wie wird deklariert struct sowie alle primitiven Typen (int, bool usw.), enum class, interface, delegate, array, string, object
Wie wird gelöscht Wird automatisch vom Stack entfernt, wenn der Gültigkeitsbereich verlassen wird. Wird vom Garbage Collector gelöscht, wenn keine Referenzen mehr darauf zeigen.

4. Beispiel in einer Anwendung

Stell dir vor, wir entwickeln eine einfache Konsolenanwendung für einen Benutzerfragebogen. Wir haben eine Struktur für Prüfungspunkte und eine Klasse für das Benutzerprofil.

// Werttyp: Struktur zur Speicherung von Punkten
struct Score
{
    public int Points;
    public string Grade; // Zur Veranschaulichung hinzugefügt
}

// Referenztyp: Klasse für das Benutzerprofil
class User
{
    public string Name;
    public Score ExamScore; // Eingebettete Struktur
}

Kopieren einer Struktur (Score)

Score score1 = new Score { Points = 100, Grade = "A" };
Score score2 = score1; // Der gesamte Inhalt von score1 wird in score2 kopiert
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=100, Grade=A

score2.Points = 88;
score2.Grade = "B";

Console.WriteLine("--- Nach Änderung von score2 ---");
Console.WriteLine($"score1: Points={score1.Points}, Grade={score1.Grade}"); // score1: Points=100, Grade=A (unverändert!)
Console.WriteLine($"score2: Points={score2.Points}, Grade={score2.Grade}"); // score2: Points=88, Grade=B

Ergebnis: score1 ist unverändert geblieben. Das liegt daran, dass bei Score score2 = score1; der gesamte Inhalt von score1 (alle Felder) in score2 kopiert wurde. Beide Variablen haben jetzt ihre eigenen unabhängigen Datensätze.

Kopieren einer Klasse (User)

User u1 = new User 
{ 
    Name = "Anna", 
    ExamScore = new Score { Points = 95, Grade = "A" } 
};
User u2 = u1; // Jetzt zeigen u2 und u1 auf EIN UND DENSELBEN Benutzer im Speicher!
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}"); // u1: Name=Anna, Score=95
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}"); // u2: Name=Anna, Score=95

u2.Name = "Jonas"; // Wir ändern den Namen über u2
u2.ExamScore.Points = 60; // Wir ändern die Punkte über u2
u2.ExamScore.Grade = "C";

Console.WriteLine("--- Nach Änderung von u2 ---");
Console.WriteLine($"u1: Name={u1.Name}, Score={u1.ExamScore.Points}, Grade={u1.ExamScore.Grade}"); // u1: Name=Jonas, Score=60, Grade=C
Console.WriteLine($"u2: Name={u2.Name}, Score={u2.ExamScore.Points}, Grade={u2.ExamScore.Grade}"); // u2: Name=Jonas, Score=60, Grade=C

Ergebnis: u1 hat sich auch geändert! Das liegt daran, dass u1 und u2 ursprünglich auf dasselbe User-Objekt im Speicher gezeigt haben. Wenn wir Eigenschaften des Objekts über u2 ändern (z.B. u2.Name = "Jonas";), ändern wir tatsächlich das Objekt selbst. Deshalb sehen wir beim Zugriff auf u1.Name schon den neuen Wert. Sogar die eingebettete Struktur ExamScore hat sich für u1 geändert, weil sie Teil des User-Objekts ist, auf das beide Referenzen zeigen.

5. Was passiert bei der Übergabe an Methoden

Zu verstehen, wie Typen an Methoden übergeben werden, ist entscheidend für das vorhersehbare Verhalten deines Programms.

Übergabe eines Werttyps (Value Type) an eine Methode

Beim Übergeben eines Werttyps an eine Methode wird standardmäßig per Wert übergeben. Das bedeutet, die Methode bekommt eine Kopie der Ursprungsvariablen. Änderungen innerhalb der Methode an dieser Kopie haben keinen Einfluss auf das Original außerhalb der Methode.

void AddTen(int x)
{
    Console.WriteLine($"Innerhalb der Methode (vor Änderung): x = {x}"); // Innerhalb der Methode (vor Änderung): x = 5
    x = x + 10; // x ist jetzt 15, aber das ist eine lokale Kopie
    Console.WriteLine($"Innerhalb der Methode (nach Änderung): x = {x}"); // Innerhalb (nach Änderung): x = 15
    // Diese lokale Kopie 'x' "stirbt" nach Verlassen der Methode.
}

int num = 5;
Console.WriteLine($"Vor Methodenaufruf: num = {num}"); // Vor Methodenaufruf: num = 5
AddTen(num);
Console.WriteLine($"Nach Methodenaufruf: num = {num}"); // Nach Methodenaufruf: num = 5 (unverändert!)

Ergebnis: Die Variable num ist unverändert geblieben (5). x innerhalb von AddTen ist eine komplett eigene Variable, die mit einer Kopie des Werts von num initialisiert wurde.

Übergabe eines Referenztyps (Reference Type) an eine Methode

Auch beim Übergeben eines Referenztyps an eine Methode wird standardmäßig per Wert übergeben, aber es wird der Wert der Referenz kopiert, nicht das Objekt selbst. Das bedeutet, dass du innerhalb der Methode eine Kopie der "Visitenkarte" (Adresse) auf das Originalobjekt bekommst. Beide Referenzen (die originale und die in der Methode) zeigen auf dasselbe Objekt im Speicher.

void RenameUser(User u)
{
    // Innerhalb der Methode (vor Änderung): u.Name = "Olga"
    Console.WriteLine($"Innerhalb der Methode (vor Änderung): u.Name = \"{u.Name}\""); 
    u.Name = "Neuer Name"; // Wir ändern die Eigenschaft des Objekts, auf das 'u' zeigt
    // Innerhalb der Methode (nach Änderung): u.Name = "Neuer Name"
    Console.WriteLine($"Innerhalb der Methode (nach Änderung): u.Name = \"{u.Name}\""); 
}

User user = new User { Name = "Olga" };
// Vor Methodenaufruf: user.Name = "Olga"
Console.WriteLine($"Vor Methodenaufruf: user.Name = \"{user.Name}\""); 
RenameUser(user); 
// Nach Methodenaufruf: user.Name = "Neuer Name"
Console.WriteLine($"Nach Methodenaufruf: user.Name = \"{user.Name}\""); 

Ergebnis: Der Name des Benutzers wurde zu "Neuer Name" geändert. Die Methode RenameUser hat eine Kopie der Referenz auf das user-Objekt bekommen. Über diese Referenz konnte die Methode auf das Originalobjekt im Heap zugreifen und dessen Eigenschaft Name ändern.

Wichtiger Zusatz: Was passiert, wenn wir innerhalb der Methode der übergebenen Referenzvariable einen neuen Objektwert zuweisen?

void ReassignUser(User u)
{
    u = new User { Name = "Ganz neuer Benutzer" }; // 'u' zeigt jetzt auf ein neues Objekt
    Console.WriteLine($"Innerhalb der Methode (nach Neuzuweisung): u.Name = \"{u.Name}\"");
}

User originalUser = new User { Name = "Originaler Benutzer" };
Console.WriteLine($"Vor Aufruf von ReassignUser: originalUser.Name = \"{originalUser.Name}\"");
ReassignUser(originalUser); 
Console.WriteLine($"Nach Aufruf von ReassignUser: originalUser.Name = \"{originalUser.Name}\""); // "Originaler Benutzer" - unverändert!

Ergebnis: originalUser ist unverändert geblieben! Das liegt daran, dass ReassignUser eine Kopie der Referenz bekommen hat. Als u = new User(...) innerhalb der Methode ausgeführt wurde, zeigte die lokale Variable u auf ein komplett neues Objekt. Die Originalreferenz originalUser zeigt weiterhin auf das alte Objekt. Das ist ein sehr wichtiger Punkt!

6. "Anfängertränen": typische Fehler und ihre Ursachen

Referenztypen und Werttypen zu verstehen, kann für Anfänger ganz schön tricky sein. Hier ein paar typische Fehler und Missverständnisse:

Verwirrung beim Kopieren von Arrays: Anfänger erwarten oft, dass bei arr2 = arr1; eine unabhängige Kopie des Arrays erstellt wird. In Wirklichkeit sind das aber nur zwei Referenzen auf dasselbe Array. Das ist wie zwei Controller für dasselbe Spiel: Was du an einem Controller drückst, wirkt sich auf das Spiel aus und ist auch am anderen Controller sichtbar. Um eine unabhängige Kopie eines Arrays zu erstellen, musst du es explizit klonen (z.B. int[] arr2 = (int[])arr1.Clone(); oder Methoden wie Copy verwenden).

Erwartung, dass Strings sich "per Referenz" ändern: Weil string ein Referenztyp ist, denken manche, er verhält sich wie ein Array beim Ändern. Aber wegen der Unveränderlichkeit von Strings erzeugt jede scheinbare Änderung tatsächlich ein neues String-Objekt. Das führt oft zu unerwarteten Ergebnissen und kann ineffizient sein, wenn du viele String-Operationen in einer Schleife machst (dafür besser StringBuilder verwenden).

Vergessen von null: Werttypen (außer Nullable-Typen wie int?, bool?) haben immer einen Wert und können nie null sein. Referenztypen können null sein, also auf kein Objekt zeigen. Der Versuch, auf ein Objekt zuzugreifen, das null ist, führt zur berüchtigten NullReferenceException. Prüfe Referenzvariablen immer auf null, wenn sie eventuell nicht gesetzt sein könnten.

Klassen statt Strukturen für kleine Daten verwenden: Aus Gewohnheit deklarieren viele alles als Klasse. Für kleine, einfache Datensätze, die ein Ganzes darstellen (z.B. Punkt Point { X, Y }, Farbe Color { R, G, B }), können Strukturen performanter sein, weil sie im Stack gespeichert und per Wert kopiert werden, was den Overhead für den Garbage Collector reduziert. Strukturen sollten aber immutable, klein und ohne Referenztypen sein, die selbst null sein oder ihren Zustand ändern können.

2
Aufgabe
C# SELF, Level 12, Lektion 3
Gesperrt
Das Verhalten von Arrays als Referenztyp
Das Verhalten von Arrays als Referenztyp
2
Aufgabe
C# SELF, Level 12, Lektion 3
Gesperrt
Interaktion mit Methoden für Wert- und Referenztypen
Interaktion mit Methoden für Wert- und Referenztypen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION