Интерфейсы в C#: полное руководство с примерами

Интерфейсы — это основа гибкого, слабосвязанного и тестируемого кода в C#. Они определяют набор членов (методов, свойств, событий), которые должны быть реализованы классом или структурой. В этой статье мы рассмотрим всё, что нужно знать об интерфейсах: от базового синтаксиса до современных возможностей вроде методов по умолчанию и ковариантности.

Что такое интерфейс?

Интерфейс — это контракт. Если класс реализует интерфейс, он обязуется предоставить реализацию всех его членов. Это позволяет писать код, который зависит от абстракции, а не от конкретной реализации, что упрощает замену компонентов, тестирование и расширение системы.

Синтаксис объявления и реализации

Интерфейс объявляется с ключевым словом 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 может, но это скорее исключение). Может содержать как абстрактные, так и полностью реализованные методы.
Предназначен для определения контракта. Предназначен для частичной реализации и общей логики.

Правило большого пальца: если вы определяете возможность («что объект умеет делать») — используйте интерфейс. Если вы хотите предоставить общую реализацию или состояние — используйте абстрактный класс.

Практические советы и лучшие практики

Заключение

Интерфейсы — мощный инструмент проектирования в C#. Они позволяют строить гибкие, слабосвязанные системы, облегчают тестирование и поддержку кода. Современные версии языка добавили новые возможности (методы по умолчанию, ковариантность), делая интерфейсы ещё более выразительными. Освоив их, вы сможете писать более качественный и профессиональный код.