Допустим, у нас есть следующая иерархия классов:
class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public void Print()
{
Console.WriteLine($"Person {Name}");
}
}
class Employee : Person
{
public string Company { get; set; }
public Employee(string name, string company) : base(name)
{
Company = company;
}
}
class Client : Person
{
public string Bank { get; set; }
public Client(string name, string bank) : base(name)
{
Bank = bank;
}
}
В этой иерархии классов мы можем проследить следующую цепь наследования: Object (все классы неявно наследуются от типа Object) -> Person -> Employee|Client.
Причем в этой иерархии классов базовые типы находятся вверху, а производные типы — внизу.
Восходящие преобразования. Upcasting
Объекты производного типа (который находится внизу иерархии) в то же время представляют и базовый тип. Например, объект Employee в то же время является и объектом класса Person. Что в принципе естественно, так как каждый сотрудник (Employee) является человеком (Person). И мы можем написать, например, следующим образом:
Employee employee = new Employee("Tom", "Microsoft");
Person person = employee; // преобразование от Employee к Person
Console.WriteLine(person.Name);
В данном случае переменной person, которая представляет тип Person, присваивается ссылка на объект Employee. Но чтобы сохранить ссылку на объект одного класса в переменную другого класса, необходимо выполнить преобразование типов — в данном случае от типа Employee к типу Person. И так как Employee наследуется от класса Person, то автоматически выполняется неявное восходящее преобразование — преобразование к типу, которые находятся вверху иерархии классов, то есть к базовому классу.
В итоге переменные employee и person будут указывать на один и тот же объект в памяти, но переменной person будет доступна только та часть, которая представляет функционал типа Person.
Подобным образом поизводятся и другие восходящие преобразования:
Person bob = new
Client("Bob", "ContosoBank"); // преобразование от Client к Person
Здесь переменная bob, которая представляет тип Person, хранит ссылку на объект Client, поэтому также выполняется восходящее неявное преобразование от производного класса Client к базовому типу Person.
Восходящее неявное преобразование будет происходить и в следующем случае:
object person1 = new Employee("Tom", "Microsoft"); // от Employee к object
object person2 = new Client("Bob", "ContosoBank"); // от Client к object
object person3 = new Person("Sam"); // от Person к object
Так как тип object — базовый для всех остальных типов, то преобразование к нему будет производиться автоматически.
Нисходящие преобразования. Downcasting
Но кроме восходящих преобразований от производного к базовому типу есть нисходящие преобразования или downcasting — от базового типа к производному. Например, в следующем коде переменная person хранит ссылку на объект Employee:
Employee employee = new Employee("Tom", "Microsoft");
Person person = employee; // преобразование от Employee к Person
И может возникнуть вопрос, можно ли обратиться к функционалу типа Employee через переменную типа Person. Но автоматически такие преобразования не проходят, ведь не каждый человек (объект Person) является сотрудником предприятия (объектом Employee). И для нисходящего преобразования необходимо применить явное преобразование, указав в скобках тип, к которому нужно выполнить преобразование:
Employee employee1 = new Employee("Tom", "Microsoft");
Person person = employee1; // преобразование от Employee к Person
//Employee employee2 = person; // так нельзя, нужно явное преобразование
Employee employee2 = (Employee)person; // преобразование от Person к Employee
Рассмотрим некоторые примеры преобразований:
// Объект Employee также представляет тип object
object obj = new Employee("Bill", "Microsoft");
// чтобы обратиться к возможностям типа Employee, приводим объект к типу Employee
Employee employee = (Employee) obj;
// объект Client также представляет тип Person
Person person = new Client("Sam", "ContosoBank");
// преобразование от типа Person к Client
Client client = (Client)person;
В первом случае переменной obj присвоена ссылка на объект Employee, поэтому мы можем преобразовать объект obj к любому типу который располагается в иерархии классов между типом object и Employee.
Если нам надо обратиться к каким-то отдельным свойствам или методам объекта, то нам необязательно присваивать преобразованный объект переменной:
// Объект Employee также представляет тип object
object obj = new Employee("Bill", "Microsoft");
// преобразование к типу Person для вызова метода Print
((Person)obj).Print();
// либо так
// ((Employee)obj).Print();
// преобразование к типу Employee, чтобы получить свойство Company
string company = ((Employee)obj).Company;
В то же время необходимо соблюдать осторожность при подобных преобразованиях. Например, что будет в следующем случае:
// Объект Employee также представляет тип object
object obj = new Employee("Bill", "Microsoft");
// преобразование к типу Client, чтобы получить свойство Bank
string bank = ((Client)obj).Bank;
В данном случае мы получим ошибку, так как переменная obj хранит ссылку на объект Employee. Данный объект является также объектом типов object и Person, поэтому мы можем преобразовать его к этим типам. Но к типу Client мы преобразовать не можем.
Другой пример:
Employee employee1 = new Person("Tom"); // ! Ошибка
Person person = new Person("Bob");
Employee employee2 = (Employee) person; // ! Ошибка
В данном случае мы пытаемся преобразовать объект типа Person к типу Employee, а объект Person не является объектом Employee. Причем в последнем случае Visual Studio не подскжет, что в данной строке ошибка, и данная строка даже нормально скомилируется, тем не менее в процессе выполнения программы мы получим ощибку. В этом в том числе и кроектся коварство преобразований, поэтому в подобных ситуациях надо проявлять осторожность.
Существует ряд способов, чтобы избежать подобных ошибок преобразования.
Способы преобразований
Во-первых, можно использовать ключевое слово as. С помощью него программа пытается преобразовать выражение к определенному типу, при этом не выбрасывает исключение. В случае неудачного преобразования выражение будет содержать значение null:
Person person = new Person("Tom");
Employee? employee = person as Employee;
if (employee == null)
{
Console.WriteLine("Преобразование прошло неудачно");
}
else
{
Console.WriteLine(employee.Company);
}
Стоит отметить, что переменная employee
здесь определяется не просто как переменная Employee, а именно Employee? — после названия типа ставится вопросительный знак. Что указывает, что переменная может хранить как значение null, так и значение Employee.
Второй способ заключается в проверке допустимости преобразования с помощью ключевого слова is:
значение is
тип
Если значение слева от оператора представляет тип, указаный справа от оператора, то оператор is возвращает true
, иначе возвращается false
.
Причем оператор is позволяет автоматически преобразовать значение к типу, если это значение представляет данный тип. Например:
Person person = new Person("Tom");
if (person is Employee employee)
{
Console.WriteLine(employee.Company);
}
else
{
Console.WriteLine("Преобразование не допустимо");
}
Выражение if (person is Employee employee)
проверяет, является ли переменная person объектом типа Employee. И если person является объектом Employee, то автоматически преобразует значение переменной person в тип Employee и преобразованное значение сохраняет в переменную employee. Далее в блоке if мы можем использовать объект employee как значение типа Employee.
Однако, если person не является объектом Employee, как в данном случае, то такая проверка вернет значение false
, и преобразование не сработает.
Оператор is также можно применять и без преобразования, просто проверяя на соответствие типу:
Person person = new Person("Tom");
if (person is Employee)
{
Console.WriteLine("Представляет тип Employee");
}
else
{
Console.WriteLine("НЕ является объектом типа Employee");
}