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

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

Управление памятью BVB.NET

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

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

Следовательно, все элементы данных, используемые в вашем приложении, подчиняются правилам управления памятью в .NET.

И это почти правда...

 

Ссылочные и структурные объекты

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

Тем не менее разработчики .NET стремились к простоте и логической последовательности, обусловленной наследованием всех переменных от корневого типа Object.

Для разрешения этого конфликта им пришлось определить два типа объектов: ссылочные объекты и структурные объекты1.

Ссылочные (reference) объекты похожи на объекты СОМ, хорошо знакомые нам по VB6. Присваивание одного ссылочного объекта другому не сопровождается копированием данных: вместо этого вы просто получаете новую ссылку на существующие данные. Ссылочные объекты всегда размещаются в куче. Ссылочные объекты могут наследовать от других объектов, и другие объекты, в свою очередь, могут наследовать от них.

1 На всякий случай поясню, что термины «ссылочный тип» и «ссылочный объект» в данном контексте означают одно и то же (как и термины «структурный тип» и «структурный объект»).

 

Структурные объекты

Структурные (value) объекты размещаются в сегменте данных приложения или в стеке — не в куче. Структурные объекты VB .NET создаются практически так же, как и пользовательские типы в VB6, если не считать того, что для них используется ключевое слово Structure.

Пример простого структурного типа, определяемого в проекте ValueType:

Public Structure mystruct 

Public AString As String

 Public Sub SetString(ByVal newstring As String)

AString = newstring 

End Sub

 Public Sub New(ByVal InitialString As String)

AString = InitialString 

End Sub

 End Structure

В этом объявлении следует обратить внимание на ряд интересных обстоятельств.

Прежде всего, структура содержит строковое поле. В VB6 можно было определять строки фиксированного размера, хранящиеся в структуре. В VB .NET тип данных String является ссылочным1. Таким образом, у вас имеется структурный объект, который содержит переменную ссылочного типа. Так зачем же в этом случае использовать структурный тип?

Абсолютно незачем. Это неудачное решение.

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

В отличие от пользовательских типов VB6, структуры VB .NET могут обладать методами и свойствами. В них также могут определяться конструкторы — методы, предназначенные для инициализации структур. Впрочем, есть одна тонкость: определяемый вами конструктор должен получать хотя бы один параметр. Конструктор по умолчанию, используемый CLR, инициализирует все поля структуры нулями.

1 Строки более подробно описаны в главах 8 и 9 — в VB .NET они заметно отличаются от строк VB6. А пока достаточно помнить, что строка хранится в структуре в виде указателя на блок памяти в куче. Непосредственное содержимое строки в структуре отсутствует.

Метод Buttonl_Click проекта ValueType выглядит следующим образом:

Private Sub Buttonl_Click(8yVal sender As Object,

ByVal e As System.EventArgs) Handles buttonl.Click 

Dim a As mystruct 

a.SetString ("Hello") 

Dim b As mystruct = a

Debug.WriteLine("B's String: " + b.AString) 

Dim с As New mystruct("Another string")

 Debug.Wri teLineC'C's String: " + c.AString)

  Debug.WriteLine("C's ToString: " + c.ToString())

Dim obj As Object 

obj = с

' Упаковка в объект

Debug.WriteLine(obj .ToString())

End Sub

В этом методе продемонстрированы три разных способа инициализации структуры mystruct.

Команда Dim a As mystruct создает структурную переменную а. Конструктор по умолчанию инициализирует все поля структуры нулями (строки инициализируются значением Nothing). Если бы тип mystruct в этой команде был ссылочным, переменная а была бы равна Nothing: ссылочные объекты должны создаваться перед использованием. Не существует такого понятия, как недействительная или пустая структурная переменная. Следовательно, объявления Dim a As mystruct и Dim a As New mystruct эквивалентны, если не считать того что во втором варианте могут использоваться другие конструкторы.

VB .NET позволяет инициализировать переменные синтаксической конструкцией вида: 

Dim myvaluetype = начальное_значение

Таким образом, команда

 Dim I As Integer = 5

является вполне допустимым способом объявить целую переменную и присвоить ей начальное значение 5.

Структуры одного типа; можно свободно присваивать друг другу, при этом VB .NET копирует данные структур на уровне полей. Таким образом, следующий фрагмент вполне допустим:

Dim a As mystruct

Dim b As mystruct

b = a

Такие конструкции можно сокращать до вида:

Dim a As mystruct 

Dim b As mystruct = a

