четверг, 26 февраля 2009 г.

Книга Фредерика Брукса "Мифический человеко-месяц"

Вот уже более 30 лет прошло с момента выхода первого издания легендарной книги Фредерика Брукса ''Мифический человеко-месяц''. Немного перефразируя самого автора можно сказать так: ''Мифический человеко-месяц'' остается книгой, с которой все еще считаются в современной практике программирования. Ее читательская аудитория выходит за пределы сообщества программистов-разработчиков, она все еще порождает статьи, цитаты и письма, причем не только разработчиков программ, но и юристов, врачей, психологов, социологов. Эта книга, написанная 30 лет назад об опыте разработки программ, имевшем место 40 лет назад, остается актуальной и даже полезной.

Причина заключается в том, что ''история человечества - это пьеса, в которой сюжеты постоянны, сценарии медленно меняются с развитием культуры, а декорации меняются непрерывно. Поэтому в ХХ веке мы узнаем себя в Шекспире, Гомере и Библии. Поэтому в той мере, в какой ''МЧ-М'' написан о людях, он устаревает медленно>.

Автор излагает материал очень красочно, сочно, в целом абсолютно не возникает мысли о том, что читаешь техническую литературу. А насколько актуальны его мысли! Ведь мы и теперь любим свою профессию за те же самые радости, мы печалимся от тех же самых проблем, наши проекты все также погрязают в смоляных ямах, мы остаемся все такими же оптимистами и точно так же ошибаемся в своих оценках, надеясь ''что все будет идти хорошо''. Мы точно также осуществляем ''свои построения на основе чистого мышления'', точно так же робеем в своих оценках перед заказчиком и точно также читаем ''Мифический человеко-месяц'' в ''попытке проложить какие-то мостки через это болото''.

Я не хочу приводить весь перечень цитат, которые произвели на меня впечатление, потому что они займут с десяток страниц. Читать книгу очень интересно еще и по той причине, что на страницах автор заложил очень многое, что стало практикой программирования только сегодня. Так, автор описывает самодокументирование программ, которое позже мы увидим в работах МакКоннелла, Ханта и Томаса. Он говорит о важности слияния документации и файлов с исходными текстами. Это именно то, о чем говорит Мейер, и что мы видим в современных средах разработки. Брукс также поднимает вопросы сложности программных систем, которые вытекают из самой природы программного обеспечения, а также модель пошагового создания программных систем. Это именно те проблемы, которые в последствие неоднократно поднимут Буч, Джейкобсон и Рамбо в своих работах. Он уже тогда говорит о необходимости постоянного уточнения требований, о том, что пользователь не сможет, даже если захочет, предоставить полные и непротиворечивые требования, а также о пользе макетирования. Об этом позднее будут говорить Шаллоуей и Тротт и многие другие авторы.

Естественно есть и достаточное количество архаизмов. Все-таки, слишком многое изменилось в мире программного обеспечения за это время. Но это нисколько не раздражает и не расстраивает, а наоборот придает некоторый особый шарм чтению. Ведь только подумайте, книга основана на опыте разработки программ, имевшем место в 60-х годах, написана в середине 70-х, в главе 16 перепечатывается статья ''Серебряной пули нет'', датированная серединой 80-х, второе издание вышло в середине 90-х, а сейчас уже вторая половина следующего десятилетия. Таким образом, мы можем проследить историю отрасли за последние 40 лет!

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

понедельник, 23 февраля 2009 г.

Книга Джона Роббинса "Отладка приложений для Microsoft .NET"

imageНе зависимо от используемого языка программирования и операционной системы, среды разработки и методологии, вашего опыта и профессионализма вашей команды, вы рано или поздно столкнетесь с одной из самых неприятных сторон нашей работы – с ошибками.

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

«Ошибки – это круто! Они помогают залезть в самую глубину и понять, как работают вещи. Мы все попали в этот бизнес, потому что нам нравится учиться, выслеживание ошибок – неотъемлемая часть обучения… Ведь так здорово бывает найти и исправить ошибку! Конечно же, самые хорошие ошибки – это те, которые обнаруживаются до того, как заказчик увидит ваш продукт. Таким образом, вы должны успевать сделать свою работу и найти ошибки до того, как это сделают ваши заказчики. Видеть, как заказчики обнаруживают ошибки, - это совершенно не круто».

