Перейти к основному содержимому

Типы данных в C#

Как выбирать тип под задачу

Быстрый ориентир:

  • int, long - счётчики, идентификаторы, индексы;
  • decimal - деньги и финансовые расчёты;
  • double - инженерные/научные вычисления;
  • bool - состояния и флаги;
  • string - текст;
  • DateTimeOffset - момент времени с учётом смещения;
  • Guid - глобально уникальный идентификатор (ключ сущности, correlation id);
  • class — сложная модель с поведением и наследованием;
  • struct — компактный значимый тип (координаты, ключ, интервал).

Практические советы по надёжности

  • Для публичных контрактов API заранее решайте, где допустим null.
  • Не смешивайте double и decimal в одной финансовой формуле.
  • Явно документируйте единицы измерения (seconds, ms, bytes) прямо в названии переменной.

Типичные ошибки

  • Использование double для цен.
  • Присваивание "магических чисел" без enum/констант.
  • Объединение слишком разнородных данных в object без нужды.

Смежные статьи

Типы данных в C#

Разработчику Архитектору

Дальше: Работа с типами — строки, коллекции, преобразования · Преобразование типов · Справочник C#


Типы данных

Система типов

C# — статически типизированный язык с проверкой на этапе компиляции; модель сильная (неявные преобразования ограничены правилами языка).

С dynamic и object возможны участки с проверкой в runtime.

Теория — типы данных, типизация; преобразования — Преобразование типов.

Типы данных в C#

Типы данных – это фундамент любого языка программирования.

В C# типы делятся на несколько категорий:

  • простые (примитивные) типы;
  • ссылочные типы;
  • структуры или значимые типы (value types);
  • кортежи;
  • пользовательские типы.

Основная конструкция, влияющая на определение типа, это конечно объявление переменной:

<тип> <имя> = <значение>;

Тип указывает категорию данных, имя предоставляет идентификатор для обращения, значение инициализирует переменную конкретными данными.

<тип> <имя>;


Play ITЗагрузка интерактивного демо…


Простые типы

ТипОписаниеРазмер
intЦелое число4 байта
longДлинное целое8 байт
shortКороткое целое2 байта
byteЧисло от 0 до 2551 байт
float, double, decimalЧисла с плавающей точкойразный
charСимвол Unicode2 байта
stringСтрока символовдинамический
boolЛогическое значение (true / false)

Play ITЗагрузка интерактивного демо…


Ссылочные и значимые типы

Ссылочные и значимые типы отличаются способом хранения данных в памяти и поведением при присваивании, передаче в методы и управлении ресурсами. Примеры для чисел, объектов и модификаторов ref, out, in — в статье Передача параметров в C#.

Значимые типы (Value Types) хранят непосредственно данные, а не ссылку на них. Обычно они размещаются в стеке, но могут быть частью объектов в куче, если входят в состав ссылочного типа.

Примеры:

int age = 30;
bool isStudent = true;
char grade = 'A';
DateTime birthDate = new DateTime(1995, 5, 20);

Разбор:

  • int age = 30; хранит целочисленное значение возраста.
  • bool isStudent = true; задаёт логический флаг состояния (да/нет).
  • char grade = 'A'; демонстрирует одиночный символ Unicode в одинарных кавычках.
  • new DateTime(1995, 5, 20) создаёт значение даты из года, месяца и дня.
  • Все переменные в примере имеют конкретные статические типы, проверяемые компилятором.

Примитивные (простые) типы также относятся к значимым типам (int, float, double, char и т.д.).

Помимо них, есть:


перечисления (enum)

enum Days { Monday, Tuesday, Wednesday }

Разбор:

  • enum Days объявляет перечисление с ограниченным набором допустимых значений.
  • Monday, Tuesday, Wednesday — именованные константы, обычно с базовыми числовыми значениями 0, 1, 2.
  • Перечисления повышают читаемость кода и уменьшают количество "магических чисел".
  • Тип Days можно использовать в switch и проверках бизнес-состояний.

Перечисление определяет именованный набор константных значений, представляющих логически связанные состояния или категории.

Объявление перечисления:

record <имя>(<тип> <параметр1>, <тип> <параметр2>)


структуры (struct)

Структура (struct) — способ описать свой тип данных в C#. Внутри можно объявить поля, свойства и методы — как в классе. Разница в том, как тип хранится в памяти и что происходит при копировании.

