воскресенье, 16 ноября 2008 г.

Хранилище записей фиксированной длины (часть III)

Хранилище записей фиксированной длины Хранилище записей фиксированной длины (часть II) Извиняюсь, что затянул с продолжением, совершенно не было времени и сил, особенно после праздников. Напомню, что в предыдущих постах была описана реализация хранилища записей фиксированной длины, выполненного в качестве обертки над Stream-ом с прямым доступом к полям записей. В этот раз опишу как прикрутить к нему простой индекс.
Предварительно хочу сделать акцент на том, что я не пытался создать библиотеку для решения общего случая, а решал конкретную задачу, но как можно более общим образом. В той конкретной задаче, которую я решал, достаточно индекса по одному полю, который бы искал записи по точному совпадению ключа. Уот таким я увидел интерфейс индекса хранилища:
public interface IStorageIndex<TKey>
{
    void Initialize(IEnumerable<TKey> keys, Func<int, TKey> readKeyFunc);

    int GetIndex(TKey key);

    void InsertIndex(TKey key, int rowIndex);
}
Метод инициализации индекса принимает последовательность всех ключей (забегая вперед отмечу, что ленивую последовательность), и метод получения ключа по заданному индексу. Полагаю, что такой метод инициализации должен удовлетворить некоторое кол-во реализаций индексов (строящихся в памяти, или на диске). Метод GetIndex - собственно то, ради чего и задумывался индекс. Он должен вернуть индекс записи с указанным ключем. Метод InsertIndex добавляет в индекс информацию о положении новой записи. Простоты ради я избавился от метода удаления записи из индекса. Простейшая реализация индекса выглядит так (комментировать не буду):
public class DictionaryIndex<TKey> : IStorageIndex<TKey>
{
    private readonly Dictionary<TKey, int> _indices = new Dictionary<TKey, int>();

    public void Initialize(IEnumerable<TKey> keys, Func<int, TKey> readKeyFunc)
    {
        int index = 0;
        foreach(TKey key in keys)
        {
            _indices[key] = index++;
        }
    }

    public int GetIndex(TKey key)
    {
        int result;
        return _indices.TryGetValue(key, out result)
            ? result
            : -1;
    }

    public void InsertIndex(TKey key, int rowIndex)
    {
        _indices.Add(key, rowIndex);
    }
}
Кстати, именно подобный индекс основанный на словаре успешно трудится уже около двух месяцев в некоторых заведениях ;) страны. Теперь опишу базовый класс индексированного хранилища записей. Для индесированного по одной колонке хранилища нам потребуется еще один generic параметр TKey, который будет представлять тип колонки, по которой проводится индексирование. Конструктор индексированного хранилища принимает дополнительные параметры - колонку, по которой проводится индексирование и реализацию индекса:
public class IndexedStorage<THeader, TRow, TKey> : StreamStorage<THeader, TRow>
    where THeader : RowBase<THeader>
    where TRow : RowBase<TRow>
{
    private readonly Column<TKey> _keyColumn;
    private readonly IStorageIndex<TKey> _index;

    protected IndexedStorage(Stream stream, Encoding encoding, Column<TKey> keyColumn, IStorageIndex<TKey> index)
        : base(stream, encoding)
    {
        if(index == null)
        {
            throw new ArgumentNullException("index");
        }
        if(keyColumn == null)
        {
            throw new ArgumentNullException("keyColumn");
        }
   
        if(keyColumn.Columns != RowBase<TRow>.ColumnCollection)
        {
            throw new ArgumentException();
        }
        _keyColumn = keyColumn;
        _index = index;
    }
    ...
}
Методы, потребующиеся для инициализации индекса (чтение ключа записи по указанному номеру и чтение последовательности ключей всех записей)
private TKey ReadKey(int index)
{
    return ReadValue(_keyColumn, index);
}

private IEnumerable<TKey> LoadKeys()
{
    int length = Length;

    using (var notOwnedStream = new NotOwnedStreamProxy(Stream))
    using (var bufferedStream = new BufferedStream(notOwnedStream))
    using (var reader = new BinaryReader(bufferedStream))
    {                   
        byte[] buffer =
            new byte[RowBase<TRow>.ColumnCollection.Size - _keyColumn.Size];
         bufferedStream.Position = RowBase<THeader>.ColumnCollection.Size + _keyColumn.Offset;

        for (int i = 0; i < length; i++)
        {
            yield return _keyColumn.ReadValue(reader);
         
            if (i < length - 1)
            {
                reader.ReadBytes(buffer.Length);
            }
        }
    }
}
второй метод потребует комментариев. Дело в том, что я его изуродовал, пытаясь выжать производительность. Самая простая его реализация - вызов в цикле вышеописанного метода ReadKey(int index) безобразно долго работает. Дело в том, что при таком подходе потребуется большое (по числу записей) количество операций Seek. Хоть изменения позиции и небольшие, но сама операция на FileStream (а это основной сценарий использования этого хранилища) все равно занимает значительное время. Вместо операции Seek я решил использовать чтение в буфер, размер которого равен размеру записи без ключевого поля. Т.е. прочитав первый ключ и вчитав в этот буфер дальнейшее содержимое, позиция потока оказывается ровно перед ключем следующего поля. Еще одна оптимизация - использование буферизованного потока дает небольшое преимущество перед чтением напрямую. Небольшое, но я решил им все же воспользоваться. Потому потребовался другой экземпляр BinaryReader. Вся эта оптимизация привела к следующей проблеме: при освобождении BufferedStream, равно как и BinaryReader-а для чтения из буферизованного стрима, освобождается несущий стрим хранилища. Решений может быть несколько: постараться не освобождать эти сущности (хранить их в полях класса), либо оградить несущий стрим от деструктивных воздействий вспомогательных сущностей. Я предпочел второе, хоть и пришлось много постучать по кнопкам: пришлось реализовать хитрый proxy для класса Stream, который делегирует все методы и свойства низлежащему стриму, за исключением метода Close(). Этот класс я худобедно назвал NotOwnedStreamProxy. Полные его исходники приводить не буду, ограничусь выбранными наугад методами (остальные выглядят похожим образом):
public override void Close()
{
    // BaseStream.Close(); ВСЕ РАДИ ТОГО, ЧТОБЫ НЕ ДОПУСТИТЬ ВЫЗОВ ЭТОГО!!!
    base.Close();
}

public override int EndRead(IAsyncResult asyncResult)
{
    return BaseStream.EndRead(asyncResult);
}

public override void EndWrite(IAsyncResult asyncResult)
{
    BaseStream.EndWrite(asyncResult);
}

public override int ReadByte()
{
    return BaseStream.ReadByte();
}
И не говорите! Сам не в восторге. Метод инициализации базового индексированного хранилища теперь выглядит так:
protected override void Initialize()
{
    lock (SyncRoot)
    {
        base.Initialize();

        _index.Initialize(LoadKeys(), ReadKey);
    }
}
Еще пара нехитрых методов базового индексированного хранилища:
protected int GetIndex(TKey key)
{
    lock(SyncRoot)
    {
        return _index.GetIndex(key);
    }
}

protected TRow InsertRow(TKey key)
{
    int newRowIndex;
    lock(SyncRoot)
    {
        newRowIndex = Length;

        _index.InsertIndex(key, newRowIndex);

        Length += 1;
        WriteValue(_keyColumn, newRowIndex, key);
    }

    return GetRow(newRowIndex);
}
Осталось чуть-чуть. Уже добрались до конкретного индексированного хранилища:
class IndexedStorage : IndexedStorage<TestHeader, TestRow, Guid>
{
    public IndexedStorage(Stream stream)
        : base(
            stream,
            Encoding.Default,
            TestRow.IdColumn,
            new DictionaryIndex<Guid>())
    {
        base.Initialize();
    }

    public TestRow AddNewRow(Guid id)
    {
        return InsertRow(id);
    }

    public TestRow GetRow(Guid id)
    {
        return GetRow(GetIndex(id));
    }
}
Класс TestRow описан в конце первого поста о хранилище записей.
В следующий раз приведу результаты стресс-тестирования этого безобразия в сравнении с SQLite.

вторник, 4 ноября 2008 г.

Хранилище записей фиксированной длины (часть II)

В предыдущем посте (Хранилище записей фиксированной длины) было описано все необходимое для класса хранилища. Теперь можно приступать к реализации хранилища.

