
Этот текст призван объяснить различия в параметрах типов как в Java, так и в Scala. На эту тему уже есть много материалов в различных блогах и статьях, но мне они всегда казались либо слишком сложными, погружаясь прямо в расширенные функции, и за ними трудно следить для кого-то, кто новичок в этой теме, либо слишком упрощенными и просто царапающими. поверхность.
Итак, вот моя попытка восполнить этот пробел.
Сначала немного предыстории
Я полагаю, что большинство (или все) из вас хорошо знакомы с концепцией полиморфизма в объектно-ориентированном программировании. Круто иметь возможность, например, проверить, равен ли массив другому массиву, но без реализации метода равенства для каждого типа массива.
Мы просто хотим использовать метод equals () (предполагая, что он определен для каждого объекта, что верно для Java и Scala), и мы не хотим заботиться о том, какой именно тип объекта фактически содержится в множество.
В Java Integer [] является подклассом Number [], и оба они являются подклассами Object []. Это называется ковариацией. Для данного A ‹: B (что означает, что A является подклассом B), если T [A]‹: T [B], то T ковариантен по своему типу. Учитывая то же отношение A ‹: B, если T [B]‹: T [A], то T является контравариантным по своему типу.
Если вы впервые слышите о контравариантности, это, вероятно, сейчас не имеет для вас особого смысла, но дождитесь конца этой статьи. Наконец, если T [A] и T [B] не имеют отношения, несмотря на то, что A ‹: B, то мы говорим, что T инвариантно по своему типу.
Итак, в Java массивы ковариантны. У нас может быть метод isEqual (), sort () или shuffle (), который принимает массив объектов, и мы можем передать любой массив, который захотим. Ура! Мы также можем определить метод, взяв массив, например, геометрические фигуры и передаются в массиве GeometricShapes, Triangles, EquateralTriangles и т. д. Полиморфизм в лучшем виде.
Что ж, за это приходится платить. Допустим, у нас есть массив целых чисел. Это подкласс массива объектов, верно? Итак, если у меня есть переменная с именем «objectArray», которая представляет собой массив объектов, вполне допустимо присвоить наш массив целых чисел objectArray.
Теперь у нас есть переменная objectArray, которую компилятор видит как массив объектов, но под тем, что у нас на самом деле, находится массив целых чисел. Ладно, это не большая проблема… пока. Но теперь наш массив целых чисел представляет собой массив объектов в глазах компилятора! Нам разрешено хранить строки, треугольники и бананы - компилятор просто встанет и позволит этому произойти. Конечно, во время выполнения наш код выйдет из строя и сгорит, когда мы получим что-то из исходной переменной, содержащей массив целых чисел (или, точнее, переменную, которая * думает *, что она все еще содержит массив целых чисел, хотя на самом деле она содержит все виды вещи).
Мы столкнулись с проблемой. Но что делать? Сейчас нет пути назад (мы говорим о 2004 году), потому что уже существуют тысячи и тысячи коммерческих проектов по всему миру, использующих Java. Массивы должны оставаться взломанными. Но в Java 5 появилась новая классная вещь, называемая generics. Как вы, наверное, уже знаете, они написаны внутри ‹› на Java, тогда как Scala использует нотацию []. Обобщения позволяют любому классу иметь тег типа, как и массив, но в отличие от массивов эти теги типа не ковариантны.
Итак, в Java MyClass ‹String› не является подклассом MyClass ‹Object›. Это здорово, дженерики не могут рухнуть и сгореть! Но да, это означает, что у нас нет той изящной функции полиморфизма, которая была у нас с массивами. Метод, который принимает MyClass ‹Object›, не может быть передан экземпляру MyClass ‹String›. Мы также не можем передать список строк, если требуется список объектов. Это позор. Мы всегда можем вызвать некрасивое приведение типов, но это может быть опасно во время выполнения (возвращая нас к тому, что мы ненавидели в массивах), и считается большим запретом.
Границы спешат на помощь
В этой ситуации есть способ добиться полиморфизма (то есть ковариантности и контравариантности) - с помощью ограниченных подстановочных знаков. Существует два типа границ для подстановочных знаков - верхняя и нижняя границы. Верхняя граница позволяет ограничить тип «сверху», то есть указать, какой разрешенный наивысший уровень иерархии, в то время как нижняя граница ограничивает тип «снизу», указав самый низкий разрешенный тип. Вот пример подстановочного знака с ограничением сверху:
public void process(List<? extends Car> list) { ... }
Это означает, что process () принимает List, параметризованный Car или любым подклассом Car. Ура, ковариация! И если мы переключаем extends с помощью super, это становится нижней границей:
public void process(List<? super Car> list) { ... }
Теперь process () принимает только списки, параметризованные Car или любым суперклассом Car, таким образом достигая контравариантности.
Хороший. У нас есть желаемый полиморфизм. У нас есть метод process (), аргумент которого может быть ковариантным или контравариантным по своему типу, в зависимости от того, используем ли мы верхний или нижний ограниченный подстановочный знак. Обратите внимание, что границы в Java доступны не только для подстановочных знаков, но и для параметров типа, поэтому мы можем объявить такой класс, как MyClass ‹T extends Car›. Параметр типа (например, T) отличается от подстановочного знака (обозначается?) В том смысле, что его можно повторно использовать в остальной части кода. Есть два основных отличия в параметрах ограниченного типа от ограниченных подстановочных знаков:
- С помощью параметров типа вы можете указать более одной границы. Но поскольку в Java вы можете расширить только один суперкласс, другие границы определяют интерфейсы, которые должны быть реализованы. Например, MyClass ‹T extends Bird & CanSwim & CanRun› означает, что параметр типа для экземпляра MyClass должен расширять Bird и реализовывать CanSwim и CanRun.
- Для параметров типа можно использовать только верхние границы. Это означает, что MyClass ‹T extends Bird› в порядке, но MyClass ‹T super Bird› не компилируется. Причины этого выходят за рамки этой (и без того достаточно длинной) статьи, но мы все равно не будем использовать границы для параметров типа, так что я думаю, что это нормально. Позвольте мне просто добавить, что в Scala вы можете иметь как верхнюю, так и нижнюю границы для параметров типа, записанные как [T ‹: Bird] и [T›: Bird] соответственно.
Давайте вернемся к ограничениям на подстановочные знаки, поскольку они предоставляют нам как ковариацию, так и контравариантность. Мы достигли полиморфизма и можем предоставить нашему методу process () подтипы или супертипы List ‹Car›, в зависимости от того, объявили ли мы параметр списка как ковариантный (используя верхнюю границу) или контравариантный (используя нижнюю границу). Теперь есть некоторые важные ограничения при использовании ограниченных подстановочных знаков, которые необходимо учитывать: мы можем получать данные только из ковариантного списка, и мы можем помещать данные только в контравариантный список. Фактическое правило является немного более общим, поскольку параметризованный класс может быть чем угодно, а не только контейнером, подобным списку. Это выглядит следующим образом: мы можем использовать только параметр ковариантного типа в качестве возвращаемого типа, и мы можем использовать только параметр контравариантного типа в качестве типа ввода. Давайте объясним это на примере получения списка.
В случае ковариантного List ‹? extends Car ›(также известный как верхний ограниченный список), мы знаем, что внутри есть экземпляры Car. Некоторые из них - спортивные автомобили, некоторые - лимузины, может быть, у нас есть и другие подклассы (например, AstonMartin), но все они автомобили. Мы можем получить объекты из этого списка и знать, что мы получаем автомобиль. Однако, поскольку мы не знаем, каков фактический базовый тип (Car, SportsCar или AstonMartin), если бы такой список позволял помещать вещи внутрь, у нас возникла бы та же проблема, что и с массивами. Мы могли бы создать список спортивных автомобилей, безопасно назначить его переменной List ‹Car›, а затем «безопасно» (с точки зрения компилятора) поместить внутрь лимузины. Во время выполнения мы снова рушились и сгорали. Единственное, что нам разрешено, - это null, потому что он расширяет все, поэтому независимо от фактического базового типа - null расширяет его.
С другой стороны, в случае контравариантного List ‹? super Car ›(также известный как ограниченный снизу список), мы знаем, что в него безопасно помещать автомобили (включая такие подтипы, как AstonMartin; они также являются автомобилями). Каким бы ни был фактический базовый тип (например, Vehicle или AnyRef), каждый Автомобиль расширяет его, поэтому, если мы поместим AstonMartin, когда фактическим базовым типом является Автомобиль - мы не сделали ничего плохого, не так ли? AstonMartin - это автомобиль. Но сейчас ситуация противоположная, чем с верхней границей - хотя мы можем поставить автомобили, мы не можем получить ничего из этого списка (на самом деле, мы можем, но это будет тип Object, который не очень полезен). Мы не можем получить Автомобиль, потому что, если базовым типом является, скажем, Транспортное средство, тогда у нас будут проблемы. Мы не знаем, что это за тип, лежащий в основе - насколько нам известно, в Листе также может быть полно мотоциклов, тракторов и вездеходов-амфибий. Получение объекта типа Car из этого списка потенциально может вызвать исключение времени выполнения.
Вот все это в коде:
List<? extends Integer> a = new ArrayList<Integer>(); List<? super Integer> b = new ArrayList<Integer>(); a.add(3); // fails; let’s try with null a.add(null); // works b.add(3); // no problem here Integer ai = a.get(0); // no problem here either Integer bi = b.get(0); // fails; let’s try with Object Object o = b.get(0); // works
Вы также можете думать об этом так: в обоих случаях вы можете указать только самый низкий (самый конкретный) разрешенный тип, а вы можете получить только самый высокий (самый общий) разрешенный тип. Для ковариантных (ограниченных сверху) списков наименьшим допустимым типом является null, а наибольшим - Car, тогда как для контравариантных (ограниченных снизу) списков наименьшим допустимым типом является Car, а наивысшим - Object.
upper bound lower bound
null ------------------- Car ------------------- Object
Итак, мы можем получать данные только из ковариантного списка, а можем помещать данные только в контравариантный список. Это называется принципом получения и отдачи. А теперь вернемся к более общему правилу:
- используйте ковариацию для методов, возвращающих общий тип.
- используйте контравариантность для методов, которые принимают общий тип
- используйте инвариантность для методов, которые оба принимают и возвращают общий тип.
Краткое резюме: Массивы в Java ковариантны, что допускает грязные вещи, которые приводят к сбою и сгоранию нашего кода. Универсальные (мы также можем называть их параметризованными) классы инвариантны, что делает их невосприимчивыми к сбоям и сгоранию, но мы теряем полиморфизм. Мы можем достичь ковариантности и контравариантности (и вернуть полиморфизм) для каждого метода с помощью границ подстановочных знаков, но при определении дисперсии для метода мы должны помнить о принципе получения и ввода.
А что насчет Scala?
Прежде всего, в Scala массивы инвариантны, что исключает возможность сбоя и сжигания. Что касается списков, то они теперь по умолчанию ковариантны. Они могут быть ковариантными благодаря своей неизменности; нет опасности, что кто-то назначит список целых чисел списку объектов (поскольку неизменяемость запрещает повторное присвоение; вместо этого каждая операция «добавления» к списку возвращает новый список). Неизменяемость избавила нас от уродливой дилеммы между потерей полиморфизма и сценариями отказа и возгорания.
Но давайте будем честными - это не серьезное улучшение. Массивы просто пошли на меньшее зло (вместо того, чтобы быть склонными к сбою и сжиганию, теперь они не имеют полиморфизма), а списки стали ковариантными только потому, что они неизменяемы по умолчанию. Можно также использовать неизменяемые списки в Java (например, ImmutableList в библиотеке Guava) или реализовать свои собственные, и пойти на ковариацию, используя верхние границы.
Настоящее улучшение заключается в самом языке. Java поддерживает только вариант использования сайта, что означает, что отклонение определяется, когда параметр типа используется. На практике это означает, что он определяется отдельно для каждого метода с использованием ограниченных подстановочных знаков. .
Scala, с другой стороны, поддерживает как вариант использования сайта (синтаксис аналогичен Java, просто замените ‹? Extends T› на [_ ‹: T]), так и вариант сайта объявления. Объявление-site, как следует из названия, означает, что отклонение определяется, когда параметр типа объявлен. Вы можете просто объявить ковариацию, поставив «+» перед параметром типа и «-» для контравариантности (отсутствие знака означает инвариантность).
Итак, на Java вы бы сказали:
public class Foo<T> { ... }
...
Foo<? extends Integer> covariantFoo = new Foo<Integer>();
Foo<? super Integer> contravariantFoo = new Foo<Integer>();
Обратите внимание, как Foo кажется ковариантным в одном случае (covariantFoo) и контравариантным в другом (контравариантнымFoo). Он не объявляется заранее ковариантным или контравариантным; вместо этого его дисперсия определяется в точке использования (на месте использования).
В Scala вы можете сделать то же самое, только с несколько другим синтаксисом (используя [_ ‹: Integer] и [_›: Integer] соответственно), но вы также можете заранее объявить дисперсию (сайт объявления):
class CovariantFoo[+T] { ... }
class ContravariantFoo[-T] { ... }
...
val covariantFoo = new CovariantFoo[Int]()
val contravariantFoo = new ContravariantFoo[Int]()
На этот раз у нас есть два класса, каждый из которых объявляет свой параметр типа как ковариантный или контравариантный. Вот краткое изложение:
Covariance
if A is a subtype of B then:
Java: L<A> is a subtype of L<? extends B> (use-site)
Scala: L[A] is a subtype of L[_ <: B] (use-site)
L[A] is a subtype of L[+B] (declaration-site)
Contravariance
if A is a supertype of B then:
Java: L<A> is a subtype of L<? super B> (use-site)
Scala: L[A] is a subtype of L[_ >: B] (use-site)
L[A] is a subtype of L[-B] (declaration-site)
Какой подход лучше: сайт использования или сайт объявления? Что ж, ни то, ни другое не лучше. Это разные способы чего-то добиться. Я лично предпочитаю вариативность на уровне объявления, потому что она хорошо сочетается со всей функциональной парадигмой «неизменяемость, поэтому о ней легко рассуждать» (Scala также позволяет писать императивным, нефункциональным способом, но это крайне не рекомендуется). После того, как вы объявите дисперсию параметра (ов) вашего типа, вам не придется возиться с ними и изменять их в остальной части кода.
Кроме того, лучше переложить бремя объявления отклонений на себя, поскольку вы дизайнер; если вы возложите это бремя на клиентов вашего класса, они могут злоупотребить им. Чтобы процитировать Программирование на Scala [1]:
[С вариацией использования-сайта] Подстановочные знаки должны быть добавлены клиентам класса, и если они ошибаются, некоторые важные методы экземпляра больше не будут применяться. Дисперсия - дело непростое, пользователи обычно ошибаются и уходят, думая, что подстановочные знаки и универсальные шаблоны слишком сложны. Используя вариант определения-сайта, вы выражаете свое намерение компилятору, и компилятор дважды проверяет, действительно ли доступны методы, которые вы хотите использовать.
Что именно дважды проверяет компилятор Scala? Он проверяет, не нарушаете ли вы правила ковариации и контравариантности в определениях методов, используя ковариантные типы в контравариантных позициях и наоборот. Обратите внимание, что это прямо связано с принципом "получить - положить"; это просто его более обобщенная версия. Мы могли бы назвать это принципом ковариантная позиция - контравариантная позиция. В нем говорится, что ковариантные типы могут служить типами, возвращаемыми методом, но не типами параметров метода, тогда как для контравариантных типов все наоборот.
Давайте посмотрим на пример. Мы могли бы с самого начала вернуться к нашему старому примеру; в нем утверждалось, что в случае отсутствия принципа получить результат мы могли бы создать список спортивных автомобилей, безопасно назначить его переменной List<Car>, а затем поместить внутрь лимузины. Теперь, когда Scala отдает предпочтение неизменным значениям, это было бы не так уж плохо. Наш старый список спортивных автомобилей останется списком спортивных автомобилей, а новый список будет списком всех видов автомобилей. Нет опасности добавить лимузины в список, предназначенный только для спортивных автомобилей. Итак, позвольте мне использовать лучший пример правил ковариации и контравариантности.
Если Foo [T] ковариантен в своем типе T, это означает, что мы можем рассматривать некоторый Foo [SportsCar] как Foo [Car], верно? Хорошо, но если Foo [SportsCar] является подклассом Foo [Car], тогда он должен поддерживать все методы из Foo [Car] (и, возможно, добавлять некоторые из своих, более конкретных). А что, если у Foo есть метод, который использует значение типа T в качестве параметра (то есть в контравариантной позиции)? В этом конкретном методе не возникнет проблем с принятием лимузина в Foo [Car], но теперь в Foo [SportsCar] он неожиданно принимает только спортивные автомобили! У нас будет вызов, который работает в суперклассе (передача лимузина этому методу), но не в подклассе. Это было бы нарушением всей концепции подкласса-суперкласса. Методы, возвращающие значения типа T, подходят, поскольку при возврате спортивного автомобиля выполняется обязательство суперкласса вернуть автомобиль.
Для классов контравариантных по своему типу все наоборот. Передача T в методы будет приемлемой - поскольку Foo контравариантен по своему типу, это означает, что подклассы Foo - это все те, которые параметризованы супертипом Car, например Foo [Vehicle] или Foo [Any]. В этом случае метод взятия автомобиля превратился бы в метод взятия автомобиля или любого другого. Хорошо. Что необходимо учитывать, так это то, что методы, принимающие Car в исходном классе, должны иметь возможность принимать Car в подклассе, а не сужать его, как мы видели в примере SportsCar. И это действительно выполнено. Любой код, который может использовать методы Foo [Car] для наполнения его автомобилями, также будет работать, если мы заменим Foo [Car] на один из его подклассов, например Foo [Vehicle]. Подача автомобилей методу, принимающему Foo [Vehicle], - это нормально.
Но теперь, когда Foo контравариантен, мы сталкиваемся с проблемами, когда хотим, чтобы метод возвращал значение типа Car. В случае ковариации он вернет лимузины, спортивные автомобили, AstonMartins и т. Д., И все они, по сути, автомобили. Однако быть Foo [Car] и иметь ваш подкласс, возвращающий Foo [Vehicle] (не забывайте, что Foo контравариантен, поэтому его подкласс должен параметризоваться супертипом Car) было бы неправильно, потому что весь смысл наличия подклассом является то, что вы можете подключить его в любом месте, где требуется его родительский класс, и в данном случае это было бы невозможно. Использование подкласса, такого как Foo [Vehicle], ограничило бы результаты вызовов нашего метода транспортными средствами, что означает, что наш старый код больше не будет работать (возможно, он вызывает «хлопнуть дверью» для результата, но то, что он действительно получил в результате был мотоцикл; метод обещал нам только транспортное средство, помните?). Не волнуйтесь, если это сложно переварить. Это действительно нетривиальная вещь, чтобы осмыслить. В следующем разделе я приведу более богатый пример контравариантности, и все должно встать на свои места к тому времени, когда вы закончите эту статью.
Вернемся к нашим правилам. Таким образом, вариация на объекте позволяет компилятору проверять соблюдение правил ковариации и контравариантности. Однако, если вы используете вариант использования сайта, ваш компилятор не сможет помочь вам с этими правилами, поскольку он не знает, ковариантен ваш класс или контравариантен по своему типу (или каждому из его типов). Объявление отклонения откладывается до момента использования класса. Это означает, что вы, как разработчик класса, не можете контролировать эти вещи. Вы определите методы своего класса и будете молиться, чтобы будущие пользователи вашего класса вызывали методы, подобные get, только тогда, когда они создают экземпляр вашего класса как ковариантный, и вызывают методы, подобные put, только когда они создают его как контравариантный. Проблема ковариации / контравариантности теперь в их руках. Не знаю, как вы, но я предпочитаю взять эту уродливую работу на себя и облегчить им жизнь.
В следующем (и последнем) разделе я приведу пример дисперсии в Scala путем создания подтипов функции. Функции в Scala предназначены для первоклассных граждан; они не только могут передаваться как параметры, возвращаться из методов, храниться в коллекциях и т. д., но они также могут быть подтипами и надтипами. Таким образом, вместо подклассов и суперклассов вы также можете иметь подфункции и суперфункции!
Разница в подтипах функций
Каждая однопараметрическая функция в Scala на самом деле является реализацией трейта Function1 (на самом деле это немного сложнее, но детали здесь для краткости опущены):
trait Function1[-S, +T] { def apply(x: S): T }
Обратите внимание, что Function1 контравариантна в S и ковариантна в T. Некоторая реализация признака Function1 (назовем его MySubFunction [S1, T1]) является подклассом другого (назовем его MySuperFunction [S2, T2]), только если она подчиняется дисперсии правила, предусмотренные в Function1 (то есть, если S2 ‹: S1 и T1‹: T2).
Это правило ковариантности функции по типу ввода и контравариантности по типу возвращаемого значения исходит из принципа подстановки Лискова (LSP). В нем говорится, что T является подтипом U, если он поддерживает те же операции, что и U, и все его операции требуют меньше (или того же) и предоставляют больше (или то же самое), чем соответствующие операции в U (подтипы рефлексивны, поэтому S ‹: S).
Хорошо, давайте посмотрим, что это значит. Подумайте о функции:
def getCarInfo: Car => AnyRef
Это все допустимые подтипы getCarInfo:
- Автомобиль = ›AnyRef
- Автомобиль = ›AnyRef
- Автомобиль = ›Строка
- Автомобиль = ›Строка
Все они требуют меньше (или столько же) и предоставляют больше (или столько же), что и Car = ›AnyRef. «Меньше» означает «более общий», а «более» - «более конкретный». Это очень логично: Vehicle «меньше» Car, потому что мы меньше знаем об объекте (мы знаем только, что это транспортное средство, но не тип транспортного средства), тогда как оно «больше, чем» AnyRef, потому что дает нам больше чем просто объект AnyRef (у нас есть все поля и методы, определенные в Vehicle).
Давайте подробнее рассмотрим эти четыре подтипа. Первый точно такой же, как getCarInfo, и это нормально, потому что подтипирование рефлексивно (вот почему я добавил «or same» после less / more в определении LSP); второй требует меньше, потому что он не ограничивает нас только автомобилями - он позволяет использовать любое транспортное средство, что является меньшим требованием, чем автомобили; третий предоставляет больше, потому что вместо того, чтобы предоставить нам только AnyRef, он предоставляет нам гораздо более богатый тип String; а четвертый требует меньше и предоставляет больше, чем getCarInfo.
Остановитесь и подумайте об этом на секунду. Это ковариация и контравариантность во всей красе! Если я хочу, чтобы функция B была подтипом функции A, то входной параметр B должен быть суперклассом входного параметра A (контравариантность), а возвращаемое значение B должно быть подкласс возвращаемого значения A (ковариация).
Давайте теперь построим полный пример. Вот код:
1 /**
2 * Remember! In Scala, every function that takes one argument
3 * is an instance of Function1 with signature:
4 *
5 * trait Function1[-T, +S] extends AnyRef
6 */
7
8 class Vehicle(val owner: String)
9 class Car(owner: String) extends Vehicle(owner)
10
11 object Printer {
12
13 val cars = List(new Car("john"), new Car("paul"))
14
15 def printCarInfo(getCarInfo: Car => AnyRef) {
16 for (car <- cars) println(getCarInfo(car))
17 }
18 }
19
20 object Customer extends App {
21
22 val getOwnerInfo: (Vehicle => String) = _.owner
23
24 Printer.printCarInfo(getOwnerInfo)
25 }
Посмотрите на метод printCarInfo (строка 15), который принимает в качестве параметра функцию Car = ›AnyRef:
def printCarInfo(getCarInfo: Car => AnyRef)
Если мы должны вызвать этот метод из нашего пользовательского кода (строка 24), мы должны либо передать функцию с идентичной сигнатурой (Car = ›AnyRef), либо функцию, являющуюся подтипом Car =› AnyRef, то есть, которая требует меньше или то же самое и предоставляет больше или то же самое, что и Car = ›AnyRef. В этом примере мы выбираем второй вариант и передаем функцию (определенную в строке 22), которая требует меньше Car (Автомобиль) и предоставляет больше AnyRef (String):
val getOwnerInfo: (Vehicle => String) = _.owner
Хорошо, LSP удовлетворен, поэтому наша функция Vehicle = ›String является допустимым подтипом Car =› AnyRef.
Метод printCarInfo использует функцию getCarInfo в строке 16, чтобы получить информацию из автомобиля и распечатать ее. Даже если бы код printCarInfo был недоступен для нас и мы не могли бы заглянуть внутрь метода, мы все равно знали бы, что он должен подавать автомобили (не забывайте, что это также включает подтипы, например, AstonMartin) для getCarInfo. Об этом говорит его подпись. Итак, если мы скажем, что наша функция, заменяющая getCarInfo, принимает Vehicle, мы ничего не сломаем, поскольку все автомобили и их подтипы также являются транспортными средствами. Все, что мы сделали, это то, что мы предоставили функцию, которая требует меньше, поэтому printCarInfo не заметит ничего другого. PrintCarInfo говорит: «Круто, как бы то ни было, пока вы предоставляете мне функцию, которая будет принимать мои машины, я в порядке». И функция, которая принимает автомобили, безусловно, принимает автомобили.
С другой стороны, наша Vehicle = ›String возвращает String, что означает, что предоставляет больше. PrintCarInfo сказал, что ему нужна функция, возвращающая AnyRef. Строка - это AnyRef? Да, это так. Опять же, мы ничего не сломали.
Большой! Мы показали, что Vehicle = ›String является вполне допустимой заменой Car =› AnyRef. PrintCarInfo продолжит свой рабочий процесс в обычном режиме, даже не заметив, что вместо Car = ›AnyRef вместо Car =› String. И мы видели контравариантность, этот странный маленький член семейства дисперсий, на реальном примере вместо некоторого эзотерического теоретического объяснения.
Вот и все. На этом моя попытка объяснить дисперсию заканчивается. Если вы хотите оставить отзыв, свяжитесь со мной по адресу [email protected].
использованная литература
[1]: Мартин Одерски, Лекс Спун, Билл Веннерс, Программирование на Scala, 2-е издание »
Дальнейшее обучение
5 книг для изучения Scala и функционального программирования
10 причин для изучения Scala на Hackernoon
10-минутное введение в Scala от Тейвы Харсани
5 бесплатных курсов по изучению Scala
30 лучших вопросов на собеседовании по программированию на Scala
Пример внедрения Scala в мир Java от Стефана Дерозио