Ключевые термины:

  • Значимый тип (value type) — переменная хранит сами данные (число, дата, координаты). Примитивы int, bool и пользовательские struct относятся к этой категории. Подробнее — ниже и статья Стек и куча.
  • Ссылочный тип (reference type) — переменная хранит ссылку (адрес) на объект в куче. Классы, строки, массивы и списки — ссылочные типы.
  • Куча (heap) — область памяти для объектов с непредсказуемым временем жизни; память освобождает сборщик мусора (GC).
  • Стек (stack) — область памяти для локальных переменных и параметров метода; очищается автоматически при выходе из метода.
  • Неизменяемый тип (immutable) — после создания поля не меняются. Для struct это рекомендуемый подход; помогают модификатор readonly, свойства только для чтения и record struct.
  • Интерфейсконтракт с набором членов без реализации. И class, и struct могут его реализовать; наследование классов у struct недоступно.
  • Boxing — упаковка значимого типа в объект в куче при присвоении переменной типа object или интерфейсу. См. значимые типы и boxing.
public readonly struct Point
{
public int X { get; }
public int Y { get; }

public Point(int x, int y) => (X, Y) = (x, y);

public double DistanceTo(Point other) =>
Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}

Разбор:

  • readonly struct — поля и свойства нельзя изменить после создания экземпляра; при копировании по значению это снижает риск случайно изменить копию вместо оригинала.
  • Конструктор задаёт начальные координаты.
  • Метод DistanceTo показывает, что у struct может быть поведение, как у класса.
  • Два поля int занимают 8 байт — типичный размер для struct.

Синтаксис объявления: struct Имя { ... }. Тип неявно запечатан (sealed) — от него нельзя наследоваться. Реализовать можно несколько интерфейсов.

Связанные материалы по ООП — class и struct в C#.


Отличия class и struct
classstruct
КатегорияСсылочный типЗначимый тип
Где лежат данныеОбъект в куче; в переменной — ссылкаДанные внутри переменной; локальная переменная обычно на стеке
Присваивание b = aДве переменные указывают на один объектСоздаётся вторая копия всех полей
Значение по умолчаниюnull (пустая ссылка)Экземпляр с нулями в полях (default)
НаследованиеОдин базовый класс и интерфейсыТолько интерфейсы
Конструктор без параметровРазрешён явноДо C# 10 явный parameterless запрещали; с C# 10+ — если все поля инициализированы (версии C#)
Изменение после созданияЛюбое; все ссылки видят изменениеОсторожно с изменяемыми полями — см. примеры
Сравнение ==По ссылке, если не переопределён EqualsПо полям, если не переопределён Equals
// class — одна машина, два имени
var car1 = new Car { Model = "Tesla" };
var car2 = car1;
car2.Model = "BMW";
Console.WriteLine(car1.Model); // BMW

// struct — две отдельные точки
var p1 = new Point(1, 2);
var p2 = p1;
p2 = p2 with { X = 10 }; // выражение with (C# 9+) — новая копия с другим X
Console.WriteLine(p1.X); // 1

Чтобы изменить struct внутри метода и сохранить результат снаружи, передайте параметр по ссылке — ref, out, in — или верните новое значение из метода.


Типы в стандартной библиотеке .NET

BCL (Base Class Library) — встроенные типы платформы .NET. Часть из них объявлена как struct, часть как class. От категории зависит копирование и работа с null.