Напомню: хранилище - надстройка над классом Stream для хранения типизированных записей фиксированной длины и некого заголовка. Класс хранилища параметризован двумя generic параметрами: типом заголовка хранилища и типом записи хранилища:
public class StreamStorage<THeader, TRow> : StreamStorage, IDisposable
    where THeader : RowBase<THeader>
    where TRow : RowBase<TRow>
{
    private readonly object _syncRoot = new object();
    private readonly Stream _stream;
    private readonly BinaryWriter _writer;
    private readonly BinaryReader _reader;
    private int _length;

    protected StreamStorage(
        Stream stream,
        Encoding encoding)
    {
        _stream = stream;
        encoding = encoding ?? Encoding.Default;

        if (stream.CanWrite)
        {
            _writer = new BinaryWriter(_stream, encoding);
        }
        if (stream.CanRead)
        {
            _reader = new BinaryReader(_stream, encoding);
        }
    }

    public void Dispose()
    {
        if (_writer != null)
        {
            _writer.Close();
        }
        if(_reader != null)
        {
            _reader.Close();
        }
        _stream.Close();
    }
Конструктор хранилища принимает поток данных, в котором хрянятся записи и кодировку для записи строковых полей (реализацию строковых колонок я пока не приводил). Метод Dispose() освобождает ресурсы. Следом короткая форма доступа к экземплярам коллекций колонок заголовка и записи хранилища:
static ColumnCollection HeaderColumns
    {
        get { return RowBase<THeader>.ColumnCollection; }
    }

    static ColumnCollection RowColumns
    {
        get { return RowBase<TRow>.ColumnCollection; }
    }
Затем доступ к ридеру, райтеру и собственно потоку данных:
protected BinaryWriter Writer
    {
        get
        {
            if (_writer == null)
            {
                throw new NotSupportedException();
            }
            return _writer;
        }
    }

    protected BinaryReader Reader
    {
        get
        {
            if (_reader == null)
            {
                throw new NotSupportedException();
            }
            return _reader;
        }
    }

    protected Stream Stream
    {
        get { return _stream; }
    }

    protected object SyncRoot { get { return _syncRoot; } }
Далее серия методов, производящих строки. Есть небольшие проблемы в том каким образом создавать экземпляры TRow и THeader, ведь экземпляры строк требуют экземпляр хранилища и свой индекс при создании, т.е. мы не можем воспользоваться их конструктором по умолчанию.

Можно было параметризовать хранилище фабрикой строк и фабрикой заголовка, но я пришел к решению предусмотреть расширение через Template Method.
protected virtual TRow CreateRowInstance(int index)
    {
        return CreateRowInstance<TRow>(this, index);
    }

    protected virtual THeader CreateHeaderInstance()
    {
        return CreateRowInstance<THeader>(this, 0);
    }

    private static T CreateRowInstance<T>(StreamStorage storage, int index)
        where T : RowBase<T>
    {
        return (T)Activator.CreateInstance(typeof(T), storage, index);
    }

    protected TRow GetRow(int index)
    {
        lock (SyncRoot)
        {
            return CreateRowInstance(index);
        }
    }

    public THeader Header { get; private set; }
Такое решение позволит без дополнительных усилий создавать экземпляры строк, которые принимают в конструкторе экземпляр хранилища и индекс. Пример строки TestRow из предыдущего поста принимает именно такой набор параметров. Если потребуется изменить сигнатуру конструктора записи, то производный класс хранилища сможет перекрыть методы CreateRowInstance и CreateHeaderInstance.

Далее способ управления размером хранилища:
protected int Length
    {
        get
        {
            return _length;
        }
        set
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException("value");
            }

            long streamLength = HeaderColumns.Size + RowColumns.Size * value;

            lock (SyncRoot)
            {
                Stream.SetLength(streamLength);

                _length = value;
            }
        }
    }
Реализация свойства Length практически не нуждается в комментариях. Она устанавливает длину потока данных руководствуясь размером заголовка, размером записи и требуемым кол-вом записей. Метод установки позиции потока для чтения или записи значения:
private void SetPosition(int rowIndex, Column column)
    {
        if (column == null)
        {
            throw new ArgumentNullException("column");
        }

        int pos;
        if (ReferenceEquals(column.Columns, HeaderColumns))
        {
            pos = column.Offset;
        }
        else
        {
            if (ReferenceEquals(column.Columns, RowColumns))
            {
                if (rowIndex < 0 || rowIndex >= Length)
                {
                    throw new ArgumentOutOfRangeException("rowIndex");
                }
                pos = HeaderColumns.Size + RowColumns.Size * rowIndex + column.Offset;
            }
            else
            {
                throw new ArgumentException("", "column");
            }
        }

        if (Stream.Position != pos)
        {
            Stream.Position = pos;
        }
    }
Написано много, но смысл простой: если колонка относится к заголовку, то установить позицию потока к смещению колонки. Если колонка относится к типу записи - установить позицию потока руководствуясь размером заголовка, размером записи, индексом записи и смещением колонки.

Следует заметить, что этот метод не будет работать адекватно при идентичных типах TRow и THeader. Проверку на такое совпадение я опустил. Если потребуется сделать что-то подобное, то лучше указать тип заголовка без колонок.

Теперь сердце хранилища - методы чтения и записи значений:
public override T ReadValue<T>(Column<T> column, int rowIndex)
    {
        if (column == null)
        {
            throw new ArgumentNullException("column");
        }
        lock (SyncRoot)
        {
            SetPosition(rowIndex, column);
            return column.ReadValue(Reader);
        }
    }

    public override void WriteValue<T>(Column<T> column, int rowIndex, T value)
    {
        if (column == null)
        {
            throw new ArgumentNullException("column");
        }
        lock (SyncRoot)
        {
            SetPosition(rowIndex, column);
            column.WriteValue(Writer, value);
        }
    }
Осталось всего ничего - код инициализации хранилища:
protected virtual void Initialize()
    {
        Header = CreateHeaderInstance();

        if (RowColumns.Size == 0)
        {
            CreateRowInstance(0); // вызвать инициализацию колонок записи.
        }

        int headerSize = HeaderColumns.Size;
        if (Stream.Length == 0)
        {
            Stream.SetLength(headerSize);
            Header.Initialize();
        }
        else
        {
            _length = (int)((Stream.Length - headerSize) / RowColumns.Size);

            if (headerSize + Length * RowColumns.Size != Stream.Length)
            {
                throw new InvalidOperationException("Unexpected stream length.");
            }
        }
    }
Сам по себе этот код не сложен, но тащит за собой одну проблему: кто-то его должен вызывать. Но это не может быть конструктор хранилища, потому как метод инициализации использует виртуальные методы. В отличие от C++, в C# виртуальные методы могут быть вызваны из конструктора базового класса, но существует опасность что перекрытые методы могут использовать поля производного класса, которые еще не будут проинициализированы. Потому из конструктора StreamStorage этот метод я вызывать не стал. Не нравится мне так же решение с отложенной инициализацией, где пришлось бы чуть ли не во всех методах вставлять проверку на наличие инициализации. Отказ от Template Method в пользу параметризации хранилища фабриками строк тоже не идеальное решение. Ведь одно из очевидных решений - реализация фабрик в классе произвольного хранилища. Но тогда будут сложности с передачей таких фабрик через конструктор класса StreamStorage.

 Хорошим решением этой проблемы стало бы решение сделать класс StreamStorage не расширяемым (sealed), опубликовать методы GetRow(int index), свойство Length и при необходимости расширения использовать композицию... Но, при реализации индексированного хранилища потребуется доступ к BinaryReader, BinaryWriter, Stream-у и даже к корню синхронизации. Не хотелось бы делать эти вещи общедоступными, потому было принято решение расширять хранилище через наследование (вариант понапихать всю необходимую функциональность в один класс кажется мне скверным).

Итого обязанность вызова метода Initialize назначается на классы, расширяющие StreamStorage. Резюме: в итоге этого поста завершена реализация хранилища записей с фиксированной длинной, с доступом к записям по индексу, с потокобезопасным доступом к полям записей. Для того, чтобы воспользоваться хранилищем, нужно определить два типа записей (один для заголовка) примерно как TestRow в предыдущем посте, определить свой класс хранилища, унаследовавшись от StreamStorage<THeader, TRow> и предоставить доступ к методам GetRow(int index) и способу управления размером хранилища.

При необходимости производное хранилище может реализовать интерфейс списка (IList), организовать кэширование записей и вести себя как полноценная коллекция. Напомню, что методы ReadValue и WriteValue у хранилища чисто вспомогательные, что они нужны для базового класса RowBase, и что обращение к полям по задумке должно происходить через свойства класса записи, например как в TestRow из предыдущего поста. Впрочем, методы ReadValue и WriteValue не мешают, а даже немного дополняют функциональность хранилища возможностью получать доступ к значениям не создавая экземпляры записей, потому я не стал предпринимать попыток скрыть эти методы.

В следующем посте расскажу о способах индексации этого хранилища.

четверг, 30 октября 2008 г.

Хранилище записей фиксированной длины

Хранилище записей фиксированной длины (часть II) Случилось вдруг создать такой велосипед, который хранил бы в файле записи фиксированной длины и позволял бы ими оперировать.  Почему бы не воспользоваться базой данных? На самом деле этот велосипед должен предоставлять локальный кэш одной из таблиц центральной БД в системе, в разработке которой я принимаю участие. Требования к нему специфические:
  • производительность, настроенная на чтение-запись отдельных полей записей;
  • доступ только по индексу записи, либо по её ключу (никаких запросов и выборок);
  • отказоустойчивость может быть принесена в жертву (это лишь кэш);
  • размеры файла могут достигать нескольких гигобайт (записи маленькие, но их мно-о-о-ого);
  • доступ к записям производится в случайном порядке (т.е. любое кэширование мало что даст);
Впрочем, это все оправдания. Мне же хотелось показать идею и её реализацию, а точнее прототип кода, адаптированный к блогу.
Организацию хранилища записей я позаимствовал у классов ADO. Таблица - набор записей, у таблицы есть набор колонок, доступ к значению поля записи определен на пересечении таблицы, колонки и записи (точнее ее индекса в таблице).
Отличия от класса DataTable в способе хранения данных. Данные хранятся в физическом потоке (Stream) и доступ к ним осуществляется через классы BinaryReader/BinaryWriter.
Начну, пожалуй, с реализации базового класса нетипизированной колонки. Будут и типизированные, но нетипизированные нужны для складывания их в коллекцию колонок:
public class Column
   {
       private readonly int _offset;
       private readonly int _size;
       private readonly ColumnCollection _columns;

       protected Column(ColumnCollection columns, int offset, int size)
       {
           if (columns == null)
           {
               throw new ArgumentNullException("columns");
           }
           _columns = columns;
           _offset = offset;
           _size = size;
       }

       public int Offset { get { return _offset; } }

       public int Size { get { return _size; } }

       public ColumnCollection Columns { get { return _columns; } }
   }