Поскольку процесс поиска и исправления ошибок является такой же неотъемлемой частью разработки программного обеспечения, как и анализ требований, проектирование, кодирование и тестирование, польза этой книги очевидна. Очень интересной является первая глава книги, в которой автор рассказывает о типах ошибок и причинах их возникновения. Ведь с многими причинами, такими как нереальные сроки выполнения проекта или подход «сначала кодируем, потом думаем», сталкиваются большинство из нас. И хотя сам факт правоты автора никак не повлияет на судьбу простого разработчика, вполне вероятно, что уже сейчас вы способны решать вопросы, влияющие на качество вашего продукта, и тогда советы автора окажутся просто бесценными.

Во второй главе, «Подготовка к отладке», автор описывает основополагающие вещи, без которой не то что отладка, а сам процесс разработки практически невозможен. Здесь речь идет о системах управления версиями и отслеживания ошибок, о важности модульных и дымовых (smoke) тестов, о важности отладочных символов и хранилища символов, о предупреждениях компилятора и средствах анализа кода. Все это (или многое) уже давно известно многим из нас. Но это позволит критическим взглядом окинуть ваш процесс разработки и сделать соответствующие выводы.

Третья глава посвящена «упреждающему программированию» или «отладке во время кодирования», а именно утверждениям, трассировке и комментированию кода. Наиболее интересным является обзор и реализация инструмента SUPERASSERT.NET, который способен в значительной степени облегчить процесс последующего поиска ошибок.

Далее, автор касается чрезвычайно широкого спектра вопросов. Это и отладка запуска служб, и решение проблем, связанных с многопоточностью и отладкой взаимоблокировок, и реализация обработки исключений, и решение проблем с загрузкой сборок. Также описаны расширенные возможности отладки с использованием Visual Studio, утилиты WinDBG, SOS, FxCop и даже написание собственных правил Code Analysis.

Автор дает ответы на многие вопросы, связанные с разработкой и отладкой программного обеспечения. Но что еще больше впечатляет, так это перечень тем, которые автор поднимает на страницах своей книги. Книга не отличается глубиной изложения, в ней не рассматривается во всех подробностях IL, работа компилятора или внутренности среды CLR. Самым полезным является расширение вашего кругозора, возможность проанализировать собственный опыт и более подробно изучить хромающую область.

Омрачает эту светлую картину только одна проблема – перевод. В книге хватает как откровенных ляпов, так и просто некорректно переведенных терминов или фраз. В книге встречаются переходные разработчики (intermediate developer), собственные приложения (native applications), вползание функций (feature creep), язык посредника (intermediate language) и многое другое. Нельзя сказать, что подобных проблем слишком много или что они сильно сказываются на восприятии материала, но все же издательству следует подходить к таким вопросам более серьезно.

Несмотря на это книга, безусловно, стоит затраченных средств и усилий и будет полезна большинству разработчиков. И самое главное, «помните, что отладчик – это всего лишь инструмент, как, например, отвертка. Он делает только то, что вы приказываете ему делать. Настоящий отладчик находится у вас в голове».

пятница, 20 февраля 2009 г.

List Visualizer и сериализация с использованием суррогатов

Введение

Отладчик Visual Studio предоставляет множество полезных инструментов, без которых сложно себе представить разработку сложных коммерческих приложений. Одним из главных инструментов в процессе отладки являются окна семейства Watch, предназначенные для отображения и редактирования текущего состояния объектов. С его помощью вы можете увидеть значение любого поля или свойства, независимо от того, насколько сложным является объект. Но, как и любой механизм общего назначения, окна семейства Watch содержат ряд ограничений, существенно усложняющих процесс отладки.. Для просмотра и редактирования сложных объектов, разработчики отладчика Visual Studio создали механизм визуализаторов (Visualizer), способных представлять данные объектов в их естественной форме. В комплекте Visual Studio поставляются визуализаторы строковых типов данных (Text Visualizer, Xml Visualizer и Html Visualizer), а также визуализаторы контейнеров ADO.NET (DataSet Visualizer, DataTable Visualizer, DataView Visualizer и DataViewManager Visualizer). Но значительно более важным является возможность добавления собственных визуализаторов для создания в отладчике альтернативных представлений данных в удобном пользовательском интерфейсе.

Архитектура визуализаторов