Значимые (struct)

  • целые и дробные числа — int, long, byte, bool, char, decimal, double, float (внутри это System.Int32, System.Boolean и т.д.);
  • дата и время — DateTime, DateTimeOffset, TimeSpan;
  • идентификаторы — Guid;
  • отсутствие значения у примитива — Nullable<T> (int?);
  • кортежи — ValueTuple, синтаксис (int, string);
  • математика в играх — System.Numerics.Vector2, Matrix4x4;
  • компактные записи — record struct (C# 10+).

Ссылочные (class)

  • текст и корень иерархии — string, object;
  • коллекции и массивы — List<T>, Dictionary<,>, int[];
  • ошибки и доменные сущности — Exception, ваши классы сервисов и моделей;
  • делегаты — Action, Func;
  • изменяемый текст в цикле — StringBuilder;
  • DTO по ссылке — record (class), анонимные типы.

В Java у каждого примитива есть отдельный класс-обёртка (Integer для int). В C# примитив и есть структура — отдельный класс-обёртка для каждой переменной не нужен. Сравнение — значимые типы и boxing, типы в Java.

Проверка в коде:

Console.WriteLine(typeof(DateTime).IsValueType); // True
Console.WriteLine(typeof(string).IsValueType); // False

Как выбрать class или struct

Перед объявлением типа задайте три вопроса.

1. Размер данных

  • Для struct ориентир — до 16 байт (два double или три int).
  • При каждом вызове метода без ref/in копируются все поля целиком.
  • Большой набор полей удобнее оформить как class — копируется только ссылка (4 или 8 байт).

2. Изменяемость

  • struct чаще делают неизменяемымreadonly struct, record struct, методы, возвращающие новый экземпляр.
  • Если поля меняются после создания, проще ошибиться: метод получает копию, а не оригинал (см. примеры).

3. Иерархия типов

  • Цепочки вроде AnimalDog — только через class и полиморфизм.
  • Общий контракт без общего базового класса — интерфейс; его реализуют и class, и struct.
ЗадачаПодходящий тип
Координаты, цвет RGB, сумма с валютойreadonly struct или record struct
Сервис, репозиторий, сущность БД, MonoBehaviourclass
Ключ в Dictionary или HashSetrecord struct или readonly struct с Equals/GetHashCode
Список разных подтипов в одной коллекцииclass + наследование или интерфейсы

При сомнении начните с class. Переходите на struct, когда тип маленький, редко меняется и профилировщик показывает лишнюю нагрузку на GC.


Примеры struct

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

public readonly struct Vector2
{
public float X { get; }
public float Y { get; }
public Vector2(float x, float y) => (X, Y) = (x, y);
public Vector2 Add(Vector2 other) => new(X + other.X, Y + other.Y);
}

Цвет RGB — важно содержимое полей, отдельная "личность" объекта не нужна:

public readonly record struct RgbColor(byte R, byte G, byte B)
{
public static RgbColor White => new(255, 255, 255);
}

Интерфейс без наследования класса

public interface IArea
{
double Area { get; }
}

public readonly struct Circle : IArea
{
public double Radius { get; }
public Circle(double radius) => Radius = radius;
public double Area => Math.PI * Radius * Radius;
}

Переменная типа Circle хранит значение на стеке. При присвоении IArea shape = new Circle(5) сработает boxing — значение копируется в объект в куче. Для горячего кода предпочтительнее обобщения (where T : struct) или иерархия классов.

Изменяемый struct и параметр метода

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

void MoveRight(MutablePoint p) => p.X++; // меняется локальная копия параметра

var origin = new MutablePoint { X = 0, Y = 0 };
MoveRight(origin);
Console.WriteLine(origin.X); // 0

void MoveRightRef(ref MutablePoint p) => p.X++; // меняется исходная переменная

Подробнее о refПеременные и области видимости.

Struct в List<T>

var points = new List<MutablePoint> { new() { X = 1, Y = 2 } };
points[0].X = 5; // ошибка компиляции: индексатор возвращает копию
var p = points[0];
p.X = 5;
points[0] = p; // записать изменённую копию обратно

С коллекциями удобнее работать через неизменяемый struct и выражение with.


Краткая шпаргалка
  • struct — компактные данные (точка, интервал, ключ сортировки), чаще неизменяемые.
  • class — объекты с идентичностью, наследованием и долгим жизненным циклом.
  • Память и копирование — Стек и куча.
  • ООП-контекст — class и struct в C#.
  • Синтаксис record, readonly, withсправочник C#.

ValueTuple

var person = (name: "Alice", age: 25);

Разбор:

  • Создаётся кортеж (ValueTuple) с именованными элементами name и age.
  • var выведет тип как (string name, int age).
  • К элементам можно обращаться по именам (person.name, person.age), что читабельнее Item1/Item2.
  • Кортежи подходят для возврата нескольких связанных значений без отдельного класса.

При присваивании одного экземпляра значимого типа другому, копируются данные целиком:

int a = 10;
int b = a; // b получает копию значения a
a = 20;
Console.WriteLine(b); // Выведет 10

Разбор:

  • b = a копирует значение, а не ссылку: каждая переменная хранит собственную копию числа.
  • После изменения a на 20 переменная b остаётся прежней (10).
  • Это типичное поведение значимых типов (value types) при присваивании.
  • Пример наглядно показывает, почему изменение одной переменной не влияет на другую.

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

  • более быстрые операции за счёт хранения в стеке;
  • не поддерживают множественное наследование;
  • не могут быть null, если не объявлены как nullable (int?, bool? и т.д.);

nullable

Знак вопроса после типа позволяет переменной принимать значение null в дополнение к обычным значениям типа. Применяется к значимым типам.

<тип>? <имя> = <значение>;

Пример nullable-типа:

int? nullableInt = null;

Разбор:

  • int? — сокращённая форма Nullable<int>, позволяющая хранить либо число, либо null.
  • Присваивание null обозначает отсутствие значения, а не число 0.
  • Это удобно для полей, где значение может быть неизвестно (например, необязательный возраст).
  • Перед использованием такого значения обычно проверяют HasValue или сравнивают с null.

Nullable-версии типов могут принимать значение null, а внутри реализуются через обобщённую структуру System.Nullable<T>.


Значимые типы, boxing и аналог обёрток Java

В Java у каждого примитива есть отдельный класс-обёртка (intInteger, boolBoolean). Подробнее — Типы данных в Java — примитив и обёртка.

В C# отдельных классов Int32-обёрток для каждой переменной нет — значимые типы (int, double, bool, …) — это структуры, они хранятся по значению. Когда значимый тип нужен как объект (коллекция до generic-оптимизаций, object, интерфейс), компилятор выполняет boxing — копирует значение в объект в куче:

int count = 42; // значение в стеке (или внутри другого объекта)
object boxed = count; // boxing: int → объект в куче, ссылка в boxed
int again = (int)boxed; // unboxing: копия обратно в int
Java int / IntegerC# int / boxing
Базовый типПримитив intЗначимый тип struct (синоним Int32)
"Обёртка"Явный класс IntegerОбъект создаётся при boxing в object
Nullable "нет числа"Integer x = nullint? x = null (Nullable<int>)
КоллекцииList<Integer>List<int> — без boxing на каждой операции
Сравнениеequals() для объектов== для int; для nullable — HasValue

int? (или Nullable<int>) — ближайший аналог "обёртки с null" из Java — это структура, которая либо хранит int, либо "пусто". Подробнее — Nullable-типы в C#.

Boxing дорогой (аллокация + копирование). В горячих циклах избегайте присваивания int в object и старых необобщённых коллекций ArrayList. Детали размещения в памяти — Стек, куча и boxing.


Число

Число (int, double, decimal).

Значимые типы. decimal используется для финансовых вычислений (более точный, чем double).

Простой пример:

int age = 30;

Разбор:

  • age объявляется как int, то есть 32-битное целое число со знаком.
  • Литерал 30 напрямую записывается в переменную при инициализации.
  • Такой тип подходит для счётчиков, индексов и небольших числовых диапазонов.
  • Пример минимален, но подчёркивает базовый шаблон объявления: тип + имя + значение.

Сложный пример:

decimal taxAmount = subtotal * (region switch {
"EU" => 0.2m,
"US" => stateTaxRates[state],
"ASIA" => 0.15m,
_ => 0.0m
});

Разбор:

  • Используется выражение switch, которое возвращает значение в зависимости от region.
  • Суффикс m у литералов (0.2m) указывает тип decimal, важный для точных финансовых расчётов.
  • Ветка "US" берёт ставку из словаря stateTaxRates по ключу state.
  • _ => 0.0m — ветка по умолчанию, если регион не распознан.
  • Итоговая ставка умножается на subtotal, и результат сохраняется в taxAmount.

Использование выражения switch для определения налоговой ставки в зависимости от региона. Результат умножается на сумму. Тип decimal гарантирует точность при финансовых операциях.


Булево

Булево (bool).

Примитивный тип, аналогичный Java.

Простой пример:


bool isActive = true;

Разбор:

  • bool хранит только два значения: true или false.
  • Переменная isActive обычно используется как флаг состояния сущности.
  • Инициализация true задаёт начальное состояние "активен".
  • Такой тип часто участвует в условиях if, фильтрации и валидации.

Сложный пример:

bool isValid = !string.IsNullOrEmpty(email) &&
email.Contains("@") &&
allowedDomains.Any(domain => email.EndsWith(domain)) &&
!blacklistedEmails.Contains(email);

Разбор:

  • Выражение строит цепочку логических проверок через &&; итог true только если все условия выполнены.
  • !string.IsNullOrEmpty(email) защищает от null и пустой строки.
  • email.Contains("@") проверяет базовый признак формата e-mail.
  • Any(domain => email.EndsWith(domain)) ищет совпадение хотя бы с одним разрешённым доменом.
  • !blacklistedEmails.Contains(email) исключает адреса из чёрного списка.

Проверка валидности email включает несколько условий — не null/empty, наличие символа @, соответствие домену и отсутствие в чёрном списке. Используется LINQ (Any, Contains).


Ссылочные типы

Ссылочные типы (Reference Types) содержат ссылку (адрес) на область памяти в куче, где находятся реальные данные.

Примеры:

string name = "John";
object obj = 42;
List<int> numbers = new List<int>();

Разбор:

  • string name хранит ссылку на строковый объект в куче.
  • object obj = 42; приводит значимый тип int к object (boxing).
  • new List<int>() создаёт экземпляр обобщённой коллекции целых чисел.
  • Пример показывает, что ссылочные типы могут ссылаться на разные виды объектов.

Категории ссылочных типов:

  • классы (class):
class Person {
public string Name { get; set; }
}
  • интерфейсы (interface):
interface ILogger {
void Log(string message);
}
  • делегаты (delegate):
delegate void Notify(string message);
  • массивы (array):
int[] numbers = new int[] {1, 2, 3};
  • тип object (базовый тип для всех типов в .NET);
  • тип string (строка), неизменяемая (immutable) и имеет особое поведение.

При присваивании одного экземпляра ссылочного типа другому, копируется ссылка, а не сам объект:

Person p1 = new Person { Name = "Alice" };
Person p2 = p1; // p2 указывает на тот же объект
p1.Name = "Bob";
Console.WriteLine(p2.Name); // Выведет "Bob"

Особенности ссылочных типов:

  • требуют управления памятью через Garbage Collector (GC);
  • могут быть null;
  • поддерживают наследование и полиморфизм;
  • могут иметь сложную структуру и включать другие типы.

Строка

Строка (string).

Неизменяемый ссылочный тип. Богатый набор методов и поддержка интерполяции. Простой пример:

string name = "Анна";

Сложный пример:

string report = $@"
Отчёт за {DateTime.Now:MMMM yyyy}
-----------------------------------
Пользователь: {user.Name?.Trim() ?? "Не указан"}
Роль: {(user.Role.HasValue ? Enum.GetName(typeof(Role), user.Role) : "N/A")}
Баланс: {user.Balance:C}
Статус: {(user.IsActive ? "Активен" : "Заблокирован")}
".Trim();

Интерполяция с условными выражениями, безопасным доступом к свойствам (?.), форматированием валюты (:C) и именем перечисления. Результат — многострочный отчёт.


Play ITЗагрузка интерактивного демо…


Объект

Объект (class, record). Ссылочные типы. record — неизменяемые объекты (с C# 9).

Простой пример:

var person = new { Name = "Иван", Age = 28 };

Сложный пример:

var employees = dbContext.Employees
.Where(e => e.DepartmentId == deptId && e.Salary > minSalary)
.Select(e => new EmployeeSummary(
e.Id,
$"{e.FirstName} {e.LastName}".Trim(),
e.Position,
e.Salary * (1 + bonusPercentage)
))
.ToList();

LINQ-запрос к базе данных, фильтрация и проекция в DTO-объект с вычислением бонуса. Демонстрирует интеграцию с ORM (например, Entity Framework).


Play ITЗагрузка интерактивного демо…


Массив

Массив (array).

Фиксированная коллекция одного типа.

Простой пример:

int[] numbers = { 1, 2, 3 };

Сложный пример:

int[,] distanceMatrix = new int[cities.Length, cities.Length];
for (int i = 0; i < cities.Length; i++)
{
for (int j = 0; j < cities.Length; j++)
{
distanceMatrix[i, j] = (int)CalculateDistance(cities[i], cities[j]);
}
}

Двумерный массив расстояний между городами. Заполняется через вложенный цикл и вызов функции. Типичный случай для задач маршрутизации.


Делегат / функция

Делегат / Функция (Func, Action).

Типы-обёртки для функций. Func<T>, TResult>, Action<T>.

Простой пример:

Func<string, string> greet = name => $"Привет, {name}!";

Сложный пример:

Func<User, bool> isPremium = u =>
u.SubscriptionLevel == Subscription.Premium &&
u.LastLogin > DateTime.Now.AddDays(-30) &&
u.TotalPurchases.Sum(p => p.Amount) > 1000;

var premiumUsers = allUsers.Where(isPremium).ToList();

Делегат Func<User, bool> инкапсулирует правило определения премиум-пользователя. Затем используется в LINQ-запросе. Позволяет переиспользовать логику проверки.

Когда какой тип выбрать

  • простые частые данные — встроенный значимый тип (int, bool, DateTime);
  • логически связанная группа полей — struct или record struct;
  • объекты с поведением, наследованием и коллекциямиclass;
  • фиксированный набор состояний — enum;
  • компактность и предсказуемость копирования — readonly struct.

Виды массивов

Массив – упорядоченная коллекция элементов одного типа.

Одномерный массив:

int[] numbers = { 1, 2, 3 };

Многомерный массив:

int[,] matrix = {
{ 1, 2 },
{ 3, 4 }
};

Jagged-массив (массив массивов):

int[][] rows = new int[][] {
new int[] { 1, 2 },
new int[] { 3, 4, 5 }
};