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

C# является строго типизированным языком. Каждая переменная и константа имеет тип, как и каждое выражение, результатом вычисления которого является значение. Каждое объявление метода задает имя, тип и вид (значение, ссылка или вывод) для каждого входного параметра и для возвращаемого значения. В библиотеке классов .NET определены встроенные числовые типы и комплексные типы, представляющие разнообразные конструкции. К ним относятся файловая система, сетевые подключения, коллекции и массивы объектов, а также даты. Обычная программа на C# использует типы из этой библиотеки классов и пользовательские типы, которые моделируют уникальные концепции конкретной сферы применения.

В типах может храниться следующая информация:

  • место, необходимое для хранения переменной этого типа;
  • максимальное и минимальное значения, которые могут быть представлены;
  • содержащиеся в типе члены (методы, поля, события и т. д.);
  • базовый тип, от которого наследует этот тип;
  • реализуемые им интерфейсы;
  • разрешенные виды операций.

Компилятор использует сведения о типах, чтобы проверить, все ли операции, выполняемые в коде, являются типобезопасными. Например, при объявлении переменной типа int компилятор позволяет в дополнение использовать переменную и операции вычитания. При попытке выполнить эти же операции для переменной типа bool компилятор выдаст ошибку, как показано в следующем примере:

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

Примечание

Тем, кто ранее использовал C и C++, нужно обратить внимание на то, что в C# тип bool нельзя преобразовать в int.

Компилятор внедряет сведения о типе в исполняемый файл в виде метаданных. Среда CLR использует эти метаданные во время выполнения для дальнейшего обеспечения безопасности типа при выделении и освобождении памяти.

Задание типов в объявлениях переменных

Когда вы объявляете в программе переменную или константу, для нее нужно задать тип либо использовать ключевое слово var, чтобы компилятор определил тип самостоятельно. В следующем примере показаны некоторые объявления переменных, использующие встроенные числовые типы и сложные пользовательские типы:

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
            where item <= limit
            select item;

Типы параметров и возвращаемых значений метода задаются в объявлении метода. Далее представлена сигнатура метода, который требует значение int в качестве входного аргумента и возвращает строку:

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };

После объявления переменной вы не можете повторно объявить ее с новым типом и назначить ей значение, несовместимое с объявленным типом. Например, нельзя объявить переменную типа int и затем присвоить ей логическое значение true. Но значения можно преобразовать в другие типы, например при сохранении в других переменных или передаче в качестве аргументов метода. Если преобразование типов не приводит к потере данных, оно выполняется компилятором автоматически. Для преобразования, которое может привести к потере данных, необходимо выполнить приведение в исходном коде.

Дополнительные сведения см. в разделе Приведение и преобразование типов.

Встроенные типы

C# предоставляет стандартный набор встроенных типов. Они используются для представления целых чисел, значений с плавающей запятой, логических выражений, текстовых символов, десятичных значений и других типов данных. Также существуют встроенные типы string и object. Такие типы доступны для использования в любой программе C#. Полный список встроенных типов см. в разделе Встроенные типы.

Пользовательские типы

Для создания собственных пользовательских типов используется structenumclassinterface, и record конструкции , а также конструкции. Сама библиотека классов .NET — это коллекция пользовательских типов, которые вы можете свободно использовать в приложениях. По умолчанию в любой программе C# доступны наиболее часто используемые типы из библиотеки классов. Чтобы сделать доступными другие типы, нужно явным образом добавить в проект ссылку на сборку, которая определяет их. Если компилятору предоставлена ссылка на сборку, то вы можете объявлять в коде переменные (и константы) любых типов, объявленных в этой сборке. См. дополнительные сведения о библиотеке классов .NET.

Система общих типов CTS

