О чем статья

Ключевое слово covariant было внедрено в Dart для борьбы с важной проблемой переопределения методов.

В статье содержатся разбор проблемы, описание работы covariant и пример его использования для решения проблемы.

Проблема

Если в дочернем классе переопределяется метод родительского класса, то параметр метода дочернего класса может быть экземпляром только того класса, что указан в данного параметра в методе родительского класса.

Т.о., если в переопределяемом методе использовать параметр класса, который является дочерним для класса, который указан для параметра в методе наследуемого класса, то возникнет ошибка. Пример представлен на Рис. 1.

Рис.1 Пример проблемы
Рис.1 Пример проблемы
  • Cat - это класс-наследник Animal, который переопределяет метод eat. Animal в методе eat требует параметр типа данных Food. 

  • Fish - это подкласс Food - т.е. неявно экземпляры класса Fish являются экземплярами класса Food. Однако, если в Cat.eat указать параметр с типом данных Fish, то возникнет ошибка.

Аналогичная ситуация с реализацией интерфейса (см. Рис.2).

Рис.2 - Пример с интерфейсом
Рис.2 - Пример с интерфейсом

Чтобы решить эту проблему, в Dart 2.12 было введено ключевое слово covariant.

Что такое covariant

covariant - это ключевое слово, которое используется вместе с параметрами методов при переопределении в дочерних классах методов родительских классов для того, чтобы решить проблему нераспознавания дочерних классов при переопределении методов.

Параметр дочернего метода вместо типа данных, указанного в переопределяемом методе родительского класса, может использовать тип данных, который является дочерним для указанного в методе родительского класса. Пример представлен на Рис.3.

Рис.3 - Пример использования covariant в дочернем классе
Рис.3 - Пример использования covariant в дочернем классе

Ключевое слово covariant сообщает анализатору то, что не нужно осуществлять строгую проверку типа данных. Однако, если использовать для параметра тип данных, который не является дочерним классом класса, указанного для параметра в методе родительского класса, то возникнет ошибка (см. Рис. 4).

Рис.4 - Пример ошибки
Рис.4 - Пример ошибки

Ключевое слово covariant может быть использовано в методе суперкласса или в методе производного класса. Однако, хорошей практикой считается использование covariant в методах родительского класса, чтобы в  интерфейсе родительского класса явно указать то, что дочерние классы могут изменять этот интерфейс. Также такой подход позволяет всем дочерним классам в переопределении метода изменять тип данных для параметра, определенного с covariant. Пример представлен на Рис. 5.

Рис.5 - Пример использования covariant в родительском классе
Рис.5 - Пример использования covariant в родительском классе

Заключение

Ключевое слово covariant знать полезно, ведь что-то аналогичное примеру, представленному в статье, очень даже может встретиться в реальной жизни. Также covariant часто используется в исходниках Flutter и его не редко спрашивают на собеседованиях.

Благодарю за внимание!

Код из примеров
class Food {}

class Animal {
  void eat(covariant Food food) {
    print('Animal eats $food');
  }
}

class Fish extends Food {}

class Cat extends Animal {
  @override
  void eat(Fish food) {
    print('Cat eats $food');
  }
}

class Meat extends Food {}

class Dog extends Animal {
  @override
  void eat(Meat food) {
    print('Dog eats $food');
  }
}

void main() {
  Food food = Food();
  Animal animal = Animal();
  animal.eat(food);

  Fish fish = Fish();
  Cat cat = Cat();
  cat.eat(fish);

  Meat meat = Meat();
  Dog dog = Dog();
  dog.eat(meat);
}

Комментарии (10)


  1. HemulGM
    07.10.2024 04:42

    Таким образом получается, что класс Cat не может принимать параметром, например, `Meat food`?

    Но таким образом получается, что если Cat как Animal в общем списке Animals, то мы можем передать в него любой Food, и объект Cat примет Meat и будет обращаться к Meat как к Fish. Это как-то регулируется вообще?


    1. maratxat Автор
      07.10.2024 04:42

      Таким образом получается, что класс Cat не может принимать параметром, например, `Meat food`?

      да, верно - так происходит из-за того, в Cat.eat мы указали параметр food с типом данным Fish, а Meat - это не Fish и даже не подкласс Fish. статический анализатор не пропустить аргумент типа Meat в Cat.eat

      Но таким образом получается, что если Cat как Animal в общем списке Animals, то мы можем передать в него любой Food, и объект Cat примет Meat и будет обращаться к Meat как к Fish.

      нет, не верно. кастинг инстанса дочернего класса Cat к родительскому Animal не меняет поведения дочернего класса - метод Cat.eat по прежнему будет требовать от инжектора инстанс класса Fish и т.о. Meat не подойдет


      1. HemulGM
        07.10.2024 04:42

        Создайте список из объектов класса Animal, добавьте туда Cat, Dog и вызывайте Eat для каждого, передавая туда Meat


        1. maratxat Автор
          07.10.2024 04:42
          +1

          Создал, вызвал, получил ошибку, о которой писал в ответе на ваш комментарий


          1. HemulGM
            07.10.2024 04:42

            Так об этом я и говорил. Ошибка проявляется в рантайме, а не при написании кода.


            1. maratxat Автор
              07.10.2024 04:42
              +1

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

              ошибка в листе - это проблема тайп кастинга элементов листа а не наследования

              если в Cat.eat передать meat, то анализатор задетектит эту ошибку до run-time


  1. ParaMara
    07.10.2024 04:42

    Приятно видеть, что авторы Хабра могут получить статью просто ткнув пальцем в любую букву любой документации. Осталось научиться получать минимально содержательную статью для чего в данном случае следовало написать

    • почему нельзя просто считать что всё covariant

    • как с этим в других языках

    • и для фанатов - когда в Dart было введено covariant


    1. maratxat Автор
      07.10.2024 04:42

      приятно видеть, комментарий от внимательного и справедливого читателя

      • и для фанатов - когда в Dart было введено covariant

      в статье это указано

      авторы Хабра могут получить статью просто ткнув пальцем в любую букву любой документации.

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

      • как с этим в других языках

      я до этого имел опыт работы только с динамически типизированными япами - там таких проблем априори не может быть. поэтому, как там у других это реализовано вам поможет гугл, а эта статья про дарт

      • почему нельзя просто считать что всё covariant

      не понял вопроса, сформулируйте свои мысли получше.


  1. Dragon274
    07.10.2024 04:42
    +2

    Почему используя для параметра тип данных, который не дочерний классом класса, указанного для параметра в методе род. класса возникает ошибка??


    1. maratxat Автор
      07.10.2024 04:42

      так устроен статический анализатор дарта. а почему он так устроен - можно только догадываться.

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