1. Operator is: sprawdzanie kompatybilności typów
W prawdziwych aplikacjach często masz referencję do obiektu bazowego typu, ale wiesz lub podejrzewasz, że faktyczny obiekt w pamięci to bardziej specyficzny, pochodny typ. W takich sytuacjach możesz chcieć dostać się do unikalnych członków tego pochodnego typu. Do bezpiecznej i wygodnej pracy z takimi przypadkami w C# masz kilka kluczowych narzędzi: operatory is, as i, co szczególnie ważne w nowoczesnych wersjach języka, potężne możliwości pattern matching.
is pozwala sprawdzić, czy obiekt jest instancją wskazanego typu, jego pochodnego typu lub implementuje dany interfejs. Zwraca true, jeśli obiekt jest kompatybilny z tym typem, i false w przeciwnym razie.
Główne zastosowanie: sprawdzić, czy można bezpiecznie rzutować obiekt na określony typ.
Przykład: Sprawdzanie typu bazowego i dziedziczenia
class Animal { public string Species { get; set; } = "Nieznany"; }
class Dog : Animal { public void Bark() => Console.WriteLine("Hau!"); }
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
W tym przykładzie myAnimal faktycznie jest Dog. Dlatego myAnimal is Animal i myAnimal is Dog zwrócą true.
Przykład: Sprawdzanie null
Operator is zachowuje się przewidywalnie z null.
Animal nullAnimal = null;
Dog specificDog = new Dog();
Console.WriteLine($"nullAnimal is Animal: {nullAnimal is Animal}"); // False (null nie jest instancją żadnego typu)
Console.WriteLine($"specificDog is null: {specificDog is null}"); // False (obiekt nie jest null)
Zwróć uwagę, że null nie jest instancją żadnego typu, więc null is MyType zawsze false. Jednak someObject is null — to wygodny i bezpieczny sposób na sprawdzenie, czy referencja null.
Przykład: Użycie is z interfejsami
Operator is działa też do sprawdzania implementacji interfejsów.
interface IFlyable { void Fly(); }
class Bird : Animal, IFlyable { public void Fly() => Console.WriteLine("Mach mach!"); }
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. Operator as: bezpieczne rzutowanie typów
Operator as służy do bezpiecznego rzutowania obiektu na wskazany typ. W przeciwieństwie do jawnego rzutowania (Type)obj, które rzuci InvalidCastException przy niepowodzeniu, as zwraca null, jeśli konwersja jest niemożliwa. To idealne rozwiązanie, gdy nie masz pewności co do faktycznego typu obiektu.
Główne zastosowanie: Spróbować rzutować typ, dostając null w razie niepowodzenia.
Przykład: Podstawowe użycie 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 };
// Próba rzutowania na Circle
Circle circle = myShape as Circle;
if (circle != null) // Obowiązkowe sprawdzenie na null!
{
Console.WriteLine($"To jest koło o promieniu: {circle.Radius}"); // Wyjście: To jest koło o promieniu: 5
}
// Próba rzutowania na Square
Square square = myShape as Square;
if (square == null) // Rzutowanie nieudane, square == null
{
Console.WriteLine("To nie jest kwadrat."); // Wyjście: To nie jest kwadrat.
}
Jak widać, as pozwala uniknąć potencjalnych błędów w czasie wykonania, zwracając null zamiast wyjątku.
Przykład: Ograniczenia as
Ważne: operator as działa tylko z typami referencyjnymi i nullable typami wartościowymi. Nie działa z normalnymi typami wartościowymi, bo one nie mogą przyjmować wartości null.
object someValue = 100;
int num = someValue as int; // BŁĄD KOMPILACJI: 'as' nie może być używany z nie-null typami
int? nullableNum = someValue as int?; // OK, nullable int
Console.WriteLine($"Nullable int: {nullableNum}"); // Wyjście: Nullable int: 100
string str = someValue as string; // Zwraca null, bo 100 to nie string
Console.WriteLine($"String from int: {str ?? "null"}"); // Wyjście: String from int: null
Dla nie-nullable typów wartościowych używaj jawnego rzutowania ((int)someValue), jeśli jesteś pewien typu (i jesteś gotów na wyjątek), albo, co lepsze, pattern matching.
3. Pattern Matching (Dopasowanie do wzorca)
Pattern matching to potężna i rozwijająca się funkcja, która daje bardziej eleganckie i bezpieczne sposoby sprawdzania typów i wyciągania danych z obiektów. Znacznie skraca powtarzalny kod i poprawia czytelność.
Type Pattern (Wzorzec typu) z is
To najczęściej używana forma pattern matching, pozwalająca sprawdzić typ obiektu i, jeśli się zgadza, od razu przypisać go do nowej zmiennej tego typu.
Składnia: wyrażenie is Typ zmienna
Przykład: Zamiana is + rzutowanie
// Kontynuujemy z klasami Shape, Circle, Square
Shape currentShape = new Circle { Radius = 7.5 };
// Stary, rozwlekły sposób:
if (currentShape is Circle)
{
Circle c = (Circle)currentShape;
Console.WriteLine($"Stary sposób: Koło o promieniu {c.Radius}");
}
// Nowy, elegancki sposób z type pattern
if (currentShape is Circle c) // Sprawdzenie typu i stworzenie zmiennej 'c'
{
Console.WriteLine($"Nowy sposób: Koło o promieniu {c.Radius}"); // 'c' już ma typ Circle
}
Shape anotherShape = new Square { Side = 10.0 };
if (anotherShape is Square s)
{
Console.WriteLine($"To jest kwadrat o boku {s.Side}");
}
Zmienna c (albo s) jest dostępna tylko w bloku if, co zapobiega przypadkowemu użyciu poza kontekstem właściwego typu.
Property Pattern (Wzorzec właściwości)
Od C# 8.0 możesz nie tylko sprawdzić typ, ale jednocześnie sprawdzić wartości jednego lub kilku właściwości obiektu, a także wyciągnąć je do nowych zmiennych.
Składnia: wyrażenie is Typ { Właściwość1: wzorzec_wartości, Właściwość2: zmienna_wyciągnięcia }
Przykład: Sprawdzanie właściwości
Shape testShape = new Circle { Color = "Zielony", Radius = 12.0 };
// Sprawdzamy, czy obiekt to koło i czy jego kolor to zielony
if (testShape is Circle { Color: "Zielony" })
{
Console.WriteLine("Znaleziono zielone koło.");
}
// Sprawdzamy, czy obiekt to koło i jednocześnie wyciągamy jego promień
if (testShape is Circle { Radius: var r })
{
Console.WriteLine($"Promień wyciągnięty: {r}");
}
// Łączymy sprawdzenie i wyciągnięcie:
if (testShape is Circle { Color: "Zielony", Radius: var radiusVal } circleObj)
{
Console.WriteLine($"Wyciągnięto zielone koło. Promień: {radiusVal}, Obiekt: {circleObj.Radius}");
}
Wzorzec właściwości mocno upraszcza złożone wyrażenia warunkowe.
Przykład: Zakresy i operatory logiczne w Property Patterns
Teraz możesz sprawdzać, czy właściwości obiektu spełniają określony warunek:
Circle bigCircle = new Circle { Radius = 25.0, Color = "Niebieski" };
// Sprawdzamy promień większy niż 20 I kolor niebieski
if (bigCircle is Circle { Radius: > 20, Color: "Niebieski" })
{
Console.WriteLine("Znaleziono duże niebieskie koło.");
}
// Sprawdzamy promień w zakresie (5..15)
if (testShape is Circle { Radius: >= 5 and <= 15 })
{
Console.WriteLine("Koło średniego rozmiaru.");
}
Ważne! W przeciwieństwie do typu bool, tutaj używasz słów kluczowych: and, or i not.
4. Switch Expressions i Switch Statements z Pattern Matching
Największą elastyczność i czytelność pattern matching pokazuje w operatorach switch. Pozwalają wykonać różną logikę w zależności od dopasowanego wzorca.
Przykład: Switch Statement
Pozwala określić bloki kodu dla każdego dopasowanego wzorca.
// Klasy Animal, Dog, Cat jak na początku wykładu
Animal currentCreature = new Dog { Species = "Pudel" };
switch (currentCreature)
{
case Dog d: // Wzorzec typu: jeśli to Dog, przypisz do zmiennej 'd'
d.Bark();
Console.WriteLine($"To jest pies rasy {d.Species}.");
break;
case Cat c when c.Species == "Syjamski": // Wzorzec typu z warunkiem (when clause)
c.Meow();
Console.WriteLine($"To jest kot syjamski.");
break;
case Animal a: // Jeśli to po prostu Animal (i nie Dog/Cat)
Console.WriteLine($"To po prostu zwierzę gatunku {a.Species}.");
break;
case null: // Wzorzec null do obsługi wartości null
Console.WriteLine("Obiekt jest null.");
break;
default: // Jeśli żaden z powyższych wzorców nie pasuje
Console.WriteLine("Nieznane stworzenie.");
break;
}
Kolejność case'ów w switch ma znaczenie: bardziej szczegółowe wzorce powinny być przed bardziej ogólnymi.
Przykład: Switch Expression
To bardziej zwięzła składnia dla switch, która zwraca wartość. Idealna do przekształcania obiektu na inną wartość na podstawie jego typu lub właściwości.
Składnia: wyrażenie switch { wzorzec1 => wynik1, wzorzec2 => wynik2, ... _ => domyślny_wynik }
Shape processShape = new Rectangle { Width = 5, Height = 5, Color = "Czerwony" };
string shapeInfo = processShape switch
{
Circle { Radius: var r } when r > 10 => $"Duże koło (R={r})", // Wzorzec właściwości z warunkiem
Circle { Color: "Niebieski" } c => $"Niebieskie koło (R={c.Radius})", // Wzorzec właściwości z wyciągnięciem
Circle c => $"Zwykłe koło (R={c.Radius})", // Prosty wzorzec typu
Rectangle { Width: var w, Height: var h } when w == h => $"Kwadrat ({w}x{h})", // Kwadrat
Rectangle r => $"Prostokąt ({r.Width}x{r.Height})", // Każdy inny prostokąt
null => "Figura nie istnieje (null)", // Wzorzec null
_ => "Nieznana figura" // Wzorzec discard (_), zamiast 'default'
};
Console.WriteLine(shapeInfo); // Wyjście: Kwadrat (5x5)
switch expression jest bardzo wygodny do krótkich, czytelnych przekształceń.
Var Pattern (Wzorzec var)
Od C# 7.0 możesz używać var w pattern matching. var zawsze pasuje do każdego obiektu (poza null) i wyciąga go do zmiennej odpowiedniego typu.
Przykład: Użycie var Pattern
object obj = "Hello World";
if (obj is var result) // Zawsze true, result będzie string
{
Console.WriteLine($"Typ: {result.GetType().Name}, Wartość: {result}");
}
obj = 123;
string typeName = obj switch
{
var x when x is int => "To jest liczba całkowita",
var y when y is string => "To jest string",
_ => "Coś innego"
};
Console.WriteLine(typeName); // Wyjście: To jest liczba całkowita
var Pattern rzadko używa się samodzielnie do sprawdzania typu, ale jest bardzo przydatny wewnątrz innych wzorców lub switch do wyciągania wartości.
5. is vs as vs Pattern Matching: kiedy i czego używać?
Wybór narzędzia zależy od twojego konkretnego zadania i wersji C#, której używasz.
Operator is (prosty):
Zastosowanie: Gdy chcesz po prostu sprawdzić, czy obiekt jest określonego typu, a dalsze działania nie wymagają natychmiastowego rzutowania lub dostępu do specyficznych członków.
if (myObject is SomeType)
Operator as:
Zastosowanie: Gdy próbujesz rzutować typ referencyjny (lub nullable typ wartościowy) i chcesz obsłużyć niepowodzenie bez wyjątku, używając sprawdzenia na null.
MyDerivedClass derived = myBaseObject as MyDerivedClass;
if (derived != null) { /* ... */ }
Pattern Matching (is Type variable):
Nowoczesne, preferowane podejście: Łączy sprawdzenie typu i bezpieczne rzutowanie w jednym, bardziej czytelnym wierszu. Działa dla wszystkich typów.
Zastosowanie: Gdy chcesz sprawdzić typ i od razu użyć specyficznych członków tego typu.
if (myObject is MyDerivedClass derived) { derived.SpecificMethod(); }
Pattern Matching (switch expressions / statements):
Nowoczesne, preferowane podejście: Gdy chcesz wykonać różne akcje lub uzyskać różne wartości w zależności od typu obiektu, jego właściwości lub innych cech. To znacznie czyściej niż długie łańcuchy if-else if.
Zastosowanie: Realizacja polimorficznego zachowania lub złożonej logiki wyboru na podstawie cech obiektu.
string GetShapeInfo(Shape s) => s switch
{
Circle c => $"Koło R={c.Radius}",
Rectangle r => $"Prostokąt W={r.Width} H={r.Height}",
_ => "Nieznane"
};
GO TO FULL VERSION