
Объектно-ориентированное программирование.-6
.pdf
Пример:
StreamWriter sw = new StreamWriter("TextFile.txt");
CultureInfo ukr = new CultureInfo("uk-UA");
DateTime dt = DateTime.Today;
StreamReader sr;
String str;
str = "Дата:";
Console.WriteLine("В файл записана строка: " + str); sw.WriteLine(str);
str = dt.ToString("dd MMMM yyyy", ukr); Console.WriteLine("В файл записана строка: " + str); sw.WriteLine(str);
sw.Close();
Console.WriteLine();
sr = new StreamReader("TextFile.txt");
Console.WriteLine("Размер файла: {0} байт", sr.BaseStream.Length);
while ((str = sr.ReadLine()) != null)
{
Console.WriteLine("Из файла считана строка: " + str);
}
sr.Close();
3.5.2.2. Чтение и запись двоичного файла
Аналогично, используем наиболее простой способ работы с бинарными файлами – функциональность классов IO.BinaryWriter и IO.BinaryReader.
Пример:
FileStream fs = new FileStream("BinFile.bin", FileMode.Create); BinaryWriter bw = new BinaryWriter(fs);
BinaryReader br;
Random rnd = new Random(); int num;
for (int i = 0; i < 10; i++)
{
num = rnd.Next(-100, 100);
Console.WriteLine("В файл записано число: " + num); bw.Write(num);
}
Console.WriteLine();
bw.Close();
fs.Close();
fs = new FileStream("BinFile.bin", FileMode.Open, FileAccess.Read); br = new BinaryReader(fs);
Console.WriteLine("Размер файла: {0} байт", fs.Length);
while (fs.Position < fs.Length)
{
num = br.ReadInt32();
Console.WriteLine("Из файла считано число: " + num);
221

}
br.Close();
fs.Close();
3.5.3. Управление ресурсами потока
Потоки при работе с ними выделяют ресурсы. Например, файловые потоки организуют в оперативной памяти буферы. Дело в том, что операции ввода-вывода на физический носитель очень затратные по времени (по сравнению со временем доступа к оперативной памяти, ресурсам процессора и т.д.). Если бы при посимвольной записи текстового файла при каждой операции символ записывался в файл на жестком диске, это происходило бы очень медленно. Чтение происходит несколько быстрее, но все равно недостаточно. Поэтому при записи данные фактически пишутся в некий буфер в ОЗУ, и только при его заполнении попадают на физический носитель. Аналогично при чтении – данные читаются из буфера, а в него считываются с физического носителя блоками. Когда все данные в буфере прочитаны, загружается новый блок. Поэтому так важно закрывать файл по окончании работы (метод Close), особенно при записи – иначе некоторые данные могут остаться в буфере и не попасть в файл.
Другие виды потоков также выделяют различные ресурсы. После закрытия потока они не уничтожаются немедленно. Задача удаления этих ресурсов, как и любых данных в управляемой динамической куче, на которые не осталось ссылок, лежит на сборщике мусора. Но предсказать, когда именно он выполнит свою работу невозможно.
Между тем, представим ситуацию, что только что закончили работу с файлом большого объема. Ресурсы, выделенные потоку, больше не нужны, но еще некоторое неопределенное время будут недоступны для повторного выделения. Либо имеем ситуацию, когда ресурсов в системе и так мало. Если мы уверены, что файл нам больше не нужен, желательно его закрыть и сразу же освободить все занимаемые им ресурсы (которые при большом объеме файла могут быть весьма существенными).
Такая проблема появляется не только при работе с файлами, но и с любыми другими ресурсоемкими задачами. Для ее решения компилятор языка C# позволяет при помощи оператора using управлять выделением и освобождением ресурсов. Его синтаксис был приведен в п. 3.4.1.1:
222