Последний объект с использует перегруженный1 метод New, чтобы создание и инициализация строки выполнялись одной командой: Dim с As New mystruct("Another string")

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

Dim obj As Object ob] = с

Когда у CLR возникает необходимость интерпретировать структурный тип в ссылочном контексте, выполняется операция, называемая упаковкой (boxing): в куче создается временный объект, и в него копируются члены структурного типа. Обратный процесс называется распаковкой (unboxing).

1 Термин «перегрузка» означает наличие нескольких одноименных методов в классе или структуре. Перегруженные методы различаются только параметрами. Перегрузка подробно рассматривается в главе 10.

При выполнении обработчика события Buttonl_Click в окне отладки выводится следующий результат:

B's String: Hello

C's String: Another string

C's ToString: ValueType.mystruct

ValueType.mystruct

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

Обработчик Button2_Click (листинг 6.1) еще нагляднее поясняет различия между структурными и ссылочными типами.

Листинг 6.1. Копирование объектов ссылочных и структурных типов1

Private  

Public AString As String 

Public Sub SetString(ByVal newstring As String)

AString = newstring

 End Sub 

Public Sub New(ByVal InitialString As String)

AString = InitialString 

End Sub

 End

Private Sub Button2_Click(ByVal sender As Object,

ByVal e As System.EventArgs) Handles button2.Click

Dim SI As New mystruct("Hello")

Dim 52 As mystruct

Dim cl As New myello")

Dim c2 As my

S2 = S1

c2 = cl

S2.SetString ("Modified")

c2.SetString ("Modified")

Debug.WriteLineC'Struct: " + SI.AString)

Debug.WriteLineC'; + cl.AString)

 End Sub 

Класс myпрактически идентичен структуре mystruct, и инициализация, казалось бы, происходит одинаковым образом. Что произойдет, если создать копии структуры и объекта, а затем модифицировать их? При нажатии кнопки в окне отладки выводятся значения свойства AString исходной структуры и объекта:

Struct: Hello

ied 

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

1 Все исходные тексты можно найти на сайте издательства «Питер» www.piter.com. — Примеч. ред.

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

  •  Структуры не могут объявляться производными от других классов/структур.
  •  От структур нельзя производить классы.
  •  Структурные переменные всегда содержат действительные данные; им нельзя присвоить Nothing.
  •  Присваивание структур сводится к копированию данных на уровне полей, что может привести к большим потенциальным затратам времени.
  •  Сравнение структур сводится к сравнению значений их полей.
  •  Структуры не могут содержать деструкторов (завершителей).
  •  Неконстантные поля структур не могут инициализироваться конкретными значениями. Для полей-массивов не допускается задание начального размера.
  •  В рекомендациях Microsoft говорится, что затраты на копирование данных в структурных типах превышают затраты на сборку мусора, когда размер структурного типа превышает 16 байт.

Ссылочные объекты

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

В структурах массивы и строки остаются объектами, создаваемыми в куче и находящимися под управлением CLR1. Куча устроена таким образом, что операции создания и освобождения объектов в ней выполняются очень быстро. При создании ссылочной переменной память просто выделяется из свободной области в верхней части кучи2. Освобождение ссылочной переменной сводится к простому присваиванию Nothing или ссылки на другой объект — куча в этом вообще не участвует. Конечно, замечательная скорость операций выделения и освобождения памяти не обходится бесплатно. Как вы узнали из главы 4, системе приходится периодически выполнять сборку мусора, чтобы освободить память от неиспользуемых объектов и перевести ее в свободную часть кучи.

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

1 Существуют средства, позволяющие более точно управлять расположением полей в структурах и определять в структурах строки и массивы фиксированной длины — в первую очередь, при вызовах функций API и передаче данных средствами СОМ. Об этом речь пойдет в части 3 этой книги.

2 Я немного упрощаю; в действительности CLR для повышения эффективности поддерживает раздельные пространства свободной памяти для малых и больших объектов.

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

Снова о сборке мусора .

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

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

На рис. 6.1 показана куча с пятью объектами. С двумя объектами (помеченными буквой F) ассоциируются завершители (то есть в программный код этих объектов входит метод Finalizer). CLR в сочетании с компилятором JIT опознает объекты, имеющие завершители, и отслеживает их особо.

Рис. 6.1. Куча, содержащая два объекта с завершителями

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

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

1 Да, все верно — объекты с завершителями проходят сборку мусора дважды: при включении в список завершения и при фактическом освобождении памяти.

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

Сколько времени проходит от исчезновения последней ссылки на объект до выполнения завершителя?

