← Назад к вопросам

В каких случаях использовать значимый тип данных?

2.0 Middle🔥 141 комментариев
#Основы C# и .NET#Память и Garbage Collector

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

В каких случаях использовать значимый тип данных в C#

Определение

Значимый тип (Value Type) — это тип данных, который хранит данные прямо в переменной (в стеке), а не ссылку на объект в куче. Это включает простые типы (int, bool, double) и пользовательские структуры (struct).

Контраст: ссылочные типы (class) хранят ссылку на объект в куче.

Основные значимые типы

// Встроенные значимые типы
int age = 30;                    // целые числа
double price = 19.99;            // числа с плавающей точкой
bool isActive = true;            // булевы значения
DateTime date = DateTime.Now;    // даты/время
decimal money = 99.99m;          // денежные суммы (точность!)

// Пользовательские значимые типы
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

var location = new Point { X = 10, Y = 20 };

Когда использовать значимые типы (struct)

1. Маленькие, неизменяемые данные

// ✅ ХОРОШО: небольшая структура точки
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// ✅ ХОРОШО: идентификатор (UUID)
public struct UserId
{
    public Guid Value { get; set; }
}

// ❌ НЕПРАВИЛЬНО: слишком большой struct
public struct HugeData
{
    public byte[] Data { get; set; }      // 1 МБ данных
    public List<int> Items { get; set; }  // Ещё данные
}

2. Когда нужна производительность

Значимые типы хранятся в стеке, что быстрее чем кучa:

// ✅ ВЫСОКАЯ ПРОИЗВОДИТЕЛЬНОСТЬ: работаем в стеке
Vector3 position = new Vector3(1, 2, 3);
Vector3 velocity = new Vector3(0.5f, 0.5f, 0);

for (int i = 0; i < 1_000_000; i++)
{
    position = position + velocity;  // Быстро! В стеке
}

// ❌ НИЗКАЯ ПРОИЗВОДИТЕЛЬНОСТЬ: работаем в куче + GC
class Vector3Class
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

// Каждая операция создаёт объект в куче, потом GC его чистит

3. Когда важна копия, а не ссылка

// ✅ ПРАВИЛЬНО: каждая переменная — своя копия
public struct Color
{
    public byte R { get; set; }
    public byte G { get; set; }
    public byte B { get; set; }
}

var color1 = new Color { R = 255, G = 0, B = 0 };  // Красный
var color2 = color1;

color1.R = 0;  // Меняем color1

Console.WriteLine(color2.R);  // 255 — color2 не изменился (своя копия)

// ❌ НЕПРАВИЛЬНО: класс — одна ссылка
public class ColorClass
{
    public byte R { get; set; }
    public byte G { get; set; }
    public byte B { get; set; }
}

var color1 = new ColorClass { R = 255, G = 0, B = 0 };
var color2 = color1;

color1.R = 0;  // Меняем color1

Console.WriteLine(color2.R);  // 0 — color2 изменился! Один объект

Практические примеры

Пример 1: Денежная сумма (decimal)

// ✅ ИСПОЛЬЗУЙ decimal ДЛЯ ДЕНЕГ
decimal price = 19.99m;
decimal tax = 2.00m;
decimal total = price + tax;  // Точный результат 21.99

Console.WriteLine(total);  // 21.99 (точное значение)

// ❌ НЕ ИСПОЛЬЗУЙ double ДЛЯ ДЕНЕГ
double priceD = 19.99;
double taxD = 2.00;
double totalD = priceD + taxD;

Console.WriteLine(totalD);  // 21.99 (может быть неточно из-за floating point)

Пример 2: Идентификатор пользователя (struct или Guid)

// ✅ ТИПИЗИРОВАННЫЙ ИДЕНТИФИКАТОР (сильная типизация)
public struct UserId
{
    public Guid Value { get; set; }
    
    public UserId(Guid value) => Value = value;
    
    public static UserId New() => new UserId(Guid.NewGuid());
}

var userId = UserId.New();
var productId = UserId.New();  // Случайно не перепутаешь типы

// Метод принимает ТОЛЬКО UserId
void DeleteUser(UserId id)
{
    Console.WriteLine($"Deleted user: {id.Value}");
}

DeleteUser(userId);      // OK
// DeleteUser(productId);  // Ошибка компиляции! Типы не совпадают

Пример 3: Вектор для игры/3D

// ✅ STRUCT: быстро, независимо, для математики
public struct Vector3
{
    public float X { get; set; }
    public float Y { get; set; }
    public float Z { get; set; }
    
    public Vector3(float x, float y, float z)
    {
        X = x;
        Y = y;
        Z = z;
    }
    
    public float Length => MathF.Sqrt(X * X + Y * Y + Z * Z);
    
    public static Vector3 operator +(Vector3 a, Vector3 b)
        => new Vector3(a.X + b.X, a.Y + b.Y, a.Z + b.Z);
}

