Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы объектно ориентированного программирования на языке С# базовые сведения о языке. Учебное пособие

.pdf
Скачиваний:
1
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
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
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]