C#-Grundlagen Teil 2 – Typen in C# verstehen

Value Types, Reference Types, Nullable Reference Types und warum decimal für Geld gemacht ist: die Grundlagen des C#-Typsystems – mit Übungsaufgaben zum Selbermachen.

This article has not been translated yet. You are viewing the original German version.

Read in German →

C#

C#-Grundlagen Teil 2 – Typen in C# verstehen

Marcel Strahl5 min read

C#-Grundlagen Teil 2 – Typen in C# verstehen

Im ersten Teil ging es darum, warum C# und .NET besser sind als ihr alter Ruf.

Jetzt geht es richtig los. Und zwar da, wo jede Sprache anfängt: bei den Typen.

C# ist statisch typisiert – der Compiler kennt jeden Typ und meckert vor dem Start, nicht erst in Produktion. Und C# hat ein Feature, das im Grundlagen-Kontext fast immer unterschlagen wird, obwohl es Geld spart: einen eingebauten Typ für exakte Dezimalzahlen.


Was ist überhaupt ein Typ?

Ein Typ sagt dem Compiler, was ein Wert ist und was man damit machen darf.

42 ist eine Zahl. Damit kann man rechnen. "42" ist Text, der nur zufällig aussieht wie eine Zahl.

Zum Ausprobieren reicht ein Online-Playground wie dotnetfiddle.net – oder lokal dotnet run mit einer einzigen Datei.

Console.WriteLine(42.GetType());   // System.Int32
Console.WriteLine("42".GetType()); // System.String
Console.WriteLine(4.2.GetType());  // System.Double
Console.WriteLine(true.GetType()); // System.Boolean

GetType() zeigt dir jederzeit, was ein Wert wirklich ist. Unsicher? Reinschauen. Fertig.


Die wichtigsten Basis-Typen

int – Ganzzahlen

int alter = 34;
int temperatur = -5;
int jahr = 2026;

Ganze Zahlen von etwa -2,1 Milliarden bis +2,1 Milliarden. Reicht das nicht, nimmt man long.

double – Kommazahlen

double preis = 19.99;
double pi = 3.14159;

Wichtig: Der Dezimaltrenner ist ein Punkt, kein Komma – auch wenn wir im Deutschen „Kommazahl“ sagen.

Doubles haben außerdem eine fiese Falle eingebaut – dazu gleich mehr, denn C# hat dafür eine Antwort, die andere Sprachen nicht haben.

decimal – die Geld-Waffe

Und hier kommt der Typ, den Umsteiger aus PHP, Java oder JavaScript nicht kennen:

decimal preis = 19.99m; // das m-Suffix macht den Unterschied
decimal summe = 0.1m + 0.2m;
 
Console.WriteLine(summe == 0.3m); // true (!)

decimal rechnet dezimal statt binär – 0.1 + 0.2 ist damit wirklich exakt 0.3. Dafür ist er langsamer und hat einen kleineren Wertebereich als double. Die Faustregel:

  • Geld, Rechnungen, alles Kaufmännische → decimal
  • Wissenschaft, Grafik, Messwerte → double

bool – Wahr oder Falsch

bool istAngemeldet = true;
bool hatBezahlt = false;

Zwei mögliche Werte: true oder false. Kein „truthy“, kein „falsy“ – if (1) kompiliert in C# nicht.

string und char – Text und Zeichen

string name = "Marcel";
char note = 'A'; // einfache Anführungszeichen, genau ein Zeichen
 
string begruessung = $"Hallo {name}"; // String-Interpolation mit $

Das $ vor dem String aktiviert Interpolation – Ausdrücke in {...} werden eingesetzt. Verkettung mit + braucht man damit fast nie.

Gute Nachricht für alle, die aus Java kommen: == vergleicht bei Strings in C# den Inhalt. Die equals()-Falle gibt es hier nicht.


Value Types vs. Reference Types

Jetzt zum Konzept hinter dem C#-Typsystem – dem einen Unterschied, der vieles erklärt:

Value Types (int, double, bool, decimal, structs): Die Variable enthält den Wert selbst. Zuweisung kopiert.

Reference Types (string, Klassen, Arrays, Listen): Die Variable enthält einen Verweis auf das Objekt. Zuweisung kopiert nur den Verweis.

// Value Type: Kopie
int a = 1;
int b = a;
b = 2;
Console.WriteLine(a); // 1 – unverändert
 
