Самоучитель по VB.NET
Сайт Алексея Муртазина (Star Cat) E-mail: starcat @ nm.ru
Мои программы Новости сайта Мои идеи Мои стихи Форум Моё фото Мой ЖЖ
Заработай!!!VB коды Статьи о VB6 API функции VB.NET
Более 3000 ссылок Интернет Все работы с фото и видео
Сайт о моём деде Муртазине ГР Картинная галерея "Дыхание души"
Звёздный Кот

Самоучитель по VB.NET
Глава 4.

.NET в практическом контексте

Наверное, вам не терпится увидеть хотя бы строчку программного кода. Потерпите еще немного — вскоре мы доберемся и до программ.

Как я уже говорил, если бы эта книга была типичным учебником для начинающих программистов, изучающих VB .NET, она бы имела совершенно иную структуру. Я бы начал с описания основных концепций и синтаксиса языка.

Но эта книга предназначена для программистов VB6 среднего и высокого уровня. Честно говоря, я нисколько не сомневаюсь в том, что вы легко усвоите синтаксис VB .NET. Вы уже знаете, что такое переменные и как работают циклы For...Next. Вы уже умеете работать с библиотеками объектов по прошлому опыту использования ActiveX DLL и элементов ActiveX. Вы уже знаете, как использовать методы и свойства объектов.

Я не стану обижать читателя, ставя под сомнение его квалификацию1, и описывать те аспекты Visual Basic, которые либо совпадают в обоих вариантах языка, либо обходятся тривиальными различиями. В части 3 этой книги мы рассмотрим большое количество конкретных изменений, что поможет вам быстрее привыкнуть к новому синтаксису.

Нет, такие изменения меня совершенно не беспокоят.

Меня беспокоит другое: изменения, связанные со сменой парадигмы; изменения, влияющие на архитектуру программ .NET, связанные с теми концепциями, которые покажутся новыми для большинства программистов VB .NET. Что конкретно имеется в виду?

1 Я вообще никоим образом не собираюсь обижать читателя, но бывает всякое. Если вы все же на что-нибудь обидитесь — честное слово, я этого не хотел.

2 По мнению моего технического редактора Скотта Стабберта, эта фраза создает впечатление, будто я в принципе против наследования. Это не совсем так. Как будет показано в главе 5, наследование — серьезная штука, которую нельзя ни легко отвергнуть, ни легко принять.

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

 

Виртуальная машина

С точки зрения программиста, термин «виртуальная машина» описывает платформу, для которой пишется программный код1. На компьютере могут быть установлены разные процессоры (Pentium III, Athlon, Pentium II и т. д.), он может быть оснащен разными объемом памяти — программиста не интересует физическая конфигурация компьютера. Он пишет программу для виртуальной машины, зависящей от операционной системы и программной среды.

На рис. 4.1 показана виртуальная машина, использовавшаяся в дни MS-DOS.

Рис. 4.1. Виртуальная машина с точки зрения DOS-программиста

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

1 Не путайте понятия «виртуальная машина» и «исполнительная среда». Например, виртуальная машина Java реализована на базе исполнительной среды и классов. Именно виртуальная машина определяет среду программирования, однако этот термин относится к любой среде. Даже при работе на ассемблере программист пишет код для виртуальной машины, определяемой набором инструкций процессора (которые, в свою очередь, реализуются микрокодом самого процессора).

В наше время программисты VB6 привыкли к более сложной виртуальной машине, изображенной на рис. 4.2. В нее входит исполнительная среда Visual Basic, Win32 API, подсистема СОМ и различные подсистемы, использующие СОМ. Прямой доступ к оборудованию практически невозможен.

Рис. 4.2. Виртуальная машина с точки зрения программиста VB6

Главное, что необходимо понять из сравнительного анализа этих двух рисунков, — то, что наши программы работают на том же компьютере, но виртуальная машина принципиально отличается от той, что использовалась в MS-DOS и даже (хотя из рисунка этого не видно) во времена VB3. ;

А теперь взгляните на рис. 4.3, изображающий виртуальную машину с точки зрения программиста VB .NET.

Рис. 4.3. Виртуальная машина для программистов VB .NET

Что бросается в глаза на этом рисунке?

1 Если уж придираться к словам, то CLR, конечно, включает пространства имен, специфические для Visual Basic.

Короче говоря, программист VB .NET пишет код для виртуальной машины, которая принципиально отличается от виртуальной машины, используемой программистом VB6. Изучение VB .NET сопоставимо с изучением программирования в совершенно другой операционной системе. Теоретически приложение Common Language (то есть соответствующее спецификации Common Language Specification, определенной на уровне .NET) сможет без изменений и без перекомпиляции работать на любой платформе или в операционной системе, в которой имеются модули Common Language Runtime!

Почему же для Visual Basic понадобилось так сильно изменять виртуальную машину? И почему Microsoft фактически предлагает вам то, что с точки зрения программиста является едва ли не новой операционной системой? Это очень важные вопросы, потому что большинство языковых изменений при переходе от VB6 к VB .NET было обусловлено требованиями CLR, а не наоборот.

Чем была плоха старая виртуальная машина, почему потребовались столь радикальные изменения?

Ответ на этот вопрос дает сравнение рис. 4.1-4.3. В Windows не было CLR.

 

СОМ умер. Да здравствует СОМ?

Честно говоря, я крайне неохотно приступаю к дальнейшим объяснениям. То, что специалисты по маркетингу из Microsoft натворили с терминами COM, OLE и СОМ+ (едва не добравшись до СОМ+ 2.0), было непростительно. Любые попытки хоть как-то исправить положение — дело по меньшей мере рискованное, но я все же попытаюсь.

 

СОМ — идеи и реализация

