1. Оператор is: проверка совместимости типов
В реальных приложениях часто бывает, что у вас есть ссылка на объект базового класса, но вы знаете или подозреваете, что фактический объект в памяти — более специфичный производный тип. В таких случаях может потребоваться получить доступ к уникальным членам этого производного типа. Для безопасной и удобной работы с такими ситуациями в C# есть несколько ключевых инструментов: операторы is, as и, что особенно важно в современных версиях языка, мощные возможности сопоставления с образцом (pattern matching).
is позволяет проверить, является ли объект экземпляром указанного типа, его производного типа или реализует заданный интерфейс. Он возвращает true, если объект совместим с этим типом, и false в противном случае.
Основное применение: определить, можно ли безопасно привести объект к определённому типу.
Пример: Проверка базового типа и наследования
class Animal { public string Species { get; set; } = "Unknown"; }
class Dog : Animal { public void Bark() => Console.WriteLine("Woof!"); }
class Cat : Animal { public void Meow() => Console.WriteLine("Meow!"); }
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
В этом примере myAnimal фактически является Dog. Поэтому myAnimal is Animal и myAnimal is Dog вернут true.
Пример: Проверка на null
Оператор is также ведет себя предсказуемо с null.
Animal nullAnimal = null;
Dog specificDog = new Dog();
Console.WriteLine($"nullAnimal is Animal: {nullAnimal is Animal}"); // False (null не является экземпляром никакого типа)
Console.WriteLine($"specificDog is null: {specificDog is null}"); // False (объект не null)
Заметьте, что null не является экземпляром никакого типа, поэтому null is MyType всегда false. Однако someObject is null — это удобный и безопасный способ проверить, является ли ссылка null.
Пример: Использование is с интерфейсами
Оператор is также работает для проверки реализации интерфейсов.
interface IFlyable { void Fly(); }
class Bird : Animal, IFlyable { public void Fly() => Console.WriteLine("Flap flap!"); }
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. Оператор as: безопасное приведение типов
Оператор as предназначен для безопасного преобразования объекта к указанному типу. В отличие от явного приведения (Type)obj, которое выбросит InvalidCastException при неудаче, as возвращает null, если преобразование невозможно. Это делает его идеальным для ситуаций, когда вы не уверены в фактическом типе объекта.
Основное применение: Попытаться привести тип, получив null в случае неудачи.
Пример: Базовое использование 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 };
// Попытка приведения к Circle
Circle circle = myShape as Circle;
if (circle != null) // Обязательная проверка на null!
{
Console.WriteLine($"Это круг с радиусом: {circle.Radius}"); // Вывод: Это круг с радиусом: 5
}
// Попытка приведения к Square
Square square = myShape as Square;
if (square == null) // Приведение не удалось, square == null
{
Console.WriteLine("Это не квадрат."); // Вывод: Это не квадрат.
}
Как видно, as позволяет избежать потенциальных ошибок времени выполнения, возвращая null вместо исключения.
Пример: Ограничения as
Важно помнить, что оператор as работает только со ссылочными типами и nullable-значимыми типами. Он не применим к обычным значимым типам, поскольку они не могут принимать значение null.
object someValue = 100;
int num = someValue as int; // ОШИБКА КОМПИЛЯЦИИ: 'as' не может использоваться не null-типами
int? nullableNum = someValue as int?; // Ок, nullable int
Console.WriteLine($"Nullable int: {nullableNum}"); // Вывод: Nullable int: 100
string str = someValue as string; // Возвращает null, так как 100 не строка
Console.WriteLine($"String from int: {str ?? "null"}"); // Вывод: String from int: null
Для не-nullable значимых типов используйте либо явное приведение ((int)someValue), если вы уверены в типе (и готовы к исключению), либо, что предпочтительнее, pattern matching.
3. Pattern Matching (Сопоставление с образцом)
Pattern matching — это мощная и развивающаяся функция, которая предоставляет более элегантные и безопасные способы проверки типов и извлечения данных из объектов. Она значительно сокращает шаблонный код и улучшает читаемость.
Type Pattern (Шаблон типа) с is
Это наиболее часто используемая форма pattern matching, которая позволяет проверить тип объекта и, если проверка успешна, сразу же присвоить его новой переменной указанного типа.
Синтаксис: выражение is Тип переменная
Пример: Замена is + приведение
// Продолжаем с классами Shape, Circle, Square
Shape currentShape = new Circle { Radius = 7.5 };
// Старый, многословный способ:
if (currentShape is Circle)
{
Circle c = (Circle)currentShape;
Console.WriteLine($"Старый способ: Круг с радиусом {c.Radius}");
}
// Новый, элегантный способ с type pattern
if (currentShape is Circle c) // Проверка типа и создание переменной 'c'
{
Console.WriteLine($"Новый способ: Круг с радиусом {c.Radius}"); // 'c' уже имеет тип Circle
}
Shape anotherShape = new Square { Side = 10.0 };
if (anotherShape is Square s)
{
Console.WriteLine($"Это квадрат со стороной {s.Side}");
}
Переменная c (или s) доступна только в блоке if, что предотвращает ее случайное использование вне контекста правильного типа.
Property Pattern (Шаблон свойства)
Начиная с C# 8.0, вы можете не только проверить тип, но и одновременно проверить значения одного или нескольких свойств объекта, а также извлечь их в новые переменные.
Синтаксис: выражение is Тип { Свойство1: шаблон_значения, Свойство2: переменная_извлечения }
Пример: Проверка свойства
Shape testShape = new Circle { Color = "Green", Radius = 12.0 };
// Проверяем, является ли объект кругом и является ли его цвет зеленым
if (testShape is Circle { Color: "Green" })
{
Console.WriteLine("Найден зеленый круг.");
}
// Проверяем, является ли объект кругом, и одновременно извлекаем его радиус
if (testShape is Circle { Radius: var r })
{
Console.WriteLine($"Радиус извлечен: {r}");
}
// Комбинируем проверку и извлечение:
if (testShape is Circle { Color: "Green", Radius: var radiusVal } circleObj)
{
Console.WriteLine($"Извлечен зеленый круг. Радиус: {radiusVal}, Объект: {circleObj.Radius}");
}
Шаблон свойства значительно упрощает сложные условные выражения.
Пример: Диапазоны и логические операторы в Property Patterns
Теперь вы можете проверять, что свойства объекта удовлетворяют определенному условию:
Circle bigCircle = new Circle { Radius = 25.0, Color = "Blue" };
// Проверяем радиус больше 20 И цвет синий
if (bigCircle is Circle { Radius: > 20, Color: "Blue" })
{
Console.WriteLine("Большой синий круг обнаружен.");
}
// Проверяем радиус в диапазоне (5..15)
if (testShape is Circle { Radius: >= 5 and <= 15 })
{
Console.WriteLine("Круг среднего размера.");
}
Важно! В отличие от типа bool, тут используются ключевые слова: and, or и not.
4. Switch Expressions и Switch Statements с Pattern Matching
Наибольшую гибкость и читаемость pattern matching демонстрирует в операторах switch. Они позволяют выполнять различную логику в зависимости от совпавшего шаблона.
Пример: Switch Statement
Позволяет определять блоки кода для каждого совпавшего шаблона.
// Классы Animal, Dog, Cat как в начале лекции
Animal currentCreature = new Dog { Species = "Poodle" };
switch (currentCreature)
{
case Dog d: // Шаблон типа: если это Dog, присвоить переменной 'd'
d.Bark();
Console.WriteLine($"Это собака породы {d.Species}.");
break;
case Cat c when c.Species == "Siamese": // Шаблон типа с условием (when clause)
c.Meow();
Console.WriteLine($"Это сиамская кошка.");
break;
case Animal a: // Если это просто Animal (и не Dog/Cat)
Console.WriteLine($"Это просто животное вида {a.Species}.");
break;
case null: // Шаблон null для обработки null-значений
Console.WriteLine("Объект равен null.");
break;
default: // Если ни один из вышеперечисленных шаблонов не совпал
Console.WriteLine("Неизвестное существо.");
break;
}
Порядок кейсов в switch имеет значение: более специфичные шаблоны должны идти перед более общими.
Пример: Switch Expression
Это более компактный синтаксис для switch, который возвращает значение. Идеально подходит для преобразования объекта в другое значение на основе его типа или свойств.
Синтаксис: выражение switch { шаблон1 => результат1, шаблон2 => результат2, ... _ => дефолтный_результат }
Shape processShape = new Rectangle { Width = 5, Height = 5, Color = "Red" };
string shapeInfo = processShape switch
{
Circle { Radius: var r } when r > 10 => $"Большой круг (R={r})", // Шаблон свойства с условием
Circle { Color: "Blue" } c => $"Синий круг (R={c.Radius})", // Шаблон свойства с извлечением
Circle c => $"Обычный круг (R={c.Radius})", // Простой шаблон типа
Rectangle { Width: var w, Height: var h } when w == h => $"Квадрат ({w}x{h})", // Квадрат
Rectangle r => $"Прямоугольник ({r.Width}x{r.Height})", // Любой другой прямоугольник
null => "Фигура не существует (null)", // Шаблон null
_ => "Неизвестная фигура" // Дискард-шаблон (_) вместо 'default'
};
Console.WriteLine(shapeInfo); // Вывод: Квадрат (5x5)
switch expression очень удобен для коротких, выразительных преобразований.
Var Pattern (Шаблон var)
Начиная с C# 7.0, вы можете использовать var в pattern matching. var всегда совпадает с любым объектом (кроме null) и извлекает его в переменную соответствующего типа.
Пример: Использование var Pattern
object obj = "Hello World";
if (obj is var result) // Всегда true, result будет string
{
Console.WriteLine($"Тип: {result.GetType().Name}, Значение: {result}");
}
obj = 123;
string typeName = obj switch
{
var x when x is int => "Это целое число",
var y when y is string => "Это строка",
_ => "Что-то другое"
};
Console.WriteLine(typeName); // Вывод: Это целое число
var Pattern редко используется сам по себе для проверки типа, но очень полезен внутри других шаблонов или switch для извлечения значения.
5. is vs as vs Pattern Matching: когда и что использовать?
Выбор инструмента зависит от вашей конкретной задачи и версии C#, которую вы используете.
Оператор is (простой):
Применение: Когда вам нужно просто проверить, является ли объект определенного типа, и дальнейшие действия не требуют немедленного приведения или доступа к специфичным членам.
if (myObject is SomeType)
Оператор as:
Применение: Когда вы пытаетесь привести ссылочный тип (или nullable значимый тип) и хотите обработать неудачу без исключения, используя проверку на null.
MyDerivedClass derived = myBaseObject as MyDerivedClass;
if (derived != null) { /* ... */ }
Pattern Matching (is Type variable):
Предпочтительный современный подход: Комбинирует проверку типа и безопасное приведение в одной, более читабельной строке. Работает для всех типов.
Применение: Когда вам нужно проверить тип и сразу же использовать специфичные члены этого типа.
if (myObject is MyDerivedClass derived) { derived.SpecificMethod(); }
Pattern Matching (switch expressions / statements):
Предпочтительный современный подход: Когда вам нужно выполнить различные действия или получить разные значения в зависимости от типа объекта, его свойств или других характеристик. Это значительно чище, чем длинные цепочки if-else if.
Применение: Реализация полиморфного поведения или сложной логики выбора на основе характеристик объекта.
string GetShapeInfo(Shape s) => s switch
{
Circle c => $"Круг R={c.Radius}",
Rectangle r => $"Прямоугольник W={r.Width} H={r.Height}",
_ => "Неизвестно"
};
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