Delphi Prism -- работаем с файлами

 

Михаил Перов

 

Решение более-менее серьезных задач обычно требует длительного хранения информации -- настроек программы, истории работы и т.д. Этим объясняется постоянный интерес к теме работы с файлами. Платформа .NET имеет все необходимое для того, чтобы программист забыл про существование устройств хранения данных и полностью доверился «матрице» -- мощной объектной модели.

Получив такой инструмент, как Delphi Prism, многие «дельфисты» обрели полноценный доступ к технологиям самой последней версии платформы .NET. Конечно, если сравнивать новый продукт, например, с C#, наверняка в нем пока будет чего-то не хватать, однако сам факт выхода Delphi Prism, кажется, решает проблему постоянного отставания Delphi.NET от поддержки широкого набора средств работы под .NET и Mono.

В настоящее время все тонкости работы в новой среде программирования можно условно разделить на относящиеся к употреблению пока еще непривычного для большинства «дельфистов» синтаксиса и на касающиеся свежих возможностей самой платформы .NET.

 

Особенности платформы .NET

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



>>> schema.bmp

Фрагмент иерархии классов, связанных с потоками из пространства имен System.IO



Болая полная информация по иерархии классов имеется на сайте http://msdn.microsoft.com/ru-ru/library/default.aspx.

Отдельно стоит упомянуть класс File (System.IO.File). В нем собраны многие возможности по работе с файловой структурой. Для его использования требуется .NET Framework 3.5 -- современная версия .NET, поддерживаемая Delphi Prism. С нее-то мы и начнем.

Сначала в первом примере просто создадим новый файл с помощью волшебного класса File.

Листинг 1. Чтение текстового файла с использованием классов File и StreamWriter

 

namespace Ex1;

interface

uses

  System.IO;

type

  ConsoleApp = class

  public

    class method Main;

  end;

implementation

 

class method ConsoleApp.Main;

begin

//Создание текстового файла и запись в него данных

var filename:="C:Ex1.txt";

 using tr:StreamWriter:=File.CreateText(filename) do begin

    tr.WriteLine(" Насколько схожи традиционный Delphi и Prism? ");

    tr.WriteLine(" Первое и важнейшее их различие");

    tr.WriteLine(" -- поддержка в Prism современной");

    tr.WriteLine(" актуальной версии платформы .NET");

 end;

//Вывод в консоль содержимого файла вместе с датой создания

//Чем-то похоже на FileToString из JEDI для Delphi

   Console.WriteLine(File.ReadAllText(filename));

   Console.WriteLine((new FileInfo(filename)).CreationTime.ToLongDateString);

  

   File.Delete(filename);

   Console.ReadKey;

end;

end.

Класс StreamWriter предназначен специально для записи текстового файла. При этом мы почти до предела упростили для себя работу, используя конструкцию usingdo beginend; Она заменяет конструкцию

try
finally
    if (_Object is Idisposable) then (_Object as Disposable).Dispose();
end;

где _Object -- условное обозначение создаваемого объекта. Таким образом, программист частично снимает с себя ответственность за жизненный цикл созданного объекта независимо от модели его памяти. Напоследок упомянем FileInfo -- еще один волшебный класс, сопоставимый по мощности с классом File. Оба они предназначены для решения одних и тех же задач, но для FileInfo нужно создавать экземпляр класса.

StreamWriter и StreamReader -- классы, специально созданные для работы с текстом. Конечно, желательно, чтобы вы были уверены, что будете работать именно с текстом, в противном случае рекомендуется выбрать класс FileStream. Классы StreamWriter и StreamReader освобождают программиста от забот, связанных с кодировкой. Поразительно, насколько все кажется в .NET взаимозаменяемым и универсальным!

 

>>>> img1.

Результат работы первого примера в вариантах .NET и Mono



Теперь напишем еще более скромную программу для чтения файла EMPLOYEE.txt, использовав класс StreamReader. Сначала экспортируем таблицу EMPLOYEE.FDB (из демонстрационной базы для пакета FireBird 2.0) в текстовый файл. В качестве разделителя зададим символ «;». В результате получим полноценный CSV-файл, с которым можно упражняться.



Листинг 2. Чтение текстового файла EMPLOYEE.txt с использованием класса StreamReader

class method ConsoleApp.Main;

begin

  //Для чтения текстового файла применим класс StreamReader

  using tr: StreamReader:=new StreamReader("C:EMPLOYEE.txt") do begin

   

       loop begin

 

           var line:=tr.ReadLine ;

       

           if not(Assigned(line)) then break;

          

           Console.WriteLine(line);

      

        end;

      Console.ReadLine;

    end;

  end;

