Освоєння ключового слова `override` в C# — Поглиблений посібник

Ключове слово override у C# є основною можливістю для поліморфізму, дозволяючи похідним класам надавати різні реалізації методів, властивостей чи індексаторів, які були оголошені як virtual або abstract в їх базових класах.

Якщо ви вже знайомі з основами перевизначення в C#, ця стаття йде далі, охоплюючи нюанси та найкращі практики, які можуть мати велике значення в реальних проектах.

Ключові концепції

У C# перевизначення (override) означає створення нової реалізації члена (методу, властивості чи індексатора), який був оголошений як virtual, abstract або override у базовому класі. Це лежить в основі поліморфізму, дозволяючи поведінку визначати похідному класу під час виконання.

public class Shape  
{  
 public virtual void Draw()  
 {  
 Console.WriteLine("Drawing a generic shape.");  
 }  
}  

public class Circle : Shape  
{  
 public override void Draw()  
 {  
 Console.WriteLine("Drawing a circle.");  
 }  
}

Коли ви створюєте екземпляр new Circle() і викликаєте Draw(), використовується версія методу в класі Circle, навіть якщо змінна посилання має тип Shape.

Override vs. new: Перевизначення vs. Сховання

Окрім override, C# має ключове слово new, яке сховує (а не перевизначає) член базового класу. Насправді:

  • override: частина поліморфічного ланцюга; якщо викликати метод через базове посилання, виконується версія похідного класу.
  • new: створює новий метод з тим самим іменем, але він не є частиною поліморфізму; якщо викликати його через базове посилання, виконується базова версія.
public class BaseClass  
{  
 public virtual void DoSomething()  
 {  
 Console.WriteLine("BaseClass.DoSomething()");  
 }  
}  

public class DerivedClass : BaseClass  
{  
 public new void DoSomething()  
 {  
 Console.WriteLine("DerivedClass.DoSomething() - hiding");  
 }  
}  

public class DerivedOverrideClass : BaseClass  
{  
 public override void DoSomething()  
 {  
 Console.WriteLine("DerivedOverrideClass.DoSomething() - overriding");  
 }  
}
  • Якщо ви викликаєте DoSomething() в класі DerivedClass через BaseClass b1 = new DerivedClass();, буде виконано базовий метод.
  • Якщо ви викликаєте DoSomething() в класі DerivedOverrideClass через BaseClass b2 = new DerivedOverrideClass();, буде виконано похідний метод.

Sealed override: Запечатування перевизначення

Ви можете поєднати sealed з override, щоб заборонити подальше перевизначення в похідних класах:

public class BaseClass  
{  
 public virtual void SomeMethod()  
 {  
 Console.WriteLine("BaseClass.SomeMethod()");  
 }  
}  

public class IntermediateClass : BaseClass  
{  
 public override void SomeMethod()  
 {  
 Console.WriteLine("IntermediateClass.SomeMethod()");  
 }  
}  

public class FinalClass : IntermediateClass  
{  
 public sealed override void SomeMethod()  
 {  
 Console.WriteLine("FinalClass.SomeMethod()");  
 }  
}  

// Помилка, якщо спробувати перевизначити знову  
public class AnotherClass : FinalClass  
{  
 // public override void SomeMethod() { ... } // Illegal  
}

Використання sealed override має сенс, коли ви хочете обмежити розширення поліморфізму з міркувань проектування або продуктивності.

Перевизначення властивостей і індексаторів

Перевизначення не обмежується лише методами — властивості та індексатори також можуть бути оголошені як virtual або abstract.
Те саме правило діє й тут: щоб перевизначити, використовуєте override і зберігаєте оригінальний підпис (тип повернення, параметри тощо).

public class Person  
{  
 public virtual string Name { get; set; } = "No name";  

 public virtual string Description  
 {  
 get { return $"Person: {Name}"; }  
 }  
}  

public class Student : Person  
{  
 public override string Name  
 {  
 get => base.Name;  
 set => base.Name = $"Student: {value}";  
 }  

 public override string Description  
 {  
 get { return $"Student: {Name}"; }  
 }  
}

Ви можете перевизначити лише get або set, якщо обидва були оголошені як virtual в базовому класі, але потрібно дотримуватись обмежень доступності.

Перевизначення узагальнених методів (з обмеженнями)

Те саме стосується і узагальнених методів. Якщо базовий клас оголошує узагальнений метод як virtual або abstract, похідний клас повинен його перевизначити, зберігаючи всі параметри типів і обмеження з оригінального підпису:

public abstract class Repository where T : class  
{  
 public abstract T Save(T entity) where TId : struct;  
}  

public class MyRepository : Repository  
{  
 // Треба зберегти "where TId : struct"  
 public override MyEntity Save(MyEntity entity)  
 {  
 // ...  
 return entity;  
 }  
}

Зміна або "ослаблення" обмежень у перевизначенні не дозволено компілятором.

Ковariantні типи повернення

Починаючи з C# 9, ковariantні типи повернення дозволяють перевизначеному методу мати більш специфічний тип повернення, ніж той, що оголошений у базовому класі, не порушуючи поліморфічну сумісність.

public class Animal  
{  
 public virtual Animal Clone()  
 {  
 return new Animal();  
 }  
}  

public class Dog : Animal  
{  
 // Ковariantний тип повернення: змінюється з Animal на Dog  
 public override Dog Clone()  
 {  
 return new Dog();  
 }  
}

Коли метод викликається на екземплярі Dog, тип повернення розпізнається як Dog, що запобігає непотрібним перетворенням типів.

