Рубріки: Теория

Полиморфизм в языках программирования: виды и примеры как использовать в коде

Ольга Змерзла

Что такое полиморфизм в программировании

Вы знаете такое устройство как пульт дистанционного управления? Его используют для управления домашней техникой и одна и та же кнопка может выполнять несколько разных функций. Например, кнопка «питание» включает или выключает телевизор.

В программировании полиморфизм работает так же. Объекты могут принимать разные формы в зависимости от контекста, в котором они используются.

Это делает полиморфизм мощным инструментом для создания гибкого кода. Он широко используется во многих языках программирования и средах, упрощает обслуживание и обновление кода, а также делает программы более надежными и менее подверженными ошибкам.

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

Другими словами, полиморфизм позволяет разным объектам совместно использовать один и тот же интерфейс, но вести себя по-разному в зависимости от их целей исполнения.

Например, у вас есть функция, принимающая на вход объект Animal. Она может работать с любым типом животных: кошкой, собакой, хомячком. У каждого типа животных будет своя собственная реализация функции, но самой функции не нужно знать, с каким конкретным типом животных она работает. Именно эта особенность полиморфизма позволяет с его помощью создавать гибкий и повторно используемый код.

История возникновения полиморфизма в языках программирования

Полиморфизм появился, когда сформировалась концепция объектно-ориентированного программирования (ООП), что сделало его неотъемлемой частью ООП, а также одной из ключевых составляющих языков программирования в целом.

Идею полиморфизма впервые представил в середине 1960-х Кристофер Стрейчи в своей статье «Фундаментальные концепции языков программирования». Но только в конце 1970-х — начале 1980-х годов эта концепция начала внедряться в языки программирования.

Одним из первых языков программирования, использующих полиморфизм, стал Simula (разработан в конце 1960-х — начале 1970-х годов).

Simula разрабатывали для симуляции и моделирования. Он представил концепцию объектов, классов и наследования, благодаря чему стал первым языком, использующим термин «виртуальный» для описания полиморфизма.

Smalltalk, который создали в 1970-е годы, тоже один из ранних примеров языка программирования, где была введена концепция полиморфизма.

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

Важную роль в развитии полиморфизма сыграл C++. Его разработали в 1980-х годах как расширение языка программирования C и включили в него концепцию перегрузки функций, которая позволяла нескольким функциям иметь одно и то же имя, но разные параметры.

C++ также представил концепцию шаблонов, которая позволила использовать универсальное программирование и еще больше расширила возможности полиморфизма.

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

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

Сейчас полиморфизм — одна из ключевых концепций ОО-языков программирования и широко применяется при разработке сложных программных систем.

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

Преимущества и недостатки полиморфизма

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

Начнем с преимуществ:

  1. Возможность повторного использования кода. Полиморфизм упрощает повторное использование и переназначение кода. Это может экономить время и усилия во время разработки ПО, потому что программисты могут использовать уже существующий код для создания новых функций.
  2. Гибкость. Полиморфизм позволяет сделать код более гибким и адаптируемым, потому что разные объекты можно рассматривать так, как если бы они принадлежали одному типу. Также, так как изменения можно вносить, не влияя на поведение других частей программы, это делает код более надежным и менее подверженным ошибкам.
  3. Удобство обслуживания. Полиморфизм упрощает обслуживание и обновление кода, потому что изменения можно внести только в одной части программы, а применить ко всей. Это также экономит время при исправлении ошибок или добавлении новых функций.
  4. Удобочитаемость кода. Концепция полиморфизма повышает читаемость и понятность кода, потому что может использовать общие интерфейсы и методы для разных объектов. Такой подход делает код более интуитивным, и разработчики могут быстрее определять функциональность чужой программы. Кроме того, это упрощает процесс отладки и тестирования программного обеспечения.

Недостатки полиморфизма:

  1. Производительность. Иногда полиморфизм может отрицательно повлиять на производительность. Это происходит, потому что устройству нужно больше вычислительной мощности, чтобы определить, какой метод или функцию вызывать в этом конкретном случае. Если вариантов использования слишком много, а устройство слишком слабое, замедление времени выполнения может быть существенным.
  2. Сложность. Хотя обычно полиморфизм упрощает код, если вы работаете с с большими и сложными иерархиями объектов, концепция, наоборот, может привести к усложнению кода и затруднить его понимание. Такая ситуация также замедлит процесс отладки и тестирования программного обеспечения.
  3. Зависимости. Чем больше вариантов использования есть у одной функции, тем больше зависимостей появляется между частями программы. Это затрудняет изменение или обновление затронутых участков кода.
  4. Ошибки во время исполнения. Иногда полиморфизм может стать катализатором возникновения ошибок во время выполнения. Часто это происходит при работе с большим количеством интерфейсов. Поэтому при проектировании программных систем необходимо учитывать этот фактор.

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

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

