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 предназначен специально для записи текстового файла. При этом мы почти до предела упростили для себя работу, используя конструкцию using … do begin … end; Она заменяет конструкцию
try finally if (_Object is Idisposable) then (_Object as Disposable).Dispose(); end;
где _Object -- условное обозначение создаваемого объекта. Таким образом, программист частично снимает с себя ответственность за жизненный цикл созданного объекта независимо от модели его памяти. Напоследок упомянем FileInfo -- еще один волшебный класс, сопоставимый по мощности с классом File. Оба они предназначены для решения одних и тех же задач, но для FileInfo нужно создавать экземпляр класса.
StreamWriter и StreamReader -- классы, специально созданные для работы с текстом. Конечно, желательно, чтобы вы были уверены, что будете работать именно с текстом, в противном случае рекомендуется выбрать класс FileStream. Классы StreamWriter и StreamReader освобождают программиста от забот, связанных с кодировкой. Поразительно, насколько все кажется в .NET взаимозаменяемым и универсальным!
Результат работы первого примера в вариантах .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
C# 2008 и платформа .NET 3.5 для профессионалов. Christian Nagel, Bill Evjen, Jay Glynn, Karli Watson, Morgan Skinner. Диалектика