Типы данных в C#
Типы данных определяют, какие значения может хранить переменная и сколько памяти она занимает. Ниже собраны все основные типы C#: значимые и ссылочные, целые и вещественные, decimal, bool, char, string, object и dynamic.
Вы увидите таблицу диапазонов, примеры работы с var, nullable-типами и преобразованиями, а также узнаете, как определить тип через typeof и GetType.

Содержание
- Система типов в C#
- Значимые и ссылочные типы
- Целочисленные типы
- Вещественные типы и decimal
- Логический и символьный типы
- Строки и тип string
- Типы object, dynamic и ключевое слово var
- Типы, допускающие null
- Преобразование типов
- Определение типа: typeof и GetType
- Значения по умолчанию и default
- Как выбрать подходящий тип
- Частые вопросы
Система типов в C#
C# относится к языкам со строгой статической типизацией. Каждая переменная, поле, параметр и результат выражения имеют тип, который известен компилятору ещё до запуска программы. Тип определяет, сколько памяти займёт значение, какие значения допустимы и какие операции с ним разрешены. Если попытаться сложить число и логическое значение или передать строку туда, где ожидается целое число, программа просто не соберётся.
Все типы .NET выстроены в единую иерархию, на вершине которой стоит System.Object. Ключевые слова вроде int, string или bool являются псевдонимами (алиасами) для типов из пространства имён System: int это System.Int32, string это System.String, bool это System.Boolean. Разницы в поведении между алиасом и полным именем нет, подробнее об этом написано в статье про различия между String и string.
Простейший способ увидеть типы в работе - объявить несколько переменных разных видов:
int age = 30;
double price = 199.99;
decimal balance = 1500.50m;
bool isActive = true;
char grade = 'A';
string name = "Анна";
Console.WriteLine($"{name}, {age}, {price}, {balance}, {isActive}, {grade}");
Больше о синтаксисе объявления и правилах именования переменных можно прочитать в материале как объявить переменную в C#.
Значимые и ссылочные типы
Главное деление в системе типов проходит между значимыми (value types) и ссылочными (reference types). От него зависит, как значение хранится в памяти, что происходит при присваивании и как ведёт себя передача в метод.
Значимые типы
Переменная значимого типа хранит само значение. К этой группе относятся все числовые типы, bool, char, перечисления (enum) и структуры (struct). Обычно такие значения лежат в стеке или прямо внутри объекта, которому принадлежат. При присваивании создаётся независимая копия: изменение одной переменной не затрагивает другую.
Ссылочные типы
Переменная ссылочного типа хранит адрес объекта, который находится в управляемой куче. К ним относятся классы, интерфейсы, делегаты, массивы, а также string, object и dynamic. Присваивание копирует только ссылку, поэтому две переменные указывают на один и тот же объект, и изменение через одну из них видно через другую. Освобождением памяти занимается сборщик мусора.
Разницу удобно показать на двух похожих по форме типах, структуре и классе:
PointStruct s1 = new PointStruct { X = 1 };
PointStruct s2 = s1; // копия значения
s2.X = 99;
Console.WriteLine(s1.X); // 1
PointClass c1 = new PointClass { X = 1 };
PointClass c2 = c1; // копия ссылки
c2.X = 99;
Console.WriteLine(c1.X); // 99
struct PointStruct { public int X; }
class PointClass { public int X; }
Сводка основных отличий выглядит так:
| Признак | Значимые типы | Ссылочные типы |
|---|---|---|
| Что хранит переменная | Само значение | Ссылку на объект в куче |
| Присваивание | Копирует значение | Копирует ссылку |
| Значение по умолчанию | Ноль или аналог | null |
| Примеры | int, double, bool, struct, enum | class, string, массивы, делегаты |
| Освобождение памяти | При выходе из области видимости | Сборщиком мусора |
Отдельно стоит помнить про массивы. Сам массив всегда ссылочный тип, даже если его элементы числа: int[] передаётся в метод по ссылке. Подробно о них рассказано в статьях про массивы в C#.
Целочисленные типы
Для хранения целых чисел в C# есть восемь основных типов. Они различаются размером и тем, допускают ли отрицательные значения. Типы со знаком используют один бит под знак, поэтому их положительный диапазон вдвое меньше, чем у беззнаковых аналогов.
| Тип | Размер | Диапазон |
|---|---|---|
sbyte | 1 байт | от -128 до 127 |
byte | 1 байт | от 0 до 255 |
short | 2 байта | от -32 768 до 32 767 |
ushort | 2 байта | от 0 до 65 535 |
int | 4 байта | от -2 147 483 648 до 2 147 483 647 |
uint | 4 байта | от 0 до 4 294 967 295 |
long | 8 байт | от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 |
ulong | 8 байт | от 0 до 18 446 744 073 709 551 615 |
В повседневном коде почти всегда используют int: это тип по умолчанию для целых литералов, его же ожидают индексаторы и свойства вроде Length. Тип long нужен для больших величин, например размеров файлов или меток времени. Тип byte применяют при работе с двоичными данными, потоками и сетью, а short и sbyte встречаются редко, в основном когда важна экономия памяти в больших массивах.
Границы каждого типа доступны через константы MinValue и MaxValue. Выход за границу приводит к переполнению: по умолчанию результат молча оборачивается, а в блоке checked выбрасывается исключение.
int max = int.MaxValue;
Console.WriteLine(max); // 2147483647
Console.WriteLine(max + 1); // -2147483648 (переполнение без проверки)
try
{
int result = checked(max + 1);
}
catch (OverflowException)
{
Console.WriteLine("Переполнение обнаружено");
}
long big = 5_000_000_000L; // суффикс L, разделители разрядов
byte flags = 0b_0000_1010; // двоичный литерал
int hex = 0xFF; // шестнадцатеричный литерал
Похожая ловушка связана с делением: если оба операнда целые, результат тоже целый, и дробная часть отбрасывается. Почему так происходит и как этого избежать, объяснено в статье про целочисленное деление.
Вещественные типы и decimal
Для чисел с дробной частью предназначены три типа: float, double и decimal. Первые два хранят число в двоичном формате с плавающей точкой и очень быстро обрабатываются процессором, третий хранит десятичное число и рассчитан на точные денежные вычисления.
| Тип | Размер | Примерный диапазон | Точность | Суффикс |
|---|---|---|---|---|
float | 4 байта | от ±1,5e-45 до ±3,4e38 | 6-9 знаков | f |
double | 8 байт | от ±5e-324 до ±1,7e308 | 15-17 знаков | d (необязателен) |
decimal | 16 байт | от ±1e-28 до ±7,9e28 | 28-29 знаков | m |
Погрешность чисел с плавающей точкой
Двоичные дроби не могут точно представить многие десятичные числа, например 0,1. Из-за этого сумма 0,1 и 0,2 в double не равна ровно 0,3, и сравнивать такие значения оператором == нельзя. Для сравнения берут допустимую разницу (эпсилон) или переходят на decimal.
double a = 0.1 + 0.2;
Console.WriteLine(a == 0.3); // False
Console.WriteLine(Math.Abs(a - 0.3) < 1e-9); // True
decimal d = 0.1m + 0.2m;
Console.WriteLine(d == 0.3m); // True
float f = 3.14f; // без суффикса f литерал считался бы double
double e = 1.5e3; // 1500
Когда выбирать decimal
Тип decimal работает медленнее и занимает вдвое больше памяти, чем double, зато хранит десятичные дроби без накопления ошибки. Он подходит для цен, зарплат, налогов и любых расчётов, где важна каждая копейка. Для физических величин, графики и научных вычислений лучше double: скорость там важнее, а небольшая погрешность закономерна. Развёрнутое сравнение с примерами есть в статье выбор между decimal и double.
У вещественных типов есть и особые значения: double.NaN (не число), double.PositiveInfinity и double.NegativeInfinity. Деление вещественного числа на ноль не вызывает исключения, а даёт бесконечность или NaN, тогда как для decimal выбрасывается DivideByZeroException.
Логический и символьный типы
Два небольших типа, без которых не обходится почти ни одна программа, отвечают за условия и отдельные символы.
Тип bool
Тип bool принимает только два значения, true и false. В отличие от C и C++, числа и ссылки в условиях автоматически к логическому типу не приводятся: запись if (count) вызовет ошибку компиляции, нужно писать if (count != 0). Логические значения возвращают операторы сравнения, а над ними работают операции &&, || и !.
Тип char
Тип char хранит один символ в кодировке UTF-16 и занимает два байта. Литерал записывается в одинарных кавычках. Символ можно привести к числу и обратно, потому что внутри это код символа. Помните, что некоторые символы Unicode, например эмодзи, не помещаются в один char и состоят из двух.
bool ok = 5 > 3;
char letter = 'C';
int code = letter; // 67
char next = (char)(letter + 1); // 'D'
Console.WriteLine(char.IsDigit('7')); // True
Console.WriteLine(char.ToUpper('q')); // Q
Console.WriteLine($"{ok} {code} {next}");
Строки и тип string
Тип string хранит последовательность символов char. Это ссылочный тип, но ведёт он себя почти как значимый: строки неизменяемы. Любая операция, которая как будто меняет строку (Replace, ToUpper, Trim, конкатенация), на самом деле создаёт новый объект, а старый остаётся без изменений.
Из-за неизменяемости многократное склеивание строк в цикле создаёт много временных объектов. Для таких случаев есть класс StringBuilder, который накапливает текст в изменяемом буфере. Сравнение строк оператором == сравнивает содержимое, а не ссылки, чего нельзя сказать о многих других ссылочных типах.
string s = "Привет";
string t = s.ToUpper();
Console.WriteLine(s); // Привет (исходная строка не изменилась)
Console.WriteLine(t); // ПРИВЕТ
string path = @"C:\Temp\file.txt"; // буквальная строка
string multi = """
Многострочный
текст
"""; // сырая строка, C# 11
int n = 7;
string msg = $"Значение: {n:D3}"; // интерполяция, 007
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 3; i++) sb.Append(i).Append(';');
Console.WriteLine(sb.ToString()); // 0;1;2;
Преобразовать строку в массив символов или разбить на части можно несколькими способами, они собраны в статье про строку в массив.
Типы object, dynamic и ключевое слово var
Эти три средства нередко путают, хотя работают они совершенно по-разному: один хранит любое значение, второй переносит проверки на время выполнения, третий лишь сокращает запись.
Object и упаковка
Тип object является базовым для всех остальных типов, поэтому в переменную object можно положить что угодно. Когда в неё попадает значимый тип, происходит упаковка (boxing): значение копируется в кучу и оборачивается в объект. Обратная операция называется распаковкой (unboxing) и требует явного приведения к точному исходному типу. Упаковка стоит времени и памяти, поэтому в горячих участках кода её стараются избегать, используя обобщённые коллекции вроде List<int> вместо старого ArrayList.
int number = 42;
object boxed = number; // упаковка
int back = (int)boxed; // распаковка
Console.WriteLine(back);
// long wrong = (long)boxed; // InvalidCastException: внутри int, а не long
long right = (int)boxed; // сначала распаковать как int
Dynamic
Тип dynamic отключает проверку типов на этапе компиляции: обращения к членам разрешаются во время выполнения. Это полезно при работе с COM, динамическими языками и JSON неизвестной структуры, но цена высока. Опечатка в имени метода превратится в исключение RuntimeBinderException уже в работающей программе, а подсказки среды разработки перестанут работать. Использовать dynamic стоит точечно.
Var
Ключевое слово var не создаёт динамический тип. Компилятор выводит тип из правой части инициализации и подставляет его на этапе компиляции, так что переменная остаётся строго типизированной. Объявить var без инициализатора нельзя, а присвоить переменной потом значение другого типа не получится.
var list = new List<string>(); // List<string>
var total = 10 * 2.5; // double
// total = "текст"; // ошибка компиляции
dynamic x = 10;
x = "теперь строка"; // допустимо
Console.WriteLine(x.Length); // 13, проверка идёт во время выполнения
Хорошее правило: используйте var, когда тип очевиден из выражения (new, результат понятного метода), и пишите тип явно, когда он помогает читателю понять, что за значение хранится в переменной.
Типы, допускающие null
Значимые типы не могут быть пустыми: int всегда содержит число. Но данные из базы или формы часто бывают не заданы. Для таких случаев есть Nullable<T>, записываемый как T?. Это структура с флагом наличия значения.
int? age = null;
Console.WriteLine(age.HasValue); // False
Console.WriteLine(age ?? 18); // 18, значение по умолчанию
age = 25;
if (age is int a)
Console.WriteLine(a + 1); // 26
int safe = age.GetValueOrDefault(); // 25
// int bad = age.Value; // InvalidOperationException, если null
Для ссылочных типов с C# 8 появились аннотации допустимости null. Если в проекте включён <Nullable>enable</Nullable>, то string считается значением, которое не должно быть null, а string? явно допускает его. Компилятор выдаёт предупреждения при возможном разыменовании пустой ссылки. Это один из лучших способов защититься от самой частой ошибки времени выполнения, о которой подробно сказано в разборе NullReferenceException.
Полезные операторы для работы с null: ?? подставляет запасное значение, ??= присваивает его только если переменная пуста, ?. безопасно обращается к члену и возвращает null, если объект пуст.
Преобразование типов
Значения одного типа часто приходится превращать в другой. В C# для этого есть несколько механизмов, которые различаются по безопасности. Более детальный разбор с примерами собран в статье про приведение типов, а ниже даны основные приёмы.
Неявное и явное приведение
Неявное преобразование выполняется автоматически, если оно не может привести к потере данных: из int в long, из float в double, из любого типа в object. Явное преобразование нужно, когда значение может не поместиться или потерять точность. Для него в скобках указывают целевой тип. При приведении дробного числа к целому дробная часть отбрасывается без округления.
int small = 100;
long wide = small; // неявно, без потерь
double pi = 3.99;
int cut = (int)pi; // 3, дробная часть отброшена
Console.WriteLine(Math.Round(pi)); // 4
int big = 300;
byte b = (byte)big; // 44, старшие биты потеряны
Console.WriteLine(b);
Parse, TryParse и класс Convert
Для превращения строки в число используют int.Parse, но при неверном формате он выбрасывает исключение. Если пользователь может ввести что угодно, надёжнее TryParse: он возвращает признак успеха и не создаёт исключения. Класс Convert универсален и умеет работать между многими типами, при этом Convert.ToInt32(null) вернёт 0, а округляет он до ближайшего чётного, а не отбрасывает дробь.
string input = "123abc";
if (int.TryParse(input, out int value))
Console.WriteLine($"Число: {value}");
else
Console.WriteLine("Это не число");
int ok = int.Parse("456");
double dbl = Convert.ToDouble("3,14"); // зависит от текущей культуры
Console.WriteLine(Convert.ToInt32(2.5)); // 2 (банковское округление)
Console.WriteLine(Convert.ToInt32(3.5)); // 4
Операторы is и as
Для ссылочных типов существуют безопасные способы проверить и привести тип. Оператор is проверяет совместимость и при необходимости сразу объявляет переменную нужного типа. Оператор as пытается привести объект и при неудаче возвращает null вместо исключения. Обычное приведение в скобках при несовместимых типах бросает InvalidCastException.
object obj = "строка";
if (obj is string text)
Console.WriteLine(text.Length);
string? maybe = obj as string; // "строка"
int? notInt = obj as int?; // null, не исключение
// int wrong = (int)obj; // InvalidCastException
Определение типа: typeof и GetType
Иногда нужно узнать тип во время выполнения. Оператор typeof принимает имя типа и возвращает объект System.Type; он работает на этапе компиляции, и экземпляр ему не нужен. Метод GetType() вызывается у конкретного объекта и возвращает его фактический тип, даже если переменная объявлена с базовым типом. Эти два механизма часто сравнивают для строгой проверки типа. Оператор is в отличие от них учитывает наследование.
object o = 3.14;
Console.WriteLine(typeof(int)); // System.Int32
Console.WriteLine(o.GetType()); // System.Double
Console.WriteLine(o.GetType() == typeof(double)); // True
Console.WriteLine(o.GetType().Name); // Double
Console.WriteLine(o is double); // True
Console.WriteLine(o is IFormattable); // True, double реализует интерфейс
Console.WriteLine(typeof(string).IsValueType); // False
Строковое имя типа также пригодится при отладке и логировании. Для обобщённых методов, которым нужен Type в переменной, посмотрите статью про вызов generic-метода через Type.
Значения по умолчанию и default
Если поле класса или элемент массива не инициализированы явно, они получают значение по умолчанию. Для числовых типов это ноль, для bool это false, для char это символ с кодом 0, для ссылочных типов это null. Локальные переменные такого значения не получают: компилятор не даст прочитать переменную, пока ей что-то не присвоено.
Получить значение по умолчанию для любого типа можно ключевым словом default. Это особенно удобно в обобщённом коде, где конкретный тип заранее неизвестен.
Console.WriteLine(default(int)); // 0
Console.WriteLine(default(bool)); // False
Console.WriteLine(default(string) is null); // True
DateTime dt = default; // 01.01.0001 0:00:00
int[] numbers = new int[3]; // {0, 0, 0}
static T Get<T>() => default!;
Console.WriteLine(Get<double>()); // 0
Как выбрать подходящий тип
Несколько практических правил позволяют не ошибиться в большинстве случаев:
- для счётчиков, индексов и обычных целых используйте
int, для очень больших значенийlong; - для денег и точных десятичных расчётов берите
decimal, для научных и графических вычисленийdouble; - для флагов и условий применяйте
bool, а набор взаимоисключающих состояний оформляйте черезenum; - для текста используйте
string, для интенсивной сборки текстаStringBuilder; - для необязательных значений выбирайте
T?вместо магических чисел вроде -1; - для небольших неизменяемых наборов данных подойдёт
structилиrecord struct, для остальных случаевclass.
Типичные ошибки новичков: сравнение вещественных чисел через ==, использование float для денег, потеря дробной части при делении двух целых, приведение через скобки там, где нужен TryParse, и забытые проверки на null. Если следить за этими местами, большинство проблем с типами исчезает ещё на этапе написания кода.
Частые вопросы
Сколько типов данных в C#?
Встроенных простых типов около пятнадцати: восемь целочисленных, три вещественных (float, double, decimal), bool, char, а также ссылочные string и object. Всего в .NET тысячи типов, потому что любой класс, структура и интерфейс тоже являются типом.
Чем отличается int от Int32?
Ничем. int - это ключевое слово языка, псевдоним для System.Int32. Компилятор обрабатывает обе записи одинаково, различается только стиль. В коде принято использовать короткие ключевые слова, а имя класса удобно при вызове статических членов вроде Int32.Parse.
Является ли string значимым типом?
Нет, string относится к ссылочным типам. Путаница возникает из-за неизменяемости и сравнения по содержимому: строка ведёт себя похоже на значение, но хранится в куче, а переменная содержит ссылку и может быть null.
В чём разница между var и dynamic?
Тип var определяется компилятором один раз при компиляции и далее не меняется, все проверки остаются строгими. Тип dynamic проверяется только при выполнении и позволяет менять тип значения в той же переменной, но ошибки проявляются уже в работающей программе.
Почему 0.1 + 0.2 не равно 0.3?
Типы float и double хранят числа в двоичной системе, где 0,1 и 0,2 записываются бесконечными дробями и поэтому округляются. Сумма даёт число, отличающееся от 0,3 на малую величину. Для точных десятичных вычислений используйте decimal, а вещественные значения сравнивайте с допустимой погрешностью.
Что выбрать для денег в C#?
Тип decimal. Он хранит десятичные дроби точно, поддерживает до 28-29 значащих цифр и обычно используется для сумм, цен и налогов. Литералы этого типа пишутся с суффиксом m, например 19.99m.





