Delphi-Help

  • Increase font size
  • Default font size
  • Decrease font size
Главная Азы Урок 26. Записи (часть 2)

Урок 26. Записи (часть 2)

Оцените материал
(22 голосов)

Записи (часть 2)

Введение

В позапрошлом уроке мы познакомились с записями: узнали, что это такое, как их заводить в своих программах, и как с ними потом работать. В прошлом уроке мы вели разговор о работе с файлами и научились работать с содержимым файлов. Сегодня мы объединим полученные знания, попутно продолжив знакомство с записями.

Записи с вариантами

Записи с вариантами - такой тип записей, у которых создано несколько наборов полей, а используемый набор определяется специальным полем-селектором. При этом часть полей могут быть общими для всех наборов.

0026_01

Например, мы хотим в программе хранить информацию об отрезке прямой на плоскости. Математически мы можем представить отрезок двумя способами:

Двумя точками, каждая из которых имеет координаты X и Y (т.е. X1,Y1,X2,Y2).

Одной точкой (X,Y), длиной отрезка и углом между ним и какой-либо осью (например, осью X).

Оба метода проиллюстрированы. Совершенно очевидно, что такую структуру удобно хранить в виде записи. Опишем первый вариант:

type
  TLineSegment = record
    X1,Y1: Real;
    X2,Y2: Real;
  end;

Для наглядности точки описаны отдельно, хотя короче будет поместить их в одну строку:

(X1,Y1,X2,Y2: Real);

Теперь второй вариант:

type
  TLineSegment = record
    X,Y: Real; //Один из концов отрезка
    Angle: Real; //Угол наклона
    Length: Real; //Длина отрезка
  end;

Всё хорошо, оба варианта рабочие и удобные... Но теперь представьте, что в программе мы должны предоставить пользователю возможность ввода отрезка и первым, и вторым способом, т.е. он сам будет решать, как ему удобнее. Что делать в этом случае? Не писать же 2 программы, базируясь то на одной структуре, то на другой? Вот тут-то нам и придут на помощь записи с вариантами.

Сначала давайте опишем простой перечислимый тип данных, который содержит два значения - тип описания отрезка:

type TLineSegmentType = (lsPoints,lsPolar);

Второй тип я назвал полярным, потому что это ни что иное, как полярные координаты. Что ж, теперь добавим в нашу запись переменную, которая будет определять тип описания отрезка:

TLineSegment = record
  LType: TLineSegmentType;
end;

Но тип данных должен быть описан ещё до компиляции программы, поэтому оба набора данных следует описать прямо сейчас. А для выбора варианта применяется уже известный нам оператор case:

TLineSegment = record
  case LType: TLineSegmentType of
    lsPoints:
      //Здесь нужно описать первый набор полей...
    lsPolar:
      //...а здесь второй
  end;
end;

Ещё раз: мы заводим переменную-селектор, применяем к ней оператор множественного выбора case, и для каждого из значений описываем нужный набор полей. Наборы необходимо заключать в круглые скобки. В нашем случае получится вот что:

TLineSegment = record
  case LType: TLineSegmentType of
    lsPoints: (
      X1,Y1: Real;
      X2,Y2: Real;
    );
    lsPolar: (
      X,Y: Real;
      Angle: Real;
      Length: Real;
    );
end;

Здесь есть одна особенность: оператор case не требуется закрывать командой end. Варианты наборов должны располагаться всегда в конце списка полей (т.е. сначала описываются фиксированные поля, а затем вариантные) - это объясняет отсутствие end для case - запись так и так будет закрыта с помощью следующего end.

В нашем случае структура всё ещё не оптимальна: координаты одной из точек у нас описаны в обоих наборах. Давайте вынесем их как постоянные поля:

type
  TLineSegmentType = (lsPoints,lsPolar);
 
  TLineSegment = record
    X,Y: Real; //Один из концов отрезка
    case LType: TLineSegmentType of
      lsPoints: (
        X2,Y2: Real; //Второй конец отрезка
      );
      lsPolar: (
        Angle: Real; //Угол наклона
        Length: Real; //Длина
      );
  end;

Это окончательный вид нашей записи. Посмотрите ещё раз и осмыслите написанное.

