Наследование через 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 поддерживает только одиночное наследование; остальное выражают композицией.