Автоматическое управление памятью
C# развертывает автоматическое управление памятью, что освобождает разработчиков от ручного выделения и освобождения памяти, занятой объектами. Политики автоматического управления памятью реализованы в сборщике мусора. Далее представлен жизненный цикл объекта управления памятью.
1. Когда объект создается, для него выделяется память, выполняется конструктор и объект считается существующим.
2. Если объект или его любая часть не могут быть вызваны посредством любой возможной продолжительности выполнения, отличной от выполнения деструкторов, объект считается неиспользуемым и становится доступным для деструкции. Компилятор C# и сборщик мусора могут анализировать код с целью определения того, какие ссылки на объект могут быть использованы в будущем. Например, если локальная переменная в области является единственной существующей ссылкой на объект, но на нее никогда не ссылались в любом возможном предложении выполнения, начиная с текущего момента выполнения в процедуре, сборщик мусора может (но не обязательно) посчитать данный объект неиспользуемым.
3. После того, как объект становится доступным для деструкции, через некоторое указанное время выполняется деструктор (§10.13) (при наличии) для объекта. В обычных ситуациях деструктор выполняется для объекта только один раз, хотя связанные с реализацией API могут разрешать переопределить это поведение.
4. Если после выполнения деструктора для объекта данный объект или любая его часть не доступны для любого продолжения выполнения, включая выполнение деструкторов, объект считается недоступным и становится доступным для коллекции.
5. В итоге, через некоторое время после того, как объект стал доступным для коллекции, сборщик мусора очищает память, связанную с объектом.
Сборщик мусора хранит информацию об использовании объекта и использует ее для принятия решений управления памятью, например, где расположить в памяти новый созданный объект, когда следует переместить объект и когда объект больше не используется или недоступен.
Подробно другим языкам, допускающим наличие сборщика мусора, C# разработан таким образом, чтобы сборщик мусора мог реализовывать широкий спектр политик управления памятью. Например, C# не требует, чтобы осуществлялись выполнение деструкторов или подборка объектов сразу после того, как они становятся доступными, или выполнение деструкторов в любом определенном порядке или в любом определенном потоке.
Поведение сборщика мусора можно в некоторой степени контролировать посредством статических методов класса System.GC. Этот класс может использоваться для запроса выполнения сборки, выполнения деструкторов (или отмены выполнения) и т. п.
Так как сборщику мусора предоставляются широкие возможности в решении момента сборки объектов и выполнения деструкторов, соответствующая реализация может привести к результату, отличающемуся от представленного кода. Программа
using System;
class A
{
~A() {
Console.WriteLine("Destruct instance of A");
}
}
class B
{
object Ref;
public B(object o) {
Ref = o;
}
~B() {
Console.WriteLine("Destruct instance of B");
}
}
class Test
{
static void Main() {
B b = new B(new A());
b = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
приводит к созданию экземпляра класса A и экземпляра класса B. Данные объекты становятся доступны сборщику мусора, когда переменной b присваивается значение null, так как после этого доступ к ним невозможен посредством любого пользовательского кода. Результат может быть одним из следующих:
Destruct instance of A
Destruct instance of B
или
Destruct instance of B
Destruct instance of A
так как язык не налагает ограничений на порядок, в котором объекты обрабатываются сборщиком мусора.
В особых случаях отличие между «доступен для деструкции» и «доступен для сборки» может быть очень важным. Например,
using System;
class A
{
~A() {
Console.WriteLine("Destruct instance of A");
}
public void F() {
Console.WriteLine("A.F");
Test.RefA = this;
}
}
class B
{
public A Ref;
~B() {
Console.WriteLine("Destruct instance of B");
Ref.F();
}
}
class Test
{
public static A RefA;
public static B RefB;
static void Main() {
RefB = new B();
RefA = new A();
RefB.Ref = RefA;
RefB = null;
RefA = null;
// A and B now eligible for destruction
GC.Collect();
GC.WaitForPendingFinalizers();
// B now eligible for collection, but A is not
if (RefA != null)
Console.WriteLine("RefA is not null");
}
}
В вышеуказанной программе, если сборщик мусора выбирает выполнение деструктора A до деструктора B, то результат программы может быть следующим:
Destruct instance of A
Destruct instance of B
A.F
RefA is not null
Обратите внимание, что, несмотря на то, что экземпляр A не был использован, а деструктор A был выполнен, методы A (в данном случае F) могут быть все равно вызваны из другого деструктора. Также обратите внимание, что выполнение деструктора может привести к тому, что объект станет снова доступен для использования из основной программы. В этом случае выполнение деструктора B привело к тому, что экземпляр A, который ранее не был использован, стал доступен из существующей ссылки Test.RefA. После вызова WaitForPendingFinalizers экземпляр B доступен для сборки мусора, а экземпляр A недоступен из-за ссылки Test.RefA.
Чтобы избежать несогласованности и неожиданного поведения, для деструкторов рекомендуется выполнять только очистку данных, сохраненных в их собственных полях объектов, и не выполнять действий со ссылочными объектами или статическими полями.
Альтернативой использованию деструкторов может служить реализация классом интерфейса System.IDisposable. Это позволит клиенту объекта определить момент освобождения ресурсов объекта, что обычно происходит путем вызова объекта в качестве ресурса с помощью оператора using (§8.13).
Порядок выполнения
Выполнение программы на языке C# выполняется таким образом, что побочные эффекты каждого выполняемого потока сохраняются в критических точках выполнения. Побочный эффект— это чтение или запись поля с модификатором volatile, запись в переменную без модификатора volatile, запись во внешний ресурс и передача исключения. Критические точки выполнения, в которых сохраняется порядок данных побочных эффектов, ссылаются на поля с модификаторами volatile (§10.5.3), операторы lock (§8.12) и создание и завершение потока. Среда выполнения может изменить порядок выполнения программы на C#, учитывая следующие ограничения.
· Зависимости данных сохраняются в рамках потока выполнения. То есть значение каждой переменной рассчитывается таким образом, как было бы рассчитано, если бы все операторы в потоке выполнялись в исходном программном порядке.
· Правила упорядочивания инициализации сохраняются (§10.5.4 и §10.5.5).
· Упорядочивание побочных эффектов сохраняется с учетом чтения и записи модификатора volatile (§10.5.3). Кроме того, среда выполнения не должна вычислять часть выражения, если невозможно определить, что значение данного выражения не используется, и что необходимые побочные эффекты не имели места (включая вызванное при вызове метода или доступе к полю с модификатором volatile). При прерывании выполнения программы асинхронным событием (например, при передаче исключения другим потоком) оригинальный программный порядок отображения наблюдаемых побочных эффектов не гарантируется.
Типы
В языке C# представлены две основные категории типов: типы значений и ссылочные типы. Оба типа значения и ссылочные типы могут использоваться как универсальные типы, принимающие один или несколько параметров типа. Параметры типа могут обозначать как типы значений, так и ссылочные типы.
type:
value-type
reference-type
type-parameter
Типы третьей категории — указатели — доступны только в небезопасном коде. Дополнительные сведения см. далее в разделе §18.2.
Различие между типами значений и ссылочными типами заключается в том, что переменные первого типа непосредственно содержат данные, а переменные второго типа хранят ссылки на соответствующие данные (объекты). Две переменные ссылочного типа могут ссылаться на один объект. Это позволяет изменять объект, на который ссылается одна переменная, выполняя соответствующие операции с другой. Каждая переменная типа значений содержит собственную копию данных. В связи с этим операции с одной переменной не влияют на другую.
В C# применяется унифицированная система типов, в которой значение любого типа может обрабатываться как объект. В C# каждый тип прямо или косвенно наследует от типа класса object, и класс object является первичным базовым классом для всех типов. Обработка значений ссылочного типа как объектов выполняется посредством рассмотрения их как значений типа object. Значения типа значений обрабатываются как объекты с использованием операций упаковки и распаковки (§4.3).
Типы значений
Существуют следующие типы значений: тип структуры и перечисляемый тип. В C# представлен набор предопределенных типов структуры, называемых простыми типами. Простые типы обозначаются зарезервированными словами.
value-type:
struct-type
enum-type
struct-type:
type-name
simple-type
nullable-type
simple-type:
numeric-type
bool
numeric-type:
integral-type
floating-point-type
decimal
integral-type:
sbyte
byte
short
ushort
int
uint
long
ulong
char
floating-point-type:
float
double
nullable-type:
non-nullable-value-type ?
non-nullable-value-type:
type
enum-type:
type-name
В отличие от переменных ссылочного типа, переменные типа значений могут содержать null только в том случае, если значение имеет обнуляемый тип. Для каждого необнуляемого типа значений существует соответствующий обнуляемый тип, включающий те же значения и значение null.
Когда переменной назначается значение типа значения, создается копия присвоенного значения. В этом заключается отличие от переменных ссылочного типа, при присвоении которых копируются ссылки, а не определяемые ими объекты.
Тип System.ValueType
Все типы значений неявно наследуются от класса System.ValueType, который, в свою очередь, наследуется от класса object. Получение типов от типов значений не поддерживается. Таким образом, типы значений являются неявно запечатанными (§10.1.1.2).
Обратите внимание, что тип System.ValueType не является типом значения. Он является типом класса, от которого автоматически получаются все типы значений.
Конструкторы по умолчанию
Во всех типах значений неявно объявляется не содержащий параметров открытый конструктор экземпляров, называемый конструктором по умолчанию. Конструктор по умолчанию возвращает заполненный нулями экземпляр типа значений, называемый значением по умолчанию.
· Для всех простых типов значение по умолчанию представляет собой битовый шаблон, заполненный нулями:
o Для типов sbyte, byte, short, ushort, int, uint, long и ulong значением по умолчанию является 0.
o Для char значение по умолчанию — '\x0000'.
o Для float значение по умолчанию — 0.0f.
o Для double значение по умолчанию — 0.0d.
o Для decimal значение по умолчанию — 0.0m.
o Для bool значение по умолчанию — false.
· Для перечисляемого типа E значение по умолчанию составляет 0 и преобразуется к типу E.
· Для типа структуры значение по умолчанию формируется путем присвоения значений по умолчанию всем полям, имеющим тип значений, а всем полям ссылочного типа — значения null.
· Для обнуляемого типа значение по умолчанию представляет собой экземпляр, свойство HasValue которого имеет значение false, а свойство Value не определено. Для обнуляемых типов значение по умолчанию также называется пустым значением.
Как и любой другой конструктор экземпляров, конструктор по умолчанию для типа значений вызывается с помощью оператора new. В целях повышения эффективности в реализации не обязательно создается вызов конструктора. В приведенном ниже примере выполняется инициализация переменных i и j нулевыми значениями.
class A
{
void F() {
int i = 0;
int j = new int();
}
}
Поскольку для каждого типа значений неявно определяется не содержащий параметров открытый конструктор экземпляров, в типе структуры не допускается явное объявление не содержащего параметров конструктора. Тем не менее, в типе структуры допускается объявление параметризованных конструкторов экземпляров (§11.3.8).
Типы структуры
Тип структуры представляет собой тип значений, в котором допускается объявление констант, полей, методов, свойств, индексаторов, операторов, конструкторов экземпляров, статических конструкторов и вложенных типов. Дополнительные сведения об объявлении типов структуры см. в разделе §11.1.
Простые типы
В C# представлен набор предопределенных типов структуры, называемых простыми типами. Простые типы обозначаются зарезервированными словами. Эти слова представляют собой псевдонимы предопределенных типов структуры в пространстве имен System (см. таблицу ниже).
Зарезервированное слово | Тип с псевдонимом |
sbyte | System.SByte |
byte | System.Byte |
short | System.Int16 |
ushort | System.UInt16 |
int | System.Int32 |
uint | System.UInt32 |
long | System.Int64 |
ulong | System.UInt64 |
char | System.Char |
float | System.Single |
double | System.Double |
bool | System.Boolean |
decimal | System.Decimal |
Поскольку простой тип представляет собой псевдоним типа структуры, каждый простой тип содержит члены. Например, int содержит члены, объявленные в System.Int32, а также члены, унаследованные от System.Object. При этом допускаются следующие операторы:
int i = int.MaxValue; // System.Int32.MaxValue constant
string s = i.ToString(); // System.Int32.ToString() instance method
string t = 123.ToString(); // System.Int32.ToString() instance method
Различие между простыми типами и типами структуры заключается в том, что в простых типах допускаются некоторые дополнительные операции:
· Для большинства простых типов допускается создание значений путем записи литералов (§2.4.4). Например, 123 представляет собой литерал типа int, тогда как 'a' — литерал типа char. В C# не предусматривается использование литералов для типов структуры. Создание значений не по умолчанию для других типов структуры практически во всех случаях осуществляется с использованием конструкторов экземпляров соответствующих типов.
· Значение выражения, все операнды которого являются константами простого типа, может быть вычислено на этапе компиляции. Такие выражения называются константными выражениями (§7.19). Выражения, которые содержат операторы, определяемые другими типами структуры, не являются константными.
· Для объявления констант простых типов используется объявление const (§10.4). Константы других типов структуры не поддерживаются, однако аналогичное действие имеют поля static readonly.
· При вычислении операторов преобразования, определенных другими типами структуры, могут использоваться преобразования простых типов. Однако пользовательские операторы преобразования никогда не используются при вычислении другого пользовательского оператора (§6.4.3).
Целочисленные типы
В C# поддерживается девять целых типов: sbyte, byte, short, ushort, int, uint, long, ulong и char. Целые типы имеют следующие размеры и диапазоны значений:
· Тип sbyte представляет 8-разрядные целые числа со знаком в диапазоне от –128 до 127.
· Тип byte представляет 8-разрядные целые числа без знака в диапазоне от 0 до 255.
· Тип short представляет 16-разрядные целые числа со знаком в диапазоне от –32768 до 32767.
· Тип ushort представляет 16-разрядные целые числа без знака в диапазоне от 0 до 65535.
· Тип int представляет 32-разрядные целые числа со знаком в диапазоне от –2147483648 до 2147483647.
· Тип uint представляет 32-разрядные целые числа без знака в диапазоне от 0 до 4294967295.
· Тип long представляет 64-разрядные целые числа со знаком в диапазоне от –9223372036854775808 до 9223372036854775807.
· Тип ulong представляет 64-разрядные целые числа без знака в диапазоне от 0 до 18446744073709551615.
· Тип char представляет 16-разрядные целые числа без знака в диапазоне от 0 до 65535. Набор возможных значений для типа char соответствует набору символов Юникода. Несмотря на то что представления типов char и ushort совпадают, наборы допустимых операций для каждого типа различаются.
Унарные и бинарные операторы целого типа всегда работают с точностью, соответствующей 32- или 64-разрядным числам со знаком или без знака:
· Для унарных операторов + и ~ операнд преобразуется к типу T, где T — первый тип из набора int, uint, long и ulong, с помощью которого могут быть полностью представлены все возможные значения операнда. Операция выполняется с использованием точности, соответствующей типу T. Результат операции имеет тип T.
· Для унарного оператора – операнд преобразуется к типу T, где T — первый тип из набора int и long, с помощью которого могут быть полностью представлены все возможные значения операнда. Операция выполняется с использованием точности, соответствующей типу T. Результат операции имеет тип T. Не допускается применение унарного оператора – к операндам типа ulong.
· Для бинарных операторов +, –, *, /, %, &, ^, |, ==, !=, >, <, >= и <= операнды преобразуются к типу T, где T — первый тип из набора int, uint, long и ulong, с помощью которого могут быть полностью представлены все возможные значения обоих операндов. Операция выполняется с использованием точности, соответствующей типу T. Результат операции имеет тип T (или bool для операторов отношения). Для бинарных операторов не допускается использование двух переменных различных типов (например long и ulong).
· Для бинарных операторов << и >> левый операнд преобразуется к типу T, где T — первый тип из набора int, uint, long и ulong, с помощью которого могут быть полностью представлены все возможные значения операнда. Операция выполняется с использованием точности, соответствующей типу T. Результат операции имеет тип T.
Тип char классифицируется как целый тип, однако имеет два отличия от других целых типов:
· Не поддерживается неявное преобразование из других типов к типу char. В частности, несмотря на то что диапазоны значений для типов sbyte, byte и ushort могут быть полностью представлены с помощью типа char, неявное преобразование из типа sbyte, byte или ushort к типу char не существует.
· Константы типа char должны записываться в виде символьных или целочисленных литералов в сочетании с приведением к типу char. Например, (char)10 совпадает с '\x000A'.
Для управления проверкой переполнения при выполнении целочисленных арифметических операций и преобразований используются операторы checked и unchecked (§7.6.12). В контексте checked при переполнении возникает ошибка времени компиляции или порождается исключение System.OverflowException. В контексте unchecked переполнение игнорируется, а все старшие биты, не соответствующие размеру конечного типа, удаляются.
Типы с плавающей запятой
В C# поддерживается два типа с плавающей запятой: float и double. Типы float и double представляются в 32-разрядном (одинарная точность) или 64-разрядном (двойная точность) формате IEEE 754 и поддерживают следующие наборы значений:
· Положительный и отрицательный нуль. В большинстве случаев поведение значений положительного и отрицательного нуля совпадает. Различие между ними используется лишь в некоторых операторах (§7.8.2).
· Положительная и отрицательная бесконечность. Бесконечность может получиться, например, в результате деления ненулевого значения на нуль. К примеру, в результате операции 1.0 / 0.0 получается положительная бесконечность, а в результате операции –1.0 / 0.0 — отрицательная.
· Нечисловые значения зачастую сокращаются как NaN. Значения NaN получаются в результате выполнения недопустимых операций с плавающей запятой, например при делении нуля на нуль.
· Конечный набор ненулевых значений вида s × m × 2e, где s равно 1 или −1, а значения m и e зависят от конкретного типа с плавающей запятой, выглядит для типа float как 0 < m < 224 и −149 ≤ e ≤ 104, для типа double как 0 < m < 253 и −1075 ≤ e ≤ 970. Допустимыми ненулевыми значениями считаются денормализованные числа с плавающей запятой.
Тип float представляет значения в диапазоне от 1,5 x 10−45 до 3,4 x 1038 (приблизительно) с точностью до 7 знаков.
Тип double представляет значения в диапазоне от 5,0 x 10−324 до 1,7 x 10308 (приблизительно) с точностью до 15–16 знаков.
Если один из операндов бинарного оператора имеет тип с плавающей запятой, второй операнд должен иметь целый тип или тип с плавающей запятой. В этом случае вычисление операции выполняется следующим образом:
· Если один из операторов имеет целый тип, он преобразуется к типу с плавающей запятой, соответствующему типу второго операнда.
· В этом случае, если один из операндов имеет тип double, второй операнд преобразуется к типу double. Операция выполняется с применением точности и диапазона, соответствующих типу double. Результат операции имеет тип double (или bool для операторов отношения).
· В противном случае операция выполняется с применением точности и диапазона, соответствующих типу float. Результат операции имеет тип float (или bool для операторов отношения).
При использовании операторов с плавающей запятой, включая операторы присваивания, никогда не порождаются исключения. Вместо этого в исключительных ситуациях в результате выполнения операции с плавающей запятой получается нуль, бесконечность или нечисловое значение (NaN), как описано ниже:
· Если результат выполнения операции с плавающей запятой слишком мал для конечного формата, результату присваивается значение положительного или отрицательного нуля.
· Если результат выполнения операции с плавающей запятой слишком велик для конечного формата, результату присваивается значение положительной или отрицательной бесконечности.
· Если при выполнении операции с плавающей запятой получается недопустимый результат, операция возвращает результат NaN.
· Если один или оба операнда операции с плавающей запятой имеют значение NaN, операция возвращает результат NaN.
Операции с плавающей запятой могут выполняться с точностью, превышающей точность типа результата операции. Например, в некоторых аппаратных архитектурах поддерживаются типы с плавающей запятой «extended» или «long double», обладающие расширенными диапазоном и точностью по сравнению с типом double. В таких случаях все операции с плавающей запятой неявно выполняются с использованием типа с более высокой точностью. В таких аппаратных архитектурах операции с плавающей запятой выполняются с меньшей точностью только в целях повышения производительности (в случае ее значительного снижения). Чтобы не допустить одновременного снижения производительности и точности при реализации, в C# возможно применение типа с более высокой точностью для выполнения всех операций с плавающей запятой. Применение таких типов не дает какого-либо измеримого эффекта, за исключением получения более точного результата. Однако в выражениях вида x * y / z, в которых в результате умножения получается значение, выходящее за рамки диапазона типа double, но при последующем делении может быть получен временный результат, принадлежащий диапазону double, вычисление выражения с более высокой точностью позволяет получить определенный результат вместо бесконечности.
Тип decimal
Тип decimal представляет собой 128-битовый тип данных, ориентированный на применение в финансовых и денежных вычислениях. Тип decimal представляет значения в диапазоне от 1,0 x 10−28 до 7,9 x 1028 (приблизительно) с 28–29 значащими цифрами.
Тип decimal определяет конечный набор значений вида (–1)s x c x10-e, где параметр s может принимать значения 0 или 1, коэффициент c принадлежит диапазону 0 ≤ c < 296, а значение степени e принадлежит диапазону 0 ≤ e ≤ 28. Тип decimal не поддерживает нули и бесконечности со знаками, а также нечисловые значения. Тип decimal представляется в виде 96-разрядного целого числа, умноженного на определенную степень десяти. Значения типа decimal с абсолютной величиной менее 1.0m имеют точность не более 28 десятичных разрядов. Значения типа decimal с абсолютной величиной не менее 1.0m имеют точность 28 или 29 разрядов. В отличие от типов данных float и double, с помощью типа decimal возможно точное представление десятичных дробей, например 0,1. В представлениях float и double такие числа зачастую представляют собой бесконечные дроби, что увеличивает вероятность возникновения ошибок округления.
Если один из операндов бинарного оператора имеет тип decimal, второй операнд должен иметь целочисленный тип или тип decimal. Перед выполнением операции целочисленный операнд (при его наличии) преобразуется к типу decimal.
В результате выполнения операции получается результат типа decimal. Результат определяется путем вычисления точного результата (с сохранением масштаба, определенного для каждого оператора) и последующего округления в соответствии с разрядностью представления. Результат округляется до ближайшего допустимого значения. Если результат одинаково близок к двум допустимым значениям, округление выполняется до значения, в самом младшем разряде которого содержится четное число (так называемое «банковское округление»). Нулевой результат всегда имеет знак 0 и масштаб 0.
Если в результате выполнения арифметической операции с использованием типа decimal получается значение с абсолютной величиной меньшей или равной 5 ×10-29, операция возвращает нуль. Если в результате выполнения арифметической операции с использованием типа decimal получается значение, превышающее максимально допустимое для типа decimal, возникает исключение System.OverflowException.
Тип decimal обеспечивает более высокую точность, однако обладает меньшим диапазоном по сравнению с типами с плавающей запятой. Таким образом, при преобразовании значений, имеющих тип с плавающей запятой, в значение типа decimal возможно возникновение исключений переполнения. При преобразовании из типа decimal в типы с плавающей запятой возможно снижение точности результата. В связи с этим не существует неявного преобразования между типами с плавающей запятой и типом decimal. Чтобы использовать операнды с плавающей запятой и операнды типа decimal в одном выражении, необходимо выполнить явное приведение их значений.
Тип bool
Тип bool представляет логические величины. Тип bool поддерживает два возможных значения: true и false.
Стандартных преобразований между типом bool и другими типами не существует. В частности, тип bool содержит конечное число значений и является отдельным от целых типов. Не допускается использование значений типа bool вместо целочисленных значений и наоборот.
В языках C и C++ нулевые целочисленные значения или значения с плавающей запятой, а также пустые указатели могут быть преобразованы в логическое значение false. Ненулевые целочисленные значения или значения с плавающей запятой и непустые указатели могут быть преобразованы в логическое значение true. В C# такие преобразования выполняются посредством явного сравнения целочисленного значения или значения с плавающей запятой с нулем, а также посредством явного сравнения ссылки на объект со значением null.
Перечисляемые типы
Перечисляемые типы содержат конечное число значений, представляющих собой именованные константы. Для каждого перечисляемого типа существует базовый тип, в качестве которого должен используется один из следующих типов: byte, sbyte, short, ushort, int, uint, long и ulong. Набор значений перечисляемого типа соответствует набору значений базового типа. Возможные значения перечисляемого типа не ограничиваются значениями именованных констант. Определение перечисляемых типов выполняется посредством объявлений перечисления (§14.1).
Обнуляемые типы
Обнуляемые типы могут представлять все значения базового типа и дополнительное пустое значение. Обнуляемый тип обозначается как T?, где T — базовый тип. Данный синтаксис представляет собой сокращенное обозначение типа System.Nullable<T>. Эти синтаксические формы являются взаимозаменяемыми.
Напротив, необнуляемый тип значений представляет собой тип значений, отличный от System.Nullable<T> или его сокращенной формы T? (для любого типа T)), плюс любой параметр типа, который должен иметь необнуляемый тип значений (то есть любой параметр типа с ограничением struct). Тип System.Nullable<T> определяет ограничение типа значений для типа T ( (§10.1.5). Это означает, что в качестве базового для обнуляемого типа может использоваться любой необнуляемый тип значений. Обнуляемые или ссылочные типы не могут использоваться в качестве базовых для обнуляемого типа. Например, типы int?? и string? не являются допустимыми.
Экземпляр обнуляемого типа T? обладает двумя открытыми свойствами, доступными только для чтения:
· Свойство HasValue типа bool.
· Свойство Value типа T.
Экземпляр, свойство HasValue которого имеет значение true, считается непустым. Свойство Value непустого экземпляра содержит возвращаемое значение.
Экземпляр, свойство HasValue которого имеет значение false, считается пустым. Пустой экземпляр имеет неопределенное значение. Попытка чтения свойства Value пустого экземпляра приводит к исключению System.InvalidOperationException. Процесс доступа к свойству Value обнуляемого экземпляра называется развертыванием.
Помимо конструктора по умолчанию, для каждого обнуляемого типа T? предусмотрен открытый конструктор, принимающий один аргумент типа T. При вызове конструктора со значением x типа T в виде
new T?(x)
создается непустой экземпляр типа T?, свойству Value которого присваивается значение x. Процесс создания непустого экземпляра обнуляемого типа с использованием заданного значения называется свертыванием.
Доступны неявные преобразования литерала null к типу T? (§6.1.5) и типа T к T? (§6.1.4).
Ссылочные типы
Предусмотрены следующие виды ссылочных типов: тип класса, тип интерфейса, тип массива и тип делегата.
reference-type:
class-type
interface-type
array-type
delegate-type
class-type:
type-name
object
dynamic
string
interface-type:
type-name
array-type:
non-array-type rank-specifiers
non-array-type:
type
rank-specifiers:
rank-specifier
rank-specifiers rank-specifier
rank-specifier:
[ dim-separatorsopt ]
dim-separators:
,
dim-separators ,
delegate-type:
type-name
Значение ссылочного типа представляет собой ссылку на экземпляр типа (объект). Особое значение null совместимо со всеми ссылочными типами и обозначает отсутствие экземпляра.
Типы классов
Тип класса определяет структуру данных, которая содержит данные-члены (константы и поля), функции-члены (методы, свойства, события, индексаторы, операторы, конструкторы экземпляров, деструкторы и статические конструкторы), а также вложенные типы. Типы классов поддерживают механизм наследования, который позволяет создавать производные классы, расширяющие функциональные возможности базового класса. Экземпляры типов классов создаются с помощью выражений создания объекта (§7.6.10.1).
Дополнительные сведения о типах классов см. в разделе §10.
В языке C# некоторые предопределенные типы классов имеют особое значение (см. таблицу ниже).
Тип класса | Описание |
System.Object | Первичный базовый класс для всех типов. См. §4.2.2. |
System.String | Строковый тип языка C#. См. §4.2.4. |
System.ValueType | Базовый класс для всех типов значений. См. §4.1.1. |
System.Enum | Базовый класс для всех перечисляемых типов. См. §14. |
System.Array | Базовый класс для всех типов массивов. См. §12. |
System.Delegate | Базовый класс для всех типов делегатов. См. §15. |
System.Exception | Базовый класс для всех типов исключений. См. §16. |
Тип объекта
Тип класса object является первичным базовым классом для всех типов. В C# все типы прямо или косвенно наследуются от типа класса object.
Ключевое слово object — это всего лишь псевдоним предопределенного класса System.Object.
Динамический тип
Тип dynamic, как и тип object, может ссылаться на любой объект. Если операторы применяются к выражениям типа dynamic, их разрешение откладывается на время выполнения программы. Таким образом, если оператор не может быть легально применен к ссылочному объекту, во время компиляции ошибок не возникает. Вместо этого вызывается исключение при сбое разрешения оператора во время выполнения программы.
Более подробно динамический тип описывается в §4.7, а динамическая привязка в §7.2.2.
Строковый тип
Тип string представляет собой запечатанный тип класса, наследуемый непосредственно от класса object. Экземпляры класса string представляют собой строки, состоящие из символов Юникода.
Значения типа string могут быть записаны в виде строковых литералов (§2.4.4.5).
Ключевое слово string представляет собой псевдоним предопределенного класса System.String.
Типы интерфейсов
В интерфейсе определяется контракт. Класс или структура, в которых реализуется этот интерфейс, должны соблюдать условия данного контракта. Интерфейс может наследовать от нескольких базовых интерфейсов, а в классе или структуре может быть реализовано несколько интерфейсов.
Дополнительные сведения о типах интерфейса см. в разделе §13.
Типы массивов
Массив представляет собой структуру данных, содержащую нуль или более переменных, доступ к которым осуществляется с помощью вычисляемых индексов. Все переменные, содержащиеся в массиве, которые также называются элементами массива, имеют одинаковый тип, который называется типом элементов массива.
Дополнительные сведения о типах массивов см. в разделе §12.
Типы делегатов
Делегат представляет собой структуру данных, содержащую ссылки на один или несколько методов. Для методов экземпляров делегат также содержит ссылки на соответствующие экземпляры объектов.
Ближайшим аналогом делегата в C и C++ является указатель на функцию. Различие между ними заключается в том, что указатель на функцию может содержать ссылки только на статические функции, тогда как делегат может содержать ссылки как на статические методы, так и на методы экземпляров. В последнем случае в делегате хранится не только ссылка на точку входа метода, но и ссылка на экземпляр объекта, для которого осуществляется вызов метода.
Дополнительные сведения о типах делегатов см. в разделе §15.
Упаковка и распаковка
Понятие упаковки и распаковки является ключевым в системе типов C#. С помощью данных операций осуществляется связь между типами значений и ссылочными типами за счет возможности преобразования любого значения типа значений к типу object и обратно. С помощью упаковки и распаковки обеспечивается унифицированное представление системы типов, в котором значение любого типа может обрабатываться как объект.
4.3.1 Преобразования упаковки
Преобразование упаковки обеспечивает неявное преобразование типа значений в ссылочный тип. Предусмотрены следующие преобразования упаковки:
· из любого типа значений к типу object;
· из любого типа значений к типу System.ValueType;
· из любого необнуляемого типа значений к любому типу интерфейса, реализованному с помощью типа значений;
· из любого обнуляемого типа к любому типу интерфейса, реализованному с помощью базового типа для обнуляемого типа;
· из любого перечисляемого типа к типу System.Enum;
· из любого обнуляемого типа, базовым для которого является перечисляемый тип, к типу System.Enum.
Обратите внимание, что если неявное преобразование из параметра типа во время выполнения включает в себя преобразование из типа значений к ссылочному типу, оно будет выполнено как преобразование упаковки (§6.1.10).
Упаковка значения включает в себя выделение экземпляра объекта и копирование значения необнуляемого типа значений в указанный экземпляр.
Если выполняется упаковка значения null (свойство HasValue имеет значение false) обнуляемого типа, то при этом создается пустая ссылка. В противном случае возвращается результат развертывания и упаковки базового значения.
Чтобы более наглядно представить процесс упаковки значения необнуляемого типа значений, предположим существование универсального класса упаковки, поведение которого могло бы определяться следующим объявлением:
sealed class Box<T>: System.ValueType
{
T value;
public Box(T t) {
value = t;
}
}
Упаковка значения v типа T будет включать в себя выполнение выражения new Box<T>(v) с последующи<