Ну а теперь перейдём к более знакомым вещам - сделаем интерфейс для ввода информации об отрезке и запрограммируем внесение всех данных в запись.

0026_02

Уверен, что с интерфейсной частью вы справитесь сами, поэтому привожу лишь код кнопки:

procedure TForm1.SaveButtonClick(Sender: TObject);
var L: TLineSegment;
begin
  if PointsRadio.Checked then
    L.LType:=lsPoints
  else
    L.LType:=lsPolar;
  case L.LType of
    lsPoints:
      begin
        L.X:=StrToFloat(X1Edit.Text);
        L.Y:=StrToFloat(Y1Edit.Text);
        L.X2:=StrToFloat(X2Edit.Text);
        L.Y2:=StrToFloat(Y2Edit.Text);
      end;
    lsPolar:
      begin
        L.X:=StrToFloat(XEdit.Text);
        L.Y:=StrToFloat(YEdit.Text);
        L.Angle:=StrToFloat(AngleEdit.Text);
        L.Length:=StrToFloat(LengthEdit.Text);
      end;
  end;
end;

Сначала мы смотрим, какой способа ввода у нас выбран на форме и соответствующим образом устанавливаем переменную-селектор LType. А затем уже переносим данные из полей ввода в нашу запись: если первый способ - из 4 левых полей, если второй - из 4 правых.

С какой целью мы ввели отрезок? Ну например давайте посчитаем его длину. Для этого введём собственную функцию, которая на вход будет принимать запись, а на выходе будет выдавать длину отрезка. Если забыли математику, напомню способ вычисления длины:

По двум точкам: длина - это квадратный корень из суммы квадратов разностей соответствующих координат.

По точке, углу и длине: промолчу, ага.

function GetLength(L: TLineSegment): Real;
begin
  case L.LType of
    lsPoints: Result:=Sqrt(Sqr(L.X2-L.X)+Sqr(L.Y2-L.Y));
    lsPolar: Result:=L.Length;
  end;
end;

Что может быть проще? Ну и в конец обработчика нажатия кнопки добавим:

MessageDlg('Длина отрезка: '+FloatToStr(GetLength(L)),mtInformation,[mbOk],0)

Проверьте правильность работы, запустив программу и введя какие-нибудь данные. Помните, что программа работает с расчётом на то, что исходные данные верны, т.е. что вместо чисел вы не вписали "всем привет!".

Упакованные записи

Пару слов о том, что такое упакованные записи, и с чем их едят. По умолчанию память под записи выделяется не очень экономно - помимо самих данных добавляются и служебные байты, которые отделяют блоки данных друг от друга. Существует принудительный способ заставить Delphi упаковать запись, т.е. минимизировать занимаемую ей память. Делается это указанием слова packed перед словом record. Разница порой может быть достаточно ощутимой. Пример: запись из строки длиной 5 символов, одного символа и трёх чисел разного типа. Объявим две разные записи: одна обычная, а другая упакованная:

TRecord1 = packed record
  Name: String[5];
  A: LongInt;
  C: Char;
  D: Double;
  N: Integer;
end;
 
TRecord2 = record
  Name: String[5];
  A: LongInt;
  C: Char;
  D: Double;
  N: Integer;
end;

А теперь самое интересное: посмотрим, сколько памяти занимает каждая из записей. Сделаем это функцией SizeOf():

var R1: TRecord1; R2: TRecord2;
begin
  ShowMessage(IntToStr(SizeOf(R1)));
  ShowMessage(IntToStr(SizeOf(R2)));
end;

В первом сообщении мы увидим 24, а во втором 32. Обычная запись занимает на треть больше памяти, чем упакованная! А теперь представьте, что у вас 100 000 таких записей?

Тем не менее, не стоит пренебрегать этим способом экономии памяти. В некоторых случаях использование пакованных записей может создавать разные ошибки в работе программы. Понять, что дело именно в packed, удаётся далеко не сразу. Так что, если храните десяток отрезков - не торопитесь паковать - если и выиграете, то не сильно.

Быстрый доступ к полям записей

