Производные типы и связанные процедуры

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

Производный тип (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.
  • Для непараметрического типа методы диспетчеризуются статически — накладных расходов нет.
Проверьте себя
1. Почему передаваемый объект (self) в type-bound procedure объявляют как class(particle), а не type(particle)?
AЧтобы метод мог принять сам тип и любой его наследник (полиморфизм)
BПотому что type(particle) запрещён внутри модуля
CЧтобы объект передавался по значению, а не по ссылке
DЭто синонимы, разницы нет
2. Как вызывается метод move у объекта p типа particle?
Amove(p, ...)
Bparticle%move(p, ...)
Cp%move(...) — объект p неявно передаётся как self
Dcall particle_move без объекта