Перейти к содержимому

Зачем нужна упаковка в си шарп

  • автор:

Зачем нужна упаковка в си шарп

Кроме обычных типов фреймворк .NET также поддерживает обобщенные типы (generics), а также создание обобщенных методов. Чтобы разобраться в особенности данного явления, сначала посмотрим на проблему, которая могла возникнуть до появления обобщенных типов. Посмотрим на примере. Допустим, мы определяем класс для хранения данных пользователя:

class Person < public int Id < get;>public string Name < get;>public Person(int id, string name) < Name = name; >>

Класс Person определяет два свойства: Id — уникальный идентификатор пользователя и Name — имя пользователя.

Здесь идентификатор пользователя задан как числовое значение, то есть это будут значения 1, 2, 3, 4 и так далее.

Однако также нередко для идентификатора используются и строковые значения. И у числовых, и у строковых значений есть свои плюсы и минусы. И на момент написания класса мы можем точно не знать, что лучше выбрать для хранения идентификатора — строки или числа. Либо, возможно, этот класс будет использоваться другими разработчиками, которые могут иметь свое мнение по данной проблеме, например, они могут для представления идентификатора создать специальный класс.

И на первый взгляд, чтобы выйти из подобной ситуации, мы можем определить свойство Id как свойство типа object. Так как тип object является универсальным типом, от которого наследуется все типы, соответственно в свойствах подобного типа мы можем сохранить и строки, и числа:

class Person < public object Id < get;>public string Name < get;>public Person(object id, string name) < Name = name; >>

Затем этот класс можно было использовать для создания пользователей в программе:

Person tom = new Person(546, "Tom"); Person bob = new Person("abc123", "Bob"); int tomId = (int)tom.Id; string bobId = (string) bob.Id; Console.WriteLine(tomId); // 546 Console.WriteLine(bobId); // abc123

Все вроде замечательно работает, но такое решение является не очень оптимальным. Дело в том, что в данном случае мы сталкиваемся с такими явлениями как упаковка (boxing) и распаковка (unboxing) .

Так, при передаче в конструктор значения типа int, происходит упаковка этого значения в тип Object:

Person tom = new Person(546, "Tom"); // упаковка в значения int в тип Object

Чтобы обратно получить данные в переменную типов int, необходимо выполнить распаковку:

int tomId = (int)tom.Id; // Распаковка в тип int

Упаковка (boxing) предполагает преобразование объекта значимого типа (например, типа int) к типу object. При упаковке общеязыковая среда CLR обертывает значение в объект типа System.Object и сохраняет его в управляемой куче (хипе). Распаковка (unboxing), наоборот, предполагает преобразование объекта типа object к значимому типу. Упаковка и распаковка ведут к снижению производительности, так как системе надо осуществить необходимые преобразования.

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

Person tom = new Person(546, "Tom"); string tomId = (string)tom.Id; // !Ошибка - Исключение InvalidCastException Console.WriteLine(tomId); // 546

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

Для решения этих проблем в язык C# была добавлена поддержка обобщенных типов (также часто называют универсальными типами). Обобщенные типы позволяют указать конкретный тип, который будет использоваться. Поэтому определим класс Person как обощенный:

class Person  < public T Id < get; set; >public string Name < get; set; >public Person(T id, string name) < Name = name; >>

Угловые скобки в описании class Person указывают, что класс является обобщенным, а тип T, заключенный в угловые скобки, будет использоваться этим классом. Необязательно использовать именно букву T, это может быть и любая другая буква или набор символов. Причем сейчас на этапе написания кода нам неизвестно, что это будет за тип, это может быть любой тип. Поэтому параметр T в угловых скобках еще называется универсальным параметром , так как вместо него можно подставить любой тип.

Например, вместо параметра T можно использовать объект int, то есть число, представляющее номер пользователя. Это также может быть объект string, либо или любой другой класс или структура:

Person tom = new Person(546, "Tom"); // упаковка не нужна Person bob = new Person("abc123", "Bob"); int tomId = tom.Id; // распаковка не нужна string bobId = bob.Id; // преобразование типов не нужно Console.WriteLine(tomId); // 546 Console.WriteLine(bobId); // abc123

Поскольку класс Person является обобщенным, то при определении переменной после названия типа в угловых скобках необходимо указать тот тип, который будет использоваться вместо универсального параметра T. В данном случае объекты Person типизируются типами int и string :

Person tom = new Person(546, "Tom"); // упаковка не нужна Person bob = new Person("abc123", "Bob");

Поэтому у первого объекта tom свойство Id будет иметь тип int, а у объекта bob — тип string. И в случае с типом int упаковки происходить не будет.

При попытке передать для параметра id значение другого типа мы получим ошибку компиляции:

Person tom = new Person("546", "Tom"); // ошибка компиляции