В нашем примере у записи сравнительно мало полей - 4. Но бывают программы, где создаются записи с десятком полей и работа из-за этого замедляется. Хотя бы потому, что в коде приходится каждый раз набирать имя записи и точку, и лишь затем имя поля. Обрадую: мучаемся не только мы - компьютеру тоже приходится делать больше телодвижений. Каждый раз нужно определять адрес и искать запись в памяти, и лишь после этого можно найти значение поля. Чтобы облегчить участь и программисту и машине, был введён специальный оператор with (англ. "с"). К сожалению, о нём далеко не все знают, а ведь его использование увеличивает эффективность и кода, и работы самого программиста.

Итак, общая форма записи:

with запись do

{обращение к полям записи}

Не очень понятно? А теперь на нашем примере:

with L do
begin
  X:=StrToFloat(X1Edit.Text);
  Y:=StrToFloat(Y1Edit.Text);
  X2:=StrToFloat(X2Edit.Text);
  Y2:=StrToFloat(Y2Edit.Text);
end;

Что же мы имеем? А вот что: мы нашу запись "вынесли за скобку", и далее напрямую обращаемся к её полям. Удобно, не правда ли? Этот код абсолютно эквивалентен тому, что был написан нами ранее, только он более эффективен.

Несложно догадаться, что использование with для единичного обращения к записи бессмысленно:

with L do
  X:=5;

В этом случае мы ни в чём не выигрываем - только пишем больше кода.

Помните про оператор with и почаще его используйте - и себе жизнь облегчите, и программы станут профессиональнее.

Хранение записей в файлах

Ну вот мы и подошли к тому, для чего пришлось затронуть тему работы с файлами. Использовать записи мы научились, но тут вопрос: а как их хранить? Сохранять в файлах отдельно каждое поле - совершенно неудобно. А потом его нужно оттуда ещё как-то прочитать... Неужели нет способа проще? Есть!

Мы можем создать типизированный файл на основе имеющегося типа записи. Помните, как мы описывали файлы? file of ..., верно? Так вот, теперь в качестве типа будет выступать наша запись.

Вернёмся к программе, которая позволяет ввести отрезок. Теперь перед нами задача сохранить этот отрезок в файл. Ну, не в буквальном смысле, конечно - просто сохранить все его параметры.

Начнём с создания указателя на файл, который опишем указанным образом:

F: file of TLineSegment;

Этим мы сказали, что каждый элемент нашего файла - запись типа TLineSegment. А дальше всё как обычно - ничего нового: связываем указатель с файлом, открываем, записываем, закрываем. Без комментариев, что называется:

AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
Rewrite(F);
Write(F,L);
CloseFile(F);

Всё это нужно добавить в конец обработчика кнопки "Сохранить". Запустите программу, введите произвольный отрезок и нажмите кнопку. Если всё было сделано правильно, после сообщения о длине отрезка в папке с программой появится файл lines.dat. Расширение dat - стандартное для нестандартных данных (вот так фразу завернул!). Вы можете открыть этот файл любым текстовым редактором, но прочитать что-либо там будет затруднительно - это бинарный файл.

Теперь попробуем прочитать данные из этого файла. Создайте ещё одну кнопку "Загрузить". Код для неё будет такой:

procedure TForm1.LoadButtonClick(Sender: TObject);
var F: file of TLineSegment; L: TLineSegment;
begin
  AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
  Reset(F);
  Read(F,L);
  CloseFile(F);
  MessageDlg('Длина отрезка: '+FloatToStr(GetLength(L)),mtInformation,[mbOk],0);
end;

Опять же, если всё было сделано верно, то вы увидите длину введённого ранее отрезка.

Теперь давайте изменим нашу программу таким образом, чтобы вводимые отрезки добавлялись в файл, т.е. чтобы файл содержал сразу несколько записей. Но здесь нас подстерегает проблема: функция Append(), предназначенная для добавления данных в конец в файла, работает только с текстовыми файлами. У нас же файл типизированный и здесь такой фокус не пройдёт. Будем выполнять обходной манёвр. Предлагаю вот что: создадим временный файл, перепишем туда все записи из существующего файла, добавим новую запись, после чего удалим старый файл, а новый переименуем. Хитро, сложно? На самом деле не очень.

Чтобы провернуть всё это, пришлось добавить 2 переменные - ещё одну запись и один файл. Под буквами "N" и "O" я подразумеваю "new" и "old" (новый и старый).

