Содержание
На данные момент нам известно как в 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# при разработке приложений, то, на мой взгляд, механизм сокрытия стоит использовать с особой осторожностью и только там, где это использование полностью оправдано. В остальных же случаях лучше использовать переопределение или перегрузку методов и свойств.