А при получении значения из Id нам больше не потребуется операция приведения типов и распаковка тоже применяться не будет:

int tomId = tom.Id; // распаковка не нужна

Тем самым мы избежим проблем с типобезопасностью. Таким образом, используя обобщенный вариант класса, мы снижаем время на выполнение и количество потенциальных ошибок.

При этом универсальный параметр также может представлять обобщенный тип:

// класс компании class Company  < public P CEO < get; set; >// президент компании public Company(P ceo) < CEO = ceo; >> class Person  < public T Id < get;>public string Name < get;>public Person(T id, string name) < Name = name; >>

Здесь класс компании определяет свойство CEO, которое хранит президента компании. И мы можем передать для этого свойства значение типа Person, типизированного каким-нибудь типом:

Person tom = new Person(546, "Tom"); Company microsoft = new Company(tom); Console.WriteLine(microsoft.CEO.Id); // 546 Console.WriteLine(microsoft.CEO.Name); // Tom

Статические поля обобщенных классов

При типизации обобщенного класса определенным типом будет создаваться свой набор статических членов. Например, в классе Person определено следующее статическое поле:

class Person  < public static T? code; public T Id < get; set; >public string Name < get; set; >public Person(T id, string name) < Name = name; >>

Теперь типизируем класс двумя типами int и string:

Person tom = new Person(546, "Tom"); Person.code = 1234; Person bob = new Person("abc", "Bob"); Person.code = "meta"; Console.WriteLine(Person.code); // 1234 Console.WriteLine(Person.code); // meta

В итоге для Person и для Person будет создана своя переменная code.

Использование нескольких универсальных параметров

Обобщения могут использовать несколько универсальных параметров одновременно, которые могут представлять одинаковые или различные типы:

class Person  < public T Id < get;>public K Password < get; set; >public string Name < get;>public Person(T id, K password, string name) < Name = name; Password = password; >>

Здесь класс Person использует два универсальных параметра: один параметр для идентификатора, другой параметр — для свойства-пароля. Применим данный класс:

Person tom = new Person(546, "qwerty", "Tom"); Console.WriteLine(tom.Id); // 546 Console.WriteLine(tom.Password);// qwerty

Здесь объект Person типизируется типами int и string. То есть в качестве универсального параметра T используется тип int , а для параметра K — тип string .

Обобщенные методы

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

int x = 7; int y = 25; Swap(ref x, ref y); // или так Swap(ref x, ref y); Console.WriteLine($»x= y=»); // x=25 y=7 string s1 = «hello»; string s2 = «bye»; Swap(ref s1, ref s2); // или так Swap(ref s1, ref s2); Console.WriteLine($»s1= s2=»); // s1=bye s2=hello void Swap(ref T x, ref T y)

Здесь определен обощенный метод Swap, который принимает параметры по ссылке и меняет их значения. При этом в данном случае не важно, какой тип представляют эти параметры.

При вызове метода Swap типизируем его определенным типом и передаем ему соответствующие этому типу значения.

  • Вопросы для самопроверки
  • Упражнения

C#. Упаковка и распаковка. Необходимость (преимущества) применения обобщений

Упаковка и распаковка. Необходимость (преимущества) применения обобщений. Повышение типовой безопасности с помощью обобщений

Поиск на других ресурсах:

1. Понятие упаковки (boxing) и распаковки (unboxing)

Как известно, в .NET Framework все базовые типы ( int , double , char и т.д.) представлены соответствующим классом или структурой ( Integer , Double , Char и т.п.) в общей иерархической структуре классов. В вершине этой структуры лежит тип Object , к которому также можно обращаться по имени object . Это означает, что допускается объявлять переменную типа Object и использовать ее для работы с любым типом как показано ниже

. // Можно работать также через базовый класс Object Object a; object b; a = 25; // присваивание значения типа int b = "bestprog"; // присваивание значения типа string Console.WriteLine("a = ", a); // a = 25 Console.WriteLine("b = ", b); // b = bestprog // Переменной a опять присваивается значение другого типа double a = 2.88; Console.WriteLine("a = ", a); // a = 2.88 .

Допускается также использовать переменную типа object в правой части оператора присваивания:

. // Переменная типа object object c; c = 'A'; // присваивается значение char char cc; // Переменная типа char cc = (char)c; // здесь нужно явное приведение типов, иначе ошибка на этапе компиляции Console.WriteLine("cc = ", cc); // cc = A .

Но в этом случае нужно указывать явное приведение типов, как видно из строки

. cc = (char)c; .

иначе будет ошибка на этапе компиляции.

Если переменная типа object используется в левой части оператора присваивания, то компилятор выполняет так называемую упаковку. Если переменная или значение типа object используется в правой части оператора присваивания, то компилятор выполняет распаковку.