Архитектура визуализаторов основана на том, что в процессе отладки участвуют две составляющие: сторона отладчика (Debugger Side) – код, работающий под управлением Visual Studio (окна Watch, DataTips, QuickWatch и др.) и отлаживаемая сторона (Debuggee Side) – код, который вы отлаживаете (ваша программа). Алгоритм работы визуализатора следующий. Вначале отладчик должен загрузить классы визуализаторов, которые располагаются в одном из двух каталогов: каталог_установки_Visual_studio\Common7\ Packages\Debugger\Visualizers, для загрузки визуализаторов, доступных всем пользователям; \Documents and Setting\%profile%\My Documents\Visual Studio\Visualizers, для загрузки визуализаторов, доступных только текущему пользователю. Отладчик узнает, что сборка содержит визуализатор, когда в сборке есть хотя бы один атрибут DebuggerVisualizerAttribute. Этот атрибут сообщает отладчику класс визуализатора, класс, ответственный за передачу данных между Debuggee Side и Debugger Side, тип объекта, предназначенного для отображения и редактирования, а также описание визуализатора. Когда в окне семейства Watch выводится значение, для типа которого определен визуализатор, то в столбце Value будет находиться значок увеличительного стекла. Если щелкнуть на нем, отладчик выберет и запустит последний визуализатор, который использовался для данного класса (рисунок 1).
Рисунок 1 – Визуализатор класса string
После активации визуализатора отладчик сериализует объект на отлаживаемой стороне с использованием класса, указанного в атрибуте DebuggerVisualizerAttribute. Обычно для этих целей используется класс VisualizerObjectSource, который для сериализации/десериализации использует BinaryFormatter. Затем состояние объекта в сериализованной форме передается стороне отладчика, где он десериализуется и отображается в окне пользовательского интерфейса. Если визуализатор предназначен не только для отображения, но и для изменения объекта, этот процесс повторяется в обратном порядке, после чего измененный объект передается на отлаживаемую сторону и заменяет исходный объект.

Создание простого визуализатора

Теперь перейдем к реализации простого визуализатора, предназначенного для отображения списка объектов.
[assembly: DebuggerVisualizer(
  //Класс визуализатора
  typeof(ListVisualizer.SerializableListVisualizer), 
  //Класс, осуществляющий передачу данных между Debuggee Side и Debugger Side
  typeof(VisualizerObjectSource), 
  //Тип объекта, предназначенного для отображения 
  // и редактирования визуализатором
  Target = typeof(List<>), 
  //Текстовое описание, которое будет видеть пользователь 
  //при выборе вашего визуализатора
  Description = "List Visualizer (for serializable data ONLY!)"
  )]
namespace ListVisualizer
{
  /// <summary>
  /// Получает данные от отлаживаемой программы. Отображает их.
  /// "Отправляет" измененные данные обратно.
  /// </summary>
  public class SerializableListVisualizer : DialogDebuggerVisualizer
  {
    protected override void Show(
      IDialogVisualizerService windowService, 
      IVisualizerObjectProvider objectProvider)
    {
      IList list = (IList)objectProvider.GetObject();
 
      Debug.Assert(list != null, "list != null");
 
      if (list != null)
      {
        using (var form =
            new ListVisualizerForm(list, objectProvider.IsObjectReplaceable))
        {
          if (windowService.ShowDialog(form) == DialogResult.OK)
          {
            if (objectProvider.IsObjectReplaceable)
            {
              var ms = new MemoryStream();
              VisualizerObjectSource.Serialize(ms, form.List);
              objectProvider.ReplaceData(ms);
            }
          }
        }
      }
 
    }
 
    /// <summary>
    /// Предназначен для тестирования. Может быть использован в
    /// модульных тестах, консольных приложениях etc.
    /// </summary>
    /// <param name="objectToVisualize">
    /// Данные, необходимые для визуализации</param>
    public static void TestListVisualizer(object objectToVisualize)
    {
      var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, 
                                 typeof(SerializableListVisualizer));
      visualizerHost.ShowVisualizer();
    }
  }
}
Вверху файла находится атрибут DebugerVisualizerAttribute, который отладчик ищет в момент загрузки визуализатора. Как уже отмечалось выше, данный атрибут содержит 4 параметра: класс визуализатора, класс, предназначенный для поддержки сериализации, тип объекта, для которого предназначен данный визуализатор, а также описание визуализатора.
 
ПРИМЕЧАНИЕ
В качестве свойства Target атрибута DebuggerVisualizerAttribute необходимо указывать класс объекта, предназначенного для редактирования и отображения визуализатором. В таком случае визуализатор будет доступен для объектов указанного класса, а также для всех объектов производных классов. В свойстве Target нельзя указать тип интерфейса. В нашем примере следующее значение свойства Target недопустимо: Target = typeof(IList<>).
 
