Когда базовый класс определяет защищенные данные или защищенные члены, он устанавливает набор элементов, которые могут быть непосредственно доступны любому наследнику. Если вы хотите разрешить дочерним классам SalesPerson и Manager напрямую обращаться к разделу данных, который определен в Employee, то модифицируйте исходный класс Employee (в файле EmployeeCore.cs), как показано ниже:
// Защищенные данные состояния.
partial class Employee
{
// Производные классы теперь могут иметь прямой доступ к этой информации.
protected string EmpName;
protected int EmpId;
protected float CurrPay;
protected int EmpAge;
protected string EmpSsn;
protected EmployeePayTypeEnum EmpPayType;
...
}
На заметку! По соглашению защищенные члены именуются в стиле Pascal (EmpName), а не в "верблюжьем" стиле с подчеркиванием (_empName). Это не является требованием языка, но представляет собой распространенный стиль написания кода. Если вы решите обновить имена, как было сделано здесь, тогда не забудьте переименовать все поддерживающие методы в свойствах, чтобы они соответствовали защищенным свойствам с именами в стиле Pascal.
Преимущество определения защищенных членов в базовом классе заключается в том, что производным классам больше не придется обращаться к данным косвенно, используя открытые методы и свойства. Разумеется, подходу присущ и недостаток: когда производный класс имеет прямой доступ к внутренним данным своего родителя, то есть вероятность непредумышленного обхода существующих бизнес-правил, которые реализованы внутри открытых свойств. Определяя защищенные члены, вы создаете уровень доверия между родительским классом и дочерним классом, т.к. компилятор не будет перехватывать какие-либо нарушения бизнес-правил, предусмотренных для типа.
Наконец, имейте в виду, что с точки зрения пользователя объекта защищенные данные расцениваются как закрытые (поскольку пользователь находится "снаружи" семейства). По указанной причине следующий код недопустим:
// Ошибка! Доступ к защищенным данным из клиентского кода невозможен!
Employee emp = new Employee;
emp.empName = "Fred";
На заметку! Несмотря на то что защищенные поля данных могут нарушить инкапсуляцию, определять защищенные методы вполне безопасно (и полезно). При построении иерархий классов обычно приходится определять набор методов, которые предназначены для применения только производными типами, но не внешним миром.
Добавление запечатанного класса
Вспомните, что запечатанный класс не может быть расширен другими классами. Как уже упоминалось, такой прием чаще всего используется при проектировании обслуживающих классов. Тем не менее, при построении иерархий классов вы можете обнаружить, что определенная ветвь в цепочке наследования нуждается в "отсечении", т.к. дальнейшее ее расширение не имеет смысла. В качестве примера предположим, что вы добавили в приложение еще один класс (PtSalesPerson), который расширяет существующий тип SalesPerson. Текущее обновление показано на рис. 6.3.
Класс PtSalesPerson представляет продавца, который работает на условиях частичной занятости. В качестве варианта скажем, что нужно гарантировать отсутствие возможности создания подкласса PtSalesPerson. Чтобы предотвратить наследование от класса, необходимо применить ключевое слово sealed:
sealed class PtSalesPerson : SalesPerson
{
public PtSalesPerson(string fullName, int age, int empId,
float currPay, string ssn, int numbOfSales)
: base(fullName, age, empId, currPay, ssn, numbOfSales)
{
}
// Остальные члены класса...
}
Наследование с типами записей (нововведение в версии 9.0)
Появившиеся в версии C# 9.0 типы записей также поддерживают наследование. Чтобы выяснить как, отложите пока свою работу над проектом Employees и создайте новый проект консольного приложения по имени RecordInheritance. Добавьте в него два файла с именами Car.cs и MiniVan.cs, содержащими следующие определения записей:
// Car.cs
namespace RecordInheritance
{
//Car record type
public record Car
{
public string Make { get; init; }
public string Model { get; init; }