Данный урок посвящен типам в C#, в нем мы познакомимся с понятиями ссылочный тип данных и тип-значение, Nullable-типами, возможностями в части динамической типизации и системой типов языка C#.
- Языки программирования и типы данных
- Общая система типов (CTS)
- Объявление и инициализация переменных
- Ссылочные типы данных и типы-значения в C#
- Nullable-типы (нулевые типы) и операция ??
- Ключевое слово dynamic
- Оператор default
Исходный код примеров из этой статьи можете скачать из нашего github-репозитория.
Языки программирования и типы данных
В зависимости от принятой системы типов и способов с ней работать различают языки программирования:
- со статической и динамической типизацией;
- с сильной и слабой типизацией;
- с явной и неявной типизацией.
В языках со статической типизацией тип переменной выводится на этапе компиляции и в случае, если эта операция не может быть выполнена, то процесс компиляции не будет завершен. Динамическая типизация предполагает определение типа переменной во время выполнения программы, такой подход чаще всего встречается среди интерпретируемых языков.
Примеры языков со статической типизацией: C#, Java, C, C++, код объявления переменной выглядит так:
double value = 0.123;
Примеры языков с динамической типизацией: Python, PHP. Пример кода на Python:
value = 0.123
В языках с сильной типизацией операции над значениями и присваивания можно производить только над переменными одного типа. Иногда это приведение выполняется автоматически, например:
int v1 = 4; double v2 = v1 + 0.123;
В этом случае, при выполнении второй строки в первую очередь будет выполнено приведение переменной v1 к типу double, а потом сложение. Но следующий код вызовет ошибку:
double v2 = "4" + 0.123;
Так как “4” – это значение строкового типа, а 0.123 имеет тип double.
К языкам с сильной типизацией относятся C#, Java, Python.
В языках со слабой типизацией таких ограничений нет, например, на C вы можете написать следующее:
char * str = "hello"; double value = str + 0.123;
В результате код скомпилируется, если его запустить, то в переменной double будет лежать численное значение.
Языки со слабой типизацией – это C, C++.
Явная типизация предполагает явное указание типа переменной:
int value = 1;
В этом примере, мы объявляем переменную value типа int и явно это указываем.
В языке с неявной типизацией этого делать не нужно, пример на Python:
value = 1
Какой тип типизации, явный или неявный, используется не зависит от того компилируемый язык или интерпретируемый, это определяется дизайном языка. Например в C# вы можете использовать оба подхода, в первом случае код будет выглядеть так:
double value = 0.123;
Во втором так:
var value = 0.123;
Общая система типов (CTS)
Под типом в C# понимается: класс, интерфейс, структура, перечисление и делегат. При разработке программного обеспечения на этом языке, фактически вы будет создавать и организовывать определенным образом взаимодействие между различными типами данных. Так как .NET – это платформа, под которую можно разрабатывать на разных языках, то существует так называемая общая система типов CTS (Common Type System), которая определяет как должны быть описаны типы, чтобы вы могли с ними работать в других языках, то есть как они должны быть представлены в CLR. CLR – это аббревиатура от Common Language Runtime – общеязыковая исполняющая среда, она отвечает за обнаружение, загрузку и управление типами, управляет памятью, отвечает за безопасность, обеспечивает работу многопоточных приложений и т.п.
Объявление и инициализация переменных
В общем случае при объявлении переменной в C#, вначале указывается тип данных переменной, затем ее имя:
int nVal; string strVal;
Задание значения переменной можно произвести в момент инициализации:
int radius = 10; string name = "John";
либо после инициализаций:
string name; name = "John";
Необходимо иметь ввиду, что переменную нельзя использовать до тех пор пока она не будет проинициализирована, Например, выполнение следующей программы завершится с ошибкой:
int notInitedVal; Console.Write(notInitedVal);
В примерах мы не будем приводить код импорта и объявления класса. В конце главы будет приведен листинг программы со всеми примерами из данного урока.
Ключевое слово new
Ключевое слово new, как правило, используется при инициализации переменных, которые имеют ссылочный тип данных. О том, что это такое мы расскажем чуть ниже. Пусть у нас есть класс Rectangle:
class Rectangle { public double Width = 0; public double Height = 0; }
Данный класс нам нужен только для демонстрации, при разработке собственных классов не стоит создать поля с ключевым словом public. О создании классов и основах объектно-ориентированного программирования будет рассказано в одном из ближайших уроков.
Создадим переменную класса Rectangle:
Rectangle rect = new Rectangle(); Console.WriteLine($"Rectangle Width={rect.Width}, Height={rect.Height}");
Переменные типа int, double и т.п. также можно проинициализировать с помощью ключевого слова new, в этом случае будет присвоено значение по умолчанию:
int newInitedValue = new int(); Console.WriteLine("Default int value: " + newInitedValue);
Ключевое слово var. Неявная типизация
При объявлении переменной вместо явного задания типа можно поставить ключевое слово var. В этом случае будет использована система вывода типов для определения типа переменной по ее значению.
int v1 = 12345; var v2 = 12345; Console.WriteLine($"Type of v1: {v1.GetType()}\nType of v2: {v2.GetType()}");
При работе с var необходимо помнить следующее:
- использовать var можно только для объявления локальных переменных;
- var нельзя использоваться для объявления типа возвращаемого значения, типов полей и параметров;
- при объявлении переменной с использованием var она обязательно должна быть проинициализирована, при этом использовать для этого null запрещено;
- объявлять переменную допускающую null-значение с использованием лексемы ? через var нельзя.
Ссылочные типы данных и типы-значения в C#
Типы данных в C# можно разделить на типы-значения (value type) и ссылочные типы (reference type). В первую очередь они отличаются схемой наследования, местом размещения и представлением.
Для начал стоит сказать о том, что такое стек и управляемая куча. Стек – это область памяти процесса, особенность которой состоит в том, что участки из нее выделяются по принципу LIFO (last in – first out, последним пришёл – первым вышел), аналогией является стопка тарелок, в которой, для того чтобы добраться до какой-то из них, вам нужно снять сверху все те, что мешают это сделать. Данные из стека удаляются предсказуемым образом, например, после завершения работы функции, все что было размещено в стеке в процессе ее работы будет уничтожено. Куча – это также область памяти процесса, но в отличии от стека, в ней нет жесткой структуры хранения, за уничтожение объектов, которые в ней размещены, в C# отвечает сборщик мусора (garbage collector), в таких языках как C / C++ за этим должен следить сам разработчик.
Общая диаграмма наследования типов в C# (не полная) представлена на рисунке ниже.
Типы значения
Переменные типа-значения располагаются в стеке, что позволяет их быстро создавать и уничтожать. Фактически время жизни такой переменной определяется контекстом, в которой она объявлена. Сама переменная представляется в виде локальной копии. Типы-значения являются классами наследниками от System.ValueType, который, в свою очередь, наследуется от System.Object. К типам-значениям относятся: простые типы, типы перечисления, типы структур, типы значений, допускающие NULL, типы значений кортежей. Далее, обзорно будут рассмотрены указанные выше типы.
Простые типы
К простым типа относятся:
- Целочисленные типы;
- Типы с плавающей точкой;
- Тип bool для представления логических значений;
- Тип char для представления символьных значений.
Целочисленные типы
Для простых типов в C# есть зарезервированные слова, которые позволяют не указывать тип данных как класс .NET. Ниже представлена таблица соответствия ключевых слов C#, целочисленных типов .NET, их диапазона и размеров.
Тип C# | Тип .NET | Диапазон | Описание |
sbyte | System.SByte | От -128 до 127 | 8-ми разрядное целое число со знаком |
byte | System.SByte | От 0 до 255 | 8-ми разрядное целое число без знака |
short | System.Int16 | От -32 768 до 32 767 | 16-разрядное целое число со знаком |
ushort | System.UInt16 | От 0 до 65 535 | 16-разрядное целое число без знака |
int | System.Int32 | От -2 147 483 648 до 2 147 483 647 | 32-разрядное целое число со знаком |
uint | System.UInt32 | От 0 до 4 294 967 295 | 32-разрядное целое число без знака |
long | System.Int64 | От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 | 64-разрядное целое число со знаком |
ulong | System.UInt64 | От 0 до 18 446 744 073 709 551 615 | 64-разрядное целое число без знака |
Примеры работы с целыми числами:
int n1 = 1000; System.Int32 n2 = 1000;
Для явного указания, что число имеет тип long или ulong необходимо добавить суффикс L или l для long и UL и все возможные комбинации регистров эти символов для ulong.
var l1 = 1000L; // число типа long var l2 = 1000UL; // число типа ulong
Число может быть представлено в десятичном, шестнадцатеричном и двоичном виде:
int nDec = 456; // десятичный формат int nHex = 0x1C8; // шестнадцатеричный формат: префикс 0x или 0X int nBin = 0b_1101; // двоичный формат: префикс 0b или 0B
Для всех целочисленных простых типов значение по умолчанию: 0, оно присваивается переменной при инициализации с помощью ключевого слова new.
Типы с плавающей точкой
Сводная таблица с типами с плавающей точкой:
Тип C# | Тип .NET | Точность | Описание |
float | System.Single | 6–9 цифр | 4-х байтовое число |
double | System.Double | 15–17 цифр | 8-ми байтовое число |
decimal | System.Decimal | 28-29 знаков | 16-ти байтовое число |
Для явного указания типа данных при записи числа, можно использовать литералы:
float f1 = 0.123f; // тип данных float, литеры f или F double d1 = 0.123d; // тип данных double, литеры d или D либо без литеры decimal dd1 = 0.123m; // тип данных decimal, литеры m или M
Для чисел с плавающей точкой значение по умолчанию: 0.0 с соответствующей литерой в конце.
Тип bool
Тип bool является типом C#, который представляется как System.Boolean в .NET. Может принимать одно из двух значений true или false.
bool b1 = false; System.Boolean b2 = true;
Значение по умолчанию для типа bool: false.
Тип char
Тип char является ключевым словом для обозначения типа System.Char, который служит для представления символьных значений, занимает два байта в памяти.
Переменной типа char можно задать значение:
- в виде символа:
char c1 = 'a';
- escape-последовательности Юникода (начинаются с префикса \u):
char c2 = '\u0026';
- шестнадцатеричной escape-последовательности (начинается с префикса \x):
char c3 = '\x0026';
- через приведение типа:
char c4 = (char)38;
Значением по умолчанию для char: ‘\u0000′.
Типы перечисления (enum)
Перечисления являются набором целочисленных именованных констант. Переменные типа перечисления создаются с помощью ключевого слово enum, после которого следует имя типа и набор значений в фигурных скобках. Создадим enum для представления дней недели:
enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday };
Каждому значению перечисления соответствует целое число.
Пример создания переменной типа Day:
Day day1 = Day.Monday; var day2 = Day.Tuesday; Console.WriteLine($"day1={day1}, day2={day2}");
Более подробно про работу с enum будет рассказано в одном из следующих уроков.
Типы структур
Структуры по своей внутренней организации похожи на классы, они содержат набор полей и методов. Как правило, их используют для объявления типов, которые определяются только значениями полей и не имеют индивидуальности. Например, объекты, описывающие транзакции, несмотря на то, что значения их полей могут совпадать не будут тождественными, то есть нам их нужно уметь различать несмотря на внешнее сходство. А точки на геометрической плоскости, которые задаются двумя координатами, такой индивидуальности не имеют, и если координаты двух точек совпадают, то это значит, что речь идет об одной и той же точке. Именно для таких типов хорошо подходят структуры. Для их объявления используется ключевое слово struct:
struct Point { public Point(double x, double y) { X = x; Y = y; } public double X {get;} public double Y {get;} } Point p1 = new Point(1,2); Console.WriteLine($”({p1.X}, (p1.Y})”);
Типы значений, допускающие null
Про типы значений, допускающих null см. ниже “Nullable-типы (нулевые типы) и операция ??”.
Типы значений кортежей
Кортежи используются для группировки данных, которые могут иметь разные типы в единую именованную сущность. Они являются объектами типа System.ValueTuple. Объявим кортеж, состоящий из двух элементов типа double:
(double, double) tp1 = (1.0, 2.0); // явное задание типов элементов кортежа var tp2 = (8.1, 4.3); // использование var для объявления кортежа
Поля кортежа могут быть именованными:
(double X, double Y) tp3 = (3.2, 5.34); var tp4 = (X: 1.2, Y: 3.4); var X = 5.6; var Y = 7.8; var tp5 = (X, Y);
Более подробно про кортежи типов System.ValueTuple (тип-значение) и System.Tuple (ссылочный тип) будет рассказано в одном из следующих уроков.
Ссылочные типы
Переменные ссылочного типа располагаются в куче, за их уничтожение отвечает сборщик мусора, поэтому про них нельзя точно сказать, когда занимаемая ими память будет освобождена. Переменная представляется в виде ссылки на соответствующее место в куче. Ссылочные типы являются наследниками от System.Object.
Типы классов
Классы являются наиболее фундаментальным элементов в системе типов C#. Тип System.Object, который является родительским для всех типов данных представляет собой класс. Из рассмотренных выше типов данных, класс больше всего похож на структуру, у них даже объявление похожи, только вместо ключевого слова struct нужно использовать class.
class Persone { public Persone(string name, int age) { Name = name; Age = age; } public string Name {get;set;} public int Age {get;set;} } Persone persone1 = new Persone("John", 21); Console.WriteLine($"Persone: Name: {persone1.Name}, Age: {persone1.Age})");
Среди классов в C# можно выделить ряд классов, которые играю важную роль в языке, они перечислены в таблице ниже.
Класс | Описание |
System.Object | Базовый класс для всех типов в C# |
System.ValueType | Базовый класс для всех типов-значений |
System.Enum | Базовый класс для всех перечислений |
System.Array | Базовый класс для всех массивов |
System.Delegate | Базовый класс для всех делегатов |
System.Exception | Базовый класс для всех исключений |
System.String | Класс, определяющий строкой тип данных |
Типы интерфейсов
Интерфейс представляет собой набор методов, свойств, событий и индексаторов. До версии C# 8.0 интерфейс предполагал только декларацию (объявление) указанных выше элементов, начиная с 8.0, в рамках интерфейса можно располагать реализацию по умолчанию. Фактически интерфейс представляет собой контракт, а класс, который от него наследуюется, реализует этот контракт.
Создадим интерфейс для описания человека, у которого есть два свойства имя: Name, и возраст: Age:
interface IPersone { string Name {get;set;} int Age {get;set;} }
Изменим объявление класса Persone, так, чтобы он представлял реализацию интерфейса IPersone:
class Persone: IPersone { //… }
Объявим переменную типа IPersone:
IPersone persone2 = new Persone("Jim", 25); Console.WriteLine($"Persone: Name: {persone2.Name}, Age: {persone2.Age})");
Более подробно про интерфейсы будет рассказано в одном из следующих уроков.
Типы массивов
Массив – это структура данных, которая позволяет хранить один или более элементов. Массивы в C# делятся на одномерные и многомерные, среди последних наибольшее распространение получили двумерные массивы. Все массивы являются наследниками класса System.Array.
Создание и инициализация одномерного массива:
int[] nArr1 = new int[5]; nArr1[0] = 0; nArr1[1] = 1; nArr1[2] = 2; nArr1[3] = 3; nArr1[4] = 4;
Пример прямоугольного массива, в нем строки имеют одинаковую длину:
int[,] nMx = new int[2,2]; // прямоугольный массив nMx[0,0]=0; nMx[0,1]=1; nMx[1,0]=2; nMx[1,1]=3;
Пример зубчатого (jagged) массива, в нем строки могут иметь разную длину:
int[][] jg = new int[2][]; // зубчатый массив jg[0] = new int[3]; jg[1] = new int[1];
Более подробно про массивы будет рассказано в одном из следующих уроков.
Типы делегатов
Делегаты являются аналогом указателей на функции из языков C / C++. Они используются в случаях, когда нужно передать некоторую функциональность как аргумент, перенаправлять вызовы и т.д.
Nullable-типы (нулевые типы) и операция ??
Объявление и инициализация Nullable-переменных
В работе с типами-значениями есть одна особенность, они не могут иметь значение null. При наличии любой из следующих строк кода, компиляция программы не будет выполнена:
int nv = null; bool bv = null;
На практике, особенно при работе с базами данных, может возникнуть ситуация, когда в записи из таблицы пропущены несколько столбцов (нет данных), в этом случае, соответствующей переменной нужно будет присвоить значение null, но она может иметь тип int или double, что приведет к ошибке.
Можно объявить переменную с использованием символа ? после указания типа, тогда она станет nullable-переменной – переменной поддерживающей null-значение:
int? nv1 = null; bool? bv1 = null;
Использование символа ? является синтаксическим сахаром для конструкции Nullable<T>, где T – это имя типа. Представленные выше примеры можно переписать так:
Nullable<int> nv1 = null; Nullable<bool> bv1 = null;
Проверка на null. Работа с HasValue и Value
Для того чтобы проверить, что переменная имеет значение null можно воспользоваться оператором is с шаблоном типа:
bool? flagA = true; if(flagA is bool valueOfFlag) { Console.WriteLine("flagA is not null, value: {valueOfFlag}"); }
Также можно воспользоваться свойствами класса Nullable:
- Nullable<T>.HasValue
- Возвращает true если переменная имеет значение базового типа. То есть если она не null.
- Nullable<T>.Value
- Возвращает значение переменной если HasValue равно true, иначе выбрасывает исключение InvalidOperationException.
bool? flagB = false; if(flagB.HasValue) { Console.WriteLine("flagB is not null, value: {flagB.Value}"); }
Приведение Nullable-переменной к базовому типу
При работе с Nullable-переменными их нельзя напрямую присваивать переменным базового типа. Следующий код не будет скомпилирован:
double? nvd1 = 12.3; double nvd2 = nvd1; // error
Для приведения Nullable-переменной к базовому типу можно воспользоваться явным приведением:
double nvd3 = (double) nvd1;
В этом случае следует помнить, что если значение Nullable-переменной равно null, то при выполнении данной операции будет выброшено исключение InvalidOperationException.
Второй вариант – это использование оператора ??, при этом нужно дополнительно задаться значением, которое будет присвоено переменной базового типа если в исходной лежит значение null:
double nvd4 = nvd1 ?? 0.0; Console.WriteLine(nvd4); bool? nvb1 = null; bool nvb2 = nvb1 ?? false; Console.WriteLine(nvb1); Console.WriteLine(nvb2);
Второй вариант позволяет более лаконично обрабатывать ситуацию, когда вызов какого-то метода может возвращать null, а результат его работы нужно присвоить типу-значению, при этом заранее известно, какое значение нужно присвоить переменной в этой ситуации:
static int? GetValue(bool flag) { if (flag == true) return 1000; else return null; } static void Main(string[] args) { int test1 = GetValue(true) ?? 123; Console.WriteLine(test1); int test2 = GetValue(false) ?? 123; Console.WriteLine(test2); }
Ключевое слово dynamic
Вначале статьи мы говорили о том, что есть языки со статической и динамической типизацией, C# – язык со статической типизацией, т.е. типы переменных определяются на этапе компиляции. Но в рамках платформы .NET есть возможность работать с Python и Ruby в реализациях IronPython и IronRuby, но это языки с динамической типизацией, в них тип определяется во время выполнения программы. Для того чтобы можно было в C# проекте работать с тем, что было создано в рамках IronPython (или IronRuby) начиная с C# 4, в языке появилось ключевое слово dynamic и среда DLR (Dynamic Language Runtime), благодаря которой можно создавать динамические объекты, тип которых будет определен на этапе выполнения программы, а не в процессе компиляции.
С помощью ключевого слова dynamic объявляются переменные, для которых нужно опустить проверку типов в процессе компиляции. Для этой переменной не производится присвоение типа из BCL (Base Class Library) – стандартной библиотеки классов .NET, фактически dynamic – это тип System.Object с дополнительным набором метаданных, они нужны для определения типа переменной в процессе выполнения (так называемое, позднее связывание).
Ниже приведены несколько примеров, на которых можно разобраться с тем, как работать с dynamic:
// Создадим переменную типа dynamic и проинициализируем ее double значением dynamic dval1 = 12.3; // Посмотрим на ее значение и тип Console.WriteLine($"Value: {dval1}"); Console.WriteLine($"Type: {typeof(dval1)}")); // Изменим значение переменной: dval1 += 17; Console.WriteLine($"Value: {dval1}"); Console.WriteLine($"Type: {typeof(dval1)}")); // Присвоим переменной значение другого типа: bool dval1 = true; // Посмотрим на ее значение и тип Console.WriteLine($"Value: {dval1}"); Console.WriteLine($"Type: {typeof(dval1)}"));
Как вы можете видеть значение и тип переменной dval1 менялись в процессе выполнения программы. При этом нужно помнить, что если вы присвоили переменной dynamic, какое-то значение, которое определило ее тип, а пытаетесь с ней работать как с переменной другого типа, то будет вызвано исключение:
dynamic dval2 = "hello"; // в переменной dval2 хранится строковое значение Console.WriteLine($"Value: {dval2}"); Console.WriteLine($"Type: {typeof(dval2)}"); dval2 = 123; // теперь значение типа int dval2 = dval2.ToUpper() // попытка вызвать на ней .ToUpper() приведет к ошибке
Оператор default
Оператор default создает значение по умолчанию для указанного типа, используется оно следующим образом: default(T), где T – это тип, для которого нужно создать соответствующее значение.
Объявим переменную типа int и присвоим ей значение по умолчанию с помощью new:
int n3 = new int(); Console.WriteLine($"Default int value: {n3}");
Тоже самое можно сделать с помощью оператора default:
int n4 = default(int); Console.WriteLine($"Value of int that inited by default(T): {n4}");
Если C# может самостоятельно вывести тип, то можно воспользоваться не оператором, а литерой default, без явного указания типа:
int n5 = default; Console.WriteLine($"Value of int that inited by default: {n5}");
Данный оператор полезен при разработке методов с обобщенным типом. Создадим метод, который выводит на консоль значение по умолчанию для типа переданного в нее аргумента:
static void PrintDefaultValue<T>(T val) { Console.WriteLine($"Type of val: {val.GetType()}, default value: {default(T)}, current value: {val}"); }
Вызовем эту функцию:
static void Main(string[] args) { PrintDefaultValue<int>(5); PrintDefaultValue<bool>(true); }
Исходный код примеров из этой статьи можете скачать из нашего github-репозитория.
“Задание значения переменной можно произвести в момент инициализации:
int radius = 10;
string name = “John”;
“Задание значения переменной можно произвести в момент инициализации” – Присвоение значения ранее объявленной переменной и есть инициализация.
“либо после инициализаций: – *Либо после объявления, инициализация происходит во второй строке
1 string name;
2 name = “John”;”