Неизвестно.

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

 

Завершители

Не много найдется областей, которые бы лучше демонстрировали различия между VB .NET и VB6, чем проблемы завершения.

Программисты VB6, изучавшие теорию объектно-ориентированного программирования, знают «правильный» способ инициализации и деинициализации компонентов. В обработчике события Initialize инициализируются переменные класса и даже выделяются необходимые системные ресурсы. В обработчике события Terminate выполняются необходимые завершающие действия. Событие Terminate происходит сразу же после освобождения последней ссылки на объект, поэтому в общем случае можно считать, что в обработчике Terminate все объектные переменные сохраняют действительные значенитя1. Вы можете твердо рассчитывать на то, что событие Terminate наступит и даст вам возможность «прибрать» за приложением в момент прекращения его работы (освободить системные ресурсы, закрыть файлы и т. д.).

В VB .NET эти правила не действуют2.

Чтобы понять, почему это происходит, достаточно рассмотреть пример Undeadl. В этом проекте определен класс Undying:

Public /font>

Protected Overrides Sub Finalize()

 Debug.WriteLineC'I have been finalized")

End Sub

 End

На форме находится кнопка cmdCreate, которая создает и удаляет объект:

Private Sub buttonl_Click(ByVal sender As System.Object_

 ByVal e As System.EventArgs) Handles buttonl.Click 

' Внимание: в отличие от VB6 следующая команда создает объект. 

Dim obj As New Undying()

End Sub

В VB .NET в отличие от VB6 команда New немедленно создает объект. В VB6 команда New просто сообщает VB о том, что объект должен быть автоматически создан при первом обращении к его свойствам или методам.

Это изменение влияет и на освобождение объектных переменных. В VB6 при объявлении переменной в команде New следующего вида:

 Dim myObject As New SomeObjectType

объект myObject создавался заново при каждой ссылке на него в программе. Если присвоить переменной myObject значение Nothing, а затем снова сослаться на него, будет использован новый экземпляр объекта SomeObjectType.

В VB .NET объект SomeObjectType создается непосредственно при выполнении команды Dim. Если затем присвоить myObject значение Nothing и попытаться сослаться на него, возникает ошибка времени выполнения.

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

1 При выходе из приложений VB6 объекты освобождаются в произвольном порядке. Следовательно, во время обработки события класса Terminate ссылки на объекты могут стать недействительными, что иногда даже приводит к ошибкам защиты.

2 И не обманывайте себя мыслью, будто вы избежите всех затруднений переходом на С#. В СП имеются деструкторы, похожие на деструкторы C++, однако работают они точно так же, как и завершители VB .NET.

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

 

Недетерминированное завершение: трудный выбор

Visual Basic 6 (и объекты СОМ вообще) обеспечивает детерминированное завершение: вы всегда знаете, что завершители будут вызваны и когда именно это произойдет. Как видно из предыдущего обсуждения, в CLR детерминированное завершение не поддерживается. Нельзя точно предсказать, когда будет вызван завершитель, а в некоторых случаях вызов завершителя вообще не гарантирован.

Некоторые программисты VB осуждают эти изменения и ругают Microsoft за то, что в VB .NET не поддерживается детерминированное завершение.

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

Лично я считаю, что преимущества сборки мусора компенсируют отказ от детерминированного завершения.

С другой стороны, эти изменения неизбежно отразятся на процессе программирования объектов в VB .NET.

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

Вместо того чтобы дожидаться вызова завершителя для освобождения ресурсов, в .NET рекомендуется использовать другой подход: когда надобность в объекте отпадает, работающий с ним клиент требует выполнить все необходимые действия по деинициализации и освобождению ресурсов. По общепринятым правилам для этой цели обычно используется метод Dispose. Деинициализация в методе Dispose позволяет отказаться от вызова завершителей (в результате из процесса сборки мусора исключается лишняя стадия, что улучшает быстродействие программы). Также часто встречается комбинированное решение: метод Dispose освобождает ресурсы по запросу клиента, а завершитель выполняет окончательную деинициализацию в том случае, если метод не вызывался.

Одна из возможных реализаций использована в приложении InitAndDestract. В этом примере класс Component1 объявляется производным от класса System. ComponentModel .Component. Класс Component может использовать дизайнер компонентов, обеспечивающий включение других компонентов. Класс System. ComponentModel .Component также наследует интерфейс IDisposable, в котором определяется метод Dispose. Реализация этого метода по умолчанию вызывает методы Dispose внутренних компонентов, что помогает автоматизировать процесс деинициализации.

