Speak.Me Учить иностранные слова

C#: классы, структуры, наследование

Классы и структуры в C# относятся к пользовательским типам (Custom types), т.е. типам, введенным разработчиком.

Классы

Класс — самый общий ссылочный тип. Простейшее объявление класса выглядит следующим образом:

Перед ключевым словом class могут присутствовать атрибуты (attributes) и модификаторы класса (class modifiers)public, internal, abstract, sealedstatic, unsafe и partial. Класс не может иметь модификатор private или protected. После названия класса могут указываться параметры обобщенного типа (generic type parameters), базовый класс (base class) и интерфейсы (interfaces).

Внутри фигурных скобок могут располагаться члены класса (сlass members): методы (methods), свойства (properties), индексаторы (indexers), события (events), поля (fields), конструкторы (constructors), перегруженные операторы (overloaded operators), вложенные типы (nested types) и файналазер (finalizer). Члены также могут иметь модификаторы доступа public, internal, private, protected, abstract, sealedstatic и partial.

Поля (Fields)

Поле — это переменная, являющаяся членом класса или структуры.

У поля может быть модификатор readonly, который предотвратит изменение поля после того как оно будет создано. Такому полю значение может быть присвоено только при объявлении или во внутреннем конструкторе типа.

Инициализировать поля не обязательно. Поля, которым не присвоено значение, принимают значение по умолчанию (0, \0, null, false). Инициализация полей запускается до выполнения конструктора, в том порядке, в котором следуют поля.

Для удобства поля одного типа можно объявлять в одной инструкции через запятую. При этом атрибуты и модификаторы так же распространяются на все поля в списке.

Методы (Methods)

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

Сигнатура метода должна быть уникальна в пределах типа. Сигнатура метода включает его имя и типы передаваемых параметров (но не имена параметров и не возвращаемый тип).

Тип может содержать перегруженные (overload) методы, т.е. методы с одинаковыми именами. При этом типы параметров у перегруженных методов должны отличаться.

Конструкторы

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

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

В вызываемый конструктор можно передавать выражения, однако в таком выражении не должна присутствовать ссылка на this (например, чтоб вызвать метод объекта), но допустимо вызывать статические методы.

Автоматический конструктор. У любого класса обязательно должен быть конструктор.  Но он может быть не задан явно. Поэтому в том случае (и только в том случае) если для класса не задан ни один конструктор компилятор C# автоматически генерирует публичный (public) конструктор без параметров.

Непубличный конструктор. Конструктор необязательно должен быть публичным (public). Самый распространенный пример, когда может понадобиться непубличный конструктор — необходимость контролировать создание экземпляров класса через вызов статического метода. Такой класс сначала проверяет наличие уже созданного объекта и если он уже создан — возвращает его, если нет — создает новый. Также такой метод может возвращать определенный подкласс исходя из входных аргументов.

Инициализаторы объекта

Чтобы упростить инициализацию объекта, все его доступные поля и свойства могут быть инициализированы с помощью инициализатора объекта сразу после создания объекта.

Ссылка this

Ссылка this указывает на экземпляр объекта (но не на тип с его статическими методами). Она может быть использована для вызова перегруженного конструктора, как указывалось выше, а так же в теле методов:

Ссылка this также разрешает неоднозначность между локальными переменными или параметрами и полями объекта.

Ссылку this допустимо использовать только с нестатичными членами класса или структуры.

Свойства (Properties)

Внешне свойства ничем не отличаются от полей, но внутри самого класса они отличаются тем, что подобно методам содержат логику. Свойства определяются также как поля, но с добавлением блока get/set.

get и set — это средства доступа к свойствам (accessors). get выполняется при чтении свойства. Он должен возвращать значение того же типа, что и само свойство. set выполняется когда свойству присваивается значение. Оно имеет скрытый параметр value того же типа, что и само свойство.

Хотя доступ к полям и свойствам осуществляется одинаково, свойства в отличие от полей предоставляют полный контроль над процессом присвоения и получения значений (например, метод set может выбрасывать исключения и т.д.).

Если из средств доступа установлен только get, свойство будет доступно только для чтения (read-only), а если только set — только для записи (write-only). Обычно для свойства создается частное поле, в котором сохраняются данные, но это не обязательно: свойство может возвращать результаты расчета других данных.

Как уже отмечалось, самое распространенное назначение свойств — получение и присвоение значения частным полям того же типа что и свойство. Это можно сделать автоматически, используя объявление автоматических свойств (automatic property).

В этом случае компилятор автоматически сгенерирует частное поле (имя поля автоматически генерируется компилятором), которое будет доступно только через свойство.

Модификаторы доступа для get и set. Средства доступа get и set могут иметь разные уровни доступа, которые задаются с помощью соответствующих модификаторов доступа.

Индексаторы (Indexers)

Индексаторы позволяют использовать более естественный синтаксис для доступа к элементам класса или структуры, содержащим список (list) или словарь (dictionary) значений. Индексаторы похожи на свойства, но они обеспечивают доступ с помощью аргумента индекса, а не по имени свойства. Например, класс string имеет индексатор, позволяющий получить доступ к каждому символу (char) строки с помощью целочисленного (int) индекса.

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