// Reference Type: geteiltes Objekt
var listeA = new List<int> { 1 };
var listeB = listeA;
listeB.Add(2);
Console.WriteLine(listeA.Count); // 2 (!) – beide zeigen auf dieselbe Liste

Der zweite Fall überrascht jeden Anfänger genau einmal. Danach weiß man, warum der Unterschied ein eigenes Kapitel verdient.


null und Nullable Types

null bedeutet: Hier ist bewusst kein Wert.

Value Types können von Haus aus nicht null sein – dafür gibt es das Fragezeichen:

int? alter = null;     // int ODER null
decimal? rabatt = null;

Und seit C# 8 gilt dasselbe Prinzip auch für Reference Types – die Nullable Reference Types:

string name = "Marcel";     // Compiler geht davon aus: nie null
string? mittlererName = null; // explizit: darf null sein
 
// Console.WriteLine(mittlererName.Length);  // Compiler-Warnung!
Console.WriteLine(mittlererName?.Length ?? 0); // 0 – sicher
  • ?. – Zugriff nur, wenn nicht null
  • ?? – Fallback-Wert, falls null

In neuen Projekten ist das standardmäßig aktiv. Damit rückt C# nah an Kotlins Null-Safety heran – als Warnung statt als harter Fehler, aber die Denkweise ist dieselbe: Wer null sein kann, muss es im Typ zugeben.


Collections – Listen und Dictionaries

var sprachen = new List<string> { "C#", "PHP", "TypeScript" };
Console.WriteLine(sprachen[0]); // C#
 
var user = new Dictionary<string, string>
{
    ["name"] = "Marcel",
    ["beruf"] = "Software-Entwickler",
};
Console.WriteLine(user["name"]); // Marcel

Für strukturierte Daten nimmt man aber besser gleich ein Record:

public record Profil(string Name, int Alter, List<string> Hobbys);
 
var profil = new Profil("Marcel", 34, ["Programmieren", "Lesen", "Gaming"]);
Console.WriteLine(profil.Name); // Marcel

var und Typen in Methoden

var lässt den Compiler den Typ herleiten – statisch bleibt es trotzdem:

var alter = 34;      // int, steht zur Compile-Zeit fest
var name = "Marcel"; // string
// name = 42;        // Compilerfehler

Und in Methoden sind Parameter- und Rückgabetypen Pflicht:

decimal BerechnePreisMitSteuer(decimal nettoPreis, decimal steuersatz = 0.19m)
{
    return nettoPreis * (1 + steuersatz);
}
 
string? FindeBenutzername(int id)
{
    // string? bedeutet: string ODER null
    return id == 1 ? "marcel" : null;
}

Was man dafür bekommt:

  • Der Code dokumentiert sich selbst
  • Die IDE kann einem wirklich helfen
  • Falsche Aufrufe scheitern beim Kompilieren, nicht beim Kunden

Aufgaben

Genug Theorie.

Am meisten hängen bleibt, wenn man es selbst tippt. dotnetfiddle.net auf – oder lokal ein Projekt mit dotnet new console.

Aufgabe 1: Typen raten

Was gibt C# hier jeweils aus? Erst überlegen, dann ausführen.

Console.WriteLine(10 / 3);
Console.WriteLine(10 / 3.0);
Console.WriteLine("5" + 3);
Console.WriteLine(0.1 + 0.2 == 0.3);
Console.WriteLine(0.1m + 0.2m == 0.3m);

Aufgabe 2: Steckbrief

Lege ein record Profil(string Name, int Alter, List<string> Hobbys, string? Beruf) an. Erzeuge eine Instanz und gib einen Satz aus wie: Marcel ist 34 und hat 3 Hobbys.

Tipp: Hobbys.Count zählt die Einträge, $"..." baut den Satz.

Aufgabe 3: Referenzen erleben

Erzeuge eine List<int>, weise sie einer zweiten Variablen zu und füge über die zweite Variable ein Element hinzu. Wie viele Elemente hat die erste Liste danach – und warum?

Aufgabe 4: Der Geld-Klassiker

Ein Kunde kauft drei Artikel zu je 0.10 €. Prüfe mit ==, ob die Summe 0.30 ergibt – einmal mit double, einmal mit decimal. Was fällt auf?

