Производные типы и связанные процедуры
Производный тип объединяет данные в единое целое, а связанные процедуры превращают его из пассивной структуры в объект с поведением.
Производный тип (derived type) — пользовательский составной тип, объединяющий именованные компоненты; type-bound procedure — процедура, привязанная к типу и вызываемая через компонент объекта в стиле
obj%method(...).
От структуры к объекту
Производные типы появились ещё в Fortran 90 как замена разрозненным параллельным массивам. Вместо того чтобы держать координаты частицы в трёх отдельных массивах x, y, z и её массу в четвёртом, естественнее описать тип particle с полями и работать с массивом частиц. Это улучшает и читаемость, и локальность данных. Но до Fortran 2003 производный тип оставался чисто пассивным: связки «данные + операции над ними» не было, процедуры жили отдельно от типов. Fortran 2003 принёс полноценный объектный слой, и центральная его часть — type-bound procedures: процедуры, объявленные внутри типа и вызываемые через объект. Это превращает структуру в объект в полном смысле — с инкапсулированным поведением.
Объявление типа и компоненты
Тип объявляют конструкцией type :: имя ... end type. Компоненты описывают как обычные переменные. Компоненты могут иметь атрибуты доступа (private), значения по умолчанию, быть массивами, аллоцируемыми, указателями или другими производными типами.
module particle_mod
implicit none
private
public :: particle
type :: particle
real :: x = 0.0, y = 0.0, z = 0.0 ! значения по умолчанию
real :: mass = 1.0
contains
procedure :: kinetic => particle_kinetic ! связанная процедура
procedure :: move => particle_move
end type particle
contains
pure function particle_kinetic(self, vx, vy, vz) result(e)
class(particle), intent(in) :: self
real, intent(in) :: vx, vy, vz
real :: e
e = 0.5 * self%mass * (vx**2 + vy**2 + vz**2)
end function particle_kinetic
subroutine particle_move(self, dx, dy, dz)
class(particle), intent(inout) :: self
real, intent(in) :: dx, dy, dz
self%x = self%x + dx
self%y = self%y + dy
self%z = self%z + dz
end subroutine particle_move
end module particle_mod
Внутри типа после contains идут связанные процедуры. Запись procedure :: kinetic => particle_kinetic означает: «у типа есть метод kinetic, реализованный процедурой particle_kinetic». Имя метода (видимое снаружи) и имя процедуры (реализация) можно сделать разными — это даёт свободу переименовать реализацию, не трогая публичный вызов.
Передаваемый объект: pass и self
Ключевая деталь — первый аргумент связанной процедуры. По умолчанию метод неявно получает сам объект как первый аргумент; этот механизм называется pass. В примере это self (имя произвольно, часто пишут self или this). Важнейший момент: тип передаваемого аргумента объявляют не как type(particle), а как class(particle). Слово class делает аргумент полиморфным — он сможет принять и сам тип, и любой его наследник (об этом — в уроке про наследование). Даже если наследования пока нет, для type-bound procedure объявлять передаваемый объект через class — норма и требование совместимости с будущим расширением типа.
Вызов выглядит так:
use particle_mod
type(particle) :: p
p = particle(x=1.0, y=2.0, z=0.0, mass=4.0) ! конструктор по умолчанию
call p%move(0.5, 0.0, 0.0) ! self = p передаётся неявно
print *, "Ek =", p%kinetic(1.0, 0.0, 0.0) ! self = p
print *, "x =", p%x
Когда вы пишете p%move(0.5, 0.0, 0.0), объект p подставляется как self, а явные аргументы идут следом. Это и есть объектный синтаксис: данные и операция связаны, вызов читается как «частица, сдвинься».
Конструкторы: структурный и пользовательский
Для каждого типа компилятор бесплатно предоставляет структурный конструктор — функцию с именем типа, принимающую значения компонентов: particle(x=1.0, y=2.0, z=0.0, mass=4.0). Аргументы можно задавать по ключевым словам (имена компонентов) или позиционно. Если у компонентов есть значения по умолчанию, опущенные аргументы примут их. Структурного конструктора часто достаточно. Когда же создание объекта требует логики (валидация, вычисление производных полей), пишут собственную функцию-конструктор и обычно прячут структурный, делая компоненты private. Тогда единственная точка создания — ваша фабричная функция, что даёт контроль над инвариантами объекта.
Инкапсуляция через private-компоненты
Чтобы скрыть внутреннее представление, компоненты помечают private (либо весь тип объявляют с private внутри). Тогда снаружи нельзя ни читать, ни писать поля напрямую — только через методы. Это классическая инкапсуляция: пользователь видит поведение, но не устройство, и вы вольны менять представление, не ломая код потребителей.
type :: account
private
real :: balance = 0.0 ! недоступно снаружи напрямую
contains
procedure :: deposit
procedure :: get_balance
end type account
Как работает под капотом
Производный тип в памяти — это просто последовательно (с учётом выравнивания) уложенные компоненты, как struct в C. Никаких скрытых полей у непараметрического, ненаследуемого типа нет — массив particle столь же компактен, как четыре параллельных массива, но удобнее. Type-bound procedure тоже не добавляет данных к объекту: связь «тип → метод» хранится в метаданных типа, а вызов p%move(...) для непараметрического типа компилятор разрешает статически — превращает в обычный вызов particle_move(p, ...). Накладных расходов нет: объектный синтаксис здесь чистый сахар. Динамическая диспетчеризация (через таблицу методов) включается лишь при полиморфизме с наследованием — это отдельная история следующих уроков.
Аллоцируемые компоненты и динамические структуры
Компоненты производного типа не обязаны иметь фиксированный размер. Объявив компонент с атрибутом allocatable, вы получаете объект, чьё внутреннее представление выделяется в куче и может менять размер в течение жизни. Это превращает производный тип в строительный блок для динамических структур данных: векторов переменной длины, разреженных матриц, деревьев, графов. Классический пример — тип «динамический массив», который умеет расти.
type :: dyn_array
real, allocatable :: data(:)
integer :: n = 0 ! сколько элементов реально занято
contains
procedure :: push => dyn_push
end type dyn_array
! ...
subroutine dyn_push(self, value)
class(dyn_array), intent(inout) :: self
real, intent(in) :: value
real, allocatable :: tmp(:)
if (.not. allocated(self%data)) allocate(self%data(4))
if (self%n == size(self%data)) then ! место кончилось — удваиваем
allocate(tmp(2 * size(self%data)))
tmp(1:self%n) = self%data(1:self%n)
call move_alloc(tmp, self%data) ! передать владение без копии
end if
self%n = self%n + 1
self%data(self%n) = value
end subroutine dyn_push
Здесь важна встроенная подпрограмма move_alloc: она передаёт владение выделенной памятью от одной переменной к другой без копирования данных и без явного deallocate — старый массив автоматически освобождается, новый занимает его место. Это идиоматичный и эффективный способ «переселять» аллоцируемые массивы, в частности при росте контейнеров. Аллоцируемые компоненты обладают ещё одним приятным свойством: при присваивании производного типа (b = a) Fortran автоматически делает глубокое копирование аллоцируемых компонентов — выделяет новую память и копирует содержимое, а не просто дублирует указатель. Поэтому копии независимы, и не возникает классической проблемы разделяемого владения, типичной для сырых указателей в C. Это делает аллоцируемые компоненты безопасным выбором по умолчанию для динамических данных внутри объектов.
Параметризованные производные типы
Fortran 2003 ввёл ещё одну возможность — параметризованные производные типы (PDT), где тип получает параметры, влияющие на его компоненты. Параметры бывают двух видов: kind-параметры (известны на этапе компиляции, задают, например, точность) и len-параметры (могут определяться в рантайме, задают, например, размеры). Это позволяет одним определением типа покрыть семейство «настроек».
type :: matrix(k, rows, cols)
integer, kind :: k = 8 ! точность (compile-time)
integer, len :: rows, cols ! размеры (runtime)
real(k) :: a(rows, cols)
end type matrix
! применение:
type(matrix(8, 3, 3)) :: m ! double-матрица 3x3
PDT — продвинутая и не самая широко используемая возможность (её поддержка в компиляторах долго была неполной), но она показывает направление мысли: производный тип в современном Fortran — гибкая абстракция, способная параметризоваться и точностью, и размерами. На практике для большинства задач хватает аллоцируемых компонентов, дающих динамические размеры проще и переносимее. Тем не менее знать о PDT полезно: иногда они позволяют выразить семейство типов одним определением там, где иначе пришлось бы плодить generic-перегрузки. Главный вывод этого урока шире конкретных техник: производный тип в Fortran — это полноценный объект с данными (в том числе динамическими), поведением (type-bound procedures) и инкапсуляцией (private-компоненты), и именно на нём строится вся объектная модель языка, к которой мы переходим дальше.
Производные типы как основа абстракций предметной области
За технической стороной производных типов стоит более глубокая идея, ради которой они и существуют: моделирование предметной области. Хороший расчётный код говорит на языке задачи, а не на языке голых массивов чисел. Сравните два стиля. В «массивном» стиле частицы представлены четырьмя параллельными массивами x, y, z, mass, и по коду рассыпаны индексы x(i), y(i), которые читатель должен мысленно собирать в понятие «частица». В объектном стиле есть тип particle с осмысленными полями и методами, и код оперирует частицами напрямую: call p%move(...), p%kinetic(...). Второй стиль кардинально понятнее: он выражает намерение, а не механику доступа к памяти. Производный тип становится словом в словаре предметной области — «частица», «ячейка сетки», «граничное условие», «уравнение состояния», — и программа из манипуляций индексами превращается в осмысленное описание физики или инженерии задачи. Это резко повышает читаемость, снижает число ошибок (труднее перепутать поля именованного объекта, чем индексы параллельных массивов) и облегчает сопровождение: новый человек в проекте понимает структуру данных по именам типов и полей, а не реконструирует её из соглашений об индексации. Важно, что этот выигрыш в выразительности достаётся без потери производительности: как мы видели, непараметрический производный тип в памяти столь же компактен, как параллельные массивы, а его методы диспетчеризуются статически. Поэтому современный численный код всё чаще строят вокруг продуманных производных типов, моделирующих сущности задачи, — это лучшее из двух миров: ясность объектного описания и скорость нативных массивов. Овладев производными типами и связанными процедурами, вы получаете инструмент, позволяющий писать расчётный код, который читается почти как описание самой задачи, и именно это отличает поддерживаемую инженерную систему от трудночитаемого нагромождения индексов.
Частые ошибки
- Объявить передаваемый объект как
typeвместоclass. Тогда метод нельзя будет унаследовать/переопределить, и полиморфизм сломается. Для type-bound procedure первый аргумент — почти всегдаclass(...). - Забыть про
intentуself. Метод, меняющий объект, должен иметьclass(...), intent(inout); читающий —intent(in). Неверный intent либо запрещает изменение, либо мешает оптимизации. - Ожидать инкапсуляции без
private. По умолчанию компоненты публичны. Безprivateлюбой код лезет в поля напрямую, и инкапсуляции нет. - Путать имя метода и имя процедуры. В
procedure :: move => particle_moveслева — имя вызова, справа — реализация; их легко перепутать местами.
Итоги
- Производный тип группирует данные; type-bound procedures добавляют поведение, превращая структуру в объект.
- Метод вызывают через объект:
obj%method(args), объект неявно приходит первым аргументом (pass). - Передаваемый объект объявляют через
class(...)— это открывает путь к наследованию и полиморфизму. - Структурный конструктор бесплатен; для логики создания пишут свою фабрику и прячут компоненты
private. - Для непараметрического типа методы диспетчеризуются статически — накладных расходов нет.