Таким образом можно дать следующие определения. Упаковка — это процесс сохранения значения простого типа ( int , char , double …) в экземпляре объекта (object). Распаковка — это процесс вытягивания упакованного значения ( int , double , char …) из объекта ( object ). Следующий пример демонстрирует различие между этими терминами:

. object a; // тип int упаковывается в тип object a = 300; // упаковка: object int b; b = (int)a; // распаковка: int .
2. Какая разница между использованием обобщений и приведением к типу object ? Демонстрация преимуществ применения обобщений. Пример

Как было сказано в п. 1 в программах на C# можно объявлять ссылки на тип Object , обращаясь к именам object или Object . Благодаря наследованию, переменным типа Object может быть присвоено значение любых унаследованных типов (смотрите п. 1).

Исходя из вышесказанного, можно сделать вывод, что использование типа Object может заменить обобщения. Тогда возникает резонный вопрос: зачем использовать обобщения, если они целиком могут быть заменены типом object ?

Использование обобщений вместо использования типа object дает следующие преимущества:

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

В следующих пунктах эти преимущества рассматриваются более подробно.

2.1. Преимущество 1. Отсутствие явного приведения типа

Если используется обобщение, то не нужно выполнять явное приведение типов в операции присваивания как показано на рисунке 1.

C#. Обобщения. Отличие в явном приведении к типу int между обобщением и типом object

Рисунок 1. Отличие в явном приведении к типу int между обобщением и типом object

2.2. Преимущество 2. Обеспечение типовой безопасности в обобщениях

При использовании класса object в качестве типа можно допустить ошибку, которая на этапе компиляции не будет обнаружена. Эта ошибка окажется на этапе выполнения, что неприемлемо.

На рисунке 2 реализованы такие же классы как на рисунке 1. Однако, в функции main() , для обоих классов осуществляется попытка установить значение типа double .

В случае с классом ObjectClass ошибки на этапе компиляции не возникает. Эта ошибка вызовет исключительную ситуацию на этапе выполнения.

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

C#. Обобщения. Особенности выявления ошибки компилятором

Рисунок 2. Особенности выявления ошибки компилятором для обобщенного и необобщенных класса

2.3. Преимущество 3. Повышение производительности

Использование обобщенных классов дает большую производительность (быстродействие) по сравнению с необобщенными. При присвоении значения типа object другим типам и наоборот, компилятор выполняет упаковку и распаковку (смотрите п. 1). Этот процесс требует больше временных затрат чем использование обобщений. В случае с обобщениями формируется типизированный код с привязкой к конкретному типу, который выполняется быстрее.

Рисунок 3 отражает объявление двух классов ObjectClass и GenClass . В функции main() выделены фрагменты кода, в которых проявляется различие в производительности между объектами ( object ) и обобщениями.

C#. Обобщения. Производительность

Рисунок 3. Отличие в производительности выполнения кода между обобщенным классом и классом типа object . Операция присваивания для обобщенных классов выполняется быстрее

Связанные темы

  • Обобщения. Основные понятия. Обобщенные классы и структуры
  • Обобщенные интерфейсы. Синтаксис объявления. Реализация обобщенных интерфейсов в классах. Примеры
  • Обобщенные методы в классах. Синтаксис объявления. Способы вызова
  • Обобщенные делегаты

Система типов 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  

Типы параметров и возвращаемых значений метода задаются в объявлении метода. Далее представлена сигнатура метода, который требует значение 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 enum class interface , и 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

Константа 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 . При создании экземпляра этого типа необходимо указать тип объектов, которые будут содержаться в списке, например string :

List stringList = new List(); 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 . Типы, допускающие значение NULL, особенно полезны при передаче данных в базы данных, где могут использоваться числовые значения null , и из таких баз данных. Дополнительные сведения см. в разделе Типы, допускающие значение NULL.

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

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

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

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

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

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

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

Связанные разделы

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

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

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

Stack and heap. Структуры данных в .NET

Stack and heap. Структуры данных в .NET

10.09.2018

11304

Рейтинг: 5 . Проголосовало: 2
Вы проголосовали:
Для голосования нужно авторизироваться

advertisement advertisement

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

Разделение памяти

По умолчанию, как только .NET приложение запускается и определяется виртуальный адрес текущего процесса, создаются следующие "кучи":

Куча для кода - JIT-компилируемый нативный код

Малая объектная куча - объекты до 85 кб

Большая объектная куча - объекты свыше 85 кб*

Куча для обработки данных

*примечание: в случае массивов для данных типа double существует исключение, согласно которому они хранятся в большой объектной куче задолго до достижения размера в 85 кб (double[] считается системой "большим" объектом при достижении размера в 1000 элементов). По отношению к оптимизации 32-битного кода это, конечно, не очень хорошо.

