Что такое интерфейс?
Интерфейс — это контракт. Если класс реализует интерфейс, он обязуется предоставить реализацию всех его членов. Это позволяет писать код, который зависит от абстракции, а не от конкретной реализации, что упрощает замену компонентов, тестирование и расширение системы.
Синтаксис объявления и реализации
Интерфейс объявляется с ключевым словом interface. По соглашению имена интерфейсов начинаются с заглавной буквы I.
public interface IMovable
{
void Move(int distance);
int Speed { get; set; }
}
Класс реализует интерфейс, указывая его имя после двоеточия и предоставляя реализацию всех членов:
public class Car : IMovable
{
public int Speed { get; set; }
public void Move(int distance)
{
Console.WriteLine($"Машина едет {distance} км со скоростью {Speed} км/ч");
}
}
Интерфейс могут реализовывать не только классы, но и структуры (struct).
Множественная реализация интерфейсов
В C# класс может наследовать только один базовый класс, но реализовывать множество интерфейсов. Это позволяет достичь гибкости, аналогичной множественному наследованию, без его проблем.
public interface IPrintable
{
void Print();
}
public interface ISerializable
{
void Save(string file);
}
public class Document : IPrintable, ISerializable
{
public void Print() => Console.WriteLine("Печать документа");
public void Save(string file) => Console.WriteLine($"Сохранение в {file}");
}
Интерфейсы как контракты и полиморфизм
Благодаря интерфейсам мы можем работать с разными объектами единообразно:
public void ProcessMovable(IMovable movable)
{
movable.Move(10);
}
// Использование
Car car = new Car();
Bicycle bicycle = new Bicycle(); // предположим, Bicycle тоже реализует IMovable
ProcessMovable(car);
ProcessMovable(bicycle);
Это основа полиморфизма и принципа подстановки Лисков.
Явная реализация интерфейсов
Если класс реализует два интерфейса с одинаковыми членами, или мы хотим скрыть реализацию от публичного API класса, используется явная реализация. При этом метод доступен только через ссылку на интерфейс.
public interface IWriter
{
void Write(string text);
}
public interface ILogger
{
void Write(string message);
}
public class FileProcessor : IWriter, ILogger
{
// Явная реализация IWriter
void IWriter.Write(string text)
{
Console.WriteLine($"Запись текста: {text}");
}
// Явная реализация ILogger
void ILogger.Write(string message)
{
Console.WriteLine($"Логирование: {message}");
}
}
// Использование
var processor = new FileProcessor();
// processor.Write("hello"); // Ошибка! Неоднозначность или метод скрыт
IWriter writer = processor;
writer.Write("Hello"); // Вызов IWriter.Write
ILogger logger = processor;
logger.Write("Hello"); // Вызов ILogger.Write
Явная реализация также полезна, когда интерфейс содержит члены, не имеющие смысла в публичном API класса (например, служебные методы).
Методы по умолчанию в интерфейсах (C# 8.0+)
Начиная с C# 8, можно предоставлять реализацию по умолчанию для членов интерфейса. Это позволяет добавлять новые методы в интерфейс без поломки существующих реализаций.
public interface ICalculator
{
int Add(int x, int y);
// Метод с реализацией по умолчанию
int Multiply(int x, int y) => x * y;
}
public class SimpleCalculator : ICalculator
{
public int Add(int x, int y) => x + y;
// Multiply не требуется реализовывать
}
// Использование
ICalculator calc = new SimpleCalculator();
Console.WriteLine(calc.Multiply(3, 4)); // 12
Обратите внимание: методы по умолчанию доступны только через ссылку на интерфейс. Если у класса есть собственная реализация, она имеет приоритет.
Ковариантность и контравариантность в интерфейсах
В C# обобщённые интерфейсы могут быть помечены как ковариантные (out) или контравариантные (in). Это позволяет более гибко работать с типами.
Ковариантность (out)
Позволяет использовать более производный тип, чем указан изначально. Применимо только для возвращаемых значений.
public interface IProducer<out T>
{
T Produce();
}
public class Animal { }
public class Dog : Animal { }
public class DogProducer : IProducer<Dog>
{
public Dog Produce() => new Dog();
}
// Использование
IProducer<Dog> dogProducer = new DogProducer();
IProducer<Animal> animalProducer = dogProducer; // Ковариантность работает!
Контравариантность (in)
Позволяет использовать менее производный тип, чем указан. Применимо только для входных параметров.
public interface IConsumer<in T>
{
void Consume(T item);
}
public class AnimalConsumer : IConsumer<Animal>
{
public void Consume(Animal animal) => Console.WriteLine("Animal consumed");
}
// Использование
IConsumer<Animal> animalConsumer = new AnimalConsumer();
IConsumer<Dog> dogConsumer = animalConsumer; // Контравариантность!
dogConsumer.Consume(new Dog());
Эти возможности широко используются в стандартных интерфейсах .NET, например IEnumerable<out T> (ковариантный) и IComparer<in T> (контравариантный).
Интерфейсы или абстрактные классы?
Часто возникает вопрос: что выбрать — интерфейс или абстрактный класс? Вот основные различия:
| Интерфейс | Абстрактный класс |
|---|---|
| Не может содержать поля (кроме статических). | Может содержать поля, конструкторы, деструкторы. |
| Все члены по умолчанию public (до C# 8) и не могут иметь модификаторов доступа (кроме explicit). | Члены могут иметь любые модификаторы доступа. |
| Класс может реализовать множество интерфейсов. | Класс может наследовать только один абстрактный класс. |
| Не может содержать реализацию (до C# 8; с C# 8 может, но это скорее исключение). | Может содержать как абстрактные, так и полностью реализованные методы. |
| Предназначен для определения контракта. | Предназначен для частичной реализации и общей логики. |
Правило большого пальца: если вы определяете возможность («что объект умеет делать») — используйте интерфейс. Если вы хотите предоставить общую реализацию или состояние — используйте абстрактный класс.
Практические советы и лучшие практики
- Принцип разделения интерфейсов (ISP): интерфейсы должны быть узкоспециализированными. Не создавайте «толстые» интерфейсы с кучей методов — разбивайте их на мелкие.
- Зависимость от абстракций (DIP): старайтесь ссылаться на интерфейсы, а не на конкретные классы (например, передавать в метод
IEnumerable, а неList). - Используйте интерфейсы для тестирования: с их помощью легко подменять реальные зависимости моками (Moq, NSubstitute).
- Избегайте интерфейсов-маркеров (без членов): используйте атрибуты, если нужно просто пометить класс.
- Соблюдайте соглашения об именовании: начинайте имя с
I, делайте названия осмысленными (IRepository,ILogger).
Заключение
Интерфейсы — мощный инструмент проектирования в C#. Они позволяют строить гибкие, слабосвязанные системы, облегчают тестирование и поддержку кода. Современные версии языка добавили новые возможности (методы по умолчанию, ковариантность), делая интерфейсы ещё более выразительными. Освоив их, вы сможете писать более качественный и профессиональный код.