TODO: отвалидировать – текст собран автоматически из материалов курса на Google Drive, проверьте формулировки и код.
Введение
Инкапсуляция отвечала на вопрос «что спрятать», наследование – «как переиспользовать». Полиморфизм отвечает на третий: как написать код, который работает с объектами, ничего не зная об их конкретном типе.
Из трёх базовых концепций эта самая непростая. Инкапсуляцию видно в коде сразу – вот закрытое поле, вот метод. Наследование тоже: вот двоеточие после имени класса. А полиморфизм в исходнике не виден вовсе – он проявляется в момент выполнения, когда за одним и тем же вызовом оказывается разный код. Отсюда и репутация: механизм простой, а поймать его на глаз тяжело.
Проще всего увидеть его на бытовых примерах:
- Кнопка «Пуск». У стиральной машины, микроволновки и кофеварки она называется одинаково и нажимается одинаково, а происходит после нажатия каждый раз своё. Вы не задумываетесь, что там внутри, – вы знаете, что кнопка работает.
- Розетка. В неё включают чайник, ноутбук и пылесос. Розетка не знает, что именно в неё воткнули, и знать не должна: договор описан вилкой, а не устройством.
- Педаль тормоза. В легковушке она давит на диски, в электричке замыкает контакт, в грузовике управляет пневматикой. Водитель во всех трёх случаях делает одно движение.
Общее у примеров одно: есть договор – кнопка, вилка, педаль, – и есть разные исполнители этого договора. Тот, кто пользуется договором, не выбирает исполнителя и не знает о нём. В коде так же: вызывающий код опирается на договор, а какой объект придёт, решается уже во время работы программы.
Определение: полиморфизм – способность одного и того же обращения приводить к разному поведению в зависимости от того, с каким объектом или типом мы работаем.
- Пульт от телевизора включает телевизор.
- Пульт от кондиционера включает кондиционер.
Как именно язык связывает обращение с реализацией – вопрос отдельный, и способов несколько: интерфейсы, наследование с виртуальными методами, перегрузка, обобщения, утиная типизация. Каждому из них посвящён отдельный урок, а здесь разберём сквозной пример на интерфейсах.
Сквозной пример: одно обращение – разное поведение
using System;
using System.Collections.Generic;
public interface IAnimal
{
void Say();
}
public class Dog : IAnimal
{
public string Name { get; set; } = "Bob";
public void Say()
{
Console.WriteLine($"Woof im {Name}");
}
}
public class Cat : IAnimal
{
public string Name { get; set; } = "Murzik";
// Реализация интерфейса
public void Say()
{
Console.WriteLine($"Meow im {Name}");
}
}
class Program
{
static void Main()
{
List<IAnimal> animals = new List<IAnimal> { new Cat(), new Dog() };
foreach (var animal in animals)
{
animal.Say(); // Работает для всех животных
}
}
}
#include <iostream>
#include <memory>
#include <vector>
// Абстрактный класс с чисто виртуальным методом – аналог интерфейса
class Animal {
public:
virtual void Say() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void Say() const override { std::cout << "Woof im " << name_ << std::endl; }
private:
std::string name_ = "Bob";
};
class Cat : public Animal {
public:
// Реализация интерфейса
void Say() const override { std::cout << "Meow im " << name_ << std::endl; }
private:
std::string name_ = "Murzik";
};
int main() {
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Cat>());
animals.push_back(std::make_unique<Dog>());
for (const auto& animal : animals) {
animal->Say(); // Работает для всех животных
}
return 0;
}
package main
import "fmt"
// Интерфейс в Go реализуется неявно: Dog и Cat нигде не пишут,
// что они Animal – достаточно совпадения метода.
type Animal interface {
Say()
}
type Dog struct {
Name string
}
func (d Dog) Say() { fmt.Printf("Woof im %s\n", d.Name) }
type Cat struct {
Name string
}
func (c Cat) Say() { fmt.Printf("Meow im %s\n", c.Name) }
func main() {
animals := []Animal{Cat{Name: "Murzik"}, Dog{Name: "Bob"}}
for _, animal := range animals {
animal.Say() // Работает для всех животных
}
}
import random
class Dog:
def __init__(self):
self.name = "Bob"
def say(self):
print("Woof im %s" % self.name)
class Cat:
def __init__(self):
self.name = "Murzik"
def say(self):
print("Meow im %s" % self.name)
if __name__ == "__main__":
animals = [Cat(), Dog()]
for i in range(0, 5):
random.choice(animals).say()
interface Animal {
say(): void;
}
class Dog implements Animal {
name = "Bob";
say(): void {
console.log(`Woof im ${this.name}`);
}
}
class Cat implements Animal {
name = "Murzik";
// Реализация интерфейса
say(): void {
console.log(`Meow im ${this.name}`);
}
}
const animals: Animal[] = [new Cat(), new Dog()];
for (const animal of animals) {
animal.say(); // Работает для всех животных
}
import java.util.List;
public interface Animal {
void say();
}
public class Dog implements Animal {
private String name = "Bob";
public void say() {
System.out.println("Woof im " + name);
}
}
public class Cat implements Animal {
private String name = "Murzik";
// Реализация интерфейса
public void say() {
System.out.println("Meow im " + name);
}
}
public class Program {
public static void main(String[] args) {
List<Animal> animals = List.of(new Cat(), new Dog());
for (Animal animal : animals) {
animal.say(); // Работает для всех животных
}
}
}
interface Animal {
fun say()
}
class Dog : Animal {
val name = "Bob"
override fun say() = println("Woof im $name")
}
class Cat : Animal {
val name = "Murzik"
// Реализация интерфейса
override fun say() = println("Meow im $name")
}
fun main() {
val animals: List<Animal> = listOf(Cat(), Dog())
animals.forEach { it.say() } // Работает для всех животных
}