Сам класс визуализатора, являющийся наследником DialogDebuggerVisualizer, содержит единственный метод Show, который и реализует всю работу визуализатора. В первой строке вызывается метод objectProvider.GetObject() с помощью которого визуализатор получает данные, необходимые для отображения. Затем создается форма, которая отображается с использованием интерфейса IDialogVisualizerService после чего проверяется возможность редактирования данных с помощью свойства IsObjectReplaceable интерфейса IVisualizerObjectProvider, и если такая возможность присутствует – вызываю метод ReplaceData, для замены данных в отлаживаемой программе. Второй метод класса – SerializableListVisualizer TestListVisualizer предназначен для упрощения задачи тестирования визуализатора, и может вызываться из консольного приложения или модульного теста. После копирования сборки визуализатора (со всеми зависимостями) в одну из соответствующих папок (речь о которых шла выше) данный визуалитор можно будет использовать в любом проекте Visual Studio в последующих сеансах отладки. Поскольку SerializableListVisualizer для передачи данных между процессами использует VisualizerObjectSource, который (как уже говорилось выше) в свою очередь использует BinaryFormatter для сериализации/десериализации объектов, то данный визуализатор будет работать только с объектами, помеченными атрибутом SerializableAttribute. Однако при попытке использовать данный визуализатор с классом, не помеченным атрибутом SerializableAttribute (и не реализующим интерфейс ISerializable), вы получите исключение, в котором говорится о том, что указанный класс не является сериализуемым. Для тестирования работы визуализатора воспользуемся следующим тестовым классом:
[Serializable]
public class SomeSerializableClass
{
    public string S1 { get; set; }
    public string S2 { get; set; }
    public int I1 { get; set; }
}
Рисунок 2. List Visualizer для сериализиуемых данных.

Сериализация с использованием суррогатов

Хотя класс SerializableListVisualizer является полноценным визуализатором списка объектов, его практическое применение слишком ограничено. Мало кто согласится добавить атрибут SerializableAttribute к своему классу только для того, чтобы объекты этого класса можно было посмотреть в красивом виде. Поэтому необходимо как-то обойти это досадное ограничение, и все же реализовать возможность отображения и редактирования списков несериализуемых объектов. Архитектура визуализаторов предусматривает возможность вмешаться в процесс сериализации и десериализации путем создания наследника от VisualizerObjectSource и указания этого типа в атрибуте DebuggerVisualizerAttribute. Таким образом, решение задачи отображения и редактирования несереализуемых объектов по сути своей, сводится к решению задачи сериализации и десериализации несериализируемых объектов. Инфраструктура сериализации в .Net Framework предусматривает возможность «делегирования» полномочий по сериализации некоторого объекта другим объектам. Для этого необходимо определить «суррогатный тип» («surrogate type»), который возьмет на себя операции сериализации и десериализации существующего типа (путем реализации интерфейса ISerializationSurrogate). Затем необходимо зарегистрировать экземпляр суррогатного типа в форматирующем объекте, сообщая ему, какой тип подменяется суррогатным. Когда форматирующий объект обнаруживает, что выполняется сериализация или десериализация экземпляра существующего типа, он вызывает методы, определенные в соответствующем суррогатном типе. Предположим, существует некоторый несериализуемый класс следующего вида:
public class NonSerializableClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Класс не помечен атрибутом SerializableAttrubute и не реализует интерфейс ISerializable, т.е. не предусматривает сериализацию своих экземпляров. Это ограничение можно обойти, создав суррогатный тип, который возьмет на себя ответственность за сериализацию и десериализацию экземпляров указанного типа. Для этого нужно создать класс, реализующий интерфейс ISerializationSurrogate, который определен следующим образом:
public interface ISerializationSurrogate
{
    void GetObjectData(object obj,
      SerializationInfo info, StreamingContext context);
 
    object SetObjectData(object obj,
      SerializationInfo info, StreamingContext context,
      ISurrogateSelector selector);
}
Этот интерфейс аналогичен интерфейсу ISerializable. Отличие состоит в том, что методы интерфейса ISerializationSurrogate принимают дополнительный параметр – ссылку на реальный объект, подлежащий сериализации. Поскольку сам класс NonSerializableClass достаточно прост, то и реализация соответствующего суррогата будет простой. В методе GetObjectData первый параметр нужно привести к соответствующему типу и сохранить все поля в объекте SerializationInfo. Для десериализации объекта вызывается метод SetObjectData, при этом ссылка на десериализуемый объект возвращается статическим методом GetUnitializedObject, принадлежащим FormatterServices. Т.е. все поля объекта перед десериализацией пусты и для объекта не вызван никакой конструктор. Задача метода SetObjectData – инициализировать поля объекта, получая значения из объекта SerializationInfo.
public class NonSerializableClassSurrogate : ISerializationSurrogate
{
    public void GetObjectData(
      object obj, SerializationInfo info, StreamingContext context)
    {
        var nonSerializable = (NonSerializableClass)obj;
        info.AddValue("Id", nonSerializable.Id);
        info.AddValue("Name", nonSerializable.Name);
    }
 
