Краткий обзор языка C#
C# (произносится как «си шарп») — современный объектно-ориентированный и типобезопасный язык программирования. C# позволяет разработчикам создавать разные типы безопасных и надежных приложений, выполняющихся в .NET. C# относится к широко известному семейству языков C, и покажется хорошо знакомым любому, кто работал с C, C++, Java или JavaScript. Здесь представлен обзор основных компонентов языка C# 8 и более ранних версий. Если вы хотите изучить язык с помощью интерактивных примеров, рекомендуем поработать с вводными руководствами по C#.
C# — объектно-ориентированный, ориентированный на компоненты язык программирования. C# предоставляет языковые конструкции для непосредственной поддержки такой концепции работы. Благодаря этому C# подходит для создания и применения программных компонентов. С момента создания язык C# обогатился функциями для поддержки новых рабочих нагрузок и современными рекомендациями по разработке ПО. В основном C# — объектно-ориентированный язык. Вы определяете типы и их поведение.
Вот лишь несколько функций языка C#, которые позволяют создавать надежные и устойчивые приложения. Сборка мусора автоматически освобождает память, занятую недостижимыми неиспользуемыми объектами. Типы, допускающие значение null, обеспечивают защиту от переменных, которые не ссылаются на выделенные объекты. Обработка исключений предоставляет структурированный и расширяемый подход к обнаружению ошибок и восстановлению после них. Лямбда-выражения поддерживают приемы функционального программирования. Синтаксис LINQ создает общий шаблон для работы с данными из любого источника. Поддержка языков для асинхронных операций предоставляет синтаксис для создания распределенных систем. В C# имеется Единая система типов. Все типы C#, включая типы-примитивы, такие как int и double , наследуют от одного корневого типа object . Все типы используют общий набор операций, а значения любого типа можно хранить, передавать и обрабатывать схожим образом. Более того, C# поддерживает как определяемые пользователями ссылочные типы, так и типы значений. C# позволяет динамически выделять объекты и хранить упрощенные структуры в стеке. C# поддерживает универсальные методы и типы, обеспечивающие повышенную безопасность типов и производительность. C# предоставляет итераторы, которые позволяют разработчикам классов коллекций определять пользовательские варианты поведения для клиентского кода.
C# подчеркивает Управление версиями , чтобы обеспечить совместимость программ и библиотек с течением времени. Вопросы управления версиями существенно повлияли на такие аспекты разработки C#, как раздельные модификаторы virtual и override , правила разрешения перегрузки методов и поддержка явного объявления членов интерфейса.
Архитектура .NET
Программы C# выполняются в .NET, виртуальной системе выполнения, вызывающей общеязыковую среду выполнения (CLR) и набор библиотек классов. Среда CLR — это реализация общеязыковой инфраструктуры языка (CLI), являющейся международным стандартом, от корпорации Майкрософт. CLI является основой для создания сред выполнения и разработки, в которых языки и библиотеки прозрачно работают друг с другом.
Исходный код, написанный на языке C# компилируется в промежуточный язык (IL), который соответствует спецификациям CLI. Код на языке IL и ресурсы, в том числе растровые изображения и строки, сохраняются в сборке, обычно с расширением .dll. Сборка содержит манифест с информацией о типах, версии, языке и региональных параметрах для этой сборки.
При выполнении программы C# сборка загружается в среду CLR. Среда CLR выполняет JIT-компиляцию из кода на языке IL в инструкции машинного языка. Среда CLR также выполняет другие операции, например, автоматическую сборку мусора, обработку исключений и управление ресурсами. Код, выполняемый средой CLR, иногда называют «управляемым кодом». «Неуправляемый код» компилируется на машинный язык, предназначенный для конкретной платформы.
Обеспечение взаимодействия между языками является ключевой особенностью .NET. Код IL, созданный компилятором C#, соответствует спецификации общих типов (CTS). Код IL, созданный из кода на C#, может взаимодействовать с кодом, созданным из версий .NET для языков F#, Visual Basic, C++. Существует более 20 других языков, совместимых с CTS. Одна сборка может содержать несколько модулей, написанных на разных языках .NET, и все типы могут ссылаться друг на друга, как если бы они были написаны на одном языке.
В дополнение к службам времени выполнения .NET также включает расширенные библиотеки. Эти библиотеки поддерживают множество различных рабочих нагрузок. Они упорядочены по пространствам имен, которые предоставляют разные полезные возможности: от операций файлового ввода и вывода до управления строками и синтаксического анализа XML, от платформ веб-приложений до элементов управления Windows Forms. Обычно приложение C# активно используют библиотеку классов .NET для решения типовых задач.
Дополнительные сведения о .NET, см. в статье Обзор .NET.
Здравствуй, мир
Для первого знакомства с языком программирования традиционно используется программа «Hello, World». Вот ее пример на C#:
using System; class Hello < static void Main() < Console.WriteLine("Hello, World"); >>
Программа «Hello, World» начинается с директивы using , которая ссылается на пространство имен System . Пространства имен позволяют иерархически упорядочивать программы и библиотеки C#. Пространства имен содержат типы и другие пространства имен. Например, пространство имен System содержит несколько типов (в том числе используемый в нашей программе класс Console ) и несколько других пространств имен, таких как IO и Collections . Директива using , которая ссылается на пространство имен, позволяет использовать типы из этого пространства имен без указания полного имени. Благодаря директиве using в коде программы можно использовать сокращенное имя Console.WriteLine вместо полного варианта System.Console.WriteLine .
Класс Hello , объявленный в программе «Hello, World», имеет только один член — это метод с именем Main . Метод Main объявлен с модификатором static . Методы экземпляра могут ссылаться на конкретный экземпляр объекта, используя ключевое слово this , а статические методы работают без ссылки на конкретный объект. По стандартному соглашению точкой входа программы C# является статический метод с именем Main .
Выходные данные программы создаются в методе WriteLine класса Console из пространства имен System . Этот класс предоставляется библиотеками стандартных классов, ссылки на которые компилятор по умолчанию добавляет автоматически.
Типы и переменные
Тип определяет структуру и поведение любых данных в C#. Объявление типа может включать его члены, базовый тип, интерфейсы, которые он реализует, и операции, разрешенные для этого типа. Переменная — это метка, которая ссылается на экземпляр определенного типа.
В C# существуют две разновидности типов: ссылочные типы и типы значений. Переменные типа значений содержат непосредственно данные, а в переменных ссылочных типов хранятся ссылки на нужные данные, которые именуются объектами. Две переменные ссылочного типа могут ссылаться на один и тот же объект, поэтому может случиться так, что операции над одной переменной затронут объект, на который ссылается другая переменная. Каждая переменная типа значения имеет собственную копию данных, и операции над одной переменной не могут затрагивать другую (за исключением переменных параметров ref и out ).
Идентификатор — это имя переменной. Идентификатор — это последовательность символов Юникода без пробелов. Идентификатор может быть зарезервированным словом C#, если он имеет префикс @ . При взаимодействии с другими языками в качестве идентификатора может быть полезно использовать зарезервированное слово.
Типы значений в C# делятся на простые типы, типы перечислений, типы структур, типы, допускающие значение NULL, и типы значений кортежей. Ссылочные типы в C# подразделяются на типы классов, типы интерфейсов, типы массивов и типы делегатов.
Далее представлены общие сведения о системе типов в C#.
- Типы значений
- Простые типы
- Целое со знаком: , short , int , long
- Целое без знака: , ushort , uint , ulong
- Символы Юникода: , представляющие блок кода UTF-16
- Двоичная с плавающей запятой IEEE: , double
- Десятичная точность с плавающей запятой с высокой точностью:
- Логический: bool , используется для представления логических значений, которые могут иметь значение true или false .
- Пользовательские типы в формате enum E . Тип enum является отдельным типом со списком именованных констант. Каждый тип enum имеет базовый тип, в роли которого выступает один из восьми целочисленных типов. Набор значений типа enum аналогичен набору значений его базового типа.
- Пользовательские типы в формате struct S
- Расширения других типов значений, допускающие значение null
- Пользовательские типы в формате (T1, T2, . )
- Типы классов
- Исходный базовым классом для всех типов: object
- Строки в Юникоде: , представляющие последовательность единиц кода UTF-16
- Пользовательские типы в формате class C
- Пользовательские типы в формате interface I
- Одномерные, многомерные массивы и массивы массивов. Например, int[] , int[,] и int[][] .
- Пользовательские типы в формате delegate int D(. )
Программы C# используют объявления типов для создания новых типов. В объявлении типа указываются имя и члены нового типа. Шесть категорий типов в C# определяются пользователем: типы классов, типы структур, типы интерфейсов, типы перечисления, типы делегатов и типы значений кортежей. Можно также объявлять типы record , либо record struct , либо record class . Типы записей имеют члены, синтезированные компилятором. Записи используются в основном для хранения значений с минимальным связанным поведением.
- Тип class определяет структуру данных, которая содержит данные-члены (поля) и функции-члены (методы, свойства и т. д.). Классы поддерживают механизмы одиночного наследования и полиморфизма, которые позволяют создавать производные классы, расширяющие и уточняющие определения базовых классов.
- Тип struct похож на тип класса тем, что он представляет структуру с данными-членами и функциями-членами. Но, в отличие от классов, структуры являются типами значений и обычно не требуют выделения памяти из кучи. Типы структуры не поддерживают определяемое пользователем наследование, и все типы структуры неявно наследуют от типа object .
- Тип interface определяет контракт в виде именованного набора открытых элементов. Объект типа class или struct , реализующий interface , должен предоставить реализации для всех элементов интерфейса. Тип interface может наследовать от нескольких базовых интерфейсов, а class или struct могут реализовывать несколько интерфейсов.
- Тип delegate (делегат) представляющий ссылки на методы с конкретным списком параметров и типом возвращаемого значения. Делегаты позволяют использовать методы как сущности, сохраняя их в переменные и передавая в качестве параметров. Делегаты аналогичны типам функций, которые используются в функциональных языках. Их принцип работы близок к указателям функций из некоторых языков. В отличие от указателей функций, делегаты являются объектно-ориентированными и типобезопасными.
Типы class , struct , interface и delegate поддерживают универсальные шаблоны, которые позволяют передавать им другие типы в качестве параметров.
C# поддерживает одномерные и многомерные массивы любого типа. В отличие от перечисленных выше типов, типы массивов не требуется объявлять перед использованием. Типы массивов можно сформировать, просто введя квадратные скобки после имени типа. Например, int[] является одномерным массивом значений типа int , а int[,] — двумерным массивом значений типа int , тогда как int[][] представляет собой одномерный массив одномерных массивов (или массив массивов) значений типа int .
Типы, допускающие значение NULL, не требуют отдельного определения. Для каждого обычного типа T , который не допускает значение NULL, существует идентичный тип T? , который отличается только тем, что может содержать дополнительное значение null . Например, int? является типом, который может содержать любое 32-разрядное целое число или значение null , а string? — любое значение string или null .
Система типов в C# унифицирована таким образом, что значение любого типа можно рассматривать как object (объект). Каждый тип в C# является прямо или косвенно производным от типа класса object , и этот тип object является исходным базовым классом для всех типов. Чтобы значения ссылочного типа обрабатывались как объекты, им просто присваивается тип object . Чтобы значения типов значений обрабатывались как объекты, выполняются операции упаковки-преобразования и распаковки-преобразования. В следующем примере значение int преобразуется в object , а затем обратно в int .
int i = 123; object o = i; // Boxing int j = (int)o; // Unboxing
Если значение типа назначается ссылке object , для хранения значения выделяется упаковка. Эта упаковка является экземпляром ссылочного типа, и в нее копируется значение. И наоборот, если ссылка типа object используется для типа значения, для соответствующего object выполняется проверка, является ли он упаковкой правильного типа. Если эта проверка завершается успешно, копируется значение этой упаковки.
Унифицированная система типов C# фактически означает, что типы значений обрабатываются как object ссылки «по запросу». Из-за унификации библиотеки общего назначения, использующие тип object , могут использоваться со всеми типами, производными от object , включая ссылочные типы и типы значений.
В C# существует несколько типов переменных, в том числе поля, элементы массива, локальные переменные и параметры. Переменные представляют собой места хранения, и каждая переменная имеет тип, который определяет допустимые значения для хранения в этой переменной. Примеры представлены ниже.
- Тип значения, не допускающий значения Null
- Значение такого типа
- Значение null или значение такого типа
- Ссылка null , ссылка на объект любого ссылочного типа или ссылка на упакованное значение любого типа значения
- Ссылка null , ссылка на экземпляр такого типа класса или ссылка на экземпляр любого класса, производного от такого типа класса
- Ссылка null , ссылка на экземпляр типа класса, который реализует такой тип интерфейса, или ссылка на упакованное значение типа значения, которое реализует такой тип интерфейса
- Ссылка null , ссылка на экземпляр такого типа массива или ссылка на экземпляр любого совместимого типа массива
- Ссылка null или ссылка на экземпляр совместимого типа делегата
Структура программы
Основными понятиями Организации в C# являются программы, пространства имен, типы, членыи сборки. В программе объявляются типы, которые содержат члены. Эти типы можно организовать в пространства имен. Примерами типов являются классы, структуры и интерфейсы. К членам относятся поля, методы, свойства и события. При компиляции программы на C# упаковываются в сборки. Сборки обычно имеют расширение .exe файла или .dll , в зависимости от того, реализуют ли они .exe или библиотекисоответственно.
В качестве небольшого примера рассмотрим сборку, содержащую следующий код:
namespace Acme.Collections; public class Stack < Entry _top; public void Push(T data) < _top = new Entry(_top, data); >public T Pop() < if (_top == null) < throw new InvalidOperationException(); >T result = _top.Data; _top = _top.Next; return result; > class Entry < public Entry Next < get; set; >public T Data < get; set; >public Entry(Entry next, T data) < Next = next; Data = data; >> >
Полное имя этого класса: Acme.Collections.Stack . Этот класс содержит несколько членов: поле с именем _top , два метода с именами Push и Pop , а также вложенный класс с именем Entry . Класс Entry , в свою очередь, содержит три члена: свойство с именем Next , свойство с именем Data и конструктор. Stack Является Stack классом. Он имеет параметр одного типа T , который замещается конкретным типом при использовании.
Стек — это коллекция типа FILO (прибыл первым — обслужен последним). Новые элементы добавляются в верх стека. Удаляемый элемент исключается из верхней части стека. В предыдущем примере объявляется тип Stack , который определяет хранилище и поведение для стека. Можно объявить переменную, которая ссылается на экземпляр типа Stack для использования этой возможности.
Сборки содержат исполняемый код в виде инструкций промежуточного языка (IL) и символьную информацию в виде метаданных. Перед выполнением JIT-компилятор среды CLR .NET преобразует код IL в сборке в код, зависящий от процессора.
Сборка полностью описывает сама себя и содержит весь код и метаданные, поэтому в C# не используются директивы #include и файлы заголовков. Чтобы использовать в программе C# открытые типы и члены, содержащиеся в определенной сборке, вам достаточно указать ссылку на эту сборку при компиляции программы. Например, эта программа использует класс Acme.Collections.Stack из сборки acme.dll :
class Example < public static void Main() < var s = new Acme.Collections.Stack(); s.Push(1); // stack contains 1 s.Push(10); // stack contains 1, 10 s.Push(100); // stack contains 1, 10, 100 Console.WriteLine(s.Pop()); // stack contains 1, 10 Console.WriteLine(s.Pop()); // stack contains 1 Console.WriteLine(s.Pop()); // stack is empty > >
Для компиляции программы вам потребуется создать ссылку на сборку, содержащую класс стека, определенный в примере выше.
Программы C# можно хранить в нескольких исходных файлах. При компиляции программы C# все исходные файлы обрабатываются вместе, при этом они могут свободно ссылаться друг на друга. По сути, это аналогично тому, как если бы все исходные файлы были объединены в один большой файл перед обработкой. В C# никогда не используются опережающие объявления, так как порядок объявления, за редким исключением, не играет никакой роли. В C# нет требований объявлять только один открытый тип в одном исходном файле, а также имя исходного файла не обязано совпадать с типом, объявляемом в этом файле.
Такие организационные блоки описываются в других статьях этого обзора.
Тонкие моменты C#
Не секрет, что C# сегодня популярный и динамично развывающийся язык, в отличие от своего прямого конкурента — языка Java, который в плане функциональности переживает период застоя. Основное неоспоримое преимущество Java — настоящая кросплатформенность, а не унылая и ограниченная, как у C#.
C# — простой язык, благодаря простоте живёт и PHP. Но в то же время он весьма функциональный, и имеет статус «гибридного» языка, совмещая в себе различные парадигмы, встроенную поддержку как императивного стиля программирования, так и функционального.
Как и любой язык, шарп имеет свои тонкости, особенности, «подводные камни» и малоизвестные возможности. Что я имею ввиду? Читайте под катом…
Упаковка и распаковка — знают все, да не каждый
Ссылочные типы (object, dynamic, string, class, interface, delegate) хранятся в управляемой куче, типы значений (struct, enum; bool, byte, char, int, float, double) — в стеке приложения (кроме случая, когда тип значения является полем класса). Преобразование типа значений к ссылочному типу сопровождается неявной операцией упаковки (boxing) — помещение копии типа значений в класс-обёртку, экземпляр которого сохраняется в куче. Упаковочный тип генерируется CLR и реализует интерфейсы сохраняемого типа значения. Преобразование ссылочного типа к типу значений вызывает операцию распаковки (unboxing) — извлечение из упаковки копии типа значения и помещение её в стек.
class Program
static void Main()
int val = 5;
object obj = val; // присваивание сопровождается упаковкой
int valUnboxed = ( int )obj; // приведение вызовет распаковку
>
>.locals init ([0] int32 val, [1] object obj, [2] int32 valUnboxed)
IL_0000: nop
IL_0001: ldc.i4.5
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: box [mscorlib]System.Int32
IL_0009: stloc.1
IL_000a: ldloc.1
IL_000b: unbox.any [mscorlib]System.Int32
IL_0010: stloc.2
IL_0011: retУпаковка и распаковка являются относительно медленными операциями (подразумевают копирование), поэтому по возможности следует их избегать. Нижеследующий код отображает неочевидные случаи, приводящие к упаковке:
class Program
static void Main()
// 1. Преобразование типа значений в ссылку на реализуемый им интерфейс
IComparable < int >iComp = 1;
// 2. Преобразование типа enum в ссылку на System.Enum
Enum format = UriFormat.Unescaped;
// 3. Преобразование типа значений к типу dynamic
dynamic d = 1;
>
>В msdn рекомендуется избегать типов значений в случаях, когда они должны быть упакованы много раз, например в не универсальных классах коллекций (ArrayList). Упаковки-преобразования типов значений можно избежать с помощью универсальных коллекций (System.Collections.Generic namespace). Также следует помнить, что dynamic на уровне IL-кода — это тот же object, только (не всегда) помеченный атрибутами.
Рекурсия в лямбдах — о зловредном замыкании
Обратимся к классической реализации рекурсивного вычисления факториала при помощи лямбда-выражений:
class Program
static void Main()
Func < int , BigInteger>fact = null ;
fact = x => x > 1 ? x * fact(x — 1) : 1;
>
>Мы знаем, что лямбда способна ссылаться на саму себя благодаря способности захватывать переменные окружения (замыкание). Мы знаем, что захваченные объекты могут быть изменены вне контекста лямбда-выражения, и не существует встроенной в язык возможности переопределить такое поведение. Понятно, что в
большинстверяде случаев такой подход не будет приемлемым, и хотелось бы как-то ограничить возможность изменять захваченную переменную вне контекста лямбды.В общем случае представленная проблема решается реализацией комбинатора неподвижной точки:
class Program
static void Main()
var fact = YPointCombinator.Create< int , BigInteger>(f => (n) => n > 1 ? n * f(n — 1) : 1);
var power = YPointCombinator.Create< int , int , BigInteger>(f => (x, y) => y > 0 ? x * f(x, y — 1) : 1);
>
>
public static class YPointCombinator
public static Func Create(Funcf)
return f(r => Create( f )( r ));
>
public static Func Create(Funcf)
return f((r1, r2) => Create(f)(r1, r2));
>
>Поля private и рефлексия, или плевали мы на ваше ООП
При помощи механизма отражения можно изменить значение даже private-поля класса.
Понятно, что применять это строить только в случае крайней необходимости, соблюдая принцип инкапсуляции.class Sample
private string _x = «No change me!» ;
public override string ToString()
return _x;
>
>
class Program
static void Main()
var sample = new Sample();
typeof (Sample).GetField( «_x» , BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(sample, «I change you. » );
Console .Write(sample);
Console .ReadKey();
>
>UPD: Как справедливо заметил braindamaged, изменить приватное поле удастся только если сборка принадлежит группе кода, располагающей необходимыми полномочиями. Затребовать такое полномочие можно декларативно, пометив класс (метод) чем-то вроде этого:
С системой безопасности .NET не всё просто, причём в .NET 4 она претерпела серьёзные изменения.
«Утиная» типизация и цикл foreach
Чтобы иметь возможность итерировать по элементам экземпляра некоторого класса при помощи foreach, достаточно реализовать в нём метод GetEnumerator().
using System;
using System.Collections.Generic;class Sample
public IEnumerator < int >GetEnumerator()
for ( var i = 0; i < 10; ++i)
yield return i;
>
>
class Program
static void Main()
foreach ( var t in new Sample())
Console .WriteLine(t);
Console .ReadKey();
>
>Это небольшое проявление так называемой «утиной» типизации, обычно применяемой в динамических языках, имеет место и в C#.
Анонимные типы — можно больше
Переменные анонимного типа можно сохранять в коллекции. Убедитесь сами:
Console .Write(list.Find(x => x.Name == «Petr» ));
Console .ReadKey();
>
>Переменные анонимного типа можно передавать в другую область видимости:
ref иногда можно опустить
Начиная с версии C# 4.0 ключевое слово ref можно опускать при вызове метода через COM Interop. В сочетании с именованными аргументами выглядит весьма эффектно:
using System;
using Word = Microsoft.Office.Interop.Word;class Program
static void Main()
var app = new Word.Application();
Word.Document doc = null ;// C# 2.0 — 3.5
object
filename = «test.doc» ,
visible = true ,
missing = Type.Missing;doc = app.Documents.Open(
ref filename, ref missing, ref missing, ref missing, ref missing, ref missing,
ref missing, ref missing, ref missing, ref missing, ref missing, ref visible,
ref missing, ref missing, ref missing, ref missing);// C# 4.0
doc = app.Documents.Open(FileName: «test.doc» , Visible: true );
>
>Заметьте: именованные параметры и возможность опускать ref — это средства языка, поэтому в качестве базового фреймворка приложения может быть выбран как .NET Framework 4.0, так и .NET Framework 2.0, 3.0, 3.5.
Что осталось за кадром
Среди всех прочих «тонкостей» языка я бы выделил проблему детерминированного уничтожения объектов, сложность обработки асинхронных исключений типа ThreadAbortException. Интерес представляют мощные средства синхронизации потоков и грядущие изменения в C# 5.0, связанные со встраиванием в язык поддержки асинхронных операций.
Зачем нужна упаковка в си шарп
Кроме обычных типов фреймворк .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#)
Упаковка представляет собой процесс преобразования типа значения в тип object или в любой другой тип интерфейса, реализуемый этим типом значения. Когда тип значения упаковывается общеязыковой средой выполнения (CLR), он инкапсулирует значение внутри экземпляра System.Object и сохраняет его в управляемой куче. Операция распаковки извлекает тип значения из объекта. Упаковка является неявной; распаковка является явной. Понятия упаковки и распаковки лежат в основе единой системы типов C#, в которой значение любого типа можно рассматривать как объект.
В следующем примере выполнена операция i упаковки целочисленной переменной, которая присвоена объекту o .
int i = 123; // The following line boxes i. object o = i;
Затем можно выполнить операцию распаковки объекта o и присвоить его целочисленной переменной i :
o = 123; i = (int)o; // unboxing
Следующий пример иллюстрирует использование упаковки в C#.
// String.Concat example. // String.Concat has many versions. Rest the mouse pointer on // Concat in the following statement to verify that the version // that is used here takes three object arguments. Both 42 and // true must be boxed. Console.WriteLine(String.Concat("Answer", 42, true)); // List example. // Create a list of objects to hold a heterogeneous collection // of elements. List mixedList = new List(); // Add a string element to the list. mixedList.Add("First Group:"); // Add some integers to the list. for (int j = 1; j < 5; j++) < // Rest the mouse pointer over j to verify that you are adding // an int to a list of objects. Each element j is boxed when // you add j to mixedList. mixedList.Add(j); >// Add another string and more integers. mixedList.Add("Second Group:"); for (int j = 5; j < 10; j++) < mixedList.Add(j); >// Display the elements in the list. Declare the loop variable by // using var, so that the compiler assigns its type. foreach (var item in mixedList) < // Rest the mouse pointer over item to verify that the elements // of mixedList are objects. Console.WriteLine(item); >// The following loop sums the squares of the first group of boxed // integers in mixedList. The list elements are objects, and cannot // be multiplied or added to the sum until they are unboxed. The // unboxing must be done explicitly. var sum = 0; for (var j = 1; j < 5; j++) < // The following statement causes a compiler error: Operator // '*' cannot be applied to operands of type 'object' and // 'object'. //sum += mixedList[j] * mixedList[j]; // After the list elements are unboxed, the computation does // not cause a compiler error. sum += (int)mixedList[j] * (int)mixedList[j]; >// The sum displayed is 30, the sum of 1 + 4 + 9 + 16. Console.WriteLine("Sum: " + sum); // Output: // Answer42True // First Group: // 1 // 2 // 3 // 4 // Second Group: // 5 // 6 // 7 // 8 // 9 // Sum: 30
Производительность
По сравнению с простыми операциями присваивания операции упаковки и распаковки являются весьма затратными процессами с точки зрения вычислений. При выполнении упаковки типа значения необходимо создать и разместить новый объект. Объем вычислений при выполнении операции распаковки, хотя и в меньшей степени, но тоже весьма значителен. Дополнительные сведения см. в разделе Производительность.
Упаковка
Упаковка используется для хранения типов значений в куче со сбором мусора. Упаковка представляет собой неявное преобразование типа значения в тип object или в любой другой тип интерфейса, реализуемый этим типом значения. При упаковке типа значения в куче выделяется экземпляр объекта и выполняется копирование значения в этот новый объект.
Рассмотрим следующее объявление переменной типа значения.
int i = 123;
Следующий оператор неявно применяет операцию упаковки к переменной i .
// Boxing copies the value of i into object o. object o = i;
Результат этого оператора создает ссылку на объект o в стеке, которая ссылается на значение типа int в куче. Это значение является копией значения типа значения, присвоенного переменной i . Разница между этими двумя переменными, i и o , показана на рисунке упаковки-преобразования ниже:
Можно также выполнять упаковку явным образом, как в следующем примере, однако явная упаковка не является обязательной.
int i = 123; object o = (object)i; // explicit boxing
Пример
В этом примере целочисленная переменная i преобразуется в объект o при помощи упаковки. Затем значение, хранимое переменной i , меняется с 123 на 456 . В примере показано, что исходный тип значения и упакованный объект используют отдельные ячейки памяти, а значит могут хранить разные значения.
class TestBoxing < static void Main() < int i = 123; // Boxing copies the value of i into object o. object o = i; // Change the value of i. i = 456; // The change in i doesn't affect the value stored in o. System.Console.WriteLine("The value-type value = ", i); System.Console.WriteLine("The object-type value = ", o); > > /* Output: The value-type value = 456 The object-type value = 123 */
Распаковка-преобразование
Распаковка является явным преобразованием из типа object в тип значения или из типа интерфейса в тип значения, реализующего этот интерфейс. Операция распаковки состоит из следующих действий:
- проверка экземпляра объекта на то, что он является упакованным значением заданного типа значения;
- копирование значения из экземпляра в переменную типа значения.
В следующем коде показаны операции по упаковке и распаковке.
int i = 123; // a value type object o = i; // boxing int j = (int)o; // unboxing
На рисунке ниже представлен результат выполнения этого кода.
Для успешной распаковки типов значений во время выполнения необходимо, чтобы экземпляр, который распаковывается, был ссылкой на объект, предварительно созданный с помощью упаковки экземпляра этого типа значения. Попытка распаковать null создает исключение NullReferenceException. Попытка распаковать ссылку на несовместимый тип значения создает исключение InvalidCastException.
Пример
В следующем примере показан случай недопустимой распаковки, в результате чего создается исключение InvalidCastException . В случае использования try и catch при возникновении ошибки выводится сообщение.
class TestUnboxing < static void Main() < int i = 123; object o = i; // implicit boxing try < int j = (short)o; // attempt to unbox System.Console.WriteLine("Unboxing OK."); >catch (System.InvalidCastException e) < System.Console.WriteLine("Error: Incorrect unboxing.", e.Message); > > >
При выполнении этой программы выводится следующий результат:
Specified cast is not valid. Error: Incorrect unboxing.
При изменении оператора:
int j = (short)o;
int j = (int)o;
будет выполнено преобразование со следующим результатом:
Спецификация языка C#
Дополнительные сведения см. в спецификации языка C#. Спецификация языка является предписывающим источником информации о синтаксисе и использовании языка C#.
См. также
- Руководство по программированию на C#
- Ссылочные типы
- Типы значений
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
- Простые типы