procedure TForm1.SaveButtonClick(Sender: TObject);
var
  L,OL: TLineSegment;
  F,NF: file of TLineSegment;
begin
  {предыдущий код здесь опущен}
  SetCurrentDir(ExtractFilePath(Application.ExeName)+'lines.dat');
  AssignFile(NF,'temp.dat');
  Rewrite(NF);
  if FileExists('lines.dat') then
  begin
    AssignFile(F,'lines.dat');
    Reset(F);
    while not EOF(F) do
    begin
      Read(F,OL);
      Write(NF,OL);
    end;
    CloseFile(F);
  end;
  Write(NF,L);
  CloseFile(NF);
  DeleteFile('lines.dat');
  RenameFile('temp.dat','lines.dat');
end;

Разберём этот код подробно. Сначала делаем папку с программой рабочей, чтобы каждый раз не писать путь. Далее ассоциируем указатель с новым файлом, и открываем этот файл для записи. Далее проверяем, есть ли файл с предыдущими записями. Как мы договорились, если он есть, то нужно переписать из него все записи: связываемся с файлом, открываем его для чтения, а далее цикл по всем полям записи. Функция EOF() позволяет узнать, дошли ли мы до конца файла. Таким образом, пока файл не кончился, читаем из него одну запись и переписываем её в новый файл. После завершения закрываем старый файл. Осталось самое простое - записать новую запись, что мы и делаем. После этого старый файл lines.dat удаляем, а временный temp.dat переименовываем в новый lines.dat. Таким образом, достигнута требуемая цель. Запустите программу и добавьте в наш файл ещё несколько отрезков. О том, что добавление происходит успешно, можно судить по увеличивающемуся объёму файла.

Следующая задача: узнать, сколько записей в файле. Делается это очень просто - функцией FileSize(). Когда мы объявляем "файл из байтов" (file of byte), то получаем объём файла в байтах. Сейчас же мы узнаем, сколько записей в файле:

var F: file of TLineSegment;

{...}

AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');

Reset(F);

ShowMessage('В файле '+IntToStr(FileSize(F))+' записей');

CloseFile(F);

Код очень простой и понятный.

Ну и наконец последнее, о чём мне хотелось бы рассказать - это о "перемотке" файла. Я имею ввиду о том, как добраться до записи в середине файла, не перебирая все предыдущие через Read(). Такая задача встречается очень часто и решается она достаточно просто. Процедура Seek() перемещается по файлу на указанный по номеру элемент:

Seek(указатель_на_файл,номер_записи);

Пример: добавим на форму TListBox и кнопку "Обновить список":

procedure TForm1.UpdateButtonClick(Sender: TObject);
var F: file of TLineSegment; I,N: Integer;
begin
  AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
  Reset(F);
  N:=FileSize(F);
  CloseFile(F);
  ListBox1.Items.Clear;
  for I := 1 to N do
    ListBox1.Items.Add('Запись #'+IntToStr(I))
end;

0026_03

Как видно, эта кнопка добавляет в ListBox список записей в файле. Теперь кнопка "Загрузить" должна загрузить выбранную в списке запись и отобразить длину отрезка:

procedure TForm1.LoadButtonClick(Sender: TObject);
var F: file of TLineSegment; L: TLineSegment;
begin
  AssignFile(F,ExtractFilePath(Application.ExeName)+'lines.dat');
  Reset(F);
  Seek(F,ListBox1.ItemIndex);
  Read(F,L);
  CloseFile(F);
  MessageDlg('Длина отрезка: '+FloatToStr(GetLength(L)),mtInformation,[mbOk],0);
end;

Свойство ItemIndex у ListBox определяет номер строки, выбранной в данный момент (строки нумеруются с нуля). После открытия файла мы прыгаем на запись с таким номером, читаем её и затем определяем длину.

Просто? Думаю, что да. Работа с типизированными файлами принципиально ничем не отличается от работы с текстовыми. Зато обратите внимание, как легко можно оперировать записями! Теперь вы легко сможете создать простейшую базу данных.

Да, и помните, что при работе с типизированными файлами нельзя использовать функции ReadLn() и WriteLn() - они предназначены исключительно для текстовых файлов.

Другие материалы в этой категории: « Урок 25. Работа с файлами и каталогами (часть 1)

Авторизация



Счетчики