В классе Component1 продемонстрированы следующие возможности:

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

Определение компонента приведено в листинге 6.2 (код конструктора частично опущен).

Листинг 6.2. Пример InitAndDestruct

' Инициализация и уничтожение

' Copyright ® 2001 by Desaware Inc.

Public tl

Inherits System.ComponentModel.Component

Shared Sub New()

MsgBox ("Shared component initializer called") 

End Sub

Public Sub New()

 MyBase.New()

' Следующий вызов необходим для дизайнера компонентов 1 InitializeComponent()

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

' InitializeComponent()

MsgBox ("My component instance created")

End Sub

Protected Overrides Sub Finalize()

MsgBox ("My component finalizer called")

 End Sub '

Public Overloads Overrides Sub Dispose()

MyBase.Dispose()

MsgBox ("I've been disposed")

 End Sub

End

Для тестирования компонента используется обработчик события Click, который создает и удаляет объект:

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

 ByVal e As System.EventArgs) Handles buttonl.Click

Dim obj As New Componentl()

' Завершив работу с объектом, вызовите метод Dispose (см. текст)

obj .Dispose() 

End Sub 

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

Клиент должен помнить о необходимости вызова Dispose. К счастью, во многих случаях .NET решает эту задачу за вас. Например, элемент, размещаемый на форме, включается в коллекцию Controls формы. Когда форма выполняет собственную деинициализацию (это происходит в тот момент, когда среда вызывает метод Dispose для формы), она, в свою очередь, вызывает методы Dispose всех элементов в коллекции Controls. Тем не менее среда ничего не знает о компонентах, создаваемых в программе (таких, как компонент Component1 в рассмотренном примере). Для таких объектов вы должны вызвать Dispose самостоятельно.

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

Ниже перечислены некоторые рекомендации, относящиеся к завершению объектов.

  •  По возможности избегайте решений, требующих деинициализации объектов. Не определяйте завершитель, если без него можно обойтись.
  •  Если ваш объект требует деинициализации, реализуйте метод Dispose.
  •  Рассмотрите возможность создания объекта-контейнера (производного от System.ComponentModel .Container) и включения в него всех используемых компонентов. Вызов Dispose для объекта-контейнера приводит к автоматическому вызову методов Dispose всех объектов, содержащихся в нем. Помните, что программа-мастер (wizard) часто автоматически генерирует соответствующий код для компонентов, находящихся под ее управлением.
  •  Даже если вы реализовали метод Dispose, подумайте, не стоит ли реализовать завершитель для проведения необходимой деинициализации на случай, если метод Dispose не вызывался в программе. Для некоторых типов компонентов (например, элементов управления) в вызове Dispose можно практически не сомневаться, поскольку элементы должны входить в коллекцию Controls и для них метод Dispose вызывается средой автоматически1. Но если вы создаете компонент, который должен использоваться внешними клиентами, программисты могут забыть о вызове Dispose, сколько бы вы ни твердили об этом в документации.
  •  В Windows NT/2000/XP большинство системных ресурсов освобождается автоматически при выходе из приложения. В Windows 95/98/ME освобождение ресурсов не столь надежно.

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

Воскрешение объектов

Сценарий, о котором я собираюсь поведать, выглядит совершенно фантастически. Несомненно, он полностью противоречит опыту практически любого программиста VB.

Рассмотрим следующую последовательность событий.

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

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

Рассмотрим класс Resurrected из проекта Resurrection

Public ted

Public mycontainer As Forml

Public Sub AreYouThereO

HsgBox ("I am here") End Sub

Protected Overrides Sub FinalizeO

MsgBox ("I'm being finalized")

mycontainer.PriorObject = Me 

End Sub

End

Открытому свойству mycontainer присваивается ссылка на форму-контейнер. Также имеется метод с именем AreYouThere, который просто доказывает, что объект еще существует. У формы имеется открытое свойство с именем PriorObject, содержащее ссылку на объект Resurrected. Циклическая ссылка (форма ссылается на объект, а объект ссылается на форму) в VB .NET не вызывает проблем. Как только форма перестает ссылаться на объект, он будет передан сборщику мусора.

Помимо свойства PriorObject в форме определены четыре события для четырех кнопок: Create & Delete, ForceGC, Check Prior Object, Turn Finalizer Back On.

  •  Кнопка Create & Delete инициирует событие cmdCreate_Click. Обработчик этого события создает объект и присваивает ссылку на форму свойству mycontainer. Это позволяет объекту обращаться к свойству PriorObject формы.
  •  Кнопка ForceGC инициирует событие cmdGC_Click. Обработчик этого события форсирует немедленную сборку мусора и ожидает выполнения всех завершителей.
  •  Кнопка Check Prior Object инициирует событие cmdPrior_Click. Обработчик этого события проверяет действительность свойства PriorObject формы. Если свойство действительно, вызывается метод AreYouThere.
  •  Кнопка Turn Finalizer Back On инициирует событие cmd Priоr_Click. Обработчик этого события активизирует завершитель объекта (см. ниже). Содержимое модуля формы приведено в листинге 6.3.

Листинг 6.3. Модуль формы приложения Resurrection 

Public PriorObject As Resurrected

Private Sub cmdRef1nal1ze_CHck(ByVal sender As Object,

ByVal e As System.EventArgs) Handles crodReflnalize.Click

If Not PriorObject Is Nothing Then

GC.ReReg1sterForF1nalize (PriorObject)

 End If

PriorObject = Nothing

End Sub

Private Sub cmdPr1or_CHck(ByVal sender As Object,

ByVal e As System.EventArgs) Handles cmdPMor.Click 

If Not PriorObject Is Nothing Then

' Доказательство того, что завершенный объект ]

 ' все еще существует! PriorObject.AreYouThere()

End If

 End Sub

Private Sub cmdCreate_CUck(ByVal sender As Object,

ByVal e As System.EventArgs) Handles cmdCreate.Click 

Dim obj As New ResurrectedO

' Объект получает ссылку на форму,

 obj.mycontainer = Me 

End Sub

Private Sub cmdGC_Click(ByVal sender As Object.

ByVal e As System.EventArgs) Handles cmdGC.Click

' Выполнить принудительную сборку мусора.

GC.Collect()

GC.WaitForPendingFinalizersO 

End Sub

End

За «воскрешением» объектов можно понаблюдать на примере проекта Resurrection (см. листинг 6.3).

Выполните следующие действия.

1. Нажмите кнопку Create & Destroy — приложение создает объект, присваивает его свойству my container ссылку на форму и освобождает объект (при выходе из функции форма уже не содержит переменную со ссылкой на объект).

2. Нажмите кнопку Force GC, форсирующую немедленную сборку мусора и завершение объектов. В процессе завершения объект использует свое свойство mycontainer и создает в свойстве PriorObject формы ссылку на себя. Другими словами, на объект снова существует ссылка в переменной Form (корневого уровня).

3. Нажмите кнопку Check Prior Object — как видите, объект, для которого был вызван завершитель, продолжает существовать!

4. Закройте приложение. Хотя завершители должны вызываться при выходе из приложения, вы увидите, что завершитель нашего объекта не вызывается.

Как такое возможно? Выполненный завершитель не выполняется снова, даже если объект «воскрес»!

Снова запустите программу Resurrection, повторите действия 1-3 и нажмите кнопку Turn Finalizer Back On; обработчик этой кнопки тоже присваивает переменной Pri orObj ect значение Nothing. Снова нажмите кнопку Force GC — вы увидите, что завершитель выполняется повторно. Метод GC.ReRegisterForFinalize сообщает сборщику мусора о том, что объект вернулся к жизни и его завершитель должен быть снова выполнен при повторной сборке мусора1.

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

1 При закрытии приложения окно сообщения не выводится: умный VB .NET понимает, что приложение завершает работу, и не выводит окна сообщений в это время.

Однако понимание этого процесса очень важно для овладения одной важной методикой, которой можно и нужно пользоваться.

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

 

Итоги

Управление памятью в .NET не только имеет далеко идущие последствия для языка VB .NET, но и значительно влияет на общий подход к проектированию приложений.

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

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

Из этого следует, что программисты VB .NET должны избегать освобождения ресурсов в методе Finalize. Вместо этого следует унаследовать интерфейс IDisposable1 и реализовать метод Di spose. Метод-завершитель должен использоваться как последнее средство для выполнения деинициализации в ситуациях, когда метод Dispose не вызывался. При вызове Dispose для повышения быстродействия можно запретить вызов завершителя методом GC.SupressFinalize.

Наконец, вы узнали, что «мертвые» объекты могут вернуться к жизни, если из-за ошибок объектной модели на стадии завершения на объект будет создана ссылка.

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

1 Или объявить класс производным от класса, наследующего интерфейс IDisposable.

Назад   Вперёд

 


Инфо
Сайт создан: 20 июня 2015 г.
Рейтинг@Mail.ru
Главная страница