Для начала, чтобы не тратить ваше время, эта статья посвящена ковариации в объектно-ориентированном программировании, а не в математическом (как в статистике или обработке сигналов).

Ковариантность и контравариантность — это функции, поддерживаемые современными языками программирования, которые позволяют входным или выходным значениям изменять тип, когда мы специализируем метод в дочернем классе.

В этот момент вы можете спросить себя, как они могут быть связаны; Пожалуйста, оставайтесь со мной. Путь короткий: всего три шага. И я думаю, что это самый простой способ понять эти понятия.

Шаг первый — принцип подстановки Лискова

Законный дочерний класс не изменит поведение кода, использующего родительский класс.

OR

Когда мы заменяем экземпляр дочернего класса экземпляром базового класса, это не нарушит существующий код.

Когда мы пытаемся понять принцип замещения, первая реакция: «И все? Это очевидно. Зачем столько суеты?».

Давайте посмотрим на известный пример. Я не буду придумывать еще один, так как этот идеален. Все кредиты принадлежат Роберту С. Мартину (дядя Боб для близких).

Мы пытаемся разработать иерархию геометрических форм. Допустим, он содержит Прямоугольник и Квадрат. Мы сделали домашнее задание и знаем, что квадрат — это прямоугольник. С математической точки зрения множество квадратов является подмножеством прямоугольников.

Поэтому у нас может возникнуть соблазн сделать класс Square дочерним по отношению к классу Rectangle, потому что квадрат — это своего рода прямоугольник. Ширина и высота одинаковые, вот и все.

Некоторый код использует прямоугольник таким образом: установите ширину на 10, высоту на 20 и получите площадь. Мы заменим прямоугольник квадратом, и теперь он получит площадь 400 вместо 200. Что случилось? Квадрат - это прямоугольник, не так ли? Так говорит математика.

Барбара Лисков говорит, что ваша интуиция о наследовании с точки зрения подмножеств неверна. Что имеет значение, так это поведение, а не статическое определение.

Но как мы можем формализовать поведение?

Шаг 2 — смысловой контракт

Первоначально введенный Бертраном Мейером, семантический контракт представляет собой простой формализм для описания поведения метода. Он состоит из двух частей:

Предложение require: что мы ожидаем от клиента, вызывающего метод

  • например, всегда указывайте положительное значение для свойств ширины и высоты прямоугольника.
  • вызывайте метод off() коммутатора только в том случае, если статус «включен».

Предложение обеспечить: что клиент может ожидать после выполнения

  • высота не меняется, когда мы устанавливаем ширину
  • площадь всегда равна ширине х высоте
  • состояние переключателя «выключено» после выполнения метода off()

Теперь мы можем естественным образом выразить принцип замещения в терминах семантического контракта. Для законной специализации метода должны быть выполнены два условия:

  1. у нас может быть такой же или более сложный пункт «гарантировать»
  2. у нас может быть такой же или более мягкий пункт «require»

Проще говоря:

При переопределении метода мы можем требовать меньше и производить (обеспечивать) больше, но не наоборот.

Шаг 3 — ковариация/контравариантность

Ковариация возникает из-за принципа подстановки, когда метод создает сложный объект (экземпляр класса).

Он может создать объект того же типа или более специализированный, но не более общий = мы можем сделать больше.

Метод

Animal PetShop.MakePet()

может быть специализированным как

Кот CatShop.MakePet()

Клиент ожидает обычных животных, так что кошка в порядке.

С другой стороны, контравариантность является логическим следствием другой части контракта. Когда мы переопределяем метод, использующий экземпляр класса, мы можем принять более общий тип. Клиент, манипулирующий специализированными объектами, не будет жаловаться = мы можем потребовать меньше.

Допустим, владелец улитки использует услугу, чтобы кормить своих улиток. Что-то вроде

ISnailFeeder.Feed(Snail)

Кормушка для животных может реализовать эту услугу

AnimalFeeder.Feed(Животное)

Владелец улитки не будет жаловаться, так как улитки животные.

Вот и все. Мы намеренно избегали подробностей, связанных с языком, и сосредоточились на основной концепции.