Наследование в языке Dart имеет важное ограничение: мы не можем наследовать класс сразу от нескольких классов, например, следующим образом:
class Worker{ String company = "Necrosoft"; Worker(this.company); void work()=>print("works in $company"); } class Student{ String university = "Stanford"; Student(this.university); void study() =>print("studies in $university"); } class WorkingStudent extends Worker, Student{ // ! Ошибка - множественное наследование не допускается }
Здесь у нас есть два класса - Worker (класс работающего) и класс Student (класс студента). Но что, если мы хотим определить класс работающего студента, например, WorkingStudent, который бы использовал функционал обоих классов. Просто унаследовать класс WorkingStudent от классов Worker и Student мы не можем, так как Dart не поддерживает множественное наследования.
Для решения этой проблемы в Dart применяется реализация интерфейсов. При этом важно понимать, что интерфейс - это не отдельная сущность, как в некоторых языках программирования (например, тип interface в C# или Java), а тот же класс. То есть класс в Dart одновременно выступает в роли интерфейса, и другой класс может реализовать данный интерфейс.
Интерфейс представляет синтаксический контракт, которому должны следовать реализующие этот интерфейс классы. То есть, если класс-интерфейс определяет какие-нибудь поля и методы, то класс, реализующий данный интерфейс, должен также определить эти поля и методы.
Для реализации интерфейсов применяется оператор implements, после которого указывает класс реализуемого интерфейса:
class Worker{ String company = "Microsoft"; Worker(this.company); void work()=>print("works in $company"); } class Student{ String university = "Stanford"; Student(this.university); void study() =>print("studies in $university"); } class WorkingStudent implements Worker, Student{ String name; @override String company; @override String university; WorkingStudent(this.name, this.university, this.company); @override void study() =>print("$name studies in $university"); @override void work()=>print("$name works in $company"); } void main (){ WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google"); tom.study(); // Tom studies in MIT tom.work(); // Tom works in Google }
В данном случае класс WorkingStudent применяет интерфейс классов Worker и Student:
class WorkingStudent implements Worker, Student
Применение интерфейса означает, что класс WorkingStudent должен реализовать все поля и методы, которые определены в классах Worker и Student. Поэтому класс WorkingStudent должен определить поле company и метод work класса Worker и поле university и метод study класса Student. По сути интерфейс - это контракт, что класс должен содержать определенный функционал (в данном случае поля и методы). Если класс WorkingStudent не определил бы все поля и методы, которые есть в классах Worker и Student, то мы столкнулись бы с ошибкой.
В то же время из приведенного выше примеры видно, что класс WorkingStudent не обязан реализовать конструкторы реализуемых интерфейсов и может вообще не определять никакого конструктора.
Реализация интерфейса также означает, что в любом месте программы, где требуется объект Worker или объект Student, мы можем использовать объект WorkerStudent, то есть работающий студент одновременно это и работающий и студент. Например:
void makeWork(Worker worker){ worker.work(); } void makeStudy(Student student){ student.study(); } void main (){ WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google"); // вместо Worker можно передать WorkingStudent makeWork(tom); // Tom works in Google // вместо Student можно передать WorkingStudent makeStudy(tom); // Tom studies in MIT }
Причем мы можем комбинировать наследование и реализацию интерфейсов. Например, мы решили, что класс WorkingStudent все таки будет наследовать класс Student, а не реализовать его интерфейс:
class Worker{ String company = "Microsoft"; Worker(this.company); void work()=>print("works in $company"); } class Student{ String university = "Stanford"; Student(this.university); void study() =>print("studies in $university"); } class WorkingStudent extends Student implements Worker{ String name; @override String company; WorkingStudent(this.name, super.university, this.company); @override void study() =>print("$name studies in $university"); @override void work()=>print("$name works in $company"); } void main (){ WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google"); tom.study(); // Tom studies in MIT tom.work(); // Tom works in Google }
Теперь класс WorkingStudent наследуется от Student и реализует интерфейс класса Worker:
class WorkingStudent extends Student implements Worker{
В этом случае нам не требуется определять в классе WorkingStudent поле university, так как оно унаследовано от базового класса. Также можно было бы не определять метод study, но в данном случае оно переопределено, чтобы учитывать имя работающего студента.
Но поскольку в Student определен конструктор, то при определении конструктора WorkingStudent нам надо вызвать конструктор базового класса:
WorkingStudent(this.name, super.university, this.company);
Выражение super.university
позволяет передать соответствующее значение для параметра university конструктора Student.
Подобным образом через слово super мы можем обращаться к функционалу базового класса. А к фукционалу класса-интерфейса через super
мы обращаться
не можем.
Стоит отметить, что начиная с версии Dart 3.0 в язык было добавлено ключевое слово interface. Класс, определенный со словом interface, может быть унаследован только в рамках своей библиотеки. Вне своей библиотеки он может быть только реализован как интерфейс.
interface class Worker{ String company; Worker(this.company); void work()=>print("works in $company"); } class Manager implements Worker{ String name; @override String company; Manager(this.name, this.company); @override void work()=>print("$name works in $company"); } void main (){ var tom = Manager("Tom", "Google"); tom.work(); // Tom works in Google }
В примерах выше классы-интерфейсы определяют некоторую реализацию по умолчанию - определяют переменные, в методах определены некоторые действия. Однако это не всегда нужно. Иногда интерфейс нужен просто, чтобы определить некоторый функционал без реализации и обозначить, что данный тип имеет определенный набор методов. В этом случае мы можем определить интерфейсы как абстрактные:
abstract interface class Worker{ void work(); } abstract interface class Student{ void study(); } class WorkingStudent implements Worker, Student{ String name; String university; String company; WorkingStudent(this.name, this.university, this.company); @override void study() =>print("$name studies in $university"); @override void work()=>print("$name works in $company"); } void main (){ WorkingStudent tom = WorkingStudent("Tom", "MIT", "Google"); studentStudy(tom); workerWork(tom); } void studentStudy(Student st){ st.study(); } void workerWork(Worker w){ w.work(); }
В данном случае Worker не определяет реализации для метода work. Аналогично интерфейс Student не определяет реализацию метода study. Данные методы абстрактные и реализуются классом WorkingStudent.
При наследовании производный класс не обязан определять те же поля и методы, которые есть в базовом классе (за исключением абстрактных методов). Если в базовом классе определяется конструктор, то производный класс обязан определить свой конструктор, при котором вызывается конструктор базового класса. В производном классе мы можем обращаться к реализации базового класса с помощью ключевого слова super. Не поддерживается множественное наследование.
При реализации интерфейса производный класс должен определить все поля и методы, которые определены в классе интерфейса. Если в базовом классе есть конструктор, то производный класс НЕ обязан определять свой конструктор. В производном классе мы НЕ можем обращаться к методам реализованного интерфейса с помощью ключевого слова super. Поддерживается множественная реализация интерфейсов.