end.

 

 

Рассмотрим второй пример. Вместо строки

 

tr:StreamReader:=new StreamReader("C:EMPLOYEE.txt")

можно просто написать

tr:=new StreamReader("C:EMPLOYEE.txt")

Полученная конструкция также будет работать. Обратите внимание: если вместо конструктора StreamReader поставить родительский конструктор TextReader и явно указать тип StreamReader для идентификатора tr, то компилятор и это верно расценит.

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



Листинг 3. Применение технологии OLEDB для чтения и анализа данных текстового файла

class method ConsoleApp.Main;

begin

//Читаем текстовый файл с использованием технологии OLEDB

    var FileName:='C:EMPLOYEE.txt';

    var Delimited:=';';

    var file:FileInfo:=new FileInfo(FileName);

 

  using  con:OleDbConnection:=

            new OleDbConnection(

         'Provider=Microsoft.Jet.OLEDB.4.0;Data Source='+

         file.DirectoryName+

         ';Extended Properties=''text;HDR=Yes;FMT=Delimited('+

         Delimited

         +')'';') do begin

 

       con.Open();

 

        using cmd:OleDbCommand:=new OleDbCommand(string.Format

                             ("SELECT * FROM [{0}]", File.Name), con) do begin

 

        using rdr:OleDbDataReader:=cmd.ExecuteReader() do begin

           

                while (rdr.Read()) do begin

 

  //Выборочно выводим столбцы в консоль

  Console.WriteLine('Имя:{0} Фамилия:{1} Отдел:{2}',rdr[1],rdr[2],rdr[5]);

                   

                end;

               

                rdr.Close;

           

             end;

           Console.ReadLine;

           end;

        end;

    end;

end.

 

В третьем примере была использована технология работы с базами данных без самой базы. Конечно, можно решать задачу в лоб, используя основные возможности платформы .NET, – применить метод класса String.Split, который автоматически будет разбивать очередную читаемую строку на элементы и заносить их в массив, но, согласитесь, что вышло оригинально.

И все это можно делать в Delphi Prism только потому, что реализован полноценный доступ к богатству средств .NET. Аналогичный примененному в нашем примере текст может быть элементом хитрого алгоритма для обработки разнородных данных из различных источников.

 

Теперь перейдем к классу TextFieldParser, изначально созданному для программистов, работающих на Бейсике. Для доступа к этому классу надо добавить в программу ссылку на Microsoft.VisualBasic.FileIO (Microsoft.VisualBasic.dll).



>>> img2

Результат работы третьего примера в вариантах .NET и Mono



Листинг 4. Чтение текстового файла и запись его данных в бинарном формате с помощью сериализации

...

[Serializable]

TEMPLOYEE = public class

        public

        EMP_NO:SmallInt;

        HIRE_DATE:DateTime;

        JOB_GRADE:SmallInt;

        SALARY:Decimal;

        FIRST_NAME:String;

        LAST_NAME:String;

        PHONE_EXT:String;

        DEPT_NO:String;

        JOB_CODE:String;

        JOB_COUNTRY:String;

        FULL_NAME:String;

end;



implementation



//В этот раз для чтения текстового файла используем класс TextFieldParser

//и сериализуем данные -- в этом случае записываем их на диск в бинарном формате.

 

class method ConsoleApp.Main;

    var EMP:TEMPLOYEE;

    win1251:=Encoding.GetEncoding("windows-1251");

    EMPS:=new ArrayList();

begin

  File.Delete('C:EMPLOYEE.dat');

 

  using parser:TextFieldParser:=new

                    TextFieldParser('C:EMPLOYEE.txt',win1251) do begin

    parser.SetDelimiters(";");

 

  while not(parser.EndOfData) do begin

    

     var fields:array of string:=parser.ReadFields();

   

    //Тут можно что-то сделать с полями строки

 

        EMP:=new TEMPLOYEE();

 

        EMP.EMP_NO:=SmallInt.Parse(fields[0]);

        EMP.HIRE_DATE:=DateTime.Parse(fields[4]);

        EMP.JOB_GRADE:=SmallInt.Parse(fields[7]);

        EMP.SALARY:=Decimal.Parse(fields[9]);

        EMP.FIRST_NAME:=fields[1];

        EMP.LAST_NAME:=fields[2];

        EMP.PHONE_EXT:=fields[3];

        EMP.DEPT_NO:=fields[5];

        EMP.JOB_CODE:=fields[6];

        EMP.JOB_COUNTRY:=fields[8];

        EMP.FULL_NAME:=fields[10];

 

        EMPS.Add(EMP);

 

       end;

 end;

 

   //Сохраняем список объектов на диск в бинарном формате

   // -- проводим сериализацию объекта класса ArrayList

    using fileStream:=new FileStream('C:EMPLOYEE.dat',FileMode.Create) do begin

 

     (new BinaryFormatter()).Serialize(fileStream,EMPS);

 

    end;

 System.Console.ReadLine;

