Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы объектно ориентированного программирования на языке С# базовые сведения о языке. Учебное пособие
.pdf
5. КЛАССЫ. ЧЛЕНЫ КЛАССА. СТРУКТУРЫ
В большинстве современных языков возможность определения собственных типов является одной из ключевых. В языке C# одним из базовых способов создания собственного типа данных является возможность
определения классов. Класс – это некоторая структура данных, которая
может содержать в себе как данные (константы и поля), так и функциональность (методы, свойства, события, индексаторы, конструкторы,
финалайзеры и т. д.). Кроме того, класс может содержать вложенные определения типов, область видимости которых может быть ограничена классом. Очень сложно определить упомянутые термины и возможности классов независимо друг от друга, поэтому некоторые определения могут показаться читателю слишком странными, но, как мы надеемся, только до
конца данного раздела. В общем случае описание класса имеет вид (подчеркнуты необязательные части описания) <атрибуты класса> <модифи-
каторы класса> partial class <идентификатор> <описание параметров
для универсального класса> : <базовый класс и интерфейсы> <требования к типам-параметрам> {<Тело класса>};
Ключевое слово partial сообщает компилятору, что приводится только
часть описания класса и что остальное описание находится в другом файле
(в принципе, возможно, что остальная часть класса вообще описана на каком-то другом языке, например на XAML).
Пока пропустим пояснения, что такое атрибуты, модификаторы и параметры класса, а также наследование и интерфейсы, и сосредоточимся на
теле класса. Тело класса содержит описания членов класса и вложенных
типов. Любое из этих описаний может содержать модификаторы доступности этого описания. Сам класс среди модификаторов класса тоже может
содержать модификаторы доступности.
5.1. Модификаторы доступности
Модификатор private считается заданным по умолчанию, но для повышения читабельности кода его лучше задавать явно. Модификатор означает, что описываемый член класса не будет доступен вне класса.
Модификатор public означает, что описываемый член класса полностью (без каких-либо ограничений) доступен вне класса (при этом его
внешнее имя будет иметь вид <идентификатор класса> . <идентифика-
тор члена>).
31

Модификатор internal отличается от public тем, что описываемый
член класса не будет доступен вне текущей сборки (т. е. если проект собирает некоторую библиотеку, то пользователь библиотеки не будет иметь
доступа к этому члену).
Модификатор protected означает, что описываемый член класса доступен внутри класса и для всех его наследников, доступа снаружи нет.
Модификатор protected internal совмещает возможности двух модификаторов. Внутри текущей сборки описываемый элемент полностью доступен, а вне ее доступен только в случае наследования.
Модификатор private protected ограничивает доступ наследников
класса только текущим проектом, т. е. вне проекта описываемый элемент
не доступен даже в случае наследования.
Модификатор file ограничивает доступ текущим файлом.
Заметим, что ограничения доступа не имеют никакого отношения к
защите информации. Эти ограничения чисто синтаксические, они вводятся
не с целью реально закрыть информацию, а с целью помочь понять из описания класса тому, кто его использует, какие члены класса предназначены
для внешнего пользователя, а какие разработчик класса предполагал применять в своих внутренних алгоритмах. При этом в C# существуют вполне
«легальные» механизмы, позволяющие (при большом желании) «достучаться» до приватных членов класса.
5.2. Создание экземпляра класса. Конструктор
Оператор new позволяет создать экземпляр класса, т. е. объект, имеющий необходимый тип. Например, если существует описание класса
MyClass, то следующий код создает экземпляр класса и присваивает его
(точнее, ссылку на него) переменной x:
MyClass a = new MyClass();
Для того чтобы не набирать имя типа дважды, можно воспользоваться
двумя способами. Один из них вам уже известен, это использование ключевого слов var:
var a = new MyClass();
Второй способ – это выведение типа создаваемого объекта из контекста:
MyClass a = new ();
32