    public object SetObjectData(
      object obj, SerializationInfo info,
      StreamingContext context, ISurrogateSelector selector)
    {
        var nonSerializable = (NonSerializableClass)obj;
        nonSerializable.Id = info.GetInt32("Id");
        nonSerializable.Name = info.GetString("Name");
        return obj;
    }
}
Единственная проблема, которая может возникнуть при создании суррогатных типов даже для простых объектов – это создание суррогатов для value-типов. Проблема в том, что первый параметр метода SetObjectData относится к типу Object, т.е. value-тип будет передан в упакованном виде, а в таких языках программирования как C# и Visual Basic просто не предусмотрена возможность изменения свойств непосредственно в упакованном объекте. Единственный способ сделать это – воспользоваться механизмом рефлексии (reflection) следующим образом:
public object SetObjectData(
  object obj, SerializationInfo info, 
  StreamingContext context, ISurrogateSelector selector)
{
  typeof(NonSerializableClass).GetProperty("Id").SetValue(
    obj, info.GetInt32("Id"), null);
  typeof(NonSerializableClass).GetProperty("Name").SetValue(
    obj, info.GetString("Name"), null);
  return obj;
}
Использование суррогатного типа следующее:
//Создание объекта, подлежащего сериализации
var ns1 = new NonSerializableClass { Id = 47, Name = "TestName" };
var formatter = new BinaryFormatter();
var ss = new SurrogateSelector();
// Зарегистрировать суррогатный класс
ss.AddSurrogate(typeof(NonSerializableClass),
    new StreamingContext(StreamingContextStates.All),
    new NonSerializableClassSurrogate());
// Указать селектор
formatter.SurrogateSelector = ss;
 
using (var ms = new MemoryStream())
{
    //Сериализирую объект класса NonSerializableClass
    formatter.Serialize(ms, ns1);
    //Устанавливаю в 0 позицию в потоке MemoryStream
    ms.Position = 0;
    //Десериализирую объект класса NonSerializableClass
    var ns2 = (NonSerializableClass)formatter.Deserialize(ms);
    //Осталось проверить правильность сериализации и десериализации
    Assert.AreEqual(ns1.Id, ns2.Id);
    Assert.AreEqual(ns1.Name, ns2.Name);
}
Теперь перейдем к реализации суррогатного типа, осуществляющего сериализацию/десериализацию несериализируемых типов. Основная работа по сериализации объекта осуществляет функция SerializeFields. Ее реализация основана на использовании механизма рефлексии, с помощью которого я получаю все поля объекта и, если поле является сериализуемым, добавляю значение поля в объект SerializationInfo. Поскольку я получаю только поля объекта, объявленные в текущем типе, функцию SerializeFields нужно вызвать рекурсивно для всех базовых классов сериализуемого объекта. Рекурсия останавливается при достижении класса Object. Десериализация осуществляется с помощью функции DeserializeFields и ее реализация является аналогичной. Ограничением данной реализации является то, что если сериализуемый объект в качестве поля будет содержать объект несериализуемого типа, то это поле останется неинициализированным, что в некоторых случаях может привести к непредсказуемому поведению.
/// <summary>
/// "Суррогат" сериализирует все сериализируемые поля объекта
/// </summary>
public class NonSerializableSurrogate : ISerializationSurrogate
{
    public void GetObjectData(
      object obj, SerializationInfo info, StreamingContext context)
    {
        SerializeFields(obj, obj.GetType(), info);
    }
 
    public object SetObjectData(
      object obj, SerializationInfo info,
      StreamingContext context, ISurrogateSelector selector)
    {
        DeserializeFields(obj, obj.GetType(), info);
        return obj;
    }
 
    private static void SerializeFields(
      object obj, Type type, SerializationInfo info)
    {
        // Попытка сериализации полей типа Object 
        // является ограничением рекурсии
        if (type == typeof(object))
            return;
 