Чтобы задать индексатор нужно определить свойство с именем this и указать ему аргументы в квадратных скобках.

Использовать индексатор из примера можно так:

Тип может содержать несколько индексаторов, но каждый из них должен принимать параметры разных типов. Также индексатор может принимать больше одного параметра.

Если опустить средство доступа set, индексатор будет доступен только для чтения.

Константы (Constants)

Константа — это статическое поле, значение которого не может быть изменено. Константа может быть любого предопределенного числового типа, а также типа bool, char, string или enum.

Константа объявляется с помощью ключевого слова const и должна быть сразу инициализирована (в отличие от readonly поля она не может быть инициализирована в конструкторе).

Константа более ограничена чем static readonly поле — в использовании типов и инициализации, а также в том, что она вычисляется при компиляции.

Константа может быть объявлена локально внутри метода.

Статические конструкторы (Static Constructors)

Статический конструктор выполняется один раз для типа, а не для экземпляра объекта. Для типа можно определить только один статический конструктор. Называться он должен как сам тип и не может принимать параметров.

Статический конструктор вызывается при выполнении перед первым использованием типа:

  1. перед созданием экземпляра типа
  2. перед доступом к статическому члену типа.

При создании экземпляра статический конструктор вызывается первым до вызова любого другого (экземплярного) конструктора.

Если статический конструктор выбросит неперехваченное исключение, тип станет недоступным на все время выполнения программы.

Статические классы

Класс может быть помечен как static, что будет означать, что он может содержать только статические члены и не может быть наследован.

Финализаторы (Finalizers)

Финализатор — это метод класса (и только класса), который выполняется непосредственно перед тем как сборщик мусора очистит память от неиспользуемого объекта (на который больше нет ссылок). Финализатор должен называться как сам класс с префиксом в виде символа ~.

Разделяемые типы и методы (Partial Types and Methods)

Разделяемые типы позволяют разбить определение класса на несколько частей (обычно на несколько файлов). Каждый участник должен быть помечен ключевым словом partial, а также участники не должны иметь конфликтующих членов. Все участники должны содержаться в одной сборке.

Базовый класс может быть указан у одного участника или у всех. К тому же у каждого участника может быть указан отдельный интерфейс.

Разделяемый тип может включать разделяемые методы. Разделяемый метод состоит из двух частей: определение (definition) и реализация (implementation).

Разделяемые методы не должны возвращать значения (void) и по умолчанию являются частными (private).

Методы расширения (Extension Methods)

Методы расширения позволяют расширить существующие типы новыми методами без внесения изменений в определение типа. Методы расширения — это статические (static) методы статического класса, к первому параметру которых добавляется модификатор this, а тип этого первого параметра указывает на расширяемым типом.

В результате метод расширения может быть вызван как метод того типа, который он расширяет:

Либо как статический метод статического класса:

Помимо типов расширить можно интерфейс:

Методы расширения позволяют объединять вызовы и делают код более читабельным:

Метод расширения можно использовать только если пространство имен статического класса, в котором он объявлен, находится в области видимости (импортировано с помощью директивы using).

При возникновении конфликта имен метода расширения и собственного метода типа (экземплярного метода), последний будет иметь приоритет.

Если два метода расширения имеют одинаковую сигнатуру (имя и типы параметров), их можно вызвать только как статические методы своего класса, иначе компилятор выдаст ошибку.

Наследование (Inheritance)

Класс может наследовать от другого класса таким образом расширяя или модифицируя его. Наследование от класса позволяет переиспользовать его функциональность, вместо того чтоб писать ее с нуля.

Множественное наследование в C# не возможно — класс может наследовать только от одного класса, но сам может быть наследован любым количеством классов, формируя таким образом классовую иерархию.

В примере подклассы Stock и House наследуют свойство Name от базового класса Asset. Подклассы также называют производными классами (derived classes).

Полиморфизм (Polymorphism)

Полиморфизм — дословно с греческого — много форм. Объекты, переменные, функции — полиморфны. Это означает, что они могут содержать или обрабатывать значения различных типов. Так переменная типа X может ссылаться не только на объекты типа X, но и на объекты всех производных от X классов.

Методу из примера выше можно передавать не только объекты типа Asset, но и объекты типов Stock и House (т.к. оба относятся к типу Asset). Полиморфизм основывается на базисе, что производные классы (Stock и House) обладают всеми возможностями своего базового класса (Asset). Но не наоборот: базовый класс не обладает всеми возможностями производных. Если пример выше переделать, чтобы метод принимал только тип House, передать ему объект типа Asset будет нельзя.

Приведение к типу и ссылочное преобразование (Casting and Reference Conversions)

Переменные, ссылающиеся на объект (объектные ссылки), могут:

  • скрыто (автоматически) приводиться к типу базового класса — upcast
  • явно приводиться к типу производного класса — downcast

