C# статически типизирован: у каждой переменной, поля и выражения есть тип, известный при компиляции. В языке есть набор встроенных типов со своими ключевыми словами (int, double, string...). Каждое ключевое слово это псевдоним типа из пространства имён System, поэтому int и System.Int32 это один и тот же тип, и string и System.String тоже один и тот же тип.
Встроенные типы
| Тип | Тип .NET | Размер | Диапазон или точность | Литерал |
|---|---|---|---|---|
bool | Boolean | 1 байт | true или false | true |
byte | Byte | 1 байт | от 0 до 255 | 200 |
sbyte | SByte | 1 байт | от -128 до 127 | -5 |
short | Int16 | 2 байта | от -32 768 до 32 767 | 1200 |
ushort | UInt16 | 2 байта | от 0 до 65 535 | 60000 |
int | Int32 | 4 байта | от -2 147 483 648 до 2 147 483 647 | 42 |
uint | UInt32 | 4 байта | от 0 до 4 294 967 295 | 42u |
long | Int64 | 8 байт | примерно ±9,2 × 10^18 | 42L |
ulong | UInt64 | 8 байт | от 0 примерно до 1,8 × 10^19 | 42ul |
float | Single | 4 байта | примерно ±3,4 × 10^38, ~6 до 9 цифр | 2.5f |
double | Double | 8 байт | примерно ±1,8 × 10^308, ~15 до 17 цифр | 2.5 или 2.5d |
decimal | Decimal | 16 байт | примерно ±7,9 × 10^28, 28 до 29 цифр | 2.5m |
char | Char | 2 байта | одна кодовая единица UTF-16, от U+0000 до U+FFFF | 'A' |
string | String | ссылка | текст любой длины, неизменяемый | "hello" |
object | Object | ссылка | базовый тип для всех типов | new object() |
Целочисленный литерал без суффикса имеет тип int, а если он слишком велик для int, то первый из uint, long и ulong, в который он помещается; литерал с десятичной точкой имеет тип double. Поэтому float f = 2.5; и decimal d = 2.5; не компилируются: литерал имеет тип double, а неявного преобразования из double во float или decimal нет. Добавьте суффикс: 2.5f, 2.5m.
Каждый числовой тип предоставляет свои границы в виде констант:
Вывод:
byte: 0 to 255
short: -32768 to 32767
int: -2147483648 to 2147483647
long: -9223372036854775808 to 9223372036854775807
decimal max: 79228162514264337593543950335
double max: 1.798E+308
sizes: int 4, long 8, double 8, char 2
255 161 3000000000
На практике большая часть кода использует четыре из них: int для счётчиков и индексов, long для идентификаторов и больших сумм, double для измерений и математики и decimal для денег.
decimal или double
double (и float) хранят числа в двоичном виде. У большинства десятичных дробей, включая 0.1, нет точного двоичного представления, поэтому они хранятся как ближайшее представимое значение, и мелкие ошибки накапливаются. decimal хранит десятичное целое плюс масштаб, поэтому 0.1 это ровно 0.1.
Вывод:
False
True
0.30000000000000004
0.3
double sum == 1.0: False
decimal sum == 1.0: True
Total: 59.97
Split three ways: 19.99
Используйте decimal для денег, цен, налогов и всего, что человек будет складывать вручную и сверять. Он сохраняет нули в конце из входных данных (1.50m печатается как 1.50) и имеет от 28 до 29 значащих цифр.
Используйте double для физических измерений, статистики, геометрии и игр. Он в несколько раз быстрее, занимает вдвое меньше памяти, а его диапазон огромен. Никогда не сравнивайте два вычисленных double через ==; сравнивайте разницу с допуском: Math.Abs(x - y) < 1e-9.
decimal не волшебный: 1m / 3m всё равно равно 0.3333333333333333333333333333, а арифметика double не выбрасывает исключение при делении на ноль (получается бесконечность или NaN для 0.0 / 0.0), тогда как деление на ноль у decimal и целых чисел выбрасывает DivideByZeroException.
Переполнение целых чисел и checked
Целочисленная арифметика, выходящая за диапазон типа, по умолчанию молча переходит через границу. Компилятор ловит переполнение в константных выражениях, но не тогда, когда значения берутся из переменных:
Вывод:
-2147483648
OverflowException
-1294967296
2147483648
checked(...) или блок checked { ... } превращает переполнение в OverflowException. Можно также включить проверку для всего проекта через <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>. Запись int x = int.MaxValue + 1; с константами это ошибка компиляции (CS0220), потому что компилятор видит переполнение.
Последняя строка показывает исправление большинства ошибок переполнения: преобразуйте в более широкий тип до арифметики. (long)(max + 1) было бы неправильно, потому что к моменту приведения сложение уже переполнилось.
Типы значений и ссылочные типы
Каждый тип C# является либо типом значения, либо ссылочным типом, и от этого зависит, что делает присваивание.
- Типы значений: все числовые типы,
bool,char,DateTime, перечисления и любаяstruct. Переменная содержит данные; присваивание их копирует. - Ссылочные типы:
string, массивы,objectи любойclass. Переменная содержит ссылку на объект; присваивание копирует ссылку, поэтому обе переменные видят один и тот же объект.
Вывод:
a=10 b=99
first[0]=99
s1=cat s2=cats
string это ссылочный тип, который на практике ведёт себя как значение: он неизменяемый, а == сравнивает текст, а не ссылки. Подробнее о пользовательских типах значений в разделе структуры.
char и bool
char это одна кодовая единица UTF-16, записанная в одинарных кавычках. Внутри это числовой тип, поэтому он преобразуется в int и поддерживает арифметику:
Вывод:
66
C
True
True
7
b
True
У bool ровно два значения, и он не преобразуется в числа и из чисел: if (count) это ошибка компиляции, а не проверка на ненулевое значение. Пишите if (count != 0). Символы за пределами базовой многоязычной плоскости, например большинство эмодзи, занимают два значения char (суррогатную пару), поэтому Length у string считает единицы UTF-16, а не видимые символы.
Значения по умолчанию
Поля, элементы массивов и default(T) получают значение по умолчанию для своего типа: ноль для чисел, false для bool, '\0' для char и null для ссылочных типов. Локальные переменные значения по умолчанию не получают, и им нужно присвоить значение перед использованием.
Вывод:
0
0
False
True
0001-01-01
0,0,0
Начиная с C# 7.1 можно писать литерал default без типа, если компилятор может его вывести (int x = default;). Тип значения не может быть null, и именно для этого существует int? (тип значения, допускающий null); см. типы, допускающие null.
Целые числа размера указателя и большие целые
Для особых случаев есть ещё два числовых типа. nint и nuint (C# 9) это целые числа размера указателя, 32 или 64 бита в зависимости от платформы, используемые во взаимодействии с нативным кодом и в низкоуровневом коде. System.Numerics.BigInteger хранит целые числа любого размера ценой скорости:
using System.Numerics;
BigInteger factorial = 1;
for (int i = 2; i <= 30; i++) factorial *= i;
// 265252859812191058636308480000000
Часто задаваемые вопросы
Какие типы данных есть в C#?
Встроенные типы: целые числа (sbyte, byte, short, ushort, int, uint, long, ulong), типы с плавающей точкой float и double, decimal для точной десятичной арифметики, char для символа UTF-16, bool и ссылочные типы string и object. Каждое ключевое слово это псевдоним типа .NET: int это System.Int32, string это System.String.
Что использовать для денег в C#, decimal или double?
Используйте decimal. double хранит двоичные дроби, поэтому значения вроде 0.1 нельзя представить точно, и суммы расходятся (0.1 + 0.2 == 0.3 даёт false). decimal хранит десятичные цифры, сохраняет от 28 до 29 значащих цифр и даёт результаты, которых ожидают бухгалтеры. double быстрее и имеет намного больший диапазон, поэтому его используют в науке, графике и измерениях.
Какое максимальное значение int в C#?
int.MaxValue равно 2 147 483 647, а int.MinValue равно -2 147 483 648 (32-битное знаковое целое). Прибавление 1 к int.MaxValue молча переходит в int.MinValue, если код не выполняется в контексте checked, где выбрасывается OverflowException. Для больших значений используйте long (примерно до 9,2 квинтиллиона).
Чем float отличается от double в C#?
float занимает 32 бита и даёт примерно от 6 до 9 значащих цифр; double занимает 64 бита и даёт примерно от 15 до 17. Литералы с плавающей точкой по умолчанию имеют тип double, поэтому для float нужен суффикс f: float speed = 2.5f;. Используйте double, если только память или API (например, Vector3 в Unity) не требуют float.
Чем типы значений отличаются от ссылочных типов в C#?
Переменная типа значения (int, double, bool, DateTime, любая struct) хранит сами данные, и присваивание их копирует. Переменная ссылочного типа (string, массивы, любой class) хранит ссылку на объект в куче, и присваивание копирует ссылку, поэтому две переменные могут указывать на один и тот же объект.