В предыдущей главе мы говорили о преобразованиях объектов простых типов. Сейчас затронем тему преобразования объектов классов. Допустим, у нас есть следующая иерархия классов:
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.
Причем в этой иерархии классов базовые типы находятся вверху, а производные типы - внизу.
Объекты производного типа (который находится внизу иерархии) в то же время представляют и базовый тип. Например, объект 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 - от базового типа к производному. Например, в следующем коде переменная 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"); }