Здесь все просто. Каждая колонка знает размер значения в байтах, свою коллекцию колонок и смещение относительно начала записи. Все это запоминается в констуркторе колонки. Далее класс типизированной колонки. Типизированная колонка абстрактная, т.к. читать и писать данные придется с помощью BinaryReader/BinaryWriter-а, а они не имеют generic методов для чтения/записи значений.
public abstract class Column<T> : Column
    {
        protected Column(ColumnCollection columns, int offset, int size)
            : base(columns, offset, size)
        {
        }

        public abstract T ReadValue(BinaryReader reader);
        public abstract void WriteValue(BinaryWriter writer, T value);
    }
Типичный класс колонки:
class Int32Column : Column<int>
    {
        public Int32Column(ColumnCollection columns, int offset)
            : base(columns, offset, 4)
        {
        }

        public override int ReadValue(BinaryReader reader)
        {
            return reader.ReadInt32();
        }

        public override void WriteValue(BinaryWriter writer, int value)
        {
            writer.Write(value);
        }
    }
Таких классов несколько, не буду приводить код каждого из них, все они подобны. Немного отличается только класс строковой колонки, но о ней возможно позже. Обратите внимание, что класс типизированной колонки скрытый! Это объясняется тем, что я не собираюсь предоставлять возможность создания колонок сложных типов. Ограничусь лишь некоторыми примитивными. Класс коллекции колонок содержит в себе список колонок и число байт, требуемое для записи данных во всех колонках.
public class ColumnCollection
    {
        private readonly List<Column> _columns = new List<Column>();
        private int _size;

        public Column<int> AddInt32Column()
        {
            return AddInt32Column(this.Size);
        }

        public Column<int> AddInt32Column(int offset)
        {
            return AddColumn(new Int32Column(this, offset));
        }

        private Column<T> AddColumn<T>(Column<T> column)
        {
            _size = Math.Max(_size, column.Offset + column.Size);
            _columns.Add(column);
            return column;
        }

        public int Size { get { return _size; } }
    }
Коллекция колонок является так же производящим классом для типизированных колонок. Для каждого типа колонки (а их у меня чуть больше, чем только Int32Column) определено два метода создания экземпляра колонки: первый метод добавляет колонку в конец записи (указывает Offset, равный текущему размеру коллекции колонок в байтах), а второй - позволяет указать Offset вручную. Это потребуется для так называемых union полей.

Вот что из себя представляет базовый тип хранилища:
public abstract class StreamStorage
    {
        public abstract T ReadValue<T>(Column<T> column, int rowIndex);
        public abstract void WriteValue<T>(Column<T> column, int rowIndex, T value);
    }
Всего лишь два абстрактных метода, позволяющий писать и читать значения, соответствующие указанным колонкам и индексу записи. Этот базовый тип нужен для объявления базового типа записи. Хотел я параметризовать тип записи типом хранилища и тип хранилища типом записи, и возможно смог бы это сделать, но меня посетила идея, что у файла должен быть заголовок. И что заголовок - это второй тип записи. Т.е. хранилище надо параметризовывать двумя типами записи, но тогда каждую запись надо будет параметризовывать типом хранилища, в котором 2 типа записи... Но это все лишнее. Записи от хранилища не нужно ничего, кроме возможности обратиться к вышеуказанным методам. Потому базовый тип хранилища не имеет generic параметров. А базовый тип записи имеет generic параметр - конкретный тип записи:
public class RowBase<TRow>
        where TRow : RowBase<TRow>
    {
        private static readonly ColumnCollection s_columns = new ColumnCollection();
        public static ColumnCollection ColumnCollection { get { return s_columns; } }

        private readonly int _index;
        private readonly StreamStorage _storage;

        protected RowBase(StreamStorage storage, int index)
        {
            _storage = storage;
            _index = index;
        }

        public virtual void Initialize()
        {
        }

        public int GetIndex()
        {
            return _index;
        }

        public StreamStorage GetStorage()
        {
            return _storage;
        }

        protected T ReadValue<T>(Column<T> column)
        {
            return _storage.ReadValue(column, GetIndex());
        }

        protected void WriteValue<T>(Column<T> column, T value)
        {
            _storage.WriteValue(column, GetIndex(), value);
        }
    }
Базовый класс записи со спецификацией типа (т.е. все записи одного типа) привязаны к одному экземпляру коллекции колонок. Сделано это лишь для проверок. Для работоспособности хранилища интересен лишь размер записи, который определен в коллекции колонок и смещения самих колонок относительно записи. Но хранилище так же будет знать экземпляр коллекции колонок для проверки экземпляров колонок, которые приходят на публичные методы чтения и записи значений. Базовый класс записи содержит в себе ссылку на экземпляр хранилища, свой индекс, и определяет защищенные методы для чтения и записи значений.

Надеюсь, что код конкретной записи прояснит все что не ясно:
public class TestRow : RowBase<TestRow>
    {
        private static readonly Column<Guid> s_GuidColumn = ColumnCollection.AddGuidColumn();
        private static readonly Column<int> s_Int32Column = ColumnCollection.AddInt32Column();

        public TestRow(StreamStorage storage, int index)
            : base(storage, index)
        {
        }

        public Guid GuidValue
        {
            get { return ReadValue(s_GuidColumn); }
            set { WriteValue(s_GuidColumn , value); }
        }

        public int Int32Value
        {
            get { return ReadValue(s_Int32Column); }
            set { WriteValue(s_Int32Column, value); }
        }

        public override void Initialize()
        {
            base.Initialize();
            GuidValue = Guid.NewGuid();
            Int32Value = 0;
        }
    }
Итак, тип конкретной записи определяет колонки (статические поля), которые создает обращаясь к статической коллекции колонок, определенной у базового типа записи. Тип конкретной записи определяет свойства экземпляра записи. Реализация каждого свойства обращается к базовым методам чтения/записи значения и передает соответствующий экземпляр колонки. При обращении к свойству записи происходит передача колонки и номера записи хранилищу и вызывается метод чтения либо записи значения из хранилища.

О том, как устроено хранилище - в другой раз. Впрочем, наверняка идея уже ясна.

четверг, 25 сентября 2008 г.

Asynchronous Stream Copying

Не так давно возникла необходимость копирования данных неопределенного размера в из одного потока данных (System.IO.Stream) в другой максимально быстрым образом. Речь шла о передаче больших файлов по локальной сети.

Самый простой способ выполнить требуюмую операцию - в цикле читать кусок данных из source потока и затем писать его в destination поток. При таком подходе время копирования данных складывается из суммарного времени чтения и суммарного времени записи, однако, теоретический нижний предел выполнения такой операции - суммарное время работы с самым медленным потоком.

Как приблизиться к теоретическому пределу? Ответ очевиден: читать очередную порцию данных во время записи предыдущей порции, т.е. выполнять работу по обмену с потоками одновременно. Что для этого потребуется? Управление потоками, объекты синхронизации? Вовсе нет. Задача выполнима в рамках паттерна IAsyncResult.

Далее я представлю метод, который копирует данные из потока в поток до тех пор, пока source поток не вернет 0 байт, но не больше чем указано в параметре count. Метод не очень большой (весь файл занимает около 100 строк вместе с объявлением класса и импортом пространств имен), но я разобью его на логические куски, так чтобы было удобнее комментировать. При желании куски можно будет сложить, и они будут работать.