var v1 = new Vector3(1, 0, 0);
var v2 = new Vector3(0, 1, 0);
var v3 = v1 + v2;  // (1, 1, 0)

Console.WriteLine(v3.Length);  // ~1.41

Пример 4: Координата (struct)

// ✅ STRUCT: маленькие, неизменяемые данные
public readonly struct Point
{
    public int X { get; }
    public int Y { get; }
    
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
    
    public double DistanceTo(Point other)
    {
        var dx = X - other.X;
        var dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
}

var p1 = new Point(0, 0);
var p2 = new Point(3, 4);

Console.WriteLine(p1.DistanceTo(p2));  // 5

Когда НЕ использовать значимые типы

1. Большие структуры (> 16 байт)

// ❌ НЕПРАВИЛЬНО: копируется много данных
public struct LargeData
{
    public int[] Array { get; set; }    // 1000+ элементов
    public string Text { get; set; }    // 1000+ символов
    public List<int> List { get; set; } // Много элементов
}

// Каждая копия = копирование всех данных!
var data1 = new LargeData();
var data2 = data1;  // ДОРОГО! Копируется килобайты

2. Когда нужна полиморфия

// ❌ НЕПРАВИЛЬНО: struct не поддерживает полиморфию
public struct Animal
{
    public virtual void Speak() { }  // ОШИБКА: struct не может быть virtual
}

// ✅ ПРАВИЛЬНО: используй class для наследования
public class Animal
{
    public virtual void Speak() => Console.WriteLine("Some sound");
}

public class Dog : Animal
{
    public override void Speak() => Console.WriteLine("Woof!");
}

3. Когда нужна ссылка на ОДИН объект

// ❌ НЕПРАВИЛЬНО: struct создаёт копии
public struct Counter
{
    public int Value { get; set; }
}

var counter1 = new Counter { Value = 0 };
var counter2 = counter1;

counter1.Value = 5;
counter2.Value = 10;

Console.WriteLine(counter1.Value);  // 5
Console.WriteLine(counter2.Value);  // 10 — ДВЕ РАЗНЫЕ КОПИИ!

// ✅ ПРАВИЛЬНО: используй class
public class Counter
{
    public int Value { get; set; }
}

var counter1 = new Counter { Value = 0 };
var counter2 = counter1;

counter1.Value = 5;

Console.WriteLine(counter2.Value);  // 5 — один объект

Таблица: Struct vs Class

ХарактеристикаStructClass
Где хранитсяСтекКуча
КопированиеКопируется значениеКопируется ссылка
ПроизводительностьБыстро (стек)Медленнее (куча + GC)
РазмерДо ~16 байтЛюбой
nullНетДа
НаследованиеНетДа
Когда использоватьМаленькие, неизменяемыеБольшие, изменяемые, иерархия

Лучшие практики

1. Сделай struct readonly (неизменяемым)

// ✅ ПРАВИЛЬНО: readonly struct не может быть изменён
public readonly struct Point
{
    public int X { get; }
    public int Y { get; }
    
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

// var point = new Point(1, 2);
// point.X = 5;  // ОШИБКА: readonly struct

// ❌ НЕПРАВИЛЬНО: mutable struct (можно случайно изменить)
public struct BadPoint
{
    public int X { get; set; }
    public int Y { get; set; }
}

2. Реализуй GetHashCode и Equals для структур

public readonly struct UserId : IEquatable<UserId>
{
    public Guid Value { get; }
    
    public UserId(Guid value) => Value = value;
    
    public override bool Equals(object obj) 
        => obj is UserId other && Value == other.Value;
    
    public bool Equals(UserId other) 
        => Value == other.Value;
    
    public override int GetHashCode() 
        => Value.GetHashCode();
    
    public static bool operator ==(UserId left, UserId right) 
        => left.Equals(right);
    
    public static bool operator !=(UserId left, UserId right) 
        => !left.Equals(right);
}

3. Используй новые типы вместо примитивов

// ❌ ПЛОХО: путаешь int'ы
void UpdatePrice(int id, int price)
{
    // Случайно можешь передать параметры в неправильном порядке
}

UpdatePrice(price: 100, id: 5);  // Упс! Перепутал

// ✅ ХОРОШО: типизированные ID и Price
public readonly struct ProductId { public int Value { get; } }
public readonly struct Price { public decimal Value { get; } }

void UpdatePrice(ProductId id, Price price)
{
    // Невозможно перепутать типы
}

Краткие выводы

  1. Используй struct для маленьких, неизменяемых данных (< 16 байт)
  2. Struct = копия значения — каждая переменная независима
  3. Struct хранится в стеке — быстрее, чем класс в куче
  4. Сделай struct readonly — избегай неожиданных изменений
  5. НЕ наследуй от struct — используй class для полиморфии
  6. Используй struct для ID и маленьких значений — сильная типизация

Золотое правило: Struct = маленький, неизменяемый, независимый. Класс = большой, изменяемый, с ссылками.