Сокращение СОМ означает «Component Object Model», то есть «модель составного объекта». Речь идет о способе взаимодействия объектов, не зависящем от языка, на котором они написаны. Технология СОМ позволяет создавать приложения и компоненты, состоящие из объектов, написанных разными программистами на разных языках. Кроме того, с ее помощью объекты могут вызывать методы, обращаться к свойствам и инициировать события других объектов даже в том случае, если последние находятся на другом компьютере в сети. СОМ основывается на нескольких относительно простых идеях.

Практическая реализация, заложенная в основу СОМ, решает все перечисленные задачи.

Говоря о недостатках СОМ, я скорее имею в виду реализацию, а не саму идею, причем к недостаткам реализации относятся не ее теоретические основы и даже не конкретный программный код, а практические последствия ее применения. Иначе говоря, недостаток технологии СОМ состоит не в том, что она не работает, а в том, что она плохо защищает нас от человеческой тупости — как нашей собственной, так и тупости конечных пользователей2.

1 Я выдержал немало жарких споров с людьми, которые применяли или выступали за этот подход. Упоминания о нем часто встречались в журналах и даже в книгах; к счастью, многие авторы предупреждали, что эту методику следует рассматривать как академическое упражнение, нежели практическое решение. Помнится, я утверждал, что это плохая практика программирования, которая крайне затрудняет сопровождение программы и вряд ли сохранится в следующей версии Visual Basic. Я и не думал, что окажусь настолько прав...

2 В своей книге «The Dilbert Principle» Скотт Адаме (Scott Adams) говорит, что все мы время от времени оказываемся идиотами. Именно это обстоятельство и подвело СОМ — к сожалению, эта технология не оставляет места для идиотизма. Мы снова возвращаемся к тому, о чем я говорил в главе 2: «Признание и успех технологии почти всегда определяется человеческими, политическими и экономическими факторами, а вовсе не технологическими!»

Проблемы с интерфейсами

Интерфейсы СОМ подчиняются одному простому правилу: после определения и распространения интерфейса вы никогда, НИКОГДА не должны изменять его. Более того, постоянным должен оставаться не только интерфейс, но и реализуемые им функциональные возможности.

Рассмотрим следующую ситуацию. Вы создаете объект с методом: 

Public Function Verify(CreditCardlnfо As String) As Boolean 

...и выпускаете версию 1 компонента, в котором этот метод используется для проверки кредитных карт. Также выходит версия 1 коммерческого приложения, использующего версию 1 компонента.

Затем происходит одно из следующих событий (или оба сразу).

Или:

Итак, выпускается версия 2 компонента с версией 1 системы проверки кредитных карт.

К сожалению, версия 2 компонента несовместима с версией 1 коммерческого приложения, что немедленно проявляется во всех системах, где была установлена программа (и обновленный компонент).

После катастрофического падения репутации и колоссальных затрат на техническую поддержку вы торопитесь создать версию 2 коммерческого приложения, работающую с версией 2 компонента. Более того, вы нашли способ существенно повысить быстродействие программы и поэтому переходите прямо к версии 2 компонента. На этот раз двоичная совместимость с компонентом версии 2 находится под самым тщательным контролем. К сожалению, один из программистов в процессе оптимизации компонента случайно изменяет одну из таблиц с описанием типа кредитной карты, поэтому счета по картам Discover теперь направляются в American Express.

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

Это происходит из-за того, что компоненты СОМ в DLL предназначены для внешнего использования.

Теоретически в СОМ «Кошмар DLL» просто исключен. Правила СОМ вполне однозначны: однажды определенный интерфейс никогда не изменяется, а обратная совместимость не нарушается.

К сожалению, программисты не идеальны. Более того, программисты Microsoft тоже не идеальны — вероятно, они создали больше проблем с «Кошмаром DLL», чем любая другая компания (что вполне объяснимо, поскольку они вообще создают больше DLL).

Проблемы с реестром

Самый очевидный выход из «Кошмара DLL» — разрешить каждому приложению загружать и запускать свои собственные версии компонентов. Ситуация, при которой разные версии одного компонента могут одновременно выполняться разными приложениями, называется «параллельным выполнением» (side-by-side execution). Хотя в Windows 2000 такая возможность существует, пользоваться ей неудобно СОМ требует, чтобы для каждой версии регистрировался только один компонент.

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

Наконец, при повреждении реестра вам придется заново переустанавливать и регистрировать все компоненты.

Проблемы с IUnknown

В СОМ каждое сохранение ссылки на объект должно сопровождаться вызовом метода AddRef интерфейса IUnknown объекта. При освобождении ссылок должен вызываться метод Release. Для объекта ведется внутренний счетчик текущих ссылок; когда его значение уменьшается до нуля, объект удаляет себя.

Возникают две проблемы.

Первая проблема в большей степени относится к C++, нежели к Visual Basic — программист может случайно забыть об удалении объекта. Пропущенные вызовы AddRef обнаруживаются очень легко, поскольку при обращении к удаленному объекту обычно инициируется исключение. С другой стороны, забытый вызов Release приводит к тому, что объект остается в памяти до завершения программы. Это может привести к утечке памяти, в результате которой приложение постепенно захватит все ресурсы системы — вполне реальная проблема для приложений, работающих по схеме 24/71.

Вторая проблема знакома программистам VB и продемонстрирована в следующем фрагменте.

Создайте класс со следующим кодом:

Public HoldingCollection As Collection

Private Sub e() 

Debug.Print "Object Freed" 

End Sub

Затем создайте форму с кнопкой, обработчик которой выглядит так:

Private Sub cmdExecute_Click() 

Dim col As New Collection 

Dim myobject As New

