суббота, 25 апреля 2009 г.

О выборе языка программирования

Данное сообщение навеяно очередной "войной" на rsdn.ru о выборе между С++ и C#.  

Мне кажется, что языки, используемые нами для выражения своих идей, становятся частью нас самих, поэтому, если вы знаете только один язык, может показаться, будто сторонники других языков представляют опасность лично для вас. Мне представляется, что выход из этой ситуации – освоение других языков. Сомневаюсь, что можно быть профессионалом в области ПО и знать лишь один язык программирования. Может быть и экономическая причина: фундаментальные знания выходят за границы языка программирования в отличие от многих практических навыков. Поэтому, если я знаю только язык X и его наборы инструментов, а вы являетесь сторонником языка Y и его наборов инструментов, вы представляете угрозу для источника моего заработка. И вновь решение, как мне кажется, - в знании нескольких языков и наборов инструментов (а также твердое понимание фундаментальных концепций).
Интервью++: Бьерн Страуструп об эволюции языков  

Портфелем знаний мы предпочитаем называть все факты, известные программистам об информатике, области приложений, в которых они работают, и накопленный ими опыт. Управление портфелем знаний очень похоже на управление финансовыми портфелем. Предложения по поддержке портфеля знаний:
- Учите (как минимум) по одному языку программирования каждый год.  
- Читайте по одной технической книге ежеквартально.   
- Читайте книги, не относящиеся к технической литературе.  
- Экспериментируйте с различными операционными средами.
Важно продолжать инвестирование. Как только вы почувствуете, что освоили новый язык или фрагмент технологии, двигайтесь дальше. Изучайте другой. Неважно, будете ли вы когда-либо использовать одну из этих технологий в проекте или даже не упомянете о них в своем резюме. Процесс обучения расширит ваше мышление, открывая вас для новых возможностей и новых путей в творчестве. "Перекрестное опыление" идей важно; попытайтесь применить выученные уроки к проекту, над которым вы работаете в настоящее время. Даже если в вашем проекте не используется некая технология, вы наверняка сможете позаимствовать некоторые идеи.
Эндрю Хант и Дэвид Томас. Программист-прагматик. Путь от подмастерья к мастеру  

Как сказал Дэвид Грайс (Gries) подход к программированию не должен определяться используемыми инструментами. В связи с этим он проводит различие между программированием на языке (programming in lanuage) и программирование с использованием языка (programming into language). 
Разработчики, программирующие «на» языке, ограничивают свое мышление конструкциями, непосредственно поддерживаемых языком. Если предоставляемые языком средства примитивны, мысли программистов будут столь же примитивными. Разработчики, программирующие «с использованием» языка, сначала решают, какие мысли они хотят выразить, после чего определяют, как выразить их при помощи конкретного языка.
Стив Макконнелл. Совершенный код.

Я привел три цитаты, каждая из которых говорит о второстепенной роли языка программирования в жизни разработчика. Причем автор одной из них имеет непосредственное отношение к одному из языков программирования!
Конечно же, я не придерживаюсь мысли, что язык программирования не играет никакой роли. Нет, это не так.
Я не считаю, что не нужно углублять свои знания в конкретном языке или технологии; не считаю, что нужно выбросить книги Герба Саттера и Андея Александреску, Джошуа Блоха и Билла Вагнера, и не зачитываться этюдами nikov-а на rsdn-е. Все это очень полезно. Но польза не только в том, что эти знания вы можете использовать сейчас на текущем проекте с текущим языком и технологией, а в том, что эти знания позволят перейти вам на следующий уровень своего развития и решать более сложные задачи в следующих проектах, уже на совершенно других языках программирования и совершенно других технологиях.

Продолжая рассуждение о языках программирования в жизни разработчика, достаточно вспомнить, что пишет Брукс и Буч о сложности программных систем:  
Эйнштейн неоднократно утверждал, что природа должна иметь простые объяснения, поскольку Богу не свойственны капризность и произвол. У программиста нет такого утешения: сложность, с которой он должен справиться, лежит в самой природе системы. а затем: Сложность программного обеспечения – отнюдь не случайное его свойство. Сложность вызывается четырьмя основными причинами: сложностью реальной предметной области, из которой исходит заказ на разработку; турдностью управления процессом разработки; необходимостью обеспечить достаточную гибкость программы; неудовлетворительными способами описания поведения больших дискретных систем.

