Выбери формат для чтения
Загружаем конспект в формате docx
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Введение в объектно-ориентированное программирование
Принципы ООП проще всего понять на примере программ моделирования. В реальном мире каждый предмет или процесс обладает набором статических и динамических характеристик, иными словами, свойствами и поведением. Поведение объекта зависит от его состояния и внешних воздействий. Например, объект "автомобиль" никуда не поедет, если в баке нет бензина, а если повернуть руль, изменится положение колес.
Понятие объекта в программе совпадает с обыденным смыслом этого слова: объект представляется как совокупность данных, характеризующих его состояние, и функций их обработки, моделирующих его поведение. Вызов функции на выполнение часто называют посылкой сообщения объекту. Например, вызов функции "повернуть руль" интерпретируется как посылка сообщения "автомобиль, поверни руль!".
При создании объектно-ориентированной программы предметная область представляется в виде совокупности объектов. Выполнение программы состоит в том, что объекты обмениваются сообщениями. Это позволяет использовать при программировании понятия, более адекватно отражающие предметную область.
При представлении реального объекта с помощью программного необходимо выделить в первом его существенные особенности. Их список зависит от цели моделирования. Например, объект "крыса" с точки зрения биолога, изучающего миграции, ветеринара или, скажем, повара будет иметь совершенно разные характеристики. Выделение существенных с той или иной точки зрения свойств называется абстрагированием. Таким образом, программный объект — это абстракция.
Важным свойством объекта является его обособленность. Детали реализации объекта, то есть внутренние структуры данных и алгоритмы их обработки, скрыты от пользователя объекта и недоступны для непреднамеренных изменений. Объект используется через его интерфейс — совокупность правил доступа.
Скрытие деталей реализации называется инкапсуляцией (от слова "капсула"). Ничего сложного в этом понятии нет: ведь и в обычной жизни мы пользуемся объектами через их интерфейсы. Сколько информации пришлось бы держать в голове, если бы для просмотра новостей надо было знать устройство телевизора!
Таким образом, объект является "черным ящиком", замкнутым по отношению к внешнему миру. Это позволяет представить программу в укрупненном виде — на уровне объектов и их взаимосвязей, а следовательно, управлять большим объемом информации и успешно отлаживать сложные программы.
Сказанное можно сформулировать более кратко и строго: объект — это инкапсулированная абстракция с четко определенным интерфейсом.
Инкапсуляция позволяет изменить реализацию объекта без модификации основной части программы, если его интерфейс остался прежним. Простота модификации является очень важным критерием качества программы: ведь любой программный продукт в течение своего жизненного цикла претерпевает множество изменений и дополнений.
Кроме того, инкапсуляция позволяет использовать объект в другом окружении и быть уверенным, что он не испортит не принадлежащие ему области памяти, а также создавать библиотеки объектов для применения во многих программах. Инкапсуляция наряду с наследованием и полиморфизмом считаются тремя "китами", на которых стоит ООП.
Каждый год в мире пишется огромное количество новых программ, и важнейшее значение приобретает возможность многократного использования кода. Преимущество объектно-ориентированного программирования состоит в том, что для объекта можно определить наследников, корректирующих или дополняющих его поведение. При этом нет необходимости не только повторять исходный код родительского объекта, но даже иметь к нему доступ.
Наследование является мощнейшим инструментом ООП и применяется для следующих взаимосвязанных целей:
• исключения из программы повторяющихся фрагментов кода;
• упрощения модификации программы;
• упрощения создания новых программ на основе существующих.
Кроме того, только благодаря наследованию появляется возможность использовать объекты, исходный код которых недоступен, но в которые требуется внести изменения.
Наследование позволяет создавать иерархии объектов. Иерархия представляется в виде дерева, в котором более общие объекты располагаются ближе к корню, а более специализированные — на ветвях и листьях. Наследование облегчает использование библиотек объектов, поскольку программист может взять за основу объекты, разработанные кем-то другим, и создать наследников с требуемыми свойствами.
Объект, на основании которого строится новый объект, называется родительским объектом, объектом-предком, базовым классом, или суперклассом, а унаследованный от него объект — потомком, подклассом, или производным классом.
ООП позволяет писать гибкие, расширяемые и читабельные программы. Во многом это обеспечивается благодаря полиморфизму, под которым понимается возможность во время выполнения программы с помощью одного и того же имени выполнять разные действия или обращаться к объектам разного типа. Чаще всего понятие полиморфизма связывают с механизмом виртуальных методов.
Подводя итог сказанному, сформулирую достоинства ООП:
• использование при программировании понятий, близких к предметной области;
• возможность успешно управлять большими объемами исходного кода благодаря инкапсуляции, то есть скрытию деталей реализации объектов и упрощению структуры программы;
• возможность многократного использования кода за счет наследования;
• сравнительно простая возможность модификации программ;
• возможность создания и использования библиотек объектов.
Эти преимущества особенно явно проявляются при разработке программ большого объема и классов программ. Однако ничто не дается даром: создание объектно-ориентированной программы представляет собой весьма непростую задачу. Чтобы эффективно использовать готовые объекты из библиотек, необходимо освоить большой объем достаточно сложной информации. Неграмотное же применение ООП способно привести к созданию излишне сложных программ, которые невозможно отлаживать и усовершенствовать.
Понятие класса
Для представления объектов в языках C#, Java, С++, Delphi и т. п. используется понятие класс, аналогичное обыденному смыслу этого слова в контексте "класс членистоногих", "класс млекопитающих", "класс задач" и т. п. Класс является обобщенным понятием, определяющим характеристики и поведение некоторого множества конкретных объектов этого класса, называемых экземплярами класса.
"Классический" класс содержит данные, задающие свойства объектов класса, и функции, определяющие их поведение. В последнее время в класс часто добавляется третья составляющая — события, на которые может реагировать объект класса.
Все классы библиотеки .NET, а также все классы, которые создает программист в среде .NET, имеют одного общего предка — класс object, и организованы в единую иерархическую структуру. Внутри нее классы логически сгруппированы в так называемые пространства имен, которые служат для упорядочивания имен классов и предотвращения конфликтов имен: в разных пространствах имена могут совпадать. Пространства имен могут быть вложенными, их идея аналогична знакомой вам иерархической структуре каталогов на компьютере.
Любая программа, создаваемая в .NET, использует пространство имен System. В нем определены классы, которые обеспечивают базовую функциональность, например, поддерживают выполнение математических операций, управление памятью и ввод-вывод.
Обычно в одно пространство имен объединяют взаимосвязанные классы. Например,пространство System.Net содержит классы, относящиеся к передаче данных по сети, System.Windows.Forms — элементы графического интерфейса пользователя, такие как формы, кнопки и т. д. Имя каждого пространства имен представляет собой неделимую сущность, однозначно его определяющую.
Описание класса
Класс является типом данных, определяемым пользователем. Он должен представлять собой одну логическую сущность, например, являться моделью реального объекта или процесса. Элементами класса являются данные и функции, предназначенные для их обработки.
Описание класса содержит ключевое слово class, за которым следует его имя, а далее в фигурных скобках — тело класса, то есть список его элементов. Кроме того, для класса можно задать его базовые классы (предки) и ряд необязательных атрибутов и спецификаторов, определяющих различные характеристики класса:
[ атрибуты ] [ спецификаторы ] class имя_класса [ : предки ]
тело_класса
Обязательными являются только ключевое слово class, имя и тело класса. Тело класса — это список описаний его элементов, заключенный в фигурные скобки. Список может быть пустым, если класс не содержит ни одного элемента. Таким образом, простейшее описание класса может выглядеть так:
class Simple {}
Спецификаторы определяют свойства класса, а также доступность класса для других элементов программы. Возможные значения спецификаторов перечислены в таблице 1. Класс можно описывать непосредственно внутри пространства имен или внутри другого класса. В последнем случае класс называется вложенным.
Таблица 1. Спецификаторы класса
№
Спецификатор
Описание
1
new
Используется для вложенных классов. Задает новое описание класса взамен унаследованного от предка. Применяется в иерархиях объектов
2
public
Доступ не ограничен
3
protected
Используется для вложенных классов. Доступ только из элементов данного и производных классов
4
internal
Доступ только из данной программы (сборки)
5
protected
internal
Доступ только из данного и производных классов или из данной программы (сборки)
6
private
Используется для вложенных классов. Доступ только из элементов класса, внутри которого описан данный класс
7
abstract
Абстрактный класс. Применяется в иерархиях объектов, рассматривается в главе 8
8
sealed
Бесплодный класс. Применяется в иерархиях объектов, рассматривается в главе 8
9
static
Статический класс. Введен в версию языка 2.0. Рассматривается в разделе "Конструкторы"
Спецификаторы 2–6 называются спецификаторами доступа. Они определяют, откуда можно непосредственно обращаться к данному классу. Спецификаторы доступа могут присутствовать в описании только в вариантах, приведенных в таблице, а также могут комбинироваться с остальными спецификаторами.
Исключая вложенные классы, для остальных классов допускаются два спецификатора: public и internal (по умолчанию).
Класс определяет характеристики и поведение некоторого множества объектов этого класса, называемых экземплярами, или объектами, класса. Объекты создаются явным или неявным образом, то есть либо программистом, либо системой. Программист создает экземпляр класса с помощью операции new, например:
Simple a = new Simple();// создание экземпляра класса Simple
Simple b = new Simple();//создание другого экземпляра класса Simple
Память под объекты выделяется в куче (область памяти под ссылочные значения). Для каждого объекта при его создании в памяти выделяется отдельная область, в которой хранятся его данные. Кроме того, в классе могут присутствовать статические элементы, которые существуют в единственном экземпляре для всех объектов класса. Часто статические данные называют данными класса, а остальные — данными экземпляра.
Функциональные элементы класса всегда хранятся в единственном экземпляре. Для работы с данными класса используются методы класса ( статические методы ), для работы с данными экземпляра — методы экземпляра, или просто методы.
Основные элементы класса - это поля и методы. Кроме того, в классе можно задавать другие элементы такие как: свойства, события, индексаторы, операции, конструкторы, деструкторы, а также типы ( рис. 14).
Рис. 14. Состав класса
Ниже приведено краткое описание всех элементов класса, изображенных на рисунке:
• Константы класса хранят неизменяемые значения, связанные с классом.
• Методы реализуют вычисления или другие действия, выполняемые классом или экземпляром.
• Свойства определяют характеристики объектов класса в совокупности со способами их задания и получения, то есть методами записи и чтения.
• Конструкторы реализуют действия по инициализации экземпляров или класса в целом.
• Деструкторы определяют действия, которые необходимо выполнить до того, как объект будет уничтожен.
• Индексаторы обеспечивают возможность доступа к элементам класса по их порядковому номеру (индексу).
• Операции задают действия с объектами с помощью знаков операций.
• События определяют уведомления, которые может генерировать класс.
• Типы — это типы данных, внутренние по отношению к классу.
Прежде чем начать изучение элементов класса, необходимо поговорить о присваивании и сравнении объектов.
Механизм выполнения присваивания один и тот же для величин любого типа, как ссылочного, так и значимого, однако результаты различаются. При присваивании значения копируется значение, а при присваивании ссылки — ссылка, поэтому после присваивания одного объекта другому мы получим две ссылки, указывающие на одну и ту же область памяти (рис. 15).
Рис. 15. Присваивание объектов
Аналогичная ситуация с операцией проверки на равенство. Величины значимого типа равны, если равны их значения. Величины ссылочного типа равны, если они ссылаются на одни и те же данные (на рисунке объекты b и c равны, но a не равно b даже при равенстве их значений или если они обе равны null ).
Данные: поля и константы
Данные, содержащиеся в классе, могут быть переменными или константами и задаются в соответствии с правилами. Переменные, описанные в классе, называются полями класса. При описании элементов класса можно также указывать атрибуты и спецификаторы, задающие различные характеристики элементов. Синтаксис описания элемента данных:
[ атрибуты ] [ спецификаторы ] [ const ] тип имя [ = начальное_значение ]
Возможные спецификаторы полей и констант перечислены в таблице 2. Для констант можно использовать только спецификаторы 1–6.
Таблица 2. Спецификаторы полей и констант класса
№
Спецификатор
Описание
1
new
Новое описание поля, скрывающее унаследованный элемент класса
2
public
Доступ к элементу не ограничен
3
protected
Доступ только из данного и производных классов
4
internal
Доступ только из данной сборки
5
protected
internal
Доступ только из данного и производных классов и из данной сборки
6
private
Доступ только из данного класса
7
static
Одно поле для всех экземпляров класса
8
readonly
Поле доступно только для чтения
9
volatile
Поле может изменяться другим процессом или системой
По умолчанию элементы класса считаются закрытыми ( private ). Для полей класса этот вид доступа является предпочтительным. Все методы класса имеют непосредственный доступ к его закрытым полям.
Внимание
Поля, описанные со спецификатором static, а также константы существуют в единственном экземпляре для всех объектов класса, поэтому к ним обращаются не через имя экземпляра, а через имя класса. Если класс содержит только статические элементы, экземпляр класса создавать не требуется. Именно этим фактом мы пользовались во всех предыдущих листингах.
Обращение к полю класса выполняется с помощью операции доступа (точка). Справа от точки задается имя поля, слева — имя экземпляра для обычных полей или имя класса для статических. В листинге 1 приведен пример простого класса Simple и два способа обращения к его полям.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleClasses1
{
class Simple
{
// Поле данных.
public int i = 1;
// Константа.
public const double d = 2.5;
// Статическое поле класса.
public static string s = " Simple ";
// Закрытое поле данных.
double z = 3;
}
class Program
{
static void Main(string[] args)
{
// Создание экземпляра класса Simple.
Simple x = new Simple();
// x.i - обращение к полю класса.
Console.WriteLine(x.i);
// Simple.d - обращение к константе.
Console.WriteLine(Simple.d);
// Обращение к статическому полю.
Console.WriteLine(Simple.s);
}
}
}
Листинг 1. Класс Simple, содержащий поля и константу
Поля со спецификатором readonly предназначены только для чтения. Установить значение такого поля можно либо при его описании, либо в конструкторе (конструкторы рассматриваются далее).
Методы
Методы определяют поведение класса. Метод — это функциональный элемент класса. Он реализует вычисления или другие действия, выполняемые классом или экземпляром.
Метод представляет собой законченный фрагмент кода, к которому можно обратиться по имени. Он описывается один раз, а вызываться может столько раз, сколько необходимо. Один и тот же метод может обрабатывать различные данные, переданные ему в качестве аргументов.
Синтаксис метода:
[ атрибуты ] [ спецификаторы ] тип имя_метода ( [ параметры ] )
тело_метода
Первая часть описания представляет собой заголовок метода или сигнатуру. Тело метода задаёт действия, выполняемые методом. Оно чаще всего представляет собой блок.
При описании методов можно использовать спецификаторы 1–7 из Таблицы 2, имеющие тот же смысл, что и для полей, а также спецификаторы virtual, sealed, override, abstract и extern, которые будут рассмотрены по мере необходимости. Методы со спецификатором доступа public составляют интерфейс класса — то, с чем работает пользователь.
Пример простейшего метода:
// Метод для получения значения z.
public double Get_z()
{
return z;
}
Тип определяет, значение какого типа вычисляется с помощью метода. Часто употребляется термин "метод возвращает значение". Если метод не возвращает никакого значения, в его заголовке задается тип void, а оператор return отсутствует.
С помощью параметров метод обменивается данными с вызывающим его методом. Параметр представляет собой локальную переменную, которая при вызове метода принимает значение соответствующего фактического параметра (параметра переданного при вызове). Область действия параметра — весь метод.
Например, чтобы выделить целую часть вещественной величины a, мы передаем ее в качестве аргумента в метод Truncate класса Math, а чтобы вывести значение переменной r на экран, мы передаем ее в метод WriteLine класса Console:
double a = 2.56;
double r = Math.Truncate(a);
Console.WriteLine(r);
При этом метод Truncate возвращает в точку своего вызова вещественное значение - целую часть a, которое присваивается переменной r, а метод WriteLine ничего не возвращает. Иллюстрация вызова метода приведена на рис. 16.
Рис. 16. Вызов метода
Внимание
Метод, не возвращающий значение, вызывается отдельным оператором, а метод, возвращающий значение, — в составе выражения в правой части оператора присваивания.
Параметры, описываемые в заголовке метода, определяют множество значений аргументов, которые можно передавать в метод. Список аргументов при вызове как бы накладывается на список параметров, поэтому они должны попарно соответствовать друг другу. Для каждого параметра должны задаваться его тип и имя. Например, заголовок метода Sin выглядит следующим образом:
public static double Sin( double a );
Имя метода вместе с количеством, типами и спецификаторами его параметров представляет собой сигнатуру метода. В классе не должно быть методов с одинаковыми сигнатурами.
В листинге 2 в класс Simple добавлены методы установки и получения значения поля z.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleClasses3
{
class Simple
{
// Поле данных.
public int i = 1;
// Константа.
public const double d = 2.5;
// Статическое поле класса.
public static string s = " Simple ";
// Закрытое поле данных.
double z = 3;
// Метод для получения значения z.
public double Get_z()
{
return z;
}
// Метод установки поля z.
public void Set_z(double z_)
{
z = z_;
}
}
class Program
{
static void Main(string[] args)
{
// Создание экземпляра класса Simple.
Simple x = new Simple();
// Вызов метода установки поля z объекта x значением 10.
x.Set_z(10);
// Вызов метода получения поля z объекта x.
Console.WriteLine(x.Get_z());
}
}
}
Листинг 2. Простейшие методы
Параметры методов
При вызове метода выполняются следующие действия:
1. Вычисляются выражения, стоящие на месте аргументов.
2. Выделяется память под параметры метода в соответствии с их типом.
3. Каждому из параметров сопоставляется соответствующий аргумент (аргументы как бы накладываются на параметры и замещают их).
4. Выполняется тело метода.
5. Если метод возвращает значение, оно передается в точку вызова; если метод имеет тип void, управление передается на оператор, следующий после вызова.
При этом проверяется соответствие типов аргументов и параметров и при необходимости выполняется их преобразование. При несоответствии типов выдается диагностическое сообщение. Листинг 3 иллюстрирует этот процесс.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleClasses4
{
class Example
{
// Метод выбора минимального значения.
public static int Min(int a, int b)
{
if ( a < b ) return a;
else return b;
}
}
class Program
{
static void Main(string[] args)
{
int a = 2, b = 4;
// Вызов метода Min.
int r = Example.Min( a, b );
// Результат: 2.
Console.WriteLine( r );
short c = 3, d = 4;
// вызов метода Min.
int w = Example.Min(c, d);
// Результат: 3.
Console.WriteLine( w );
// вызов метода Min.
int v = Example.Min(a + c, d / 2 * b);
// Результат: 5.
Console.WriteLine( v );
}
}
}
Листинг 3. Передача параметров методу.
Внимание
Главное требование при передаче параметров состоит в том, что аргументы при вызове метода должны записываться в том же порядке, что и в заголовке метода, и должно существовать неявное преобразование типа каждого аргумента к типу соответствующего параметра. Количество аргументов должно соответствовать количеству параметров. Иллюстрация приведена на рис. 17.
Рис. 17. Соответствие параметров при вызове метода
Существуют два способа передачи параметров: по значению и по ссылке.
При передаче по значению метод получает копии значений аргументов, и операторы метода работают с этими копиями. Доступа к исходным значениям аргументов у метода нет, а, следовательно, нет и возможности их изменить.
При передаче по ссылке ( по адресу ) метод получает копии адресов аргументов, он осуществляет доступ к ячейкам памяти по этим адресам и может изменять исходные значения аргументов, модифицируя параметры.
В C# для обмена данными между вызывающей и вызываемой функциями предусмотрено четыре типа параметров:
• параметры-значения;
• параметры-ссылки — описываются с помощью ключевого слова ref ;
• выходные параметры — описываются с помощью ключевого слова out ;
• параметры-массивы — описываются с помощью ключевого слова params.
Ключевое слово предшествует описанию типа параметра. Если оно опущено, параметр считается параметром-значением. Параметр-массив может быть только один и должен располагаться последним в списке, например:
public int Calculate( int a, ref int b, out int c, params int[] d ) …
Параметры-значения
Параметр-значение описывается в заголовке метода следующим образом:
тип имя
Пример заголовка метода, имеющего один параметр-значение целого типа:
void P( int x )
Имя параметра может быть произвольным. Параметр х представляет собой локальную переменную, которая получает свое значение из вызывающей функции при вызове метода. В метод передается копия значения аргумента.
Механизм передачи следующий: из ячейки памяти, в которой хранится переменная, передаваемая в метод, берется ее значение и копируется в специальную область памяти — область параметров. Метод работает с этой копией. По завершении работы метода область параметров освобождается. Этот способ годится только для передачи в метод исходных данных.
При вызове метода на месте параметра, передаваемого по значению, может находиться выражение, для типа которого существует неявное преобразование типа выражения к типу параметра.
Например, пусть в вызывающей функции описаны переменные и им до вызова метода присвоены значения:
int x = 1;
sbyte c = 1;
ushort y = 1;
Тогда следующие вызовы метода Р, заголовок которого был описан ранее, будут синтаксически правильными:
P( x ); P( c ); P( y ); P( 200 ); P( x / 4 + 1 );
Параметры-ссылки
Признаком параметра-ссылки является ключевое слово ref перед описанием параметра:
ref тип имя
Пример заголовка метода, имеющего один параметр-ссылку целого типа:
void P( ref int x )
При вызове метода в область параметров копируется адрес аргумента, и метод через него имеет доступ к ячейке, в которой хранится аргумент. Метод работает непосредственно с переменной из вызывающей функции и, следовательно, может ее изменить, поэтому если в методе требуется изменить значения параметров, они должны передаваться только по ссылке.
Внимание
При вызове метода на месте параметра-ссылки может находиться только ссылка на инициализированную переменную точно того же типа. Перед именем параметра указывается ключевое слово ref.
Проиллюстрируем передачу параметров-значений и параметров-ссылок на примере (листинг 4).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleMethodParams
{
class Program
{
static void P(int a, ref int b)
{
a = 11; b = 22;
Console.WriteLine("Внутри метода \ta = {0} \tb = {1}", a, b);
}
static void Main(string[] args)
{
int a = 2, b = 4;
Console.WriteLine("До вызова \tа = {0} \tb = {1}", a, b);
P(a, ref b);
Console.WriteLine("После вызова \tа = {0} \tb = {1}", a, b);
}
}
}
Листинг 4. Параметры-значения и параметры-ссылки
Результаты работы этой программы:
Несколько иная картина получится, если передавать в метод не величины значимых типов, а экземпляры классов, то есть величины ссылочных типов. Для простоты можно считать, что объекты всегда передаются по ссылке.
Выходные параметры
Довольно часто возникает необходимость в методах, которые формируют несколько величин. В этом случае становится неудобным ограничение параметров-ссылок: необходимость присваивания значения аргументу до вызова метода. Это ограничение снимает спецификаторout. Параметру, имеющему этот спецификатор, должно быть обязательно присвоено значение внутри метода.
Изменим описание второго параметра в листинге 4 так, чтобы он стал выходным (листинг 5).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleOutParam
{
class Program
{
static void P(int a, out int b)
{
a = 11; b = 22;
Console.WriteLine("Внутри метода \ta = {0} \tb = {1}", a, b);
}
static void Main(string[] args)
{
int a = 2, b;
P(a, out b);
Console.WriteLine("После вызова \tа = {0} \tb = {1}", a, b);
}
}
}
Листинг 5. Выходные параметры
При вызове метода перед соответствующим параметром тоже указывается ключевое слово out.
Ключевое слово this
Каждый объект содержит свой набор полей класса. Методы находятся в памяти в единственном экземпляре и используются всеми объектами совместно, поэтому необходимо обеспечить работу методов с полями именно того объекта, для которого они были вызваны. Для этого в любой нестатический метод автоматически передается скрытый параметр this, в котором хранится ссылка на вызвавший функцию объект. Этот процесс иллюстрирует рис. 18.
Рис. 18. Передача методу скрытого параметра this
В явном виде параметр this применяется для того, чтобы возвратить из метода ссылку на вызвавший объект, а также для идентификации поля в случае, если его имя совпадает с именем параметра метода, например:
class Simple
{
double y;
// Метод возвращает ссылку на экземпляр.
public Simple T()
{
return this;
}
public void Sety(double y)
{
// Полю y присваивается значение параметра y.
this.y = y;
}
}
Конструкторы
Конструктор предназначен для инициализации объекта. Он вызывается автоматически при создании объекта класса с помощью операции new. Имя конструктора совпадает с именем класса. Ниже перечислены свойства конструкторов
• Конструктор не возвращает значение, даже типа void.
• Класс может иметь несколько конструкторов с разными параметрами для разных видов инициализации.
• Если программист не указал ни одного конструктора или какие-то поля не были инициализированы, полям значимых типов присваивается нуль, полям ссылочных типов — значение null.
• Конструктор, вызываемый без параметров, называется конструктором по умолчанию.
До сих пор мы задавали начальные значения полей класса при описании класса. Это удобно в том случае, когда для всех объектов класса начальные значения некоторого поля одинаковы. Если же при создании объектов требуется присваивать полю разные значения, это следует делать в конструкторе. В листинге 6 в класс Simple добавлен конструктор, а поля сделаны закрытыми.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleConstructor
{
class Simple
{
// Конструктор с параметрами.
public Simple ( int a, double z )
{
this.a = a;
this.z = z;
}
// Метод получения поля y
public double Get_z()
{
return z;
}
int a;
double z;
}
class Program
{
static void Main(string[] args)
{
// Вызов конструктора.
Simple a = new Simple ( 10, 2.2 );
// Результат: 2,2.
Console.WriteLine( a.Get_z() );
// Вызов конструктора.
Simple b = new Simple ( 1, 6.54 );
// Результат: 6,54.
Console.WriteLine( b.Get_z() );
}
}
}
Листинг 6. Класс с конструктором
Часто бывает удобно задать в классе несколько конструкторов, чтобы обеспечить возможность инициализации объектов разными способами. Все конструкторы должны иметь разные сигнатуры.
Если один из конструкторов выполняет какие-либо действия, а другой должен делать то же самое плюс еще что-нибудь, удобно вызвать первый конструктор из второго. Для этого используется уже известное вам ключевое слово this в другом контексте, например:
class Simple
{
// Конструктор 1.
public Simple ( int a )
{
this.a = a;
}
// Вызов конструктора 1.
public Simple ( int a, double y ) : this( a )
{
this.y = y;
}
...
}
Конструкция, находящаяся после двоеточия, называется инициализатором.
Как вы помните, все классы в C# имеют общего предка — класс object. Конструктор любого класса, если не указан инициализатор, автоматически вызывает конструктор своего предка.
До сих пор речь шла об "обычных" конструкторах, или конструкторах экземпляра. Существует второй тип конструкторов — статические конструкторы, или конструкторы класса. Конструктор экземпляра инициализирует данные экземпляра, конструктор класса — данные класса.
Статический конструктор не имеет параметров, его нельзя вызвать явным образом. Система сама определяет момент, в который требуется его выполнить.
Некоторые классы содержат только статические данные и, следовательно, создавать экземпляры таких объектов не имеет смысла. В версию 2.0 введена возможность описывать статический класс, то есть класс с модификатором static. Экземпляры такого класса создавать запрещено, и кроме того, от него запрещено наследовать. Все элементы такого класса должны явным образом объявляться с модификатором static (константы и вложенные типы классифицируются как статические элементы автоматически). В листинге 7 приведен пример статического класса.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleStatClass
{
static class Simple
{
static int a = 10;
static double b = 2.5;
const double pi = 3.1415;
public static void Print ()
{
Console.WriteLine("a = " + a );
Console.WriteLine("b = " + b);
Console.WriteLine("pi = " + pi);
}
}
class Program
{
static void Main(string[] args)
{
Simple.Print();
}
}
}
Листинг 7. Статический класс (начиная с версии 2.0)
В качестве "сквозного" примера, на котором будет демонстрироваться работа с различными элементами класса, создадим класс, моделирующий простую дробь. Для этого требуется задать значения числителя и знаменателя и операцию сложения.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleFrac1
{
class TFrac
{
int num;
int denom;
public override string ToString()
{
string s = string.Format("{0}/{1}", num, denom);
return s;
}
public TFrac(int n, int d = 1)
{
this.num = n;
this.denom = d;
Reduce();
}
public TFrac(string s = "0/1")
{
int n = s.IndexOf('/');
if (n == -1)
{
this.num = int.Parse(s);
this.denom = 1;
}
else
{
this.num = int.Parse(s.Substring(0, n));
this.denom = int.Parse(s.Substring(n + 1));
}
Reduce();
}
public TFrac Add(TFrac b)
{
int n = this.num * b.denom + b.num * this.denom;
int d = this.denom * b.denom;
return new TFrac(n, d);
}
static int nod(int a, int b)
{
int t;
a = Math.Abs(a);
b = Math.Abs(b);
if (a > b) { t = a; a = b; b = t; }
while (a != 0 & a != b)
{
if (a <= b) { b = b % a; }
else { t = a; a = b; b = t; }
}
return b;
}
void Reduce()
{
int t = nod(this.num, this.denom);
this.num /= t;
this.denom /= t;
}
}
class Program
{
static void Main(string[] args)
{
TFrac a = new TFrac(1, 5);
Console.WriteLine(a.ToString());
TFrac b = new TFrac("3/5");
Console.WriteLine(b.ToString());
TFrac c = a.Add(b);
Console.WriteLine(c.ToString());
}
}
}
Листинг 8. Класс TFrac
Результат работы программы:
Свойства
Свойства служат для организации доступа к полям класса. Как правило, свойство связано с закрытым полем класса и определяет методы его получения и установки. Синтаксис свойства:
[ атрибуты ] [ спецификаторы ] тип имя_свойства
{
[ get код_доступа ]
[ set код_доступа ]
}
Значения спецификаторов для свойств и методов аналогичны. Чаще всего свойства объявляются со спецификатором public. Код доступа представляет собой блоки операторов, которые выполняются при получении ( get ) или установке ( set ) свойства. Может отсутствовать либо часть get, либо set, но не обе одновременно.
Если отсутствует часть set, свойство доступно только для чтения (read-only), если отсутствует часть get, свойство доступно только для записи (write-only). В версии C# 2.0 введена возможность задавать разный уровень доступа для частей get и set.
Пример описания свойств:
public class Button: Control
{
// Закрытое поле, с которым связано свойство
private string caption;
// свойство
public string Caption
{
// способ получения свойства
get {
return caption;
}
// способ установки свойства
set {
if (caption != value) {
caption = value;
}
}
}
...
}
Метод записи обычно содержит действия по проверке допустимости устанавливаемого значения, метод чтения может содержать, например, поддержку счетчика обращений к полю.
В программе свойство выглядит как поле класса, например:
Button ok = new Button();
// вызывается метод установки свойства
ok.Caption = "OK";
// вызывается метод получения свойства
string s = ok.Caption;
При обращении к свойству автоматически вызываются указанные в нем методы чтения и установки.
Синтаксически чтение и запись свойства выглядят почти как методы. Метод get должен содержать оператор return. В методе set используется параметр со стандартным именем value, который содержит устанавливаемое значение.
Добавим в класс TFrac, описанный в листинге 8, свойства, позволяющие работать с закрытыми полями этого класса. Код класса несколько разрастется, зато упростится его использование.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleFrac1
{
class TFrac
{
int num;
int denom;
// Свойство для работы с полем num.
public int Num
{
get { return num; }
set { if (num != value) num = value; }
}
// Свойство для работы с полем denom.
public int Denom
{
get { return denom; }
set { if (denom != value) denom = value; }
}
// Свойство для работы с полями num, denom.
public string Frac
{
get
{
string s = string.Format("{0}/{1}", num, denom);
return s;
}
}
public TFrac(int n, int d = 1)
{
this.num = n;
this.denom = d;
Reduce();
}
public TFrac(string s = "0/1")
{
int n = s.IndexOf('/');
if (n == -1)
{
this.num = int.Parse(s);
this.denom = 1;
}
else
{
this.num = int.Parse(s.Substring(0, n));
this.denom = int.Parse(s.Substring(n + 1));
}
Reduce();
}
public TFrac Add(TFrac b)
{
int n = this.num * b.denom + b.num * this.denom;
int d = this.denom * b.denom;
return new TFrac(n, d);
}
static int nod(int a, int b)
{
int t;
a = Math.Abs(a);
b = Math.Abs(b);
if (a > b) { t = a; a = b; b = t; }
while (a != 0 & a != b)
{
if (a <= b) { b = b % a; }
else { t = a; a = b; b = t; }
}
return b;
}
void Reduce()
{
int t = nod(this.num, this.denom);
this.num /= t;
this.denom /= t;
}
}
class Program
{
static void Main(string[] args)
{
TFrac a = new TFrac(1, 5);
// Используем свойство.
Console.WriteLine(a.Frac);
// Используем свойство.
a.Num = 3;
// Используем свойство.
a.Denom = 7;
// Используем свойство.
Console.WriteLine(a.Frac);
}
}
}
Листинг 9. Класс TFrac со свойствами
Результат работы программы:
Контрольные вопросы
1. Перечислите и опишите элементы класса в C#.
2. Опишите способы передачи параметров в методы.
3. Запишите алгоритм вычисления чисел Фибоначчи с помощью рекурсии.
4. Для чего в классе может потребоваться несколько конструкторов?
5. Как можно вызвать один конструктор из другого? Зачем это нужно?
6. Что такое this? Что в нем хранится, как он используется?
7. Что такое деструктор? Гарантирует ли среда его выполнение?
8. Какие действия обычно выполняются в части set свойства?
9. Может ли свойство класса быть не связанным с его полями?
10. Можно ли описать разные спецификаторы доступа к частям get и set свойства?