Предполагается, что читатель уже знаком с языком программирования C# и здесь будет идти углубление полученных знаний слой за слоем.
Типы данных
Один из главных вопросов на любом интервью на любой уровень позиции C# так, или иначе связан с типами данных. У джуниоров спросят базы, от мидл-разработчиков уже будут спрашивать понимания, а от сениор-разработчиков потребуют не только глубокие знания предметной области, но также умения применения их на практике. Действительно ли это важно? Давайте разбираться..
Вначале вспомним основы — что является значимым, а что ссылочным типом данных. Это просто.
Значимый | Ссылочный |
Целочисленные типы: byte, sbyte, short, ushort, int, uint, long, ulong | object |
С плавающей точкой: float, double | string |
С фиксированной точкой: decimal | class |
bool | interface |
char | delegate |
enum | |
struct | |
record struct |
В чем разница? Когда мы передаем куда-то значимый тип — мы копируем его значение, а когда ссылочный тип — мы копируем ссылку на его значение (которое уже хранится в другом месте).
В C# есть два типа памяти — стек (stack) и куча (heap). Разные типы данных хранятся в разных местах. Также можно перекладывать данные (boxing / unboxing), но об этом позже.
Куча делится ещё на несколько видов — Small Object Heap (SOH), Large Object Heap (LOH) и Pinned Object Heap (POH) и об этом тоже позже.
Стек (stack) как структура данных
Вначале вспомним — что такое стек с точки зрения структуры данных.
Работает по принципу FIFO, или LIFO — First In First Out, Last In First Out.
Стек (stack) как область хранения данных в CLR
Стек, как область памяти для хранения данных в CLR так названа не случайно — это действительно стек. У каждого потока (thread) свой стек. Обычно размер стека — 1мб. Есть вариации, можно даже указать свой, но тем не менее — после создания — он статичен и размера больше не меняет.
Рассмотрим пример
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var coordinate = new Coordinate(); coordinate.x = 1; coordinate.y = 2; var point = new Point(); point.Name = "Point name"; point.Coordinates = coordinate; struct Coordinate { public int x; public int y; } class Point { public string Name; public Coordinate Coordinates; } |
Структура Coordinate является value-type (struct), поэтому хранится в стеке.
Класс Point является ref-type, поэтому хранится в куче.
Исходя из этого — экземпляр структуры координат (как и его поля) в экземпляре класса Point — являются копиями того, что был изначально. Поэтому меняя любой из них — мы меняем только конкретный экземпляр класса.
Возможно правильнее воспринимать value и ref типы с точки зрения не их расположения хранения, а с точки зрения копирования. Когда мы копируем value-type в новую переменную — мы копируем значение. А когда копируем ref-type переменную — мы копируем ссылку на объект в куче. Даже скопировав ссылку на объект в куче — это будет другая ссылка уже, но вести она будет на тот же объект в куче.
Вернемся к примеру выше. Может возникнуть вопрос — каким образом хранятся данные внутри экземпляра класса Point — тут всё просто. При создании экземпляра класса CLR выделяет в куче блок памяти, размером под данный класс. Мы его знаем ещё на момент компиляции.
Что может быть внутри класса? Свойства и поля. Свойства — это, по своей сути, синтаксический сахар над полями в виде getter’ов и setter’ов. Его легко можно создать самому, создав два публичных метода для работы над приватным полем.
В полях могут хранится ссылки на ссылочные типы данных (ссылка имеет константный размер). И могут хранится value-type (int, long, byte), которые также имеют свой константный размер. То есть CLR четко знает сколько байт необходимо для создания экземпляра класса в куче — это общее количество памяти для всех полей и немного байт на служебную информацию. Ссылочные типы внутри экземпляра класса ведут уже на другие области памяти в куче.
Более того — структурой данных внутри класса, либо структуры, можно управлять. Например вот код:
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 34 35 36 37 |
public static void Main(string[] args) { var t = new Test(); t.X = 11; t.Y = 22; t.Z = 33; Console.WriteLine(t.ShadowX); Console.WriteLine(t.ShadowY); Console.WriteLine(t.ShadowZ); } [StructLayout(LayoutKind.Explicit)] public class Test { [FieldOffset(0)] public int X; [FieldOffset(4)] public int Y; [FieldOffset(8)] public int Z; [FieldOffset(0)] public int ShadowX; [FieldOffset(4)] public int ShadowY; [FieldOffset(8)] public int ShadowZ; } |
Как думаете — что будет внутри ShadowX, ShadowY и ShadowZ? Всё тоже самое, что и у X, Y и Z. Так как отступы у этих полей будут смещены.