end;

end.

 

Пойдем дальше -- перезапишем наш файл в какой-нибудь экзотический формат. На самом деле это будет бинарный формат (т.е. не текстовый), и тогда полученный файл по размеру окажется больше, чем исходный текстовый. Но идея в данном случае заключается не в экономии жизненного пространства на громадном диске, а в универсальности подхода к хранению информации. Метод сериализации позволяет выбрать оптимальный вариант между экономией места на жестком диске (для данного рассматриваемого случая), правилами безопасности и спецификой бизнес-логики, а именно особенностями структуры данных. Здесь имеется в виду ориентация на структуру, оптимизированную не для хранения, а для обработки данных. Сериализация -- хорошее решение для сохранения данных программы, написанной под .NET, во временном промежутке между ее сессиями. Но это далеко не единственное применение такого подхода. В нашем варианте мы выбрали бинарный формат для сериализации и получили новый файл EMPLOYEE.dat, в котором хранятся те же данные, что и в EMPLOYEE.txt, но уже в недоступном для постороннего глаза виде. Однако мы выбрали формат, удобный для объекта класса ArrayList -- «навороченного» массива объектов. Для сериализации необязательно пользоваться сложными классами-контейнерами, можно написать свою структуру.

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

Кстати, заметили ли вы, что объекту класса TextFieldParser пришлось явно указывать кодировку файла?

Следующий пример будет достаточно простым. Проводим десериализацию объекта класса ArrayList (т.е. выполняем действие, обратное сериализации), убеждаемся, что она получилась, выводя уже знакомые данные в консоль, и записываем снова все на диск, только уже в формате XML.



Листинг 5. Десериализация бинарного файла и сериализация в формате XML

...

class method ConsoleApp.Main;

var EMP:Ex4.TEMPLOYEE;

    EMPS:=new ArrayList(); i:Integer;

begin

  // Читаем бинарный файл --

  // десериализуем его обратно в объект класса ArrayList

 

  using filestream:=new FileStream('C:EMPLOYEE.dat',FileMode.Open) do begin

 

    EMPS:=(new BinaryFormatter()).Deserialize(filestream) as ArrayList;

 

  end;

 

  for i:=0 to EMPS.Count-1 do begin

 

   EMP:=(EMPS.Item[i] as TEMPLOYEE);

   System.Console.Writeline(

 

//Выводим в консоль некоторые столбцы

//Сложно привыкнуть, когда IDE за вас постоянно расставляет

//в тексте END к месту и не к месту, иногда приходится долго

//искать следы этих диверсий!

 

//Выборочно выводим в консоль значения столбцов

String.Format('{0}'+Chr(9)+'{1}'+Chr(9)+'{2}'+Chr(9)+'{3}'+Chr(9)+'{4}',

                EMP.EMP_NO.ToString,

                EMP.DEPT_NO.ToString,

                EMP.HIRE_DATE.ToShortDateString,

                EMP.FULL_NAME,

                EMP.SALARY)

   );

 

  end;

 

   //Сохраняем список объектов на диск в формате XML,

   //т.е. опять проводим сериализацию, но уже с использованием класса SoapFormatter

    using fileStream:=new FileStream('C:EMPLOYEE.xml',FileMode.Create) do begin

 

     (new SoapFormatter()).Serialize(fileStream,EMPS);

 

    end;

 

  System.Console.ReadLine;

 

end;

end.

 

 

На выходе получили громадный файл (по сравнению с предыдущими), имеющий, однако, открытый формат -- важнейшее из преимуществ, которое дает XML.

 

 

>>>img3.

Результат работы пятого примера [ЭТОТ РИСУНОК МОЖНО ВЫРЕЗАТЬ]

>>>img4.

Фрагмент файла, полученного в пятом примере, – результат сериализации класса ArrayList

[НАЧИНАЯ С ЭТОГО МЕСТА – ПОЙДЕТ ТОЛЬКО НА САЙТ] 

Формат XML оптимален для межпрограммного кросс-платформенного взаимодействия. Фактически он давно уже служит общепринятым стандартом, однако каждый разработчик вправе захотеть чего-то необычного, например хранить данные в старом добром (и горячо любимом мною) формате INI.

