Ответ
Модификаторы 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>.
Правила и почему это безопасно:
out(ковариантность): Гарантирует, чтоTбудет использоваться только для возврата значений. Вы не сможете объявить метод, принимающийTкак параметр. Это безопасно, потому что потребитель интерфейсаIProducer<Animal>ожидает получитьAnimal, а реализацияIProducer<Dog>всегда вернетDog, который является подтипомAnimal.in(контравариантность): Гарантирует, чтоTбудет использоваться только как входной параметр. Вы не сможете объявить метод, возвращающийT. Это безопасно, потому что потребитель интерфейсаIConsumer<Dog>будет передавать в методConsumeаргумент типаDog, а реализацияIConsumer<Animal>готова принять любойAnimal, включаяDog.
Итог: out позволяет присваивать коллекции производных типов коллекциям базовых типов (можно читать). in позволяет присваивать делегаты/обработчики, работающие с базовыми типами, делегатам, работающим с производными типами (можно писать). Без этих модификаторов обобщенные типы являются инвариантными.
Ответ 18+ 🔞
А, вот эти ваши модификаторы in и out в дженериках! Ну, это ж классика, блядь. Сидишь такой, пишешь код, и тут бац — тебе нужно IEnumerable<Dog> запихнуть туда, где ждут IEnumerable<Animal>. А компилятор тебе: «Не-а, мудила, нельзя, типы разные». И стоишь ты, как дурак, с этими собаками и животными, и думаешь: «Да они же, сука, наследуются!».
Так вот, чтобы не выглядеть идиотом, и придумали ковариантность (out) и контравариантность (in). Это не магия, а просто способ сказать компилятору: «Расслабься, я знаю, что делаю, здесь всё безопасно».
Короче, out — это когда ты только получаешь (как будто «выводишь»).
Смотри, объявляешь интерфейс:
interface IProducer<out T> // Видишь `out`? Это ключ!
{
T GiveMeSomething(); // T используется ТОЛЬКО чтобы вернуть. Никаких входных параметров с T!
}
И теперь, если у тебя есть IProducer<Dog>, ты его спокойно можешь присвоить в IProducer<Animal>. Почему? Да потому что если эта хрень обещает вернуть Dog, то для того, кто ждёт Animal, это вообще шикарно — Dog ведь частный случай Animal. Всё логично. В .NET так устроен, например, IEnumerable<out T> — его можно только читать, поэтому он ковариантный.
А in — это полная противоположность, когда ты только отдаёшь (как будто «вводишь»).
interface IConsumer<in T> // А вот и `in`!
{
void TakeThis(T item); // T используется ТОЛЬКО как вход. Никаких возвратов T!
}
И тут магия в другую сторону. Если у тебя есть IConsumer<Animal>, ты его можешь присвоить в IConsumer<Dog>. Звучит как бред? А вот и нет. Потому что если твой обработчик умеет работать с любым Animal, то с конкретным Dog он уж точно справится, ему не привыкать. Классический пример — делегат Action<in T>.
А теперь самое важное, почему это не ерунда, а гениально и безопасно:
Компилятор — он не дурак, он следит за тобой, как ястреб. Если ты пометил параметр как out, то ты не сможешь объявить метод, который принимает этот T как аргумент. Только возвращать. И наоборот, с in — нельзя вернуть T, только принять. Это чтобы ты, балда, случайно не выстрелил себе в ногу, пытаясь запихнуть кота в интерфейс для собак.
Итог простой, как три копейки:
out(читай «отсюда») — позволяет использовать более конкретный тип вместо более общего.Что-то<Сын>превращается вЧто-то<Отец>. Можно получать.in(читай «сюда») — позволяет использовать более общий тип вместо более конкретного.Что-то<Отец>превращается вЧто-то<Сын>. Можно передавать.
А если эти модификаторы не указать, то дженерик будет инвариантным — то есть, List<Dog> и List<Animal> это два абсолютно разных, несовместимых типа, хоть ты тресни. Вот такая, блядь, строгость.