Различия между классом и структурой

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

· Структуры являются типами значения (§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.

Наши рекомендации