1. Der Operator is: Typkompatibilität prüfen
In echten Anwendungen hast du oft eine Referenz auf ein Objekt der Basisklasse, weißt aber (oder vermutest), dass das tatsächliche Objekt im Speicher ein spezifischerer abgeleiteter Typ ist. In solchen Fällen willst du vielleicht auf die einzigartigen Member dieses abgeleiteten Typs zugreifen. Für solche Situationen gibt es in C# ein paar wichtige Tools: die Operatoren is, as und – besonders in modernen Sprachversionen – die mächtigen Pattern Matching-Features.
is prüft, ob ein Objekt eine Instanz eines bestimmten Typs ist, von diesem Typ erbt oder ein bestimmtes Interface implementiert. Er gibt true zurück, wenn das Objekt kompatibel ist, und false sonst.
Hauptanwendung: Herausfinden, ob man ein Objekt sicher auf einen bestimmten Typ casten kann.
Beispiel: Basistyp und Vererbung prüfen
class Animal { public string Species { get; set; } = "Unbekannt"; }
class Dog : Animal { public void Bark() => Console.WriteLine("Wuff!"); }
class Cat : Animal { public void Meow() => Console.WriteLine("Miau!"); }
Animal myAnimal = new Dog { Species = "Golden Retriever" };
Console.WriteLine($"myAnimal is Animal: {myAnimal is Animal}"); // True
Console.WriteLine($"myAnimal is Dog: {myAnimal is Dog}"); // True
Console.WriteLine($"myAnimal is Cat: {myAnimal is Cat}"); // False
In diesem Beispiel ist myAnimal tatsächlich ein Dog. Deshalb liefern myAnimal is Animal und myAnimal is Dog true.
Beispiel: Prüfung auf null
Der Operator is verhält sich auch mit null vorhersehbar.
Animal nullAnimal = null;
Dog specificDog = new Dog();
Console.WriteLine($"nullAnimal is Animal: {nullAnimal is Animal}"); // False (null ist keine Instanz irgendeines Typs)
Console.WriteLine($"specificDog is null: {specificDog is null}"); // False (Objekt ist nicht null)
Beachte: null ist keine Instanz irgendeines Typs, daher ist null is MyType immer false. Aber someObject is null ist ein praktischer und sicherer Weg, um zu prüfen, ob eine Referenz null ist.
Beispiel: is mit Interfaces
Der Operator is funktioniert auch, um zu prüfen, ob ein Interface implementiert wird.
interface IFlyable { void Fly(); }
class Bird : Animal, IFlyable { public void Fly() => Console.WriteLine("Flatter flatter!"); }
class Fish : Animal { }
Animal creature = new Bird();
Console.WriteLine($"creature is Bird: {creature is Bird}"); // True
Console.WriteLine($"creature is IFlyable: {creature is IFlyable}"); // True
creature = new Fish();
Console.WriteLine($"creature is IFlyable: {creature is IFlyable}"); // False
2. Der Operator as: sicheres Casten
Der Operator as ist für das sichere Konvertieren eines Objekts zu einem bestimmten Typ gedacht. Im Gegensatz zum expliziten Cast (Type)obj, der bei Fehlschlag eine InvalidCastException wirft, gibt as null zurück, wenn das Casten nicht möglich ist. Das ist ideal, wenn du dir beim Typ nicht sicher bist.
Hauptanwendung: Versuch, einen Typ zu casten, und bei Fehlschlag null bekommen.
Beispiel: Grundlegende Nutzung von as
class Shape { }
class Circle : Shape { public double Radius { get; set; } }
class Square : Shape { public double Side { get; set; } }
Shape myShape = new Circle { Radius = 5.0 };
// Versuch, zu Circle zu casten
Circle circle = myShape as Circle;
if (circle != null) // Immer auf null prüfen!
{
Console.WriteLine($"Das ist ein Kreis mit Radius: {circle.Radius}"); // Ausgabe: Das ist ein Kreis mit Radius: 5
}
// Versuch, zu Square zu casten
Square square = myShape as Square;
if (square == null) // Casten fehlgeschlagen, square == null
{
Console.WriteLine("Das ist kein Quadrat."); // Ausgabe: Das ist kein Quadrat.
}
Wie du siehst, hilft as dabei, Laufzeitfehler zu vermeiden, indem es null statt einer Exception zurückgibt.
Beispiel: Einschränkungen von as
Wichtig: Der Operator as funktioniert nur mit Referenztypen und nullable Value Types. Für normale Value Types geht das nicht, weil sie kein null annehmen können.
object someValue = 100;
int num = someValue as int; // KOMPILIERFEHLER: 'as' geht nicht mit nicht-nullbaren Typen
int? nullableNum = someValue as int?; // Ok, nullable int
Console.WriteLine($"Nullable int: {nullableNum}"); // Ausgabe: Nullable int: 100
string str = someValue as string; // Gibt null zurück, weil 100 kein string ist
Console.WriteLine($"String from int: {str ?? "null"}"); // Ausgabe: String from int: null
Für nicht-nullbare Value Types nutze entweder explizites Casten ((int)someValue), wenn du dir sicher bist (und Exceptions akzeptierst), oder – besser – Pattern Matching.
3. Pattern Matching
Pattern Matching ist ein mächtiges und modernes Feature, das elegantere und sicherere Wege bietet, Typen zu prüfen und Daten aus Objekten zu extrahieren. Es spart viel Boilerplate-Code und macht alles lesbarer.
Type Pattern mit is
Das ist die häufigste Form von Pattern Matching: Du prüfst den Typ eines Objekts und, wenn es passt, weist du es direkt einer neuen Variablen zu.
Syntax: Ausdruck is Typ variable
Beispiel: is + Cast ersetzen
// Wir bleiben bei Shape, Circle, Square
Shape currentShape = new Circle { Radius = 7.5 };
// Alter, umständlicher Weg:
if (currentShape is Circle)
{
Circle c = (Circle)currentShape;
Console.WriteLine($"Alter Weg: Kreis mit Radius {c.Radius}");
}
// Neuer, eleganter Weg mit type pattern
if (currentShape is Circle c) // Typ prüfen und Variable 'c' erzeugen
{
Console.WriteLine($"Neuer Weg: Kreis mit Radius {c.Radius}"); // 'c' ist schon vom Typ Circle
}
Shape anotherShape = new Square { Side = 10.0 };
if (anotherShape is Square s)
{
Console.WriteLine($"Das ist ein Quadrat mit Seite {s.Side}");
}
Die Variable c (oder s) ist nur im if-Block sichtbar – das verhindert versehentliche Nutzung außerhalb des richtigen Typs.
Property Pattern
Ab C# 8.0 kannst du nicht nur den Typ prüfen, sondern gleichzeitig Werte von einem oder mehreren Properties checken und sie in neue Variablen extrahieren.
Syntax: Ausdruck is Typ { Property1: value_pattern, Property2: extraction_variable }
Beispiel: Property prüfen
Shape testShape = new Circle { Color = "Grün", Radius = 12.0 };
// Prüfen, ob das Objekt ein Kreis ist und die Farbe grün hat
if (testShape is Circle { Color: "Grün" })
{
Console.WriteLine("Grüner Kreis gefunden.");
}
// Prüfen, ob das Objekt ein Kreis ist und gleichzeitig den Radius extrahieren
if (testShape is Circle { Radius: var r })
{
Console.WriteLine($"Radius extrahiert: {r}");
}
// Kombinieren: prüfen und extrahieren
if (testShape is Circle { Color: "Grün", Radius: var radiusVal } circleObj)
{
Console.WriteLine($"Grüner Kreis extrahiert. Radius: {radiusVal}, Objekt: {circleObj.Radius}");
}
Das Property Pattern macht komplexe Bedingungen viel einfacher.
Beispiel: Bereiche und logische Operatoren in Property Patterns
Jetzt kannst du prüfen, ob Properties bestimmte Bedingungen erfüllen:
Circle bigCircle = new Circle { Radius = 25.0, Color = "Blau" };
// Prüfen: Radius größer 20 UND Farbe blau
if (bigCircle is Circle { Radius: > 20, Color: "Blau" })
{
Console.WriteLine("Großer blauer Kreis gefunden.");
}
// Prüfen: Radius im Bereich (5..15)
if (testShape is Circle { Radius: >= 5 and <= 15 })
{
Console.WriteLine("Kreis mittlerer Größe.");
}
Wichtig! Anders als bei bool nutzt du hier die Schlüsselwörter: and, or und not.
4. Switch Expressions und Switch Statements mit Pattern Matching
Die größte Flexibilität und Lesbarkeit zeigt Pattern Matching in switch-Anweisungen. Damit kannst du verschiedene Logik je nach passendem Pattern ausführen.
Beispiel: Switch Statement
Damit kannst du für jedes Pattern einen eigenen Codeblock definieren.
// Klassen Animal, Dog, Cat wie am Anfang der Vorlesung
Animal currentCreature = new Dog { Species = "Pudel" };
switch (currentCreature)
{
case Dog d: // Type Pattern: wenn Dog, dann Variable 'd'
d.Bark();
Console.WriteLine($"Das ist ein Hund der Rasse {d.Species}.");
break;
case Cat c when c.Species == "Siam": // Type Pattern mit Bedingung (when clause)
c.Meow();
Console.WriteLine($"Das ist eine Siamkatze.");
break;
case Animal a: // Wenn einfach Animal (nicht Dog/Cat)
Console.WriteLine($"Das ist einfach ein Tier der Art {a.Species}.");
break;
case null: // Null-Pattern für null-Werte
Console.WriteLine("Objekt ist null.");
break;
default: // Wenn kein Pattern passt
Console.WriteLine("Unbekanntes Wesen.");
break;
}
Die Reihenfolge der Cases im switch ist wichtig: spezifischere Patterns zuerst, dann allgemeinere.
Beispiel: Switch Expression
Das ist eine kompaktere Syntax für switch, die einen Wert zurückgibt. Perfekt, um ein Objekt je nach Typ oder Property in etwas anderes umzuwandeln.
Syntax: Ausdruck switch { pattern1 => result1, pattern2 => result2, ... _ => default_result }
Shape processShape = new Rectangle { Width = 5, Height = 5, Color = "Rot" };
string shapeInfo = processShape switch
{
Circle { Radius: var r } when r > 10 => $"Großer Kreis (R={r})", // Property Pattern mit Bedingung
Circle { Color: "Blau" } c => $"Blauer Kreis (R={c.Radius})", // Property Pattern mit Extraktion
Circle c => $"Normaler Kreis (R={c.Radius})", // Einfaches Type Pattern
Rectangle { Width: var w, Height: var h } when w == h => $"Quadrat ({w}x{h})", // Quadrat
Rectangle r => $"Rechteck ({r.Width}x{r.Height})", // Sonstiges Rechteck
null => "Figur existiert nicht (null)", // Null-Pattern
_ => "Unbekannte Figur" // Discard-Pattern (_) statt 'default'
};
Console.WriteLine(shapeInfo); // Ausgabe: Quadrat (5x5)
switch expression ist super praktisch für kurze, ausdrucksstarke Umwandlungen.
Var Pattern (var Pattern)
Ab C# 7.0 kannst du var im Pattern Matching nutzen. var matched immer jedes Objekt (außer null) und extrahiert es in eine Variable des passenden Typs.
Beispiel: var Pattern nutzen
object obj = "Hallo Welt";
if (obj is var result) // Immer true, result ist string
{
Console.WriteLine($"Typ: {result.GetType().Name}, Wert: {result}");
}
obj = 123;
string typeName = obj switch
{
var x when x is int => "Das ist eine ganze Zahl",
var y when y is string => "Das ist ein String",
_ => "Irgendwas anderes"
};
Console.WriteLine(typeName); // Ausgabe: Das ist eine ganze Zahl
Das var Pattern wird selten allein für Typprüfungen genutzt, ist aber sehr praktisch in anderen Patterns oder im switch zum Extrahieren von Werten.
5. is vs as vs Pattern Matching: Wann was?
Welches Tool du nimmst, hängt von deiner Aufgabe und der C#-Version ab.
Operator is (einfach):
Verwendung: Wenn du einfach nur prüfen willst, ob ein Objekt ein bestimmter Typ ist und danach nicht sofort casten oder spezielle Member nutzen musst.
if (myObject is SomeType)
Operator as:
Verwendung: Wenn du einen Referenztyp (oder nullable Value Type) casten willst und Fehlschläge ohne Exception behandeln möchtest, indem du auf null prüfst.
MyDerivedClass derived = myBaseObject as MyDerivedClass;
if (derived != null) { /* ... */ }
Pattern Matching (is Type variable):
Moderner, bevorzugter Ansatz: Kombiniert Typprüfung und sicheres Casten in einer, viel lesbareren Zeile. Funktioniert für alle Typen.
Verwendung: Wenn du Typ prüfen und direkt spezielle Member dieses Typs nutzen willst.
if (myObject is MyDerivedClass derived) { derived.SpecificMethod(); }
Pattern Matching (switch expressions / statements):
Moderner, bevorzugter Ansatz: Wenn du verschiedene Aktionen oder Werte je nach Typ, Property oder anderen Merkmalen brauchst. Viel sauberer als lange if-else if-Ketten.
Verwendung: Polymorphes Verhalten oder komplexe Auswahl-Logik je nach Objektmerkmalen.
string GetShapeInfo(Shape s) => s switch
{
Circle c => $"Kreis R={c.Radius}",
Rectangle r => $"Rechteck W={r.Width} H={r.Height}",
_ => "Unbekannt"
};
GO TO FULL VERSION