        // Получаю все экземплярные поля, 
        // объявленные в объекте текущего класса
        var fields = type.GetFields(Flags);
        foreach (var field in fields)
        {
            // Игнорирую все несериализируемые поля
            if (field.IsNotSerialized)
                continue;
 
            var fieldName = type.Name + "+" + field.Name;
            // Добавляю значение поля в объект SerializationInfo
            info.AddValue(fieldName, field.GetValue(obj));
        }
        // Сериализирую базовую составляющую текущего объекта
        SerializeFields(obj, type.BaseType, info);
    }
 
    private static void DeserializeFields(
      object obj, Type type, SerializationInfo info)
    {
        // Попытка сериализации полей типа Object 
        // является ограничением рекурсии
        if (type == typeof(object))
            return;
 
        // Получаю все экземплярные поля, объявленные в объекте текущего класса
        var fields = type.GetFields(Flags);
 
        foreach (var field in fields)
        {
            // Игнорирую все несериализируемые поля
            if (field.IsNotSerialized)
                continue;
            var fieldName = type.Name + "+" + field.Name;
            // Получаю значение поля из объекта SerializationInfo
            var fieldValue = info.GetValue(fieldName, field.FieldType);
            // Устанавливаю значение соответствующего поля объекта
            field.SetValue(obj, fieldValue);
        }
        // Десериализирую базовую составляющую текущего объекта
        DeserializeFields(obj, type.BaseType, info);
    }
 
    private const BindingFlags Flags = BindingFlags.Instance
                                     | BindingFlags.DeclaredOnly
                                     | BindingFlags.NonPublic
                                     | BindingFlags.Public;
}
Для простоты использования класса NonSerializableSurrogate создадим соответствующий селектор (класс, реализующий интерфейс ISurrogateSelector), который будет возвращать NonSerializableSurrogate только при попытке сериализации класса, не поддерживающего сериализацию.
/// <summary>
/// Реализует выбор необходимого суррогата
/// </summary>
public class NonSerializableSurrogateSelector : ISurrogateSelector
{
    public void ChainSelector(ISurrogateSelector selector)
    {
        throw new NotImplementedException();
    }
 
    public ISurrogateSelector GetNextSelector()
    {
        throw new NotImplementedException();
    }
 
    public ISerializationSurrogate GetSurrogate(
      Type type, StreamingContext context, out ISurrogateSelector selector)
    {
        //Для несерилазируемых типов возвращаю суррогат, который
        //сериализирует все сериализуемые поля объекта
        selector = null;
        if (type.IsSerializable)
            return null;
        selector = this;
        return new NonSerializableSurrogate();
    }
 
}
Пример использования классов NonSerializableSurrogate и NonSerializableSurrogateSelector:
// Создание объекта, подлежащего сериализации
var ns1 = new NonSerializableClass { Id = 47, Name = "TestName" };
var formatter = new BinaryFormatter();
formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
 
using (var ms = new MemoryStream())
{
    // Сериализирую объект класса NonSerializableClass
    formatter.Serialize(ms, ns1);
    ms.Position = 0;
 
    // Десериализирую объект класса NonSerializableClass
    var ns2 = (NonSerializableClass)formatter.Deserialize(ms);
    // Осталось проверить правильность сериализации и десериализации
    Assert.AreEqual(ns1.Id, ns2.Id);
    Assert.AreEqual(ns1.Name, ns2.Name);
}

Реализация визуализатора списка объектов, не поддерживающих сериализацию

Для реализации визуализатора списка объектов, не поддерживающих сериализацию, необходимо реализовать класс-наследник от VisualizerObjectSource, который с помощью суррогатного типа, определенного в предыдущем разделе, будет заниматься сериализацией/десериализацией списка объектов, не поддерживающих сериализацию.
/// <summary>
/// Предназначен для сериализации списка объектов
/// </summary>
public class ListVisualizerObjectSource : VisualizerObjectSource
{
    public override void GetData(object target, System.IO.Stream outgoingData)
    {
        var list = target as IList;
        if (list == null)
            return;
 
        SerializeList(list, outgoingData);
    }
 
    public override object CreateReplacementObject(
      object target, Stream incomingData)
    {
        return DeserializeList(incomingData);
    }
 
    public static IList DeserializeList(Stream stream)
    {
        var formatter = new BinaryFormatter();
        formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
        return (IList)formatter.Deserialize(stream);
    }
 
    public static Stream SerializeList(IList list)
    {
        var stream = new MemoryStream();
        SerializeList(list, stream);
        return stream;
    }
 