Dim counter As Long For counter = 1 To 1000

Set myobject.HoldingCol. lection = col

col.Add myobject Next counter End Sub

1 To есть 24 часа в сутки, 7 дней в неделю. — Примеч. перев.

Запустите программу и нажмите кнопку несколько раз. Понаблюдайте за сообщениями в окне отладки, свидетельствующими об удалении объекта. Ни одного такого сообщения вы не увидите. Попробуйте вызвать диспетчер задач и найдите приложение (или VB6, если код работает в среде программирования) в списке процессов. Запустите программу и нажимайте кнопку, наблюдая за столбцом Mem Usage.

Объем используемой памяти растет.

Перед нами типичная проблема циклических ссылок. Ссылка на каждый объект хранится в коллекции, но каждый объект также содержит ссылку на коллекцию (вероятно, для того, чтобы иметь возможность обратиться к другим объектам . При выходе из функции ссылка на коллекцию в переменной col освобождается, однако коллекция не удаляется, поскольку ее счетчик ссылок не равен нулю (кстати говоря, его значение совпадает с количеством объектов в коллекции).

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

Обработка ошибок

Проблемы с обработкой ошибок в наши дни обусловлены не столько недостатками СОМ, как тем, что программисты никак не могут договориться о том, как именно должна быть организована обработка ошибок. Должны ли ошибки инициироваться при создании компонентов VB? А может, коды ошибок следует возвращать в качестве результатов вызова функций? Даже беглый взгляд на функции API обнаруживает множество несоответствий в способах возврата информации об ошибках.

И давайте честно признаем, что обработка ошибок в Visual Basic оставляет желать лучшего. On Error Goto? Никто не использует Goto — нас десятилетиями учили тому, что это нехорошо. Синтаксис обработки ошибок в VB является пережитком доисторической эпохи BASIC.

СОМ+

Забавно — хотя я и раньше сталкивался со всеми описанными проблемами, лишь при написании этого раздела понял, сколькими недостатками обладает текущая реализация СОМ. Поэтому прежде чем показывать, как эти проблемы решаются в .NET, я должен хотя бы в общих чертах упомянуть о СОМ+.

В целом СОМ+ относится к числу технологий, порожденных в основном маркетинговыми соображениями (см. главу 1). Не поймите меня превратно — в СОМ+ появилось немало технологических новшеств (контексты, транзакции, асинхронные операции и т. д.), но все они в той или иной степени уже существовали под другими названиями (главным образом в Microsoft Transaction Server и Microsoft Message Queue). Появление термина СОМ+ не принесло ничего нового — просто появился новый маркетинговый ярлык для раскрутки1. В том, что касается описанных выше идей и их реализации, СОМ+ ничем не отличается от СОМ.

1 И не ждите, что я сейчас скажу что-нибудь о DNA. Эта технология умерла и уже не вернется.

СОМ+2.0

В какой-то момент показалось, что Microsoft собирается предпринять шаг, который, на мой взгляд, не имел никаких логических объяснений. Совершенно серьезно рассматривался вопрос о присвоении библиотеке .NET Framework, реализованной на Common Runtime Language, названия «СОМ+2.0».

.NET Framework не использует СОМ. Ее работа не основана на СОМ. Да, классы .NET Framework могут взаимодействовать с СОМ, использовать компоненты СОМ и использоваться в них, но на этом все и кончается. 

Я рад сообщить, что здравый смысл все же победил и Microsoft отказалась от идеи назвать .NET Framework «COM+ 2.0». Текущие ссылки на СОМ+ 2.0 в MSDN всего лишь перенаправляют читателя к .NET Framework.

Значит ли это, что технология СОМ мертва?

И да, и нет.

Да — если Microsoft удастся превратить .NET в доминирующую платформу разработки в мире Windows (а может, и не только?). В этом случае СОМ будет играть все меньшую роль. Если .NET интегрируется в будущих операционных системах, а основные приложения будут перестроены для .NET Frameworks, СОМ превратится в пережиток прошлого, в исторический казус, который по каким-то причинам продолжает существовать.

Но можно не сомневаться, существовать он будет. Слишком много всего построено на базе СОМ, чтобы эта технология могла умереть. Ведь Windows до сих пор поддерживает DDE, хотя в наши дни многие программисты даже не знают, что это такое2.

2 DDE (Dynamic Data Exchange) — старый протокол Windows для обмена информацией между приложениями. Ваш скромный слуга, которому досталось «удовольствие» программировать работу с DDE до появления стандартных библиотек, вспоминает об этом самыми сокровенными, задушевными словами...

 

Common Language Runtime

Любая технология должна изучаться в практическом контексте. Россыпи новых возможностей и преувеличенные обещания хорошо подходят для презентаций, но при оценке новой технологии нет ничего важнее, чем понимание контекста, в котором она существует, и проблем, которые она призвана решать. Это особенно важно при решении вопроса, подходит ли та или иная технология для выполнения ваших конкретных задач.

Итак, теперь вы знаете основные недостатки СОМ, и мы можем посмотреть, как же эти проблемы решаются в .NET.

Для начала зададим себе следующие вопросы1.

  •  Как добиться, чтобы программа, использующая компонент, продолжала работать даже в том случае, если кто-то установит более новую и притом несовместимую версию компонента?
  •  Как добиться, чтобы программа обнаруживала сам факт установки новой версии компонента, несовместимой с рабочей версией?
  •  Как устранить необходимость в регистрации компонентов?
  •  Как решить проблему циклических ссылок и утечки памяти даже в тех случаях, когда программист забывает освободить объект?

Во-первых, потребуется, чтобы в системе могли одновременно работать несколько версий одного компонента. В этом случае присутствие новой или старой копии компонента где-то в системе не повлияет на работу «правильной» копии, находящейся в каталоге приложения.

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

Операционная система должна автоматически находить и «регистрировать» компоненты без реестра — как правило, поиск должен происходить в каталоге приложения или в других заранее определенных каталогах, где могут находиться общие компоненты.

Наконец, операционная система должна отслеживать все объекты, используемые приложением в процессе его работы, и автоматически освобождать те из них, надобность в которых отпала.

Все эти требования невозможно удовлетворить в системе Windows в том виде, в котором она существует сейчас. Несовместимы они и с текущей реализацией СОМ. Понадобится совершенно другая архитектура, совершенно новая виртуальная машина. Такая виртуальная машина поддерживается исполнительной средой Common Language Runtime.

Visual Basic DLL или ЕХЕ-файл, созданные в VB .NET, сильно отличаются от тех, что использовались раньше. Да, в них используется тот же формат РЕ (Portable Executable), но при попытке выполнить исполняемый файл VB .NET в системе без установленной среды CLR на вас обрушатся многочисленные ошибки «DLL not found»2. Дело в том, что среда CLR нужна Windows для интерпретации новых типов записей, хранящихся в исполняемом файле.

1 Интересно, не составляли ли разработчики .NET подобный список и в каком порядке они расположили бы эти вопросы? К сожалению, об этом можно лишь гадать.

2 Во всяком случае, так было в предварительных версиях. Будем надеяться, что в окончательной версии Microsoft придумает более удобный способ проверки и оповещения пользователей о том, что для работы программы необходима среда .NET.

В мире .NET вместо терминов «DLL» и «ЕХЕ-файлы» чаще используется термин «сборка» (assembly). Мы еще поговорим о сборках в этой главе, а пока просто считайте, что существует однозначное соответствие — каждый созданный вами DLL- или ЕХЕ- файл содержит одну сборку, и каждая сборка состоит из одного DLL- или ЕХЕ-файла1. Среди новых типов записей в исполняемых файлах .NET хранится так называемый «манифест» (manifest). Манифест содержит подробные сведения о сборке:

  •  список всех компонентов, используемых сборкой;
  •  номера версий этих компонентов и хэш- коды, по которым исполнительная среда узнает об изменении этих компонентов;
  •  список всех объектов, поддерживаемых сборкой, их методов, свойств, типов параметров и возвращаемых значений;
  •  список всех объектов, необходимых для работы сборки, их методов, свойств, типов параметров и возвращаемых значений и т. д.

Существует и другой новый тип записей — записи промежуточного языка (Intermediate Language, сокращенно IL). Вскоре мы поговорим на эту тему более подробно.

 

Манифест

Манифест призван решить проблемы с контролем версии и установкой компонентов, существовавшие в СОМ. Давайте последовательно рассмотрим его основные возможности.

  •  Программа, использующая некоторый компонент, сможет продолжить работу даже в том случае, если в системе будет установлена обновленная версия этого компонента, несовместимая с предыдущей.

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

Означает ли это, что разработчики начнут устанавливать компоненты в свои собственные каталоги вместо System32 или других общих каталогов, чтобы избавиться от проблем с распространением своих компонентов?

Да, именно так. Вы по-прежнему сможете создавать общие компоненты, но, несомненно, они будут встречаться реже.

Означает ли это, что система будет загромождаться разными версиями одного и того же компонента? Разве это не приведет к напрасным затратам дискового пространства, не говоря уже о затратах памяти на одновременную загрузку компонентов, когда это совершенно не нужно? Разве библиотеки динамической компоновки создавались не для того, чтобы обеспечить совместное использование памяти и уменьшить затраты места на диске?

1 На самом деле сборка может состоять из нескольких DLL- или ЕХЕ-файлов. Тем не менее в текущей бета-версии VB .NET эта возможность не поддерживается и ничто не говорит о том, что в окончательной версии VB .NET ситуация изменится.

Да, такой подход потенциально расточителен по отношению к использованию памяти и дискового пространства. Да, DLL создавались для решения именно этих проблем. Но все это было во времена, когда на обычном компьютере стояло 640 Кбайт памяти, на особо мощных машинах объем памяти достигал нескольких мегабайт, а дисковое пространство стоило от $10 за мегабайт1. В наши дни даже на слабых компьютерах устанавливается от 64 Мбайт памяти, а мегабайт дискового пространства редко стоит больше 1 цента. При таких характеристиках затратами на дублирование файлов DLL в системе и даже в памяти можно пренебречь. В наши дни разработчики обращают основное внимание на борьбу с утечкой памяти, возникающей во время работы программ. Если ваше приложение работает целыми днями и даже неделями, такая утечка способна полностью израсходовать даже эти огромные ресурсы. Кроме того, разработчики стараются свести к минимуму влияние компонентов одного приложения на работу других приложений.

  •  Манифест позволяет программе обнаружить факт установки несовместимой версии компонента поверх его рабочей версии.

Что произойдет, если существующий компонент будет физически заменен новым, несовместимым с вашим приложением?

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

Кроме того, можно разрешить приложению использовать обновленные версии компонента. В этом случае CLR по данным манифеста проверяет новую версию и убеждается в том, что она поддерживает все нужные объекты, а все методы, свойства и типы совпадают с теми, которые нужны для работы вашего приложения. В случае несовпадения выполнение программы не разрешается.

  •  Манифест снимает необходимость в регистрации компонентов.

CLR получает данные манифеста на стадии загрузки от приложения и зависимых компонентов, а не из реестра. Компоненты загружаются из каталога приложения или из глобального кэша (в зависимости от конфигурации приложения). Любопытно заметить, что благодаря этому обстоятельству (а также некоторым другим) становится возможным принципиально новый подход к установке. Теперь приложение можно установить простым копированием файлов или дерева каталогов2!

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

Кроме того, следует учитывать, что долгосрочный успех этого подхода в немалой степени зависит от того, удастся ли Microsoft обеспечить обратную совместимость самой среды CLR по мере ее усовершенствования3.

1 Помню свой восторг от покупки 500-мегабайтного жесткого диска — и притом всего за $1000!

2 Думаю, это многих позабавит. После долгих лет развития Windows-технологий мы наконец-то пришли к тому, что в DOS было вполне заурядным явлением!

3 До меня доносились слухи о том, что в системе можно будет установить сразу несколько версий CLR. Возможно, это поможет избежать некоторых проблем с совместимостью.

Промежуточный язык (IL)

Как же CLR выполняет все эти чудеса с манифестом? Откомпилированная программа содержит большое количество вызовов функций. Работа СОМ основана на том, что при компиляции приложения, использующего компоненты СОМ, для каждого объекта создается таблица виртуальных функций — массив указателей на функции, вызываемые приложением. Впрочем, так работает механизм раннего связывания. При позднем связывании возможен динамический вызов функций по именам, однако это сопряжено с существенными затратами.

И конечно, остается последнее требование из нашего списка: среда CLR должна решать проблемы циклических ссылок и утечки памяти даже в тех случаях, когда программист забывает освободить объект.

Каким образом исполнительная среда может проанализировать откомпилированное приложение и выяснить, где объект используется, а где его можно освободить? И не отразится ли эта проверка на быстродействии приложения?

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

В .NET эта проблема решается наиболее очевидным способом: часть компиляции откладывается до момента загрузки приложения.

Компиляция сборки .NET в DLL-библиотеку или ЁХЕ-файл не доводится до конца. Большая часть информации, обычно используемой компилятором, хранится в манифесте. Вместо машинного кода компилятор генерирует так называемый IL-код.

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

  •  JIT-компилятор анализирует программу и убеждается в том, что все обращения к памяти осуществляются через переменные правильного типа (то есть проверяет, что ваше приложение не выходит за пределы тех объектов и структур данных, которые в нем определяются1).
  •  JIT-компилятор генерирует код и таблицы, по которым CLR находит корневые переменные вашей программы. К их числу относятся как глобальные переменные, так и локальные переменные, созданные в стеке или временно хранящиеся в регистрах процессора.
  •  Программа компилируется только в случае необходимости.

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

Не путайте IL-код с Р-кодом, знакомым многим программистам Visual Basic. Да, Р-код является разновидностью промежуточного языка, однако он интерпретируется исполнительной средой, а IL-код компилируется в машинный код, который затем сохраняется. В результате время загрузки увеличивается, но зато при каждом последующем выполнении этого блока обеспечивается превосходное быстродействие машинного кода. Сборку также можно обработать JIT-компиля-тором во время установки и сохранить полученный машинный код; это позволит ускорить первый запуск приложения1.

Под термином «управляемый код» (managed code) понимается IL-код, который всегда обращается к памяти через определенные типы данных. В управляемом коде не разрешено использовать указатели, поскольку им могут быть присвоены значения, соответствующие недопустимым областям памяти. VB .NET создает управляемый код. Язык С# тоже ориентирован на управляемый код, однако в нем предусмотрен режим создания обычного кода. В C++ были включены специальные расширения, позволяющие создавать управляемый код.

Использование управляемого IL-кода приводит к интересному побочному следствию: поскольку машинный код генерируется лишь в момент обработки IL-кода JIT-компилятором на рабочем компьютере, теоретически возможно, что приложения .NET будут работать на всех платформах или операционных системах с поддержкой CLR. Интересно, появится ли CLR в каких-нибудь операционных системах, кроме Windows? А пока будем надеяться, что приложения .NET по крайней мере будут легко совмещаться с всевозможными разновидностями Windows, которых становится все больше.

1 При предварительной компиляции во время установки необходимо сохранить исходный IL-код и манифест — это объясняется тем, что при модификации зависимых сборок CLR, возможно, придется откомпилировать программу заново.

Прощание с циклическими ссылками

Благодаря двухшаговой схеме компиляции (IL + JIT-компиляция) CLR может получить список корневых переменных приложения или компонента во время работы программы.

  •  CLR знает, где находятся глобальные переменные приложения.
  •  CLR знает, как перебрать содержимое стека и взять из каждого кадра его локальные переменные.
  •  CLR знает, в каких регистрах хранятся временные переменные.
  •  CLR знает, в каких переменных хранятся ссылки на объекты.
  • CLR знает, какие члены объектов содержат ссылки на другие объекты.
  •  Все объекты создаются на стадии выполнения по данным сборок, поэтому CLR известно местонахождение всех объектов в куче и стеке.

В начале работы приложения .NET создается куча (heap), состоящая из одного большого блока памяти. Как правило, когда сборка запрашивает у CLR объект (a CLR рассматривает любые элементы данных как объекты), память выделяется из свободного блока в верхней части кучи, при этом среда не пытается заполнять пустые места, появившиеся на месте ранее освобожденных объектов2. Когда весь свободный блок будет исчерпан, CLR приступает к «сборке мусора» (garbage collection). Ниже приведено упрощенное описание того, что при этом происходит.

2 Как будет показано ниже, объекты структурных типов тоже могут создаваться в стеке, но пока мы говорим о куче.

  •  CLR помечает все объекты в куче как неиспользуемые.
  •  CLR проверяет все глобальные переменные вашего приложения и помечает объекты, на которые они ссылаются, как используемые. Далее производится рекурсивный поиск всех объектов, на которые ссылаются эти объекты «первого уровня», и они также помечаются как используемые. Если в процессе поиска оказывается, что найденный объект уже помечен как используемый, дальнейший поиск для этого объекта не производится — известно, что все объекты уже были найдены.
  •  CLR перебирает содержимое стека и проверяет все объекты, на которые ссылаются локальные переменные в каждом кадре стека. Производится аналогичный рекурсивный поиск, и все найденные объекты помечаются как используемые.
  •  CLR проверяет все объекты, ссылки на которые хранятся в регистрах процессора, и помечает эти объекты как используемые (тоже с рекурсивным поиском).
  •  После завершения этого процесса все объекты кучи, оставшиеся непомеченными, удаляются. Все остальные объекты перемещаются в нижнюю часть кучи, а все ссылки на них обновляются. Этот процесс схематически изображен на рис. 4.4-4.7.

Рис. 4.4. Глобальные переменные и переменные, найденные в процессе трассировки стека, считаются «корневыми». Ссылки объектов на кучу представлены указателями. Объекты в куче тоже могут ссылаться на другие объекты. Обратите внимание на циклические ссылки, которые не будут удалены в СОМ

Рис. 4.5. В корневых переменных освобождаются две ссылки. Объекты, на которые ссылаются остальные переменные, помечаются как используемые — как и объекты, на которые они, в свою очередь, ссылаются

Рис. 4.6. Блоки памяти, занятые непомеченными объектами, освобождаются

Рис. 4.7. Объекты перемещаются в нижнюю часть кучи с соответствующим изменением ссылок

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

Конечно, за все хорошее приходится платить. При нехватке памяти для выполнения очередного запроса ваше приложение приостанавливается для сборки мусора. К счастью, Microsoft приложила огромные усилия по оптимизации процесса сборки мусора. Если учесть все преимущества, затраты вполне оправдываются.

 

Первая программа

Проект MemoryLeakNot наглядно показывает, что в CLR решена проблема циклических ссылок.

ВНИМАНИЕ 

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

Исходный вариант приложения VB6 включает следующий класс:

Public HoldingCollection As Collection

Private Sub e()

Debug.Print "Object freed" 

End Sub

...и форму с кнопкой, при нажатии которой вызывается следующая процедура:

Private Sub cmdExecute_Click()

Dim col As New Collection

Dim myobject As New

Dim counter As Long 

For counter = 1 To 1000

Set myobject.HoldingCollection = col 

col.Add myobject

Next counter

End Sub

Аналогичный класс VB .NET приведен в листинге 4.1.

Листинг 4.1. Модуль Testта MemoryteakNot

' Отсутствие утечки памяти

' Copyright ©2001 by Desaware Inc. All Rights Reserved 

Public sObject

Shared m_Integer 

Dim m_eger

Dim m_Collection As Collection 

Public Sub New(ByVal mycontainer As Collection) 

MyBase.NewQ mycontainer.Add (Me)

m_Collection = mycontainer 

m_asscount 

m__ 

End Sub

Protected Overrides Sub FinalizeO

System.Diagnostics.Debug.writeLine ("Destructed " + _

m_ng()) End Sub

End

Код формы приведен в листинге 4.2.

Листинг 4.2. Форма TestForm.vb проекта MemoryteakNot

' Отсутствие утечки памяти

' Copyright ©2001 by Desaware Inc. All Rights Reserved 

Public ont>

 Inheri ts System.Windows.Forms.Form

#Region " Windows Form Designer generated code "

Public Sub New() 

MyBase.NewO

' Следующий вызов необходим 

' для дизайнера форм Windows.

 InitializeComponent()

' Дальнейшая инициализация выполняется после вызова 

' InitializeComponentO End Sub

' Форма переопределяет Dispose для очистки списка компонентов. 

Public Overloads Overrides Sub Dispose()

MyBase.Dispose()

If Not (components Is Nothing) Then 

components.Di spose()

End If 

End Sub

 Private WithEvents buttonl As System.Windows.Forms.Button

' Необходимо для дизайнера форм Windows.

Private components As System.ComponentModel.Container

' ВНИМАНИЕ: следующий фрагмент необходим

' для дизайнера форм Windows.

' Для его модификации следует использовать дизайнер форм.

' Не изменяйте его в редакторе!

<System.Diagnostics.DebuggerStepThrough()> Private Sub _

InitializeComponent()

Me.buttonl = New System. Windows. Forms. Button()

Me.SuspendLayout()

'

'buttonl

'

Me.buttonl.Location = New System.Drawing.Point(104, 48) 

He.buttonl.Name = "button1"

 Me.buttonl.Tablndex = 0

 Me.buttonl.Text = "Test"

'

'Forml 

'

Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)