Итак, объявление метода и проверка параметров:
public static long CopyData(Stream source, Stream destination, long count, int bufferSize)
{
    if (source == null)
        throw new ArgumentNullException("source");

    if (destination == null)
        throw new ArgumentNullException("destination");

    if (count < 0L)
        throw new ArgumentOutOfRangeException("count");

    if (bufferSize <= 0)
        throw new ArgumentOutOfRangeException("bufferSize");

    if(count == 0L)
        return 0L;
Метод возвращает число фактически скопированных байт. Оно может быть не равно параметру count в случае, если source поток достиг конца. Ничего интересного тут пока нет. Комментарии к этому куску опущу. А дальше они понадобятся:
    long totalBytes2Read = count;
  
    byte[] readingBytes = null;

    int bytesRead = 0;
    byte[] readBytes = null;

    int bytes2Write = 0;
    byte[] writingBytes = null;
     
    int bytesWrited = 0;
    byte[] freeBuffer = null;

    long totalWritedBytes = 0L;

    IAsyncResult asyncReadResult = null;
    IAsyncResult asyncWriteResult = null;
В порядке объявления:
  • totalBytes2Read - счетчик байт, которые осталось прочитать из потока source;
  • readingBytes - буфер данных, куда будут читаться данные. По мере прочтения буфер попадет в переменную readBytes;
  • bytesRead - число байт, фактически вчитанных в буфер;
  • readBytes - буфер, ожидающий записи в destination. После начала записи он будет храниться в переменной writingBytes;
  • bytes2Write - число прочитаных байт, но в другой переменной, соответствующей буферу, ожидающему записи;
  • writingBytes и bytesWrited - это записываемые байты и их число (число ранее вчитанных байт);
  • freeBuffer - освободившийся после записи буфер. При необходимости он будет использован заново, а это весьма вероятно;
  • asyncReadResult и asyncWriteResult - это результаты операций BeginRead и BeginWrite соответственно.
Как вы наверное уже догадались, буфер будет проходить циклически через несколько этапов: чтение -> ожидание записи - > запись -> ожидание чтения -> чтение... В тот момент когда буфер прошел этап чтения, переменная может хранить уже другой буфер, в который сразу же начинается чтение. Такие же этапы проходит значение числа прочитанных байт. Оно путешествует по соответствующим полям. Правда, рождается это число только в момент окончания чтения, и его не нужно хранить для начала следующего чтения, потому этапов для этого значения меньше.

Сами границы этапов я оформил анонимными методами. Дело в том, что некоторые из них из управляющего кода вызываются больше одного раза, а оформлять настоящие методы и передавать в них много параметров и результатов мне показалось неуклюжим подходом. Итак, начало и конец чтения:
Action beginRead = () =>
    {
        var size = (int) Math.Min(bufferSize, totalBytes2Read);
        asyncReadResult = source.BeginRead(
            readingBytes = freeBuffer ?? (new byte[size]),
            0,
            size,
            null,
            null);
    };

    Action endRead = () =>
    {
        bytes2Write = bytesRead = source.EndRead(asyncReadResult);
        readBytes = readingBytes;
        totalBytes2Read -= bytesRead;
    };
beginRead первым делом определяет размер данных, которые нужно заказать source потоку. Далее он анализирует наличие свободного буфера и при отсутствии его создает новый. Размер создаваемого буфера может быть меньше, чем указанно в параметре bufferSize, т.к. по достижении конца заказанного в count диапазона полноразмерный буфер не нужен. Далее полученный буфер (свободный, либо вновь созданный) записывается в переменную readingBytes и source потоку заказывается чтение в этот буфер куска данных, равному size.

Результат выполнения операции BeginRead записывается в локальную переменную. endRead завершает операцию чтения у потока source, получает фактический размер прочитанных байт и размазывает его по двум переменным. Хватило бы одной переменной, но у них разные назначения. Далее буфер, в который мы читали становится буфером, ожидающим записи. И в конце декрементируется счетчик оставшихся для чтения байт.
Action beginWrite = () => asyncWriteResult = destination.BeginWrite(
        writingBytes = readBytes,
        0,
        bytesWrited = bytes2Write,
        null,
        null);

    Action endWrite = () =>
    {
        destination.EndWrite(asyncWriteResult);
        freeBuffer = writingBytes;
        totalWritedBytes += bytesWrited;
    };
beginWrite переводит буфер и его размер в следующее состояние (присваивает другим переменным) и начинает операцию чтения. endWrite завершает операцию чтения и освобождает буфер (записывает его в переменную для освобожденного буфера). В заключении инкриментируется число фактически записанных байт.

Заключающий кусок кода - управляющая часть метода:
    beginRead();

    while (totalBytes2Read > 0L)
    {
        endRead();

        if (bytesRead <= 0)
            break;

        if (totalBytes2Read > 0L)
            beginRead();

        if (asyncWriteResult != null)
            endWrite();

        beginWrite();
    }

    endWrite();

    return totalWritedBytes;
}
Начинается управляющий кусок всегда с начала чтения. Дальше, в цикле, отслеживающим счетчик прочитанных байт, первым делом завершаем операцию чтения. Когда мы только вошли в цикл, то чтение только что начато, но пока оно не завершено, делать больше нечего - только ждать конца чтения. Затем анализируется число прочитанных байт. Если 0 - завершаем цикл. Читать больше нечего. Если есть что читать (работа со счетчиками ведется в операции endRead), то начинаем очередное чтение. Далее анализируется поле asyncWriteResult, для того, чтобы понять, начата ли операция записи. На первой итерации цикла оно пусто, т.е. окончание чтения пропускается. Во всех остальных итерациях будет ожидание завершения записи. После завершения записи - открываем новую операцию записи.

Вне тела цикла стоит ожидание записи в выходной поток. Внимательно посмотрев на управляющий цикл, можно заметить, что начала и концы парных операций стоят рядом в порядке где сначала конец операции, потом начало следующей операции. Конец чтения - сразу за ним начало чтения; конец записи и сразу начало записи. Таким образом, для завершения каждой асинхронной операции существует целая итерация цикла, в каждой из которых выполняется две асинхронной операции (чтение и запись). И каждая итерация будет занимать время, требующееся на максимально долгую операцию (чтения либо записи).

Для тестирования производительности я использовал производный от MemoryStream класс, который перекрывая соответствующие методы, регулировал время обращения к нему с помощью метода Thread.Sleep(int). Действительно, при больших объемах данных и малых размерах буфера время работы предложенного мной метода стремится к теоретическому пределу, а именно - к времени работы самого медленного потока. Даже при случайном распределении величины искувственной задержки для потоков, время копирования существенно меньше суммы времен работы с каждым из потоков.

Аналогичным способом можно оформить конвертирование файлов, либо другие асинхронные операции, выполняемые циклически. P.S. Надеюсь, что кому-нибудь пригодится данный подход. Мне кажется, что он довольно изящный )))

вторник, 23 сентября 2008 г.

Readonly Auto-Implemented Properties

Вчера расстроился по поводу реализации auto-implemented properties в C# 3.0. Началось все с того, что прочитав документацию по этому вопросу довольно продолжительное время назад, и встретив там цитату
To create a readonly auto-implemented property, give it a private set accessor.
считал, что auto-implemented readonly свойства имеют отношение к ключевому слову readonly. Вот цитата из описания этого ключевого слова:
The readonly keyword is a modifier that you can use on fields. When a field declaration includes a readonly modifier, assignments to the fields introduced by the declaration can only occur as part of the declaration or in a constructor in the same class.
Именно на эту статью в документации ведет гиперссылка из статьи про auto-implemented свойства. Воображение рисовало такую реализацию этого нововведения компилятором, где бы код
class Foo
{
  public int MyProperty { get; private set; }

  public Foo(int p)
  {
      MyProperty = p;
  }
}
соответствовал бы следующей реализации:
class Foo
{
  private readonly int _autoGeneratedReadonlyField;

  public Foo(int p)
  {
      autoGeneratedReadonlyField = p;
  }

  public int MyProperty
  {
      get { return _autoGeneratedReadonlyField; }
  }
}
Таким образом, такая реализация auto-implemented свойства с private set аксессором, максимально соответствовала бы документации. Так получилось, что довольно долго я считал, что readonly auto-implemented свойства нельзя модифицировать нигде, кроме конструктора класса (либо статического конструктора, если свойство статическое). Оказалось, что private set не генерит ничего кроме private аксессора к свойству, обращение к которому разрешено из кода любого метода или свойства класса, либо вложенных классов.

Итого, использование readonly auto-implemented свойств не имеет никакого отношения к readonly полям класса. Зачем же тогда в документации стоит эта ссылка на описание ключевого слова readonly? Думаю, что кроме меня есть еще жертвы дезинформации.

Вообще говоря, поведение private set модификатора auto-implemented свойства выбрано правильно. И такое поведение нужно, но кроме этого нужна адекватная документация. Все же, эмуляция readonly полей нужна. И на форуме в RSDN было предложено несколько вариантов синтаксиса, которые могли бы быть реализованы. Например,
public readonly string MyString { get; }

public int Property { get; readonly set; }

public int Property { readonly; }
Удобен был бы способ инициализации таких свойств при объявлении (по аналогии с полями).

суббота, 30 августа 2008 г.

Расширения для System.Enum

Довольно странно, что языковые средства расширяются от версии к версии платформы, однако нововведения как-правило не затрагивают те части FCL, что знакомы нам с .NET 1.0. Речь пойдет о способах скрыть неуклюжие обращения к методам класса System.Enum, и добавить кое-какую функциональность. Определимся со списком целей:
  • метод object Enum.Parse(Type, string): приходится дважды указывать тип перечислителя (один раз аргументом метода, другой для приведения результата). Попытаемся подсахарить;
  • метод bool Enum.IsDefined(Type, object) так же требует указания типа перечислителя. В случае, когда тип перечислителя уже зашит в аргументе это лишнее. Например, когда требуется проверить принадлежность значения перечислителя к набору констант перечислителя без применения конструкции switch. Подсахарить;
  • проверка на наличие установленного флага выглядит слишком громоздко для использования в операторах ветвления:
    if(FileAccess.Read == (fileAccess & FileAccess.Read)) { ... }
    Еще хуже выглядит проверка на наличие сразу нескольких флагов. Определим метод, который с небольшим оверхедом по производительности скрасит конструкцию. (Наверняка, в Nemerle есть соответствующие макросы)
Для реализации задуманного потребуется два класса. Один - generic класс, в котором будут определены методы для работы с перечислителями а так же кое-какие статические поля, обеспечивающие работу этих методов:
public static class EnumExtensions<TEnum>
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
}
Обратите внимание на ограничения параметра типа. Я постарался выбрать ограничения, максимально близкие к типу enum, просто для того, чтобы компилятор и intellisense не позволяли использовать этот класс для других типов. На самом деле ни одно из ограничений не понадобится для реализации требуемой функциональности. Они фиктивны. К сожалению, не удалось исключить создание типов аля EnumExtensions<int>. При создании таких типов можно добиться двух эффектов: исключения TypeInitializationException, либо ограниченной работоспособности. В данном конкретном случае предпочитаю избежать TypeInitializationException.

Второй класс нужен только для объявления extension-методов, т.к. они не могут быть определены в generic-классах:
public static class EnumExtensions
{
}
Начнем с метода Parse. В generic-классе определим следующий метод:
public static TEnum Parse(string value)
{
   return (TEnum)Enum.Parse(typeof(TEnum), value);
}
В классе для extension методов определим метод-обертку для вышеописанного:
public static TEnum Parse<TEnum>(this string value)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.Parse(value);
}
Обращаться к методу-расширению можно в следующей форме:
var mode = modeString.Parse<FileMode>();
Для реализации метода IsDefined(TEnum value) потребуется хранение набора констант, полученных с помощью метода Enum.GetValues(Type). Дело в том, что подглядев реализацию метода Enum.IsDefined(Type, object), я понял что меня не устраивает такое количество выполняемого кода для проверки значения на допустимость. Даже цена получения массива значений в Enum.GetValues(Type) слишком высока для обращения к этому методу более одного раза! Добавим в generic-класс следующий код:
private static readonly TEnum[] s_values = GetValues();