advertisement advertisement

Разбиение на большие и малые кучи достаточно целесообразно для улучшения производительности, но об этом позже, когда будем говорить о сборщике мусора.

Элементы, размещенные в кучи, обладают своими адресами, которые являются чем-то вроде указателей на ячейки памяти, где хранятся значения этих элементов.

Впрочем, куча - это не единственная структура данных, которой может похвалиться вселенная .NET. К примеру, есть еще и стек, который крайне полезен для хранения "специфических" типов данных. Сейчас мы рассмотрим в деталях, как устроены эти структуры данных в деталях.

Стек

Стек - это структура данных, организованная по принципу LIFO (последний вошел - первый вышел). Если вдуматься, это идеальное решение для хранения данных, к которым вскоре предстоит обратиться (легко извлекаются с вершины стека). Де-факто природа области стека заключается в двух постулатах: "помнить" порядок выполнения и хранить значимые типы данных.

Тема связана со специальностями:

Запоминание порядка выполнения - обращение к стеку

Большая часть кода, который мы пишем, инкапсулирован в классы и методы, которые вызывают другие методы, и так далее. .NET Framework обязан всегда "помнить" порядок вызовов участков кода. Более того, так же нужно хранить данные о состоянии переменных и значениях параметров, передаваемых при вызове методов (дабы суметь восстановить состояние вызывающего метода после завершения работы вызываемого).

При каждом вызове метода .NET инициализирует стек-фрейм (что-то вроде контейнера), где и хранится вся необходимая информация для выполнения методов: параметры, локальные переменные, адреса вызываемых строчек кода. Стек-фреймы создаются в стеке друг на друге. Все это прекрасно проиллюстрировано ниже:

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

Давайте взглянем на следующий участок кода:

Дабы вызвать Method2, фреймворк должен сохранить адрес конца выполнения метода, которым будет следующая строчка кода (строчка 4 в примере ниже). Этот адрес вместе с параметрами и локальными переменными вызываемого и вызывающего метода хранятся в стеке вызова, как показано на схеме ниже.

Также вы можете увидеть, что происходит, когда Method3 завершает свое выполнение (стек-фрейм покидает стек вызова).

Хранение значимых типов

Также стек используется для хранения переменных любых значимых типов .NET - включая: bool, decimal, int и так далее.

Ссылочные типы - это типы, которые хранят данные и ссылку на эти данные в рамках одной области памяти. Что так же интересно, так это то, что все локальные переменные значимых типов при завершении выполнения метода очищаются. Это происходит по той причине, что при завершении работы метода его стек-фрейм становится недоступным - стек имеет указатель на начало стек-фрейма на вершине стека вызова (текущий указатель стек-фрейма), который просто перемещается на следующий стек-фрейм после окончания работы текущего (физически данные все еще находятся в стеке вызова, но на практике получить доступ к ним через стандартный .NET-механизм невозможно).

Видео курсы по схожей тематике:

SQL Базовый

Разработка пользовательского графического интерфейса (GUI) на C# под Android (Xamarin)

Разработка пользовательского графического интерфейса (GUI) на C# под Android (Xamarin)

C# Starter (EN)

Abdul Rashid Hamid

Куча

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

Хранение ссылочных типов

Все прочие переменные, которые не являются значимыми (производные от object), являются ссылочными типами данных. Все ссылочные типы данных хранятся в управляемой куче (малой или большой - в зависимости от размера). Впрочем, пусть даже и значение объекта хранится в куче, ссылка на него хранится в стеке.

Рассмотрим следующий код:

Фигура ниже иллюстрирует, как выглядит стек и куча в плане хранения данных:

OBJREF, хранимый в стеке, на самом деле является ссылкой на объект MyClass, хранимый в куче.

Заметка: выражение MyClass myObj совершенно не занимает места в куче переменной myObj. Здесь всего лишь создается переменная OBJREF в стеке, после чего она инициализируется значением null. Как только выполняется команда new, куча получает действительное место памяти объекта, а сам ссылочный объект получает по адресу свое значение.

Значимые типы против ссылочных типов (стек против кучи)

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

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

Бесплатные вебинары по схожей тематике:

Алгоритмы и структуры данных в .NET.

Алгоритмы и структуры данных в .NET.

Обзор полезных ресурсов для изучения C# и .NET с нуля

Обзор полезных ресурсов для изучения C# и .NET с нуля

Практический опыт миграции с Web API на микросервисную архитектуру (продолжение)

Практический опыт миграции с Web API на микросервисную архитектуру (продолжение)

Конечно, хранение одного вида информации в стеке, другого в куче, имеет свои причины, которые мы рассмотрим в грядущих статьях. 🙂

Заключение

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

До новых встреч!

Автор перевода: Евгений Лукашук

Добавить комментарий

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