Материал: Объектно-ориентированное программирование на языке C#. Карпеев Д.О., Транин В.А

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

У всех классов имеются конструкторы, независимо от того, определите вы их или нет, поскольку в С# автоматически предоставляется конструктор, используемый по умолчанию и инициализирующий все переменные экземпляра их значениями по умолчанию. Для большинства типов данных значением по умолчанию является нулевое, для типа bool — значение false, а для ссылочных типов — пустое значение. Но как только вы определите свой собственный конструктор, то конструктор по умолчанию больше не используется.

Ниже приведен простой пример применения конструктора.

// Простой конструктор.

using System;

class MyClass {

public int x;

public MyClass() {

x = 10;

}

}

class ConsDemo {

static void Main() {

MyClass tl = new MyClass();

MyClass t2 = new MyClass();

Console.WriteLine(tl,x + " " + t2.x);

}

}

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

При использовании оператора new свободная память для создаваемых объектов динамически распределяется из доступной буферной области оперативной памяти. Разумеется, оперативная память не бесконечна, и поэтому свободно доступная память рано или поздно исчерпывается. Это может привести к неудачному выполнению оператора new из-за нехватки свободной памяти для создания требуемого объекта. Именно по этой причине одной из главных функций любой схемы динамического распределения памяти является освобождение свободной памяти от неиспользуемых объектов, чтобы сделать ее доступной для последующего перераспределения. Во многих языках программирования освобождение распределенной ранее памяти осуществляется вручную. Например, в C++ для этой цели служит оператор delete. Но в С# применяется другой, более надежный подход: "сборка мусора".

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

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

В языке С# имеется возможность определить метод, который будет вызываться непосредственно перед окончательным уничтожением объекта системой "сборки мусора". Такой метод называется деструктором и может использоваться в ряде особых случаев, чтобы гарантировать четкое окончание срока действия объекта. Например, деструктор может быть использован для гарантированного освобождения системного ресурса, задействованного освобождаемым объектом. Следует, однако, сразу же подчеркнуть, что деструкторы — весьма специфические средства, применяемые только в редких, особых случаях. И, как правило, они не нужны.

Ниже приведена общая форма деструктора:

~имя_класса() {

// код деструктора

}

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

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

2.4. Инкапсуляция в c#

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

При работе с инкапсуляцией всегда следует принимать во внимание то, какие аспекты типа видимы различным частям приложения. В частности, типы (классы, интерфейсы, структуры, перечисления и делегаты), а также их члены (свойства, методы, конструкторы и поля) определяются с использованием определенного ключевого слова, управляющего "видимостью" элемента другим частям приложения. Хотя в С# определены многочисленные ключевые слова для управления доступом, их значение может отличаться в зависимости от места применения (к типу или члену). В табл. 2 описаны роли и применение модификаторов доступа.

Таблица 2

Модификаторы доступа С#

Модификатор доступа (к чему применяется)

Назначение

public (типы или члены типов)

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

private (члены типов или вложенные типы)

Приватные (private) элементы могут быть доступны только в классе (или структуре), в котором они определены

protected (члены типов или вложенные типы)

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

internal (типы или члены типов)

Внутренние (internal) элементы доступны только в пределах текущей сборки. Таким образом, если в библиотеке классов .NET определен набор внутренних типов, то другие сборки не смогут ими пользоваться

protected internal (члены типов или вложенные типы)

Когда ключевые слова protected и internal комбинируются в объявлении элемента, такой элемент доступен внутри определяющей его сборки, определяющего класса и всех его наследников

Концепция инкапсуляции вращается вокруг принципа, гласящего, что внутренние данные объекта не должны быть напрямую доступны через экземпляр объекта. Вместо этого, если вызывающий код желает изменить состояние объекта, то должен делать это через методы доступа (accessor, или метод get) и изменения (mutator, или метод set). В С# инкапсуляция обеспечивается на синтаксическом уровне с использованием ключевых слов public, private, internal и protected. Чтобы проиллюстрировать необходимость в службах инкапсуляции, предположим, что создано следующее определение класса:

// Класс с единственным общедоступным полем.

class Book

{

public int numberOfPages;

}

Проблема с общедоступными данными состоит в том, что сами по себе эти данные не имеют возможности распознать, является ли присваиваемое значение допустимым в рамках существующих бизнес-правил системы. Как известно, верхний предел значений для типа int в С# довольно велик B 147 483 647), поэтому компилятор разрешит следующее присваивание:

//Хм… Ничего себе — мини-новелла!

static void Main(string[] args)

{

Book miniNovel = new Book();

miniNovel.numberOfPages = 30000000;

}

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

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

• определение пары методов доступа и изменения;

• определение свойства .NET.

Свойство — способ доступа к внутреннему состоянию объекта, имитирующий переменную некоторого типа. Обращение к свойству объекта реализовано через вызов функции. При попытке задать значение данного свойства вызывается один метод, а при попытке получить значение данного свойства — другой. Свойства в C# — поля с логическим блоком, в котором есть ключевые слова get и set.

Пример класса со свойством:

public class Date

{

private int month = 7;

public int Month

{

get

{

return month;

}

set

{

if ((value > 0) && (value < 13))

{

month = value;

}

}

}

}

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

Чтобы упростить процесс простой инкапсуляции данных полей, можно применять синтаксис автоматических свойств, который появился в C# в поздних версиях платформы .NET 3.5. Как следует из названия, это средство перекладывает работу по определению лежащего в основе приватного поля и связанного свойства С# на компилятор, используя небольшое усовершенствование синтаксиса. Рассмотрим класс Car, в котором этот синтаксис применяется для быстрого создания трех свойств:

class Car

{

// Автоматические свойства

public string PetName { get; set; }

public int Speed { get; set; }

public string Color { get; set; }

}

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

// Свойство только для чтения? Ошибка!

public int MyReadOnlyProp { get; }

// Свойство только для записи? Ошибка!

public int MyWriteOnlyProp { set; }

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

3. Индексаторы и свойства

3.1. Индексаторы

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

тип_элемента this[int индекс] {

// Аксессор для получения данных,

get {

// Возврат значения, которое определяет индекс.

}

// Аксессор для установки данных,

set {

// Установка значения, которое определяет индекс.

}

}

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

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

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

Наличие обоих аксессоров, get и set, в индексаторе не является обязательным. Так, можно создать индексатор только для чтения, реализовав в нем один лишь аксессор get, или же индексатор только для записи с единственным аксессором set.

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

3.2. Свойства

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

Свойство так же, как и индексаторы состоит из имени и аксессоров get и set. Аксессоры служат для получения и установки значения переменной. Главное преимущество свойства заключается в том, что его имя может быть использовано в выражениях и операторах присваивания аналогично имени обычной переменной, но в действительности при обращении к свойству по имени автоматически вызываются его аксессоры get и set. Аналогичным образом используются аксессоры get и set индексатора.

Ниже приведена общая форма свойства:

тип имя{

get{

// код аксессора для чтения из поля

}

set{

// код аксессора для записи в поле

}

где тип обозначает конкретный тип свойства, например int, а имя — присваиваемое свойству имя. Как только свойство будет определено, любое обращение к свойству по имени приведет к автоматическому вызову соответствующего аксессора. Кроме того, аксессор set принимает неявный параметр value, который содержит значение, присваиваемое свойству.

Источник: https://studfile.net/preview/16562554/