private static TEnum[] GetValues()
{
   return (typeof(TEnum).IsEnum)
       ? (TEnum[])Enum.GetValues(typeof(TEnum))
       : new TEnum[]{};
}

public static int IndexOf(TEnum value)
{
   return Array.IndexOf(s_values, value);
}

public static bool IsDefined(TEnum value)
{
   return IndexOf(value) > -1;
}
В порядке объявления: s_values - статическое поле для хранения массива величин типа TEnum. Инициализируется в объявлении. Статический метод GetValues проверяет, является ли тип TEnum перечислителем, и возвращает массив значений перечислителя, либо пустой массив, избегая исключения TypeInitializationException (оно непременно возникнет при возбуждении исключения ArgumentException при обращении к методу Enum.GetValues(Type) во время инициализации типа ExtensionMethods<TEnum>). Методы IndexOf и IsDefined тривиальны.

Отмечу только, что я не гарантирую их работоспособность при использовании типов, отличных от enum, которые смогли пролезть через набор ограничений Type-параметра (примитивные типы int, long,... и некоторые пользовательские типы). Уверен, что при желании читатель сможет самостоятельно реализовать достойное поведение этих методов для типов, отличных от enum. Думаю, что самым достойным здесь будет возбуждение исключения NotSupportedException.

Да, метод GetIndex(TEnum) получился в качестве бонуса, и я не вижу необходимости скрывать его. Может оказаться полезным. Добавим соответствующие extension-методы в класс EnumExtensions (не generic-класс):
public static int GetIndex<TEnum>(this TEnum value)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.IndexOf(value);
}

public static bool IsDefined<TEnum>(this TEnum value)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.IsDefined(value);
}
Вот код, проверяющий работу метода IsDefined и демонстрирующий обращение к нему через extension-метод:
Assert.IsTrue(FileMode.Open.IsDefined());

Assert.IsFalse(((FileMode)1000).IsDefined());
Получился весьма элегантный способ проверки значений на принадлежность к набору констант. Приступим к реализации метода bool HasFlags(TEnum value, TEnum flags). Не сложно реализовать такой метод с помощью обращения к методу ulong IConvertible.ToUInt64(IFormatProvider), однако производительность такого решения будет не на высоте (как минимум 4 операции boxing-а, 2 обращения к extern методам, 2 unboxing-а). Так же этот метод будет оперировать 64-х разрядными величинами, даже в тех случаях, когда перечислитель основан на более коротких типах.

Недавно пришло в голову, что динамически сгенерированный метод для соответствующего enum-типа может быть вполне приемлемым по производительности решением. Следующий метод определяет динамический метод DynamicMethod и делегирует генерацию кода переданному в качестве аргумента методу.
private static DynamicMethod BuildHasFlagsMethod(Type enumType, Action<ILGenerator> ilGenAction)
{
   var method = new DynamicMethod(
       "HasFlagMethod",
       typeof(bool),
       new Type[] { enumType, enumType },
       typeof(EnumExtensions).Module);

   var il = method.GetILGenerator();
   ilGenAction(il);
   return method;
}
Я не зря параметризовал этот метод параметром Type, в то время как в generic-классе известен тип TEnum. Для оптимизации работы JIT-компилятора и объема генерируемого им машинного кода следует выносить методы, не использующие явно типы generic аргументов, в не generic-классы. В рамках этого поста я буду располагать такие методы в generic-классе, но буду подразумевать, что читатель вынесет их во вспомогательный класс (если, конечно, статья окажется полезной для него, и он решит воспроизвести код).

Следующие два метода генерируют код реализации метода HasFlags. Первый генерирует хорошую реализацию, а второй - реализацию, которая выбрасывает исключение NotSupportedException. Вторая реализация пригодится для перечислителей без атрибута [Flags], либо для типов, не являющихся enum-ами.
static void EmitHasFlags(ILGenerator il)
{
   il.Emit(OpCodes.Ldarg_1);
   il.Emit(OpCodes.Ldarg_0);
   il.Emit(OpCodes.Ldarg_1);
   il.Emit(OpCodes.And);
   il.Emit(OpCodes.Ceq);
   il.Emit(OpCodes.Ret);
}

static void EmitThrowException(ILGenerator il)
{
   var ctor = typeof(NotSupportedException).GetConstructor(new Type[] { });
   il.Emit(OpCodes.Newobj, ctor);
   il.Emit(OpCodes.Throw);
}
Метод EmitHasFlags записывает два аргумента в стек для выполнения операций логического умножения и сравнения результата со втрорым аргументом, выполняет операции умножения и сравнения, затем возвращает результат. Операция логического умножения может быть выполнена только над примитивными типами, потому необходимо гарантировать обращение к этому методу генерации только при корректном generic-аргументе.
private static readonly Func<TEnum, TEnum, bool> s_hasFlagsMethod;

public static bool IsFlags { get; private set; }

static EnumExtensions()
{
   IsFlags = HasFlagsAttribute(typeof(TEnum));

   Action<ILGenerator> ilGenAction;
   if(IsFlags)
       ilGenAction = EmitHasFlags;
   else
       ilGenAction = EmitThrowException;

   var method = BuildHasFlagsMethod(typeof(TEnum), ilGenAction);

   s_hasFlagsMethod = (Func<TEnum, TEnum, bool>)method.CreateDelegate(
       typeof(Func<TEnum, TEnum, bool>));
}

internal static bool HasFlagsAttribute(Type enumType)
{
   return enumType.GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0;
}

public static bool HasFlags(TEnum value, TEnum flags)
{
   return s_hasFlagsMethod(value, flags);
}
В порядке объявления: s_hasFlagsMethod - делегат для сгенерированного метода; IsFlags - свойство, указывающее на наличие атрибута [Flags] у типа TEnum. Это еще один полезный бонус, который можно оставить опубликованным; далее - статический конструктор.

Остановимся на нем подробнее: Первым делом он инициирует свойство IsFlags значением, возвращаенным методом HasFlagsAttribute(Type). Затем выбирается метод для генерации кода. При значении свойства IsFlags, равном true, можно гарантировать, что TEnum - enum тип, т.к. компилятор не даст применить атрибут [Flags] к любому другому типу, кроме enum. Однако, варьируя условие выбора метода генерации IL кода, можно добиться генерации рабочего метода для целых типов TEnum, при необходимости. Я, правда, такой необходимости не вижу. Для целых методов нет нужды обращаться к динамически сгенерированному коду, потому как можно определить методы обычным образом.

Наконец, реализация метода HasFlags(TEnum, TEnum), которая делегирует динамически сгенерированному методу. Объявление соответствующего extension-метода:
public static bool HasFlags<TEnum>(this TEnum value, TEnum flags)
   where TEnum : struct, IComparable, IConvertible, IFormattable
{
   return EnumExtensions<TEnum>.HasFlags(value, flags);
}
Использование этого метода может быть например таким:
if(fileAccess.HasFlags(FileAccess.Read)) { ... }
Предлагаю сравнить этот кусок кода с аналогичным выше. Думаю, что этот информативнее. Однако, не следует использовать этот метод в критичном по производительности коде.

P.S. Код для проверки флагов в типе int может выглядеть так:
public static bool HasFlags(this int value, int flags)
{
   return flags == (value & flags);
}

...
if(myIntValue.HasFlags(0x80)) { ... }
P.P.S Пробовал сгенерировать код с помощью Expression. Вот что вышло:
var valueParam = Expression.Parameter(typeof(TEnum), "value");
var flagsParam = Expression.Parameter(typeof(TEnum), "flags");
Expression body = Expression.Equal(
    flagsParam,
    Expression.And(valueParam, flagsParam));
В последней строке получил исключение: System.InvalidOperationException: The binary operator And is not defined for the types 'System.IO.FileAccess' and 'System.IO.FileAccess'.. Получается, что il.Emit(OpCodes.And); можно выполнить над Enum-ом, а Expression.And - нет. Был искренне удивлен.

пятница, 15 августа 2008 г.

Короткий синтаксис ADO .NET (часть IV)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть III)

В предыдущих частях было описано все что необходимо для обращения к БД за один вызов, кроме разве что передачи параметров. В ADO .NET уже проделана вся работа по унифицированному способу задания параметров. Осталось лишь воспользоваться им.

Задумка в том, чтобы объявлять параметры к команде в том же обращении (ExecuteReader, ExecuteScalar либо ExecuteNonQuery) через запятые. Для каждого параметра достаточно передать только 2 значения - имя параметра и значение параметра. А тип параметра будет определяться типом значения. Определим следующий класс:
public class Parameter
{
    public string ParameterName { get; private set; }

    public DbType DbType { get; private set; }

    public object Value { get; private set; }

    private Parameter(string name, DbType type, object value)
    {
        ParameterName = name;
        DbType = type;
        Value = value;
    }

    public void ApplyParameter(IDbCommand command)
    {
        IDbDataParameter parameter = command.CreateParameter();
        parameter.ParameterName = ParameterName;
        parameter.DbType = DbType;

        if (ReferenceEquals(Value, null))
        {
            parameter.Value = DBNull.Value;
        }
        else
        {
            parameter.Value = Value;
        }

        command.Parameters.Add(parameter);
    }