Me.ClientSize = New System.Drawing.Size(275, 144)

Me.Controls.AddRange(New System.Windows.Forms.Control() _

{Me.buttonl})

 Me.Name = "Forml" 

Me.Text = "Memory Leak - Not" 

Me.ResumeLayout (False)

End Sub

Private Sub buttonl_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles'buttonl.Click 

Dim x As Integer 

Dim col As New Collection() 

Dim obj As Testont>

For x = 1 To 100

ob] = New Testl)

Next x

Debug.WriteLineC'Collection contains " + _ 

col.Count.ToStringO + " objects")

' Объекты все равно будут освобождены при выходе из функции, 

' здесь это делается лишь в демонстрационных целях,

col = Nothing 

obj = Nothing

'В обычных программах этот фрагмент не нужен.

' В данном примере он просто доказывает,

' что проблема циклических ссылок в VB .NET решена.

gc.Collect()

gc.WaitForPendingFinalizers()

End Sub 

#End Region 

End

He паникуйте. Все не так плохо, как кажется с первого взгляда.

В этом примере используется несколько иное архитектурное решение. В VB6 объект содержал ссылку на коллекцию, но эту ссылку приходилось задавать на уровне формы через открытое свойство объекта. В приложении MemoryNotLeak используется новая возможность VB .NET — параметризованные конструкторы, позволяющие передать параметр при инициализации объекта. В нашем примере передается ссылка на коллекцию.

