Наследование через extends

extends создаёт новый тип на основе существующего, наследуя его компоненты и методы и добавляя или переопределяя своё.

Наследование в Fortran реализуется атрибутом extends(базовый_тип): производный тип получает все компоненты и связанные процедуры родителя и может добавлять новые либо переопределять унаследованные.

Зачем наследование в численном коде

Наследование — это механизм выражения отношения «является частным случаем». В расчётных задачах оно встречается естественно: «явный решатель» и «неявный решатель» — оба решатели; «треугольный», «четырёхугольный» элементы сетки — все конечные элементы; «идеальный газ» и «реальный газ» — оба уравнения состояния. Вместо копирования общей части в каждый тип её выносят в базовый тип, а специфику — в наследников. Это сокращает дублирование и, в связке с полиморфизмом (следующий урок), позволяет писать алгоритмы, работающие с любым наследником через общий интерфейс. Fortran 2003 добавил это в язык, не ломая его расчётную природу: наследование тут служит структурированию больших инженерных кодов, а не имитации «всё есть объект».

Расширение типа

Наследника объявляют с атрибутом extends. Он автоматически содержит все компоненты родителя плюс свои.

module shapes_mod
  implicit none
  private
  public :: shape, circle

  type :: shape
    character(len=20) :: name = "shape"
  contains
    procedure :: area => shape_area
    procedure :: describe
  end type shape

  type, extends(shape) :: circle
    real :: radius = 1.0          ! новый компонент
  contains
    procedure :: area => circle_area   ! переопределение метода area
  end type circle

contains

  pure function shape_area(self) result(a)
    class(shape), intent(in) :: self
    real :: a
    a = 0.0                        ! базовая фигура без площади
  end function shape_area

  pure function circle_area(self) result(a)
    class(circle), intent(in) :: self
    real :: a
    a = 3.14159265 * self%radius**2
  end function circle_area

  subroutine describe(self)
    class(shape), intent(in) :: self
    print *, trim(self%name), " area=", self%area()
  end subroutine describe

end module shapes_mod

Тип circle наследует от shape компонент name и метод describe, добавляет компонент radius и переопределяет метод area: его реализация circle_area заменяет унаследованную shape_area. Переопределение возможно именно потому, что передаваемый объект объявлен через class: сигнатуры совместимы, и компилятор разрешает замену.

Расположение унаследованных компонентов

Унаследованные компоненты доступны напрямую по имени: self%name внутри circle работает, хотя name объявлен в shape. Кроме того, к «родительской части» объекта можно обратиться целиком — по имени базового типа как к компоненту. Это бывает нужно при инициализации или при вызове родительской версии метода.

type(circle) :: c
c%name = "circle"          ! унаследованный компонент напрямую
c%radius = 2.0             ! свой компонент
c%shape%name = "circle"    ! то же поле через родительскую часть
print *, c%area()          ! вызовет circle_area

Запись c%shape ссылается на вложенную «родительскую часть» объекта circle — она имеет тип shape и содержит все его компоненты. Это удобно, когда нужно передать объект-наследник в процедуру, ожидающую родителя по значению, или явно работать с унаследованным срезом.

Конструирование наследника

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

type(circle) :: c1, c2
! родительскую часть как объект shape, затем свои поля:
c1 = circle(shape=shape(name="c1"), radius=1.5)
! или унаследованные компоненты по именам:
c2 = circle(name="c2", radius=2.5)

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

Как работает под капотом

Объект-наследник в памяти — это объект родителя, к которому дописаны новые компоненты. Раскладка строго префиксная: первые байты circle в точности совпадают с раскладкой shape, далее идёт radius. Поэтому c%shape — это просто первый сегмент памяти объекта, без копирования. Такая префиксная компоновка — фундамент полиморфизма: указатель/ссылку на circle можно безопасно трактовать как shape, потому что общая часть лежит в начале. Методы хранятся в таблице, привязанной к типу; переопределённый метод в таблице circle указывает на circle_area, тогда как в таблице shape та же ячейка указывает на shape_area. Этот механизм таблиц и обеспечивает динамический выбор метода, который мы разберём в уроке про полиморфизм.

Вызов метода родителя из переопределения

Частая потребность при наследовании — переопределить метод так, чтобы он дополнял, а не полностью заменял родительскую версию: сделать что-то своё и при этом вызвать поведение предка. В терминах ООП это обращение к «родительской» реализации. В Fortran для этого пользуются доступом к родительской части объекта. Поскольку self%shape внутри наследника — это объект родительского типа, через него можно вызвать родительский метод явно. Правда, есть тонкость: вызов self%shape%describe() применит метод так, как он определён для статического типа shape, что обычно и нужно при «достраивании» поведения.

type, extends(shape) :: labeled_circle
  real :: radius = 1.0
  character(len=30) :: label = ""
contains
  procedure :: describe => labeled_describe
end type labeled_circle
! ...
subroutine labeled_describe(self)
  class(labeled_circle), intent(in) :: self
  ! сначала своё:
  print *, "Метка:", trim(self%label)
  ! затем поведение родителя через его часть:
  call self%shape%describe()
end subroutine labeled_describe

Этот приём избавляет от копирования родительского кода в наследник: общая логика остаётся в одном месте (у предка), а наследник лишь добавляет специфику. Он особенно полезен в иерархиях инициализации и очистки, где каждый уровень должен выполнить свою часть работы поверх родительской. Аналог в других языках — вызов super.method(); в Fortran роль super играет доступ к именованной родительской части.

Композиция против наследования

