Различия между классом и структурой
Структуры имеют несколько важных отличий от классов:
· Структуры являются типами значения (§11.3.1).
· Все типы структур неявным образом наследуются из класса System.ValueType (§11.3.2).
· При присваивании переменной с типом структуры создается копия присваиваемого значения (§11.3.3).
· Значением по умолчанию для структуры является значение, при котором все поля типов значения устанавливаются в соответствующие значения по умолчанию, а все поля ссылочного типа — в значение null (§11.3.4).
· Операции упаковки и распаковки используются для выполнения преобразования между типом структуры и объектом object (§11.3.5).
· Ключевое слово this в структурах имеет другой смысл (§7.6.7).
· Объявления полей экземпляров для структуры не могут включать инициализаторы переменных (§11.3.7).
· В структуре не разрешается объявлять конструктор экземпляров без параметров (§11.3.8).
· В структуре не может быть объявлен деструктор (§11.3.9).
Семантика значений
Структуры относятся к типам значения (§4.1) и считаются имеющими семантику значения. Наоборот, классы относятся к ссылочным типам (§4.2) и считаются имеющими семантику ссылок.
Переменная с типом структуры непосредственно содержит данные этой структуры, в то время как переменная с типом класса содержит ссылку на данные, которые считаются объектом. Если структура B содержит поле экземпляра типа A и A является типом структуры, зависимость A от B или от типа, созданного на основе B, приведет к возникновению ошибки компиляции. Структура X имеет прямую зависимость от структуры Y в том случае, если X содержит поле экземпляра с типом Y. В соответствии с этим определением полный набор структур, по отношению к которым структура является зависимой, представляет собой транзитивное замыкание связи с типом имеет прямую зависимость от. Пример
struct Node
{
int data;
Node next; // error, Node directly depends on itself
}
является ошибкой, поскольку структура Node содержит поле экземпляра с собственным типом. Другой пример. Код
struct A { B b; }
struct B { C c; }
struct C { A a; }
является ошибкой, поскольку типы A, B и C зависят друг от друга.
В классах две переменные могут ссылаться на один объект, в результате чего действия с одной переменной могут повлиять на объект, на который ссылается другая переменная. В структурах каждая из переменных имеет собственную копию данных (за исключением переменных параметров ref и out) и действия с одной из переменных не могут повлиять на другую переменную. Более того, поскольку структуры не относятся к ссылочным типам, значения с типом структуры не могут быть равны null.
При объявлении
struct Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
выполнение фрагмента кода
Point a = new Point(10, 10);
Point b = a;
a.x = 100;
System.Console.WriteLine(b.x);
возвратит значение 10. При присваивании значения переменной a переменной b создается копия этого значения, поэтому на переменную b не влияет изменение значения переменной a.x. Если бы структура Point была объявлена как класс, выходным значением было бы 100, поскольку переменные a и b ссылались бы на один и тот же объект.
Наследование
Все типы структур неявным образом наследуются от класса System.ValueType, который, в свою очередь, наследуется от класса object. В объявлении структуры может быть указан список реализованных интерфейсов, однако здесь не допускается указание базового класса.
Тип структуры никогда не является абстрактным и всегда неявным образом запечатан. Поэтому в объявлении структуры не допускаются модификаторы abstract и sealed.
Так как для структур не поддерживается наследование, член структуры не может иметь объявленный уровень доступа protected или protected internal.
Функции-члены в структуре не могут иметь модификаторы abstract или virtual, а модификатор override допускается только для переопределения методов, унаследованных от класса System.ValueType.
Присваивание
При присваивании переменной с типом структуры создается копия присваиваемого значения. В отличие от этого при выполнении присваивания в переменную с типом класса копируется ссылка, а не сам объект, на который указывает эта ссылка.
Аналогичным образом при передаче структуры в качестве параметра по значению или возвращения ее в результате выполнения функции-члена создается копия структуры. Структура может передаваться в функцию-член по ссылке с использованием параметра ref или out.
Если целевым объектом операции присваивания является свойство или индексатор структуры, в качестве переменной должно быть указано выражение экземпляра, сопоставленное доступу к индексатору или свойству. Если выражение экземпляра классифицировано как значение, возникнет ошибка компиляции. Более подробно эта тема рассматривается в §7.17.1.
Значения по умолчанию
Как рассматривалось в §5.2, некоторые виды переменных при создании автоматически инициализируются значениями по умолчанию. Для переменных с типом класса или другими ссылочными типами используется значение null. Однако в связи с тем, что структуры имеют тип значения и не могут быть равны null, значение по умолчанию для структуры является значением, полученным путем установки для всех полей с типом значения соответствующих значений по умолчанию, а для всех полей с ссылочным типом — значения null.
Для объявленной выше структуры Point строка
Point[] a = new Point[100];
выполняет инициализацию каждой переменной Point в массиве значением, полученным путем установки для полей x и y нулевых (0) значений.
Значение по умолчанию структуры соответствует значению, возвращаемому конструктором структуры по умолчанию (§4.1.2). Для структуры, в отличие от класса, не допускается объявление конструктора экземпляра без параметров. Вместо этого каждая структура неявно содержит конструктор экземпляра без параметров, который всегда возвращает значение, полученное путем установки для всех полей с типом значения соответствующих значений по умолчанию, а для всех полей с ссылочным типом — значения null.
Структуры должны создаваться таким образом, чтобы состояние инициализации по умолчанию было допустимым состоянием. В этом примере
using System;
struct KeyValuePair
{
string key;
string value;
public KeyValuePair(string key, string value) {
if (key == null || value == null) throw new ArgumentException();
this.key = key;
this.value = value;
}
}
пользовательский конструктор экземпляра обеспечивает защиту от пустых значений только при явном вызове. В случае, когда переменная KeyValuePair инициализируются значением по умолчанию, поля key и value имеют значение NULL, и в структуре следует предусмотреть обработку такого состояния.
Упаковка и распаковка
Значение с типом класса можно преобразовать в тип object или в тип интерфейса, реализуемого этим классом, путем обработки данной ссылки во время компиляции как другого типа. Аналогичным образом значение с типом object или типом интерфейса можно преобразовать обратно в тип класса без изменения ссылки (естественно, в этом случае требуется проверка во время выполнения).
Так как структуры не относятся к ссылочным типам, для типов структуры эти операции реализуются по-другому. При преобразовании значения с типом структуры в тип object или в тип интерфейса, реализуемый этой структурой, происходит операция упаковки. Точно так же при обратном преобразовании значения с типом object или значения с типом интерфейса в тип структуры выполняется операция распаковки. Ключевое отличие от аналогичных операций с типами класса состоит в том, что при упаковке и распаковке выполняется копирование значения структуры в упакованный экземпляр или из такого экземпляра. Таким образом, после выполнения операции упаковки или распаковки изменения, внесенные в распакованную структуру, не отражаются в упакованной структуре.
Если тип структуры переопределяет виртуальный метод, унаследованный от класса System.Object (например, Equals, GetHashCode или ToString), вызов этого виртуального метода через экземпляр типа структуры не приводит к выполнению упаковки. Это правило действует даже в том случае, когда структура используется в качестве параметра типа и вызов происходит в экземпляре с типом параметра типа. Пример:
using System;
struct Counter
{
int value;
public override string ToString() {
value++;
return value.ToString();
}
}
class Program
{
static void Test<T>() where T: new() {
T x = new T();
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
}
static void Main() {
Test<Counter>();
}
}
Результат выполнения примера:
1
2
3
Несмотря на то, что использование метода ToString для выполнения побочных действий является плохим стилем, в этом примере демонстрируется, что при трех вызовах метода x.ToString() упаковка не выполнялась.
Аналогичным образом упаковка не выполняется неявным образом при доступе к члену с ограниченным параметром-типом. Например, интерфейс ICounter содержит метод Increment, который можно использовать для изменения значения. Если метод ICounter используется в качестве ограничения, реализация метода Increment вызывается со ссылкой на переменную, для которой был вызван метод Increment, а не для упакованной копии.
using System;
interface ICounter
{
void Increment();
}
struct Counter: ICounter
{
int value;
public override string ToString() {
return value.ToString();
}
void ICounter.Increment() {
value++;
}
}
class Program
{
static void Test<T>() where T: ICounter, new() {
T x = new T();
Console.WriteLine(x);
x.Increment(); // Modify x
Console.WriteLine(x);
((ICounter)x).Increment(); // Modify boxed copy of x
Console.WriteLine(x);
}
static void Main() {
Test<Counter>();
}
}
При первом вызове метода Increment изменяется значение переменной x. Это не равноценно второму вызову метода Increment, при котором изменяется значение упакованной копии x. Таким образом, в результате выполнения программы будет получен следующий результат:
0
1
1
Дополнительные сведения об упаковке и распаковке см. в §4.3.