Основные метода класса 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);


Наши рекомендации