Конструкторы и деструкторы си шарп

Обновлено: 13.05.2024

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

Чем отличается деструктор от финализатора

Деструктор — это метод для деинициализации объекта. Здесь важно упомянуть о такой штуке, как deterministic destruction. То есть мы точно знаем, когда объект будет удален. Чаще всего это происходит, когда заканчивается область видимости объекта, или программист явно освобождает память(в с/с++).

А вот определение финализатора из Википедии.

Финализатор — это метод класса, который автоматически вызывается средой исполнения в промежутке времени между моментом, когда объект этого класса опознаётся сборщиком мусора как неиспользуемый, и моментом удаления объекта (освобождения занимаемой им памяти). Это уже обратная штука — nondeterministic destruction.
То есть главный минус финализатора в том, что мы не знаем, когда он вызовется. Это может создать огромное количество проблем.

Теперь пойдем еще дальше и залезем в спецификацию CLI(ECMA-335). Здесь вот что написано.

A class definition that creates an object type can supply an instance method (called a finalizer) to be called
when an instance of the class is no longer reachable.

Это, несомненно, описание финализатора, хотя на мой взгляд, немного неточное.

Далее, идем на msdn. Ни в одной статье не встречается слово finalizer в чистом виде — зато почти всегда используется слово деструктор. Возникает закономерный вопрос — почему люди называют деструктором то, что им не является. Получается, что майкрософтовские разработчики сознательно поменяли значение этого слова. И вот почему.

Вывод — либо я идиот, и совсем все неправильно понял(а вероятность этого довольно высока), либо нужно смириться с этим, и называть деструктором то, что внешне на него похоже(тильда, привет), но по сути является финализатором. Надо же как-то жить в этом мире.

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

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

Конструкторы могут иметь модификаторы, которые указываются перед именем конструктора. Так, в данном случае, чтобы конструктор был доступен вне класса Person, он определен с модификатором public .

Определив конструктор, мы можем вызвать его для создания объекта Person:

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

Подобным образом мы можем определять и другие конструкторы в классе. Например, изменим класс Person следующим образом:

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

Консольный вывод данной программы:

Ключевое слово this

Ключевое слово this представляет ссылку на текущий экземпляр/объект класса. В каких ситуациях оно нам может пригодиться?

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

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

Цепочка вызова конструкторов

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

В данном случае первый конструктор вызывает второй, а второй конструктор вызывает третий. По количеству и типу параметров компилятор узнает, какой именно конструктор вызывается. Например, во втором конструкторе:

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

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

И если при вызове конструктора мы не передаем значение для какого-то параметра, то применяется значение по умолчанию.

Инициализаторы объектов

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

С помощью инициализатора объектов можно присваивать значения всем доступным полям и свойствам объекта в момент создания. При использовании инициализаторов следует учитывать следующие моменты:

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

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

Инициализаторы удобно применять, когда поле или свойство класса представляет другой класс:

Обратите внимание, как устанавливается поле company :

Деконструкторы

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

Например, пусть у нас есть следующий класс Person:

В этом случае мы могли бы выполнить декомпозицию объекта Person так:

Значения переменным из деконструктора передаюся по позиции. То есть первое возвращаемое значение в виде параметра personName передается первой переменной - name, второе возващаемое значение - переменной age.

По сути деконструкторы это не более,чем синтаксический сахар. Это все равно, что если бы мы написали:

При получении значений из декоструктора нам необходимо предоставить столько переменных, сколько деконструктор возвращает значений. Однако бывает, что не все эти значения нужны. И вместо возвращаемых значений мы можм использовать прочерк _ . Например, нам надо получить только возраст пользователя:

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

Примечания

  • В структурах определение методов завершения невозможно. Они применяются только в классах.
  • Каждый класс может иметь только один метод завершения.
  • Методы завершения не могут быть унаследованы или перегружены.
  • Методы завершения невозможно вызвать. Они запускаются автоматически.
  • Метод завершения не принимает модификаторов и не имеет параметров.

Например, ниже показано объявление метода завершения для класса Car .