Как вы можете заметить, не один из этих пунктов не содержит фразы: «сложность программного обеспечения обусловлена неверным выбором языка программирования». Кроме того, вам будет довольно сложно найти цитату авторитетного автора, который бы сказал что-то вроде этого: «Изучение языка программирования А – это ваш путь к успеху. Благодаря ему уже через полгода его изучения вас ждет почет и уважение в среде разработчиков по всему миру, вы будете президентом корпорации и будете ездить на Bentley, а вот если ваш выбор остановится на языке программирования Б – вас ждет позор, забвение и жизнь на помойке».

На закуску, хочется привести еще одну цитату Ханта и Томаса:  
Программирование – это прикладное искусство. Простейший его смысл заключается в следующем: заставить компьютер делать то, что вам нужно (или то, что нужно пользователю, работающему с вашей программой). Программист – он и слушатель, он и советник, он и переводчик и даже диктатор. Вы пытаетесь ухватить суть не совсем ясных требований и найти такой способ их выражения, что только машина сможет оценить их по достоинству. Вы пытаетесь задокументировать работу так, чтобы она была понятна другим, спроектировать ее так, чтобы другие могли на нее положиться. Кроме того, вы пытаетесь сделать все это вопреки безжалостному ходу стрелки часов, отсчитывающих время, отпущенное на проект. Каждый день вы совершаете по маленькому чуду. Это непростая работа.
Многие предлагают вам помощь. Фирмы-поставщики инструментальных средств настойчиво говорят о чудесах, которые творят их программы. Мудрецы от методологии заверяют в том, что их средства гарантируют результаты. Каждый считает свой язык программирования лучшим из лучших, а операционную систему – панацеей. Разумеется, эти утверждения неверны. Простых ответов не существует. Не существует такого понятия, как наилучшее решение, будь то инструментальное средство, язык или операционная система. Существует лишь некие системы, которые являются более приемлемыми при конкретном стечении обстоятельств. И в этот момент на сцену выходит прагматизм. 
Стоит избегать обряда венчания с конкретной технологией, но при этом необходимо обладать подготовкой и опытом, настолько обширными, что это позволит выбрать верные решения в конкретных ситуациях. Ваша подготовка происходит из понимания основных принципов информатики, а опыт основывается на разнообразных практических проектах. Теория и практика сочетаются, придавая вам силу.

P.S. Если вы хотите стать хорошим программистом, то нужно решать сложные задачи в интересном коллективе. При этом язык программирования играет лишь второстепенную роль.

вторник, 17 марта 2009 г.

Многопоточность в Windows Forms. Control.Invoke().

Человек даже немного поработав с Windows Forms наверняка сталкивался с замечательным исключением System.InvalidOperationException с таким описанием: {"Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on."} Этим сообщением среда выполнения недвусмысленно дает понять, что обращаться к элементам управления пользовательского интерфейса можно только из того потока, который создал этот элемент управления. Такое поведение обусловлено деталями реализации, в частности использованием однопоточной модели аппартаментов (Single-threaded Apartment, STA) и механизма обработки оконных сообщений. Вот простенький пример кода, который приводит к генерации такого исключения:

public partial class Form1 : Form

{

    public Form1()

    {

        InitializeComponent();

        //на форме расположен TextBox и Button.

        //В TextBox будет выводиться количество сработок таймера,

        //таймер запускается по нажатию на кнопку Button

        timer = new Timer(AsyncHandler);

    }

 

    private void button1_Click(object sender, EventArgs e)

    {

        timer.Change(1000, 1000);

    }

    private void AsyncHandler(object data)

    {

        tickCount++;

        textBox1.Text = tickCount.ToString();

    }

 

    private readonly System.Threading.Timer timer;

    private int tickCount;

}

Существует вполне простой и понятный способ решения этой проблемы, путем проверки свойства элемента управления InvokeRequired с последующим вызовом метода Invoke.
Вот самый простой и примитивный способ:

private void AsyncHandler(object data)

{

    tickCount++;

    Action<int> action = DoChangeTicks;

    if (InvokeRequired)

    {

        Invoke(action, tickCount);

    }

    else

    {

        action(tickCount);

    }

 

}

private void DoChangeTicks(int count)

{

    textBox1.Text = tickCount.ToString();

}

Способ простой, но не совсем удобный. В данном случае вспомогательный метод только увеличивает связность и усложняет понимает решаемой задачи. Если воспользоваться новшествами C#3.0 в виде лямбда-выражений, можно сделать несколько более удобную реализацию следующего вида:

private void AsyncHandler(object data)

{

    tickCount++;

    Action action = () => textBox1.Text = tickCount.ToString();

    if (InvokeRequired)

    {

        Invoke(action);

    }

    else

    {

        action();

    }

}

Уже лучше. Мы избавились от необязательного метода, который неразрывно связан с тем действием, которое выполняет метод AsyncHandler. Но, все же, еще есть над чем подумать.
Следующий вариант решения был честно подсмотрен в неплохой книжке: Bill Wagner "More Effective C#".

/// <summary>

/// Расширения облегчающие работу с элементами управления в многопоточной среде.

/// </summary>

public static class ControlExtentions

{

    /// <summary>

    /// Вызов делегата через control.Invoke, если это необходимо.

    /// </summary>

    /// <param name="control">Элемент управления</param>

    /// <param name="doit">Делегат с некоторым действием</param>

    public static void InvokeIfNeeded(this Control control, Action doit)

    {

        if (control.InvokeRequired)

            control.Invoke(doit);

        else

            doit();

    }

    /// <summary>

    /// Вызов делегата через control.Invoke, если это необходимо.

    /// </summary>

    /// <typeparam name="T">Тип параметра делегата</typeparam>

    /// <param name="control">Элемент управления</param>

    /// <param name="doit">Делегат с некоторым действием</param>

    /// <param name="arg">Аргумент делагата с действием</param>

    public static void InvokeIfNeeded<T>(this Control control, Action<T> doit, T arg)

    {

        if (control.InvokeRequired)

            control.Invoke(doit, arg);

        else

            doit(arg);

    }

}

С помощью этого вспомогательного класса, реализация метода, взаимодействующего с элементами управления из других потоков, будет следующей:

private void AsyncHandler(object data)

{

    tickCount++;

    this.InvokeIfNeeded(

        () => textBox1.Text = tickCount.ToString()

            );

}

вторник, 3 марта 2009 г.

Полезные ссылки от Джона Роббинса

Вот решил собрать полезные ссылки, инструменты и статьи, которые советует Джон Роббинс в своей книге "Отладка приложений для Microsoft .Net".
СТАТЬИ
Оптимизация загрузки приложений Windows Forms
1. Practical Tips For Boosting The Performance Of Windows Forms Apps http://msdn.microsoft.com/en-us/magazine/cc163630.aspx
2. CLR Inside Out. Improving Application Startup Time http://msdn.microsoft.com/en-us/magazine/cc163655.aspx
Пользовательский интерфейс и юзабилити
1. Windows Vista User Experience Guidelines. http://msdn.microsoft.com/en-us/library/aa511258.aspx
2. The Joel Test: 12 Steps to Better Code. http://www.joelonsoftware.com/articles/fog0000000043.html
О финализаторах
1. Keep Your Code Running with the Reliability Features of the .NET Framework. http://msdn.microsoft.com/en-us/magazine/cc163716.aspx
О вреде разработки ПО под учетной записью с правами администратора:
2. MakeMeAdmin -- temporary admin for your Limited User account http://blogs.msdn.com/aaron_margosis/archive/2004/07/24/193721.aspx
Разное
1. SUPERASSERT.NET SUPERASSERT Goes .NET http://msdn.microsoft.com/en-us/magazine/cc188701.aspx
2. Автоматизация тестирования пользовательского интерфейса. Build Quick and Easy UI Test Automation Suites with Visual Studio .NET. http://msdn.microsoft.com/en-us/magazine/cc188784.aspx
4. SOS metadata commands. Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects. http://msdn.microsoft.com/ru-ru/magazine/cc163791(en-us).aspx
5. How do I debug assembles in the Global Assembly Cache (GAC)? http://blogs.msdn.com/junfeng/archive/2005/12/13/503059.aspx
6. Document Your Code in No Time At All with Macros in Visual Studio. http://msdn.microsoft.com/ru-ru/magazine/cc163757(en-us).aspx
8. Speeding up the C# Source Code Editor http://weblogs.asp.net/rweigelt/archive/2006/05/15/446536.aspx
9. Let The CLR Find Bugs For You With Managed Debugging Assistants. http://msdn.microsoft.com/ru-ru/magazine/cc163606(en-us).aspx

