Наследование является одним из ключевых моментов объектно-ориентированного программирования, позволяя передавать одним классам функционал других. В языке Dart наследование реализуется с помощью ключевого слова extends. Но сначала рассмотрим, для чего нам может понадобится наследования. Возьмем следующую программу:
void main (){ Person tom = Person(); tom.name = "Tom"; tom.age = 38; tom.display(); // Name: Tom Age: 38 Employee bob = Employee(); bob.name = "Bob"; bob.age = 42; bob.display(); // Name: Bob Age: 42 } class Person{ String name = ""; int age = 0; void display() => print("Name: $name Age: $age"); } class Employee{ String name = ""; int age = 0; String company = ""; void display() => print("Name: $name Age: $age"); }
Здесь у нас есть класс Person, который представляет человека, и есть класс Employee, который представляет работника некоторой компании. И хотя классы замечательно работают, но мы видим, что функционал класса Person дублируется в классе Employee. И наследование позволяет избежать подобного дублирования кода.
Применим наследование:
void main (){ Employee bob = Employee(); bob.name = "Bob"; bob.age = 42; bob.display(); // Name: Bob Age: 42 } class Person{ String name = ""; int age = 0; void display() => print("Name: $name Age: $age"); } class Employee extends Person{ String company = ""; }
Теперь класс Employee наследуется от класса Person. Для установки наследования после класса указывается ключевое слово extends и далее указывается класс, от которого идет наследование.
class Employee extends Person{
При наследовании класс Employee перенимает весь функционал класса Person - все его поля и методы и может их использовать. И также можно определить в подклассе новые поля и методы, которых нет в классе Person. В итоге код стал значительно проще, а мы можем также создать объект Employee и обращаться к его полям name и age и методу display, хотя они определены в родительском классе Person.
В этом случае класс Person еще называют родительским классом, базовым классом или суперклассом, а класс Employee - классом-наследником, подклассом или производным классом.
В отличие от полей и методов конструкторы базового класса не наследуются. Если базовый класс явным образом определяет конструктор (конструктор по умолчанию не учитывается), то его необходимо вызвать в классе-наследнике при определении конструктора:
void main (){ Employee bob = Employee("Bob", 42, "Google"); bob.display(); // Name: Bob Age: 42 } class Person{ String name; int age; Person(this.name, this.age); void display() => print("Name: $name Age: $age"); } class Employee extends Person{ String company = ""; Employee(String name, int age, this.company): super(name, age); }
Здесь базовый класс Person определяет конструктор, который принимает два параметра. В производном классе Employee также определяется конструктор, который вызывает
конструктор базового класса, передавая ему значение параметров name и age. Для обращения к функциональности базового класса из производного применяется ключевое слово
super. В частности, вызов super(name, age)
фактически будет представлять обращение к конструктору базового класса
Person(name, age)
.
Если мы не вызовем конструктор базового класса при определении конструктора в производном классе, то мы столкнемся с ошибкой.
Стоит ометить, что в последних версиях Dart мы можем сократить передачу значений базовому классу следующим образом:
void main (){ Employee bob = Employee("Bob", 42, "Google"); bob.display(); // Name: Bob Age: 42 } class Person{ String name; int age = 18; Person(this.name, this.age); void display() => print("Name: $name Age: $age"); } class Employee extends Person{ String company = ""; Employee(super.name, super.age, this.company); }
В данном случае в классе Employee строка
Employee(super.name, super.age, this.company);
будет эквивалентна
Employee(String name, int age, this.company): super(name, age);
А выражение super.name
фактически будет представлять значение для параметра name в конструктор базового класса Person.
Подобным образом можно переопределять и обращаться к именованным конструкторам базового класса:
void main (){ Employee tim = Employee.withName("Tim"); tim.display(); // Name: Tim Age: 18 } class Person{ String name; int age = 18; Person(this.name, this.age); // именнованный конструктор вызывает основной конструктор Person.withName(this.name); void display() =≫ print("Name: $name Age: $age"); } class Employee extends Person{ String company = ""; Employee(super.name, super.age, this.company); // переопределяем именнованный конструктор Employee.withName(String name) : super.withName(name); }
Производные классы могут определять свои поля и методы, но также могут переопределять, изменять поведение методов базового класса. Для этого применяется аннотация @override. Так, в примерах выше класс Employee также определяет поле company для хранения компании, в которой работает работник. И было бы неплохо, если бы метод display для объекта Employee также бы выводил его компанию:
void main (){ Employee bob = Employee("Bob", 42, "Google"); bob.display(); // Name: Bob Age: 42 Company: Google } class Person{ String name; int age = 18; Person(this.name, this.age); void display() => print("Name: $name Age: $age"); } class Employee extends Person{ String company; Employee(super.name, super.age, this.company); // переопределяем метод display @override void display() { print("Name: $name Age: $age"); print("Company: $company"); } }
Однако в примере выше мы видим, что часть кода метода display в Employee повторяет код метода display из Person. Чтобы не повторяться, с помощью ключевого слова super мы можем просто вызвать реализацию метода display из базового класса:
void main (){ Employee bob = Employee("Bob", 42, "Google"); bob.display(); } class Person{ String name; int age = 18; Person(this.name, this.age); void display() => print("Name: $name Age: $age"); } class Employee extends Person{ String company; Employee(super.name, super.age, this.company); @override void display(){ super.display(); // Вызов реализации из класса Person print("Company: $company"); } }
Консольный вывод:
Name: Bob Age: 42 Company: Google