Содержание
На данные момент нам известно как в C# работает перегрузка (overload
) и переопределение (override
) методов и свойств классов. Однако, на этом возможности C# по изменению функциональности базовых свойств и методов в классах не ограничиваются. Также, одним из способов изменить функциональность метода или свойства, унаследованного от базового класса, является сокрытие (hiding или shadowing). И сегодня мы будем рассматривать именно этот вопрос — сокрытие методов и свойств классов в C#.
Сокрытие методов и свойств класса в C#
Вначале рассмотрим, что из себя представляет сокрытие в C#, в принципе. Сокрытие в C# — это определение в классе-наследнике метода или свойства, которые полностью соответствуют по имени и набору параметров методу или свойству базового класса (родителя). Про отличия в переопределении и сокрытии в C# поговорим ниже. Пока же нам достаточно знать, что для сокрытия методов или свойств класса в C# применяется ключевое слово new
. Например:
class Person { public string FirstName { get; set; } public string LastName { get; set; } public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public void Display() { Console.WriteLine($"{FirstName} {LastName}"); } } class Employee : Person { public string Company { get; set; } public Employee(string firstName, string lastName, string company): base(firstName, lastName) { Company = company; } public new void Display() { Console.WriteLine($"{FirstName} {LastName} работает в {Company}"); } }
У класса Person
(человек) определено два свойства и метод Display()
, который выводит в консоль имя и фамилию человека. Обратите внимание на то, что метод не помечен как виртуальный (virtual
), то есть переопределить мы его не можем.
Класс Employee
(работник) наследует все свойства и методы от базового класса Person
, но также имеет собственное свойство Company
, а базовый метод Display()
скрыт — использовано ключевое слово new
. Таким образом, мы скрыли метод класса-родителя и определили для него новую функциональность — помимо имени и фамилии теперь в консоль также будет выводиться и название компании.
Используем разработанные выше классы в программе:
Person bob = new Person("Bob", "Robertson"); bob.Display(); // Bob Robertson Employee tom = new Employee("Tom", "Smith", "Microsoft"); tom.Display(); // Tom Smith работает в Microsoft Console.ReadKey();
Результат работы программы:
Tom Smith работает в Microsoft
Аналогичным образом работает и сокрытие свойств в C#. Например:
class Person { protected string name; public string Name { get { return name; } set { name = value; } } } class Employee : Person { public new string Name { get { return "Employee " + base.Name; } set { name = value; } } }
Здесь мы скрыли свойство Name
. Опять же, сокрытие методов и свойств в C# не означает, что мы не можем обращаться к базовым реализациям членов класса. Для доступа к базовым методам и свойствам классов, даже, если они скрыты, также используется ключевое слово base
. Более того, мы можем применять механизм сокрытия даже к переменным и константам:
class ExampleBase { public readonly int x = 10; public const int G = 5; } class ExampleDerived : ExampleBase { public new readonly int x = 20; public new const int G = 15; }
При этом если мы хотим обратиться именно к реализации свойства или метода в базовом классе, то опять же мы можем использовать ключевое слово base и через него обращаться к функциональности базового класса.
Более того мы даже можем применять сокрытие к переменным и константам, также используя ключевое слово new
:
class ExampleBase { public readonly int x = 10; public const int G = 5; } class ExampleDerived : ExampleBase { public new readonly int x = 20; public new const int G = 15; }
Различие переопределения и сокрытия методов
Для того, чтобы понять ключевые различия между переопределением и сокрытием методов, немного углубимся в процесс работы нашего кода и рассмотрим как работает переопределение методов в C#.
Переопределение
Рассмотрим следующий пример переопределения метода:
class Person { public string Name { get; set; } public Person(string name) { Name = name; } public virtual void Display() { Console.WriteLine(Name); } } class Employee : Person { public string Company { get; set; } public Employee(string name, string company) : base(name) { Company = company; } public override void Display() { Console.WriteLine($"{Name} работает в {Company}"); } }
Создадим объект класса-наследника (Employee
) и передадим его переменной с типом родителя (Person
)
Person tom = new Employee("Tom", "Microsoft"); tom.Display(); // Tom работает в Microsoft
При вызове tom.Display()
выполняется реализация метода Display
из класса Employee
. Что для нас, в принципе, ожидаемо. Какова механика работы с виртуальными методами в C#:
- Для работы с виртуальными методами компилятор формирует таблицу виртуальных методов (Virtual Method Table или VMT). В эту таблицу записываются адреса всех виртуальных методов класса, то есть, для каждого класса формируется своя VMT.
- Когда создается объект класса, то компилятор передает в конструктор объекта код, который связывает наш объект с таблицей VMT.
- При вызове виртуального метода из объекта:
- берется адрес его таблицы VMT.
- из VMT извлекается адрес метода и ему передается управление.
Весь процесс выбора той или иной реализации метода производится во время выполнения программы. Именно так и выполняются виртуальные методы в C#.
Исходя из этого, следует учитывать, следующее:
Сокрытие
Теперь возьмем те же классы Person и Employee, но вместо переопределения используем сокрытие:
class Person { public string Name { get; set; } public Person(string name) { Name = name; } public void Display() { Console.WriteLine(Name); } } class Employee : Person { public string Company { get; set; } public Employee(string name, string company) : base(name) { Company = company; } public new void Display() { Console.WriteLine($"{Name} работает в {Company}"); } }
Посмотрим, что получится в итоге в следующем случае:
Person tom = new Employee("Tom", "Microsoft"); tom.Display(); // Tom
Почему так произошло? Всё дело в том, что класс Employee
никак не переопределяет метод Display
, унаследованный от базового класса, а фактически определяет новый метод, то есть VMT не создается. Поэтому при вызове tom.Display()
вызывается метод Display
из класса Person
.
Итого
Сегодня мы рассмотрели ещё один механизм изменения функциональности свойств и методов классов в C# — сокрытие. Этот механизм позволяет создавать в производном классе свойства и методы с сигнатурой как и у членов базового класса и, при этом, в базовом классе не используется ключевое слово virtual. Что касается того, стоит ли использовать сокрытие методов в C# при разработке приложений, то, на мой взгляд, механизм сокрытия стоит использовать с особой осторожностью и только там, где это использование полностью оправдано. В остальных же случаях лучше использовать переопределение или перегрузку методов и свойств.