Давайте еще раз взглянем на класс, приведенный в листинге 4.1.

В классе объявлены три локальные переменные. Впрочем, действительно локальными для каждого объекта являются лишь две из них. Переменная m_nt> объявлена общей (Shared). Это означает, что все объекты, созданные классом в домене приложения, будут работать с одним экземпляром этой переменной. В нашем примере это позволяет следить за количеством созданных объектов данного типа и присваивать им номера, сохраняемые в переменной m_.

Не беспокойтесь по поводу того, что у вас быстро кончатся целые номера. В VB .NET используется 32-разрядный тип Integer (аналог типа Long в VB6).

В переменной m_Col lection хранится ссылка на коллекцию объектов.

Метод New является конструктором. Он получает ссылку на коллекцию в виде параметра и сохраняет ее в переменной. При этом нам не приходится использовать надоевшее ключевое слово Set из VB6. Первая команда MyBase.New() вызывает конструктор базового класса. Что такое базовый класс? Я отвечу на этот вопрос при описании наследования в главе 5. Далее конструктор увеличивает переменную m_ont>; новое значение будет использовано для следующего объекта этого типа.

Метод Finalize вызывается при подготовке объекта к окончательному уничтожению1. В нашем примере этот метод просто выводит служебную информацию в окне отладки. На смену знакомому объекту Debug, реализованному средствами СОМ, пришел значительно более мощный, хотя и менее знакомый объект Debug из пространства имен System. Diagnostics.