Upcast и downcast между совместимыми ссылочными типами выполняет ссылочное преобразование (reference conversions). В результате такого преобразования создается новая ссылка, указывающая на тот же самый объект. Upcast всегда завершается успешно, downcast — только в том случае, если объект имеет соответствующий тип.

Upcasting

В результате upcast создается ссылка базового класса из ссылки производного класса.

После upcast переменная a ссылается на тот же самый объект Stock, что и переменная msft. Сам объект в результате upcast никак не изменяется и не преобразуется:

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

Downcasting

В результате downcast создается ссылка производного класса из ссылки базового класса.

Как и в случае с upcast изменяется только сама ссылка, но не объект. Downcast требует явного приведения, поскольку данная операция потенциально может вызвать ошибку при выполнении (runtime error).

Оператор as

Оператор as выполняет downcast и возвращает null если downcast вызывает ошибку (исключение при этом не выбрасывается).

Оператор as не может выполнять пользовательских преобразований и преобразований чисел.

Оператор is

Оператор is позволяет проверить увенчается ли ссылочное преобразование успехом. Иными словами он проверяет происходит ли объект из указанного класса (или реализует ли интерфейс).

Оператор is неприменим для пользовательских или числовых преобразований, но работает для unboxing — восстановление значения из объектного образа.

Виртуальные методы (Virtual Function Members)

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

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

Если  модификатор override в производном классе для метода не задан, то метод не будет переопределен, а произойдет его сокрытие (о чем будет ниже), даже если в родительском классе метод помечен как virtual:

Переопределить (override) можно только виртуальный (virtual) метод. При попытке переопределить (добавить модификатор override) не виртуальный метод (не помеченный модификатором virtual) произойдет ошибка компиляции.

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

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

Перегруженный метод может вызвать свой виртуальный метод из базового класса с помощью ключевого слова base (об этом чуть ниже).

Абстрактные классы и члены (Abstract Classes and Abstract Members)

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

Абстрактный класс может содержать абстрактные члены, которые похожи на виртуальные, но в отличие от них не содержат реализации по умолчанию. Реализацию абстрактных членов должен содержать производный класс, в противном случае он также должен быть отмечен как абстрактный.

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

Сокрытие наследуемых членов (Hiding Inherited Members)

Базовый и производный классы могут содержать одинаковые члены. В этом случае член производного класса скрывает одноименного члена базового класса:

Для методов скрытие происходит если в производном классе метод не помечен как override (даже если в родительском он отмечен как virtual).

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

  • к ссылкам на объекты базового класса привязывается член из базового класса
  • к ссылкам на объекты производного класса привязывается член из производного класса

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

Предотвращение переопределения членов (Sealing Functions and Classes)

Переопределенный член может предотвратить дальнейшее его переопределение в производных классах. Делается это путем добавления к члену ключевого слова sealed.

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

При попытке переопределить sealed метод (или класс) произойдет ошибка компиляции. При этом sealed метод может быть скрыт.

Ключевое слово base

Ключевое слово base позволяет из производного класса получить доступ к переопределенным членам базового класса или вызвать конструктор базового класса.

Конструкторы и наследование

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

При этом следует не забывать, что при отсутствии у класса (как родительского, так и производного) явно заданного конструктора, компилятор создаст для него автоматический конструктор без параметров. Поэтому если у родительского класса не объявлен конструктор с параметрами, у производного также можно его не задавать, он будет создан компилятором. Также если в этом случае вы решите задать производному классу конструктор с параметрами, то нужно будет задать и конструктор без параметров, т.к. автоматический в этом случае не создастся.

В переопределенном конструкторе производного класса можно вызвать любой из конструкторов базового класса с помощью ключевого слова base:

Ключевое слово base схоже с ключевым словом this, за исключением того, что вызывает конструктор базового класса. В этом случает конструктор базового класса будет выполнен до конструктора производного класса.

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

Порядок инициализации полей и параметров конструктора при наследовании следующий:

  1. От производного класса к базовому:
    1. Инициализация полей
    2. Вычисление аргументов, передаваемых в конструктор базового класса
  2. От базового класса к производным:
    1. Выполнение тела конструктора

Перегрузка и наследование

Перегрузка при наследовании имеет свои особенности. Рассмотрим пример:

При вызове перегруженного метода будет использован наиболее специфичный тип:

При этом подходящий тип определяется по типу ссылки, а не по типу объекта:

Структуры (Structs)

Структуры схожи с классами, но имеют два ключевых отличия:

  • структура — значимый тип, класс — ссылочный тип
  • структуры не поддерживают наследования

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

Структура может быть использована вместо класса если симантика значимых типов более предпочтительны (например, в случае с числовыми типами). К тому же структуры более эффективны в плане производительности, т.к. на них расходуется меньше памяти.

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

У структур существует скрытый конструктор без параметров. Он используется по умолчанию и до C# версии 10 его нельзя было переопределить. Он присваивает всем полям нулевые значения. Начиная с 10 версии языка появилась возможность задать свой конструктор без параметров, переопределив поведение встроенного. В этом случае в нем необходимо явно присвоить значения всем полям.