    public static Stream SerializeList(IList list, Stream stream)
    {
        IFormatter formatter = new BinaryFormatter();
        formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
        formatter.Serialize(stream, list);
        return stream;
    }
 
}
Реализовать визуализатор на основе уже разработанных классов совсем несложно.
[assembly: DebuggerVisualizer(
  // Класс визуализатора
  typeof(ListVisualizer.ListVisualizer), 
  // Класс, осуществляющий передачу данных 
  // между Debuggee Side и Debugger Side
  typeof(ListVisualizer.ListVisualizerObjectSource), 
    // Тип объекта, предназначенного для отображения 
    // и редактирования визуализатором
    Target = typeof(List<>), 
      //Текстовое описание, которое будет видеть пользователь 
      // при выборе вашего визуализатора
      Description = "Cool List Visualizer" 
  )]
 
namespace ListVisualizer
{
  public class ListVisualizer : DialogDebuggerVisualizer
  {
    protected override void Show(
      IDialogVisualizerService windowService, 
      IVisualizerObjectProvider objectProvider)
    {
      IList list = ListVisualizerObjectSource.DeserializeList(
        objectProvider.GetData());
 
      Debug.Assert(list != null, "list != null");
 
      if (list != null)
      {
        using (var form =
            new ListVisualizerForm(list, objectProvider.IsObjectReplaceable))
        {
          if (windowService.ShowDialog(form) == DialogResult.OK)
          {
            if (objectProvider.IsObjectReplaceable)
            {
              objectProvider.ReplaceData(
                ListVisualizerObjectSource.SerializeList(form.List));
            }
          }
        }
      }
 
    }
 
    public static void TestShowVisualizer(object objectToVisualize)
    {
      VisualizerDevelopmentHost visualizerHost = 
        new VisualizerDevelopmentHost(
          objectToVisualize, typeof(ListVisualizer), 
          typeof(ListVisualizerObjectSource));
 
      visualizerHost.ShowVisualizer();
    }
  }
}
Осталось скопировать полученную сборку в папку визуализаторов и запустить отладку. Для проверки работы визуализатора будем использовать несериализируемый класс следующего вида:
public class NonSerializableClass
{
    public NonSerializableClass()
    {
        Time = DateTime.Now;
    }
 
    public string S1 { get; set; }
    public string S2 { get; set; }
    public int I1 { get; set; }
    public DateTime Time { get; set; }
}
Рисунок 3 – List Visualizer для списков сериализуемых и несериализуемых объектов

Выводы

В этой небольшой статье я рассмотрел два, казалось бы, совершенно не связанных вопроса: реализация собственных визуализаторов и сериализацию с использованием суррогатов. Это связано с тем, что для работы визуализатора требуется сериализация/десериализация объектов между двумя процессами: процессом отладчика и процессом отлаживаемого кода. Наличие в арсенале разработчика визуализатора списка объектов может существенно упростить отладку и просмотр состояния объектов на этапе выполнения. Но ограничить себя просмотром и изменением только сериализуемых объектов — значит отказаться от этого инструмента в 90% случаев. Поэтому я предпринял попытку обойти это ограничение и реализовать более универсальный визуализатор, предназначенный для работы со списками как сериализируемых, так и не сериализируемых объектов.

воскресенье, 15 февраля 2009 г.

Создание идентификатора объекта в окнах семейства Watch в VS2005/2008

В отладчике Visual Studio 2005/2008 при работе с языком программирования C# появилась новая замечательная возможность: создание идентификатора объекта. Если в одном из окон семейства Watch (имеются ввиду окна Locals, Autos, Quick Watch, Watch1-5, DataTips) нажать правую кнопку, то в контекстном меню будет присутствовать один интересный пункт: Make Object ID - создание идентификатора объекта.

При создании идентификатора объекта, создается идентификатор, который отображается в столбце Value окна Watch. После создания идентификатора, вы можете добавить новое значение в таблицу Watch с именем '#1', '#2', ... '#n'.

Идентификатор объекта позволяет управляемому отладчику следить за объектом в любом месте кучи, независимо от контекста. Таким образом, если вам необходимо наблюдать локальную переменную из метода на двадцать элементов выше стека, создайте идентификатор объекта для этого объекта, и вы будете видеть реальное значение переменной независимо от того, где вы находитесь. Можно также создать идентификатор объекта для объекта в других потоках. Более того, вы увидите этот объект не зависимо от того, в каком поколении сборщика мусора он находится, а если он будет собран сборщиком мусора, то представление для этого идентификатора объекта станет недоступным. К сожалению нельзя добавить никакой дополнительной информации к идентификатору объекта и придется помнить, что означает порядковый номер каждого идентификатора, но т.к. слишком большое количество подобных объектов едва ли понадобиться, то и слишком большой проблемой это не будет.