Aufgabe 5: Typ-Detektiv

Schreibe eine Methode string BeschreibeTyp(object? wert), die für einen beliebigen Wert einen Satz zurückgibt, z. B. "Das ist ein Int32 mit dem Wert 42".

Tipp: wert.GetType().Name liefert den Typnamen – oder du löst es elegant mit Pattern Matching (switch-Expression). Denk an den null-Fall!


Lösungsvorschläge

Erst selbst probieren. Ehrlich. Dann vergleichen.

Lösung zu Aufgabe 1
Console.WriteLine(10 / 3);              // 3 – int / int bleibt int, der Rest wird abgeschnitten
Console.WriteLine(10 / 3.0);            // 3.3333333333333335 – sobald ein double dabei ist, wird double gerechnet
Console.WriteLine("5" + 3);             // "53" – String + Zahl wird zur Verkettung
Console.WriteLine(0.1 + 0.2 == 0.3);    // False – die klassische double-Falle
Console.WriteLine(0.1m + 0.2m == 0.3m); // True  – decimal rechnet dezimal, also exakt

Die letzten beiden Zeilen nebeneinander sind das ganze Argument für decimal.

Lösung zu Aufgabe 2
public record Profil(string Name, int Alter, List<string> Hobbys, string? Beruf);
 
var profil = new Profil(
    Name: "Marcel",
    Alter: 34,
    Hobbys: ["Programmieren", "Lesen", "Gaming"],
    Beruf: "Software-Entwickler"
);
 
Console.WriteLine($"{profil.Name} ist {profil.Alter} und hat {profil.Hobbys.Count} Hobbys.");
Lösung zu Aufgabe 3
var listeA = new List<int> { 1 };
var listeB = listeA;
 
listeB.Add(2);
 
Console.WriteLine(listeA.Count); // 2

List<T> ist ein Reference Type: listeB = listeA kopiert nur den Verweis, nicht die Liste. Beide Variablen zeigen auf dasselbe Objekt – wer über die eine ändert, ändert auch die andere Sicht darauf.

Lösung zu Aufgabe 4
double summeDouble = 0.10 + 0.10 + 0.10;
Console.WriteLine(summeDouble == 0.30); // False – Rundungsfehler
Console.WriteLine(summeDouble);         // 0.30000000000000004
 
decimal summeDecimal = 0.10m + 0.10m + 0.10m;
Console.WriteLine(summeDecimal == 0.30m); // True – decimal ist für genau diesen Fall gebaut

In PHP oder Java lautet der Rat: Geld in Cent als Integer rechnen. In C# gibt es zusätzlich den Luxus-Weg: decimal.

Lösung zu Aufgabe 5
string BeschreibeTyp(object? wert) => wert switch
{
    null => "Das ist null – hier ist bewusst kein Wert",
    _ => $"Das ist ein {wert.GetType().Name} mit dem Wert {wert}",
};
 
Console.WriteLine(BeschreibeTyp(42));    // Das ist ein Int32 mit dem Wert 42
Console.WriteLine(BeschreibeTyp("42"));  // Das ist ein String mit dem Wert 42
Console.WriteLine(BeschreibeTyp(true));  // Das ist ein Boolean mit dem Wert True
Console.WriteLine(BeschreibeTyp(null));  // Das ist null – hier ist bewusst kein Wert

Int32 statt int? Die C#-Schlüsselwörter sind nur Aliasse: int ist System.Int32, string ist System.String. GetType() zeigt den echten Namen.


Zusatz: Warum double überhaupt „falsch“ rechnet

Die Falle aus Aufgabe 4 ist keine C#-Eigenheit:

Console.WriteLine(0.1 + 0.2 == 0.3); // False

Doubles sind intern binär gespeichert, und 0.1 ist binär ein unendlicher Bruch – wie 1/3 im Dezimalsystem. Das betrifft praktisch jede Sprache.

Der Unterschied: C# hat mit decimal eine eingebaute Antwort. Merksatz fürs Leben:

Kaufmännisch rechnen → decimal. Naturwissenschaftlich rechnen → double.


Wie geht es weiter?

Das waren die Bausteine, aus denen jedes C#-Programm besteht.

Im nächsten Teil geht es um Kontrollstrukturen – if, switch Expressions mit Pattern Matching, Schleifen. Also darum, wie ein Programm Entscheidungen trifft.