В чем разница между модификаторами in и out в C# (в контексте обобщенных типов)?

«В чем разница между модификаторами in и out в C# (в контексте обобщенных типов)?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Модификаторы in и out в C# используются для объявления вариантности (variance) в универсальных (обобщенных) интерфейсах и делегатах. Они позволяют обеспечить безопасное приведение обобщенного типа с одним параметром типа к обобщенному типу с другим параметром типа.

  • Ковариантность (out): Позволяет использовать более производный тип, чем заданный изначально. Ключевое слово out указывает, что параметр типа используется только как возвращаемое значение (выходной параметр).

    // Объявление ковариантного интерфейса
    interface IProducer<out T>
    {
        T Produce(); // T используется только на выходе
    }
    
    class Animal { }
    class Dog : Animal { }
    
    IProducer<Dog> dogProducer = ...;
    // Безопасное приведение: IProducer<Dog> -> IProducer<Animal>
    IProducer<Animal> animalProducer = dogProducer; // Ковариантность разрешена благодаря `out`
    Animal a = animalProducer.Produce(); // Вернет Dog, который является Animal

    Пример в .NET: IEnumerable<out T>.

  • Контравариантность (in): Позволяет использовать более универсальный (базовый) тип, чем заданный изначально. Ключевое слово in указывает, что параметр типа используется только как входной параметр.

    // Объявление контравариантного интерфейса
    interface IConsumer<in T>
    {
        void Consume(T item); // T используется только на входе
    }
    
    IConsumer<Animal> animalConsumer = ...;
    // Безопасное приведение: IConsumer<Animal> -> IConsumer<Dog>
    IConsumer<Dog> dogConsumer = animalConsumer; // Контравариантность разрешена благодаря `in`
    dogConsumer.Consume(new Dog()); // Метод ожидает Animal, а получает Dog (это безопасно)

    Пример в .NET: Action<in T>.

Правила и почему это безопасно:

  1. out (ковариантность): Гарантирует, что T будет использоваться только для возврата значений. Вы не сможете объявить метод, принимающий T как параметр. Это безопасно, потому что потребитель интерфейса IProducer<Animal> ожидает получить Animal, а реализация IProducer<Dog> всегда вернет Dog, который является подтипом Animal.
  2. in (контравариантность): Гарантирует, что T будет использоваться только как входной параметр. Вы не сможете объявить метод, возвращающий T. Это безопасно, потому что потребитель интерфейса IConsumer<Dog> будет передавать в метод Consume аргумент типа Dog, а реализация IConsumer<Animal> готова принять любой Animal, включая Dog.

Итог: out позволяет присваивать коллекции производных типов коллекциям базовых типов (можно читать). in позволяет присваивать делегаты/обработчики, работающие с базовыми типами, делегатам, работающим с производными типами (можно писать). Без этих модификаторов обобщенные типы являются инвариантными.