Рефлексія та перевизначення методів

У метапрограмуванні або коли потрібно динамічно інспектувати типи (наприклад, у фреймворках для впровадження залежностей, ORM чи серіалізаторах), може бути корисно перевірити, чи був метод перевизначений за допомогою рефлексії. Наприклад, MethodInfo.GetBaseDefinition() повертає оригінальний метод у ланцюгу спадкування:

MethodInfo method = typeof(DerivedClass).GetMethod("DoSomething");  
MethodInfo baseMethod = method.GetBaseDefinition();  

if (baseMethod.DeclaringType == method.DeclaringType)  
{  
 Console.WriteLine("Метод не перевизначений; це базова реалізація");  
}  
else  
{  
 Console.WriteLine("Метод є перевизначенням з " + baseMethod.DeclaringType.Name);  
}

Віртуальна таблиця (vtable) та продуктивність

Перевизначення (override) працюють через віртуальну таблицю (vtable) у CLR. Кожен virtual метод має запис у vtable, який вказує на фактичну реалізацію. Коли ви перевизначаєте, похідний клас оновлює цей запис.

  • Витрати на продуктивність: Віртуальні виклики мають невелике додаткове навантаження порівняно з невіртуальними або статичними методами. Для більшості випадків це навантаження є незначним, але може мати значення в надзвичайно продуктивних циклах.
  • sealed override може дозволити певні оптимізації, оскільки компілятор знає, що подальших перевизначень немає.

Шаблон методів: шаблонний метод

Шаблонний метод (Template Method) — класичний патерн, який базується на методах virtual або abstract, дозволяючи підкласам визначати частину поведінки:

public abstract class DataExporter  
{  
 // Шаблонний метод  
 public void Export()  
 {  
 Connect();  
 WriteHeader();  
 WriteBody();  
 WriteFooter();  
 Disconnect();  
 }  

 protected abstract void Connect();  
 protected abstract void WriteHeader();  
 protected abstract void WriteBody();  
 protected abstract void WriteFooter();  
 protected abstract void Disconnect();  
}  

public class CSVExporter : DataExporter  
{  
 protected override void Connect() { /* ... */ }  
 protected override void WriteHeader() { /* ... */ }  
...
*/ }  
 protected override void WriteBody() { /* ... */ }  
 protected override void WriteFooter() { /* ... */ }  
 protected override void Disconnect() { /* ... */ }  
}

Тут базовий клас визначає загальний потік в методі Export(), а абстрактні методи змушують підкласи реалізувати деталі.

Версіонування та еволюція API

Проектування класів з великою кількістю virtual методів може становити ризики для зворотної сумісності. Ось деякі запобіжні заходи:

  • Додавання параметрів до віртуального методу після його випуску порушує роботу класів, що перевизначають цей метод.
  • Перейменування або видалення віртуальних методів впливає на похідні класи, що залежать від них.
  • Позначення методів як віртуальних після випуску може дозволити розширення, яких ви не планували чи не тестували.

Тому зазвичай рекомендується відкривати для перевизначення лише необхідні частини через virtual, а де це потрібно, використовувати sealed, більш ретельно дотримуючись принципу Відкритість/Закритість (Open/Closed).

Інші просунуті теми

Перевизначення методів System.Object у структурах

Хоча struct не дозволяє створювати власні virtual методи, ви можете перевизначити методи, успадковані від System.ValueType (небезпосередньо від System.Object), такі як ToString(), Equals(object obj) і GetHashCode():

public struct MyStruct  
{  
 public override string ToString()  
 {  
 return "My custom struct representation";  
 }  

 public override bool Equals(object obj)  
 {  
 if (obj is MyStruct)  
 {  
 // Логіка порівняння  
 return true;  
 }  
 return false;  
 }  

 public override int GetHashCode()  
 {  
 // Логіка хешування, що відповідає методу Equals  
 return base.GetHashCode();  
 }  
}

Інтерфейси з методами за замовчуванням

C# 8 ввів Методи інтерфейсів за замовчуванням (Default Interface Methods). Хоча можливе визначення методів із прямими реалізаціями всередині інтерфейсів, ці методи не є віртуальними в класичному сенсі, і для їх перевизначення не використовується override. Щоб "перевизначити" такий метод у класі, достатньо оголосити його знову:

public interface IMyInterface  
{  
 void DoWork() => Console.WriteLine("Default implementation");  
}  

public class MyClass : IMyInterface  
{  
 // Перевизначає стандартну реалізацію, але не через `override`  
 public void DoWork()  
 {  
 Console.WriteLine("MyClass custom implementation");  
 }  
}

Висновок

Ключове слово override є важливим для поліморфізму (polymorphism) в C#, але воно значно більше, ніж просто "метод із тим самим іменем". Важливо зрозуміти, як воно взаємодіє з іншими можливостями мови (new, sealed, abstract, virtual, Default Interface Methods тощо) та які наслідки це має для проєктування, продуктивності та версіонування.

При проектуванні ваших класів та бібліотек, ретельно обирайте, де дозволяти або запечатувати перевизначення. Це забезпечить розширюваність там, де це має сенс, уникне майбутніх проблем із сумісністю та дасть вам контроль над поведінкою вашого коду.

Вам сподобалась стаття? Не забувайте ділитись вашими запитаннями та досвідом з override у коментарях! Якщо у вас є більш просунуті поради або реальні приклади використання, поділіться ними з спільнотою. Бажаю успіхів у програмуванні!

Перекладено з: Mastering override in C# — An Advanced Guide

Leave a Reply

Your email address will not be published. Required fields are marked *