Microsoft уже давно настойчиво рекомендует всем хранить информацию о конфигурации программ в реестре или (что лучше) в XML-файлах и считает формат INI анахронизмом древних 16-разрядных систем, однако стоит рассмотреть эту проблему хотя бы из любви к искусству. Для того чтобы записать наши данные в INI-файл, придется импортировать функции из библиотеки kernel32.dll.

Листинг 6. Конвертируем файл из XML в INI

namespace Ex6;

interface

uses

 System.Runtime.InteropServices,System.Text,

 System.IO,

 System.Collections,Ex4,

 System.Runtime.Serialization,

 System.Runtime.Serialization.Formatters.Soap;

type

  ConsoleApp = class

  public

    class method Main;

  end;

 

   TIniFile = public class

   private

        [DllImport("kernel32")]

         class method WritePrivateProfileString(section:String;

             key:string;val:string;filePath:string):int64; External;

        [DllImport("kernel32")]

         class method GetPrivateProfileString(section:string;

                  key:string;def:string;retVal:StringBuilder;

             size:integer;filePath:string):integer; External;

   public

             path:string;

 

        constructor(const INIPath:string);

        procedure IniWriteValue(const Section,Key,Value:String);

        function IniReadValue(const Section,Key:String):String;

   end;

implementation

 

class method ConsoleApp.Main;

var ini:=new TIniFile('C:EMPLOYEE.ini');

begin

//Конвертируем записи из XML в INI

//Для этого десериализуем объект класса ArrayList и записываем каждую строку

//данных в виде отдельной секции INI-файла.

 

 

  using filestream:=new FileStream('C:EMPLOYEE.xml',FileMode.Open) do begin

   using EMPS:=(new SoapFormatter()).Deserialize(filestream) as ArrayList do begin

 var arr:=EMPS.ToArray;

       

        var EMPLIST:=''; var i:=0;

        for each EMPOBJ in arr do begin

           var EMP:=(EMPOBJ as Ex4.TEMPLOYEE);

           var SECNAME:='EMP'+EMP.EMP_NO;

 

            ini.IniWriteValue(SECNAME,'EMP_NO',EMP.EMP_NO.ToString);

            ini.IniWriteValue(SECNAME,'HIRE_DATE',EMP.HIRE_DATE.ToShortDateString);

            ini.IniWriteValue(SECNAME,'JOB_GRADE',EMP.JOB_GRADE.ToString);

            ini.IniWriteValue(SECNAME,'SALARY',EMP.SALARY.ToString);

            ini.IniWriteValue(SECNAME,'FIRST_NAME',EMP.FIRST_NAME);

            ini.IniWriteValue(SECNAME,'LAST_NAME',EMP.LAST_NAME);

            ini.IniWriteValue(SECNAME,'PHONE_EXT',EMP.PHONE_EXT);

            ini.IniWriteValue(SECNAME,'DEPT_NO',EMP.DEPT_NO);

            ini.IniWriteValue(SECNAME,'JOB_CODE',EMP.JOB_CODE);

            ini.IniWriteValue(SECNAME,'JOB_COUNTRY',EMP.JOB_COUNTRY);

            ini.IniWriteValue(SECNAME,'FULL_NAME',EMP.FULL_NAME);

            ini.IniWriteValue(SECNAME,'INI_ID',i.ToString);

 

            EMPLIST:=EMPLIST+SECNAME+','; i:=i+1;

        

        end;

 

      ini.IniWriteValue('GENERAL','EMPLIST',EMPLIST);

      ini.IniWriteValue('GENERAL','DATE',DateTime.Now.ToLongDateString);

      ini.IniWriteValue('GENERAL','COUNT',(i+1).ToString);

 

   end;

  end;

   System.Console.ReadLine;

end;

constructor TIniFile(const INIPath:string);

begin

   path:=INIPath;

end;

 

procedure TIniFile.IniWriteValue(const Section,Key,Value:String);

begin

  WritePrivateProfileString(Section,Key,Value,self.path);

end;

 

function TIniFile.IniReadValue(const Section,Key:String): String;

begin

   var temp:=new StringBuilder(255);

   var i:=GetPrivateProfileString(Section,Key,'',temp,

                                    255, self.path);

    exit temp.ToString;

end;

end.

 

 

Несколько вариантов примера создания класса TIniFile (или аналогичного) на языке С# можно легко найти в Сети. Но суть их одна – используется обращение к системной библиотеке, что нарушает негласное табу на выход за «матрицу». И здесь встает вопрос практичности (оправданности) использования небезопасного кода. Однако данную проблему мы сейчас обсуждать не будем.

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