Важно понимать две основные вещи, касающиеся системы типов, используемой в .NET:

  • Она поддерживает принцип наследования. Типы могут быть производными от других типов, которые называются базовыми типами. Производный тип наследует все (с некоторыми ограничениями) методы, свойства и другие члены базового типа. Базовый тип, в свою очередь, может быть производным от какого-то другого типа, при этом производный тип наследует члены обоих базовых типов в иерархии наследования. Все типы, включая встроенные числовые типы, например System.Int32 (ключевое слово C#: int), в конечном счете являются производными от одного базового типа System.Object (ключевое слово C#: object). Эта унифицированная иерархия типов называется Системой общих типов CTS. Дополнительные сведения о наследовании в C# см. в статье Inheritance (Наследование).
  • Каждый тип в CTS определяется как тип значения либо ссылочный тип. Это справедливо и для всех пользовательских типов, в том числе включенных в библиотеку классов .NET или определенных вами. Если в определении типа используется ключевое слово struct, он является типом значения. Например, все встроенные числовые типы определены как structs. Если в определении типа используется ключевое слово class или record, он является ссылочным типом. Для ссылочных типов и типов значений используются разные правила компиляции, и они демонстрируют разное поведение во время выполнения.

Ниже показаны взаимоотношения между типами значения и ссылочными типами в CTS.

Screenshot that shows CTS value types and reference types.

Примечание

Как видно, все наиболее часто используемые типы организованы в пространство имен System. Но само по себе пространство имен, в котором размещен тип, никак не зависит от того, является ли он типом значения или ссылочным типом.

Классы и структуры являются двумя основными конструкциями системы общих типов CTS, используемой на платформе .NET. В C# 9 добавлены записи, которые представляют собой тип класса. Оба они являются структурами данных, которые инкапсулируют набор данных и поведений в одной логической сущности. Данные и поведение являются членами класса, структуры или записи. К ним относятся методы, свойства, события и другие элементы, которые описаны далее в этой статье.

Объявление класса, структуры или записи представляет собой своего рода чертеж, на основе которого создаются экземпляры или объекты во время выполнения. Если вы определите класс, структуру или запись с именем Person, то Person здесь обозначает имя типа. Если вы объявите и инициализируете переменную p типа Person, принято говорить, что p является объектом (или экземпляром) Person. Можно создать несколько экземпляров одного типа Person, и каждый экземпляр будет иметь разные значения свойств и полей.

Класс является ссылочным типом. Когда вы создаете объект типа и назначаете его переменной, эта переменная содержит только ссылку на память объекта. Если ссылка на объект сохраняется в новую переменную, эта переменная также ссылается на исходный объект. Изменения, внесенные через одну переменную, отражаются и в другой переменной, поскольку обе они ссылаются на одни и те же данные.

Структура (struct) является типом значения. При создании структуры переменная, которой присвоена структура, содержит фактические данные этой структуры. Если структура присваивается новой переменной, все данные копируются. Таким образом, новая переменная и исходная переменная содержат две отдельные копии одинаковых данных. Изменения, внесенные в одну копию, не влияют на другую.

Типы записей могут быть либо ссылочными типами (record class), либо типами значений (record struct).

Как правило, классы используются для моделирования более сложного поведения. Классы обычно хранят данные, которые должны быть изменены после создания объекта класса. Структуры лучше всего подходят для небольших структур данных. Структуры обычно хранят данные, которые не должны изменяться после создания структуры. Типы записей — это структуры данных с дополнительными членами, синтезированными компилятором. Записи обычно хранят данные, которые не должны изменяться после создания объекта.

Типы значений

Типы значений являются производными от System.ValueType, который является производным от System.Object. Типы, производные от System.ValueType, имеют особое поведение в среде CLR. Переменные типа значения непосредственно содержат их значения. Память для структуры выделяется встроенным образом в любом контексте, объявленном переменной. Для переменных типа значения не предусмотрены раздельное размещение в куче или накладные расходы при сборке мусора. Можно объявить типы record struct, которые являются типами значений, и включить синтезированные члены для записей.

Существует две категории типов значений: struct и enum.

Встроенные числовые типы являются структурами и имеют поля и методы, к которым можно обращаться.

// constant field on type byte.
byte b = byte.MaxValue;

Но объявление и присвоение значений вы выполняете для них так, как если бы они были простыми нестатистическими типами:

byte num = 0xA;
int i = 5;
char c = 'Z';

Типы значений являются запечатанными. Тип не может быть производным от любого типа значений, например System.Int32. Вы не можете определить структуру для наследования из любого определенного пользователем класса или структуры, так как структура может наследовать только от System.ValueType. Тем не менее структура может реализовывать один или несколько интерфейсов. Можно выполнить приведение типа структуры к любому типу интерфейса, который он реализует. Это приведет к операции упаковки-преобразования, которая создаст программу-оболочку для структуры внутри объекта ссылочного типа в управляемой куче. Операции упаковки-преобразования выполняются при передаче типа значения в метод, принимающий System.Object или любой тип интерфейса в качестве входного параметра. Дополнительные сведения см. в разделе Упаковка-преобразование и распаковка-преобразование.

Используйте ключевое слово struct, чтобы создать собственные пользовательские типы значений. Как правило, структура используется как контейнер для небольшого набора связанных переменных, как показано в следующем примере:

public struct Coords
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

См. сведения в описании типов структур. См. дополнительные сведения о типах значений.

Еще одна категория типов значений — это enum. Перечисление определяет набор именованных целочисленных констант. Например, перечисление System.IO.FileMode из библиотеки классов .NET содержит набор именованных целочисленных констант, которые определяют правила открытия файла. В следующем примере представлено определение этого типа:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

Константа System.IO.FileMode.Create имеет значение 2. Так как имена намного лучше воспринимаются человеком при изучении исходного кода, мы рекомендуем всегда использовать перечисления вместо литеральных числовых констант. Для получения дополнительной информации см. System.IO.FileMode.

Все перечисления наследуют от System.Enum, который наследует от System.ValueType. К перечислениям применимы все те же правила, что к структурам. Дополнительные сведения о перечислениях см. в разделе Типы перечислений.

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

Тип, который определен как class, record, delegate, массив или interface, является reference type.

Объявляемая переменная с типом reference type будет содержать значение null до тех пор, пока вы не назначите ей экземпляр такого типа или не создадите его с помощью оператора new. Создание и назначение класса демонстрируется в следующем примере:

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

Вы не можете создать экземпляр interface напрямую с помощью оператора new. Вместо этого создайте и назначьте экземпляр класса, который реализует интерфейс. Рассмотрим следующий пример.

MyClass myClass = new MyClass();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

При создании объекта выделяется память в управляемой куче, и переменная хранит только ссылку на расположение объекта. Хранение типов в управляемой куче требует дополнительных действий как при выделении памяти, так и при удалении, которое выполняется функцией автоматического управления памятью в среде CLR, известной как сборка мусора. Сборка мусора является хорошо оптимизированным процессом и в большинстве случаев не ухудшает производительность. Дополнительные сведения о сборке мусора см. в статье Автоматическое управление памятью.

Все массивы являются ссылочными типами, даже если их элементы являются типами значений. Массивы являются неявно производными от класса System.Array, но в C# их можно объявлять и использовать с упрощенным синтаксисом, как показано в следующем примере:

// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };

// Access an instance property of System.Array.
int len = nums.Length;

Ссылочные типы полностью поддерживают наследование. Создаваемый класс может наследовать от любого другого интерфейса или класса, который не определен как запечатанный, а другие классы могут наследовать от этого класса и переопределять его виртуальные методы. Дополнительные сведения о создании собственных классов см. в статье Классы, структуры и записи. Дополнительные сведения о наследовании в C# и виртуальных методах см. в статье Inheritance (Наследование).

Типы литеральных значений

В C# литеральные значения получают тип от компилятора. Вы можете указать способ типизации числового литерала, добавив букву в конце числа. Например, чтобы значение 4.56 рассматривалось как значение float, добавьте после этого числа букву "f" или "F": 4.56f. Если буква отсутствует, компилятор самостоятельно выберет тип для литерала. Дополнительные сведения о том, какие типы могут быть указаны с буквенными суффиксами, см. в разделах Целочисленные числовые типы и Числовые типы с плавающей запятой.

Так как литералы являются типизированными и все типы в конечном счете являются производными от System.Object, можно написать и скомпилировать следующий код:

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

Универсальные типы

Тип может быть объявлен с одним или несколькими параметрами типавместо фактического типа (конкретного типа), который клиентский код предоставит при создании экземпляра этого типа. Такие типы называются универсальными типами. Например, тип .NET System.Collections.Generic.List<T> имеет один параметр типа, которому в соответствии с соглашением присвоено имя T. При создании экземпляра этого типа необходимо указать тип объектов, которые будут содержаться в списке, например string:

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

Использование параметра типа позволяет повторно использовать один же класс для хранения элементов любого типа, не преобразовывая каждый элемент в объект. Универсальные классы коллекций называются строго типизированными коллекциями, так как компилятору известен конкретный тип элементов коллекции и он может выдать ошибку во время компиляции, если, к примеру, вы попытаетесь добавить целое число в объект stringList, созданный в предыдущем примере. Дополнительные сведения см. в статье Универсальные шаблоны.

Неявные типы, анонимные типы и типы, допускающие значение NULL

Вы можете неявно типизировать локальную переменную (но не элементы класса) с помощью ключевого слова var. Такая переменная получает конкретный тип во время компиляции, но этот тип предоставляется компилятором. Дополнительные сведения см. в статье Implicitly Typed Local Variables (Неявно типизированные локальные переменные).

Иногда нет смысла создавать именованный тип для простых наборов связанных значений, которые не будут сохраняться или передаваться за пределами метода. В таких случаях можно применить анонимные типы. Дополнительные сведения см. в статье Анонимные типы.

Обычные типы значений не могут иметь значение null. Но вы можете создать специальные типы, допускающие значения NULL, добавив символ ? после имени типа. Например, тип int? является типом int, который может иметь значение null. Типы, допускающие значение NULL, представляют собой экземпляры универсального типа структуры System.Nullable<T>. Типы, допускающие значение NULL, особенно полезны при передаче данных в базы данных, где могут использоваться числовые значения null, и из таких баз данных. Дополнительные сведения см. в разделе Типы, допускающие значение NULL.

Тип времени компиляции и тип времени выполнения

Переменная может иметь разные типы времени компиляции и времени выполнения. Тип времени компиляции является объявленным или выведенным типом переменной в исходном коде. Тип времени выполнения является типом экземпляра, на который ссылается такая переменная. Часто эти два типа совпадают:

string message = "This is a string of characters";

В других случаях тип времени компиляции отличается, как показано в следующих двух примерах:

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

В обоих предыдущих примерах используется тип времени выполнения string. Тип времени компиляции в первой строке — object, а во второй — IEnumerable<char>.

Если два типа переменной отличаются, важно понимать, когда применяются типы времени компиляции и времени выполнения. Тип времени компиляции определяет все действия, выполняемые компилятором. Такие действия компилятора включают разрешение вызовов методов, разрешение перегрузки, а также доступные неявные и явные приведения. Тип времени выполнения определяет все действия, разрешаемые при выполнении. Такие действия времени выполнения включают отправку вызовов виртуальных методов, оценку выражений is и switch, а также другие API тестирования типов. Чтобы лучше понять, как ваш код взаимодействует с типами, определите, какие действия применяются к каждому типу.

Дополнительные сведения см. в следующих статьях:

Спецификация языка C#

Дополнительные сведения см. в спецификации языка C#. Спецификация языка является предписывающим источником информации о синтаксисе и использовании языка C#.