В C# все типы объявленные с ключевым словом struct являются значимыми, как class — ссылочными. Разницы в их поведении не наблюдается, поэтому особо важно понимать какой тип используется в программе, так как это может значительно влиять на производительность в программе.
Рассмотрим пример, из которого видна разница между ссылочными и значимыми типами:
//Ссылочный тип, так как объявлено class
class Reference
{
public Int32 x {get; set;} // здесь используется сокращенный метод объявления свойств, подробнее читайте книгу "Библия С#" М. Фленова
}
//Значимый тип, так как объявлено struct
struct Structure
{
public Int32 x {get; set;}
}
static void Main()
{
Reference r1 = new Reference(); //размещается в куче
Structure s1 = new Structure(); //размещается в стеке
r1.x = 5; //разыменование указателя (обращения к значению в памяти, на которое указывает указатель, подробнее операции над указателями)
s1.x = 10; //изменения в стеке
Console.WriteLine(r1.x); // вывод 5
Console.WriteLine(s1.x); //вывод 10
Reference r2 = r1; //копируется только ссылка, указатель
Structure s2 = s2; //помещается в стек и копируются поля
r1.x = 8 // изменяется и r1.x и r2.x
s1.x = 9 // изменяется только s1.x, но не s2.x
Console.WriteLine(r1.x); // вывод 8
Console.WriteLine(r2.x); //вывод8
Console.WriteLine(s1.x); // вывод 9
Console.WriteLine(s2.x); //вывод 10
}
Как видно из примера, можно заметить, что при работе с ссылочными и значимыми типами наблюдается существенная разница. При создании ссылочных типов выделется память в куче и возвращается адрес в памяти, который сохраняется в переменной. Поэтому при инициализации переменной такого же типа и присвоения ей того же значения, то копируется только адрес на область в памяти. Как и происходит в строке «Reference r2 = r1; //копируется только ссылка, указатель». То есть 2 переменные указывают только на одну область памяти.
В тоже время совсем другое происходит при работе со значимыми типами. При создании новой переменной значимого типа и копировании значений, происходит новое выделение в стеке памяти и создание новой независимой переменной. «Structure s2 = s2; //помещается в стек и копируются поля»
Исходя из выше сказанного можно заметить также, что при изменении полей в ссылочном типе происходит изменение значений, на которые ссылаются переменные. То есть в данном примере переменные r1.x и r2.x покажут одинаковое значение, даже если изменить к примеру r1.x, так как обе переменные указывают на одну область памяти. А вот если изменить s1.x, то изменение s2.x не последует, так как s1.x и s2.x это две разные переменные, которые никак не связаны между собой.
Немного отступим от темы, но это имеет прямое отношение к ссылочному типу. Рассмотрим такой оператор как new. С его помощью и только создаются и инициализируются все ссылочные типы. Опишу я оператор new полностью и постараюсь объяснить каждое непонятное слово.
Оператор new действует так:
- Вычисляется число байтов, необходимых всем экземплярным полям типа и всем его базовым типам, включая System.Object (исходя из наследственности, производному классу, типу и т.д. наследуется все методы базового класса). Каждый объект кучи требует дополнительных членов, они называются указатель на объект-тип(type object) и индекс блока синхронизации(sync block index) и служат CLR для управления объектом.
- Выделяется память для объекта, резервируя необходимое для данного типа число байтов в управляемой куче байтов, затем все байты устанавливаются в ноль.
- Инициализируется указатель на объект-тип и индекс блока синхронизации.
- Вызывается конструктор экземпляра типа с параметрами или без. При этом если конструктор не объявлен в коде, компилятор автоматически вызовет конструктор. При этом большинство компиляторов вызывает код конструктора базового класса, в частности конструктор System.Object, но он ничего не делает и просто возвращает управление.
Указатель на объект-тип, этот член позволяет определить тип данного объекта(экземпляра).
Этот указатель использует такой метод как .GetType(), он позволяет определить какой тип у данного объекта. Еще можно использовать выражение typeof, но об этом потом. А индекс блока синхронизации используется для хранения данных при многопоточном выполнении программы. А также используется при вызове .GetHashCode(). Подробнее можно прочитать в документации .NET «Внутреннее устройство .NET Framework — как CLR создает объекты периода выполнения».
При этом у оператора new нет пары — оператора delete. Как я писал в предыдущей статье, CLR сам занимается сборкой мусора в управляемой куче, автоматически находит ненужные объекты и уничтожает их.
Исходя из выше сказанного: проектируя свой собственный тип, проверьте, какой тип лучше всего использовать. Это помогает улучшить эффективность кода. Вот несколько условий, когда можно использовать значимый тип:
- Тип ведет себя подобно примитивному типу. Это означает что он достаточно простой и у него нет членов, способных изменять экземпляры классов. Иногда говорят, что данный тип неизменяемый (immutable). Рекомендуется многие значимые типы помечать спецификатором readonly. Это подобие const, но при этом переменную нужно сразу инициализировать, а при readonly переменную можно объявить без предварительной инициализации. Удобно использовать к примеру в конструкторах, когда объявляем постоянную переменную, а в конструкторе ее инициализируем.
- Типу не нужен любой другой тип, в качестве базового
- Тип не будет иметь производных от него типов
Но при этом нужно учитывать тот факт, что поля переменных, которые являются аргументом и передаются по назначению, то они копируются, что в свою очередь приводит к снижению производительности. Поэтому, метод, возвращающий значимый тип, приводит к копированию полей экземпляра, что снижает эффективность работы программ. Следовательно, нужно еще кое-что учитывать, а именно:
- Размер экземпляров типа мал (примерно 16 байт или меньше)
- Размер экземпляров больше 16 байт, но при этом нет передаваемых аргументов или метод не возвращает значение в конце своего выполнения.
Конечно, у значимых типов есть свои преимущества перед ссылочными типами, но при этом есть и недостатки. Вот некоторые особенности, отличающие ссылочные и значимые типы.
- Объекты значимого типа существуют в двух формах: неупакованной (unboxed) и упакованной (boxed ). Ссылочные типы бывают только в упакованной форме.
- Поскольку в объявлении нового значимого или ссылочного типа нельзя указывать значимый тип в качестве базового класса, создавать в значимом тип новые виртуальные методы нельзя. Методы не могут быть абстрактными и неявно являются изолированными (то есть их нельзя переопределить)
- Переменные ссылочного типа содержат адреса объектов в куче. Когда перемененная ссылочного типа создается, ей по умолчанию присваивается nu1l, то есть в этот момент она не указывает на действительный объект. Попытка задействовать переменкую с таким значением приведет к генерации исключения Nu1lReferenceException. В то же время в переменной значимого типа всегда содержится некое значение соответствующего типа, а при инициализации всем членам этого типа присваивается. Поскольку переменная значимого типа не является указателем, при обращении к значимому типу исключение NullReferenceException возникнуть не может. CLR поддерживает понятие значимого типа особого вида, допускающего присваивание null (nullable types).
- Когда переменной значимого типа присваивается другая перемененная значимого типа, выполняется копирование всех ее полей. Когда переменной ссылочного типа присваивается перемененная ссылочного типа, копируется ТОЛЬКО ее адрес.
- Вследствие сказанного в предыдущем абзаце, несколько переменных ссылочного типа могут ссылаться на один объект в куче, благодаря чему, работая с одной переменной, можно изменить объект, на который ссылается другая переменная. В то же время каждая перемененная значимого типа имеет собственную копию данных объекта, поэтому операции с одной переменной значимого типа не влияют на другую переменную.
- Так как неупакованные значимые типы не размещаются в куче, отведенная для них память освобождается сразу при возвращении управления методом, в котором описан экземпляр этого типа.
Поэтому применяя знания, нужно наиболее полно использовать все ресурсы системы, при этом не теряя производительность и экономно расходовать ресурсы.
В следующей статье рассмотрим упаковку и распаковку значимых типов.
Автор: Priest512