5.05. ООП
ООП в C#
Основы классов
Как мы помним из основ ООП, класс - это шаблон (чертёж), описывающий структуру (какие данные может хранить), поведение (какие действия может выполнять) и способ создания (как инициализируется). Класс не занимает память сам по себе - он всего лишь описание. В C# синтаксис класса очень похож на Java:
public class Car
{
public string Model;
public int Year;
public void StartEngine()
{
Console.WriteLine("Двигатель запущен!");
}
}
Класс - это тип ссылочного вида, наследуемый от System.Object.
System.Object — корень всей иерархии типов. Все типы в .NET (включая string, массивы, классы, record, enum) неявно наследуют от System.Object.
И соответственно у System.Object есть важные методы:
ToString()- возвращает строковое представление, например,obj.ToString().Equals(object obj)проверяет равенство (мы его упоминали ранее когда говорили о сравнениях). К примеруobj1.Equals(obj2).GetHashCode()возвращает хеш-код для словарей. К примеру,dict[obj].GetType()возвращает тип объекта, к примеру,obj.GetType().Name.
object obj = "Hello";
Console.WriteLine(obj.GetType()); // System.String
Console.WriteLine(obj.ToString()); // Hello
Переопределяйте ToString(), Equals() и GetHashCode() в своих классах, если нужно осмысленное поведение.
Объект (или экземпляр) — это конкретный представитель класса, созданный в памяти с помощью оператора new, по принципу Класс имя = new Конструктор() :
Car myCar = new Car();
- myCar — переменная, ссылающаяся на объект.
- new Car() — вызов конструктора, создающего экземпляр в куче (heap).
myCar.Model = "Tesla";
myCar.Year = 2023;
myCar.StartEngine(); // Двигатель запущен!
Класс - тип, а объект - экземпляр данного типа.
Конструктор – это специальный метод, который вызывается при создании экземпляра класса new, он используется для инициализации объекта.
Пример:
class Car {
public string Brand { get; set; }
public string Model { get; set; }
// Конструктор
public Car(string brand, string model) {
Brand = brand;
Model = model;
}
}
// Использование
Car car1 = new Car("BMW", "X5");
Можно иметь несколько конструкторов – это называется перегрузкой конструкторов.
Пример простого класса:
class Car {
// Поля (состояние)
string brand;
string model;
int year;
// Методы (поведение)
public void StartEngine() {
Console.WriteLine("Двигатель запущен");
}
public void ShowInfo() {
Console.WriteLine($"Марка: {brand}, Модель: {model}, Год: {year}");
}
}
Атрибуты – это свойства и поля, которые описывают состояние объекта.
string brand; // поле
int year;
public string Model { get; set; } // свойство
Поле — это переменная, объявленная внутри класса. Хранит состояние объекта.
public class Person
{
public string Name; // открытое поле
private int _age; // приватное поле (по соглашению с подчёркиванием)
}
Свойство — это контролируемый доступ к полю. Позволяет добавлять логику при чтении/записи.
Auto-Property (автоматическое свойство) подразумевает, что C# автоматически создаёт скрытое резервное поле:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Использование:
var person = new Person();
person.Name = "Alice";
Console.WriteLine(person.Name);
Полное свойство (full property) подразумевает добавление валидации, логирования и прочего:
private int _age;
public int Age
{
get { return _age; }
set
{
if (value < 0)
throw new ArgumentException("Возраст не может быть отрицательным.");
_age = value;
}
}
Можно устанавливать свойства только для чтения:
public string FullName => $"{FirstName} {LastName}";
или
public DateTime CreatedAt { get; } = DateTime.Now;
Метод — это функция, принадлежащая классу. Описывает, что объект умеет делать. Методы в C# используются для выполнения задач и могут принимать параметры и возвращать значения.
public void Drive()
{
Console.WriteLine("Машина едет...");
}
public int CalculateAge(int birthYear)
{
return DateTime.Now.Year - birthYear;
}
Методы могут принимать параметры. Специальный модификатор params позволяет передавать произвольное количество аргументов одного типа:
public void PrintNames(params string[] names)
{
foreach (string name in names)
Console.WriteLine(name);
}
// Вызов:
PrintNames("Alice", "Bob", "Charlie"); // OK
PrintNames(); // OK — пустой массив
И поскольку класс является типом, то указывая тип возвращаемого значения метода, можно указывать соответствующий класс.
Ключевое слово this — это ссылка на текущий объект. То есть, текущий экземпляр.
public class Person
{
private string name;
// Разрешение конфликта имён
public Person(string name)
{
this.name = name; // this.name — поле, name — параметр
}
// Вызов другого конструктора
public Person() : this("Anonymous") { }
// Передача текущего объекта
public void PrintInfo()
{
Console.WriteLine($"Имя: {this.name}");
}
// Возврат самого себя (для цепочки вызовов)
public Person SetName(string name)
{
this.name = name;
return this;
}
}
this помогает избежать неоднозначности и явно указать, что вы работаете с текущим экземпляром.
Цепочка конструкторов: this() используется для того, чтобы избежать дублирования кода:
public Person() : this("Unknown", 0) { }
public Person(string name) : this(name, 0) { }
public Person(string name, int age)
{
Name = name;
Age = age;
}
Деструктор (финализатор) — метод, вызываемый перед удалением объекта сборщиком мусора. Используется редко — только для освобождения неуправляемых ресурсов (например, файловых дескрипторов).
~Person()
{
// Очистка неуправляемых ресурсов
Console.WriteLine("Объект Person уничтожен.");
}
partial class позволяет разделить определение класса на несколько файлов. Компилятор объединяет их в один тип.
// File1.cs
partial class Calculator
{
public void Add(int a, int b) => Console.WriteLine(a + b);
}
// File2.cs
partial class Calculator
{
public void Multiply(int a, int b) => Console.WriteLine(a * b);
}
Это используется для генерируемого кода, разделения логики, или удобного редактирования больших классов.
partial method, объявленный в одном файле, может быть реализован в другом (или нет).
// В генерируемом коде
partial void OnNameChanged();
// В пользовательском коде
partial void OnNameChanged()
{
Console.WriteLine("Имя изменилось!");
}
Если реализация не предоставлена — компилятор удаляет вызов, не оставляя накладных расходов.
В C# есть перечисления (enum).
enum — это тип, представляющий набор именованных констант.
public enum DayOfWeek
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
По умолчанию — int, начиная с 0.
Соответственно, задавать значения можно явно:
public enum HttpStatus
{
Ok = 200,
NotFound = 404,
ServerError = 500
}
record — легковесные классы для данных, это специальный тип класса, предназначенный для хранения данных и сравнения по значению, а не по ссылке.
public record Person(string Name, int Age);
По умолчанию record неизменяемые, сравниваются по значению и автоматически генерируют ToString(), Equals(), GetHashCode()
В C# не важно, в каком порядке вы объявляете поля, свойства, методы, конструкторы и другие члены класса. Вы можете вызывать метод, который объявлен ниже по коду, использовать свойство, которое идёт после метода, где оно используется, обращаться к полю, которое объявлено в конце класса, помещать public члены перед private, или наоборот - как удобно. Это гарантировано стандартом языка и работает во всех стандартных реализациях. Потому что C# — это язык с однофазной семантической проверкой. Компилятор C# не обрабатывает файл «сверху вниз» построчно, как это делают некоторые интерпретируемые языки. Вместо этого он сначала парсит весь исходный код и строит абстрактное семантическое дерево (AST), анализирует весь класс целиком, собирает информацию обо всех его членах, и только потом проверяет, правильно ли они используются (например, вызываются методы, ссылаются поля и т.д.). Это называется отложенной семантической проверкой.
public class Calculator
{
public int AddAndDouble(int a, int b)
{
int sum = Add(a, b); // Работает, хотя Add объявлен ниже
return Double(sum); // Double тоже ниже
}
private int Add(int x, int y)
{
return x + y;
}
private int Double(int value)
{
return value * 2;
}
}
Хотя порядок не важен для компилятора, важно удобство чтения и поддержки кода. Вот популярные подходы:
Распространённый порядок (от Microsoft и большинства стилей):
- Поля (приватные, затем публичные)
- Константы
- Свойства
- События
- Конструкторы
- Финализаторы / Деструкторы
- Методы
- Вложенные типы
Альтернатива: по видимости - сначала public, затем protected, internal, private
Или по логике (рекомендуется в сложных классах), в таком случае группируйте связанные методы и свойства вместе. Например: блок «инициализация», «валидация», «сохранение» и т.д.
Если вы используете partial классы (например, в WPF или при генерации кода), члены могут быть разделены по разным файлам. И это ещё раз подчёркивает: C# не зависит от порядка — компилятор объединяет все части и анализирует их как единое целое.
Модификаторы доступа
После знакомства с основами классов, объектов, свойств и методов, пришло время понять, как управлять доступом к этим элементам и как делиться состоянием между всеми экземплярами. На помощь приходят модификаторы доступа и статические члены.
★ Инкапсуляция – принцип, согласно которому данные и методы, работающие с ними, объединяются в одном классе, а прямой доступ к внутреннему состоянию ограничивается. Нужна для защиты данных от некорректного изменения, упрощения взаимодействия с объектом и сокрытия сложной реализации.
Без инкапсуляции код выглядит вот так:
public class BankAccount
{
public decimal Balance; // Прямо доступно!
}
...
var account = new BankAccount();
account.Balance = -1000; // Ошибка! Баланс не может быть отрицательным
А вот так считается безопаснее благодаря инкапсуляции:
public class BankAccount
{
private decimal _balance;
public decimal Balance
{
get => _balance;
private set
{
if (value < 0)
throw new InvalidOperationException("Баланс не может быть отрицательным.");
_balance = value;
}
}
public void Deposit(decimal amount)
{
if (amount <= 0) throw new ArgumentException("Сумма должна быть положительной.");
Balance += amount;
}
}
Поведение контролируется, данные защищены благодаря как раз-таки инкапсуляции - комбинации приватных полей и публичных свойств/методов.
Модификаторы доступа определяют, какие части кода могут обращаться к полям, свойствам, методам, классам и другим членам.
| Модификатор | Доступность |
|---|---|
private | Только внутри того же класса |
public | Повсеместный доступ без ограничений |
protected | Внутри класса и его наследников |
internal | В пределах одной сборки (проекта) |
protected internal | Внутри сборки или в любом наследнике, даже вне сборки |
private protected | Внутри той же сборки и только в её наследниках |
В C# поля и свойства имеют различия друг от друга.
Поле — это член класса или объекта, предназначенный для хранения данных, в то время как свойство — это член класса, который предоставляет методы для чтения, записи и вычисления значения соответствующего поля.
По умолчанию члены класса private, а классы internal.
private — только внутри класса. Наиболее строгий уровень. Доступен только в пределах класса. Используйте по умолчанию и открывайте доступ только при необходимости:
public class Logger
{
private string _logFile = "app.log"; // скрыто от внешнего мира
private void WriteToFile(string message)
{
File.AppendAllText(_logFile, message);
}
public void Log(string message)
{
WriteToFile($"[{DateTime.Now}] {message}");
}
}
public — полный доступ. Доступен всем, кто может видеть класс.
public class Calculator
{
public int Add(int a, int b) => a + b; // любой может использовать
}
protected — доступ у наследников. Доступен в текущем классе и всех его наследниках, даже если они в другом проекте. Как раз подойдёт базовым классам, где нужно передавать логику наследникам.
public class Animal
{
protected string Name;
protected Animal(string name)
{
Name = name;
}
protected virtual void MakeSound()
{
Console.WriteLine("Звук...");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
protected override void MakeSound()
{
Console.WriteLine($"{Name} лает!");
}
}
internal — только внутри сборки. Доступен внутри одного проекта (сборки). Вне проекта — как private. Используется для вспомогательных классов, которые не должны быть видны извне, но нужны внутри проекта.
internal class DatabaseHelper
{
internal string ConnectionString = "server=localhost;";
}
C# следит за согласованностью уровней доступа. Компилятор запретит:
Пример 1: публичный метод возвращает приватный тип
private class SecretData { }
public class Service
{
public SecretData GetData() // ОШИБКА!
{
return new SecretData();
}
}
Пример 2: более открытый доступ у метода, чем у класса
internal class Utility
{
public void DoWork() // ОШИБКА: public метод в internal классе
{
}
}
Правила согласованности: член не может быть доступнее, чем содержащий его тип. Уровень доступа члена не может быть выше, чем у содержащего его типа. Возвращаемый тип и параметры метода должны быть не менее доступны, чем сам метод.
А для общего состояния и поведения используется механизм статических членов.
Ключевое слово static означает, что член принадлежит не экземпляру, а самому типу. Представьте: у нас есть 100 объектов Car. Если поле static — оно одно на всех.
Статические поля хранят общее состояние для всех экземпляров.
public class Counter
{
private static int _totalCount = 0;
public int InstanceId { get; }
public Counter()
{
InstanceId = ++_totalCount;
}
public static int GetTotalCount() => _totalCount;
}
var c1 = new Counter(); // InstanceId = 1
var c2 = new Counter(); // InstanceId = 2
Console.WriteLine(Counter.GetTotalCount()); // 2
Используется это для счётчиков, кэшей, конфигураций.
Статические методы не требуют создания экземпляра и вызываются через имя типа.
public class MathUtils
{
public static double Square(double x) => x * x;
public static double Max(double a, double b) => a > b ? a : b;
}
double result = MathUtils.Square(5); // 25
Это можно использовать для утилит, не зависящих от состояния объекта.
Статические свойства:
public class AppSettings
{
public static string ApiUrl { get; set; } = "https://api.example.com";
public static int Timeout { get; set; } = 30;
}
Статический конструктор выполняется один раз при первом обращении к классу (или при создании экземпляра). Используется для инициализации статических полей, особенно с логикой:
public class Logger
{
private static readonly string _logPath;
static Logger()
{
_logPath = Path.Combine(Environment.CurrentDirectory, "logs.txt");
Directory.CreateDirectory(Path.GetDirectoryName(_logPath));
}
public static void Log(string message)
{
File.AppendAllText(_logPath, $"{DateTime.Now}: {message}\n");
}
}
Класс, помеченный static, не может иметь экземпляров и может содержать только статические члены. Используется для утилитарных классов, которые не должны создавать объекты.
public static class MathHelper
{
public static double Pi => 3.14159;
public static double CircleArea(double radius)
{
return Pi * radius * radius;
}
public static int Factorial(int n)
{
return n <= 1 ? 1 : n * Factorial(n - 1);
}
}
Как можно понять, в принципе static использовать можно для утилит, глобальных настроек, счётчиков, статистики, кэшей, констант. static помогает делиться состоянием и поведением между всеми экземплярами — но используйте его с умом.
Статические члены предоставляют механизм глобального доступа к данным и поведению без привязки к экземплярам типа. Такой подход удобен для утилит, конфигураций и счётчиков, однако несёт определённые риски, особенно при работе с изменяемым состоянием.
Риски, связанные с изменяемыми статическими полями
Статические поля инициализируются один раз при первом обращении к типу и сохраняют своё значение на протяжении всего жизненного цикла приложения. Это означает, что любое изменение такого поля влияет на все части программы, использующие его. Последствия:
- Сложность локализации изменений: так как статическое состояние доступно глобально, отследить, какое именно место кода его изменило, может быть затруднительно.
- Проблемы с тестируемостью: статическое состояние не поддаётся изоляции в юнит-тестах, что усложняет написание независимых и воспроизводимых проверок.
- Сложности многопоточности: при отсутствии синхронизации доступ к общему изменяемому статическому полю из нескольких потоков может привести к гонкам данных.
Принципы безопасного использования
-
Избегайте изменяемых статических полей
Статические поля должны быть неизменяемыми (readonly) или константами (const). Это гарантирует, что состояние остаётся предсказуемым на протяжении исполнения программы. -
Статические методы должны быть чистыми функциями
Они не должны изменять внешнее состояние, полагаться на скрытые зависимости или иметь побочные эффекты. Пример — методы расширений в LINQ: они принимают входные данные и возвращают новые, не затрагивая исходные. -
Выносите общую логику в статические утилитарные классы
Если метод не использует состояние экземпляра, его логика может быть вынесена в отдельный статический класс. Это повышает читаемость и облегчает повторное использование. -
Не используйте статические поля для временных или сессионных данных
Статическое поле, ссылающееся на изменяемую коллекцию, например список, подвержено неожиданным изменениям из любого участка кода. В таких случаях предпочтительнее применять внедрение зависимостей (dependency injection) или явное управление временем жизни объекта.
Допустимые сценарии
- Утилитарные классы без состояния, такие как
Math,Path, или пользовательские аналоги (StringUtils,JsonHelper). - Методы расширений, реализующие функциональные преобразования.
- Инициализация глобальной конфигурации, если она неизменяема после загрузки.
- Специализированные случаи многопоточности, например поле с атрибутом
[ThreadStatic], когда каждому потоку требуется собственная копия данных.
Наследование и base
Наследование — это механизм, при котором один класс (производный) может наследовать данные и поведение другого класса (базового), расширяя или изменяя их. Это позволяет строить иерархии типов, избегать дублирования кода и реализовывать гибкое поведение через полиморфизм.
Сразу заметим, что C# не поддерживает множественное наследование классов.
При помощи наследования можно создавать дочерний класс от родительского, автоматически получая поля, свойства, методы, и косвенно даже конструкторы. При этом производный класс может расширять функциональность (добавлять новые члены), изменять поведение (переопределять виртуальные методы), специализировать поведение.
Наследование реализует принцип "IS-A":
Dog is a Animal
Button is a Control
То есть, базовый класс (base class) это тот, от которого наследуют, а производный класс (derived class) это тот, который наследует от базового.
Синтаксис через двоеточие:
class DerivedClass : BaseClass
{
// Наследует всё, что доступно из BaseClass
}
Пример:
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("Звук...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Гав!");
}
}
Ключевое слово base — доступ к базовому классу. base — это ссылка на базовый класс изнутри производного. Позволяет вызывать конструктор базового класса, а также его методы и свойства.
Если в базовом классе есть важная логика (например, валидация), её нужно сохранить. И чтобы не затереть базовую логику переопределением, нужно добавлять base и вызывать базовый метод:
public override void Start()
{
base.Start(); // сначала базовая логика
Console.WriteLine("Дополнительная инициализация...");
}
Производный класс не наследует конструкторы, но может вызвать конструктор базового класса с помощью base():
public class Animal
{
public Animal(string name)
{
Name = name;
}
}
public class Dog : Animal
{
public Dog(string name) : base(name) // вызов конструктора Animal
{
// дополнительная инициализация
}
}
Это цепочка конструкторов: сначала вызывается конструктор базового класса, потом — производного.
Если метод переопределён (override), можно вызвать оригинальную реализацию из базового класса:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Звук...");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
base.MakeSound(); // сначала базовая логика
Console.WriteLine("Гав!"); // потом своя
}
}
Используется, когда нужно дополнить, а не полностью заменить поведение.
Говоря о переопределении, важно знать о virtual, override и sealed.
virtual — разрешить переопределение. Метод, помеченный virtual, может быть переопределён в производном классе.
public virtual void Start()
{
Console.WriteLine("Двигатель запущен");
}
По умолчанию методы не виртуальные — их нельзя переопределить. Поэтому если планируете переопределение - добавляйте virtual.
override — переопределить виртуальный метод. Производный класс заменяет реализацию виртуального метода.
public override void MakeSound()
{
Console.WriteLine("Гав!");
}
Если метод не virtual, abstract или override, компилятор запретит использовать override.
sealed override — запретить дальнейшее переопределение. Можно заблокировать возможность дальнейшего переопределения в цепочке наследования.
public class Wolf : Animal
{
public sealed override void MakeSound()
{
Console.WriteLine("Аууу!");
}
}
public class CyberWolf : Wolf
{
// public override void MakeSound() // ОШИБКА! Заблокировано
}
Полезно, когда логика критична и не должна изменяться дальше.
Повторное использование кода — одна из главных целей наследования. Вместо дублирования:
class Dog { public string Name; public void Eat() { ... } }
class Cat { public string Name; public void Eat() { ... } } // дубль!
Можно вынести общее в базовый класс:
class Animal
{
public string Name;
public void Eat() { Console.WriteLine("Ест..."); }
}
class Dog : Animal { }
class Cat : Animal { }
Теперь Dog и Cat автоматически имеют Name и Eat().
Порой нужно запретить наследование. sealed — запрет наследования. Если класс помечен как sealed, от него нельзя наследовать.
sealed class StringHelper
{
public static string Reverse(string s) => new(s.Reverse().ToArray());
}
Используется, когда класс завершён и не предназначен для расширения, важна производительность (компилятор может оптимизировать вызовы) или нужно предотвратить подмену поведения (например, в безопасности).
Класс, объявленный как static, автоматически становится abstract (нельзя создавать экземпляры) и sealed (нельзя наследовать).
static class MathHelper
{
public static double Square(double x) => x * x;
}
Такие классы содержат только статические члены и используются как утилиты.
Если базовый класс содержит abstract методы, производный класс обязан их реализовать.
public abstract class Animal
{
public abstract void MakeSound(); // НЕТ реализации
}
public class Dog : Animal
{
public override void MakeSound() // ОБЯЗАТЕЛЬНО
{
Console.WriteLine("Гав!");
}
}
Если не реализовать — ошибка компиляции.
Абстракция
Абстрактные классы: abstract class, абстрактные методы
Интерфейсы: interface, реализация
Отличия: когда что использовать
Множественная реализация интерфейсов
Интерфейсы с телом методов (C# 8+)
★ Абстракция – выделение ключевых характеристик объекта и игнорирование деталей реализации. Нужна для упрощения работы с объектами, скрытия сложности и формирования чёткого интерфейса.
Интерфейсы и абстрактные классы используются для определения методов, которые должны быть реализованы в производных классах.
Все интерфейсы начинаются с I — это стандарт .NET: I = Interface.
Пример абстракции:
abstract class Shape {
public abstract double Area();
}
class Circle : Shape {
private double radius;
public Circle(double r) => radius = r;
public override double Area() => Math.PI * radius * radius;
}
Пример интерфейса:
interface IDrawable {
void Draw();
}
class Rectangle : IDrawable {
public void Draw() {
Console.WriteLine("Рисую прямоугольник");
}
}
Интерфейс не может содержать реализацию, не может иметь поля, не может быть статическим, поддерживает множественное наследование.
Интерфейсы (Interfaces) – это контракты, которые определяют, какие методы и свойства должен реализовать класс. Пример:
interface IDrivable {
void Start();
void Stop();
}
Реализация интерфейса:
class Car : IDrivable {
public void Start() {
Console.WriteLine("Запуск двигателя");
}
public void Stop() {
Console.WriteLine("Остановка двигателя");
}
}
Класс может реализовывать несколько интерфейсов, но наследуется от одного класса.
Таким образом, если нужно:
- создать новый тип данных: создаём класс;
- описать поведение объекта: добавляем методы;
- сохранять состояние объекта: добавляем поля и свойства;
- обеспечить стандартное поведение для разных классов: используем интерфейс;
- создать объект по шаблону: используем конструктор.
Полиморфизм
Что такое полиморфизм
Перегрузка методов (overloading)
Переопределение методов (overriding)
Позднее связывание (runtime polymorphism)
★ Полиморфизм позволяет объектам разных классов обрабатываться как объекты одного типа, при этом выполняя «свою» версию метода. Нужен для обеспечения единого интерфейса для разных типов, гибкости и расширяемости кода, а также поддержки принципа замены Барбары Лисков (классы-наследники не должны противоречить базовому классу).
Пример:
Animal a = new Animal();
Animal d = new Dog();
a.MakeSound(); // Звук...
d.MakeSound(); // Гав!
Полиморфизм в C# бывает следующих типов:
- статический (раннее связывание) – перегрузка методов;
- динамический (позднее связывание) – переопределение методов (virtual / override).
★ Переопределение метода (override) это механизм, при котором производный класс изменяет реализацию метода, унаследованного от базового класса. То есть, дочерний класс может изменить поведение, унаследованное от родителя. В базовом классе метод помечается как virtual, а в производном переопределяется при помощи override:
class Animal {
public virtual void MakeSound() {
Console.WriteLine("Звук...");
}
}
class Dog : Animal {
public override void MakeSound() {
Console.WriteLine("Гав!");
}
}
★ Перегрузка метода (overloading) – это возможность создания нескольких методов с одним и тем же именем, но с разными параметрами. Методы считаются перегруженными, если они отличаются количеством параметров, типом параметров, порядком параметров.
Пример:
class Calculator {
public int Add(int a, int b) {
return a + b;
}
public double Add(double a, double b) {
return a + b;
}
public int Add(int a, int b, int c) {
return a + b + c;
}
}
Использование:
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(2, 3)); // 5 (int)
Console.WriteLine(calc.Add(2.5, 3.5)); // 6.0 (double)
Console.WriteLine(calc.Add(1, 2, 3)); // 6 (три параметра)
Пример полиморфизма:
interface IDrawable
{
void Draw();
}
class Circle : IDrawable
{
public void Draw() => Console.WriteLine("Circle");
}
class Square : IDrawable
{
public void Draw() => Console.WriteLine("Square");
}
Конструкторы должны быть перегружены, чтобы определить несколько конструкторов для любого заданного класса. Для этого должны быть определены параметризованные конструкторы, которые могут принимать внешние параметры. Статические конструкторы не могут быть вызваны напрямую, а только через CLR, которая не может передать параметр параметризованному конструктору.
В C# реализованы различные виды полиморфизма:
- Ad-hoc полифорфизм (или специфический полиморфизм) — это возможность использования одного и того же имени функции или оператора для выполнения разных действий в зависимости от контекста вызова. Пример - перегрузка методов (несколько методов с одинаковым именем, но разными параметрами) и неявное приведение типов (можно передать int в метод, который ожидает double - так происходит неявное преобразование).
- Параметрический полиморфизм (обобщённое программирование) достигается через обобщённые типы и методы (generics). Он позволяет создавать классы, интерфейсы и методы, которые могут работать с любыми типами данных без потери безопасности типов. Об этом мы поговорим в разделе коллекций.
- Полиморфизм включения (подтиповое наследование) основан на механизме наследования и позволяет использовать объект подкласса там, где ожидается объект базового класса или интерфейса. Можно вызывать переопределённые методы динамически во время выполнения (динамический полиморфизм). К полиморфизму включения относятся интерфейсы, абстрактные классы, виртуальные методы (virtual / override) и методы с явным переопределением (new).