Поделиться через


Система типов 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#. Полный список встроенных типов см. в разделе "Встроенные типы".

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

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

Одним из первых решений, которые вы принимаете при определении типа, является определение конструкции, используемой для вашего типа. Следующий список помогает принять это первоначальное решение. В выборах есть пересечение. В большинстве случаев более одного варианта является разумным выбором.

  • Если размер хранилища данных мал, не более 64 байтов, выберите struct или record struct.
  • Если тип неизменяем или требуется неразрушительная мутация, выберите struct или record struct.
  • Если тип должен иметь семантику значений для равенства, выберите или record classrecord struct.
  • Если тип в основном используется для хранения данных, а не поведения, выберите record class или record struct.
  • Если тип является частью иерархии наследования, выберите record class или class.
  • Если тип использует полиморфизм, выберите тип class.
  • Если основное назначение — поведение, выберите class.

Общая система типов

Важно понимать две основные моменты системы типов в .NET:

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

На следующем рисунке показана связь между типами значений и ссылочными типами в CTS.

Снимок экрана, показывающий типы значений и ссылочные типы в CTS.

Замечание

Вы можете увидеть, что наиболее часто используемые типы организованы в System пространстве имен. Однако пространство имен, в котором содержится тип, не имеет отношения к типу значения или ссылочного типа.

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

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

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

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

Типы записей могут быть ссылочными типами (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#литеральные значения получают тип от компилятора. Можно указать, как следует вводить числовый литерал, добавив букву в конец числа. Например, чтобы указать, что значение 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);

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

Тип можно объявить с одним или несколькими параметрами типа , которые служат заполнителем для фактического типа ( конкретного типа). Клиентский код предоставляет конкретный тип при создании экземпляра типа. Такие типы называются универсальными типами. Например, тип System.Collections.Generic.List<T> .NET имеет один параметр типа, который по соглашению присваивается имени 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 слова. Переменная по-прежнему получает тип во время компиляции, но тип предоставляется компилятором. Дополнительные сведения см. в разделе Неявно типизированные локальные переменные.

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

Обычные типы значений не могут иметь значение 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#.