Видимо, стоит пояснить, зачем нужны круглые скобки в конце. Дело
в том, что при создании экземпляра после выделения ему памяти вызывается специальный метод класса, который называется конструктор.
Этот метод предназначен для инициализации членов класса при необходимости. Конструктор всегда имеет имя, совпадающее с именем класса,
и, в отличие от прочих методов класса, не имеет описания типа возвращаемого значения и не может быть вызван никаким способом, кроме как из
оператора new или другого конструктора того же или порожденного
класса.
При этом конструктор может иметь параметры, которые передаются
в круглых скобках в операторе new. Если у класса нет явно описанных конструкторов, то у него создается неявный конструктор без параметров, не
выполняющий никаких дополнительных действий. Конструктор может
быть не единственным. Как и для любого обычного метода, вызывается
конструктор, наиболее подходящий по списку параметров. Если у класса
есть хотя бы один конструктор, то неявного конструктора без параметров
у него не будет. Если необходимо, конструкторы можно вызывать один из
другого специальным синтаксисом с ключевым словом this, например:
class MyClass
{
public int x;
int y;
public MyClass(int a, int b)
{
x = a;
y = b;
}
public MyClass() : this(10, 20) { }
};
В приведенном примере MyClass имеет два публичных конструктора.
Конструктор с двумя параметрами инициализирует значение двух полей
своими параметрами, а конструктор без параметров обращается к первому
конструктору с фиксированными значениями. Обратим внимание, что вызываемый конструктор всегда выполняется до тела вызывающего конструктора.
33

5.3. Ключевое слово this
Ключевое слово this может использоваться не только для обращения
к конструктору. Оно означает также ссылку на текущий экземпляр и может использоваться для передачи в статические методы или методы другого типа. Кроме того, если имена параметров метода совпадают с именами полей или свойств класса, то this может разрешать коллизию имен,
например:
class ThisDemo
{
string name;
public ThisDemo (string name)
{
this.name = name;
}
}
Заметим, что чаще для устранения проблем с именованием имена полей класса объявляют с символом подчеркивания в начале.
5.4. Модификаторы static и const
Оба модификатора означают, что описываемый элемент является
единственным для описываемого типа, для доступа к нему нет необходимости создавать экземпляр этого класса. Доступ к статическому или константному элементу класса синтаксически осуществляется через идентификатор класса (например, int.MaxValue – доступ к константе, описанной
в типе int).
Константа не может менять своего значения в процессе выполнения
программы, а статический элемент, если это не описание типа, может. Статические поля аналогичны глобальным переменным других языков программирования, но синтаксический доступ к ним вне класса возможен
только с использованием имени класса (которое, правда, можно задать неявно оператором using static).
Заметим, что модификатор static может быть у всего класса, это означает, что экземпляров такого класса быть не может; все методы, поля и
свойства класса статические.
У класса может быть static-конструктор. Он предназначен для инициализации статических полей класса. Статический конструктор всегда вы-
34