<оператор выделения ресурсов> :: using (<оператор объявления>) <внедряемый оператор>
Обязательное условие – тип данных в операторе объявления должен реализовывать интерфейс System.IDisposable, а все переменные должны быть инициализированы. Все рассмотренные нами в п. 3.5.2 классы реализуют этот интерфейс. Использование оператора using гарантирует, что ресурсы будут выделены только на время выполнения внедренного оператора, а по его окончанию будут освобождены.
Пример:
CultureInfo es = new CultureInfo("es-ES");
DateTime dt = DateTime.Today;
String str;
using (StreamWriter sw = new StreamWriter("TextFile.txt"))
{
str = "Дата:";
Console.WriteLine("В файл записана строка: " + str); sw.WriteLine(str);
str = dt.ToString("dd MMMM yyyy", es); Console.WriteLine("В файл записана строка: " + str); sw.WriteLine(str);
Console.WriteLine();
}
using (StreamReader sr = new StreamReader("TextFile.txt"))
{
Console.WriteLine("Размер файла: {0} байт", sr.BaseStream.Length);
while ((str = sr.ReadLine()) != null)
{
Console.WriteLine("Из файла считана строка: " + str);
}
}
Здесь переменные sw и sr определены только внутри блоков using. Закрывать потоки методом Close не обязательно, метод Dispose, наследуемый классами StreamWriter и StreamReader от интерфейса IDisposable, сделает это автоматически.
Пример: Samples\3.5\3_5_3_using.
Дополнительные сведения об интерфейсах см. в § 4.9.
3.5.4. Сохранение и загрузка состояния приложения
Часто возникает ситуация, когда состояние отдельных объектов или свойств приложения необходимо восстановить при его следующем запуске. Другими словами, если пользователь закончил сеанс работы с приложением,
223

при следующем запуске приложение должно восстановить свое состояние (размер и положение окон, настройки интерфейса и т.п.), чтобы пользователь начал работу ровно с того места, на котором закончил ее во время предыдущего сеанса.
Изначально для этой цели использовались конфигурационные файлы (обычно с расширением .CFG или .INI). В этих файлах настройки приложения хранились в текстовом виде, примерно следующим образом:
[название_секции_1] название_ключа_1 = значение_1 название_ключа_2 = значение_2
...
[название_секции_2]
...
Минус такого подхода состоит в том, что трудно гарантировать целостность данных, хранящихся в текстовом виде. Кроме того, обработка текстовых данных более затратная по времени, чем обработка двоичных данных. Для упорядочения этой информации в ОС Windows 3.1 появился реестр – иерархическая база данных параметров и настроек самой ОС Windows, а также установленных приложений и компонентов.
В .NET имеются специальные средства для работы с реестром, в частности, класс System.Microsoft.Win32.Registry. Он позволяет создавать и удалять секции (ветки) и ключи в реестре Windows, читать и записывать значения ключей и т.д.
Однако, слишком большое число записей в реестре приводит к увеличению его размера, а значит, к увеличению потребления ресурсов и временных затрат при обращении к реестру. Поэтому в настоящее время политика
.NET сводится к тому, чтобы не записывать настройки приложений в реестр, если в этом нет необходимости. Вместо этого используются такие механиз-
мы, как параметры приложений и сериализация.
3.5.4.1. Параметры приложения
Итак, параметры приложения – это рекомендованный механизм сохранения настроек для восстановления их при следующих запусках приложения в среде .NET.
Для того, чтобы добавить параметры приложения к проекту, необходимо выбрать пункт меню «Проект» → «Свойства…», и далее – в окне свойств
224

проекта – вкладку «Параметры». Если приложение еще не имеет параметров, то будет отображена только гиперссылка, при нажатии на которую к проекту будут добавлены три файла – «app.config», «Properties\Settings.settings» и «Properties\Settings.Designer.cs».
Файл «Settings.settings» является XML-файлом с описанием параметров. Среда разработки позволяет редактировать его в удобном виде (рис. 3.15). Для каждого параметра можно ввести его имя, тип, область (пользователя или приложения) и значение по умолчанию.
Рис. 3.15 – Редактирование параметров приложения
На рис. 3.15 видно, что к проекту добавлены два параметра – UserParam в области пользователя и AppParam в области приложения. Оба параметра строковые, а значения по умолчанию – это адреса некоторых сайтов. Содержимое файла «Settings.settings» при этом будет следующим:
<?xml version='1.0' encoding='utf-8'?> <SettingsFile
xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="ParamsSample.Properties" GeneratedClassName="Settings">
<Profiles /> <Settings>
<Setting Name="UserParam" Type="System.String" Scope="User"> <Value Profile="(Default)">ya.ru</Value>
</Setting>
<Setting Name="AppParam" Type="System.String" Scope="Application"> <Value Profile="(Default)">fdo.tusur.ru</Value>
</Setting>
</Settings>
</SettingsFile>
Файл «Settings.Designer.cs» описывает новый класс приложения, предназначенный для доступа к параметрам. Рассмотрим кратко его основные
225