Классификация полиморфизма

Классифицировать полиморфизм стали относительно недавно. Если сама концепция появились еще в 60-х, то классификацию предложили только в середине 1990-х. До этого полиморфизм описывался обобщенно.

Лука Карделли, итальянский ученый в области компьютерных наук и исследователь языков программирования, выделил такие три вида полиморфизма:

  1. Универсальный полиморфизм. (Также используются термины «полиморфизм подтипа» или «полиморфизм включения»). Этот вид полиморфизма позволяет обрабатывать объекты разных классов так, как если бы они принадлежали к общему суперклассу, обеспечивает возможность универсального программирования и использования кода повторно.
  2. Ad-hoc полиморфизм. (Также используется термин «перегрузка»). Этот вид включает использование одного имени или оператора для обозначения нескольких разных функций или методов, принимающих разные типы аргументов.
  3. Параметрический полиморфизм. (Также используется термин «универсальное программирование»). Этот вид заключается в определении функции или метода, обрабатывающего значения разных типов без явного указания типов данных заранее. Он обычно используется для создания повторно используемого кода, который можно применять к множеству различных типов.

Классификация Карделли одна из самых популярных и повлияла на развитие многих языков программирования, включая Java и C++. Но она не единственная: есть и альтернативные подходы к классификации полиморфизма. Например, есть еще классификация по Бертрану Мейеру. Она основана на концепции полиморфизма подтипов, которую он разработал как часть своей работы над языком программирования Eiffel. Дискуссии о лучшем варианте в компьютерных науках продолжаются.

Виды полиморфизма

Другой подход к классификации делит полиморфизм на два основных типа:

  1. полиморфизм времени компиляции;
  2. полиморфизм времени выполнения.

1. Полиморфизм времени компиляции

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

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

Пример:

Допустим, у нас есть такой код на Java:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
        return a + b;
    }
    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

Класс Calculator определяет три метода add. Они имеют разные параметры и возвращаемые значения:

  1. первый принимает два целочисленных аргумента и возвращает целое число;
  2. второй принимает два аргумента двойной точности и возвращает число двойной точности;
  3. третий принимает три целочисленных аргумента и возвращает целое число.

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

Другими словами, если сложить нужно будет два целых числа, метод вернет сумму целым числом, а если два числа двойной точности — сумма вернется тоже числом с двойной точностью.

2. Полиморфизм времени выполнения

Другое название полиморфизма времени выполнения — динамический полиморфизм. Он возникает, когда вызываемый метод или функция определяется во время выполнения на основе конкретного объекта.

Из-за этого полиморфизм времени компиляции эффективнее полиморфизма времени выполнения — ведь разрешение метода происходит на этапе компиляции. Но в то же время динамический полиморфизм гибче, потому что позволяет объектам проявлять разное поведение в зависимости от их конкретных типов.

Пример:

В Java пример полиморфизма времени выполнения — возможность переопределения методов. Подкласс предоставляет свою реализацию метода, который уже определен в его суперклассе:

class Animal {
   public void move() {
      System.out.println("Animals can move");
   }
}
class Dog extends Animal {
   public void move() {
      System.out.println("Dogs can walk and run");
   }
}
public class TestDog {
   public static void main(String args[]) {
      Animal animal = new Animal();   // Animal reference and object
      Animal dog = new Dog();   // Animal reference but Dog object
      animal.move();   // runs the method in Animal class
      dog.move();   // runs the method in Dog class
   }
}

В этом примере у класса Animal есть move метод, который выводит сообщение на консоль. Класс Dog расширяет класс Animal и переопределяет move метод.

  • Когда мы создаем объект Animal и вызываем его move метод, вызывается версия, определенная в классе Animal, а на консоль выводится сообщение «Animals can move».
  • Когда мы создаем объект Dog и вызываем его move метод, вызывается версия метода, определенная в классе Dog, а на консоль выводится сообщение «Dogs can walk and run».

Примеры реализации других типов полиморфизма

Ad-hoc полиморфизм

Возникает, когда функция или метод определены для конкретного набора типов.

Пример:

Возьмем код на С++:

#include <iostream>
int add(int a, int b) {
    std::cout << "Called the int version" << std::endl;
    return a + b;
}
double add(double a, double b) {
    std::cout << "Called the double version" << std::endl;
    return a + b;
}
int main() {
    std::cout << add(2, 3) << std::endl;     // calls the int version
    std::cout << add(2.0, 3.0) << std::endl; // calls the double version
    return 0;
}

