Абстрактные типы, deferred и финализация
Абстрактный тип задаёт контракт без реализации, deferred-методы обязывают наследников их определить, а финализаторы освобождают ресурсы при уничтожении объекта.
Абстрактный тип (
abstract) нельзя инстанцировать напрямую; он объявляет deferred-методы — связанные процедуры без тела, которые каждый конкретный наследник обязан реализовать. Финализатор (final) — процедура, автоматически вызываемая при уничтожении объекта.
Зачем нужен тип, который нельзя создать
Иногда базовый тип существует только чтобы задать интерфейс — набор операций, которые обязаны уметь все наследники, — но сам по себе смысла не имеет. «Решатель уравнения» вообще не определён, пока не сказано какой; «граничное условие» — лишь контракт «уметь применяться к границе». Создавать объект такого типа бессмысленно и опасно. Атрибут abstract прямо запрещает инстанцирование: type(solver) для абстрактного solver не скомпилируется. Зато class(solver) разрешён — полиморфная переменная может указывать на конкретного наследника. Так абстрактный тип становится чистым интерфейсом: он диктует, что должен уметь объект, не навязывая как.
deferred-методы и abstract interface
Метод без реализации в абстрактном типе помечают атрибутом deferred. Но компилятору всё равно нужна сигнатура такого метода — её задают отдельно через блок abstract interface, а в типе ссылаются на неё словом procedure(имя_интерфейса), deferred.
module eos_mod
implicit none
private
public :: equation_of_state, ideal_gas
type, abstract :: equation_of_state
contains
procedure(pressure_if), deferred :: pressure
end type equation_of_state
abstract interface
pure function pressure_if(self, rho, temp) result(p)
import :: equation_of_state
class(equation_of_state), intent(in) :: self
real, intent(in) :: rho, temp
real :: p
end function pressure_if
end interface
type, extends(equation_of_state) :: ideal_gas
real :: r_specific = 287.0 ! газовая постоянная, Дж/(кг*К)
contains
procedure :: pressure => ideal_pressure ! реализуем deferred-метод
end type ideal_gas
contains
pure function ideal_pressure(self, rho, temp) result(p)
class(ideal_gas), intent(in) :: self
real, intent(in) :: rho, temp
real :: p
p = rho * self%r_specific * temp ! p = rho * R * T
end function ideal_pressure
end module eos_mod
Разберём ключевые детали. Тип equation_of_state помечен abstract и объявляет deferred-метод pressure, чья сигнатура описана в abstract interface под именем pressure_if. Внутри интерфейса обязателен оператор import — он втягивает имя equation_of_state из объемлющего модуля в область видимости интерфейса (без import имя типа было бы не видно). Конкретный тип ideal_gas наследует абстрактный и обязан реализовать pressure — иначе он сам останется абстрактным и его нельзя будет создать. Здесь он реализует метод как уравнение состояния идеального газа.
Паттерн «интерфейс + реализации»
Связка «абстрактный тип + deferred + наследники» — это идиоматичный для Fortran способ выразить то, что в других языках называют интерфейсом или абстрактным классом. Алгоритм высокого уровня пишут в терминах абстрактного типа, а конкретные реализации подставляют через полиморфизм:
subroutine simulate(eos, rho, temp)
class(equation_of_state), intent(in) :: eos ! любой наследник
real, intent(in) :: rho, temp
print *, "Давление =", eos%pressure(rho, temp)
end subroutine simulate
! вызов:
type(ideal_gas) :: gas
gas = ideal_gas(r_specific=287.0)
call simulate(gas, rho=1.2, temp=300.0) ! eos%pressure -> ideal_pressure
Процедура simulate работает с любым уравнением состояния, не зная конкретных формул. Добавив новый наследник (например, уравнение Ван-дер-Ваальса), вы не трогаете simulate — это и есть выгода программирования к интерфейсу. Компилятор при этом гарантирует, что любой переданный объект умеет pressure: deferred-контракт обязателен.
Финализация: final-процедуры
Объекты с ресурсами — выделенной памятью, открытыми файлами, дескрипторами — должны корректно освобождать их при уничтожении. Fortran 2003 ввёл финализаторы: процедуры с атрибутом final, которые система вызывает автоматически, когда объект перестаёт существовать (выход переменной из области видимости, явный deallocate, перевыделение). Финализатор — подпрограмма с единственным аргументом своего типа (не class, а type!) без pass в обычном смысле.
type :: buffer
real, allocatable :: data(:)
contains
final :: buffer_final
end type buffer
! ...
subroutine buffer_final(self)
type(buffer), intent(inout) :: self
if (allocated(self%data)) then
print *, "Финализация: освобождаю", size(self%data), "элементов"
deallocate(self%data)
end if
end subroutine buffer_final
Заметьте: аллоцируемые компоненты Fortran освобождает и сам, автоматически, так что для одной лишь памяти финализатор часто не нужен. Но если объект держит внешний ресурс — открытый файловый юнит, соединение, выделенный через C-интероп указатель — финализатор незаменим: это единственное место, где гарантированно отработает очистка. Финализаторы вызываются и для элементов массива объектов, и каскадно учитывают наследование. Важная тонкость: финализатор объявляют через type(...), а не class(...), потому что он привязан к конкретному типу и не диспетчеризуется полиморфно как обычный метод.
Как работает под капотом
Абстрактность и deferred — это ограничения на этапе компиляции: компилятор просто запрещает allocate/объявление по значению для абстрактного типа и проверяет, что каждый конкретный наследник предоставил все deferred-методы, заполнив соответствующие ячейки таблицы методов. В рантайме абстрактный тип ничем не отличается от обычного полиморфного — диспетчеризация та же. Финализаторы компилятор «развешивает» по точкам уничтожения объекта: он вставляет неявные вызовы финализатора там, где переменная выходит из области видимости или явно освобождается. Порядок при наследовании — сперва финализатор наследника, потом родителя, что зеркалит порядок конструирования. Это даёт детерминированную очистку без сборщика мусора — важное свойство для предсказуемого расчётного кода.
Паттерн «шаблонный метод» на абстрактных типах
Абстрактные типы с deferred-методами позволяют выразить классический архитектурный приём — шаблонный метод (template method). Идея: базовый абстрактный тип реализует общий каркас алгоритма в обычном (не deferred) методе, оставляя deferred-методами лишь те шаги, что различаются у наследников. Каркас вызывает абстрактные шаги, а конкретику поставляет каждый наследник. Так общая логика пишется один раз, а варьируется только необходимое.
type, abstract :: iterative_solver
contains
procedure :: solve => run_iterations ! общий каркас (НЕ deferred)
procedure(step_if), deferred :: step ! один шаг (различается)
procedure(conv_if), deferred :: converged
end type iterative_solver
abstract interface
subroutine step_if(self)
import :: iterative_solver
class(iterative_solver), intent(inout) :: self
end subroutine
logical function conv_if(self)
import :: iterative_solver
class(iterative_solver), intent(in) :: self
end function
end interface
! ...
subroutine run_iterations(self)
class(iterative_solver), intent(inout) :: self
integer :: iter
do iter = 1, 1000
call self%step() ! конкретный шаг наследника
if (self%converged()) exit ! конкретный критерий наследника
end do
end subroutine run_iterations
Здесь run_iterations — общий цикл итераций, единый для всех методов (Якоби, Гаусса-Зейделя, сопряжённых градиентов), а step и converged каждый наследник определяет по-своему. Добавить новый итерационный метод — значит реализовать лишь два deferred-метода, не трогая каркас. Это резко сокращает дублирование и делает структуру семейства алгоритмов прозрачной. Шаблонный метод — один из самых полезных паттернов в численном коде, где много алгоритмов разделяют общий «скелет», различаясь в деталях.
Детерминированное управление ресурсами
Финализаторы стоит рассмотреть в более широком контексте управления ресурсами, потому что здесь Fortran делает важный архитектурный выбор. В языках со сборщиком мусора (Java, Python, C#) момент уничтожения объекта недетерминирован: объект освобождается «когда-нибудь потом», когда до него доберётся сборщик. Это удобно для памяти, но плохо для других ресурсов — файлов, сетевых соединений, блокировок: вы не знаете, когда они освободятся, и можете исчерпать лимит дескрипторов задолго до сборки мусора. Fortran (как C++ с его деструкторами) идёт иным путём — детерминированной финализации: объект уничтожается в чётко определённый момент (выход из области видимости, явный deallocate), и финализатор отрабатывает именно тогда. Для расчётного кода предсказуемость критична: вы точно знаете, когда закрылся файл результатов, когда освободилась гигабайтная матрица, когда снялась блокировка параллельного ресурса.
Связка возможностей этого раздела — абстрактные типы, deferred-методы, финализаторы — даёт Fortran полноценную объектную модель: интерфейсы (абстрактные типы), их обязательную реализацию (deferred), полиморфное использование (class) и управляемое время жизни (final), и всё это без сборщика мусора, с предсказуемой производительностью и детерминированной очисткой. Это объектная ориентация, заточенная под нужды высокопроизводительных вычислений: достаточно мощная, чтобы строить большие гибкие архитектуры решателей и моделей, и достаточно «близкая к железу», чтобы не платить скрытых накладных расходов там, где они недопустимы. Именно поэтому современные крупные научные коды — от климатических моделей до пакетов вычислительной гидродинамики — всё чаще пишут в объектном стиле на Fortran 2008/2018, сочетая архитектурную гибкость ООП с традиционной для языка вычислительной мощью.
Контракты в коде: что гарантирует deferred
Глубинная ценность абстрактных типов с deferred-методами — в том, что они превращают договорённость в проверяемый компилятором контракт. В коде без таких средств «договор» о том, что все решатели обязаны иметь метод solve, существует лишь в головах разработчиков и в документации — и потому хрупок: кто-то напишет новый решатель, забудет нужный метод или назовёт его иначе, и проблема всплывёт лишь когда чужой код попытается этот метод вызвать. Абстрактный тип с deferred-методом делает контракт частью системы типов: компилятор не позволит создать конкретный наследник, не реализовавший все deferred-методы. Договорённость превращается из пожелания в железное правило, нарушить которое нельзя — попытка даёт ошибку компиляции. Это сдвигает обнаружение целого класса ошибок («забыли реализовать обязательную операцию») на самый ранний этап и делает архитектуру самодокументирующейся: глядя на абстрактный тип, любой разработчик сразу видит полный список того, что обязан предоставить наследник. Тот же принцип «контракт как код» пронизывает весь современный Fortran: intent аргументов фиксирует, как процедура с ними обращается; pure гарантирует отсутствие побочных эффектов; явные интерфейсы проверяют сигнатуры вызовов; implicit none требует объявления всего. Каждое из этих средств отнимает у программиста возможность «молча нарушить договор» и поручает проверку компилятору. В сумме они дают язык, в котором огромная доля ошибок отлавливается до запуска — что особенно ценно для научного кода, где запуск может стоить часов суперкомпьютерного времени, а неверный результат способен исказить научные выводы. Абстрактные типы и deferred-методы — важная часть этого арсенала надёжности: они позволяют выражать архитектурные обязательства (какие операции обязан поддерживать каждый член семейства типов) так, что их соблюдение гарантируется механически. Освоив их, вы умеете не просто писать гибкие полиморфные иерархии, но и делать их безопасными — с контрактами, которые невозможно случайно нарушить.
Частые ошибки
- Забыть
importвabstract interface. Без него имя производного типа не видно внутри интерфейса, и сигнатуру deferred-метода описать не удастся. - Оставить deferred-метод нереализованным в конкретном наследнике. Тогда наследник сам останется абстрактным, и его нельзя инстанцировать — компилятор это поймает при попытке создать объект.
- Объявить финализатор через
classвместоtype. Финализатор привязан к конкретному типу; передаваемый аргумент —type(...). - Рассчитывать на финализатор для аллоцируемой памяти, которая и так освобождается. Для одной лишь памяти финализатор обычно избыточен; он нужен для внешних ресурсов (файлы, дескрипторы, C-указатели).
- Пытаться создать объект абстрактного типа.
type(equation_of_state)илиallocate(equation_of_state :: x)запрещены — допустим лишь полиморфныйclass, указывающий на конкретного наследника.
Итоги
- Абстрактный тип (
abstract) нельзя инстанцировать; он задаёт чистый интерфейс через deferred-методы. - Сигнатуру deferred-метода описывают в
abstract interface(с обязательнымimport), а каждый конкретный наследник обязан её реализовать. - Паттерн «интерфейс + реализации» позволяет писать алгоритмы к абстракции и добавлять реализации, не меняя клиентский код.
- Финализаторы (
final) автоматически освобождают ресурсы при уничтожении объекта; объявляются черезtype(...). - Всё это — детерминированная объектная модель без сборщика мусора, удобная для расчётного кода.