    ...
}
Метод ApplyParameter создает унифицированным способом экземпляр параметра, передает ему хранящиеся в экземпляре имя, унифицированный тип и значение параметра, добавляет созданный параметр в коллекцию параметров команды. Обратите внимание, что конструктор класса Parameter объявлен с private модификатором видимости. Это потому, что я параноидально опасаюсь создания экземпляров с типом параметра не согласованным со значением параметра. Определение типов параметра возьмет на себя серия производящих методов.

Метод ConvertToObject<T>(T? value) конвертирует переданные nullable значения в object тип. В сочетании с методом ApplyParameter они удачно проецируют nullable типы на значения параметров.
internal static object ConvertToObject<T>(T? value)
    where T : struct
{
    if (value.HasValue)
    {
        return value.Value;
    }
    else
    {
        return null;
    }
}

public static Parameter Make(string name, string value)
{
    return new Parameter(name, DbType.String, value);
}

public static Parameter Make(string name, Guid? value)
{
    return new Parameter(name, DbType.Guid, ConvertToObject(value));
}

public static Parameter Make(string name, int? value)
{
    return new Parameter(name, DbType.Int32, ConvertToObject(value));
}

public static Parameter Make(string name, bool? value)
{
    return new Parameter(name, DbType.Boolean, ConvertToObject(value));
}

public static Parameter Make(string name, double? value)
{
    return new Parameter(name, DbType.Double, ConvertToObject(value));
}

public static Parameter Make(string name, DateTime? value)
{
    return new Parameter(name, DbType.DateTime, ConvertToObject(value));
}

public static Parameter Make(string name, decimal? value)
{
    return new Parameter(name, DbType.Decimal, ConvertToObject(value));
}
Таким образом, обращение
Make("@Value", (int?)null)
сформирует параметр целого типа со значением DbNull.Value. Почему производящие методы, а не перегруженные конструкторы? Просто в некоторых случаях могут потребоваться делегаты (например Func<string, int?, Parameter>), а делегат можно создать только для метода. Для добавления списка параметров к методам Execute*** воспользуемся ключевым словом params.

Вот обновленный интерфейс IDbDriver:
public interface IDbDriver
{
    IDbConnection MakeConnection();

    IDbCommand MakeCommand(IDbConnection connection, string commandText, params Parameter[] parameters);
    IDbCommand MakeCommand(string text, params Parameter[] parameters);

    int ExecuteNonQuery(string commandText, params Parameter[] parameters);
    int ExecuteNonQuery(IDbConnection connection, string commandText, params Parameter[] parameters);

    void ExecuteReader(Action<IDataReader> action, string commandText, params Parameter[] parameters);
    void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText, params Parameter[] parameters);
}
Не составит сложности модифицировать методы Execute*** и протянуть параметр parameters до метода MakeCommand. Вот его новая реализация:
public IDbCommand MakeCommand(IDbConnection connection, string commandText, params Parameter[] parameters)
{
    if (connection == null)
    {
        throw new ArgumentNullException("connection");
    }
    var result = connection.CreateCommand();

    result.CommandText = commandText;
    foreach (Parameter parameter in parameters)
    {
        parameter.ApplyParameter(result);
    }
    return result;
}
Осталось привести пример обращения к БД для выполнения команды с параметром:
[TestMethod]
public void ExecuteReaderRealTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    DataTable table = new DataTable();
    dbDriver.ExecuteReader(
         table.Load,
         "SELECT * FROM [Order Details] WHERE UnitPrice > @Price",
         Parameter.Make("@Price", 20));

    Assert.IsTrue(table.Rows.Count > 0);
}
И в заключение содержательной части пример с выполнением более чем одной команды в рамках одного соединения:
static void LoadTable(DataTable table, IDbCommand command)
{
    using (var reader = command.ExecuteReader())
    {
        table.Load(reader);
    }
}

[TestMethod]
public void MultiCommandTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    var dataSet = new DataSet();
    var orderDetails = dataSet.Tables.Add("Order Details");
    var orders = dataSet.Tables.Add("Orders");

    int orderId = 10248;

    using (var connection = dbDriver.MakeConnection())
    {
        var orderIdParameter = Parameter.Make("@OrderId", orderId);
      
        Func<string, IDbCommand> makeCommand =
            x => dbDriver.MakeCommand(connection, x, orderIdParameter);
                    
        using (var selectOrders = makeCommand("SELECT * FROM Orders WHERE OrderId = @OrderId"))
        using (var selectDetails = makeCommand("SELECT * FROM [Order Details] WHERE OrderId = @OrderId"))
        using (connection.CreateSession())
        {
            LoadTable(orders, selectOrders);
            LoadTable(orderDetails, selectDetails);
        }
    }

    Assert.IsTrue(orderDetails.Rows.Count > 0);
}
Обратите внимание, для короткой записи создания команды я использовал анонимный метод, который использует один экземпляр класса Parameter для формирования всех команд. При чтении DataSet наборов данных довольно часто используется одинаковый набор параметров при обращении к БД за разными таблицами. На этом и сыграл. Запись получилась компактной и наглядной. В первом посте я поведал об истории моего собеседования.

Надеюсь, что теперь вы меня поймете, что используя такую обвязку к ADO .NET не мудрено забыть кое-что из последовательности действий, описываемых в MSDN... Применяя такую обвязку к ADO, будьте внимательны на собеседованиях ;)

Содержательная часть серии постов об короткой нотации ADO .NET закончилась. Осталось только поудивляться тому, что разработчики Microsoft все необходимое сделали сами, как то инвариантные относительно движка БД способы создания соединений, команд, параметров команд, и даже предусмотрели инвариантные типы параметров. Однако, по каким-то причинам Microsoft просто не довела эту кухню до этого логического конца. Почему-то их логическим концом стал безумный кодогенератор дизайнера адаптеров данных, который генерирует дикое количество нечитаемого кода, завязанного на тип БД. Если разработчик работает с SqlServer, то дизайнер генерирует соединения SqlConnection, команды SqlCommand, параметры SqlParameter и совершенно не ясно, как модифицировать этот код для работы с другим движком БД, либо с несколькими. К тому же, вся документация ADO .NET кишит примерами, завязанными на конкретные движки БД. Скромно замечу, что преподнесенный мной код обвязки абсолютно инвариантен к типу БД. Но заслуга в этом разработчиков Microsoft.

Короткий синтаксис ADO .NET (часть III)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть IV)

Приступим к созданию соединений и команд. Я для этого предпочитаю использовать некий класс DbDriver, параметризованный ConnectionString-ом и соответствующим DbProviderFactory классом. DbDriver создает соединения сразу же инициированные ConnectionString-ом, и команды, инициированные соединением. Следующий интерфейс как раз описывает эту сущность:
public interface IDbDriver
{
    IDbConnection MakeConnection();

    IDbCommand MakeCommand(IDbConnection connection, string commandText);
    IDbCommand MakeCommand(string text);

    int ExecuteNonQuery(string commandText);
    int ExecuteNonQuery(IDbConnection connection, string commandText);

    void ExecuteReader(Action<IDataReader> action, string commandText);
    void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText);
}
Вообще, рекомендуется в перегружаемых методах опциональный параметр писать в конце списка параметров. Забегая вперед, скажу, что это не окончательная сигнатура методов создания и выполнения команд. Перегруженные методы MakeCommand нужны для двух вариантов создания команд. Один вариант (принимающий IDbConnection) нужен для создания команд в рамках указанного соединения, когда потребуется выполнение более одной команды, другой вариант (без соединения) - когда нам требуется выполнить лишь одну команду в соединении.

Методы выполнения команд (ExecuteNonQuery и ExecuteReader) перегружены со следующей целью. Один вариант принимает соединение, создает и инициирует команду, открывает открывает соединение (если не было открыто), выполняет команду, закрывает (если было закрыто), уничтожает команду и возвращает результат. Второй вариант - создает соединение, вызывает первый вариант, после чего уничтожает соединение.
public class DbDriver : IDbDriver
{
    public DbDriver(string connectionString, DbProviderFactory providerFactory)
    {
        if (providerFactory == null)
        {
            throw new ArgumentNullException("providerFactory");
        }
        ProviderFactory = providerFactory;
        ConnectionString = connectionString;
    }

    public string ConnectionString { get; private set; }

    public DbProviderFactory ProviderFactory { get; private set; }

    public IDbConnection MakeConnection()
    {
        IDbConnection result = ProviderFactory.CreateConnection();
        result.ConnectionString = ConnectionString;
        return result;
    }

    public IDbCommand MakeCommand(IDbConnection connection, string commandText)
    {
        if (connection == null)
        {
            throw new ArgumentNullException("connection");
        }
        var result = connection.CreateCommand();
        result.CommandText = commandText;
        return result;
    }

    public IDbCommand MakeCommand(string commandText)
    {
        return MakeCommand(MakeConnection(), commandText);
    }
    ....
}
Выше приведена реализация конструктора класса и производящих методов. Надеюсь, что комментарии излишни. Далее реализация методов, выполняющих команды:
public int ExecuteNonQuery(string commandText)
{
    using (var connection = MakeConnection())
    {
        return ExecuteNonQuery(connection, commandText);
    }
}

public int ExecuteNonQuery(IDbConnection connection, string commandText)
{
    if (connection == null)
    {
        throw new ArgumentNullException("connection");
    }

    using (var command = MakeCommand(connection, commandText))
    using (connection.CreateSession())
    {
        return command.ExecuteNonQuery();
    }
}

