Типы пространства имен System.Collections.Generics
Имя типа
Аналог из
System.Collections
Описание
Collection<T>
CollectionBase
Произвольная коллекция
Comparer<T>
Comparer
Стандартизированный способ сравнения двух объектов
Dictionary<K, V>
Hashtable
Таблица пар «ключ-значение»
List<T>
ArrayList
Динамический массив
Queue<T>
Queue
Очередь
SortedDictionary<K,V>
SortedList
Отсортированное множество пар «ключ-значение»
Stack<T>
Stack
Стек
LinkedList<T>
Двусвязный список
ReadOnlyCollection<T>
ReadOnlyCollectionBase
Коллекция с доступом только для чтения
В System.Collectons.Generics определены обобщенные версии интерфейсов из System.Collectons:
· ICollection<T>
· IComparer<T>
· IDictionary<K, V>
· IEnumerable<T>
· IEnumerator<T>
· IList<T>
Описываемое пространство имен содержит несколько вспомогательных классов и структур. Так, тип LinkedListNode<T> представляет отдельный узел в списке LinkedList<T>, исключение KeyNotFoundException возникает при попытке обращения к элементу по несуществующему ключу, и так далее.
Далее представлено описание одного из обобщенных классов-коллекций – List<T>.
public class List<T> : IList<T>, ICollection<T>,
IEnumerable<T>, IList, ICollection,
IEnumerable
{
. . .
public void Add(T item);
public IList<T> AsReadOnly();
public int BinarySearch(T item);
public bool Contains(T item);
public void CopyTo(T[] array);
public int FindIndex(System.Predicate<T> match);
public T FindLast(System.Predicate<T> match);
public bool Remove(T item);
public int RemoveAll(System.Predicate<T> match);
public T[] ToArray();
public bool TrueForAll(System.Predicate<T> match);
public T this[int index] { get; set; }
}
Классы коллекций, предоставляемые .NET Framework, подходят для решения большинства типичных задач, встречающихся при написании приложений. Если же стандартных классов не достаточно, программист может воспользоваться сторонними библиотеками или разработать собственный класс для некой структуры данных.
2.4. РАБОТА С ФАЙЛАМИ И ДИРЕКТОРИЯМИ
Пространство имен System.IO содержит четыре класса, предназначенные для работы с физическими файлами и каталогами на диске. Классы Directory и File выполняют операции в файловой системе при помощи статических членов, классы DirectoryInfo и FileInfo обладают схожими возможности, однако для работы требуется создании соответствующих объектов.
Рассмотрим работу с классами DirectoryInfo и FileInfo. Данные классы являются наследниками абстрактного класса FileSystemInfo. Этот класс содержит следующие основные элементы, перечисленные в таблице 14.
Таблица 14
Элементы класса FileSystemInfo
Имя элемента
Описание
Attributes
Свойство позволяет получить или установить атрибуты объекта файловой системы (тип – перечисление FileAttributes)
CreationTime
Свойство для чтения или установки времени создания объекта файловой системы
Exists
Свойство для чтения, проверка существования объекта файловой системы
Extension
Свойство для чтения, расширение файла
FullName
Свойство для чтения, полное имя объекта файловой системы
LastAccessTime
Свойство обеспечивает чтение или установку времени последнего доступа для объекта файловой системы
LastWriteTime
Свойство обеспечивает чтение или установку времени последней записи для объекта файловой системы
Name
Свойство для чтения, которое возвращает имя файла или каталога
Delete()
Метод удаляет объект файловой системы
Refresh()
Метод обновляет информацию об объекте файловой системы
Конструктор класса DirectoryInfo принимает в качестве параметра строку с именем того каталога, с которым будет производиться работа. Для указания текущего каталога используется точка (строка "."). При попытке работать с данными несуществующего каталога, генерируется исключительная ситуация. Работу с методами и свойствами класса DirectoryInfo продемонстрируем в следующем примере:
using System;
using System.IO;
class MainClass {
public static void Main() {
// Создали объект для директории
DirectoryInfo dir = new DirectoryInfo(@"C:\Temp\D");
Console.WriteLine("Press any key to delete file");
Console.Read();
// Закрываем поток, удаляем файл
fs.Close();
file.Delete();
}
}
Для работы с файлом можно использовать метод FileInfo.Open(), который обладает большим числом возможностей, чем метод Create(). Рассмотрим перегруженную версию метода Open(), которая содержит три параметра. Первый параметр определяет режим запроса на открытие файла. Для него используются значения из перечисления FileMode:
· Append – Открывает файл, если он существует, и ищет конец файла. Если файл не существует, то он создается. Этот режим может использоваться только с доступом FileAccess.Write;
· Create – Указывает на создание нового файла. Если файл существует, он будет перезаписан;
· CreateNew – Указывает на создание нового файла. Если файл существует, генерирует исключение IOException;
· Open – Операционная система должна открыть существующий файл;
· OpenOrCreate – Операционная система должна открыть существующий файл или создать новый, если файл не существует;
· Truncate – Система должна открыть существующий файл и обрезать его до нулевой длины.
Второй параметр метода Open() определяет тип доступа к файлу как к потоку байтов. Для него используются элементы перечисления FileAccess:
· Read – Файл будет открыт только для чтения;
· ReadWrite – Файл будет открыт и для чтения, и для записи;
· Write – Файл открывается только для записи, то есть добавления данных.
Третий параметр определяет возможность совместного доступа к открытому файлу и представлен значениями перечисления FileShare:
· None – Совместное использование запрещено, на любой запрос на открытие файла будет возвращено сообщение об ошибке;
· Read – Файл могут открыть и другие пользователи, но только для чтения;
· ReadWrite – Другие пользователи могут открыть файл для чтения и записи;
· Write – Файл может быть открыт другими пользователями для записи.
Вот пример кода, использующего метод Open():
// Файл создается (или открывается) для чтения и записи,
// без возможности совместного использования
FileInfo file = new FileInfo(@"C:\Test.txt");
FileStream fs = file.Open(FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None);
2.5. ИСПОЛЬЗОВАНИЕ ПОТОКОВ ДАННЫХ
Для поддержки операций, связанных с вводом и выводом информации, библиотека классов платформы .NET предоставляет пространство имен System.IO. Основное понятие, связанное с элементами данного пространства имен, – это поток. Поток – абстрактное представление данных в виде последовательности байт. Потоки (в отличие от файлов) могут быть ассоциированы с файлами на диске, памятью, сетью. В пространстве имен System.IO поток представлен абстрактным классом Stream. От данного абстрактного класса порождены классы System.IO.FileStream (работа с файлами как с потоками), System.IO.MemoryStream (поток в памяти), System.Net.Sockets.NetworkStream (работа с сокетами как с потоками), System.Security.Cryptography.CryptoStream (потоки зашифрованных данных).
Рассмотрим основные методы и свойства класса Stream. Свойства для чтения CanRead, CanWrite и CanSeek определяют, поддерживает ли поток чтение, запись и поиск. Если поток поддерживает поиск, перемещаться по потоку можно при помощи метода Seek(). На текущую позицию в потоке указывает свойство Position (нумерация с нуля). Свойство Length возвращает длину потока, которая может быть установлена при помощи метода SetLength(). Методы Read() и ReadByte(), Write() и WriteByte() служат для чтения и записи блока байт или одиночного байта. Метод Flush() записывает данные из буфера в связанный с потоком источник данных. При помощи метода Close() поток закрывается и все связанные с ним ресурсы освобождаются.
Класс Stream вводит поддержку асинхронного ввода/вывода. Для этого служат методы BeginRead() и BeginWrite(). Уведомление о завершении асинхронной операции возможно двумя способами: или при помощи делегата тип System.AsyncCallback, передаваемого как параметр методов BeginRead() и BeginWrite(), или при помощи вызова методов EndRead() и EndWrite(), которые приостанавливают текущий поток управления до окончания асинхронной операции.
Использование методов и свойств класса Stream продемонстрируем в фрагменте кода с классом FileStream. Объект класса FileStream возвращается некоторыми методами классов FileInfo и File, кроме этого, данный объект можно создать при помощи конструктора с параметрами, включающими имя файла и режимы доступа к файлу.
// Создаем файл test.dat в текущем каталоге
FileStream fs = new FileStream("test.dat",
FileMode.OpenOrCreate,
FileAccess.ReadWrite);
// В цикле записываем туда 100 байт
for (byte i = 0; i < 100; i++) {
fs.WriteByte(i);
}
// Мы можем записывать информацию из массива байт
byte[] info = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// Первый параметр – массив, второй – смещение в массиве,
// третий – количество записываемых байт
fs.Write(info, 2, 4);
// Возвращаемся на начало файла
fs.Position = 0;
// Читаем все байты и выводим на экран
while (fs.Position <= fs.Length - 1) {
Console.Write(fs.ReadByte());
}
// Закрываем поток (и файл), освобождая ресурсы
fs.Close();
Класс MemoryStream предоставляет возможность организовать поток в оперативной памяти. Свойство Capacity этого класса позволяет получить или установить количество байтов, выделенных под поток. Метод ToArray() записывает все содержимое потока в массив байт. Метод WriteTo() переносит содержимое потока в памяти в другой поток, производный от класса Stream.
Классы-потоки представляют поток как последовательность неформатированных байт. Однако в большинстве приложений удобнее читать и записывать в поток данные примитивных типов или строк. Библиотека классов .NET Framework содержит набор парных классов вида XXXReader/XXXWriter, которые инакапсулируют поток и предоставляют к нему высокоуровневый доступ.
Классы BinaryReader и BinaryWriter позволяют при помощи своих методов читать и записывать в поток данные примитивных типов, строк и массивов байт или символов. Вся информация записывается в поток как последовательность байт. Рассмотрим работу с данными классами на следующем примере. Пусть имеется класс, который хранит информацию о студенте:
class Student {
public string Name;
public int Age;
public double MeanScore;
}
Методы, которые осуществляют запись и чтение объекта этого класса в поток в виде последовательности байт, могут иметь следующий вид:
void SaveToStream(Stream stm, Student s) {
// Конструктор класса позволяет "обернуть"
// BinaryWriter вокруг потока
BinaryWriter bw = new BinaryWriter(stm);
// BinaryWriter содержит 18 перегруженных версий
// метода Write()
bw.Write(s.Name);
bw.Write(s.Age);
bw.Write(s.MeanScore);
// Убеждаемся, что буфер BinaryWriter пуст
bw.Flush();
}
void ReadFromStream(Stream stm, Student s) {
BinaryReader br = new BinaryReader(stm);
// Для чтения каждого примитивного типа есть свой метод
s.Name = br.ReadString();
s.Age = br.ReadInt32();
s.MeanScore = br.ReadDouble();
}
Абстрактные классы TextReader и TextWriter позволяю читать и записывать данные в поток как последовательность символов. От этих классов наследуются классы StreamReader и StreamWriter. Перепишем методы для сохранения данных класса Student с использованием StreamReader и StreamWriter:
void SaveToStream(Stream stm, Student s) {
StreamWriter sw = new StreamWriter(stm);
// Запись напоминает вывод на консоль
sw.WriteLine(s.Name);
sw.WriteLine(s.Age);
sw.WriteLine(s.MeanScore);
sw.Flush();
}
void ReadFromStream(Stream stm, Student s) {
StreamReader sr = new StreamReader(stm);
// Читаем данные как строки, требуется их преобразовать
s.Name = sr.ReadLine();
s.Age = Int32.Parse(sr.ReadLine());
s.MeanScore = Double.Parse(sr.ReadLine());
}
Классы StringReader и StringWriter – это наследники классов TextReader и TextWriter, которые представляют доступ к строке или к объекту класса StringBuilder как к потоку. Это может оказаться полезным, если текстовая информация добавляется в специальный буфер в оперативной информации. Работу с данными классами иллюстрирует следующий пример:
StringWriter sw = new StringWriter();
// Пишем информацию в поток
sw.WriteLine("Hello!");
sw.WriteLine("This is an example...");
sw.Close();
// Выводим все информацию
Console.WriteLine(sw.ToString());
string s = "Big\n Big string\n 10";
// Создаем StringReader на основе строки
StringReader sr = new StringReader(s);
// Последовательно читаем "кусочки" строки
string input;
while((input = sr.ReadLine()) != null)
Console.WriteLine(input);
sr.Close();
2.6. СЕРИАЛИЗАЦИЯ
Под сериализацией понимается действие, при котором данные объекта в памяти переносятся в байтовый поток для сохранения или передачи. Десериализация – это обратное действие, заключающееся в восстановлении состояния объекта по данным из байтового потока. При выполнении сериализации следует учитывать несколько нетривиальных моментов, например: сохранение полей объекта некоторого класса требует сохранения всех данных базовых классов; если объект содержит ссылки на другие объекты, то требуется сохранить данные всех объектов, на которые имеются ссылки.
Среда .NET Framework обладает развитым механизмом поддержки сериализации, включая поддержку сериализации в различных форматах. Основные классы, связанные с сериализацией, размещены в наборе пространств имен вида System.Runtime.Serialization.* и System.Xml.Serialization.*. Так, пространство имен System.Runtime.Serialization.Formatters.Binary обеспечивает поддержку сериализации в двоичном формате. Класс BinaryFormatter из этого пространства имен способен выполнить сериализацию графа объектов в поток при помощи метода Serialize() и десериализацию при помощи метода Deserialize().
Рассмотрим сериализацию на примере. Пусть имеется класс с информацией о студенте (имя, возраст, средний балл), а также класс с информацией о группе (список студентов и средний балл группы). Эти классы могут быть описаны следующим образом:
class Student {
public string Name;
public int Age;
public double MeanScore;
public Student(string Name, int Age, double MeanScore) {
this.Name = Name;
this.Age = Age;
this.MeanScore = MeanScore;
}
}
class Group {
public ArrayList GL = new ArrayList();
public double MSG;
public Student BestStudent;
public double CalcMSG() {
double sum = 0;
foreach(Student s in GL)
sum += s.MeanScore;
MSG = sum / GL.Count;
return MSG;
}
public Student FindTheBest() {
BestStudent = (Student)GL[0];
foreach(Student s in GL)
if (s.MeanScore > BestStudent.MeanScore)
BestStudent = s;
return BestStudent;
}
}
Допустим, что планируется осуществлять сериализацию объектов класса Group. Чтобы реализовать сериализацию пользовательского типа, он должен быть помечен специальным атрибутом – [Serializable]. Кроме этого, все поля такого типа также должны иметь этот атрибут. Данное замечание актуально в том случае, если поле имеет пользовательский тип, так как встроенные типы и большинство стандартных классов уже помечены как [Serializable]. В нашем случае мы должны добавить атрибут сериализации к классу Group и к классу Student. Сериализация некоторых полей может не иметь смысла (например, эти поля вычисляются при работе с объектом или хранят конфиденциальные данные). Для таких полей можно применить атрибут [NonSerialized]. Изменим код нашего примера с учетом вышесказанного:
[Serializable]
class Student {
. . .
}
[Serializable]
class Group {
public ArrayList GL = new ArrayList();
[NonSerialized] // Не надо сохранять – просто посчитаем
public double MSG;
. . .
}
Теперь все готово для выполнения сериализации. Метод Serialize() класса BinaryFormatter получает два параметра: поток, в который требуется выполнить сериализацию, и объект, который требуется сериализовать. Вот фрагмент кода, сериализующего объект класса Group, а затем выполняющего десериализацию:
// Нам понадобятся следующие пространства имен
using System;
using System.Collections;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
. . .
// Создаем группу и добавляем в нее несколько студентов
Group g = new Group();
g.GL.Add(new Student("Vova", 20, 4.5));
g.GL.Add(new Student("Ira", 20, 5));
g.GL.Add(new Student("Peter", 19, 4));
// Выводим некоторую информацию о группе
Console.WriteLine(g.CalcMSG());
Console.WriteLine(g.FindTheBest().Name);
// Создаем поток – это будет файл
Stream fs = new FileStream("data.dat", FileMode.Create);
// Создаем объект для сериализации в двоичном формате
BinaryFormatter fmt = new BinaryFormatter();
// Сериализуем и затем закрываем поток
fmt.Serialize(fs, g);
fs.Close();
// Теперь десериализация. Создаем поток
fs = new FileStream("data.dat", FileMode.Open);
// Десериализация. Обратите внимание на приведение типов
Group d = (Group)fmt.Deserialize(fs);
// Выводим информацию о группе
Console.WriteLine(d.CalcMSG());
Console.WriteLine(d.FindTheBest().Name);
Метод Deserialize() получает в качестве параметра поток, из которого десериализуется объект и возвращает объект типа object. В одном потоке можно сериализовать несколько различных объектов – главное, чтобы последовательность десериализации соответствовала сериализации.
Десериализацию удобно представлять как своеобразный вызов конструктора, так как результатом десериализации является ссылка на существующий объект. Однако если некоторые поля класса были помечены как [NonSerialized], то возможно после десериализации потребуется просчитать значения данных полей. Допустимое решение – реализовать в классе интерфейс IDeserializationCallback из пространства имен System.Runtime.Serialization. Данный интерфейс содержит единственный метод – OnDeserialization, который вызывается исполняемой средой автоматически после десериализации объекта. Используем интерфейс для класса Group:
[Serializable]
class Group : IDeserializationCallback {
public ArrayList GL = new ArrayList();
// Не будем сохранять средний балл и лучшего студента
[NonSerialized]
public double MSG;
[NonSerialized]
public Student BestStudent;
public double CalcMSG() { . . . }
public Student FindTheBest() { . . . }
// После десериализации просчитаем средний балл и
// найдем лучшего студента. Работа с параметром метода
// исполняемой средой на данный момент не поддерживается!
public void OnDeserialization(object o) {
CalcMSG();
FindTheBest();
}
}
Рассмотрим несколько примеров сериализации в различных форматах. Класс SoapFormatter из пространства имен System.Runtime.Serialization.Formatters.Soap обеспечивает сериализацию объекта в формате протокола SOAP (для использования данного пространства имен требуется подключить библиотеку system.runtime.serialization.formatters.soap.dll). Изменения в коде примера минимальны:
using System.Runtime.Serialization.Formatters.Soap;
. . .
// Создаем объект для сериализации в формате SOAP
SoapFormatter fmt = new SoapFormatter();
. . .
Файл в формате SOAP – это xml-файл с дополнительной информацией протокола SOAP. Мы можем выполнить сериализацию в формате «чистого» XML. Данный функционал обеспечивает класс XmlSerializer из пространства имен System.Xml.Serialization (файл System.Xml.dll). Прежде чем представить пример работы с классом XmlSerializer, сделаем следующие замечания. XmlSerializer игнорирует атрибуты [Serializable] и [NonSerialized]. Однако можно использовать атрибут [XmlIgnore], действие которого аналогично [NonSerialized]. Класс, который будет сериализоваться с помощью XmlSerializer, должен иметь конструктор без параметров и являться public-классом. XmlSerializer не может обратиться к private-данным класса, для таких данных при необходимости должны быть определены public-свойства.
Внесем изменения в классы Student и Group:
// Класс объявлен с модификатором public
// Убрали атрибут [Serializable] (можно было и не убирать)
public class Student {
. . . // Основная часть осталась старой
// Добавили конструктор без параметров
public Student() { ; }
}
// Класс объявлен с модификатором public и без [Serializable]
public class Group {
// Для поля MSG используем атрибут [XmlIgnore]
[XmlIgnore]
public double MSG;
. . . // Основная часть осталась старой
// Добавили конструктор без параметров.
// В нем можно пересчитывать несохраняемые поля
public Group() { ; }
}
При создании объекта класса XmlSerializer указывается тип того объекта, который будет сериализоваться. Сериализация и десериализация выполняется ожидаемым образом:
// Необходимо использовать следующее пространство имен
using System.Xml.Serialization;
. . .
Stream fs = new FileStream("data.dat", FileMode.Create);
// Указываем тип того объекта, который сериализуем
XmlSerializer x = new XmlSerializer(typeof(Group));
// Сериализуем
x.Serialize(fs, g);
fs.Close();
// Восстанавливаем
fs = new FileStream("data.dat", FileMode.Open);
Group d = (Group) x.Deserialize(fs);
fs.Close();
2.7. СЕРИАЛИЗАЦИЯ ОБЪЕКТОВ В НЕСТАНДАРТНОМ ФОРМАТЕ
Если программиста не устраивают существующие форматы сериализации или способ организации потока сериализованных данных, он может осуществить сериализацию в собственном формате. Классы, помеченные атрибутом [Serializable], могут дополнительно реализовывать интерфейс ISerializable. Это позволяет «вклиниться» в процесс сериализации и выполнить любые действия, связанные с формированием данных для сохранения.
Интерфейс ISerializable устроен просто:
public interface ISerializable {
void GetObjectData(SerializationInfo info,
StreamingContext context);
}
Метод GetObjectData() вызывается форматером автоматически при выполнении сериализации. Реализация данного метода подразумевает заполнение объекта SerializationInfo набором данных вида «ключ-значение», которые (обычно) соответствуют полям сохраняемого объекта. Класс SerializationInfo содержит несколько перегруженных версий метода AddValue(), а также свойства для указания имени типа, имени сборки и т.п. Фрагмент определения класса SerializationInfo приведен ниже:
public sealed class SerializationInfo {
public SerializationInfo(Type type,
IFormatterConverter converter);
public string AssemblyName { get; set; }
public string FullTypeName { get; set; }
public int MemberCount { get; }
public void AddValue(string name, short value);
public void AddValue(string name, UInt16 value);
public void AddValue(string name, int value);
. . .
}
Если тип реализовывает интерфейс ISerializable, то он должен также содержать специальный конструктор:
[Serializable]
class SomeClass : ISerializable {
// Конструктор с такой сигнатурой необходим,
// чтобы CLR смогла восстановить состояние объекта
private SomeClass(SerializationInfo si,
StreamingContext ctx) { . . . }
. . .
}
Обратите внимание: конструктор объявлен с модификатором private. Первый параметр конструктора – это объект класса SerializationInfo. Второй параметр имеет тип StreamingContext. Наиболее часто используемым элементом данного класса является свойство State из перечисления StreamingContextStates.