суббота, 14 февраля 2009 г.

Трассировочные точки (Tracepoints)

Вы, вероятно, уже знаете о таких замечательных возможностях контрольных точек (или точек останова, breakpoints) в Visual Studio, как счетчики срабатываний (hit count) и условные выражения (conditional expression). Если вы не знакомы с этими понятиями, достаточно в редакторе поставить breakpoin, а затем в окне Breakpoints в контексном меню соответствующей точки выбрать Hit Count или Condition соответственно. Счетчик срабатываний говорит отладчику, что контрольную точку нужно установить в коде, но не останавливаться на ней до тех пор, пока код не будет выполнен определенное число раз. Контрольная точка для которой определено условное выражение останавливает отладчик только в том случае, если условное выражение возвращает true.
 
Помимо этих замечательных возможностей в Visual Studio 2005 появилась новая возможность отладки - трассировочные точки, которые являются всего-лишь особой разновидностью контрольных точек. Назначение трассировочных точек - вывод трассировочной информации без модификации исходного кода.
 
Для того, чтобы установить трассировочную точку, вначале нужно поставить обыкновенную контрольную точку в некотором месте исходного кода, затем в окне Breakpoints в контекстном меню выберите команду When Hit. Затем в диалоговом меню When Breakpoint Is Hit можно указать диагностическое сообщение, которое будет выводиться каждый раз, когда отладчик будет выполнять указанную строку кода. Кроме того, если установить флажок Continue Execution, то отладчик не будет прекращать выполнение программы при проходе через контрольную точку. В поле "Print a message" диалогового окна When Breakpoint is Hit вы можете обращаться к любому коду, к которому вы бы могли получить доступ в той строке кода, где установлена контрольная точка, с единственной поправкой: все обращения должны быть заключены в {}.
 
Вот пример: {DateTime.Now.ToString("yyyy.MM.dd HH:mm:ss.fff")}: Function: $FUNCTION, foo: {foo()}, x: {x}
 
В результате выполнения в окне Output может быть следующий результат:
 
"2009.02.14 23:53:41.968": Function: Program.TestFunction(), foo: "hello from foo()", x: 123
 
Помимо обращения к функциям и переменным, разработчики Visual Studio предусмотрели набор предустановленных макросов, таких как: $FUNCTION, $CALLER, $CALLSTACK, $TID, $TNAME и др. Т.о. если вам понадобится трассировка приложения, не нужно сразу же весь код засыпать вызовами Trace или другими средствами логгирования, возможно будет достаточно воспользоваться трассировочными точками.

Short circuit (сокращенное вычисление булевых выражений)

Многие знают о сокращенном вычислении булевых выражений, которые существуют в C, С++, C#, Java и других языках программирования. Имеется ввиду такая ситуация: 1. if ( firstCondition || secondCondition) 2. If (firstCondition && secondCondition ) В первом случае, если вычисление первого выражения даст true, то второе выражение не будет вычислено вообще. Во втором случае ситация аналогична: если вычисление первого выражение дает false, то результатом всего выражения в любом случае будет false, поэтому второе выражение вычислять не обязательно. Таким образом данные операторы не являются коммутатитвными, поскольку результат выражения A && B может быть определен, в то время, как результат выражения B && A, может быть неопределенным. Простой пример отсутствия коммутативности следующий: 1. if ( i != 0 && k/i > 10 ) 2. if ( k/i > 10 && i != 0 ) Эти два выражения не являются эквивалентными, т.к. во втором случае возможно деление на 0. Данное поведение противоречит теории, с той точки зрения, что стандартные математические булевы операторы являются комутативными. Т.е. а and b всегда имеет тоже значение, что и b and a, в то время как a && b не всегда имеет тоже значение, что и b && a. Но, вероятно, не многие знают, что во всех этих языках можно несколько "обойти" поведение по умолчанию и добиться коммутативности выполнения булевых операций. Для этого необходимо воспользоваться битовыми операциями. 1. if ( firstCondition & secondCondition ) 2. if ( firstCondition | secondCondition ) В этом случае все условные выражения будут вычисляться всегда, тем самым получая коммутативность булевых операторов. "Белой вороной" (как и во многих других вопросах) выглядит язык программирования Eiffel. В этом языке программирования основные булевы операторы and и or являются "строгими" (коммутативными). Их поведение совпадает с поведением битовых операторов | и &. При этом есть дополнительные булевы операторы and then и or else, которые ведут себя аналогично булевым операторам языков семейства С.