Метод завершения можно также реализовать как определение тела выражения, как показано в следующем примере.

Метод завершения неявно вызывает метод Finalize для базового класса объекта. В связи с этим вызов метода завершения неявно преобразуется в следующий код:

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

Не следует использовать пустые методы завершения. Если класс содержит метод завершения, то в очереди метода Finalize создается запись. Эта очередь обрабатывается сборщиком мусора. Когда сборщик мусора обрабатывает очередь, он вызывает каждый метод завершения. Ненужные методы завершения, включая пустые методы завершения, методы завершения, вызывающие только методы завершения базового класса, или методы завершения, которые вызывают только условно созданные методы, приводят к ненужной потере производительности.

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

Если необходимо надежно выполнить очистку при наличии приложения, зарегистрируйте обработчик для события System.AppDomain.ProcessExit. Этот обработчик обеспечит вызов IDisposable.Dispose() (или, IAsyncDisposable.DisposeAsync()) для всех объектов, для которых требуется очистка перед выходом из приложения. Поскольку нельзя вызвать метод завершения напрямую и нельзя гарантировать, что сборщик мусора вызовет все методы завершения до выхода, необходимо использовать Dispose или DisposeAsync , чтобы освободить ресурсы.

Использование методов завершения для освобождения ресурсов

Освобождение ресурсов явным образом

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

Дополнительные сведения об очистке ресурсов см. в следующих статьях:

Пример

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

В принципе ранее уже использовались классы. Например, тип string , который представляет строку, фактически является классом. Или, например, класс Console , у которого метод WriteLine() выводит на консоль некоторую информацию. Теперь же посмотрим, как мы можем определять свои собственные классы.

По сути класс представляет новый тип, который определяется пользователем. Класс определяется с помощью ключевого слова сlass :

После слова class идет имя класса и далее в фигурных скобках идет собственно содержимое класса. Например, определим в файле Program.cs класс Person, который будет представлять человека:

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

Поля и методы класса

Класс может хранить некоторые данные. Для хранения данных в классе применяются поля . По сути поля класса - это переменные, определенные на уровне класса.

Кроме того, класс может определять некоторое поведение или выполняемые действия. Для определения поведения в классе применяются методы.

Итак, добавим в класс Person поля и методы:

В данном случае в классе Person определено поле name , которое хранит имя, и поле age , которое хранит возраст человека. В отличие от переменных, определенных в методах, поля класса могут иметь модификаторы, которые указываются перед полем. Так, в данном случае, чтобы все поля были доступны вне класса Person поля определены с модификатором public .

При определении полей мы можем присвоить им некоторые значения, как в примере выше в случае переменной name . Если поля класса не инициализированы, то они получают значения по умолчанию. Для переменных числовых типов это число 0.

Также в классе Person определен метод Print() . Методы класса имеют доступ к его поля, и в данном случае обращаемся к полям класса name и age для вывода их значения на консоль. И чтобы этот метод был виден вне класса, он также определен с модификатором public .

Создание объекта класса

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

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

Конструктор по умолчанию

Если в классе не определено ни одного конструктора (как в случае с нашим классом Person), то для этого класса автоматически создается пустой конструктор по умолчанию, который не принимает никаких параметров.

Теперь создадим объект класса Person:

Для создания объекта Person используется выражение new Person() . В итоге после выполнения данного выражения в памяти будет выделен участок, где будут храниться все данные объекта Person. А переменная tom получит ссылку на созданный объект, и через эту переменную мы можем использовать данный объект и обращаться к его функциональности.

Обращение к функциональности класса

Для обращения к функциональности класса - полям, методам (а также другим элементам класса) применяется точечная нотация точки - после объекта класса ставится точка, а затем элемент класса:

Например, обратимся к полям и методам объекта Person:

Консольный вывод данной программы:

Константы классы

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

Здесь в классе Person определена константа type , которая хранит название класса:

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

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

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

Однако если мы хотим обратиться к константе вне ее класса, то для обращения необходимо использовались имя класса:

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

Добавление класса в Visual Studio