Стоит отметить, что программа конвертации наших данных в формат INI успешно запустилась в Windows XP и для .NET, и для Mono.

Часто перед программистом встает задача хранения данных на диске или в оперативной памяти в такой структуре данных, которая была бы эффективна для последующей выборочной обработки -- поиска, сортировки и т.д. Традиционно для ее решения использовались хешированные базы данных, т.е. организовывался доступ к файлам (данные на диске или в оперативной памяти), представляющим собой хеш-таблицы. У такой таблицы есть двоичный ключ, гарантирующий необходимый доступ к требующейся части информации в области бинарных данных. Иначе говоря, это словари (в программной среде .NET они реализуют интерфейс Idictionary), у которых есть свойство Item, возвращающее нужное значение по заданному ключу. По этому принципу можно выстроить целую СУБД.

Хеш-таблица (Hashtable) в .NET предназначена для быстрого доступа к необходимым данным, уже хранящимся в оперативной памяти. Но эту таблицу все равно можно сериализовать, т.е. записать на диск в бинарном или XML-формате.



Листинг 7. Использование хеш-таблицы для сериализации данных

namespace Ex7;

interface

uses

 System.Runtime.InteropServices,System.Text,

 System.IO,

 System.Collections,Ex4,

 System.Runtime.Serialization,

 System.Runtime.Serialization.Formatters.Binary;

type

  ConsoleApp = class

  public

    class method Main;

  end;

implementation

class method ConsoleApp.Main;

var hashTable:=new Hashtable();

begin

//Десериализуем данные объекта класса ArrayList

//записываем в хеш-таблицу и опять делаем сериализацию

 

  using filestream:=new FileStream('C:EMPLOYEE.dat',FileMode.Open) do begin

   using EMPS:=

       (new BinaryFormatter()).Deserialize(filestream) as ArrayList do begin

 

 var arr:=EMPS.ToArray;

 

      var i:=0;

        for each EMPOBJ in arr do hashTable.Add(

           (EMPOBJ as Ex4.TEMPLOYEE).EMP_NO.ToString,EMPOBJ as Ex4.TEMPLOYEE

                                               );

   end;

  end;

  

//Убеждаемся , что хеш-таблица создана -- пробный вызов объекта

System.Console.WriteLine(

(hashTable.Item[12.ToString] as Ex4.TEMPLOYEE).FULL_NAME

);

 

//Сериализуем хеш-таблицу

    using fileStream:=new FileStream('C:EMPLOYEE_ht.dat',FileMode.Create) do begin

     (new BinaryFormatter()).Serialize(fileStream,hashTable);

    end;

 

   System.Console.ReadLine;

end;

end.

[КОНЕЦ ФРАГМЕНТА ТОЛЬКО ДЛЯ САЙТА]

В статье мы тактично обошли вопрос, связанный с особенностями работы с бинарными файлами произвольного формата c использованием класса FileStream, не связанного с сериализацией. Также не рассматривалась похожая тема работы с текстовыми файлами, имеющими поля фиксированной длины, но не имеющими разделителей.

Оба этих вопроса лежат глубже рассматриваемой темы, поскольку связаны с другими технологиями платформы .NET, выходящими далеко за рамки данной статьи.



Литература

1.Строки форматирования в .NET, www.jenyay.net, www.realcoding.net

2. Keith Rimington. Basic File IO and the DataGridView, www.codeproject.com/

3. Nitin Kunte. Get System Info using C# ,www.codeproject.com/

4. Stephan Depoorter. Handling Fixed-width Flat Files with .NET Custom Attributes, www.codeproject.com/

5. Jeff Brand. Converting CSV Data to Objects, www.codeproject.com/

6. Oleg Axenow. Работа со строками, www.gotdotnet.ru



7. Savage, Read and Write Structures to Files with .NET, www.codeproject.com/

8. Сергей Иванов, Работа с кодировкой DOS, www.realcoding.net

9. Jan Schreuder. Using OleDb to Import Text Files (tab, CSV, custom), www.codeproject.com/

10. Jisu74. Read a certain line in a text file, www.codeproject.com/

11. Dreamzor, Getting File Info, www.codeproject.com

12. How to copy a String into a struct using C#, www.codeproject.com

13. Arun GG, www.csharphelp.com ,TextReader and TextWriter In C# 

14. BLaZiNiX, An INI file handling class using C#, www.codeproject.com

  1. C# 2008 и платформа .NET 3.5 для профессионалов. Christian Nagel, Bill Evjen, Jay Glynn, Karli Watson, Morgan Skinner. Диалектика