БЛОГИ

1. Блог Майкла Столла CLR Debugging Team: http://blogs.gotdotnet.com/jmstall/
2. Блог Марка Руссиновича: http://blogs.technet.com/markrussinovich/
3. Блог Джо Даффи: http://www.bluebytesoftware.com/blog/
4. Блог Tess Ferrandez: http://blogs.msdn.com/tess/
5. Блог Jeff Atwood: http://www.codinghorror.com/blog/
6. Блог Aaron Margosis "Non-Admin" WebLog: http://blogs.msdn.com/aaron_margosis/
7. Блоги Wintellectuals' Blogs: http://www.wintellect.com/cs/blogs/
ИНСТРУМЕНТЫ, УТИЛИТЫ
1. Reflector Лутца Родера
1.1. Сам рефлектор: http://www.aisto.com/roeder/dotnet/
1.2. Denis Bauer Reflector.FileDisassembler. Позволяет дизасемблировать целиком сборку и сохранить ее исходный код. http://www.denisbauer.com/NETTools/FileDisassembler.aspx
1.3. Reflector Diff AddIn. http://www.codingsanity.com/diff.htm
1.4. Kevin Dente's Running Assembly Reflector Add-in. http://Weblogs.asp.net/kdente/articles/438539.aspx На этом перечень AddIn-ов к рефлектору не заканчивается и в интернете можно свободно найти достаточное количество полезных дополнений.
2. Иструменты от Sysinternals. Process Explorer, Process Monitor, DebugView и многое другое. http://technet.microsoft.com/en-us/sysinternals/default.aspx
3. Автономная утилита анализа кода FxCop msdn.microsoft.com/en-us/library/bb429476(VS.80).aspx
4. Debugging Tools for Windows. WinDBG, SOS, ADPlus. www.microsoft.com/whdc/devtools/debugging/ Очень полезные инструменты для отладки, исследования памяти, поиска deadlock-ов и многих других задач.
6. NCover - A test code coverage tool for C# .NET http://ncover.sourceforge.net/
7. Dependency Walker from http://www.dependencywalker.com/
8. Набор инструментов от Sells Brothers. RegexDesigner.NET и другие. http://www.sellsbrothers.com/tools/
9. CopySourceAsHtml (CSAH). Встраиваемый модуль в Visual Studio, который позволяет копировать куски кода виде html. www.jtleigh.com/CopySourceAsHtml/
10. Windows Installer XML (WIX). http://wix.sourceforge.net/ Должно быть интересно посмотреть на бесплатный инструмент от Майкрософт, с помощью которого создаются инсталляционные пакеты для Microsoft Office, Visual Studio и SQL Server. wix.sourceforge.net.
1. GhostDoc. Free add-in for Visual Studio that automatically generates XML documentation comments for C#. http://www.roland-weigelt.de/ghostdoc/
14. Инструменты автоматизации тестирования пользовательского интерфейса: Mercury WinRunner, Rational Robot
15. PInvoke.Net. Do Interop the WIKI way. http://www.pinvoke.net/
17. Regex Kit: Regular Expression Visualizers for VS 2005 http://weblogs.asp.net/rosherove/archive/2005/11/26/AnnoucingRegexKit10.aspx

четверг, 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% случаев. Поэтому я предпринял попытку обойти это ограничение и реализовать более универсальный визуализатор, предназначенный для работы со списками как сериализируемых, так и не сериализируемых объектов.