Обычно классы помещаются в отдельные файлы. Нередко для одного класса предназначен один файл. И Visual Studio предоставляет по умолчанию встроенные шаблоны для добвления класса.

Для добавления класса нажмем в Visual Studio правой кнопкой мыши на название проекта:

В появившемся контекстном меню выберем пункт Add -> New Item. (или Add -> Class. )

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

В качестве названия класса можно вводить как Person, так и Person.cs. И после нажатия на кнопку добавления в проект будет добавлен новый класс:

В файле Person.cs определим следующий код:

Здесь определен класс Person с одним полем name и методом Print.

В файле Program.cs , который представляет основной файл программы используем класс Person:

Таким образом, мы можем определять классы в отдельных файлах и использовать их в программе.

Освобождение неуправляемых ресурсов подразумевает реализацию одного из двух механизмов:

Реализация классом интерфейса System.IDisposable

Создание деструкторов

Если вы вдруг программировали на языке C++, то наверное уже знакомы с концепцией деструкторов. Метод деструктора носит имя класса (как и конструктор), перед которым стоит знак тильды ( ~ ).

Деструкторы можно определить только в классах. Деструктор в отличие от конструктора не может иметь модификаторов доступа и параметры. Деструктор не может быть унаследован или переопределен. При этом каждый класс может иметь только один деструктор.

Например, определим в классе Person простейший деструктор:

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

Метод Finalize уже определен в базовом для всех типов классе Object, однако данный метод нельзя так просто переопределить. И фактическая его реализация происходит через создание деструктора.

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

Стоит отметить, что точное время вызова деструктора не определено. Кроме того, при финализации двух связанных объектов порядок вызова деструкторов не гарантируется. То есть если объект A хранит ссылку на объект B, и при этом оба эти объекта имеют деструкторы, то для объекта B деструктор моет уже отработать в то время, как для объекта A деструктор только начнет работу.

И здесь мы можем столкнуться со следующей проблемой: а что если нам немедленно надо вызвать деструктор и освободить все связанные с объектом неуправляемые ресурсы? В этом случае мы можем использовать второй подход - реализацию интерфейса IDisposable.

Интерфейс IDisposable

Интерфейс IDisposable объявляет один единственный метод Dispose , в котором при реализации интерфейса в классе должно происходить освобождение неуправляемых ресурсов. Например:

В данном коде используется конструкция try. finally. По сути эта конструкция по функционалу в общем эквивалентна следующим двум строкам кода:

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

Комбинирование подходов

Мы рассмотрели два подхода. Какой же из них лучше? С одной стороны, метод Dispose позволяет в любой момент времени вызвать освобождение связанных ресурсов, а с другой - программист, использующий наш класс, может забыть поставить в коде вызов метода Dispose. В общем бывают различные ситуации. И чтобы сочетать плюсы обоих подходов мы можем использовать комбинированный подход. Microsoft предлагает нам использовать следующий формализованный шаблон:

Логика очистки реализуется перегруженной версией метода Dispose(bool disposing) . Если параметр disposing имеет значение true, то данный метод вызывается из публичного метода Dispose, если false - то из деструктора.

При вызове деструктора в качестве параметра disposing передается значение false, чтобы избежать очистки управляемых ресурсов, так как мы не можем быть уверенными в их состоянии, что они до сих пор находятся в памяти. И в этом случае остается полагаться на деструкторы этих ресурсов. Ну и в обоих случаях освобождаются неуправляемые ресурсы.

Еще один важный момент - вызов в методе Dispose метода GC.SuppressFinalize(this) . GC.SuppressFinalize не позволяет системе выполнить метод Finalize для данного объекта. Если же в классе деструктор не определен, то вызов этого метода не будет иметь никакого эффекта.

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

Общие рекомендации по использованию Finalize и Dispose

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

После вызова метода Dispose необходимо блокировать у объекта вызов метода Finalize с помощью GC.SuppressFinalize

При создании производных классов от базовых, которые реализуют интерфейс IDisposable, следует также вызывать метод Dispose базового класса:

Отдавайте предпочтение комбинированному шаблону, реализующему как метод Dispose, так и деструктор

Читайте также: