Записи
   
Записи с вариантной частью (использование)  

Записи с вариантной частью

Не так давно на одном из форумов был задан вопрос: "А что такое, собственно, запись в вариантной частью, и зачем это нужно (практический пример использования)?"

По порядку... Сначала - что это такое...

Запись может иметь вариантную часть. Это означает, что можно задать несколько разных структур в пределах одного типа, а непосредственный выбор будет определяться специальным ("сигнальным") значением. вот пример записи с вариантами:
Type
  VR = Record
  s: string;
  Case byte of { Безымянный селектор }
    1: (i: integer); { или значение типа Integer }
    2: (f: double);  { или значение типа Double, причем оба значения,
                       i и f начинаются по одному и тому же адресу,
                       хотя и имеют разную длину }
  end;
Теперь в программе можно образаться как к целочисленному полю i, так и к вещественному - f:
var R: VR;
...
R.i := 10; { <--- изменяем целочисленную переменную }
R.f := R.f + 0.34; { <--- а тут - изменение переменной вещественной }
Можно в описании записи использовать не безымянный селектор, а поле-селектор:
Type
  styleType = (stInteger, stDouble);
  VR = Record
  s: string;
  Case style: styleType of { Это - тоже полноценное поле записи, к которому можно обращаться }
    stInteger: (i: integer); 
    stDouble: (f: double);
  end;
И пример использования данного поля-селектора:
var R: VR;
...
R.i := 10; { <--- устанавливаем целочисленное значение в 10 }
R.style := stInteger; { <--- и ставим соотв. признак }

{
  Теперь, анализируя поле-селектор, можно точно сказать, какое именно значение было сохранено - 
  целочисленное, или вещественное, и корректно работать с данными (к примеру, формат вывода для
  целочисленных и вещественных значений различен, выбираем правильный):
}
case R.style of
  stInteger: writeln('В записи хранится целое число: ', R.i:5);
  stDouble: writeln('В записи - вещественное значение: ', R.f:10:5);
end;
...
Теперь немного о типе селектора... В общем случае, тип его может быть любым, главное условие - чтобы он был перечислимым, и чтобы его емкость (т.е., количество значений, которые может принимать переменная этого типа) была не меньше числа вариантов в записи... Например, для описанной выше записи вполне подошел бы селектор типа Boolean, поскольку число вариантов = 2 (целое и вещественное поля), а переменная типа Boolean вполне способна хранить 2 разных значения: True и False... Тоглда описывать варианты надо было бы так:
Type
  VR = Record
  s: string;
  Case Boolean of { Безымянный селектор }
    False: (i: integer);
    True:  (f: double);
  end;
, т.е. метки должны быть того же типа, что и селектор...

Порядок описания меток не важен, вполне допустимо было бы сначала описать метку True, и только потом - False, главное - метки не должны повторяться. То есть, подобное описание будет ошибкой:
Type
  VR = Record
  s: string;
  Case Boolean of { Безымянный селектор }
    False: (i: integer);
    False: (f: double); { <--- !!! Ошибка - повторное определение метки !!! }
  end;
Существуют 2 ограничения на использование вариантных полей:
  1. описание вариантных полей должно завершать описание типа-записи, т.е. нельзя сначала определить вариантные поля, а потом - общие;
  2. в каждой записи может быть только один селектор, то есть, нельзя описать запись с двумя и более селекторами (и, естественно, с двумя или большим количеством групп вариантных полей)

Использование вариантных записей

А теперь обратимся ко второй части вопроса: "... зачем это нужно (практический пример использования)".

Допустим, что пишется программа, работающая с трехмерными фигурами... Совершенно естественно, что для такой программы понадоьбится структура, описывающая точку в трехмерной системе координат. Самое первое, что приходит в голову:
Type
  { Описываем тип Точка - с тремя координатами }
 TPoint = Record
   x, y, z: Real;
 End;
Однако у такого описания есть существенный недостаток... К примеру, нам надо все 3 координаты точки увеличить/уменьшить на определенную величину... Или присвоить всем трем координатам одинаковое значение, скажем, установить точку в начало координат. С первоначальным определением это делается так:
var p: TPoint;
...
p.x := p.x + delta; p.y := p.y + delta; p.z := p.z + delta;
{ или }
p.x := 0.0; p.y := 0.0; p.z := 0.0;
...
Уже не совсем удобно, правда? А если пространство будет иметь большее число измерений?

Здесь было бы полезно немного другое описание:
Type
  { Описываем тип Точка - с тремя координатами }
  TPoint = Record
    arr: Array[1 .. 3] Of Real;
  End;
Это дает возможность в цикле пробегать по всем координатам, и изменять их, или устанавливать в определенное значение, НО... Теперь гораздо менее удобно, например, проверять значения определенной координаты, скажем проверка, не равняется ли координата X точки нулю, будет записана вот так:
var p: TPoint;
...
if abs(p.arr[1]) < eps then ...
, что делает программу гораздо менее читабельной...

А что, если объединить эти 2 варианта? Вот так, к примеру:
Type
  { Описываем тип Точка - с тремя координатами }
  TAxis = (axisX, axisY, axisZ);
  TPoint = Record
    Case Boolean Of
      True : (x, y, z: Real);
      False: (arr: Array[TAxis] Of Real);
    End;
Теперь там, где это удобно, можно в цикле работать с координатами точки (через массив arr), а там, где надо обратиться к определенной координате - делать это напрямую, через поля x, y, z... Поскольку оба варианта находятся по одному адресу, то при изменении значения любым из способов будет меняться одно и то же поле (изменение .arr[1] совершенно аналогично изменению .X, а изменив .arr[3] получаем такой же результат, как и при работе с .Z)...