У нас есть две функции add с разными типами параметров: одна принимает два целых числа, а другая — два числа двойной точности. Реализация вызываемой функции определяется во время компиляции на основе типов аргументов, переданных в функцию.

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

Параметрический полиморфизм

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

Пример:

Возьмем такой код на Java:

public class Pair<T, U> {
    private T first;
    private U second;
    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public U getSecond() {
        return second;
    }
    public void setSecond(U second) {
        this.second = second;
    }
    public void print() {
        System.out.println("(" + first.toString() + ", " + second.toString() + ")");
    }
}
public class TestPair {
    public static void main(String[] args) {
        Pair<String, Integer> pair1 = new Pair<>("Hello", 123);
        Pair<Double, Boolean> pair2 = new Pair<>(3.14, true);
        pair1.print(); // prints "(Hello, 123)"
        pair2.print(); // prints "(3.14, true)"
    }
}

Здесь у нас есть класс Pair, который принимает два параметра универсального типа T и U. Объекты Pair создаются с разными типами для T и U и код может быть использован для любой возможной комбинации типов.

Полиморфизм принуждения

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

Пример:

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

let stringVar = "5";
let numberVar = 10;
let result = stringVar + numberVar; // result will be "510"

Оператор + используется для объединения stringVar и numberVar. Поскольку stringVar — это строка, а numberVar — это число, перед объединением двух значений компилятор автоматически преобразует numberVar в строку.

Сочетание разновидностей полиморфизма

Чтобы создавать более гибкие и сложные программные системы, некоторые типы полиморфизма можно комбинировать. Это касается таких типов:

  1. ad-hoc полиморфизм;
  2. параметрический полиморфизм;
  3. полиморфизм принуждения.

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

Пример:

Код на Python, демонстрирующий сочетание ad-hoc полиморфизма и параметрического полиморфизма:

def add(x, y):
    return x + y

def add_strings(x: str, y: str):
    return x + ' ' + y

print(add(2, 3))        # Output: 5
print(add('Hello', 'World'))  # Output: Hello World
print(add_strings('Hello', 'World')) # Output: Hello World

Мы определили две функции с именем add:

  • первая функция add — это универсальная функция, которая может принимать данные любого типа, поддерживающего оператор +;
  • вторая функция add_strings — это специализированная функция, которая принимает только две входные строки и объединяет их с помощью символа пробела.

Функция add демонстрирует ad-hoc полиморфизм, потому что она может обрабатывать разные типы данных и обеспечивать желаемую операцию в зависимости от типа ее аргументов.

Функция add_strings демонстрирует сочетание ad-hoc полиморфизма и параметрического полиморфизма, потому что это специализированная функция, которая работает только со строками, при условии, что тип входных данных указан явно.

Пример полиморфизма

Один из примеров проявления полиморфизма — это изменение реализации методов при наследовании.

Допустим, у нас есть такой код на Java:

class Animal {
  void makeSound() {
    System.out.println("The animal makes a sound");
  }
}
class Dog extends Animal {
  void makeSound() {
    System.out.println("The dog barks");
  }
}
class Cat extends Animal {
  void makeSound() {
    System.out.println("The cat meows");
  }
}

У нас есть базовый класс Animal и два производных класса Dog и Cat. У класса Animal есть метод makeSound(), который печатает стандартное звуковое сообщение. Классы Dog и Cat переопределяют этот метод, чтобы вывести, какой звук воспроизводят эти животные.

Предположим, что мы создаем объекты каждого из этих классов и вызываем метод makeSound():

Animal animal = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
animal.makeSound(); // The animal makes a sound
dog.makeSound(); // The dog barks
cat.makeSound(); // The cat meows

Мы видим, что хотя все три объекта были созданы из разных классов, все они по-разному реагируют на метод makeSound(). Это как раз свойство полиморфизма — способность объектов принимать разные формы в зависимости от их контекста.

Заключение

Полиморфизм — это ключевая концепция объектно-ориентированного программирования, которая позволяет объектам принимать различные формы или вести себя по-разному в зависимости от контекста.

Этот инструмент позволяет программистам писать более гибкий и удобный в сопровождении код. Разрабатывая наш код как полиморфный, мы можем создавать более совершенные программные системы, а также совершать меньше ошибок.

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

В то же время, есть риск ухудшить производительность ПО, если полиморфизм использовать слишком часто. Поэтому всегда тщательно взвешивайте риски, прежде чем его применять.

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023