зывается до того, как будет создан первый экземпляр класса или будет вызвана любая статическая функция этого класса. Обратим внимание, что
при отсутствии обращений к классу статический конструктор не вызывается!
5.5. Поля и свойства
Поля – это переменные любого типа, которые объявлены непосредственно в классе. Если эта переменная не имеет модификатора static или
const, то каждый экземпляр класса будет содержать независимое значение
этой переменной. Обратиться к полю внутри класса можно просто по
имени, как, например, это сделано выше в конструкторе класса, а снаружи
класса – через имя экземпляра класса (если, конечно, поле публичное).
Например, для описанного выше экземпляра класса a можно обратиться к
полю x как a.x .
Свойства – это специализированные методы, обычно используемые
для доступа к полям. Они позволяют контролировать доступ к полю на
чтение, запись и инициализацию, конвертировать значения и т. д. Обычно
в языке C# поля класса объявляются приватными, а для доступа к ним снаружи объявляются публичные свойства. И внутри, и снаружи класса синтаксис доступа к полям и свойствам одинаков. Описание свойства имеет
вид <модификаторы типа> <тип> {<модификатор доступа> get <get-
метод>; set <set-метод>; init <init-метод>}. При обращении к свойству
на чтение вызывается get-метод, а на запись – set-метод. Обращение к
init-методу происходит при задании начального значения свойству (либо
из конструктора, либо с использованием специального синтаксиса инициализации при создании экземпляра). Покажем использование свойств на
примере (на этот раз полностью работоспособной программы):
var angle = new Angle();
angle.Degrees = 90;
Console.WriteLine(angle.Degrees);
Console.WriteLine(angle.Radians);
class Angle
{
double angleInRadians;
public double Radians
{
get { return angleInRadians; }
set { angleInRadians = value; }
}
public double Degrees
35

{
get { return angleInRadians / Math.PI * 180; }
set { angleInRadians = value / 180 * Math.PI; }
}
}
В результате работы этой программы на консоль будет выведено две
строки:
90
1.5707963267948966
Поясним некоторые моменты. Ключевое слово value используется в
методе sеt для обозначения присваиваемого значения, это значение всегда
имеет тип, совпадающий с типом свойства. Math – статический класс из
стандартной библиотеки, предназначенный для работы с вещественными
числами (например, в нем содержатся тригонометрические функции),
PI – константа π.
Методы get, set и init отличаются от обычных методов только отсутствием описания параметров и возвращаемого значения, в остальном это
обычные методы, которые могут содержать любые операторы, описания
переменных, типов и т. д. Как и у обычных методов, если метод содержит
только один оператор, то возможно использование более короткой формы,
без фигурных скобок, с символами ‘=>’. Более того, если у свойства есть
только метод get, то ключевое слово get также можно опустить, например,
если бы в описанном классе Angle у свойства Radians не было бы метода
set, то можно было бы написать
public double Radians => angleInRadians;
Заметим, что в том случае, когда поле используется только для хранения значения свойства, а свойство не имеет никакой логики, а только
предоставляет доступ к полю, можно использовать специальную синтаксическую конструкцию – «автосвойство». Для задания автосвойства не задается тело у методов get, set или init, и тогда компилятор автоматически
создает недоступное внутреннее поле. Перепишем приведенный выше
пример в синтаксически сокращенном виде:
var angle = new Angle() { Degrees = 90 };
Console.WriteLine(angle.Degrees);
Console.WriteLine(angle.Radians);
class Angle
36

{
public double Radians { get; set; }
public double Degrees
{
get => Radians / Math.PI * 180;
set => Radians = value / 180 * Math.PI;
}
}
Обратим внимание, что в последнем примере мы использовали еще
один новый синтаксис – инициализацию свойства при создании экземпляра класса. Собственно, именно в этом месте вызвался бы метод init,
если бы он был задан. Однако синтаксис языка запрещает одновременное
наличие методов set и init у свойства, поэтому мы его не привели в примере.
В случае если у автоматического свойства есть только get-метод, значение ему можно задать либо в конструкторе, либо начальным значением
при описании свойства, например:
public double Radians { get; } = 1.0;
Заметим, что задавать начальные значения при описании можно любым полям и автосвойствам. При этом если у класса есть конструктор, то
все начальные значения задаются до начала его выполнения.
5.6. Модификаторы readonly и required
Модификатор readonly может применяться к полям и указывает, что
поле доступно только для чтения, его значение можно задать только при
инициализации, и затем менять его нельзя (аналогично свойствам с только
get доступом).
Модификатор required может быть у полей и свойств. Он требует,
чтобы значение соответствующего поля или свойства было задано при
инициализации экземпляра.
5.7. Индексатор
Индексатор позволяет добавить классу возможность использования
этого класса в качестве массива с индексацией через квадратные скобки.
Синтаксически это похоже на свойство с фиксированным именем this,
у которого можно задать методы get и set с параметрами, заданными в
квадратных скобках:
37

