Основные метода класса BinaryReader
Close(): закрывает поток и освобождает ресурсы.
ReadBoolean(): считывает значение bool и перемещает указатель на один байт.
ReadByte(): считывает один байт и перемещает указатель на один байт.
ReadChar(): считывает значение char, то есть один символ, и перемещает указатель на столько байтов, сколько занимает символ в текущей кодировке.
ReadDecimal(): считывает значение decimal и перемещает указатель на 16 байт.
ReadDouble(): считывает значение double и перемещает указатель на 8 байт.
ReadInt16(): считывает значение short и перемещает указатель на 2 байта.
ReadInt32(): считывает значение int и перемещает указатель на 4 байта.
ReadInt64(): считывает значение long и перемещает указатель на 8 байт.
ReadSingle(): считывает значение float и перемещает указатель на 4 байта.
ReadString(): считывает значение string.
Каждая строка предваряется значением длины строки, которое представляет 7-битное целое число.
С чтением бинарных данных все просто: соответствующий метод считывает данные определенного типа и перемещает указатель на размер этого типа в байтах, например, значение типа int занимает 4 байта, поэтому BinaryReader считает 4 байта и переместит указать на эти 4 байта.
Посмотрим на реальной задаче применение этих классов. Попробуем с их помощью записывать и считывать из файла массив структур:
struct State
{
public string name;
public string capital;
public int area;
public double people;
public State(string n, string c, int a, double p)
{
name = n;
capital = c;
people = p;
area = a;
}
}
class Program
{
static void Main(string[] args)
{
State[] states = new State[2];
states[0] = new State("Германия", "Берлин", 357168, 80.8);
states[1] = new State("Франция", "Париж", 640679, 64.7);
string path= @"C:\SomeDir\states.dat";
try
{
// создаем объект BinaryWriter
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.OpenOrCreate)))
{
// записываем в файл значение каждого поля структуры
foreach (State s in states)
{
writer.Write(s.name);
writer.Write(s.capital);
writer.Write(s.area);
writer.Write(s.people);
}
}
// создаем объект BinaryReader
using (BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)))
{
// пока не достигнут конец файла
// считываем каждое значение из файла
while (reader.PeekChar() > -1)
{
string name = reader.ReadString();
string capital = reader.ReadString();
int area = reader.ReadInt32();
double population = reader.ReadDouble();
Console.WriteLine("Страна: {0} столица: {1} площадь {2} кв. км численность населения: {3} млн. чел.",
name, capital, area, population);
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();
}
}
Есть структура State с некоторым набором полей. В основной программе создаем массив структур и записываем с помощью BinaryWriter. Этот класс в качестве параметра в конструкторе принимает объект Stream, который создается вызовом File.Open(path, FileMode.OpenOrCreate).
Затем в цикле пробегаемся по массиву структур и записываем каждое поле структуры в поток. В том порядке, в каком эти значения полей записываются, в том порядке они и будут размещаться в файле.
Затем считываем из записанного файла. Конструктор класса BinaryReader также в качестве параметра принимает объект потока, только в данном случае устанавливаем в качестве режима FileMode.Open: new BinaryReader(File.Open(path, FileMode.Open))
В цикле while считываем данные. Чтобы узнать окончание потока, вызываем метод PeekChar(). Этот метод считывает следующий символ и возвращает его числовое представление. Если символ отсутствует, то метод возвращает -1, что будет означать, что мы достигли конца файла.
В цикле последовательно считываем значения поле структур в том же порядке, в каком они записывались.
Таким образом, классы BinaryWriter и BinaryReader очень удобны для работы с бинарными файлами, особенно когда нам известна структура этих файлов. В то же время для хранения и считывания более комплексных объектов, например, объектов классов, лучше подходит другое решение - сериализация.
Сборка мусора
При использовании ссылочных типов, например, объектов классов, для них также будет отводиться место в стеке, только там будет храниться не значение, а адрес на участок памяти в хипе или куче, в котором уже и буду находиться сами значения данного объекта. И если объект класса перестает использоваться, то при очистке стека ссылка на участок памяти также очищается, однако это не приводит к немедленной очистке самого участка памяти в куче. Впоследствии сборщик мусора (garbage collector) увидит, что на данный участок памяти больше нет ссылок, и очистит его.
Например:
class Program
{
static void Main(string[] args)
{
Test();
}
private static void Test()
{
Country country = new Country();
country.x = 10;
country.y = 15;
}
}
class Country
{
public int x;
public int y;
}
В методе Test создается объект Country. С помощью оператора new в куче для хранения объекта CRL выделяет участок памяти. А в стек добавляет адрес на этот участок памяти. В главном методе Main мы вызываем метод Test. И после того, как Test отработает, место в стеке очищается, а сборщик мусора очищает ранее выделенный под хранение объекта country участок памяти.
Сборщик мусора не запускается сразу после удаления из стека ссылки на объект, размещенный в куче. Он запускается в то время, когда среда CLR обнаружит в этом потребность, например, когда программе требуется дополнительная память.
Как правило, объекты в куче располагаются неупорядочено, между ними могут иметься пустоты. Куча довольно сильно фрагментирована. Поэтому после очистки памяти в результате очередной сборки мусора оставшиеся объекты перемещаются в один непрерывный блок памяти. Вместе с этим происходит обновление ссылок, чтобы они правильно указывали на новые адреса объектов.
Так же надо отметить, что для крупных объектов существует своя куча - Large Object Heap. В эту кучу помещаются объекта, размер которых больше 85 000 байт. Особенность этой кучи состоит в том, что при сборке мусора сжатие памяти не проводится по причине больших издержек, связанных с размером объектов.
Несмотря на то что, на сжатие занятого пространства требуется время, да и приложение не сможет продолжать работу, пока не отработает сборщик мусора, однако благодаря подобному подходу также происходит оптимизация приложения. Теперь чтобы найти свободное место в куче среде CLR не надо искать островки пустого пространства среди занятых блоков. Ей достаточно обратиться к указателю кучи, который указывает на свободный участок памяти, что уменьшает количество обращений к памяти.
Кроме того, чтобы снизить издержки от работы сборщика мусора, все объекты в куче разделяются по поколениям. Всего существует три поколения объектов: 0, 1 и 2-е.
К поколению 0 относятся новые объекты, которые еще ни разу не подвергались сборке мусора. К поколению 1 относятся объекты, которые пережили одну сборку, а к поколению 2 - объекты, прошедшие более одной сборки мусора.
Когда сборщик мусора приступает к работе, он сначала анализирует объекты из поколению 0. Те объекты, которые остаются актуальными после очистки, повышаются до поколения 1.
Если после обработки объектов поколения 0 все еще необходима дополнительная память, то сборщик мусора приступает к объектам из поколения 1. Те объекты, на которые уже нет ссылок, уничтожаются, а те, которые по-прежнему актуальны, повышаются до поколения 2.
Поскольку объекты из поколения 0 являются более молодыми и нередко находятся в адресном пространстве памяти рядом друг с другом, то их удаление проходит с наименьшими издержками.
Класс System.GC
Функционал сборщика мусора в библиотеке классов .NET представляет класс System.GC. Через статические методы данный класс позволяет обращаться к сборщику мусора. Как правило, надобность в применении этого класса отсутствует. Наиболее распространенным случаем его использования является сборка мусора при работе с неуправляемыми ресурсами, при интенсивном выделении больших объемов памяти, при которых необходимо такое же быстрое их освобождение.
Рассмотрим некоторые методы и свойства класса System.GC:
Метод AddMemoryPressure информирует среду CLR о выделении большого объема неуправляемой памяти, которую надо учесть при планировании сборки мусора. В связке с этим методом используется метод RemoveMemoryPressure, который указывает CLR, что ранее выделенная память освобождена, и ее не надо учитывать при сборке мусора.
Метод Collect приводит в действие механизм сборки мусора. Перегруженные версии метода позволяют указать поколение объектов, вплоть до которого надо произвести сборку мусора
Метод GetGeneration(Object) позволяет определить номер поколения, к которому относится переданый в качестве параметра объект
Метод GetTotalMemory возвращает объем памяти в байтах, которое занято в управляемой куче
Метод WaitForPendingFinalizers приостанавливает работу текущего потока до освобождения всех объектов, для которых производится сборка мусора
Работать с методами System.GC очень просто:
// .................................
long totalMemory = GC.GetTotalMemory(false);
GC.Collect();
GC.WaitForPendingFinalizers();
//......................................
С помощью перегруженных версий метода GC.Collect можно выполнить более точную настройку сборки мусора. Так, его перегруженная версия принимает в качестве параметра число - номер поколения, вплоть до которого надо выполнить очистку.
Например, GC.Collect(0) - удаляются только объекты поколения 0.
Еще одна перегруженная версия принимает еще и второй параметр - перечисление GCCollectionMode. Это перечисление может принимать три значения:
Default: значение по умолчанию для данного перечисления (Forced)
Forced: вызывает немедленное выполнение сборки мусора
Optimized: позволяет сборщику мусора определить, является ли текущий момент оптимальным для сборки мусора
Например, немедленная сборка мусора вплоть до первого поколения объектов:
GC.Collect(1, GCCollectionMode.Forced);