Наследование — мощный, но не единственный и часто не лучший способ переиспользования. Опытные инженеры держат в голове принцип «предпочитай композицию наследованию». Композиция означает: вместо «тип B является типом A» сказать «тип B содержит A как компонент». Технически это просто поле производного типа: type(engine) :: motor внутри типа car. Разница глубокая. Наследование создаёт жёсткую связь «is-a» и открывает наследнику внутреннее устройство родителя; изменение базового типа способно сломать всех наследников (хрупкость базового класса). Композиция же даёт связь «has-a» через узкий публичный интерфейс компонента: car пользуется engine только через его методы, не завися от внутреннего устройства.

КритерийНаследование (extends)Композиция (поле-объект)
Отношение«является» (is-a)«содержит» (has-a)
Связанностьтесная, видит внутренностислабая, через интерфейс
Полиморфизместь (через class)нет автоматического
Гибкость заменынижевыше

Практическое правило таково: наследование оправдано, когда между типами действительно есть отношение «частный случай» и вам нужен полиморфизм — единый интерфейс для разных реализаций (фигуры, решатели, граничные условия). Если же вы наследуете лишь чтобы «переиспользовать пару методов», почти всегда чище включить нужный объект компонентом и делегировать ему вызовы. Злоупотребление наследованием порождает глубокие хрупкие иерархии, которые тяжело менять; разумная композиция даёт гибкие, слабосвязанные конструкции. Fortran поддерживает оба подхода, и зрелый дизайн сочетает их: неглубокие иерархии наследования там, где нужен полиморфизм, и композиция везде, где достаточно делегирования. Помня про одиночное наследование Fortran, композицию часто выбирают и вынужденно — чтобы «подмешать» несколько ролей, ведь нескольких базовых типов у extends быть не может. Полезный диагностический вопрос при проектировании: можно ли честно сказать «наследник ЕСТЬ разновидность родителя» во всех контекстах, где используется родитель? Если да — наследование уместно; если ответ «вообще-то нет, просто хочется переиспользовать код» — почти наверняка нужна композиция. Этот простой тест отсекает большинство ошибочных применений наследования и ведёт к чистым, гибким иерархиям, которые легко расширять новыми наследниками и поддерживать годами без накопления хрупкости.

Глубина иерархий и принцип подстановки

Проектируя иерархии наследования, полезно держать в голове два принципа, выработанных десятилетиями практики ООП. Первый — держите иерархии неглубокими. Соблазн строить длинные цепочки наследования (A → B → C → D → E) почти всегда оборачивается проблемами: изменение в корне иерархии непредсказуемо влияет на всех потомков, понять поведение глубокого наследника становится трудно (нужно держать в уме всю цепочку), а связанность нарастает. Опытные инженеры предпочитают плоские иерархии в один-два уровня и используют композицию там, где иначе появился бы третий уровень. Второй принцип — подстановка Лисков: наследник должен быть употребим везде, где ожидается родитель, не нарушая ожиданий. Если код, работающий с class(shape), корректен для базовой фигуры, он должен оставаться корректным и для любого наследника — круга, прямоугольника. Это значит, что переопределённые методы обязаны соблюдать «контракт» родительского метода: возвращать осмысленный результат в тех же терминах, не выбрасывать неожиданных ошибок, не требовать большего, чем обещал родитель. Нарушение подстановки — например, наследник, чей метод area в каких-то случаях возвращает бессмыслицу или требует особой предварительной настройки, — делает полиморфизм опасным: код, написанный к базовому типу, начинает ломаться на отдельных наследниках. Соблюдение же принципа подстановки — это то, что делает наследование и полиморфизм надёжными: алгоритм, написанный в терминах class(shape), работает правильно с любым корректным наследником, и добавление новых наследников ничего не ломает. Эти принципы универсальны для ООП в любом языке, и Fortran с его строгой типизацией и явными контрактами хорошо им следует. Помните: наследование в Fortran — мощный инструмент структурирования больших инженерных кодов, но его сила раскрывается лишь при дисциплинированном применении — неглубокие иерархии, честная подстановка, и композиция везде, где наследование не даёт реального выигрыша в виде полиморфизма.

Частые ошибки

  • Множественное наследование. Fortran поддерживает только одиночное наследование: extends указывает ровно один базовый тип. Композицию «много ролей» выражают вложением компонентов, а не несколькими родителями.
  • Переопределение с несовместимой сигнатурой. Переопределяющий метод должен иметь совместимый интерфейс (тот же набор аргументов, передаваемый объект через class своего типа). Иначе компилятор отвергнет переопределение.
  • Обращение к self%shape там, где нет наследования. Родительская часть существует только если тип объявлен с extends; у базового типа такого вложенного имени нет.
  • Путать наследование и включение. type, extends(shape) — это «является shape»; а просто поле type(shape) :: base внутри — это «содержит shape» (композиция), без полиморфной подстановки.

Итоги

  • Наследование задаётся атрибутом extends(базовый); наследник получает все компоненты и методы родителя.
  • Можно добавлять новые компоненты/методы и переопределять унаследованные (требуется class-передаваемый объект).
  • К родительской части обращаются по имени базового типа как к компоненту: obj%base_type.
  • В памяти наследник — префиксное расширение родителя, что и делает возможным полиморфизм.
  • Fortran поддерживает только одиночное наследование; остальное выражают композицией.
Проверьте себя
1. Сколько базовых типов может указывать extends в Fortran?
AЛюбое число — поддерживается множественное наследование
BРовно один — только одиночное наследование
CНе более двух
DНаследование в Fortran не поддерживается вовсе
2. Как внутри типа circle обратиться к его родительской части типа shape?
AЧерез self%parent
BЧерез self%shape (имя базового типа как компонент)
CЧерез super(self)
DНикак — родительская часть недоступна