1 С технической точки зрения это утверждение правильно, однако оно не вносит полной ясности. Во-первых, непонятно, когда именно уничтожается объект. Во-вторых, подготовка к уничтожению еще не гарантирует, что объект действительно будет уничтожен. В-третьих, объекты могут уничтожаться и без вызова этого метода. В двух словах этого не объяснить — за подробными комментариями обращайтесь к главе 6.

Параметр функции выглядит довольно странно. Для вывода идентификатора объекта в VB6 обычно применялись конструкции вида: 

"Destructed " & m_

VB6 видит, что вы собираетесь присоединить целое число к строке, и автоматически преобразует его в строку. В данном случае это удобно, но подобные преобразования типов часто приводят к всевозможным ошибкам. Следующая команда VB .NET осуществляет явное преобразование целого числа в строку методом ToString:

 "Destructed " + m_CVassid.ToString() .

Стоп... Откуда взялся метод у типа Integer? Ведь это обычное целое число?

И да, и нет. Переменные типа Integer содержат целые числа, но B.VB .NET они могут интерпретироваться как объекты. Все типы Данных VB .NET являются производными от класса Object. А поскольку в классе Object определен метод ToString для создания текстового представления объекта, все объекты производных классов, включая Integer, тоже содержат метод ToString. Любопытно, правда? Не огорчайтесь, если вам что-то покажется непонятным — наследование рассматривается в следующей главе.