public void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText)
{
    if (connection == null)
    {
        throw new ArgumentNullException("connection");
    }

    using (var command = MakeCommand(connection, commandText))
    using (connection.CreateSession())
    using (var reader = command.ExecuteReader())
    {
        action(reader);
    }
}

public void ExecuteReader(Action<IDataReader> action, string commandText)
{
    using (var connection = MakeConnection())
    {
        ExecuteReader(connection, action, commandText);
    }
}
Методы Execute*** делают попытку открыть соединение в самый последний момент перед обращением к команде и закрывают соединение, если оно было закрыто перед вызовом, сразу после выполнения команды. Повторюсь, гибкость метода CreateSession позволяет одними методами работать как с первоначально закрытым соединением с БД, так и с уже октрытым. ExecuteReader принимает Action<IDataReader>. Это связано с тем, что IDataReader должен быть освобожден до закрытия соединения. Если метод ExecuteReader будет закрывать соединение, т.е. если соединение было закрыто перед вызовом, то к тому моменту требуется прочитать все что надо из IDataReader-а. Метод ExecuteScalar я даже не стал включать в пример, т.к. он работает полностью аналогично методу ExecuteReader.

Unit тесты приводить не буду. Но один интеграционный, пожалуй приведу. Он довольно показателен по части того, что достигнуто:
[TestMethod]
public void ExecuteReaderRealTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    DataTable table = new DataTable();

    dbDriver.ExecuteReader(
        reader => table.Load(reader),
        "SELECT OrderId FROM [Order Details]");

    Assert.IsTrue(table.Rows.Count > 0);
}
Код, что отвечает непосредственно за обращение к БД занимает всего 3 строчки. В рамках обращения к одному методу выполняются действия, перечисленные в конце первой части этой серии постов.

Почти все! Не хватает только чего-то для работы с параметрами команд. А это в следующем посте...

четверг, 14 августа 2008 г.

Короткий синтаксис ADO .NET (часть II)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть III)
Короткий синтаксис ADO .NET (часть IV)

В предыдущей части я привел перечень действий, необходимых для выполнения запроса и корректного овсобождения ресурсов. Начнем с открытия и закрытия соединения. В пределе хочется иметь сущность, которая бы:
  1. анализировала состояние указанного соединения, и открывала бы его, если оно не открыто;
  2. гарантированно закрывала бы соединение после использования, в случае если соединение было первоначально закрыто;
  3. обращение к такой конструкции не занимало бы много места.
Отличный повод воспользоваться конструкцией using и интерфейсом IDisposable чтобы избежать конструкция try/finally. Вот код требуемой сущности:
public class ConnectionSession : IDisposable
{
   public ConnectionSession(IDbConnection dbConnection)
   {
       if (dbConnection == null)
       {
           throw new ArgumentNullException("dbConnection");
       }
       DbConnection = dbConnection;

       WasOpened = ConnectionState.Open == (DbConnection.State & ConnectionState.Open);

       if (!WasOpened)
       {
           DbConnection.Open();
       }
   }

   public IDbConnection DbConnection { get; private set; }
   public bool WasOpened { get; private set; }

   public void Dispose()
   {
       if (!WasOpened)
       {
           DbConnection.Close();
       }
   }
}
Прежде чем начну комментировать код, хочу заметить, что в связи с грядущим переходом на C# 3.0, я начинаю пробовать на вкус новый синтаксис. Однако, все или почти все можно повторить на C# 2.0.

Как я уже сказал, интерфейс IDisposable реализован чисто для поддержания короткого синтаксиса using. Сей объект не нуждается в финализации и DbConnection сам будет освобождать необходимые ресурсы при сборке мусора. Быть может когда-нибудь я уделю связке using/IDisposable больше внимания.

Почему я использовал IDbConnection интерфейс, вместо базового класса DbConnection? Очень просто. При тестировании я могу подменить объект без средств тестирования, использующих инструментацию сборок. Для генерации mock объектов и заглушек (stub) я использую Rhino Mocks. Это первая библиотека мокогенераторов, которую я использую, и очень доволен ей. Пока не было поводов переходить на другие. А с новым синтаксисом (для .NET 3.5) я просто обожаю Rhino (это не реклама! Rhino Mocks - бесплатная библиотека и мне за упоминание о ней никто не платит).

В постах я буду приводить некоторые тесты, но далеко не все, которые пишу.
[TestMethod]
   public void UsingConstructionWithOpenedConnectionTest()
   {
       var connection = MockRepository.GenerateStub<IDbConnection>();

       connection.Stub(x => x.State).Return(ConnectionState.Closed);

       using (new ConnectionSession(connection))
       {
           connection.AssertWasCalled(x => x.Open());
           connection.AssertWasNotCalled(x => x.Close());
       }

       connection.AssertWasCalled(x => x.Close());
   }

   [TestMethod]
   public void UsingConstructionWithClosedConnectionTest()
   {
       var connection = MockRepository.GenerateStub<IDbConnection>();

       connection.Stub(x => x.State).Return(ConnectionState.Open);

       using (new ConnectionSession(connection))
       {
           connection.AssertWasNotCalled(x => x.Open());
       }

       connection.AssertWasNotCalled(x => x.Close());
   }
Несмотря на то, что в каждом тесте рекомендуется проверять не более одного условия, я напичкал их множеством условий, потому как я тестировал поведение ConnectionSession в рамках конструкции using. Это уже даже не unit тест, а некий интеграционный. Unit тесты класса ConnectionSession я опустил. Их больше, чем хотелось бы вставлять в пост. Оговорки в условиях к ConnectionSession (вначале поста) нужны для безопасного использования соединения в области действия нескольких вложенных блоков using для ConnectionSession объектов. В рамках C# 3.0 будет удобен следующй extension метод:
public static class ConnectionSessionExtensions
{
    public static ConnectionSession CreateSession(this IDbConnection connection)
    {
        return new ConnectionSession(connection);
    }
}
это позволит писать код в стиле
using (connection.CreateSession())
{
    return command.ExecuteNonQuery();
}
Довольно емкий смысл вложен в одну строчку:
если соединение не открыто, то открыть его, если было закрыто, то закрыть после выхода из блока.
Уже этот код позволит многим сделать обращения к ADO (и не только) более лаконичными. Но на этом не остановимся, продолжение следует.

вторник, 12 августа 2008 г.

Короткий синтаксис ADO .NET (часть I)

Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть III)
Короткий синтаксис ADO .NET (часть IV)

Да, есть такая технология, пока. Можно даже сказать была, или почти была. Но ADO .NET еще поживет какое-то время. Не хотелось бы обсуждать недостатки этого динозавра в сравнении с LINQ to SQL или Entity Framework, либо ORM подходами. Это не тема данного поста.

Здесь лишь примем факт, что ADO .NET еще пока актуально, хоть и сильно сдает позиции. На этой технологии уже много что написано, и еще пишется, и кое-что будет написано. Хочу обратить внимание, что несмотря на повсеместное засилье ADO в среде .NET разработчиков (до последнего времени), пользоваться ADO в чистом виде довольно неудобно, громоздко. Многие вероятно видали код, сгенерированный дизайнером для DbAdapter-ов, DbCommand и их параметров... А про кол-во манипуляций, необходимых для выполнения запроса и закрытия всех щелей за собой для очистки совести, есть целая история.

История о том, как я проходил собеседование в одну контору на позицию удаленного разработчика. Надо заметить, что вакансия была с заманчивой почасовой оплатой, соразмерной ставке freelancer-а. Интервьюера в большей степени интересовали навыки работы с WinForms и ADO .NET. На вопросы о WinForms я довольно резво ответил (как мне показалось), но с ADO - поплавал: Меня попросили набросать код для чтения каких-либо данных из таблицы БД в DataTable прямо в ICQ. Я что-то быстро набросал и отправил набросок. На что последовал ответ: "Ваш код работать не будет!". Это меня довольно сильно смутило. Я смотрел на этот код и не мог понять, что же в нем не так. Когда я его вставил в Visual Studio и попробовал выполнить, меня озарило: я забыл открыть DbConnection. О чем и поведал интервьюеру.

Заверения о том, что я обычно тестирую свой код и то что такие ляпы не выходят из под моего пера в продакшн, не подействовали на собеседника. Но и сразу отказ я не получил. Мне было позволено задать пару наводящих вопросов и подождать до того, как со мной свяжутся. За пару наводящих вопросов выяснилось, что работа, мягко говоря, не фантан: имеется 250 (или около того) набросков форм, кои надо выполнить в WinForms и сваять для них запросы к 250-ти таблицам. Никакого творчества, инициативы, рефакторинга, тестирования не подразумевалось. Нужно было выдать bug free код как можно быстрее и сделать это как можно ближе к способам, описываемым в MSDN (M$ way).

Понятно, что заклинание использования ADO должно было отскакивать от кандидата как "Отче наш" от послушника в монастыре! Я рад, что мне не предложили эту работу. Я застрелился бы наверное на 10-ой форме. Однако, оправдывает мою забывчивость лишь то, что я не один год работал со своим заклинанием, которое в работе с ADO было на порядок удобнее, чем то что предлагал MSDN. А сейчас мои заклы (заклинания) стали еще удобнее. Именно о том, как может быть удобно с ADO .NET, будет мой пост (может пару или тройку, как попрет).

