Перейти к содержимому
Теория · Значимые и ссылочные типы · урок 2 из 11

Структуры

В прошлом уроке значение копировалось, как ксерокопия, а объект получал ещё одну ссылку, как общий документ. Встроенные значимые типы хранят одно значение: число, символ, true или false. А если значение состоит из нескольких частей, как точка на плане из двух координат, и копироваться должно целиком, как лист на ксероксе?

Для этого в C# есть структура — свой значимый тип, объявленный словом struct. Она похожа на класс: у неё тоже есть поля, и объявляют её после инструкций верхнего уровня.

C#
Point p = new Point();
p.X = 3;
p.Y = 4;
Console.WriteLine(p.X + " " + p.Y); // 3 4

struct Point
{
    public int X;
    public int Y;
}

Давайте разберём пример по строкам:

  1. struct Point объявляет структуру с двумя полями, X и Y.
  2. new Point() создаёт точку, у которой все поля равны нулю. Для структуры new не создаёт объект в куче.
  3. p.X = 3 и p.Y = 4 записывают координаты прямо в переменную p: ссылки на объект здесь нет.
  4. Последняя строка печатает 3 4.

Сами int, double и bool в .NET тоже объявлены как структуры. Поэтому ваша структура копируется так же, как встроенные значимые типы.

Присваивание копирует все поля

Присваивание структуры делает ксерокопию сразу всех её полей. После Point b = a; у b свои X и Y, и запись в b.X не трогает a. С классом та же строка дала бы второе имя одному объекту.

В виджете обе версии идут друг за другом: структура Point и класс Spot с теми же полями. У переменной структуры значение записано прямо в рамке, а у переменной класса стрелка → ведёт к объекту. Нажимайте «Следующий шаг» и читайте подпись: она говорит, общие ли данные у имён.

ВживуюСтруктура и классшаг 1 из 12 · рамок: 1

new Point() создаёт точку с нулями прямо в переменной a.

Подпись к шагу говорит, общие ли данные у имён

Разница видна в двух строках вывода:

  • 1 10: у структур a и b свои поля, и запись в b.X не дошла до a;
  • 10 10: c и d ведут к одному объекту, и запись через d видна через c.

Здесь аналогия с ксерокопией неточна. В жизни можно скопировать только нужную страницу. Структура же копируется целиком: все поля разом, даже если потом вы меняете одно.

struct или class

Этот вопрос часто задают на собеседовании: чем struct отличается от class? Главная часть ответа — копирование. Присваивание структуры копирует все поля, а присваивание класса — только ссылку.

Отсюда и выбор между ними:

  • структура подходит для небольшого значения из нескольких полей, которое естественно копировать: точка, цвет, сумма в рублях;
  • класс подходит для того, что должно быть общим для всех, кто с ним работает: пользователь, заказ, банковский счёт;
  • копия большой структуры обходится дороже копии ссылки, поэтому структуры делают маленькими.

Структура в параметре метода

Метод получает копию аргумента, и для структуры это копия всех полей. Поэтому метод без ref не может сдвинуть вашу точку:

C#
Point start = new Point();
start.X = 3;
Shift(start);
Console.WriteLine(start.X); // 3

void Shift(Point point)
{
    point.X = point.X + 10;
}

Разберём, что происходит:

  1. Shift(start) копирует всю точку в параметр point.
  2. point.X = point.X + 10 меняет копию: у неё X становится 13.
  3. Метод завершается, копия исчезает, и start.X печатает 3.

Чтобы метод изменил саму структуру, её передают с ref, как переменную int.

Частая ошибка

Точку в списке попробовали изменить по индексу:

C#
List<Point> route = [new Point()];
route[0].X = 5; // ✗
TEXT
error CS1612: Cannot modify the return value of 'List<Point>.this[int]' because it is not a variable

Программа не компилируется. route[0] у списка возвращает копию точки, а не саму точку из списка. Запись в поле копии сразу бы пропала, поэтому компилятор её запрещает. this[int] в сообщении — это доступ по индексу в квадратных скобках.

Исправление — прочитать точку в переменную, изменить её и записать обратно:

C#
List<Point> route = [new Point()];
Point first = route[0];
first.X = 5;
route[0] = first;
Console.WriteLine(route[0].X); // 5

У массива такой проблемы нет: points[0].X = 5 меняет точку прямо в массиве, потому что элемент массива — переменная, а не копия.

Главное

  • struct объявляет свой значимый тип: значение хранится прямо в переменной, без ссылки на объект.
  • Присваивание структуры копирует все поля, присваивание класса — только ссылку.
  • Метод получает копию структуры и без ref не меняет переменную вызывающего кода.
  • list[0] возвращает копию структуры, поэтому list[0].X = 5 для List не компилируется.