Содержание
- Классы
- Поля (Fields)
- Методы (Methods)
- Конструкторы
- Инициализаторы объекта
- Ссылка this
- Свойства (Properties)
- Индексаторы (Indexers)
- Константы (Constants)
- Статические конструкторы (Static Constructors)
- Статические классы
- Финализаторы (Finalizers)
- Разделяемые типы и методы (Partial Types and Methods)
- Методы расширения (Extension Methods)
- Наследование (Inheritance)
- Полиморфизм (Polymorphism)
- Приведение к типу и ссылочное преобразование (Casting and Reference Conversions)
- Виртуальные методы (Virtual Function Members)
- Абстрактные классы и члены (Abstract Classes and Abstract Members)
- Сокрытие наследуемых членов (Hiding Inherited Members)
- Предотвращение переопределения членов (Sealing Functions and Classes)
- Ключевое слово base
- Конструкторы и наследование
- Перегрузка и наследование
- Структуры (Structs)
Классы и структуры в C# относятся к пользовательским типам (Custom types), т.е. типам, введенным разработчиком.
Классы
Класс — самый общий ссылочный тип. Простейшее объявление класса выглядит следующим образом:
1 2 3 | class Foo { } |
Перед ключевым словом class
могут присутствовать атрибуты (attributes) и модификаторы класса (class modifiers): public
, internal
, abstract
, sealed
, static
, 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
, sealed
, static
и partial
.
Поля (Fields)
Поле — это переменная, являющаяся членом класса или структуры.
1 2 3 4 5 | class Octopus { string name; public int Age = 10; } |
У поля может быть модификатор readonly
, который предотвратит изменение поля после того как оно будет создано. Такому полю значение может быть присвоено только при объявлении или во внутреннем конструкторе типа.
Инициализировать поля не обязательно. Поля, которым не присвоено значение, принимают значение по умолчанию (0
, \0
, null
, false
). Инициализация полей запускается до выполнения конструктора, в том порядке, в котором следуют поля.
Для удобства поля одного типа можно объявлять в одной инструкции через запятую. При этом атрибуты и модификаторы так же распространяются на все поля в списке.
1 | static readonly int legs = 8, eyes = 2; |
Методы (Methods)
Метод представляет собой действие, состоящие из ряда инструкций. При вызове метод может принимать входные данные в виде параметров и возвращать данные в точку вызова в виде возвращаемого типа. Метод может возвращать тип void
, т.е. не возвращать в точку вызова никакого значения. Метод также может возвращать данные в точку вызова с помощью параметров ref
и out
.
Сигнатура метода должна быть уникальна в пределах типа. Сигнатура метода включает его имя и типы передаваемых параметров (но не имена параметров и не возвращаемый тип).
Тип может содержать перегруженные (overload) методы, т.е. методы с одинаковыми именами. При этом типы параметров у перегруженных методов должны отличаться.
1 2 3 4 | void Foo (int x); void Foo (double x); void Foo (int x, float y); void Foo (float x, int y); |
Конструкторы
Конструктор содержит инициализирующий код для класса или структуры. Конструктор определяется как обычный метод с той лишь разницей, что имя метода должно совпадать с именем класса (структуры) и в объявлении не должно быть возвращаемого типа. Конструктор вообще не может возвращать значение (он всегда void, но void в объявлении не указывается).
1 2 3 4 5 6 7 8 9 10 | public class Panda { string name; // Объявление поля public Panda (string n) // Объявление конструктора { name = n; // Код инициализации } } ... Panda p = new Panda ("Petey"); // Вызов конструктора |
Конструктор может быть перегружен. При этом один из перегруженных конструкторов может вызывать другой, используя ключевое слово this
после двоеточия в определении класса (после параметров в скобках, но до тела класса). В этом случае тело конструктора, вызываемого через this
выполняется перед вызывающим конструктором:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class Wine { public Wine (decimal price) { // Выполнится первым ... } public Wine (decimal price, int year) : this (price) { // Выполнится вторым ... } } |
В вызываемый конструктор можно передавать выражения, однако в таком выражении не должна присутствовать ссылка на this
(например, чтоб вызвать метод объекта), но допустимо вызывать статические методы.
1 2 | public Wine (decimal price, DateTime year) : this (price, year.Year) {...} |
Автоматический конструктор. У любого класса обязательно должен быть конструктор. Но он может быть не задан явно. Поэтому в том случае (и только в том случае) если для класса не задан ни один конструктор компилятор C# автоматически генерирует публичный (public) конструктор без параметров.
Непубличный конструктор. Конструктор необязательно должен быть публичным (public). Самый распространенный пример, когда может понадобиться непубличный конструктор — необходимость контролировать создание экземпляров класса через вызов статического метода. Такой класс сначала проверяет наличие уже созданного объекта и если он уже создан — возвращает его, если нет — создает новый. Также такой метод может возвращать определенный подкласс исходя из входных аргументов.
Инициализаторы объекта
Чтобы упростить инициализацию объекта, все его доступные поля и свойства могут быть инициализированы с помощью инициализатора объекта сразу после создания объекта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Bunny { public string Name; public bool LikesCarrots, LikesHumans; public Bunny () {} public Bunny (string n) { Name = n; } } Bunny b1 = new Bunny { Name="Bo", LikesCarrots = true, LikesHumans = false }; Bunny b2 = new Bunny ("Bo") { LikesCarrots = true, LikesHumans = false }; |
Ссылка this
Ссылка this
указывает на экземпляр объекта (но не на тип с его статическими методами). Она может быть использована для вызова перегруженного конструктора, как указывалось выше, а так же в теле методов:
1 2 3 4 5 6 7 8 9 | public class Panda { public Panda Mate; public void Marry (Panda partner) { Mate = partner; partner.Mate = this; } } |
Ссылка this
также разрешает неоднозначность между локальными переменными или параметрами и полями объекта.
1 2 3 4 5 | public class Test { string name; public Test (string name) { this.name = name; } } |
Ссылку this
допустимо использовать только с нестатичными членами класса или структуры.
Свойства (Properties)
Внешне свойства ничем не отличаются от полей, но внутри самого класса они отличаются тем, что подобно методам содержат логику. Свойства определяются также как поля, но с добавлением блока get/set
.
1 2 3 4 5 6 7 8 9 | public class Stock { decimal currentPrice; // Поле public decimal CurrentPrice // Свойство { get { return currentPrice; } set { currentPrice = value; } } } |
get
и set
— это средства доступа к свойствам (accessors). get
выполняется при чтении свойства. Он должен возвращать значение того же типа, что и само свойство. set
выполняется когда свойству присваивается значение. Оно имеет скрытый параметр value
того же типа, что и само свойство.
Хотя доступ к полям и свойствам осуществляется одинаково, свойства в отличие от полей предоставляют полный контроль над процессом присвоения и получения значений (например, метод set
может выбрасывать исключения и т.д.).
Если из средств доступа установлен только get
, свойство будет доступно только для чтения (read-only), а если только set
— только для записи (write-only). Обычно для свойства создается частное поле, в котором сохраняются данные, но это не обязательно: свойство может возвращать результаты расчета других данных.
Как уже отмечалось, самое распространенное назначение свойств — получение и присвоение значения частным полям того же типа что и свойство. Это можно сделать автоматически, используя объявление автоматических свойств (automatic property).
1 2 3 4 | public class Stock { public decimal CurrentPrice { get; set; } } |
В этом случае компилятор автоматически сгенерирует частное поле (имя поля автоматически генерируется компилятором), которое будет доступно только через свойство.
Модификаторы доступа для get и set. Средства доступа get
и set
могут иметь разные уровни доступа, которые задаются с помощью соответствующих модификаторов доступа.
1 2 3 4 5 6 | private decimal x; public decimal X { get { return x; } private set { x = Math.Round (value, 2); } } |
Индексаторы (Indexers)
Индексаторы позволяют использовать более естественный синтаксис для доступа к элементам класса или структуры, содержащим список (list) или словарь (dictionary) значений. Индексаторы похожи на свойства, но они обеспечивают доступ с помощью аргумента индекса, а не по имени свойства. Например, класс string
имеет индексатор, позволяющий получить доступ к каждому символу (char
) строки с помощью целочисленного (int
) индекса.
1 2 3 | string s = "hello"; Console.WriteLine (s[0]); // 'h' Console.WriteLine (s[3]); // 'l' |
Синтаксис для использования индексатора такой же как для работы с массивом, за исключением того, что аргумент индекса может быть любого типа.
Чтобы задать индексатор нужно определить свойство с именем this
и указать ему аргументы в квадратных скобках.
1 2 3 4 5 6 7 8 9 | class Sentence { string[] words = "The quick brown fox".Split(); public string this [int wordNum] // Индексатор { get { return words [wordNum]; } set { words [wordNum] = value; } } } |
Использовать индексатор из примера можно так:
1 2 3 4 | Sentence s = new Sentence(); Console.WriteLine (s[3]); // fox s[3] = "kangaroo"; Console.WriteLine (s[3]); // kangaroo |
Тип может содержать несколько индексаторов, но каждый из них должен принимать параметры разных типов. Также индексатор может принимать больше одного параметра.
1 2 3 4 | public string this [int arg1, string arg2] { get { ... } set { ... } } |
Если опустить средство доступа set
, индексатор будет доступен только для чтения.
Константы (Constants)
Константа — это статическое поле, значение которого не может быть изменено. Константа может быть любого предопределенного числового типа, а также типа bool
, char
, string
или enum
.
Константа объявляется с помощью ключевого слова const
и должна быть сразу инициализирована (в отличие от readonly
поля она не может быть инициализирована в конструкторе).
1 2 3 4 | public class Test { public const string Message = "Hello World"; } |
Константа более ограничена чем static readonly
поле — в использовании типов и инициализации, а также в том, что она вычисляется при компиляции.
Константа может быть объявлена локально внутри метода.
1 2 3 4 5 | static void Main() { const double twoPI = 2 * System.Math.PI; ... } |
Статические конструкторы (Static Constructors)
Статический конструктор выполняется один раз для типа, а не для экземпляра объекта. Для типа можно определить только один статический конструктор. Называться он должен как сам тип и не может принимать параметров.
1 2 3 4 | class Test { static Test() { Console.Write ("Type Initialized"); } } |
Статический конструктор вызывается при выполнении перед первым использованием типа:
- перед созданием экземпляра типа
- перед доступом к статическому члену типа.
При создании экземпляра статический конструктор вызывается первым до вызова любого другого (экземплярного) конструктора.
Если статический конструктор выбросит неперехваченное исключение, тип станет недоступным на все время выполнения программы.
Статические классы
Класс может быть помечен как static, что будет означать, что он может содержать только статические члены и не может быть наследован.
Финализаторы (Finalizers)
Финализатор — это метод класса (и только класса), который выполняется непосредственно перед тем как сборщик мусора очистит память от неиспользуемого объекта (на который больше нет ссылок). Финализатор должен называться как сам класс с префиксом в виде символа ~
.
1 2 3 4 | class Class1 { ~Class1() { ... } } |
Разделяемые типы и методы (Partial Types and Methods)
Разделяемые типы позволяют разбить определение класса на несколько частей (обычно на несколько файлов). Каждый участник должен быть помечен ключевым словом partial
, а также участники не должны иметь конфликтующих членов. Все участники должны содержаться в одной сборке.
Базовый класс может быть указан у одного участника или у всех. К тому же у каждого участника может быть указан отдельный интерфейс.
Разделяемый тип может включать разделяемые методы. Разделяемый метод состоит из двух частей: определение (definition) и реализация (implementation).
1 2 3 4 5 6 7 8 9 10 11 | partial class PaymentForm { partial void ValidatePayment (decimal amount); // Определение } partial class PaymentForm { partial void ValidatePayment (decimal amount) // Реализация { if (amount > 100) Console.Write ("Expensive!"); } } |
Разделяемые методы не должны возвращать значения (void
) и по умолчанию являются частными (private
).
Методы расширения (Extension Methods)
Методы расширения позволяют расширить существующие типы новыми методами без внесения изменений в определение типа. Методы расширения — это статические (static) методы статического класса, к первому параметру которых добавляется модификатор this
, а тип этого первого параметра указывает на расширяемым типом.
1 2 3 4 5 6 7 8 | public static class StringHelper { public static bool IsCapitalized (this string s) { if (string.IsNullOrEmpty (s)) return false; return char.IsUpper (s[0]); } } |
В результате метод расширения может быть вызван как метод того типа, который он расширяет:
1 | Console.Write ("Perth".IsCapitalized()); |
Либо как статический метод статического класса:
1 | Console.Write (StringHelper.IsCapitalized ("Perth")); |
Помимо типов расширить можно интерфейс:
1 2 3 4 5 6 7 8 | public static T First<T> (this IEnumerable<T> sequence) { foreach (T element in sequence) return element; throw new InvalidOperationException ("No elements!"); } ... Console.WriteLine ("Seattle".First()); // S |
Методы расширения позволяют объединять вызовы и делают код более читабельным:
1 2 | string x = "sausage".Pluralize().Capitalize(); string y = StringHelper.Capitalize(StringHelper.Pluralize ("sausage")); |
Метод расширения можно использовать только если пространство имен статического класса, в котором он объявлен, находится в области видимости (импортировано с помощью директивы using
).
При возникновении конфликта имен метода расширения и собственного метода типа (экземплярного метода), последний будет иметь приоритет.
Если два метода расширения имеют одинаковую сигнатуру (имя и типы параметров), их можно вызвать только как статические методы своего класса, иначе компилятор выдаст ошибку.
Наследование (Inheritance)
Класс может наследовать от другого класса таким образом расширяя или модифицируя его. Наследование от класса позволяет переиспользовать его функциональность, вместо того чтоб писать ее с нуля.
Множественное наследование в C# не возможно — класс может наследовать только от одного класса, но сам может быть наследован любым количеством классов, формируя таким образом классовую иерархию.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Asset { public string Name; } public class Stock : Asset // Наследуется от Asset { public long SharesOwned; } public class House : Asset // Наследуется от Asset { public decimal Mortgage; } Stock msft = new Stock { Name="MSFT", SharesOwned=1000 }; Console.WriteLine (msft.Name); // MSFT Console.WriteLine (msft.SharesOwned); // 1000 House mansion = new House { Name="Mansion", Mortgage=250000 }; |
В примере подклассы Stock
и House
наследуют свойство Name
от базового класса Asset
. Подклассы также называют производными классами (derived classes).
Полиморфизм (Polymorphism)
Полиморфизм — дословно с греческого — много форм. Объекты, переменные, функции — полиморфны. Это означает, что они могут содержать или обрабатывать значения различных типов. Так переменная типа X может ссылаться не только на объекты типа X, но и на объекты всех производных от X классов.
1 2 3 4 | public static void Display (Asset asset) { System.Console.WriteLine (asset.Name); } |
Методу из примера выше можно передавать не только объекты типа Asset
, но и объекты типов Stock
и House
(т.к. оба относятся к типу Asset
). Полиморфизм основывается на базисе, что производные классы (Stock
и House
) обладают всеми возможностями своего базового класса (Asset
). Но не наоборот: базовый класс не обладает всеми возможностями производных. Если пример выше переделать, чтобы метод принимал только тип House
, передать ему объект типа Asset
будет нельзя.
Приведение к типу и ссылочное преобразование (Casting and Reference Conversions)
Переменные, ссылающиеся на объект (объектные ссылки), могут:
- скрыто (автоматически) приводиться к типу базового класса — upcast
- явно приводиться к типу производного класса — downcast
Upcast и downcast между совместимыми ссылочными типами выполняет ссылочное преобразование (reference conversions). В результате такого преобразования создается новая ссылка, указывающая на тот же самый объект. Upcast всегда завершается успешно, downcast — только в том случае, если объект имеет соответствующий тип.
Upcasting
В результате upcast создается ссылка базового класса из ссылки производного класса.
1 2 | Stock msft = new Stock(); Asset a = msft; // Upcast |
После upcast переменная a
ссылается на тот же самый объект Stock
, что и переменная msft
. Сам объект в результате upcast никак не изменяется и не преобразуется:
1 | Console.WriteLine (a == msft); // True |
Хотя обе переменные ссылаются на один объект, а
(ссылка базового типа) имеет более ограниченное представление об объекте: через нее можно получить доступ только к членам типа, определенным в базовом классе. Последняя строка в следующем примере вызовет ошибку компиляции, поскольку переменная a
имеет тип Asset
(базовый тип) хотя и ссылается на объект типа Stock
(производный).
1 2 | Console.WriteLine (a.Name); // OK Console.WriteLine (a.SharesOwned); // Error |
Downcasting
В результате downcast создается ссылка производного класса из ссылки базового класса.
1 2 3 4 5 6 | Stock msft = new Stock(); Asset a = msft; // Upcast Stock s = (Stock)a; // Downcast Console.WriteLine (s.SharesOwned); // No error Console.WriteLine (s == a); // True Console.WriteLine (s == msft); // True |
Как и в случае с upcast изменяется только сама ссылка, но не объект. Downcast требует явного приведения, поскольку данная операция потенциально может вызвать ошибку при выполнении (runtime error).
1 2 3 | House h = new House(); Asset a = h; Stock s = (Stock)a; // Ошибка, т.к. объект не класса Stock |
Оператор as
Оператор as
выполняет downcast и возвращает null
если downcast вызывает ошибку (исключение при этом не выбрасывается).
1 2 | Asset a = new Asset(); Stock s = a as Stock; // s = null; исключение не выбрасывается |
Оператор as
не может выполнять пользовательских преобразований и преобразований чисел.
Оператор is
Оператор is
позволяет проверить увенчается ли ссылочное преобразование успехом. Иными словами он проверяет происходит ли объект из указанного класса (или реализует ли интерфейс).
1 | if (a is Stock) Console.Write (((Stock)a).SharesOwned); |
Оператор is
неприменим для пользовательских или числовых преобразований, но работает для unboxing — восстановление значения из объектного образа.
Виртуальные методы (Virtual Function Members)
Виртуальный метод — метод отмеченный ключевым словом virtual
— может быть переопределен в производном классе с целью реализовать более специализированное поведение. В отличие от перегрузки методов, виртуальный метод может быть переопределен только в производном классе (но не в том же самом), а также может (и должен) полностью совпадать по названию и типам принимаемых параметров с методом в базовом классе. Как виртуальные могут быть объявлены не только методы, но и свойства, индексаторы и события.
1 2 3 4 5 6 7 | public class ClassA { public virtual int MethodA() { return 1; } } |
Производные классы переопределяют виртуальные методы с помощью ключевого слова override
.
1 2 3 4 5 6 7 | class ClassB : ClassA { public override int MethodA() { return 2; } } |
Если модификатор override в производном классе для метода не задан, то метод не будет переопределен, а произойдет его сокрытие (о чем будет ниже), даже если в родительском классе метод помечен как virtual
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ClassA { public virtual int MethodA() { return 1; } } class ClassB : ClassA { public int MethodA() { return 2; } } |
Переопределить (override
) можно только виртуальный (virtual
) метод. При попытке переопределить (добавить модификатор override
) не виртуальный метод (не помеченный модификатором virtual
) произойдет ошибка компиляции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ClassA { public int MethodA() { return 1; } } class ClassB : ClassA { public override int MethodA() // Compile time error { return 2; } } |
Переопределенный метод полностью заменяет собой виртуальный метод родительского класса. При приведении переменной, содержащей ссылку на объект производного класса, к типу родительского класса, она все равно будет вызывать переопределенный методы производного класса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var a = new ClassA(); var b = new ClassB(); var x = b as ClassA; Console.WriteLine(a.MethodA()); // 1 Console.WriteLine(b.MethodA()); // 2 Console.WriteLine(x.MethodA()); // 2 public class ClassA { public virtual int MethodA() { return 1; } } class ClassB : ClassA { public override int MethodA() { return 2; } } |
Как отмечалось, сигнатура (имя и типы принимаемых параметров), возвращаемый тип и доступность виртуального и переопределенного методов должны быть одинаковыми.
Перегруженный метод может вызвать свой виртуальный метод из базового класса с помощью ключевого слова base
(об этом чуть ниже).
Абстрактные классы и члены (Abstract Classes and Abstract Members)
Особенность абстрактного класса состоит в том, что экземпляр такого класса создать нельзя. Вместо этого может быть создан экземпляр конкретного производного от абстрактного класса.
Абстрактный класс может содержать абстрактные члены, которые похожи на виртуальные, но в отличие от них не содержат реализации по умолчанию. Реализацию абстрактных членов должен содержать производный класс, в противном случае он также должен быть отмечен как абстрактный.
1 2 3 4 | public abstract class Asset { public abstract decimal NetValue { get; } // Не содержит реализации } |
Производные классы переопределяют абстрактные члены точно также как виртуальные.
Сокрытие наследуемых членов (Hiding Inherited Members)
Базовый и производный классы могут содержать одинаковые члены. В этом случае член производного класса скрывает одноименного члена базового класса:
1 2 | public class A { public int Counter = 1; } public class B : A { public int Counter = 2; } |
Для методов скрытие происходит если в производном классе метод не помечен как override
(даже если в родительском он отмечен как virtual
).
Обычно такое происходит случайно, когда в базовый класс добавляется член, который уже объявлен в производном. По этой причине компилятор генерирует предупреждение и разрешает конфликт следующим образом:
- к ссылкам на объекты базового класса привязывается член из базового класса
- к ссылкам на объекты производного класса привязывается член из производного класса
Однако может возникнуть необходимость намеренно скрыть член базового класса. В этом случае нужно использовать ключевое слово new
при объявлении члена в производном классе. В этом случае ключевое слово new служит только для того, чтобы запретить компилятору генерировать предупреждение.
1 2 | public class A { public int Counter = 1; } public class B : A { public new int Counter = 2; } |
Предотвращение переопределения членов (Sealing Functions and Classes)
Переопределенный член может предотвратить дальнейшее его переопределение в производных классах. Делается это путем добавления к члену ключевого слова sealed
.
1 | public sealed override decimal Liability { get { ... } } |
Можно предотвратить переопределение членов для всего класса. Для этого ключевое слово sealed
нужно добавить к самому классу. В этом случае все виртуальные члены класса уже нельзя будет переопределить в производных классах.
При попытке переопределить sealed
метод (или класс) произойдет ошибка компиляции. При этом sealed метод может быть скрыт.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | var a = new ClassA(); var b = new ClassB(); var c = new ClassC(); var x = c as ClassA; Console.WriteLine(a.MethodA()); // 1 Console.WriteLine(b.MethodA()); // 2 Console.WriteLine(c.MethodA()); // 3 Console.WriteLine(x.MethodA()); // 2 !!! несмотря на то, что переменная типа ClassA, срабатывает метод из ClassB public class ClassA { public virtual int MethodA() { return 1; } } class ClassB : ClassA { public sealed override int MethodA() { return 2; } } class ClassC : ClassB { public new int MethodA() { return 3; } } |
Ключевое слово base
Ключевое слово base
позволяет из производного класса получить доступ к переопределенным членам базового класса или вызвать конструктор базового класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class House : Asset { public House(int x) : base(x) // конструктор вызывается вне тела { ... } public override decimal Liability { get { return base.Liability + Mortgage; } } public override int CountFloors() { ... base.CountFloors(); // метод вызывается в теле ... } } |
Конструкторы и наследование
Поскольку при инициализации объекта всегда выполняется конструктор, то наличие конструктора, как уже отмечалось выше, обязательно для всех классов. При наследовании наличие конструктора обязательно и для производного (конечного) класса и для всех его родителей. Поэтому производный класс должен объявлять свой собственный конструктор, даже если в реализации конструктор производного класса полностью совпадает с конструктором базового класса. Иначе произойдет ошибка компиляции.
1 2 3 4 5 6 7 8 9 | public class Baseclass { public int X; public Baseclass () { } public Baseclass (int x) { this.X = x; } } public class Subclass : Baseclass { } Subclass s = new Subclass (123); // Недопустимо: производный класс не содержит конструктор |
При этом следует не забывать, что при отсутствии у класса (как родительского, так и производного) явно заданного конструктора, компилятор создаст для него автоматический конструктор без параметров. Поэтому если у родительского класса не объявлен конструктор с параметрами, у производного также можно его не задавать, он будет создан компилятором. Также если в этом случае вы решите задать производному классу конструктор с параметрами, то нужно будет задать и конструктор без параметров, т.к. автоматический в этом случае не создастся.
В переопределенном конструкторе производного класса можно вызвать любой из конструкторов базового класса с помощью ключевого слова base
:
1 2 3 4 | public class Subclass : Baseclass { public Subclass (int x) : base (x) { ... } } |
Ключевое слово base
схоже с ключевым словом this
, за исключением того, что вызывает конструктор базового класса. В этом случает конструктор базового класса будет выполнен до конструктора производного класса.
Если в конструкторе производного класса опустить ключевое слово base, будет автоматически вызван конструктор базового класса без параметров, а если базовый класс не содержит объявления конструктора без параметров, компилятор сгенерирует ошибку.
Порядок инициализации полей и параметров конструктора при наследовании следующий:
- От производного класса к базовому:
- Инициализация полей
- Вычисление аргументов, передаваемых в конструктор базового класса
- От базового класса к производным:
- Выполнение тела конструктора
Перегрузка и наследование
Перегрузка при наследовании имеет свои особенности. Рассмотрим пример:
1 2 | static void Foo (Asset a) { } static void Foo (House h) { } |
При вызове перегруженного метода будет использован наиболее специфичный тип:
1 2 | House h = new House (...); Foo(h); // Будет вызван Foo(House) |
При этом подходящий тип определяется по типу ссылки, а не по типу объекта:
1 2 | Asset a = new House (...); Foo(a); // Будет вызван Foo(Asset) |
Структуры (Structs)
Структуры схожи с классами, но имеют два ключевых отличия:
- структура — значимый тип, класс — ссылочный тип
- структуры не поддерживают наследования
Структуры могут содержать все члены доступные для класса, кроме конструктора без параметров, файналайзера и виртуальных членов.
Структура может быть использована вместо класса если симантика значимых типов более предпочтительны (например, в случае с числовыми типами). К тому же структуры более эффективны в плане производительности, т.к. на них расходуется меньше памяти.
Структуре можно задать конструктор с параметрами, в нем необходимо явно присвоить значения всем полям (либо вызвать встроенный конструктор с помощью ключевого слова this). Связано это с тем, что структура инициализирует поля только в конструкторе, у нее нет механизма инициализации полей до вызова конструктора, как у класса.
У структур существует скрытый конструктор без параметров. Он используется по умолчанию и до C# версии 10 его нельзя было переопределить. Он присваивает всем полям нулевые значения. Начиная с 10 версии языка появилась возможность задать свой конструктор без параметров, переопределив поведение встроенного. В этом случае в нем необходимо явно присвоить значения всем полям.