Попробую перечислить действия, необходимые для выполнения запроса и уборки за собой в общем случае для типа команды ExecuteReader:
  1. Создать DbConnection. Вообще его можно создать обратившись к конструктору, например, SqlConnection. Но в .NET 2.0 появился более правильный путь (через DbProviderFactories, что требует дополнительных манипуляций);
  2. Указать ConnectionString;
  3. Создать DbCommand. Мы можем создать ее через конструктор, либо попросить это сделать connection;
  4. Указать текст команды;
  5. Создать и заполнить свойства необходимых параметров. Тобишь имена, типы, значения;
  6. Добавить параметры в коллекцию параметров команды;
  7. Открыть соединение (именно это я забыл сделать на собеседовании);
  8. Выполнить команду и получить объект DbDataReader;
  9. Вчитать данные из ридера;
  10. Закрыть и уничтожить ридер (можно просто уничтожить);
  11. [Вообще на предыдущем этапе многие и бросают и все работает, но я доведу до конца.] Уничтожить команду. Вопрос, конечно философский, но раз команда реализует IDisposable, то уничтожить ее дело чести для меня, не вдаваясь в подробности реализации. А подробности бывают разные. Конечно, если мы хотим сделать reuse этой команды, то уничтожать ее не надо пока;
  12. Закрыть соединение (процедура крайне желательная, если не последует другая команда сразу за выполнением очередной);
  13. Уничтожить соединение.
Использование DbDataAdapter-а лишь расширит этот перечень. Ничего не забыл? :) Сосредотачиваясь на таком объеме инструкций легко забыть, для чего идешь в БД... Шутка! Но не без доли правды. Так вот, есть трюки, позволяющие сделать это все в рамках одного вызова большого метода. И этот вызов будет сосредоточен в одном месте, охвачен одним взглядом, а не размазан по коду дизайнера подальше от глаз программиста.

На авторство приемов я не претендую. Вполне допускаю, что они были известны до меня, и может быть где-то опубликованы. Но я не сталкивался ни с чем подобным до сих пор. Пока перекур. Блог свежий, пока народ подтянется (если подтянется вообще), все задуманное будет описано.

Трюк с generic-ами.

Сегодня покажу как можно одной строчкой прикрутить к классу некоторую функциональность с учетом типа класса. К сожалению, данный трюк имеет ограничения, но все же я им часто пользуюсь.

Приведу случай из реальной жизни. В системе, в разработке которой я принимаю участие, требуется довольно много разных данных сохранять в xml. Поначалу мы много использовали xml сериализацию. Требовалось уметь сериализовывать и десериализовывать 5 или 6 типов графов. Единственное, что было общее у этих графов - необходимость сериализации в файл, в строку. Вполне естевственно, что после появления второй иерархии объектов для сериализации, возникло желание использовать общий код для сериализации и десериализации графов. Приходила в голову идея использовать внешнюю утилиту, однако использование внешней утилиты показалось не очень красивым (требовалась передача типа корня сериализации во внешний метод):
Foo foo = XmlUtils.DeserializeFromFile(typeof(Foo), "Foo.xml");
или
Foo foo = XmlUtils.DeserializeFromFile<Foo>("Foo.xml");
а душе хотелось полета:
Foo foo = Foo.DeserializeFromFile("Foo.xml");
Таким образом, требуется определить статический метод, который бы для типа Foo возвращал результат типа Foo, а для типа Bar возвращал бы результат типа Bar. Есть средство. Определим хитрый базовый класс:
public class XmlSerializationRoot<TRoot>
   where TRoot : XmlSerializationRoot<TRoot>
{
   public static TRoot DeserializeFrom(TextReader reader)
   {
       if (reader == null)
           throw new ArgumentNullException("reader");

       return (TRoot)CreateSerializer().Deserialize(reader);
   }

   public static TRoot DeserializeFrom(Stream stream)
   {
       if (stream == null)
           throw new ArgumentNullException("stream");

       using (StreamReader reader = new StreamReader(stream))
           return DeserializeFrom(reader);
   }

   public static TRoot DeserializeFromString(string xml)
   {
       using (TextReader reader = new StringReader(xml))
           return DeserializeFrom(reader);
   }

   public static TRoot DeserializeFromFile(string fileName)
   {
       using (TextReader reader = File.OpenText(fileName))
           return DeserializeFrom(reader);
   }

   private static XmlSerializer CreateSerializer()
   {
       return new XmlSerializer(typeof(TRoot));
   }

   public void SerializeTo(TextWriter writer)
   {
       if (writer == null)
           throw new ArgumentNullException("writer");

       CreateSerializer().Serialize(writer, this);
   }

   public string SerializeToString()
   {
       StringBuilder builder = new StringBuilder();
       using (StringWriter writer = new StringWriter(builder))
           this.SerializeTo(writer);

       return builder.ToString();
   }

   public void SerializeToFile(string fileName)
   {
       using (StreamWriter writer = File.CreateText(fileName))
           this.SerializeTo(writer);
   }

   public void SerializeTo(Stream stream)
   {
       using (StreamWriter writer = new StreamWriter(stream))
           this.SerializeTo(writer);
   }
}
Особый интерес представляют статические методы десериализации. Методы сериализации были внесены до кучи, чтобы пример был полным. Вообще говоря, класс XmlSerializationRoot не будет являться базовым классом для классов с возможностью сериализации. Базовым классом для них будет XmlSerializationRoot<T>. Т.е. при следующем объявлении класса Foo
class Foo : XmlSerializationRoot<Foo>
{
}
мы получим ситуацию, где класс Foo наследует определенные у класса XmlSerializationRoot<Foo> методы. Теперь можно пользоваться этими методами через идентификатор типа Foo:
Foo foo = Foo.DeserializeFromFile("foo.xml");
В качестве завершающего штриха предлагаю выделить интерфейс
public interface IXmlSerializable
{
   void SerializeTo(TextWriter writer);
   void SerializeToFile(string fileName);
   void SerializeTo(Stream stream);
   string SerializeToString();
}
и поддержать его классом XmlSerializationRoot. Теперь мы сможем вызывать методы сериализации не зная типа сохраняемого объекта. Теперь буду писать гадости про этот подход.
  1. Данный подход навязывает ограничение наследования на разрабатываемые классы. При необходимости наследования классов от другого класса, использование усложняется, однако, можно объявить собственные методы и делегировать их вспомогательному классу. Например:
    class Foo : SomeBaseClass // наследуемся от чего-то другого
    {
       public static Foo DeserializeFromFile(string fileName)
       {
           return XmlSerializationRoot<Foo>.DeserializeFromFile(fileName);
       }
    }
    В данном случае потребуется убрать constraint у класса XmlSerializationRoot. В принципе, он объявлен формально для контроля за способом использования функциональности, и не требуется для компиляции методов класса.
  2. Resharper ругается на использование методов базового класса через идентификатор производного типа. Можно подкрутить его настройки, вставить управляющий комментарий, либо просто игнорировать данные сообщения.
  3. Пользуясь этим подходом мы не можем прочитать что-то из файла, а потом разобраться, что это было. Но в этом виноват не только сей подход, а так же устройство XML сериализации в FCL.
  4. Если требуется объявить только статические методы, то мы не сможем объявить базовый класс с модификатором static. Если объявим, то не сможем унаследовать от него.
Хоть область применения данного подхода не широка, но иногда это то что надо. Например, при необходимости подсчета экземпляров классов разного типа.

P.S. Хочу обратить внимание, что в данном посте не шла речь о корректности данного подхода к сериализации в архитектурном плане. Это лишь пример издевательства над языком. Однако, данный код отлично работал и был удобно используемым, пока мы не перешли на более тяжелый (но более гибкий) способ сериализации через XmlDocument.

Вывод типов (Type Inference) в С# 2.0

Довольно часто использую вывод типов чисто в декларативных целях. Представим ситуацию, когда требуется объявить массив пар (KeyValuePair<string, int>). Ситуация не частая, но на ее примере я покажу пару незатейливых приемов.
KeyValuePair<string, int>[] pairs = new KeyValuePair<string, int>[] {
  new KeyValuePair<string, int>("A", 0),
  new KeyValuePair<string, int>("B", 1)
};
Код довольно простой, но выглядит громоздко. Попробуем что-нибудь сделать с объявлением элементов массива: Введем вспомогательный метод MakePair:
static KeyValuePair<K, V> MakePair<K, V>(K key, V value)
{
  return new KeyValuePair<K, V>(key, value);
}
С учетом введенного метода, объявление массива можно переписать следующим образом:
KeyValuePair<string, int>[] pairs = new KeyValuePair<string, int>[] {
  MakePair("A", 0),
  MakePair("B", 1)
};
Следующий метод позволит переложить ответственность за создание экземпляра массива на компилятор:
public static T[] MakeArray<T>(params T[] items)
{
return items;
}
Таким образом, вышеописанный пример превращается в прилично читаемый код:
KeyValuePair<string, int>[] pairs = MakeArray(
  MakePair("A", 0),
  MakePair("B", 1));
Код приведенных методов настолько прост, что может быть расположен прямо по месту использования. Однако, я предпочитаю объявлять такие вещи в классе Utils, либо Tools (как шарахнет сверху).

понедельник, 11 августа 2008 г.

Подсветка синтаксиса

Первое, с чем столкнулся при попытке вставить пост с примерами - сложности с подсветкой синтаксиса и форматированием кода примеров. Однако, довольно быстро нашел статью, как все побороть. Спасибо проекту SyntaxHighlighter, thanks to Neil Kilbride (статья здесь). Будет время, напишу о процессе прикручивания этого проекта к блогу.

[+/-] скрытый текст

Инструкция по вставке скрытого текста

О блоге

Решил публиковать разные приемчики, которые я использую при программировании. Последнее время я в основном программирую под платформу .NET в среде MSVS на языке C#. Посмотрим, что из этого получится. Будет очень приятно, если это кому-то пригодится, кроме меня.