<модификаторы метода> <тип значения> this [<список параметров>].
Список параметров может содержать несколько переменных произвольного типа, как и у любого метода. Приведем небольшой пример использования индексатора для вычисления членов последовательности
Фибоначчи:
var fibonacci = new Fibonacci();
Console.WriteLine(fibonacci[0]);
Console.WriteLine(fibonacci[1]);
Console.WriteLine(fibonacci[2]);
Console.WriteLine(fibonacci[10]);
class Fibonacci
{
static readonly double sqrt5 = Math.Sqrt(5.0);
static readonly double frac1 = (1 + sqrt5) / 2;
static readonly double frac2 = (1 - sqrt5) / 2;
public long this[long i]
{
get => (long)((Math.Pow(frac1, i) - Math.Pow(frac2, i)) / sqrt5);
}
}
Результат работы этой программы:
0
1
1
55
5.8. Ссылочные типы и типы-значения
Для того чтобы начать чуть более углубленное изучение возможностей C#, необходимо понять, что, собственно, означают переменные. В отличие от многих других языков, в C# переменная необязательно хранит
значение какого-то типа. Хранение, присваивание, передача параметров
реально зависят от типа переменной. В языке есть две разновидности типов. Первый, тип-значение (value type), действительно хранит в переменной свое значение, передается в методы в качестве параметра копированием, копирует значение при присваивании. Примером типа-значения являются все числовые типы, описанные выше. Вторая разновидность – ссылочные типы (reference-type). Переменная ссылочного типа всегда хранит
не значение типа, а ссылку на объект. Присваивание и передача параметров в этом случае означают копирование ссылки, а не самого объекта, новый объект не создается. Для фактического создания ссылочного типа
38

обычно используется оператор new. Оператора удаления объектов, аналогичного оператору delete в С++, в C# нет. Освобождением памяти занимается специальный «сборщик мусора» (Garbage Collector), который имеет
право освободить память от объекта, на который нет действующих ссылок.
Примером ссылочного типа является любой класс. Проиллюстрируем понятие переменной ссылочного типа:
TestClass test1 = new() { Test = 12345 };
Console.WriteLine(test1.Test);
TestClass test2 = test1;
test2.Test = 54321;
Console.WriteLine(test1.Test);
class TestClass
{
public int Test { get; set; }
}
Результат работы этой программы:
12345
54321
По результатам видно, что test1 и test2 – это именно ссылки на объект,
поскольку при изменении содержимого второй переменной изменилось
содержимое первой.
Обратим внимание, что способ передачи параметров и присваивания – не единственное отличие типов-значений и ссылочных типов. Отличается, например, поведение операторов ‘==’ и ‘!=’ (если их не переопределить для типа). Для типов-значений сравниваются значения (содержимое) объектов, а для ссылочных типов – значения ссылок.
Начиная с 8-й версии языка, в C# введена поддержка проверки допустимости пустых ссылок. Ее можно включить либо для проекта в целом,
либо командой препроцессора #nullable enable в тексте программы. В этом
случае переменные ссылочного типа делятся на две разновидности – допускающие и не допускающие значения null. Синтаксически переменная,
допускающая значение null, должна описываться типом со знаком вопроса
в конце. Например, при описании
string A; string? B;
переменная A не может принимать значение null, а переменная B может.
Заметим, что проверка на допустимость пустой ссылки делается только во
39

время компиляции, поэтому ничто не запрещает во время выполнения какой-то функции, не поддерживающей nullable-соглашений, присвоить переменной A значение null. Будьте осторожны!
Для типов-значений еще до 8-й версии языка существовали типы со
знаком «?», например int?. Это специальные структуры, которые хранят
внутри себя значение соответствующего типа (Value) и признак его наличия (HasValue). Например:
int? a = null;
if (a.HasValue) Console.WriteLine(a.Value);
a = 10;
if (a.HasValue) Console.WriteLine(a.Value);
В этом примере после первой проверки ничего не напечатается, а после
второй – напечатается 10.
5.9. Условные выражения для синтаксического упрощения
работы с null
Во многих языках программирования вводится понятие пустой (нулевой) ссылки или указателя для использования в качестве значения, которое
переменная-ссылка или указатель принимать не должны. При этом для работы с такой переменной нужно проверять, содержит ли она допустимое
значение, например:
var b = a as T;
if(b == null) Console.WriteLine("b is T");
else Console.WriteLine("b is not T");
В C# для проверки на null введено несколько специальных возможностей.
Есть условное выражение с проверкой на null:
b = b??c;
которое по смыслу эквивалентно выражению
b = b == null?c:b;
Есть оператор присваивания с проверкой на null:
b ??= c;
40
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