конструкции:
...
namespace ParamsSample.Properties {
...
internal sealed partial class Settings :
global::System.Configuration.ApplicationSettingsBase {
...
public static Settings Default...
...
public string UserParam...
...
public string AppParam...
}
}
Итак, в проекте создается новое пространство имен «пространство имен по умолчанию.Properties», в котором описан класс Settings. Не рекомендуется ручное редактирование этого класса. Вместо этого лучше воспользоваться редактором среды разработки (рис. 3.15). Там же задается модификатор доступа этого класса (в данном случае – internal). Класс Settings всегда содержит статическое свойство Default. Это экземпляр класса с настройками по умолчанию. В принципе, в приложении можно организовать подборку из нескольких вариантов настроек, и предоставить пользователю возможность выбирать любой из них. Далее описываются свойства, соответствующие ранее созданным параметрам приложения.
Файл «app.config» содержит описание всех параметров приложения. Два рассмотренных выше файла предназначены только для разработчика, а XML-файл «app.config» распространяется вместе с исполняемым файлом приложения. Из него в процессе выполнения приложение получает всю информацию о своих параметрах – имена, типы, значения по умолчанию.
Отличие параметров области приложения от параметров области пользователя в том, что первые доступны только для чтения (это видно даже по коду в файле «Settings.Designer.cs»), в то время как последние можно модифицировать во время работы приложения. После модификации параметры пользователя сохраняются в XML-файле «user.config». Он располагается в папке
<профиль пользователя>\Local Settings\Application Data\<название компании>\<имя исполняемого файла><суффикс>\<версия приложения>\
для ОС Windows XP или
профиль_пользователя\AppData\Local\<название компании>\<имя исполняемого файла><суффикс>\<версия приложения>\
226

