Предыдущая лекция | | Следующая лекция
:----------------:|:----------:|:----------------:
[Регулярные выражения](./t5_regex.md) | [Содержание](../readme.md#тема-6-основные-принципы-объектно-ориентированного-программирования) | [Ещё раз про классы. Интерфейсы.](./t6_oop_habr.md)
# Тема 6. Основные принципы объектно-ориентированного программирования
## История развития ООП
Развернуть
Термины «объектно-» и «ориентированный» в современном смысле этих слов появились в MIT в конце 1950 начале 1960 годов. В среде специалистов по искусственному интеллекту термин «объект» мог относиться к идентифицированным элементам (атомы Lisp) со свойствами (атрибутами). Алан Кэй позже писал, что понимание внутреннего устройства Лиспа оказало серьезное влияние на его мышление в 1966 г. Другим ранним примером ООП в MIT был Sketchpad созданный Иваном Сазерлендом в 1960-61. В глоссарии подготовленного в 1963 г. технического отчета, основанного на его диссертации о Sketchpad, Сазерленд определяет понятия «объект» и «экземпляр» с концепцией классов на основе «мастера» или «определения», хотя все эти термины относились к графическому представлению объектов [вкратце, в Sketchpad было основное изображение, на основе которого строились копии. При изменении основного – копии тоже менялись. Прим. пер.].
В ранней MIT-версии ALGOL AED-0 структуры данных («плексы» на диалекте Алгола) напрямую были связаны с процедурами, которые впоследствии были названы сообщениями, методами или функциями-членами.
Объекты, как формализованный концепт появились в программировании в 1960-х в Simula 67, модернизированной версии Simula I, языка программирования, ориентированного на дискретно-событийное моделирование. Авторы Simula — Оле-Йохан Даль и Кристен Нюгорд из Норвежского компьютерного центра в Осло. Simula разрабатывалась под влиянием SIMSCRIPT и предложенной Чарльзом Хоаром концепцией записей-классов. Simula включала в себя понятие классов и экземпляров (или объектов), а также подклассов, виртуальных методов, сопрограмм и дискретно-событийное моделирование как часть собственной парадигмы программирования. В языке использовался автоматический сборщик мусора, который был изобретен ранее для функционального языка Lisp. Simula использовалась тогда преимущественно для физического моделирования. Идеи Simula оказали серьезное влияние на более поздние языки, такие как Smalltalk, варианты Lisp (CLOS), Object Pascal, и C++.
Язык Smalltalk, который был изобретен в компании Xerox PARC Аланом Кэем (Alan Kay) и некоторыми другими учеными, фактически навязывал использование «объектов» и «сообщений» как базиса для вычислений. Создателей Smalltalk вдохновляли некоторые идеи Simula, но Smalltalk разрабатывался как полностью динамичная система, в которой классы могут создаваться и изменяться динамически, а не только статически как в Simula. Smalltalk и ООП с его помощью были представлены широкой аудитории в журнале Byte magazine в августе 1981.
В 1970-х Smalltalk Кэя сподвиг сообщество Lisp внедрить в язык объектно-ориентированные техники, которые были представлены разработчикам с помощью Lisp машины.
Эксперименты с различными расширениями Lisp в конечном итоге привели к созданию Common Lisp Object System (CLOS, части первого стандартизованного объектно-ориентированного языка, ANSI Common Lisp), который органично включал в себя как функциональное, так и объектно-ориентированное программирование и позволял расширять себя с помощью протокола Meta-object protocol. В 1980 было несколько попыток дизайна архитектур процессоров, которые включали бы в себя аппаратную поддержку работы с объектами в памяти, но все они были безуспешны. В качестве примеров можно привести Intel iAPX 432 и Linn Smart Rekursiv.
Объектно-ориентированное программирование развилось в доминирующую методологию программирования в начале и середине 1990 годов, когда стали широко доступны поддерживающие ее языки программирования, такие как Visual FoxPro 3.0, C++, и Delphi. Доминирование этой системы поддерживалось ростом популярности графических интерфейсов пользователя, которые основывались на техниках ООП. Пример тесной связи между динамической библиотекой GUI и объектно-ориентированного языка программирования можно найти посмотрев на фреймворк Cocoa на Mac OS X, который был написан на Objective-C, объектно-ориентированом расширении к С, основанном на Smalltalk с поддержкой динамических сообщений. Инструментарии ООП повлияли на популярность событийно-ориентированного программирования (хотя, эта концепция не ограничивается одним ООП). Некоторые даже думали, что кажущаяся или реальная связь с графическими интерфейсами – это то, что вынесло ООП на передний план технологий.
В ETH Zürich, Никлаус Вирт и его коллеги тоже исследовали такие предметы, как абстрация данных и модульное программирование, хотя эти подходы широко использовались и в 60-х и ранее. Modula-2 вышедшая в 1978 включала оба эти подхода, а ее последователь Oberon имел собственный подход к объктно-ориентированности, классам и прочему, непохожий на подход Smalltalk и совсем не похожий на подход C++.
Возможности ООП добавлялись во многие языки того времени, включая Ada, BASIC, Fortran, Pascal и другие. Их добавление в языки, изначально не разрабатывавшиеся для поддержки ООП часто приводило к проблемам с совместимостью и поддержкой кода.
Позднее стали появляться языки, поддерживающие как объектно-ориентированный подход, так и процедурный вроде Python и Ruby. Пожалуй, самыми коммерчески успешными объектно-ориентированными языками можно назвать Visual Basic.NET, C# и Java. И .NET и Java демонстрируют превосходство ООП.
---
Объектно-ориентированная идеология разрабатывалась как попытка связать поведение сущности с её данными и спроецировать объекты реального мира и бизнес-процессов в программный код. Задумывалось, что такой код проще читать и понимать человеком, т. к. людям свойственно воспринимать окружающий мир как множество взаимодействующих между собой объектов, поддающихся определенной классификации.
Не следует думать, что ООП каким-то чудным образом ускорит написание программ, и ожидать ситуацию, когда жители Вилларибо уже выкатили ООП-проект в работу, а жители Виллабаджо все еще отмывают жирный спагетти-код. В большинстве случаев это не так, и время экономится не на стадии разработки, а на этапах поддержки (расширение, модификация, отладка и тестирование), то бишь в долгосрочной перспективе. Если вам требуется написать одноразовый скрипт, который не нуждается в последующей поддержке, то и ООП в этой задаче, вероятнее всего, не пригодится. Однако, значительную часть жизненного цикла большинства современных проектов составляют именно поддержка и расширение. Само по себе наличие ООП не делает вашу архитектуру безупречной, и может наоборот привести к излишним усложнениям.
## Классы и объекты
>Дальше передрано с [метанита](https://metanit.com/sharp/tutorial/3.1.php)
C# является полноценным объектно-ориентированным языком. Это значит, что программу на C# можно представить в виде взаимосвязанных взаимодействующих между собой объектов.
Описанием объекта является класс, а объект представляет экземпляр этого класса. Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики. То есть некоторый шаблон - этот шаблон можно назвать классом. Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие - другое имя. И реально существующий человек (фактически экземпляр данного класса) будет представлять объект этого класса.
По умолчанию проект консольного приложения уже содержит один класс **Program**, с которого и начинается выполнение программы.
По сути класс представляет новый тип, который определяется пользователем. Класс определяется с помощью ключевого слова **сlass**:
```cs
class Person
{
}
```
Где определяется класс? Класс можно определять внутри пространства имен, вне пространства имен, внутри другого класса. Как правило, классы помещаются в отдельные файлы.
Пример класса, определенного внутри пространства имён:
```cs
using System;
namespace HelloApp
{
class Person
{
}
class Program
{
static void Main(string[] args)
{
}
}
}
```
Вся функциональность класса представлена его членами - полями (полями называются переменные класса), свойствами, методами, событиями. Например, определим в классе **Person** поля и метод:
```cs
using System;
namespace HelloApp
{
class Person
{
public string name; // имя
public int age = 18; // возраст
public void GetInfo()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
class Program
{
static void Main(string[] args)
{
Person tom;
}
}
}
```
В данном случае класс **Person** представляет человека. Поле *name* хранит имя, а поле *age* - возраст человека. А метод *GetInfo* выводит все данные на консоль. Чтобы все данные были доступны вне класса **Person** переменные и метод определены с модификатором **public**. Поскольку поля фактически те же переменные, им можно присвоить начальные значения, как в случае выше, поле *age* инициализировано значением `18`.
Так как класс представляет собой новый тип, то в программе мы можем определять переменные, которые представляют данный тип. Так, здесь в методе *Main* определена переменная *tom*, которая представляет класс **Person**. Но пока эта переменная не указывает ни на какой объект и по умолчанию она имеет значение **null**. Поэтому вначале необходимо создать объект класса **Person**.
### Константы класса
Константы характеризуются следующими признаками:
* Константа должна быть проинициализирована при определении
* После определения значение константы не может быть изменено
Константы предназначены для описания таких значений, которые не должны изменяться в программе. Для определения констант используется ключевое слово **const**:
```cs
const double PI = 3.14;
const double E = 2.71;
```
При использовании констант надо помнить, что объявить мы их можем только один раз и что к моменту компиляции они должны быть определены.
```cs
class MathLib
{
public const double PI=3.141;
public const double E = 2.81;
public const double K; // Ошибка, константа не инициализирована
}
class Program
{
static void Main(string[] args)
{
MathLib.E=3.8; // Ошибка, значение константы нельзя изменить
}
}
```
Также обратите внимание на синтаксис обращения к константе. Так как неявно это статическое поле, для обращения к ней необходимо использовать имя класса.
```cs
class MathLib
{
public const double PI=3.141;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(MathLib.PI);
}
}
```
Но следует учитывать, что мы не можем объявить константу с модификатором **static**. Но в этом собственно и нет смысла.
### Конструкторы
Кроме обычных методов в классах используются также и специальные методы, которые называются **конструкторами**. **Конструкторы** вызываются при создании нового объекта данного класса. Конструкторы выполняют инициализацию объекта.
### Конструктор по умолчанию
Если в классе не определено ни одного конструктора, то для этого класса автоматически создается конструктор по умолчанию. Такой конструктор не имеет параметров и не имеет тела.
Выше класс **Person** не имеет никаких конструкторов. Поэтому для него автоматически создается конструктор по умолчанию. И мы можем использовать этот конструктор. В частности, создадим один объект класса **Person**:
```cs
class Person
{
public string name; // имя
public int age; // возраст
public void GetInfo()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
class Program
{
static void Main(string[] args)
{
Person tom = new Person();
tom.GetInfo(); // Имя: Возраст: 0
tom.name = "Tom";
tom.age = 34;
tom.GetInfo(); // Имя: Tom Возраст: 34
Console.ReadKey();
}
}
```
Для создания объекта **Person** используется выражение `new Person()`. Оператор **new** выделяет память для объекта **Person**. И затем вызывается конструктор по умолчанию, который не принимает никаких параметров. В итоге после выполнения данного выражения в памяти будет выделен участок, где будут храниться все данные объекта **Person**. А переменная *tom* получит ссылку на созданный объект.
Если конструктор не инициализирует значения переменных объекта, то они получают значения по умолчанию. Для переменных числовых типов это число `0`, а для типа **string** и классов - это значение **null** (то есть фактически отсутствие значения).
После создания объекта мы можем обратиться к переменным объекта **Person** через переменную *tom* и установить или получить их значения, например, `tom.name = "Tom";`.
Консольный вывод данной программы:
```
Имя: Возраст: 0
Имя: Tom Возраст: 34
```
### Создание конструкторов
Выше для инициализации объекта использовался конструктор по умолчанию. Однако мы сами можем определить свои конструкторы:
```cs
class Person
{
public string name;
public int age;
// 1 конструктор
public Person() {
name = "Неизвестно";
age = 18;
}
// 2 конструктор
public Person(string n) {
name = n;
age = 18;
}
// 3 конструктор
public Person(string n, int a) {
name = n;
age = a;
}
}
```
Теперь в классе определено три конструктора, каждый из которых принимает различное количество параметров и устанавливает значения полей класса.
Используем эти конструкторы:
```cs
static void Main(string[] args)
{
// вызов 1-ого конструктора без параметров
Person tom = new Person();
//вызов 2-ого конструктора с одним параметром
Person bob = new Person("Bob");
// вызов 3-его конструктора с двумя параметрами
Person sam = new Person("Sam", 25);
tom.GetInfo(); // Имя: Неизвестно Возраст: 18
bob.GetInfo(); // Имя: Bob Возраст: 18
sam.GetInfo(); // Имя: Sam Возраст: 25
}
```
Консольный вывод данной программы:
```
Имя: Неизвестно Возраст: 18
Имя: Bob Возраст: 18
Имя: Sam Возраст: 25
```
При этом если в классе определены конструкторы, то при создании объекта **необходимо** использовать один из этих конструкторов.
Стоит отметить, что начиная с версии C# 9.0 мы можем сократить вызов конструктора, убрав из него название типа:
```cs
// аналогично new Person();
Person tom = new ();
// аналогично new Person("Bob");
Person bob = new ("Bob");
// аналогично new Person("Sam", 25);
Person sam = new ("Sam", 25);
```
### Ключевое слово **this**
Ключевое слово **this** представляет ссылку на текущий экземпляр класса. В каких ситуациях оно нам может пригодиться? В примере выше определены три конструктора. Все три конструктора выполняют однотипные действия - устанавливают значения полей *name* и *age*. Но этих повторяющихся действий могло быть больше. И мы можем не дублировать функциональность конструкторов, а просто обращаться из одного конструктора к другому через ключевое слово **this**, передавая нужные значения для параметров:
```cs
class Person
{
public string name;
public int age;
public Person() : this("Неизвестно")
{
}
public Person(string name) : this(name, 18)
{
}
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public void GetInfo()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
```
В данном случае первый конструктор вызывает второй, а второй конструктор вызывает третий. По количеству и типу параметров компилятор узнает, какой именно конструктор вызывается. Например, во втором конструкторе:
```cs
public Person(string name) : this(name, 18)
{
}
```
идет обращение к третьему конструктору, которому передаются два значения. Причем в начале будет выполняться именно третий конструктор, и только потом код второго конструктора.
Также стоит отметить, что в третьем конструкторе параметры называются также, как и поля класса.
```cs
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
```
И чтобы разграничить параметры и поля класса, к полям класса обращение идет через ключевое слово **this**. Так, в выражении `this.name = name`; первая часть *this.name* означает, что *name* - это поле текущего класса, а не название параметра *name*. Если бы у нас параметры и поля назывались по-разному, то использовать слово **this** было бы необязательно. Также через ключевое слово **this** можно обращаться к любому полю или методу.
### Инициализаторы объектов
Для инициализации объектов классов можно применять инициализаторы. Инициализаторы представляют передачу в фигурных скобках значений доступным полям и свойствам объекта:
```cs
Person tom = new Person { name = "Tom", age=31 };
tom.GetInfo(); // Имя: Tom Возраст: 31
```
С помощью инициализатора объектов можно присваивать значения всем доступным полям и свойствам объекта в момент создания без явного вызова конструктора.
При использовании инициализаторов следует учитывать следующие моменты:
* С помощью инициализатора мы можем установить значения только доступных из внешнего кода полей и свойств объекта. Например, в примере выше поля *name* и *age* имеют модификатор доступа **public**, поэтому они доступны из любой части программы.
* Инициализатор выполняется после конструктора, поэтому если и в конструкторе, и в инициализаторе устанавливаются значения одних и тех же полей и свойств, то значения, устанавливаемые в конструкторе, заменяются значениями из инициализатора.
## Структуры
Развернуть
Наряду с классами **структуры** представляют еще один способ создания собственных типов данных в C#. Более того многие примитивные типы, например, **int**, **double** и т.д., по сути являются структурами.
Например, определим структуру, которая представляет человека:
```cs
struct User
{
public string name;
public int age;
public void DisplayInfo()
{
Console.WriteLine($"Name: {name} Age: {age}");
}
}
```
Как и классы, структуры могут хранить состояние в виде переменных и определять поведение в виде методов. Так, в данном случае определены две переменные - *name* и *age* для хранения соответственно имени и возраста человека и метод *DisplayInfo* для вывода информации о человеке.
Используем эту структуру в программе:
```cs
using System;
namespace HelloApp
{
struct User
{
public string name;
public int age;
public void DisplayInfo()
{
Console.WriteLine($"Name: {name} Age: {age}");
}
}
class Program
{
static void Main(string[] args)
{
User tom;
tom.name = "Tom";
tom.age = 34;
tom.DisplayInfo();
Console.ReadKey();
}
}
}
```
В данном случае создается объект *tom*. У него устанавливаются значения глобальных переменных, и затем выводится информация о нем.
### Конструкторы структуры
Как и класс, структура может определять конструкторы. Но в отличие от класса нам не обязательно вызывать конструктор для создания объекта структуры:
```cs
User tom;
```
Однако если мы таким образом создаем объект структуры, то обязательно надо проинициализировать все поля (глобальные переменные) структуры перед получением их значений или перед вызовом методов структуры. То есть, например, в следующем случае мы получим ошибку, так как обращение к полям и методам происходит до присвоения им начальных значений:
```cs
User tom;
int x = tom.age; // Ошибка
tom.DisplayInfo(); // Ошибка
```
Также мы можем использовать для создания структуры конструктор без параметров, который есть в структуре по умолчанию и при вызове которого полям структуры будет присвоено значение по умолчанию (например, для числовых типов это число 0):
```cs
User tom = new User();
tom.DisplayInfo(); // Name: Age: 0
```
Обратите внимание, что при использовании конструктора по умолчанию нам не надо явным образом иницилизировать поля структуры.
Также мы можем определить свои конструкторы. Например, изменим структуру User:
```cs
using System;
namespace HelloApp
{
struct User
{
public string name;
public int age;
public User(string name, int age)
{
this.name = name;
this.age = age;
}
public void DisplayInfo()
{
Console.WriteLine($"Name: {name} Age: {age}");
}
}
class Program
{
static void Main(string[] args)
{
User tom = new User("Tom", 34);
tom.DisplayInfo();
User bob = new User();
bob.DisplayInfo();
Console.ReadKey();
}
}
}
```
Важно учитывать, что если мы определяем конструктор в структуре, то он **должен инициализировать все поля структуры**, как в данном случае устанавливаются значения для переменных *name* и *age*.
Также, как и для класса, можно использовать инициализатор для создания структуры:
```cs
User person = new User { name = "Sam", age = 31 };
```
Но в отличие от класса нельзя инициализировать поля структуры напрямую при их объявлении, например, следующим образом:
```cs
struct User
{
public string name = "Sam"; // ! Ошибка
public int age = 23; // ! Ошибка
public void DisplayInfo()
{
Console.WriteLine($"Name: {name} Age: {age}");
}
}
```
## Пространства имен, псевдонимы и статический импорт
### Пространства имен
Все определяемые классы и структуры, как правило, не существуют сами по себе, а заключаются в специальные контейнеры - **пространства имен**. Создаваемый по умолчанию класс **Program** уже находится в пространстве имен, которое обычно совпадает с названием проекта:
```cs
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
}
}
}
```
Пространство имен определяется с помощью ключевого слова **namespace**, после которого идет название. Так в данном случае полное название класса **Program** будет **HelloApp.Program**.
Класс **Program** видит все классы, которые объявлены в том же пространстве имен:
```cs
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
Account account = new Account(4);
}
}
class Account
{
public int Id { get; private set;} // номер счета
public Account(int _id)
{
Id = _id;
}
}
}
```
Но чтобы задействовать классы из других пространств имен, эти пространства надо подключить с помощью директивы **using**:
```cs
using System;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("hello");
}
}
}
```
Здесь подключается пространство имен **System**, в котором определен класс **Console**. Иначе нам бы пришлось писать полный путь к классу:
```cs
static void Main(string[] args)
{
System.Console.WriteLine("hello");
}
```
Пространства имен могут быть определены внутри других пространств:
```cs
using HelloApp.AccountSpace;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
Account account = new Account(4);
}
}
namespace AccountSpace
{
class Account
{
public int Id { get; private set;}
public Account(int _id)
{
Id = _id;
}
}
}
}
```
В этом случае для подключения пространства указывается его полный путь с учетом внешних пространств имен: `using HelloApp.AccountSpace;`
### Псевдонимы
Для различных классов мы можем использовать псевдонимы. Затем в программе вместо названия класса используется его псевдоним. Например, для вывода строки на экран применяется метод *Console.WriteLine()*. Но теперь зададим для класса **Console** псевдоним:
```cs
using printer = System.Console;
class Program
{
static void Main(string[] args)
{
printer.WriteLine("Hello from C#");
printer.Read();
}
}
```
С помощью выражения `using printer = System.Console` указываем, что псевдонимом для класса **System.Console** будет имя *printer*. Это выражение не имеет ничего общего с подключением пространств имен в начале файла, хотя и использует оператор **using**. При этом используется полное имя класса с учетом пространства имен, в котором класс определен. И далее, чтобы вывести строку, применяется выражение `printer.WriteLine("Hello from C#")`.
И еще пример. Определим класс и для него псевдоним:
```cs
using Person = HelloApp.User;
using Printer = System.Console;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.name = "Tom";
Printer.WriteLine(person.name);
Printer.Read();
}
}
class User
{
public string name;
}
}
```
Класс называется **User**, но в программе для него используется псевдоним **Person**.
Также в C# имеется возможность импорта функциональности классов. Например, импортируем возможности класса **Console**:
```cs
using static System.Console;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
WriteLine("Hello from C# 8.0");
Read();
}
}
}
```
Выражение `using static` подключает в программу все статические методы и свойства, а также константы. И после этого мы можем не указывать название класса при вызове метода.
Подобным образом можно определять свои классы и импортировать их:
```cs
using static System.Console;
using static System.Math;
using static HelloApp.Geometry;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
double radius = 50;
double result = GetArea(radius); //Geometry.GetArea
WriteLine(result); //Console.WriteLine
Read(); // Console.Read
}
}
class Geometry
{
public static double GetArea(double radius)
{
return PI * radius * radius; // Math.PI
}
}
}
```
## Модификаторы доступа
Все члены класса - поля, методы, свойства - все они имеют **модификаторы доступа**. Модификаторы доступа позволяют задать допустимую область видимости для членов класса. То есть модификаторы доступа определяют контекст, в котором можно употреблять данную переменную или метод. В предыдущих темах мы уже с ним сталкивались, когда объявляли поля класса публичными (то есть с модификатором **public**).
В C# применяются следующие модификаторы доступа:
* **public**: публичный, общедоступный класс или член класса. Такой член класса доступен из любого места в коде, а также из других программ и сборок.
* **private**: закрытый класс или член класса. Представляет полную противоположность модификатору **public**. Такой закрытый класс или член класса доступен только из кода в том же классе или контексте.
* **protected**: такой член класса доступен из любого места в текущем классе или в производных классах. При этом производные классы могут располагаться в других сборках.
Развернуть
* **internal**: класс и члены класса с подобным модификатором доступны из любого места кода в той же сборке, однако он недоступен для других программ и сборок (как в случае с модификатором **public**).
* **protected internal**: совмещает функционал двух модификаторов. Классы и члены класса с таким модификатором доступны из текущей сборки и из производных классов.
* **private protected**: такой член класса доступен из любого места в текущем классе или в производных классах, которые определены в той же сборке.
Мы можем явно задать модификатор доступа, например:
```cs
private protected class State
{
internal int a;
protected void Print()
{
Console.WriteLine($"a = {a}");
}
}
```
Либо можем не указывать:
```cs
class State
{
int a;
void Print()
{
Console.WriteLine($"a = {a}");
}
}
```
Если для полей и методов не определен модификатор доступа, то по умолчанию для них применяется модификатор **private**.
Классы и структуры, объявленные без модификатора, по умолчанию имеют доступ **internal**.
Все классы и структуры, определенные напрямую в пространствах имен и не являющиеся вложенными в другие классы, могут иметь только модификаторы **public** или **internal**.
Посмотрим на примере и создадим следующий класс **State**:
```cs
public class State
{
// все равно, что private int defaultVar;
int defaultVar;
// поле доступно только из текущего класса
private int privateVar;
// доступно из текущего класса и производных классов, которые определены в этом же проекте
protected private int protectedPrivateVar;
// доступно из текущего класса и производных классов
protected int protectedVar;
// доступно в любом месте текущего проекта
internal int internalVar;
// доступно в любом месте текущего проекта и из классов-наследников в других проектах
protected internal int protectedInternalVar;
// доступно в любом месте программы, а также для других программ и сборок
public int publicVar;
// по умолчанию имеет модификатор private
void defaultMethod() => Console.WriteLine($"defaultVar = {defaultVar}");
// метод доступен только из текущего класса
private void privateMethod() => Console.WriteLine($"privateVar = {privateVar}");
// доступен из текущего класса и производных классов, которые определены в этом же проекте
protected private void protectedPrivateMethod() => Console.WriteLine($"protectedPrivateVar = {protectedPrivateVar}");
// доступен из текущего класса и производных классов
protected void protectedMethod()=> Console.WriteLine($"protectedVar = {protectedVar}");
// доступен в любом месте текущего проекта
internal void internalMethod() => Console.WriteLine($"internalVar = {internalVar}");
// доступен в любом месте текущего проекта и из классов-наследников в других проектах
protected internal void protectedInternalMethod() => Console.WriteLine($"protectedInternalVar = {protectedInternalVar}");
// доступен в любом месте программы, а также для других программ и сборок
public void publicMethod() => Console.WriteLine($"publicVar = {publicVar}");
}
```
Так как класс **State** объявлен с модификатором **public**, он будет доступен из любого места программы, а также из других программ и сборок. Класс **State** имеет пять полей для каждого уровня доступа. Плюс одна переменная без модификатора, которая является закрытой (**private**) по умолчанию.
Также имеются шесть методов, которые будут выводить значения полей класса на экран. Обратите внимание, что так как все модификаторы позволяют использовать члены класса внутри данного класса, то и все переменные класса, в том числе закрытые, у нас доступны всем его методам, так как все находятся в контексте класса **State**.
Теперь посмотрим, как мы сможем использовать переменные нашего класса в программе (то есть в методе *Main* класса **Program**), если классы **State** и **Program** находятся в одном проекте:
```cs
class Program
{
static void Main(string[] args)
{
State state1 = new State();
// присвоить значение переменной defaultVar у нас не получится,
// так как она имеет модификатор private и класс Program ее не видит
// И данную строку среда подчеркнет как неправильную
state1.defaultVar = 5; //Ошибка, получить доступ нельзя
// то же самое относится и к переменной privateVar
state1.privateVar = 5; // Ошибка, получить доступ нельзя
// присвоить значение переменной protectedPrivateVar не получится,
// так как класс Program не является классом-наследником класса State
state1.protectedPrivateVar =5; // Ошибка, получить доступ нельзя
// присвоить значение переменной protectedVar тоже не получится,
// так как класс Program не является классом-наследником класса State
state1.protectedVar = 5; // Ошибка, получить доступ нельзя
// переменная internalVar с модификатором internal доступна из любого места текущего проекта
// поэтому спокойно присваиваем ей значение
state1.internalVar = 5;
// переменная protectedInternalVar так же доступна из любого места текущего проекта
state1.protectedInternalVar = 5;
// переменная publicVar общедоступна
state1.publicVar = 5;
}
}
```
Таким образом, мы смогли установить только переменные *internalVar*, *protectedInternalVar* и *publicVar*, так как их модификаторы позволяют использовать в данном контексте.
Аналогично дело обстоит и с методами:
```cs
class Program
{
static void Main(string[] args)
{
State state1 = new State();
state1.defaultMethod(); //Ошибка, получить доступ нельзя
state1.privateMethod(); // Ошибка, получить доступ нельзя
state1.protectedPrivateMethod(); // Ошибка, получить доступ нельзя
state1.protectedMethod(); // Ошибка, получить доступ нельзя
state1.internalMethod(); // норм
state1.protectedInternalMethod(); // норм
state1.publicMethod(); // норм
}
}
```
Здесь нам оказались доступны только три метода: *internalMethod*, *protectedInternalMethod*, *publicMethod*, которые имееют соответственно модификаторы **internal**, **protected internal**, **public**.
Благодаря такой системе модификаторов доступа можно скрывать некоторые моменты реализации класса от других частей программы.
Несмотря на то, что модификаторы **public** и **internal** похожи по своему действию, но они имеют большое отличие. Классы и члены класса с модификатором **public** также будут доступны и другим программам, если данный класс поместить в динамическую библиотеку dll и потом ее использовать в этих программах.
## Свойства
Кроме обычных методов в языке C# предусмотрены специальные методы доступа, которые называют **свойства**. Они обеспечивают простой доступ к полям классов и структур, узнать их значение или выполнить их установку.
Стандартное описание свойства имеет следующий синтаксис:
```
[модификатор_доступа] возвращаемый_тип произвольное_название
{
// код свойства
}
```
Например:
```cs
class Person
{
private string name;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
}
```
Здесь у нас есть закрытое поле *name* и есть общедоступное свойство *Name*. Хотя они имеют практически одинаковое название за исключением регистра, но это не более чем стиль, названия у них могут быть произвольные и не обязательно должны совпадать.
Через это свойство мы можем управлять доступом к переменной *name*. Стандартное определение свойства содержит блоки **get** и **set**. В блоке **get** мы возвращаем значение поля, а в блоке **set** устанавливаем. Параметр **value** представляет присваиваемое значение.
Мы можем использовать данное свойство следующим образом:
```cs
Person p = new Person();
// Устанавливаем свойство - срабатывает блок Set
// значение "Tom" и есть передаваемое в свойство value
p.Name = "Tom";
// Получаем значение свойства и присваиваем его переменной - срабатывает блок Get
string personName = p.Name;
```
Возможно, может возникнуть вопрос, зачем нужны свойства, если мы можем в данной ситуации обходиться обычными полями класса? Но свойства позволяют вложить дополнительную логику, которая может быть необходима, например, при присвоении переменной класса какого-либо значения. Например, нам надо установить проверку по возрасту:
```cs
class Person
{
private int age;
public int Age
{
set
{
if (value < 18)
{
Console.WriteLine("Возраст должен быть больше 17");
}
else
{
age = value;
}
}
get { return age; }
}
}
```
Если бы переменная *age* была бы публичной, то мы могли бы передать ей извне любое значение, в том числе отрицательное. Свойство же позволяет скрыть данные объекты и опосредовать к ним доступ.
Блоки **set** и **get** не обязательно одновременно должны присутствовать в свойстве. Если свойство определяют только блок **get**, то такое свойство доступно только для чтения - мы можем получить его значение, но не установить. И, наоборот, если свойство имеет только блок **set**, тогда это свойство доступно только для записи - можно только установить значение, но нельзя получить:
```cs
class Person
{
private string name;
// свойство только для чтения
public string Name
{
get
{
return name;
}
}
private int age;
// свойство только для записи
public int Age
{
set
{
age = value;
}
}
}
```
Хотя в примерах выше свойства определялись в классе, но точно также мы можем определять и использовать свойства в структурах.
### Модификаторы доступа
Развернуть
Мы можем применять модификаторы доступа не только ко всему свойству, но и к отдельным блокам - либо get, либо set:
```cs
class Person
{
private string name;
public string Name
{
get
{
return name;
}
private set
{
name = value;
}
}
public Person(string name)
{
Name = name;
}
}
```
Теперь закрытый блок **set** мы сможем использовать только в данном классе - в его методах, свойствах, конструкторе, но никак не в другом классе:
```cs
Person p = new Person("Tom");
// Ошибка - set объявлен с модификатором private
//p.Name = "John";
Console.WriteLine(p.Name);
```
При использовании модификаторов в свойствах следует учитывать ряд ограничений:
* Модификатор для блока **set** или **get** можно установить, если свойство имеет оба блока (и set, и get)
* Только один блок **set** или **get** может иметь модификатор доступа, но не оба сразу
* Модификатор доступа блока **set** или **get** должен быть более ограничивающим, чем модификатор доступа свойства. Например, если свойство имеет модификатор **public**, то блок set/get может иметь только модификаторы **protected internal**, **internal**, **protected**, **private**
### Автоматические свойства
Свойства управляют доступом к полям класса. Однако что, если у нас с десяток и более полей, то определять каждое поле и писать для него однотипное свойство было бы утомительно. Поэтому в фреймворк .NET были добавлены автоматические свойства. Они имеют сокращенное объявление:
```cs
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
```
На самом деле тут также создаются поля для свойств, только их создает не программист в коде, а компилятор автоматически генерирует при компиляции.
В чем преимущество автосвойств, если по сути они просто обращаются к автоматически создаваемой переменной, почему бы напрямую не обратиться к переменной без автосвойств? Дело в том, что в любой момент времени при необходимости мы можем развернуть автосвойство в обычное свойство, добавить в него какую-то определенную логику.
Стоит учитывать, что нельзя создать автоматическое свойство только для записи, как в случае со стандартными свойствами.
Автосвойствам можно присвоить значения по умолчанию (инициализация автосвойств):
```cs
class Person
{
public string Name { get; set; } = "Tom";
public int Age { get; set; } = 23;
}
class Program
{
static void Main(string[] args)
{
Person person = new Person();
Console.WriteLine(person.Name); // Tom
Console.WriteLine(person.Age); // 23
Console.Read();
}
}
```
И если мы не укажем для объекта **Person** значения свойств *Name* и *Age*, то будут действовать значения по умолчанию.
Стоит отметить, что в структурах мы не можем использовать инициализацию автосвойств.
Автосвойства также могут иметь модификаторы доступа:
```cs
class Person
{
public string Name { private set; get;}
public Person(string n)
{
Name = n;
}
}
```
Мы можем убрать блок **set** и сделать автосвойство доступным только для чтения. В этом случае для хранения значения этого свойства для него неявно будет создаваться поле с модификатором readonly, поэтому следует учитывать, что подобные get-свойства можно установить либо из конструктора класса, как в примере выше, либо при инициализации свойства:
```cs
class Person
{
public string Name { get;} = "Tom"
}
```
### Сокращенная запись свойств
Как и методы, мы можем сокращать свойства. Например:
```cs
class Person
{
private string name;
// эквивалентно public string Name { get { return name; } }
public string Name => name;
}
```
## Перегрузка методов
Иногда возникает необходимость создать один и тот же метод, но с разным набором параметров. И в зависимости от имеющихся параметров применять определенную версию метода. Такая возможность еще называется перегрузкой методов (method overloading).
И в языке C# мы можем создавать в классе несколько методов с одним и тем же именем, но разной сигнатурой. Что такое сигнатура? Сигнатура складывается из следующих аспектов:
* Имя метода
* Количество параметров
* Типы параметров
* Порядок параметров
* Модификаторы параметров
Но названия параметров в сигнатуру НЕ входят. Например, возьмем следующий метод:
```cs
public int Sum(int x, int y)
{
return x + y;
}
```
У данного метода сигнатура будет выглядеть так: Sum(int, int)
И перегрузка метода как раз заключается в том, что методы имеют разную сигнатуру, в которой совпадает только название метода. То есть методы должны отличаться по:
* Количеству параметров
* Типу параметров
* Порядку параметров
* Модификаторам параметров
Например, пусть у нас есть следующий класс:
```cs
class Calculator
{
public void Add(int a, int b)
{
int result = a + b;
Console.WriteLine($"Result is {result}");
}
public void Add(int a, int b, int c)
{
int result = a + b + c;
Console.WriteLine($"Result is {result}");
}
public int Add(int a, int b, int c, int d)
{
int result = a + b + c + d;
Console.WriteLine($"Result is {result}");
return result;
}
public void Add(double a, double b)
{
double result = a + b;
Console.WriteLine($"Result is {result}");
}
}
```
Здесь представлены четыре разных версии метода *Add*, то есть определены четыре перегрузки данного метода.
Первые три версии метода отличаются по количеству параметров. Четвертая версия совпадает с первой по количеству параметров, но отличается по их типу. При этом достаточно, чтобы хотя бы один параметр отличался по типу. Поэтому это тоже допустимая перегрузка метода Add.
То есть мы можем представить сигнатуры данных методов следующим образом:
```
Add(int, int)
Add(int, int, int)
Add(int, int, int, int)
Add(double, double)
```
После определения перегруженных версий мы можем использовать их в программе:
```cs
class Program
{
static void Main(string[] args)
{
Calculator calc = new Calculator();
calc.Add(1, 2); // 3
calc.Add(1, 2, 3); // 6
calc.Add(1, 2, 3, 4); // 10
calc.Add(1.4, 2.5); // 3.9
Console.ReadKey();
}
}
```
Консольный вывод:
```
Result is 3
Result is 6
Result is 10
Result is 3.9
```
Также перегружаемые методы могут отличаться по используемым модификаторам. Например:
```cs
void Increment(ref int val)
{
val++;
Console.WriteLine(val);
}
void Increment(int val)
{
val++;
Console.WriteLine(val);
}
```
В данном случае обе версии метода *Increment* имеют одинаковый набор параметров одинакового типа, однако в первом случае параметр имеет модификатор **ref**. Поэтому обе версии метода будут корректными перегрузками метода *Increment*.
А отличие методов по **возвращаемому типу** или по **имени параметров** не является основанием для перегрузки. Например, возьмем следующий набор методов:
```cs
int Sum(int x, int y)
{
return x + y;
}
int Sum(int number1, int number2)
{
return x + y;
}
void Sum(int x, int y)
{
Console.WriteLine(x + y);
}
```
Сигнатура у всех этих методов будет совпадать:
```
Sum(int, int)
```
Поэтому данный набор методов не представляет корректные перегрузки метода Sum и работать не будет.
## Статические члены и модификатор static
Кроме обычных полей, методов, свойств класс может иметь статические поля, методы, свойства. Статические поля, методы, свойства относятся ко всему классу и для обращения к подобным членам класса необязательно создавать экземпляр класса. Например:
```cs
class Account
{
public static decimal bonus = 100;
public decimal totalSum;
public Account(decimal sum)
{
totalSum = sum + bonus;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Account.bonus); // 100
Account.bonus += 200;
Account account1 = new Account(150);
Console.WriteLine(account1.totalSum); // 450
Account account2 = new Account(1000);
Console.WriteLine(account2.totalSum); // 1300
Console.ReadKey();
}
}
```
В данном случае класс **Account** имеет два поля: *bonus* и *totalSum*. Поле *bonus* является статическим, поэтому оно хранит состояние класса в целом, а не отдельного объекта. И поэтому мы можем обращаться к этому полю по имени класса:
```cs
Console.WriteLine(Account.bonus);
Account.bonus += 200;
```
На уровне памяти для статических полей будет создаваться участок в памяти, который будет общим для всех объектов класса.
При этом **память для статических переменных выделяется даже в том случае, если не создано ни одного объекта этого класса**.
## Статические свойства и методы
Подобным образом мы можем создавать и использовать статические методы и свойства:
```cs
class Account
{
public Account(decimal sum, decimal rate)
{
if (sum < MinSum) throw new Exception("Недопустимая сумма!");
Sum = sum; Rate = rate;
}
private static decimal minSum = 100; // минимальная допустимая сумма для всех счетов
public static decimal MinSum
{
get { return minSum; }
set { if(value>0) minSum = value; }
}
public decimal Sum { get; private set; } // сумма на счете
public decimal Rate { get; private set; } // процентная ставка
// подсчет суммы на счете через определенный период по определенной ставке
public static decimal GetSum(decimal sum, decimal rate, int period)
{
decimal result = sum;
for (int i = 1; i <= period; i++)
result = result + result * rate / 100;
return result;
}
}
```
Переменная *minSum*, свойство *MinSum*, а также метод *GetSum* здесь определены с ключевым словом **static**, то есть они являются статическими.
Переменная *minSum* и свойство *MinSum* представляют минимальную сумму, которая допустима для создания счета. Этот показатель не относится к какому-то конкретному счету, а относится ко всем счетам в целом. Если мы изменим этот показатель для одного счета, то он также должен измениться и для другого счета. То есть в отличии от свойств *Sum* и *Rate*, которые хранят состояние объекта, переменная *minSum* хранит состояние для всех объектов данного класса.
То же самое с методом *GetSum* - он вычисляет сумму на счете через определенный период по определенной процентной ставке для определенной начальной суммы. Вызов и результат этого метода не зависит от конкретного объекта или его состояния.
Таким образом, переменные и свойства, которые хранят состояние, общее для всех объектов класса, следует определять как статические. И также методы, которые определяют общее для всех объектов поведение, также следует объявлять как статические.
Статические члены класса являются общими для всех объектов этого класса, поэтому к ним надо обращаться по имени класса:
```cs
Account.MinSum = 560;
decimal result = Account.GetSum(1000, 10, 5);
```
Следует учитывать, что статические методы могут обращаться только к статическим членам класса. Обращаться к нестатическим методам, полям, свойствам внутри статического метода мы не можем.
Нередко статические поля применяются для хранения счетчиков. Например, пусть у нас есть класс **User**, и мы хотим иметь счетчик, который позволял бы узнать, сколько объектов **User** создано:
```cs
class User
{
private static int counter = 0;
public User()
{
counter++;
}
public static void DisplayCounter()
{
Console.WriteLine($"Создано {counter} объектов User");
}
}
class Program
{
static void Main(string[] args)
{
User user1 = new User();
User user2 = new User();
User user3 = new User();
User user4 = new User();
User user5 = new User();
User.DisplayCounter(); // 5
Console.ReadKey();
}
}
```
### Статический конструктор
Кроме обычных конструкторов у класса также могут быть статические конструкторы. Статические конструкторы имеют следующие отличительные черты:
* Статические конструкторы не должны иметь модификатор доступа и не принимают параметров
* Как и в статических методах, в статических конструкторах нельзя использовать ключевое слово **this** для ссылки на текущий объект класса и можно обращаться только к статическим членам класса
* Статические конструкторы нельзя вызвать в программе вручную. Они выполняются автоматически при самом первом создании объекта данного класса или при первом обращении к его статическим членам (если таковые имеются)
* Статические конструкторы обычно используются для инициализации статических данных, либо же выполняют действия, которые требуется выполнить только один раз
Определим статический конструктор:
```cs
class User
{
static User()
{
Console.WriteLine("Создан первый пользователь");
}
}
class Program
{
static void Main(string[] args)
{
User user1 = new User(); // здесь сработает статический конструктор
User user2 = new User();
Console.Read();
}
}
```
### Статические классы
Статические классы объявляются с модификатором **static** и могут содержать только статические поля, свойства и методы. Например, если бы класс **Account** имел бы только статические переменные, свойства и методы, то его можно было бы объявить как статический:
```cs
static class Account
{
private static decimal minSum = 100; // минимальная допустимая сумма для всех счетов
public static decimal MinSum
{
get { return minSum; }
set { if(value>0) minSum = value; }
}
// подсчет суммы на счете через определенный период по определенной ставке
public static decimal GetSum(decimal sum, decimal rate, int period)
{
decimal result = sum;
for (int i = 1; i <= period; i++)
result = result + result * rate / 100;
return result;
}
}
```
В C# показательным примером статического класса является класс **Math**, который применяется для различных математических операций.
## Наследование
Наследование (inheritance) является одним из ключевых моментов ООП. Благодаря наследованию один класс может унаследовать функциональность другого класса.
Пусть у нас есть следующий класс **Person**, который описывает отдельного человека:
```cs
class Person
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
public void Display()
{
Console.WriteLine(Name);
}
}
```
Но вдруг нам потребовался класс, описывающий сотрудника предприятия - класс **Employee**. Поскольку этот класс будет реализовывать тот же функционал, что и класс **Person**, так как сотрудник - это также и человек, то было бы рационально сделать класс **Employee** производным (или наследником, или подклассом) от класса **Person**, который, в свою очередь, называется базовым классом или родителем (или суперклассом):
```cs
class Employee : Person
{
}
```
После двоеточия мы указываем базовый класс для данного класса. Для класса **Employee** базовым является **Person**, и поэтому класс **Employee** наследует все те же свойства, методы, поля, которые есть в классе Person. Единственное, что не передается при наследовании, это конструкторы базового класса.
Таким образом, наследование реализует отношение **is**-a (является), объект класса **Employee** также является объектом класса **Person**:
```cs
static void Main(string[] args)
{
Person p = new Person { Name = "Tom"};
p.Display();
p = new Employee { Name = "Sam" };
p.Display();
Console.Read();
}
```
И поскольку объект **Employee** является также и объектом **Person**, то мы можем так определить переменную: `Person p = new Employee()`.
По умолчанию все классы наследуются от базового класса **Object**, даже если мы явным образом не устанавливаем наследование. Поэтому все классы, кроме своих собственных методов, также будут иметь и методы класса **Object**: *ToString()*, *Equals()*, *GetHashCode()* и *GetType()*.
Все классы по умолчанию могут наследоваться. Однако здесь есть ряд ограничений:
* Не поддерживается множественное наследование, класс может наследоваться только от одного класса.
* При создании производного класса надо учитывать тип доступа к базовому классу - тип доступа к производному классу должен быть таким же, как и у базового класса, или более строгим. То есть, если базовый класс у нас имеет тип доступа **internal**, то производный класс может иметь тип доступа **internal** или **private**, но не **public**.
Однако следует также учитывать, что если базовый и производный класс находятся в разных сборках (проектах), то в этом случае производый класс может наследовать только от класса, который имеет модификатор **public**.
* Если класс объявлен с модификатором **sealed**, то от этого класса нельзя наследовать и создавать производные классы. Например, следующий класс не допускает создание наследников:
```cs
sealed class Admin
{
}
```
* Нельзя унаследовать класс от статического класса.
### Доступ к членам базового класса из класса-наследника
Вернемся к нашим классам **Person** и **Employee**. Хотя **Employee** наследует весь функционал от класса **Person**, посмотрим, что будет в следующем случае:
```cs
class Employee : Person
{
public void Display()
{
Console.WriteLine(_name);
}
}
```
Этот код не сработает и выдаст ошибку, так как переменная *_name* объявлена с модификатором **private** и поэтому к ней доступ имеет только класс **Person**. Но зато в классе **Person** определено общедоступное свойство *Name*, которое мы можем использовать, поэтому следующий код у нас будет работать нормально:
```cs
class Employee : Person
{
public void Display()
{
Console.WriteLine(Name);
}
}
```
Таким образом, производный класс может иметь доступ только к тем членам базового класса, которые определены с модификаторами **private**, **protected** (если базовый и производный класс находятся в одной сборке), **public**, **internal** (если базовый и производный класс находятся в одной сборке), **protected** и **protected internal**.
### Ключевое слово base
Теперь добавим в наши классы конструкторы:
```cs
class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public void Display()
{
Console.WriteLine(Name);
}
}
class Employee : Person
{
public string Company { get; set; }
public Employee(string name, string company)
: base(name)
{
Company = company;
}
}
```
Класс **Person** имеет конструктор, который устанавливает свойство *Name*. Поскольку класс **Employee** наследует и устанавливает то же свойство *Name*, то логично было бы не писать по сто раз код установки, а как-то вызвать соответствующий код класса **Person**. К тому же свойств, которые надо установить в конструкторе базового класса, и параметров может быть гораздо больше.
С помощью ключевого слова **base** мы можем обратиться к базовому классу. В нашем случае в конструкторе класса **Employee** нам надо установить имя и компанию. Но имя мы передаем на установку в конструктор базового класса, то есть в конструктор класса **Person**, с помощью выражения `base(name)`.
```cs
static void Main(string[] args)
{
Person p = new Person("Bill");
p.Display();
Employee emp = new Employee ("Tom", "Microsoft");
emp.Display();
Console.Read();
}
```
### Конструкторы в производных классах
Конструкторы не передаются производному классу при наследовании. И если в базовом классе не определен конструктор по умолчанию без параметров, а только конструкторы с параметрами (как в случае с базовым классом **Person**), то в производном классе мы обязательно должны вызвать один из этих конструкторов через ключевое слово **base**. Например, из класса **Employee** уберем определение конструктора:
```cs
class Employee : Person
{
public string Company { get; set; }
}
```
В данном случае мы получим ошибку, так как класс **Employee** не соответствует классу **Person**, а именно не вызывает конструктор базового класса. Даже если бы мы добавили какой-нибудь конструктор, который бы устанавливал все те же свойства, то мы все равно бы получили ошибку:
```cs
public Employee(string name, string company)
{
Name = name;
Company = company;
}
```
То есть в классе **Employee** через ключевое слово **base** надо явным образом вызвать конструктор класса **Person**:
```cs
public Employee(string name, string company)
: base(name)
{
Company = company;
}
```
Либо в качестве альтернативы мы могли бы определить в базовом классе конструктор без параметров:
```cs
class Person
{
// остальной код класса
// конструктор по умолчанию
public Person()
{
FirstName = "Tom";
Console.WriteLine("Вызов конструктора без параметров");
}
}
```
Тогда в любом конструкторе производного класса, где нет обращения к конструктору базового класса, все равно неявно вызывался бы этот конструктор по умолчанию. Например, следующий конструктор
```cs
public Employee(string company)
{
Company = company;
}
```
Фактически был бы эквивалентен следующему конструктору:
```cs
public Employee(string company)
:base()
{
Company = company;
}
```
### Порядок вызова конструкторов
При вызове конструктора класса сначала отрабатывают конструкторы базовых классов и только затем конструкторы производных. Например, возьмем следующие классы:
```cs
class Person
{
string name;
int age;
public Person(string name)
{
this.name = name;
Console.WriteLine("Person(string name)");
}
public Person(string name, int age) : this(name)
{
this.age = age;
Console.WriteLine("Person(string name, int age)");
}
}
class Employee : Person
{
string company;
public Employee(string name, int age, string company) : base(name, age)
{
this.company = company;
Console.WriteLine("Employee(string name, int age, string company)");
}
}
```
При создании объекта Employee:
```cs
Employee tom = new Employee("Tom", 22, "Microsoft");
```
Мы получим следующий консольный вывод:
```
Person(string name)
Person(string name, int age)
Employee(string name, int age, string company)
```
В итоге мы получаем следующую цепь выполнений.
* Вначале вызывается конструктор `Employee(string name, int age, string company)`. Он делегирует выполнение конструктору `Person(string name, int age)`
* Вызывается конструктор `Person(string name, int age)`, который сам пока не выполняется и передает выполнение конструктору `Person(string name)`
* Вызывается конструктор `Person(string name)`, который передает выполнение конструктору класса **System.Object**, так как это базовый по умолчанию класс для **Person**.
* Выполняется конструктор *System.Object.Object()*, затем выполнение возвращается конструктору `Person(string name)`
* Выполняется тело конструктора `Person(string name)`, затем выполнение возвращается конструктору `Person(string name, int age)`
* Выполняется тело конструктора `Person(string name, int age)`, затем выполнение возвращается конструктору `Employee(string name, int age, string company)`
* Выполняется тело конструктора `Employee(string name, int age, string company)`. В итоге создается объект Employee
## Виртуальные методы и свойства
При наследовании нередко возникает необходимость изменить в классе-наследнике функционал метода, который был унаследован от базового класса. В этом случае класс-наследник может переопределять методы и свойства базового класса.
Те методы и свойства, которые мы хотим сделать доступными для переопределения, в базовом классе помечается модификатором **virtual**. Такие методы и свойства называют виртуальными.
А чтобы переопределить метод в классе-наследнике, этот метод определяется с модификатором **override**. Переопределенный метод в классе-наследнике должен иметь тот же набор параметров, что и виртуальный метод в базовом классе.
Например, рассмотрим следующие классы:
```cs
class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public virtual void Display()
{
Console.WriteLine(Name);
}
}
class Employee : Person
{
public string Company { get; set; }
public Employee(string name, string company) : base(name)
{
Company = company;
}
}
```
Здесь класс **Person** представляет человека. Класс **Employee** наследуется от **Person** и представляет сотруднника предприятия. Этот класс кроме унаследованного свойства *Name* имеет еще одно свойство - *Company*.
Чтобы сделать метод *Display* доступным для переопределения, этот метод определен с модификатором **virtual**. Поэтому мы можем переопределить этот метод, но можем и не переопределять. Допустим, нас устраивает реализация метода из базового класса. В этом случае объекты **Employee** будут использовать реализацию метода *Display* из класса *Person*:
```cs
static void Main(string[] args)
{
Person p1 = new Person("Bill");
p1.Display(); // вызов метода Display из класса Person
Employee p2 = new Employee("Tom", "Microsoft");
p2.Display(); // вызов метода Display из класса Person
Console.ReadKey();
}
```
Консольный вывод:
```
Bill
Tom
```
Но также можем переопределить виртуальный метод. Для этого в классе-наследнике определяется метод с модификатором **override**, который имеет то же самое имя и набор параметров:
```cs
class Employee : Person
{
public string Company { get; set; }
public Employee(string name, string company)
: base(name)
{
Company = company;
}
public override void Display()
{
Console.WriteLine($"{Name} работает в {Company}");
}
}
```
Возьмем те же самые объекты:
```cs
static void Main(string[] args)
{
Person p1 = new Person("Bill");
p1.Display(); // вызов метода Display из класса Person
Employee p2 = new Employee("Tom", "Microsoft");
p2.Display(); // вызов метода Display из класса Employee
Console.ReadKey();
}
```
Консольный вывод:
```
Bill
Tom работает в Microsoft
```
Виртуальные методы базового класса определяют интерфейс всей иерархии, то есть в любом производном классе, который не является прямым наследником от базового класса, можно переопределить виртуальные методы. Например, мы можем определить класс **Manager**, который будет производным от **Employee**, и в нем также переопределить метод **Display**.
При переопределении виртуальных методов следует учитывать ряд ограничений:
* Виртуальный и переопределенный методы должны иметь один и тот же модификатор доступа. То есть если виртуальный метод определен с помощью модификатора **public**, то и переопредленный метод также должен иметь модификатор **public**.
* Нельзя переопределить или объявить виртуальным статический метод.
### Переопределение свойств
Также как и методы, можно переопределять свойства:
```cs
class Credit
{
public virtual decimal Sum { get; set; }
}
class LongCredit : Credit
{
private decimal sum;
public override decimal Sum
{
get
{
return sum;
}
set
{
if(value > 1000)
{
sum = value;
}
}
}
}
class Program
{
static void Main(string[] args)
{
LongCredit credit = new LongCredit { Sum = 6000 };
credit.Sum = 490;
Console.WriteLine(credit.Sum);
Console.ReadKey();
}
}
```
### Ключевое слово **base**
Кроме конструкторов, мы можем обратиться с помощью ключевого слова **base** к другим членам базового класса. В нашем случае вызов `base.Display();` будет обращением к методу *Display()* в классе **Person**:
```cs
class Employee : Person
{
public string Company { get; set; }
public Employee(string name, string company)
:base(name)
{
Company = company;
}
public override void Display()
{
base.Display();
Console.WriteLine($"работает в {Company}");
}
}
```
### Запрет переопределения методов
Также можно запретить переопределение методов и свойств. В этом случае их надо объявлять с модификатором **sealed**:
```cs
class Employee : Person
{
public string Company { get; set; }
public Employee(string name, string company)
: base(name)
{
Company = company;
}
public override sealed void Display()
{
Console.WriteLine($"{Name} работает в {Company}");
}
}
```
При создании методов с модификатором **sealed** надо учитывать, что **sealed** применяется в паре с **override**, то есть только в переопределяемых методах.
И в этом случае мы не сможем переопределить метод *Display* в классе, унаследованном от **Employee**.
## Абстрактные классы и члены классов
Кроме обычных классов в C# есть абстрактные классы. Абстрактный класс похож на обычный класс. Он также может иметь переменные, методы, конструкторы, свойства. Единственное, что при определении абстрактных классов используется ключевое слово **abstract**:
```cs
abstract class Human
{
public int Length { get; set; }
public double Weight { get; set; }
}
```
Но главное отличие состоит в том, что мы не можем использовать конструктор абстрактного класса для создания его объекта. Например, следующим образом:
```cs
Human h = new Human();
```
Зачем нужны абстрактные классы? Допустим, в нашей программе для банковского сектора мы можем определить две основных сущности: клиента банка и сотрудника банка. Каждая из этих сущностей будет отличаться, например, для сотрудника надо определить его должность, а для клиента - сумму на счете. Соответственно клиент и сотрудник будут составлять отдельные классы Client и Employee. В то же время обе этих сущности могут иметь что-то общее, например, имя и фамилию, какую-то другую общую функциональность. И эту общую функциональность лучше вынести в какой-то отдельный класс, например, Person, который описывает человека. То есть классы Employee (сотрудник) и Client (клиент банка) будут производными от класса Person. И так как все объекты в нашей системе будут представлять либо сотрудника банка, либо клиента, то напрямую мы от класса Person создавать объекты не будем. Поэтому имеет смысл сделать его абстрактным:
```cs
abstract class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public void Display()
{
Console.WriteLine(Name);
}
}
class Client : Person
{
public int Sum { get; set; } // сумма на счету
public Client(string name, int sum)
: base(name)
{
Sum = sum;
}
}
class Employee : Person
{
public string Position { get; set; } // должность
public Employee(string name, string position)
: base(name)
{
Position = position;
}
}
```
Затем мы сможем использовать эти классы:
```cs
Client client = new Client("Tom", 500);
Employee employee = new Employee ("Bob", "Apple");
client.Display();
employee.Display();
```
Или даже так:
```cs
Person client = new Client("Tom", 500);
Person employee = new Employee ("Bob", "Операционист");
```
Но мы НЕ можем создать объект **Person**, используя конструктор класса **Person**:
```cs
Person person = new Person ("Bill");
```
Однако несмотря на то, что напрямую мы не можем вызвать конструктор класса **Person** для создания объекта, тем не менее конструктор в абстрактных классах то же может играть важную роль, в частности, инициализировать некоторые общие для производных классов переменные и свойства, как в случае со свойством Name. И хотя в примере выше конструктор класса Person не вызывается, тем не менее производные классы Client и Employee могут обращаться к нему.
### Абстрактные члены классов
Кроме обычных свойств и методов абстрактный класс может иметь абстрактные члены классов, которые определяются с помощью ключевого слова **abstract** и не имеют никакого функционала. В частности, абстрактными могут быть:
* Методы
* Свойства
* Индексаторы
* События
Абстрактные члены классов не должны иметь модификатор **private**. При этом производный класс обязан переопределить и реализовать все абстрактные методы и свойства, которые имеются в базовом абстрактном классе. При переопределении в производном классе такой метод или свойство также объявляются с модификатором **override** (как и при обычном переопределении виртуальных методов и свойств). Также следует учесть, что если класс имеет хотя бы одный абстрактный метод (или абстрактные свойство, индексатор, событие), то этот класс должен быть определен как абстрактный.
Абстрактные члены также, как и виртуальные, являются частью полиморфного интерфейса. Но если в случае с виртуальными методами мы говорим, что класс-наследник наследует реализацию, то в случае с абстрактными методами наследуется интерфейс, представленный этими абстрактными методами.
### Абстрактные методы
Например, сделаем в примере выше метод *Display* абстрактным:
```cs
abstract class Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public abstract void Display();
}
class Client : Person
{
public int Sum { get; set; } // сумма на счету
public Client(string name, int sum)
: base(name)
{
Sum = sum;
}
public override void Display()
{
Console.WriteLine($"{Name} имеет счет на сумму {Sum}");
}
}
class Employee : Person
{
public string Position { get; set; } // должность
public Employee(string name, string position)
: base(name)
{
Position = position;
}
public override void Display()
{
Console.WriteLine($"{Position} {Name}");
}
}
```
### Абстрактные свойства
Следует отметить использование абстрактных свойств. Их определение похоже на определение автосвойств. Например:
```cs
abstract class Person
{
public abstract string Name { get; set; }
}
class Client : Person
{
private string name;
public override string Name
{
get { return "Mr/Ms. " + name; }
set { name = value; }
}
}
class Employee : Person
{
public override string Name { get; set; }
}
```
В классе Person определено абстрактное свойство Name. Оно похоже на автосвойство, но это не автосвойство. Так как данное свойство не должно иметь реализацию, то оно имеет только пустые блоки get и set. В производных классах мы можем переопределить это свойство, сделав его полноценным свойством (как в классе Client), либо же сделав его автоматическим (как в классе Employee).
### Отказ от реализации абстрактных членов
Производный класс обязан реализовать все абстрактные члены базового класса. Однако мы можем отказаться от реализации, но в этом случае производный класс также должен быть определен как абстрактный:
```cs
abstract class Person
{
public abstract string Name { get; set; }
}
abstract class Manager : Person
{
}
```
### Пример абстрактного класса
Xрестоматийным примером является система геометрических фигур. В реальности не существует геометрической фигуры как таковой. Есть круг, прямоугольник, квадрат, но просто фигуры нет. Однако же и круг, и прямоугольник имеют что-то общее и являются фигурами:
```cs
// абстрактный класс фигуры
abstract class Figure
{
// абстрактный метод для получения периметра
public abstract float Perimeter();
// абстрактный метод для получения площади
public abstract float Area();
}
// производный класс прямоугольника
class Rectangle : Figure
{
public float Width { get; set; }
public float Height { get; set; }
public Rectangle(float width, float height)
{
this.Width = width;
this.Height = height;
}
// переопределение получения периметра
public override float Perimeter()
{
return Width * 2 + Height * 2;
}
// переопрелеление получения площади
public override float Area()
{
return Width * Height;
}
}
```
---
## Контрольные вопросы
1. Члены класса
2. Константы класса
3. Конструкторы класса
4. Что такое this
5. Инициализатор объекта
6. Модификаторы доступа
7. Свойства класса
8. Перегрузка методов
## Задание на дом
Написать иерархию классов для произвольной предметной области. Предметная область должна быть уникальной. Выбранную предметную область вы будете использовать до конца курса и, если захотите, продолжите использовать на следующих курсах при написании курсовых проектов.
Например, предметная область "Ресторан". В ресторане есть меню (с категориями) и сотрудники (с ролями):
```cs
class Category {
public string title { get;set; }
}
class Menu {
public string title { get;set; }
public string description { get;set; }
public Category category { get;set; }
}
class Role {
public string title { get;set; }
}
class User {
public Role role { get;set; }
public string firstName { get;set; }
public string lastName { get;set; }
}
var menuItem = new Menu {
title = "Бургер",
description = "Описание бургера",
category = new Category { title = "фастфуд" }
}
```
Приветствуется использование конструкторов и наследования
Предыдущая лекция | | Следующая лекция
:----------------:|:----------:|:----------------:
[Регулярные выражения](./t5_regex.md) | [Содержание](../readme.md#тема-6-основные-принципы-объектно-ориентированного-программирования) | [Ещё раз про классы. Интерфейсы.](./t6_oop_habr.md)