В листинге 4.3 приведена процедура button1_Click модуля формы.

Листинг 4.3. Метод buttonl_Click модуля TestForm.vb

Private Sub buttonl_Click(ByVal sender As System.Object, _ 

ByVal e "As' System. EventArgs) Handles buttonl.Click 

Dim x As Integer 

Dim col As New Collection()

 Dim obj As Testsp;

For x = 1 To 100

obj = New Testl) 

Next x

Debug.WriteLine("Collection contains " + _ 

col.Count.ToString() + " objects")

' Объекты все равно будут освобождены при выходе из функции, 

' здесь это делается лишь в демонстрационных целях,

 col = Nothing 

obj = Nothing

' В обычных программах этот фрагмент не нужен.

' В данном примере он просто доказывает,

' что проблема циклических ссылок в VB .NET решена.

GC.Collect()

GC.WaitForPendingFinalizers()

End Sub

Большинство программистов без особого труда разберется в этом коде. Возможно, вас несколько удивят параметры события Button_Click — ведь вы привыкли к тому, что это событие вызывается без параметров.

Процедура события создает объект коллекции, затем в цикле создает 100 объектов Testont> и включает их в коллекцию. Помните, что в нашем примере объект включается в коллекцию передачей ссылки при вызове конструктора (вызываемого при создании объекта командой New). Заполнение коллекции проходит успешно, на что указывает свойство Count, выводимое в окне отладки методом Debug.WriteLine.

В конце этого метода приведены два примера «плохого» кода VB .NET. В первом фрагменте мы присваиваем переменным col и obj значение Nothing. Делать этого не нужно, поскольку обе переменные автоматически освобождаются при выходе объекта из области видимости. Иногда значение Nothing приходится присваивать объектам внутри процедуры, но присваивание Nothing локальным переменным перед выходом из процедуры абсолютно излишне.

В следующем фрагменте запускается сборщик мусора. Скорее всего, запускать его вручную вам практически никогда не придется1. В нашем примере это делается только потому, что я хочу продемонстрировать уничтожение объектов в CLR. Не забывайте, что перед вами один из примеров циклических ссылок — коллекция содержит ссылку на каждый объект, который, в свою очередь, содержит ссылку на коллекцию. В СОМ коллекция и объекты будут освобождены лишь при выходе из приложения. В VB .NET они освобождаются, поскольку на них не ссылается ни одна корневая переменная приложения. Тем не менее освобождение происходит лишь во время работы сборщика мусора, но нельзя заранее предсказать, когда это произойдет. В нашей программе я запускаю сборщик мусора специальной командой, чтобы вы могли проследить за уничтожением объектов, наблюдая за сообщениями в окне отладки.

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

Впрочем, мы только начинаем рассматривать управление памятью в Common Language Runtime. В главе 6 эта тема будет рассмотрена более подробно.

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

 

Новый подход

Обращает на себя внимание и новый код в модуле формы. Присмотритесь к нему. Не огорчайтесь, если вы чего-то не поймете — многие команды выглядят вполне знакомо. В частности, мы видим команды для создания формы и элемента-кнопки. Также бросается в глаза интересный комментарий:

' ВНИМАНИЕ: следующий фрагмент необходим

' для дизайнера форм Windows.

' Для его модификации следует использовать дизайнер форм.

' Не изменяйте его в редакторе!

Означает ли это, что вся среда программирования VB .NET генерирует весь код, необходимый для реализации приложений VB? Что произошло с инкапсуляцией кода на уровне языка, благодаря которой все технические подробности скрывались от пользователя? Не было ли это одной из тех особенностей, благодаря которым Visual Basic стал таким простым и доступным для многих программистов?

Перед нами один из первых спорных моментов, связанных с VB .NET. Раньше Visual Basic и Visual C++ номинально входили в пакет Visual Studio, но в действительности это были разные среды программирования.

Visual Basic скрывал от программиста все внутреннее взаимодействие компонентов приложения, которое фактически инкапсулировалось на уровне языка и исполнительной среды. Программисту не приходилось думать о том, откуда берутся формы и кнопки — они существовали и ими можно было пользоваться. При двойном щелчке на элементе открывался обработчик события, и никто не знал, что на самом деле происходило «за кулисами».

В Visual C++ обеспечивался аналогичный уровень автоматизации: чтобы создать обработчик события, было достаточно дважды щелкнуть на элементе в диалоговом окне, а свойства и методы добавлялись в программе Однако в каждом из этих случаев вы в действительности взаимодействовали с большой и сложной программой. Эта программа генерировала код, который можно было видеть, но обычно не стоило модифицировать.

Что же произошло в VB .NET? Ответ на этот вопрос зависит от точки зрения.

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

Другие говорят, что разработчики Visual Studio ограбили Visual Basic; что новая среда, основанная на использовании автоматически сгенерированного кода, заметно уступает по эффективности традиционной среде VB и воплощает в себе все, чего многие программисты VB намеренно избегали.

Сторонники обеих точек зрения высказываются страстно и красноречиво.

Вероятно, вы заметили, что я тоже бываю страстным и красноречивым — но боюсь, моя точка зрения никого не устроит.

Дело в том, что я просто не берусь оценивать правильность подхода, избранного Microsoft, и реакцию на него программистов VB. Хотя я часто использую VB в работе над приложениями и компонентами, я также занимаюсь СОМ- программированием в Visual C++ для ATL1, поэтому к Visual Studio я уже привык.

Думаю, большинство программистов Visual Basic без особого труда привыкнет к новому подходу, особенно если учесть, что редактор Visual Studio скроет от них все сложности.

И все же... Мне бы очень хотелось послушать споры, которыми сопровождалось принятие этого решения в Microsoft.

1 Не помню, когда я в последний раз создавал приложение Visual C++ с пользовательским интерфейсом. К тому же с появлением ATL я полностью забросил MFC, хотя ATL гораздо сложнее. Впрочем, это совсем другая история.

 

Итоги

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

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

В .NET для решения этих проблем используется новая архитектура, основанная на Common Language Runtime. Приложения и компоненты .NET состоят из сборок, реализованных в виде одного или нескольких DLL и ЕХЕ-файлов. В каждой сборке имеется манифест с описанием всех компонентов, необходимых для ее работы, а также IL-код, компилируемый JIT-компилятором в машинный код при первой загрузке сборки или при ее установке в системе.

Архитектура .NET исправляет два основных недостатка СОМ: проблемы контроля версии и размещения, а также циклических ссылок и утечки памяти в тех случаях, когда программист забывает своевременно освобождать объекты в своей программе.

Кроме того, эта глава дает представление о коде VB .NET. Вы познакомились с параметризованными конструкторами, вызываемыми при создании объекта. Заметные изменения в синтаксисе языка оправдывают мое утверждение о том, что VB .NET — это совсем не тот Visual Basic, к которому вы привыкли. Наконец, мы рассмотрели наследование, пусть в самом элементарном варианте, а также выяснили, что в VB .NET абсолютно все переменные, включая числовые, являются объектными.

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

В ближайших главах будут описаны многие новые возможности VB .NET:

  •  фактическое объединение механизмов наследования и включения;
  •  применение многопоточности для увеличения числа одновременно обслуживаемых клиентов, особенно в web-приложениях (требует величайшей осторожности при проектировании!);
  •  огромная библиотека функций Common Language Runtime;
  •  структурная обработка ошибок на базе исключений (в настоящей главе эта тема была лишь кратко упомянута, но мы непременно вернемся к ней в будущем);
  •  создание защищенного кода (вы можете управлять тем, какие полномочия в системе предоставляются коду из разных источников, а также создавать код с корректным сокращением «функциональности в зависимости от уровня предоставленных привилегий.

Следующая глава посвящена широко разрекламированной теме — наследованию в VB .NET. Наследование играет ключевую роль в архитектуре CLR, однако на практике оно иногда преподносит неприятные сюрпризы.

Назад   Вперёд

 



Заказ программ!
Вы можете заказать у меня написание необходимой вам программы. Чем популярнее будет она, тем меньше стоит работа.
Инфо
Сайт создан: 3 февраля 2000 г.
Рейтинг@Mail.ru
Главная страница