для ОС Windows Vista/7. Название компании берется из настроек ОС, а версия – из информации о сборке (файл «Properties\AssemblyInfo.cs»). Учитывая, что в системе могут быть установлены несколько программ с одинаковыми именами исполняемых файлов, при образовании имени папки к имени исполняемого файла добавляется суффикс, состоящий из строки «_URL_» и дальнейшего набора символов английского алфавита и цифр. Это гарантирует уникальность имени папки с параметрами для каждого приложения.
Рассмотрим пример работы с описанными выше параметрами:
using System;
using System.Diagnostics; using System.IO;
using ParamsSample.Properties;
namespace ParamsSample
{
class Program
{
static int Main()
{
ConsoleKeyInfo action;
string site = Settings.Default.AppParam;
do
{
Console.Clear();
Console.WriteLine("1. Выполнить команду ping для сайта по умолчанию [{0}]", Settings.Default.AppParam);
Console.WriteLine("2. Выполнить команду ping для сайта [{0}]", Settings.Default.UserParam);
Console.WriteLine("3. Изменить адрес сайта"); Console.WriteLine("4. Выход");
do
{
action = Console.ReadKey(true);
} while (action.KeyChar < '1' || action.KeyChar
> '4');
Console.WriteLine(action.KeyChar);
switch (action.KeyChar)
{
case '1':
site = Settings.Default.AppParam; break;
case '2':
site = Settings.Default.UserParam; break;
case '3': Console.WriteLine();
Console.WriteLine("Введите адрес:
");
227

Settings.Default.UserParam =
Console.ReadLine();
break;
}
if (action.KeyChar == '1' || action.KeyChar ==
'2')
{
ProcessStartInfo psi = new ProcessStartInfo(Environment.GetFolderPath(Environment.SpecialFolder.System) + "\\ping.exe", site);
psi.WindowStyle =
ProcessWindowStyle.Hidden;
psi.RedirectStandardOutput = true; psi.UseShellExecute = false; psi.CreateNoWindow = true;
Console.WriteLine("Команда
обрабатывается...");
Process proc = Process.Start(psi); StreamReader sr = proc.StandardOutput; Console.WriteLine(sr.ReadToEnd()); proc.WaitForExit(); Console.ReadKey(true);
}
} while (action.KeyChar != '4');
Properties.Settings.Default.Save(); return 0;
}
}
}
Сначала можно подключить пространство имен, в котором описан класс Settings (чтобы не нужно было каждый раз писать Properties.Settings). Для доступа к параметрам используем конструкцию «Settings.Default.<имя параметра>». Если имя соответствует параметру области пользователя, то такое свойство доступно как для чтения, так и для записи. Параметры области приложения, как уже было сказано, доступны только для чтения. При завершении работы приложения сохраняем параметры вызовом метода Save, который наследуется классом Settings от базового абстрактного класса ApplicationSettingsBase. Другие методы этого класса можно посмотреть в справочной системе.
Данный пример позволяет проверить доступ к сайтам при помощи команды «ping». Адрес одного из сайтов (fdo.tusur.ru) задан как параметр области приложения, поэтому не подлежит модификации. Адрес второго сайта описан в области пользователя, поэтому его можно изменить.
Пример: Samples\3.5\3_5_4_params.
228

3.5.4.2.Сериализация
Втерминах .NET сериализация – это процесс преобразования экземпляров объектов в поток байтов (т.е. файловый поток вывода). Соответственно, обратный процесс называется десериализацией – это преобразование потока байтов (файлового потока ввода) в экземпляры объектов. Потоки вводавывода могут быть бинарными, а могут иметь формат XML. Структура XML-
файла описывается протоколом SOAP (Simple Object Access Protocol).
Ранее мы говорили о недостатках текстового формата хранения данных. По сравнению с ним, формат XML имеет преимущества – целостность данных в нем обеспечить проще (т.к. имеется определенная иерархическая теговая структура), а за счет оптимизированных средств чтения и записи XML файлов достигается достаточная скорость обработки. Обработка двоичных данных в этом плане еще более надежна и быстра, но теряется наглядность представления данных – в текстовом редакторе изучить их уже не получится. Конечный выбор осуществляет программист на этапе проектирования программной системы.
Для сериализации и десериализации в двоичном формате используется класс System.Runtime.Serialization.Formatters.Binary.BinaryFormatter, а в формате SOAP – System.Runtime.Serialization.Formatters.Soap.SoapFormatter. Есть одна особенность: класс BinaryFormatter содержится в сборке «mscorlib», которая подключается к любому проекту автоматически, а класс SoapFormatter
является членом сборки System.Runtime.Serialization.Formatters.Soap, кото-
рую нужно вручную подключить к проекту (подробнее о работе со сборками говорится в пункте 4.1.3). Оба класса содержат два наиболее часто используемых метода:
void Serialize(Stream serializationStream, object graph); object Deserialize(Stream serializationStream);
Первый метод позволяет осуществлять сериализацию объекта, массива (а также коллекции) объектов или даже графа объектов. Под графом объектов подразумевается набор взаимосвязанных между собой при помощи ссылок объектов. Второй метод осуществляет десериализацию объектов. Т.к. результат возвращается в виде ссылки на объект типа object, требуется явное преобразование к нужному типу.
Чтобы объект мог участвовать в сериализации, его необходимо пометить атрибутом [Serializable]. В этом случае в сериализации также может
229

участвовать массив или коллекция из таких объектов. Если же какие-либо члены данного объекта не должны участвовать в сериализации, они помечаются атрибутом [NonSerialized] (больше сведений об атрибутах можно найти в § 5.4).
Пример:
using System;
using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization.Formatters.Soap; using System.IO;
namespace SerializeSample
{
class Program
{
[Serializable] struct Person
{
[NonSerialized] public static int Count = 0; public string Name;
public int Age;
public Address Address;
public Person(string name, int age, Address address)
{
Name = name; Age = age;
Address = address; Count++;
}
}
[Serializable] struct Address
{
public string Country; public string City; public int Index;
public Address(string country, string city, int index)
{
Country = country; City = city;
Index = index;
}
}
static void Out(Person[] a)
{
Console.WriteLine("Количество записей: {0}",
Person.Count);
for (int i = 0; i < a.Length; i++)
{
Console.WriteLine("Name[{0}] = {1}", i,
a[i].Name);
Console.WriteLine("Age[{0}] = {1}", i,
a[i].Age);
Console.WriteLine("Address[{0}] = {1}, {2}, {3}", i, a[i].Address.Country,
230