Основы языка С# и платформы .NET Framework : пособие


101 downloads 5K Views 3MB Size

Recommend Stories

Empty story

Idea Transcript


Министерство образования Республики Беларусь Учреждение образования «Белорусский государственный университет информатики и радиоэлектроники» Факультет компьютерных систем и сетей

БГ УИ Р

Кафедра информатики

Д. Е. Бережнов

ОСНОВЫ ЯЗЫКА C# И ПЛАТФОРМЫ .NET FRAMEWORK

Би бл ио т

ек а

Рекомендовано УМО по образованию в области информатики и радиоэлектроники в качестве пособия для специальности 1-40 04 01 «Информатика и технологии программирования»

Минск БГУИР 2017

УДК 004.43(075.8) ББК 32.973.26-018.1я73 Б48 Р е ц е н з е н т ы:

БГ УИ Р

кафедра информационных технологий государственного учреждения образования «Республиканский институт повышения квалификации и переподготовки работников Министерства труда и социальной защиты Республики Беларусь» (протокол №3 от 16.03.2017); доцент кафедры управления информационными ресурсами Академии управления при Президенте Республики Беларусь, кандидат технических наук А. А. Перепелица;

Би бл ио т

ек а

заведующий кафедрой программного обеспечения информационных технологий учреждения образования «Белорусский государственный университет информатики и радиоэлектроники», кандидат технических наук, доцент Н. В. Лапицкая

Бережнов, Д. Е. Основы языка С# и платформы .NET Framework : пособие / Д. Е. БеБ48 режнов. – Минск : БГУИР, 2017. – 83 с. : ил. ISBN 978-985-543-373-7. Рассматриваются основные элементы синтаксиса языка C#, базовые концепции и типы платформы .NET Framework. Приводится теоретический материал, описывающий построение пользовательских типов и их составляющих, а также некоторые общепринятые практики программирования на платформе .NET.

ISBN 978-985-543-373-7

2

УДК 004.43(075.8) ББК 32.973.26-018.1я73

© Бережнов Д. Е., 2017 © УО «Белорусский государственный университет информатики и радиоэлектроники», 2017

Содержание 1. Общая характеристика платформы .NET ......................................................4 2. Общие концепции синтаксиса языка C# .......................................................6 3. Система типов CLR и языка C#......................................................................8 4. Идентификаторы, ключевые слова и литералы ......................................... 11 5. Выражения и операции ................................................................................ 13 6. Операторы...................................................................................................... 15 7. Начальные сведения о массивах ................................................................. 20

БГ УИ Р

8. Классы ............................................................................................................ 22 9. Методы ........................................................................................................... 25 10. Свойства и индексаторы ............................................................................ 30 11. Статические элементы и методы расширения ......................................... 33 12. Конструкторы и инициализация объектов ............................................... 35 13. Наследование классов ................................................................................ 38

ек а

14. Класс System.Object и иерархия типов ..................................................... 42 15. Структуры .................................................................................................... 44 16. Перечисления .............................................................................................. 46 17. Интерфейсы ................................................................................................. 47 18. Универсальные шаблоны ........................................................................... 50

Би бл ио т

19. Использование универсальных шаблонов ............................................... 56 20. Делегаты ...................................................................................................... 59 21. Анонимные методы и лямбда-выражения ............................................... 62 22. События ....................................................................................................... 66 23. Перегрузка операций .................................................................................. 70 24. Анонимные типы ........................................................................................ 73 25. Пространства имён ..................................................................................... 74 26. Генерация и обработка исключительных ситуаций ................................ 76 27. Директивы препроцессора ......................................................................... 79 28. Документирование исходного кода .......................................................... 80 Список использованных источников ...............................................................82

3

1. Общая характеристика платформы .NET В середине 2000 года компания Microsoft объявила о работе над новой платформой для создания программ, которая получила имя платформа .NET (.NET Framework). Платформа .NET образует каркас, включающий среду исполнения, библиотеку классов и набор технологий для построения приложений и служб. Основным инструментом разработки для платформы .NET является интегрированная среда Microsoft Visual Studio.

ек а

БГ УИ Р

1.1. Инфраструктура платформы .NET Основой платформы .NET является общеязыковая среда исполнения (Common Language Runtime, CLR) (рис. 1). CLR работает как «прослойка» между операционной системой и программой для платформы .NET. Каждая программа для .NET состоит из одной или нескольких сборок. Сборка (assembly) является результатом компиляции исходных текстов на некотором языке программирования для платформы .NET и содержит метаданные и код на Common Intermediate Language. Метаданные – это информационные таблицы с полным описанием всех типов, размещённых в сборке. Common Intermediate Language (CIL, или IL) – внутренний язык платформы .NET, он не зависит от типа процессора. В процессе работы программы CIL компилируется в машинный код специальным JIT-компилятором (Just-in-Time compiler). Программа на VB.NET

Компилятор для C#

Компилятор для VB.NET

Сборка A.exe: - Метаданные - CIL

Сборка B.dll: - Метаданные - CIL

Би бл ио т

Программа на C#

JIT-компилятор

Commom Language Runtime

Код в виде инструкций для CPU Операционная система

Рис. 1. Компиляция и выполнение программ для платформы .NET

4

Би бл ио т

ек а

БГ УИ Р

Основная задача CLR – это манипулирование сборками: загрузка, JITкомпиляция, создание окружения для выполнения сборок. Важной функцией CLR является управление памятью при работе приложения и выполнение автоматической сборки мусора, т. е. освобождения неиспользуемой памяти. Кроме этого, CLR реализует в приложениях для .NET проверку типов, управление политиками безопасности при доступе к коду и другие функции. В состав платформы .NET входит обширная библиотека классов Framework Class Library (FCL). Частью этой библиотеки является базовый набор классов Base Class Library (BCL), в который входят классы для работы со строками и коллекциями данных, для поддержки многопоточности и множество других классов. В FCL также входят компоненты, поддерживающие различные технологии обработки данных и организации взаимодействия с пользователем. Это классы для работы с XML и базами данных, а также для создания пользовательских интерфейсов. В стандартную поставку платформы .NET включено несколько компиляторов. Это компиляторы языков C#, F#, Visual Basic .NET, C++/CLI. Благодаря открытым спецификациям компиляторы для .NET предлагаются различными сторонними производителями. Необходимо подчеркнуть, что любой язык для платформы .NET является верхним элементом архитектуры. Имена элементов библиотеки FCL не зависят от языка программирования. Специфичной частью языка остаётся только синтаксис. Этот факт упрощает межъязыковое взаимодействие, перевод текста программы с одного языка на другой. Конечно, в синтаксисе любого языка программирования для .NET неизбежно находит своё отражение тесная связь с CLR. Для поддержки межъязыкового взаимодействия служат две спецификации платформы .NET: 1) общая система типов (Common Type System, CTS) описывает набор типов, который должен поддерживаться любым языком программирования для .NET; 2) общеязыковая спецификация (Common Language Specification, CLS) – это общие правила поведения для всех .NET-языков. 1.2. Версии платформы .NET Компанией Microsoft было выпущено несколько версий платформы .NET. В феврале 2002 года вышла первая официальная версия .NET Framework. Затем, в апреле 2003 года была опубликована версия 1.1 (пакет обновлений для версии 1.0). Ноябрь 2005 года ознаменовался выпуском версии 2.0, содержащей обновлённую CLR с поддержкой универсальных шаблонов (generics). В синтаксис языков C# и VB.NET были внесены изменения для поддержки шаблонов, а также улучшены технологии ASP.NET и ADO.NET. В ноябре 2006 года вместе с выпуском операционной системы Windows Vista вышла третья версия платформы .NET, которая содержала технологии Windows Presentation Foundation, Windows Communication Foundation, Workflow Foundation. В ноябре 2007 года вышла платформа .NET 3.5, основными особенностями которой являются реа5

лизация технологии LINQ и новые компиляторы для C# и VB.NET. В августе 2008 года опубликован пакет обновлений для версии 3.5. В апреле 2010 года была выпущена четвёртая версия платформы .NET, которая содержит переработанную CLR, а также интегрирует множество новых технологий, существовавших ранее, в виде отдельных проектов (например, Parallel Task Library, Dynamic Language Runtime, ASP.NET MVC). Платформа .NET 4.5 доступна с августа 2012 года. Эта версия предлагает набор улучшений для CLR и новые компиляторы с поддержкой средств асинхронного программирования. Табл. 1 поясняет соотношение между версиями платформы .NET, версиями CLR и версиями языка C#.

БГ УИ Р

Таблица 1

Версии платформы .NET, CLR и языка C# Версия .NET CLR C#

2002 1.0 1.0

2003 1.1 1.1

2005 2.0

1.0

Год выпуска 2006 2007 3.0 3.5 2.0

2.0

2008 3.5 SP1

3.0

2010 4.0 4.0 4.0

2012 4.5 4.5 5.0

2. Общие концепции синтаксиса языка C#

Би бл ио т

ек а

Специально для платформы .NET был разработан новый язык программирования C#. Этот язык сочетает простой синтаксис, похожий на синтаксис языков C++ и Java, и полную поддержку всех современных объектноориентированных концепций и подходов. В качестве ориентира при разработке языка было выбрано безопасное программирование, нацеленное на создание надёжного и простого в сопровождении кода. Здесь и далее рассматривается синтаксис пятой версии языка C#, доступной в составе .NET Framework 4.5. Ключевыми структурными понятиями в языке C# являются программы, сборки, пространства имён, пользовательские типы, элементы типов. Исходный код программы на языке C# размещается в одном или нескольких текстовых файлах, имеющих стандартное расширение .cs. В программе объявляются пользовательские типы, которые состоят из элементов. Примерами пользовательских типов являются классы и структуры, а примером элемента типа – метод класса. Типы могут быть логически сгруппированы в пространства имён, а физически (после компиляции) – в сборки, представляющие собой файлы с расширением .exe или .dll. Исходный код программы на языке C# – это набор операторов (statements), директив препроцессора и комментариев. Операторы языка C# и допустимые директивы препроцессора подробно будут рассмотрены далее. Комментарии игнорируются при компиляции и бывают двух видов: 1. Однострочный комментарий – это комментарий, начинающийся с последовательности // и продолжающийся до конца строки.

6

БГ УИ Р

2. Блочный (многострочный) комментарий – все символы, заключённые между парами /* и */. В C# различаются строчные и прописные символы при записи идентификаторов и ключевых слов. Количество пробелов в начале строки, в конце строки и между элементами строки значения не имеет. Это позволяет улучшить визуальную структуру исходного кода программы – операторы одного уровня вложенности обычно сопровождаются одинаковым начальным отступом. Рассмотрим простейшую программу на языке C#, которая переводит расстояние в милях в километры (текст программы поместим в файл FirstProgram.cs). using System;

ек а

class FirstProgram { static void Main() { Console.Write("Input miles: "); string s = Console.ReadLine(); double miles = double.Parse(s); Console.Write("In kilometers: "); Console.WriteLine(miles * 1.609); } }

Би бл ио т

Приведённая программа представляет собой описание пользовательского типа – класса с именем FirstProgram. Необязательная директива using в первой строке программы служит для ссылки на пространство имён System, группирующее базовый набор классов. Использование using System позволяет вместо полного имени класса System.Console записать короткое имя Console. Любая исполняемая программа на C# должна иметь специальную точку входа, с которой начинается выполнение программы. Такой точкой входа всегда является метод Main() с модификатором static, объявленный в некотором пользовательском типе программы (в данном случае – в классе FirstProgram). В примере метод Main() начинается с вызова метода Write() класса Console. Методы Console.WriteLine() и Console.Write() выводят информацию на экран, а метод Console.ReadLine() ожидает ввод пользователя и возвращает введённые данные как строку. Информация сохраняется в локальной строковой переменной s. Метод double.Parse() выполняет преобразование строки в вещественный тип. Программа на C# может быть скомпилирована при помощи компилятора командной строки csc.exe1. При этом допустимо указание различных пара-

Файл csc.exe обычно расположен в папке Microsoft.NET\Framework\ в системном каталоге. 1

7

метров и опций (например, имени итоговой сборки, ссылок на необходимые сборки и так далее). csc.exe FirstProgram.cs

После компиляции будет получена сборка FirstProgram.exe, готовая для запуска на любом компьютере с установленной платформой .NET.

3. Система типов CLR и языка C#

БГ УИ Р

Основой платформы .NET является развитая система типов. CLR использует уникальное имя типа, обычной составляющей которого является указание на пространство имён. Так, для представления строк служит тип System.String, где System – название пространства имён. Для наиболее распространённых типов платформы .NET язык C# предлагает короткие имена-псевдонимы (табл. 2). Например, тип int в C# – это псевдоним типа System.Int32, тип string – псевдоним типа System.String, и т. д. Таблица 2

Сопоставление типовых псевдонимов C# и типов CLR Тип C#

Имя типа в CLR System.SByte System.Int16 System.Int32 System.Int64 System.Byte System.UInt16 System.UInt32 System.UInt64 System.Char System.Single System.Double System.Decimal System.Boolean System.Nullable System.String System.Object

dynamic

System.Object

Примечание

Знаковые целочисленные типы

ек а

sbyte short int long byte ushort uint ulong char float double decimal bool T? string object

Би бл ио т

Беззнаковые целочисленные типы

Типы с плавающей запятой Тип данных повышенной точности Тип для хранения логических значений Тип значений T с поддержкой null (например int?) Тип для представления строк Базовый тип Динамический тип с проверкой элементов при выполнении программы

Все типы платформы .NET можно разделить на типы значений (value types) и ссылочные типы (reference types). Переменная типа значения непосредственно содержит данные. К типам значений относятся структуры и перечисления. Структуры включают пользовательские структуры, простые типы (это числовые типы и тип bool) и типы с поддержкой null. Переменная ссылочного типа, далее называемая объектом, хранит ссылку на данные, которые размещены в управляемой динамической памяти. Ссылочные типы – это классы, интерфейсы, строки, массивы, делегаты и тип object. 8

БГ УИ Р

С точки зрения компилятора языка C# типы можно разделить на примитивные типы и пользовательские типы. Поддержка примитивных типов обеспечена компилятором, такие типы не нуждаются в дополнительном объявлении. Простые типы и их варианты с поддержкой null, а также типы string, object и dynamic принято относить к примитивным типам. Пользовательские типы перед применением должны быть описаны при помощи особых синтаксических конструкций. Любая программа представляет собой набор определений пользовательских типов. Рассмотрим некоторые типы подробнее. Числовые типы делятся на целочисленные типы, типы с плавающей запятой2 и тип decimal (96 бит для хранения основания, 1 бит для хранения знака, 8 бит – число от 0 до 28, позиция запятой в основании – справа). Информация о числовых типах представлена в табл. 3. Таблица 3

Числовые типы

Знаковые целочисленные типы

Тип C# sbyte short int long

Диапазон и точность

–128…127 –32 768…32 767 –2 147 483 648…2 147 483 647 –9 223 372 036 854 775 808… 64 9 223 372 036 854 775 807 8 0…255 16 0…65535 16 Символ в кодировке UTF-16 32 0…4 294 967 295 64 0…18 446 744 073 709 551 615 32 От ±1.5×10–45 до ±3.4×1038, точность 7 цифр 64 От ±5.0×10–324 до ±1.7×10308, точность 15 цифр 128 От ±1.0×10–28 до ±7.9×1028, точность 28 цифр sbyte, ushort, uint, ulong не соответствуют Common

Би бл ио т

Беззнаковые целочисленные типы

byte ushort char uint ulong float double decimal

Размер (бит) 8 16 32

ек а

Категория

Типы с плавающей запятой Тип decimal

Отметим, что типы Language Specification. Это означает, что данные типы не следует использовать в интерфейсах межъязыкового взаимодействия. Так как C# – это язык со строгой типизацией, необходимо соблюдать соответствие типов при присваивании и вызове методов. В случае несоответствия выполняется преобразование типов, которое бывает явным и неявным. Для явного преобразования (explicit conversion) служит операция приведения в форме (целевой-тип)выражение. При этом ответственность за корректность преобразования возлагается на программиста. Неявное преобразование (implicit conversion) не требует особых синтаксических конструкций и осуществляется компилятором. Подразумевается, что неявное преобразование безопасно, т. е., например, для целочисленных типов не происходит переполнения. Для числовых тиТипы с плавающей запятой удовлетворяют стандарту IEEE 754 (см. сайт ieee.org или статью по адресу softelectro.ru/ieee754.html). 2

9

пов определено неявное преобразование типа A в тип B, если на рис. 2 существует путь из узла A в узел B. Тип char представляет символ в Unicode-кодировке UTF-16. Тип char преобразуется в типы sbyte, short, byte явно, а в остальные числовые типы – неявно. Преобразование числового типа в тип char может быть выполнено только в явной форме. sbyte

byte

short

ushort

БГ УИ Р

char

int

uint

long

ulong

float

ек а

double

decimal

Би бл ио т

Рис. 2. Схема неявного преобразования числовых типов

Тип bool служит для хранения логических (булевых) значений. Переменные данного типа могут принимать значения true или false. Ни неявное, ни явное преобразование bool в числовые типы и обратно невозможно. Тип string используется для работы со строками и является последовательностью Unicode-символов. Тип object – это ссылочный тип, переменной которого можно присвоить любое значение. Опишем функциональность, которой обладают пользовательские типы: 1. Класс – тип, поддерживающий всю функциональность объектноориентированного программирования, включая наследование и полиморфизм. 2. Структура – тип значения, обеспечивающий инкапсуляцию данных, но не поддерживающий наследование. Синтаксически структура похожа на класс. 3. Интерфейс – абстрактный тип, реализуемый классами и структурами для обеспечения оговорённой функциональности. 4. Массив – пользовательский тип для представления упорядоченного набора значений. 5. Перечисление – тип, содержащий в качестве членов именованные целочисленные константы. 10

6. Делегат – пользовательский тип, инкапсулирующий метод.

4. Идентификаторы, ключевые слова и литералы

base catch continue double extern for in lock object private return stackalloc this uint using

ек а

as case const do explicit float implicit is null params ref sizeof switch typeof ushort while

Би бл ио т

abstract byte class delegate event fixed if internal new override readonly short struct try unsafe volatile

БГ УИ Р

Идентификатор – это пользовательское имя для переменной, константы, метода или типа. В C# идентификатор – это произвольная последовательность букв, цифр и символов подчёркивания, начинающаяся с буквы, символа подчёркивания либо символа @. При записи идентификатора допустимо использовать четыре шестнадцатеричных цифры кода UTF-16 с префиксом \u. Идентификатор должен быть уникальным внутри области видимости. Он не может совпадать с ключевым словом языка, за исключением того случая, когда используется специальный префикс @ (не являющийся частью идентификатора). Примеры допустимых идентификаторов: Temp, _variable, _ (символ подчёркивания), @class (используется префикс @, т. к. class – ключевое слово), cl\u0061ss (применяется код UTF-16 для символа a, этот идентификатор совпадает с идентификатором @class). Ключевые слова – это предварительно определённые зарезервированные слова, имеющие специальное значение для компилятора. Ключевые слова нельзя использовать в качестве идентификаторов. Ниже приведён список всех ключевых слов языка C#. bool char decimal else false foreach int long operator protected sbyte static throw ulong virtual

break checked default enum finally goto interface namespace out public sealed string true unchecked void

Контекстные ключевые слова имеют особое значение для компилятора только в ограниченном программном контексте. Они формально не считаются ключевыми словами языка и могут использоваться в качестве идентификаторов за пределами своего контекста (хотя следовать такой практике не желательно). Далее перечислены все контекстные ключевые слова. add await field into

alias by from join

ascending descending get let

assembly dynamic global method

async equals group module 11

on remove value

orderby select var

param set where

partial type yield

property typevar

Би бл ио т

ек а

БГ УИ Р

Литерал – это последовательность символов, которая может интерпретироваться как значение определённого типа. Так как C# является языком со строгой типизацией, часто необходимо явно указать, к какому типу относится последовательность символов, описывающая данные. Рассмотрим правила записи некоторых литералов. Для ссылочных типов определён литерал null, который указывает на неинициализированную ссылку. В языке C# два булевых литерала: true и false. Целочисленные литералы могут быть записаны в десятичной или шестнадцатеричной форме. Признаком шестнадцатеричного литерала является префикс 0x (или 0X). Конкретный тип целочисленного литерала определяется следующим образом: – если литерал не имеет суффикса, то его тип – это первый из типов int, uint, long, ulong, который способен вместить значение литерала; – если литерал имеет суффикс U или u, его тип – это первый из типов uint, ulong, который способен вместить значение литерала; – если литерал имеет суффикс L или l, то его тип – это первый из типов long, ulong, который способен вместить значение литерала3; – если литерал имеет суффикс UL, Ul, uL, ul, LU, Lu, lU, lu, его тип – ulong. Если в числе с десятичной точкой не указан суффикс, то подразумевается тип double. Суффикс f (или F) используется для указания на тип float, суффикс d (или D) используется для явного указания на тип double, суффикс m (или M) определяет литерал типа decimal. Число с плавающей точкой может быть записано в научном формате: 3.5E-6, -7e10, .6E+7. Символьный литерал записывают в одинарных кавычках. Обычно это единичный символ (например 'a'). Допустимо указать четыре шестнадцатеричных цифры кода UTF-16 с префиксом \u ('\u005C' – это символ '\'). Можно использовать префикс \x и не более четырёх шестнадцатеричных цифр кода UTF16 ('\x5c' – это тоже символ '\'). Кроме этого, для представления некоторых специальных символов используются следующие пары: \' – одинарная кавычка \" – двойная кавычка \\ – обратная косая черта \0 – символ с кодом ноль \a – звуковой сигнал \b – забой

\f – новая страница \n – новая строка \r – возврат каретки \t – горизонтальная табуляция \v – вертикальная табуляция

Для строковых литералов в языке C# существуют две формы. Обычно строковый литерал записывается как ряд символов в двойных кавычках. Среди При записи целочисленных литералов не рекомендуется использовать суффикс l (строчная латинская буква L), т. к. его легко перепутать с единицей. 3

12

символов строки могут быть и управляющие последовательности: "This is a \t tabbed string". Дословная форма (verbatim form) строкового литерала – это запись строки в кавычках с использованием префикса @: @"There is \t no tab". В этом случае управляющие последовательности воспринимаются как обычные пары символов. Дословная форма может занимать несколько строк.

5. Выражения и операции

БГ УИ Р

Любое выражение в языке C# состоит из операндов и операций. Следующий список содержит допустимые операции4, разбитые на группы. Порядок групп соответствует приоритету операций. Операции одной группы имеют одинаковый приоритет.

Би бл ио т

ек а

1. Первичные операции x.m Доступ к элементу типа x(...) Вызов методов и делегатов x[...] Доступ к элементу массива или индексатора x++ Пост-инкремент x-Пост-декремент new T(...) Создание объекта или делегата new T(...){...} Создание объекта с инициализацией new {...} Инициализация объекта анонимного типа new T[...] Создание массива с элементами типа T typeof(T) Получение для типа T объекта System.Type checked(x) Вычисление в контролируемом контексте unchecked(x) Вычисление в неконтролируемом контексте default(T) Получение значения по умолчанию для типа T delegate {...} Описание анонимного метода 2. Унарные операции +x Идентичность -x Отрицание !x Логическое отрицание ~x Битовое отрицание ++x Пре-инкремент --x Пре-декремент (T)x Явное преобразование x к типу T sizeof(T) Размер в байтах для типа значения T await x Асинхронное ожидание завершения x 3. Мультипликативные операции x * y Умножение x / y Деление x % y Вычисление остатка

4

Не указаны операции с указателями, специфичные для небезопасного кода (unsafe code). 13

4. Аддитивные операции x + y Сложение чисел, сцепление строк и делегатов x – y Вычитание 5. Операции сдвига x > y Битовый сдвиг вправо

7. Операции равенства x == y Равно x != y Не равно

БГ УИ Р

6. Операции отношения и проверки типов x < y Меньше x > y Больше x = y Больше или равно x is T Возвращает true, если x приводим к типу T x as T Возвращает x, приведённый к типу T, или null

8. Логическое AND x & y Целочисленное битовое AND, булево AND

ек а

9. Логическое XOR x ^ y Целочисленное битовое XOR, булево XOR 10. Логическое OR x | y Целочисленное битовое OR, булево OR

Би бл ио т

11. Условное AND x && y Вычисляется y, только если x равно true 12. Условное OR x || y

Вычисляется y, только если x равно false

13. Операция проверки на null x ?? y Возвращает x, если x не равно null. Иначе возвращает y 14. Условие x ? y : z

Если x равно true, вычисляется y, иначе z

15. Операции присваивания и лямбда-выражений x = y Присваивание x op= y Составное присваивание, поддерживаются операции x => code

*= /= %= += -= = &= ^= |= Описывает блок кода code

Поясним использование некоторых операций. Для проверки значений, получаемых при работе с числовыми выражениями, в C# предусмотрено использование контролируемого и неконтролируемого контекстов. Контролируемый контекст объявляется в форме checked операторный-блок, либо как операция checked(выражение). Если при вычислении в контролируемом контексте полу14

БГ УИ Р

чается значение, выходящие за пределы целевого типа, то генерируется либо ошибка компиляции (для константных выражений), либо обрабатываемое исключение (для выражений с переменными). Неконтролируемый контекст объявляется в форме unchecked операторный-блок, либо как операция unchecked(выражение). При использовании неконтролируемого контекста выход за пределы целевого типа ведёт к автоматическому «урезанию» результата либо путём отбрасывания бит (целые типы), либо путём округления (вещественные типы). Неконтролируемый контекст применяется в вычислениях по умолчанию. Арифметические операции +, -, *, /, % определены для всех числовых типов, за исключением 8- и 16-битовых целых типов. Для коротких целых типов компилятор выполняет неявное преобразование типов (при этом операция с целыми числами должна остаться операцией с целыми числами). Арифметические операции для типов с плавающей запятой не генерируют исключительных ситуаций при переполнении, потере точности или делении на ноль. В таких случаях получаются особые значения, определённые в виде констант double.NaN, double.NegativeInfinity, double.PositiveInfinity (т. е. «не число», «минус бесконечность», «плюс бесконечность»).

6. Операторы

ек а

Методы пользовательских типов состоят из операторов, которые выполняются последовательно. Часто используется операторный блок – последовательность операторов, заключённая в фигурные скобки. Иногда возникает необходимость в пустом операторе – он записывается как символ ; (точка с запятой).

Би бл ио т

6.1. Операторы объявления К операторам объявления относятся операторы объявления переменных и операторы объявления констант. Для объявления локальных переменных метода применяется оператор следующего формата: тип имя-переменной [= начальное-значение];

Здесь тип – тип переменной, имя-переменной – допустимый идентификатор, необязательное начальное-значение – литерал или выражение, соответствующее типу переменной. Локальные переменные методов не могут использоваться в вычислениях, не будучи инициализированы. Если необходимо объявить несколько переменных одного типа, то идентификаторы переменных можно перечислить через запятую после имени типа. При этом для каждой переменной можно выполнить инициализацию. int int int int

a; a = 20; a, b, c; a = 20, b = 10;

// // // //

простейший вариант объявления объявление с инициализацией объявление однотипных переменных инициализация нескольких переменных

15

Локальная переменная может быть объявлена без указания типа, с использованием ключевого слова var. В этом случае компилятор выводит тип переменной из обязательного выражения инициализации. var x = 3; var y = "Student"; var z = new Student();

БГ УИ Р

Не стоит воспринимать переменные, объявленные с var, как некие универсальные контейнеры для данных любого типа. Все эти переменные строго типизированы. Так, переменная x в приведённом выше примере имеет тип int. Оператор объявления константы имеет следующий синтаксис: const тип-константы имя-константы = выражение;

ек а

Допустимый тип-константы – это числовой тип, bool, string, перечисление или произвольный ссылочный тип. Выражение, которое присваивается константе, должно быть полностью вычислимо на момент компиляции. Обычно в качестве выражения используется литерал соответствующего типа. Для ссылочных типов (за исключением string) единственно допустимым выражением является null. Как и при объявлении переменных, можно определить в одном операторе несколько однотипных констант: const double Pi = 3.1415926, E = 2.718281828; const string Name = "Student"; const object locker = null;

Би бл ио т

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

int i = 10;

} Console.WriteLine(i); // ошибка компиляции, переменная i не доступна

Если операторные блоки вложены друг в друга, то внутренний блок не может содержать объявлений переменных, идентификаторы которых совпадают с переменными внешнего блока: {

int i = 10; { int i = 20; }

// ошибка компиляции

}

6.2. Операторы выражений Операторы выражений – это выражения, одновременно являющиеся допустимыми операторами: 16

– операция присваивания (включая инкремент и декремент); – операция вызова метода или делегата; – операция создания объекта; – операция асинхронного ожидания. Приведём несколько примеров операторов выражений: // // // // //

присваивание инкремент вызов метода создание объекта асинхронное ожидание

БГ УИ Р

x = 1 + 2; x++; Console.Write(x); new StringBuilder(); await Task.Delay(1000);

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

Би бл ио т

ек а

6.3. Операторы перехода К операторам перехода относятся break, continue, goto, return, throw. Оператор break используется для выхода из операторного блока циклов и оператора switch. Оператор break выполняет переход на оператор за блоком. Оператор continue располагается в теле цикла и применяется для запуска новой итерации цикла. Если циклы вложены, то запускается новая итерация того цикла, в котором непосредственно располагается continue. Оператор goto передаёт управление на помеченный оператор. Обычно данный оператор употребляется в форме goto метка, где метка – это допустимый идентификатор. Метка должна предшествовать помеченному оператору и заканчиваться двоеточием, отдельно описывать метки не требуется: goto label; . . . label: A = 100;

Оператор goto и помеченный оператор должны располагаться в одном операторном блоке. Возможно использование оператора goto в одной из следующих форм: goto case константа; goto default;

Данные формы обсуждаются при рассмотрении оператора switch. Оператор return служит для завершения методов. Оператор throw генерирует исключительную ситуацию (работа с методами и исключительными ситуациями рассматривается далее). 6.4. Операторы выбора Операторы выбора – это операторы if и switch. Оператор if в языке C# имеет следующий синтаксис: 17

if (условие) вложенный-оператор-1 [else вложенный-оператор-2]

Здесь условие – это некоторое булево выражение, вложенный-оператор – оператор (за исключением оператора объявления) или операторный блок. Ветвь else является необязательной. Оператор switch выполняет одну из групп инструкций в зависимости от значения тестируемого выражения. Синтаксис оператора switch:

ек а

БГ УИ Р

switch (выражение) { case константное-выражение-1: операторы оператор-перехода case константное-выражение-2: операторы оператор-перехода . . . [default: операторы оператор-перехода] }

Би бл ио т

Тестируемое выражение должно возвращать значение одного из следующих типов5: целочисленный тип (включая char), тип bool, перечисление, строка. При совпадении тестируемого и константного выражений выполняется соответствующая ветвь case. Если совпадения не обнаружено, то выполняется ветвь default (если она есть). Оператор-перехода – это один из следующих операторов: break, goto, return, throw. Оператор goto используется либо с указанием определённой ветви case (goto case константное-выражение), либо в виде goto default. Хотя после case может быть указано только одно константное выражение, при необходимости несколько ветвей case можно сгруппировать: switch (n) { case 0: case 1: case 2: . . . }

Для целочисленных типов, типа bool и перечислений допустимо использовать соответствующие типы с поддержкой null (например int?). 5

18

6.5. Операторы циклов К операторам циклов относятся операторы for, while, do-while, foreach. Для циклов с известным числом итераций используется оператор for: for ([инициализатор]; [условие]; [итератор]) вложенный-оператор

for (int i = 0; i < 10; i++) { Console.WriteLine(i); }

БГ УИ Р

Здесь инициализатор задаёт начальное значение счётчика (или счётчиков) цикла. Для счётчика может использоваться существующая переменная или объявляться новая переменная, время жизни которой будет ограничено циклом (при этом вместо типа переменной допустимо указать var). Цикл выполняется, пока булево условие истинно, а итератор определяет изменение счётчика цикла на каждой итерации. Простейший пример использования цикла for: // i доступна только в цикле for // вывод чисел от 0 до 9

ек а

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

Би бл ио т

// цикл выполнится 5 раз, на последней итерации i = 4, j = 6 for (int i = 0, j = 10; i < j; i++, j--) { Console.WriteLine("i = {0}, j = {1}", i, j); }

Если число итераций цикла заранее неизвестно, можно использовать цикл while или цикл do-while. Данные циклы имеют схожий синтаксис: while (условие) вложенный-оператор do

вложенный-оператор while (условие);

В обоих операторах цикла тело цикла выполняется, пока булево условие истинно. В цикле while условие проверяется в начале очередной итерации, а в цикле do-while – в конце. Таким образом, цикл do-while всегда выполнится, по крайней мере, один раз. Обратите внимание: условие должно присутствовать обязательно. Для организации бесконечных циклов на месте условия можно использовать литерал true: while (true) Console.WriteLine("Endless loop");

19

Для перебора элементов объектов перечисляемых типов (например массивов) в C# существует специальный цикл foreach: foreach (тип идентификатор in коллекция) вложенный-оператор

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

БГ УИ Р

6.6. Прочие операторы Следующие операторы C# не попадают ни в одну из перечисленных выше категорий, их синтаксис подробно рассматривается при изучении соответствующих разделов и тем: – операторы checked и unchecked позволяют описать блоки контролируемого и неконтролируемого контекстов вычислений; – оператор try (в различных формах) применяется для перехвата и обработки исключительных ситуаций; – оператор using используется при освобождении управляемых ресурсов6; – оператор yield служит для создания итераторов; – оператор lock применяется для объявления критической секции.

ек а

7. Начальные сведения о массивах

Би бл ио т

Массивы – это ссылочные пользовательские типы. Объявление массива в C# схоже с объявлением переменной, но после указания типа размещается спецификатор размерности – пара квадратных скобок: int[] data;

Массив является ссылочным типом, поэтому перед началом работы любой массив должен быть создан в памяти. Для этого используется конструктор в форме new тип[количество-элементов]. int[] data; data = new int[10];

Создание массива можно совместить с его объявлением: int[] data = new int[10];

Созданный массив автоматически заполняется значениями по умолчанию для своего базового типа (ссылочные типы – null, числа – 0, тип bool – false). Для доступа к элементу массива указывается имя массива и индекс в квадратных скобках: data[0] = 10. Индекс массива должен неявно приводиться к В C# имеется директива using для импорта пространств имён. Следует различать директиву using и оператор using. 6

20

типам int, uint, long или ulong. Элементы массива нумеруются с нуля, в C# не предусмотрено синтаксических конструкций для указания особого значения нижней границы массива. При выходе индекса массива за допустимый диапазон генерируется исключительная ситуация. В C# существует способ задания всех элементов массива при создании. Для этого используется список значений в фигурных скобках. При этом можно не указывать количество элементов, а также полностью опустить указание на тип и ключевое слово new: data_1 data_2 data_3 data_4

= = = =

new int[4] {1, 2, 3, 5}; new int[] {1, 2, 3, 5}; new[] {1, 2, 3, 5}; {1, 2, 3, 5};

БГ УИ Р

int[] int[] int[] int[]

// двумерный массив d int[,] d; d = new int[10, 2];

ек а

Первые три примера инициализации допускают указание вместо типа переменной ключевого слова var (например, var data_3 = new[] {1, 2, 3, 5}). Компилятор вычислит тип массива автоматически. При необходимости можно объявить массивы, имеющие несколько размерностей. Для этого в спецификаторе размерности помещают запятые, «разделяющие» размерности:

Би бл ио т

// трёхмерный массив Cube int[,,] Cube = new int[3, 2, 5];

// объявим двумерный массив и инициализируем его int[,] c = new int[2, 4] { {1, 2, 3, 4}, {10, 20, 30, 40} }; // то же самое, но немного короче int[,] c = {{1, 2, 3, 4}, {10, 20, 30, 40}};

В приведённых примерах объявлялись массивы из нескольких размерностей. Такие массивы всегда являются прямоугольными. Можно объявить массив массивов, используя следующий синтаксис7: int[][] table; table = new int[2][]; table[0] = new int[2]; table[1] = new int[20];

// // // //

table – массив одномерных массивов в table будет 2 одномерных массива в первом массиве будет 2 элемента во втором – 20 элементов

Объявление int[,][] data задаёт двумерный массив, состоящий из одномерных массивов. Иными словами, спецификаторы размерностей читаются слева направо. 7

21

table[1][3] = 1000;

// работаем с элементами table

// совместим объявление и инициализацию массива массивов int[][] t = {new[] {10, 20}, new[] {1, 2, 3}};

int[] data = {1, 3, 5, 7, 9}; var sum = 0; foreach (var element in data) { sum += element; }

БГ УИ Р

Язык CIL содержит специальные инструкции для работы с одномерными массивами, индексированными с нуля. Поэтому массив массивов обрабатывается немного быстрее, чем двумерный массив. При работе с массивом можно использовать цикл foreach, перебирающий все элементы. В цикле foreach возможно перемещение по массиву в одном направлении – от начала к концу, при этом попытки присвоить значение элементу массива игнорируются. В следующем фрагменте кода производится суммирование элементов массива:

ек а

В заключение рассмотрим вопрос о приведении типов массивов. Массивы ковариантны для ссылочных типов. Это означает следующее: если ссылочный тип A неявно приводим к ссылочному типу B, массив с элементами типа A может быть присвоен массиву с элементами типа B. При этом количество элементов в массиве роли не играет, но массивы должны иметь одинаковую размерность.

Би бл ио т

public class Student { . . . }

// объявление класса

Student[] students = new Student[10]; object[] array = students; // ковариантность массивов

Все массивы в платформе .NET являются классами, унаследованными от класса System.Array.

8. Классы

Класс является основным пользовательским типом. Синтаксис объявления класса в C# следующий: модификаторы class имя-класса { [элементы-класса] }

8.1. Допустимые элементы класса 1. Поле. Синтаксис объявления поля класса совпадает с синтаксисом оператора объявления переменной (как правило, идентификаторы полей снабжа22

class Person { readonly int _age = 20; string _name = "None"; }

БГ УИ Р

ются неким оговорённым префиксом). Тип поля всегда должен быть указан явно, использование var не допускается. Если для поля не указано начальное значение, то поле принимает значение по умолчанию для соответствующего типа (для числовых типов – 0, для типа bool – false, для ссылочных типов – null). Для полей можно применять модификатор readonly, который запрещает изменение поля после его начальной установки.

Би бл ио т

ек а

Поля с модификатором readonly похожи на константы, но имеют следующие отличия: тип поля может быть любым; значение поля можно установить при объявлении или в конструкторе класса; значение поля вычисляется в момент выполнения, а не при компиляции. 2. Константа. Синтаксис объявления константы в классе аналогичен синтаксису, применяемому при объявлении константы в теле метода. Следующие элементы класса будут подробно рассмотрены в дальнейшем. 3. Метод. Методы описывают функциональность класса. 4. Свойство. Свойства класса предоставляют защищённый доступ к полям. 5. Индексатор. Индексатор – это свойство-коллекция, отдельный элемент которого доступен по индексу. 6. Конструктор. Задача конструктора – начальная инициализация объекта (экземплярный конструктор) или класса (статический конструктор). 7. Финализатор. Финализатор автоматически вызывается сборщиком мусора и содержит завершающий код для объекта. 8. Событие. События представляют собой механизм рассылки уведомлений различным объектам. 9. Операция. Язык C# допускает перегрузку некоторых операций для объектов класса. 10. Вложенный пользовательский тип. Описание класса8 может содержать описание другого пользовательского типа – класса, структуры, перечисления, интерфейса, делегата. Обычно вложенные типы выполняют вспомогательные функции и явно вне основного типа не используются. 8.2. Модификаторы доступа для элементов и типов Для поддержания принципа инкапсуляции элементы класса могут снабжаться специальными модификаторами доступа: – private. Элемент с этим модификатором доступен только в том типе, в котором определён. Например, поле доступно только в содержащем его классе;

8

Только классы и структуры могут содержать вложенные типы. 23

БГ УИ Р

– protected. Элемент виден в типе, в котором определён, и в наследниках этого типа (даже если наследники расположены в других сборках). Данный модификатор может применяться только в типах, поддерживающих наследование, т. е. в классах; – internal. Элемент доступен без ограничений, но только в той сборке, где описан; – protected internal. Элемент виден в содержащей его сборке без ограничений, а вне сборки – только в наследниках типа (т. е. это комбинация protected или internal9); – public. Элемент доступен без ограничений как в той сборке, где описан, так и в других сборках, к которым подключается сборка с элементом. По умолчанию (без указания) для всех элементов типа применяется модификатор private. Для локальных переменных методов и операторных блоков модификаторы доступа не используются. При описании самостоятельного класса допустимо указать для него модификаторы public или internal (internal применяется по умолчанию). Если же класс вложен в другой пользовательский тип, то такой класс можно объявить с любым модификатором доступа10. Заметим, что у internal-класса publicэлементы за пределами сборки не видны.

Би бл ио т

ек а

8.3. Разделяемые классы Хорошей практикой программирования считается размещение каждого класса в отдельном файле. Однако иногда классы получаются настолько большими, что указанный подход становится непрактичным. Это часто справедливо при использовании средств автоматической генерации кода. Разделяемые классы (partial classes) – это классы, разбитые на несколько фрагментов, описанных в отдельных файлах с исходным кодом11. Для объявления разделяемого класса используется модификатор partial: // файл part1.cs partial class BrokenClass { private int someField; private string anotherField; } // файл part2.cs partial class BrokenClass { public void Method() { } }

В CLR есть модификатор доступа, соответствующий protected и internal. Но при помощи языка C# такой уровень доступа описать нельзя. 10 Важно: вложенный класс может обращаться к закрытым элементам объемлющего типа. 11 Разделяемыми могут быть не только классы, но также структуры и интерфейсы. 9

24

Все фрагменты разделяемого класса должны быть доступны во время компиляции, т. к. «сборку» типа выполняет компилятор. Ещё одно замечание касается использования модификаторов, применяемых к классу. Модификаторы доступа должны быть одинаковыми у всех фрагментов. Если же к одному из фрагментов применяется модификатор sealed или abstract, то эти модификаторы считаются применёнными ко всем фрагментам, т. е. к классу в целом.

имя-класса имя-объекта;

БГ УИ Р

8.4. Использование класса Чтобы использовать класс после объявления (т. е. получить доступ к его открытым экземплярным элементам12), необходима переменная класса – объект. Объект объявляется как обычная переменная:

Так как класс – ссылочный тип, то объекты должны быть инициализированы до непосредственного использования, иначе работа с любыми экземплярными элементами объекта будет генерировать исключение. Для инициализации объекта используется операция new – вызов конструктора класса (имя конструктора совпадает с именем класса). Если конструктор не описывался, применяется предопределённый конструктор без параметров:

ек а

имя-объекта = new имя-класса();

Инициализацию объекта можно совместить с его объявлением: имя-класса (или var) имя-объекта = new имя-класса();

Би бл ио т

Доступ к экземплярным элементам класса через объект осуществляется по синтаксису имя-объекта.имя-элемента.

9. Методы

Методы в языке C# являются неотъемлемой частью описания таких пользовательских типов, как класс или структура. В C# не существует глобальных методов – любой метод должен быть членом класса или структуры. 9.1. Описание метода Рассмотрим общий синтаксис описания метода: модификаторы тип имя-метода([параметры]) тело-метода

Здесь тип – это тип возвращаемого методом значения. Допустимо использование любого типа. В C# формально не существует процедур – любой метод считается функцией. Для «процедуры» в качестве типа возвращаемого значения указывается специальное ключевое слово void. После имени метода всегда сле-

12

Для доступа к константе применяется синтаксис имя-класса.имя-константы. 25

дует пара круглых скобок, в которых указывается список формальных параметров метода (если этот список не пуст). Список формальных параметров метода – это набор элементов, разделённых запятыми. Каждый элемент имеет следующий формат: [модификатор] тип имя-формального-параметра [= значение]

Би бл ио т

ек а

БГ УИ Р

Существуют четыре вида параметров: 1. Параметры-значения объявляются без модификатора. 2. Параметры, передаваемые по ссылке, используют модификатор ref. 3. Выходные параметры объявляются с модификатором out. 4. Параметры-списки применяют модификатор params. Параметры, передаваемые по ссылке и по значению, ведут себя аналогично тому, как это происходит в других языках программирования. Выходные параметры подобны ссылочным, т. е. при работе с ними в теле метода не создаётся копия фактического параметра. Компилятор отслеживает, чтобы выходным параметрам в теле метода обязательно было присвоено значение. Параметры-списки позволяют передать в метод любое количество аргументов. Метод может иметь не более одного параметра-списка, который обязательно должен быть последним в списке формальных параметров. Тип параметра-списка объявляется как одномерный массив, и работа с таким параметром происходит в методе как с массивом. Каждый аргумент из передаваемого в метод списка ведёт себя как параметр, переданный по значению. При объявлении метода для параметров-значений допустимо указать значение параметра по умолчанию. Это значение должно быть вычислимо на этапе компиляции. Так определяется опциональный параметр. Опциональные параметры должны быть указаны в конце списка формальных параметров метода. Для выхода из метода служит оператор return или оператор throw. Если тип метода не void, то после return обязательно указывается возвращаемое значение (тип этого значения должен совпадать с типом метода или неявно приводиться к нему). Кроме этого, оператор return или оператор throw должны встретиться в таком методе во всех ветвях кода хотя бы один раз. Исключением являются ветви метода, для которых компилятор распознаёт бесконечность выполнения (например, из-за бесконечного цикла). Рассмотрим несколько примеров объявления методов. 1. Простейшее объявление метода-процедуры без параметров: void SayHello() { Console.WriteLine("Hello!"); }

2. Метод без параметров, возвращающий целое значение: int ReturnInt() { 26

return 5; }

3. Функция Add() выполняет сложение двух аргументов: int Add(int a, int b) { return a + b; }

int ReturnTwoValues(out int a) { a = 100; return 10; }

БГ УИ Р

4. Функция ReturnTwoValues() возвращает 10 как результат своей работы, кроме этого значение параметра a устанавливается равным 100:

5. Метод PrintList() использует параметр-список:

ек а

void PrintList(params int[] list) { foreach(int item in list) { Console.WriteLine(item); } }

Би бл ио т

6. Метод AddWithOptional() имеет опциональный параметр y: int AddWithOptional(int x, int y = 5) { return x + y; }

В экземплярных методах доступен параметр this (в методах класса – только для чтения, в методах структуры – и для чтения, и для записи). Это ссылка на текущий экземпляр. Данную ссылку можно применять для устранения конфликта имён (если имя элемента типа совпадает с именем параметра метода): class Pet { private int age; private string name;

public void SetAge(int age) { this.age = age > 0 ? age : 0; } 27

}

C# позволяет выполнить в пользовательских типах перегрузку методов. Перегруженные методы имеют одинаковое имя, но разную сигнатуру. Сигнатура – это упорядоченный набор из модификаторов и типов формальных параметров. При выполнении перегрузки следует учитывать некоторые нюансы. Например, типы object и dynamic эквивалентны с точки зрения CLR. Значит, нельзя выполнить перегрузку методов, базируясь только на разнице этих типов:

БГ УИ Р

// код не компилируется – методы Foo() нельзя различить при вызове void Foo(object a) { . . . } void Foo(dynamic a) { . . . }

Если одна версия метода как признак отличия содержит модификатор ref, а другая – out, то вызов методов также будет неразличим с точки CLR: // код не компилируется – методы Foo() нельзя различить при вызове void Foo(out int a) { . . . } void Foo(ref int a) { . . . }

Однако если одна версия метода содержит модификатор ref или out, а другая нет, то методы различимы:

ек а

// код компилируется void Foo(out int a) { . . . } void Foo(int a) { . . . }

Би бл ио т

Наконец, если две версии перегруженного метода различаются только модификатором params, они считаются неразличимыми: // код не компилируется – методы Foo() нельзя различить при вызове void Foo(params int[] a) { . . . } void Foo(int[] a) { . . . }

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

Рассмотрим примеры вызова метода Add(), содержащего три параметра: int Add(int x, int y = 3, int z = 5) { return x + y + z; 28

} int res_1 = Add(10, 20, 30); int res_2 = Add(x:10, z:20, y:30); int res_3 = Add(10, z:20, y:30);

// передача по позиции // именованные параметры // комбинирование двух способов

Использование именованных аргументов зачастую необходимо, если метод содержит опциональные параметры: int res_4 = Add(10, z:20);

БГ УИ Р

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

ек а

// передаём два аргумента PrintList(10, 20); // теперь передаём четыре аргумента PrintList(1, 2, 3, 4); // создаём и передаём массив целых чисел PrintList(new[] {10, 20, 30, 40}); // можем вообще ничего не передавать PrintList();

Би бл ио т

Если при описании параметра использовались модификаторы ref или out, то они должны быть указаны и при вызове. Кроме этого, фактические аргументы с такими модификаторами должны быть представлены переменными, а не литералами или выражениями. В случае параметров-значений тип аргумента должен совпадать или неявно приводиться к типу формального параметра. При согласовании типов в случае возникновения двусмысленности делается выбор числового типа из той же группы знаковости. Например, пусть имеются перегруженные методы M(uint x) и M(int x), а переменная y имеет тип ushort. Тогда вызов M(y) означает вызов версии с формальным параметром типа uint. Для ref- и out-параметров требуется абсолютное совпадение типов. 9.3. Разделяемые методы Разделяемые классы и структуры могут содержать разделяемые методы. Разделяемый метод состоит из двух частей: заголовка и реализации. Обычно эти части размещаются в различных частях разделяемого типа: public partial class Student { partial void M(int x); } public partial class Student 29

{ partial void M(int x) { Console.WriteLine("M body"); } }

10. Свойства и индексаторы

БГ УИ Р

Разделяемые методы подчиняются следующим правилам: – объявление метода начинается с модификатора partial; – метод обязан возвращать значение void; – метод может иметь параметры, но out-параметры запрещены; – метод неявно объявляется как private; – разделяемые методы могут быть статическими или универсальными; – вызов разделяемого метода нельзя инкапсулировать в делегат. Отметим ещё одну особенность разделяемого метода: его реализация может быть опущена. В этом случае компилятор даже не генерирует код вызовов разделяемого метода.

ек а

Свойства класса призваны предоставить защищённый доступ к полям. Как и в большинстве объектно-ориентированных языков, в C# непосредственная работа с полями не приветствуется. Поля класса обычно объявляются с модификатором private, а для доступа к ним используются свойства. Рассмотрим базовый синтаксис описания свойства:

Би бл ио т

модификаторы тип-свойства имя-свойства { get { операторы } set { операторы } }

Синтаксис описания заголовка свойства напоминает синтаксис описания обычного поля. Тип свойства обычно совпадает с типом того поля, для обслуживания которого свойство создаётся. У свойства присутствует специальный блок, содержащий методы для доступа к свойству. Данный блок состоит из getчасти и set-части, далее называемых аксессор и мутатор соответственно. Одна из частей может отсутствовать, так получается свойство только для чтения или свойство только для записи. Аксессор отвечает за возвращаемое свойством значение и работает как функция. Мутатор работает как процедура, устанавливающая значение свойства. Считается, что параметр, передаваемый в мутатор, имеет специальное имя value. Рассмотрим пример класса, имеющего свойства: public class Student { private int _age; 30

private string _name; public int Age { get { return _age; } set { _age = value > 0 ? value : 0; } }

БГ УИ Р

public string Name { get { return "My name is " + _name; } set { _name = value; } } }

Би бл ио т

ек а

Свойства транслируются при компиляции в вызовы методов. В скомпилированный код класса добавляются методы со специальными именами get_Name() и set_Name(), где Name – это имя свойства. Побочным эффектом трансляции является то, что пользовательские методы с данными именами допустимы в классе, только если они имеют сигнатуру, отличающуюся от методов, соответствующих свойству. Как правило, свойства открыты, т. е. снабжаются модификатором доступа public. Однако иногда логика класса требует разделения права доступа чтения и записи свойства. Например, чтение позволено всем, а запись – только из методов того класса, где свойство объявлено. В C# разрешено при описании свойства указывать модификаторы доступа для аксессоров и мутаторов. При этом действуют два правила. Во-первых, модификатор может быть только у одной из частей. Во-вторых, он должен понижать видимость части по сравнению с видимостью всего свойства: public class SomeClass { public int Prop { get { return 0; } private set { } } }

Достаточно часто свойство содержит только простейший код доступа к полю. Вот фрагмент класса с таким свойством: public class Person { private string _name; public string Name { 31

get { return _name; } set { _name = value; } } }

Чтобы облегчить описание таких свойств-обёрток, в C# имеются автосвойства (auto properties). Используя автосвойства, приведённый фрагмент кода можно переписать следующим образом:

БГ УИ Р

public class Person { public string Name { get; set; } }

В этом случае компилятор сам сгенерирует необходимое поле класса, связанное со свойством. Обратите внимание: в автосвойстве должны присутствовать и часть get, и часть set. При необходимости получить аналог классических свойств только для чтения необходимо использовать модификаторы доступа для частей:

ек а

public class Person { public string Name { get; private set; } }

Би бл ио т

Кроме скалярных свойств язык C# поддерживает индексаторы. При помощи индексаторов осуществляется доступ к коллекции данных, содержащихся в объекте, с использованием привычного синтаксиса для доступа к элементам массива – пары квадратных скобок и индекса. Объявление индексатора напоминает объявление обычного свойства: модификаторы тип this[параметры] { get-и-set-блоки }

Параметры индексатора служат для описания типа и имён индексов, применяемых для доступа к данным. Параметры индексатора могут быть описаны как параметры-значения или как параметр-список (с использованием params). Также допустимо использование опциональных параметров. Рассмотрим пример класса, содержащего индексатор. Пусть данный класс описывает студента с набором оценок13: public class Student { private readonly int[] _marks = new int[5]; public int this[int i] В приведённом примере не производится проверка корректности индекса и значения оценки. 13

32

{ get { return _marks[i]; } set { _marks[i] = value; } } }

var student = new Student(); student[1] = 8; student[3] = 4; for (int i = 0; i < 5; i++) Console.WriteLine(student[i]);

БГ УИ Р

При использовании индексатора указывается имя объекта и значение индекса (или индексов) в квадратных скобках. Допустимы именованные индексы (по аналогии с именованными аргументами метода). Если необходимо использовать индексатор в пределах класса, применяют синтаксис this[значение].

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

11. Статические элементы и методы расширения

ек а

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

Би бл ио т

11.1. Статические элементы Статические элементы предназначены для работы не с объектами, а с классом. Статические поля хранят информацию, общую для всех объектов, статические методы либо вообще не используют поля, либо работают только со статическими полями. Чтобы объявить статический элемент, применяется модификатор static: public class Account { private static double _tax = 0.1; public static double GetTax() { return _tax * 100; }

}

Для вызова статических элементов требуется использовать имя класса: Console.WriteLine(Account.GetTax()); Индексаторы транслируются в методы с именами get_Item() и set_Item(). Изменить имена методов можно, используя атрибут [IndexerName]. 14

33

Подчеркнём, что статическими могут быть сделаны поля, методы и обычные свойства. Открытая константа, описанная в классе, уже работает как статический элемент. Индексатор класса не может быть статическим15. 11.2. Статические классы Если класс содержит только статические элементы, то при объявлении класса можно указать модификатор static. Так определяется статический класс:

БГ УИ Р

public static class ApplicationSettings { public static string BaseDir { } public static string GetRelativeDir() { } }

ек а

Экземпляр статического класса не может быть создан или даже объявлен в программе. Для таких классов запрещено наследование. Все открытые элементы статического класса доступны только с использованием имени класса. Упомянем некоторые полезные статические классы из пространства имён System. Для преобразования данных различных типов удобно использовать класс Convert. Класс Math содержит большой набор различных математических функций. Класс Console предназначен для чтения и записи информации на консоль, а также для настройки консоли. Класс Environment содержит полезные свойства, описывающие окружение запуска программы (например, Version – текущая версия платформы .NET).

Би бл ио т

11.3. Методы расширения В C# 3.0 была представлена концепция метода расширения (extension method). Методы расширения позволяют «добавлять» методы в существующие типы без создания нового производного типа, перекомпиляции или иного изменения исходного типа. Методы расширения являются особым видом статического метода, но они вызываются, как если бы они были методами экземпляра в расширенном типе. Для клиентского кода нет видимого различия между вызовом метода расширения и вызовом методов, фактически определённых в типе. Рассмотрим следующий пример. Пусть разрабатывается программный проект, в различных частях которого требуется подсчёт суммы элементов целочисленного массива. Для реализации данной задачи создан специальный вспомогательный класс, содержащий статический метод подсчёта: public static class ArrayHelper { public static int Sum(int[] array) { var result = 0; foreach (var item in array) 15

В отличие от языка C#, CLR позволяет создавать статические индексаторы.

34

{ result += item; } return result; } } // использование метода Sum() int[] m = {3, 4, 6}; Console.WriteLine(ArrayHelper.Sum(m));

БГ УИ Р

Превратим Sum() в метод расширения. Для этого достаточно добавить ключевое слово this перед первым аргументом метода: public static class ArrayHelper { public static int Sum(this int[] array) { . . . } }

Теперь метод Sum() можно вызывать как традиционным способом, так и как «экземплярный» метод массива:

ек а

int[] m = {3, 4, 6}; Console.WriteLine(m.Sum());

Би бл ио т

Подчеркнём, что методами расширения могут быть только статические методы16 статических классов. Количество параметров такого метода может быть любым (один и более), но только первый следует указывать с модификатором this. Соответственно, метод расширит тип первого параметра. Методы расширения применимы к типу, как только импортируется пространство имён, содержащее класс с этими методами расширения. Если выполняется импорт нескольких пространств имён, содержащих классы с одинаковыми методами расширения для одного типа, то возникает ошибка компиляции. В этом случае методы расширения должны использоваться как обычные статические методы вспомогательных классов.

12. Конструкторы и инициализация объектов Конструктор выполняет начальную инициализацию объекта или класса. Синтаксис описания конструктора напоминает синтаксис описания метода. Однако имя конструктора всегда совпадает с именем класса, а любое указание на тип возвращаемого значения отсутствует (даже void). Задача экземплярных конструкторов – создание и инициализация объекта. Любой экземплярный конструктор в начале своей работы выполняет размещеЯзык C# допускает только методы расширения, свойств и индексаторов расширения не существует. 16

35

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

БГ УИ Р

// класс Pet не содержит описания конструктора public class Pet { public int Age; public string Name = "I'm a pet"; } var dog = new Pet(); // вызов конструктора по умолчанию Console.WriteLine(dog.Age); // выводит "0" Console.WriteLine(dog.Name); // выводит "I'm a pet"

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

Би бл ио т

ек а

// класс Pet содержит два пользовательских конструктора public class Pet { public int Age; public string Name; public Pet() { Name = "I'm a pet"; }

public Pet(int age, string name) { Age = age; Name = name; }

}

В приведённом примере можно было ограничиться одним пользовательским конструктором, если использовать значения параметров по умолчанию: public class Pet { public int Age; public string Name; public Pet(int age = 0, string name = "I'm a pet") 36

{ Age = age; Name = name; } }

БГ УИ Р

Пользовательские конструкторы могут применяться для начальной инициализации readonly-полей (т. е. readonly-поля доступны для записи, но только в конструкторе). Пользовательский конструктор может вызвать другой конструктор того же класса, но только в начале своей работы. Для этого при описании конструктора используется синтаксис, аналогичный приведённому в следующем примере: public Pet() : this(10, "Pet") { . . . }

Для вызова экземплярных конструкторов используется операция new, которая возвращает созданный объект. У объекта нельзя вызвать конструктор как метод (т. е. в виде имя-объекта.имя-конструктора). // вызов конструкторов для создания объекта var cat = new Pet(); var dog = new Pet(5, "Buddy");

Би бл ио т

ек а

Статические конструкторы используются для начальной инициализации статических полей класса. Статический конструктор всегда объявляется с модификатором static и без параметров. Область видимости у статических конструкторов не указывается. В теле статического конструктора возможна работа только со статическими полями и методами класса. Статический конструктор не может вызывать экземплярные конструкторы в начале своей работы. public class Account { private static double _tax; static Account() { _tax = 0.1; }

}

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

Если класс содержит статический конструктор (возможно, пустой), компилятор C# генерирует код, выполняющий инициализацию статических полей класса непосредственно перед первым использованием класса. Без статического конструктора такая инициализация проводится в произвольный момент до использования класса. 17

37

– перед созданием первого объекта класса или при первом обращении к элементу класса, не унаследованному от предка; – перед первым обращением к статическому полю, не унаследованному от предка. При работе с объектами достаточно типичной является ситуация, когда объект вначале создаётся, а затем настраивается путём установки свойств: var s = new Student(); s.Name = "Mr. Spiderman"; s.Age = 18;

БГ УИ Р

C# позволяет совместить создание объекта с его настройкой. Для этого после параметров конструктора в фигурных скобках требуемым public-элементам класса (обычно это свойства) присваиваются их значения (если конструктор не имел параметров, можно не указывать круглые скобки после его имени): var s = new Student {Name = "Mr. Spiderman", Age = 18};

ек а

Инициализация объектов действует и для классов-коллекций. Предполагается, что такой класс реализует интерфейс IEnumerable и имеет public-метод Add(). Именно этот метод вызывает компилятор, когда обрабатывает код инициализации: var x = new List {1, 4, 8, 8}; var y = new List {"There", "is", "no", "spoon"};

Би бл ио т

Инициализация коллекций работает, даже если у метода Add() несколько параметров. В таком случае эти параметры записываются в фигурных скобках: var cars = new Dictionary {{1, "Ford"}, {2, "Opel"}};

13. Наследование классов

Язык C# полностью поддерживает объектно-ориентированную концепцию наследования для классов. Чтобы указать, что один класс является наследником другого, используется следующий синтаксис: class имя-класса-наследника : имя-класса-предка {тело-класса}

Наследование от двух и более классов в C# и CLR запрещено. Наследник обладает всеми полями, методами и свойствами предка, но элементы предка с модификатором private не доступны в наследнике. Конструкторы классапредка не переносятся в класс-наследник. При наследовании также нельзя расширить область видимости класса, т. е. internal-класс может наследоваться от public-класса, но не наоборот. Для объектов класса-наследника определено неявное преобразование к типу класса-предка. C# содержит две специальные операции, связанные с контролем типов при наследовании. Выражение x is T возвращает значение true, ес38

ли типом x является или тип T, или наследник типа T. Выражение x as T возвращает объект, приведённый к типу T, если это возможно, и null в противном случае. Для обращения к методам класса-предка класс-наследник может использовать ключевое слово base в форме base.метод-базового-класса. Если конструктор наследника должен вызвать конструктор предка, то для этого также используется ключевое слово base: конструктор-наследника([параметры]) : base([параметры_2])

ек а

БГ УИ Р

Для конструкторов производного класса справедливо следующее замечание: в начале работы конструктор должен совершить вызов другого конструктора своего или базового класса. Если вызов конструктора базового класса отсутствует, компилятор автоматически подставляет в заголовок конструктора вызов base(). Если в базовом классе нет конструктора без параметров, происходит ошибка компиляции. Для классов можно указать два модификатора, связанных с наследованием. Модификатор sealed определяет запечатанный класс, от которого запрещено наследование. Модификатор abstract определяет абстрактный класс, у которого обязательно должны быть наследники. Объект абстрактного класса создать нельзя, хотя статические элементы такого класса можно вызвать: sealed class SealedClass { } abstract class AbstractClass { }

Би бл ио т

Класс-наследник может дополнять базовый класс новыми элементами, а может замещать элементы базового класса. Для замещения нужно указать в новом классе элемент с прежним именем и, возможно, новой сигнатурой: public class Pet { public void Speak() { Console.WriteLine("I'm a pet"); } } public class Dog : Pet { public void Speak() { Console.WriteLine("I'm a dog"); } }

При компиляции данного фрагмента будет получено предупреждающее сообщение о том, что метод Dog.Speak() закрывает метод базового класса Pet.Speak(). Чтобы подчеркнуть, что метод класса-наследника сознательно замещает метод базового класса, используется ключевое слово new: public class Dog : Pet { public new void Speak() { Console.WriteLine("I'm a dog"); } } 39

При замещении методов с изменением типов параметров метод базового класса вызывается только в том случае, если компилятор не может подобрать метод производного класса, выполняя неявное приведение типов: public class A { public void Do(int x) { Console.WriteLine("A.Do()"); } }

B x = new B(); x.Do(3);

БГ УИ Р

public class B : A { public void Do(double x) { Console.WriteLine("B.Do()"); } }

// печатает "B.Do()"

Замещение методов класса не является полиморфным по умолчанию. Следующий фрагмент кода печатает две одинаковые строки: // объект класса Dog присвоен объекту Pet // печатает "I'm a pet" // также печатает "I'm a pet"

ек а

Pet pet = new Pet(); Pet dog = new Dog(); pet.Speak(); dog.Speak();

Би бл ио т

Для организации полиморфного вызова применяется два модификатора: virtual указывается для метода базового класса, который мы хотим сделать полиморфным, override – для методов производных классов. Эти методы должны совпадать по имени, типу и сигнатуре с перекрываемым методом класса-предка. public class Pet { public virtual void Speak() { Console.WriteLine("I'm a pet"); } } public class Dog : Pet { public override void Speak() { Console.WriteLine("I'm a dog"); } } Pet pet = new Pet(); Pet dog = new Dog(); pet.Speak(); dog.Speak();

// печатает "I'm a pet" // печатает "I'm a dog"

При описании метода возможно совместное указание модификаторов new и virtual. Такой приём создаёт новую полиморфную цепочку замещения: 40

public class A { public virtual void Do() { Console.WriteLine("A.Do()"); } } public class B : A { public override void Do() { Console.WriteLine("B.Do()"); } }

БГ УИ Р

public class C : A { public new virtual void Do() { Console.WriteLine("C.Do()"); } } A[] x = {new A(), new B(), new C()}; x[0].Do(); // печатает "A.Do()" x[1].Do(); // печатает "B.Do()" x[2].Do(); // печатает "A.Do()"

ек а

Если на некоторой стадии построения иерархии классов требуется запретить дальнейшее переопределение виртуального метода в производных классах, этот метод помечается ключевым словом sealed:

Би бл ио т

public class Dog : Pet { public override sealed void Speak() { } }

Для методов, объявленных в абстрактных классах, можно применить модификатор abstract, который говорит о том, что метод не реализуется в классе, не содержит тела и должен обязательно переопределяться в наследнике (в такой ситуации модификатор abstract эквивалентен модификатору virtual). public abstract class AbstractClass { // реализации метода в классе нет public abstract void AbstractMethod(); }

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

41

14. Класс System.Object и иерархия типов Диаграмма, показанная на рис. 3, связывает типы платформы .NET с точки зрения отношения наследования. SByte Массив A[]

Byte Int16

Array

Класс C

Структура S

UInt16

String

Object

БГ УИ Р

Int32 ValueType

UInt32

Int64

Delegate

Enum

UInt64

Single

MulticastDelegate

Char

Перечисление E

Double

Делегат D

ек а

Boolean

Decimal

Рис. 3. Иерархия типов платформы .NET

Би бл ио т

Все типы в .NET Framework наследуются (прямо или косвенно) от класса System.Object18 (в C# для этого типа используется псевдоним object). Тип System.ValueType является предком всех типов значений (включая числовые типы, пользовательские структуры и перечисления). Массивы наследуются от класса System.Array, а класс System.Delegate является предком всех делегатов. Рассмотрим элементы класса System.Object в алфавитном порядке. public virtual bool Equals(object obj)

Данный метод определяет, равен ли объект obj текущему объекту. Реализация Equals() по умолчанию обеспечивает равенство ссылок для ссылочных типов и побитовое равенство для типов значений. Пользовательский тип может переопределять метод Equals(). При этом должны выполняться следующие правила: 1. x.Equals(x) == true. 2. x.Equals(y) == y.Equals(x). 3. (x.Equals(y) && y.Equals(z)) == true ⟹ x.Equals(z) == true. Формально, от object не наследуются типы-указатели, используемые в неуправляемом коде (например, int*), а также интерфейсы (но интерфейсы приводятся к object). 18

42

БГ УИ Р

4. Вызовы метода x.Equals(y) возвращают одинаковое значение до тех пор, пока объекты x и y остаются неизменными. 5. x.Equals(null) == false, если x != null. 6. Метод Equals() не должен генерировать исключений. Типы, переопределяющие метод Equals(), должны также переопределять метод GetHashCode() (и наоборот); в противном случае коллекции-словари могут работать неправильно. Если применяется перегрузка операции равенства для заданного типа, то этот тип также должен переопределять и метод Equals(). Реализация Equals() должна возвращать те же результаты, что и перегруженная операция равенства. public static bool Equals(object a, object b)

Метод определяет, равны ли экземпляры a и b. Если оба аргумента равны null, метод возвращает true. Если только один аргумент равен null, возвращается false. Если оба аргумента не равны null, возвращается a.Equals(b). protected virtual void Finalize()

ек а

Метод Finalize() позволяет объекту попытаться освободить ресурсы и выполнить другие операции очистки, перед тем как объект будет утилизирован в процессе сборки мусора. public virtual int GetHashCode()

Би бл ио т

Метод GetHashCode() играет роль хеш-функции для определённого типа. Этот метод можно использовать в алгоритмах хеширования и таких структурах данных, как хеш-таблицы. Реализация метода GetHashCode() по умолчанию не гарантирует уникальность возвращаемых кодов. Пользовательские типы могут переопределять данный метод для эффективного вычисления хеш-функции. Если два объекта при сравнении оказались равны, методы GetHashCode() этих объектов должны возвращать одинаковые значения. Однако если при сравнении оказалось, что объекты не равны, методы GetHashCode() не обязательно должны возвращать разные значения. public Type GetType()

Данный метод возвращает объект System.Type для текущего экземпляра. Объект System.Type содержит метаданные, связанные с классом текущего экземпляра. protected object MemberwiseClone()

Метод MemberwiseClone() применяется для создания неполной копии объекта. Метод создаёт новый объект (конструктор при этом не вызывается), а затем копирует в него нестатические поля текущего объекта. Если поле относится к типу значения, выполняется побитовое копирование полей. Если поле отно43

сится к ссылочному типу, копируются ссылки, а не объекты, на которые они указывают. Следовательно, ссылки в исходном объекте и его клоне указывают на один и тот же объект. public static bool ReferenceEquals(object a, object b)

Этот статический метод возвращает значение true, если параметр a соответствует тому же экземпляру, что и параметр b, или же оба они равны null; в противном случае метод возвращает false. public virtual string ToString()

object o = 123; int j = (int)o;

ек а

БГ УИ Р

Метод ToString() возвращает строку, которой представлен текущий объект. Метод может быть переопределён в производном классе для возврата адекватных значений для данного типа. Так как System.Object является предком любого типа, переменной типа object можно присвоить любую переменную. Если для ссылочных типов при этом происходит только присваивание указателей, для типов значений выполняется специальная операция, называемая операцией упаковки (boxing)19. При упаковке в динамической памяти создаётся объект, содержащий значение переменной и информацию о её типе. Упакованный объект можно подвергнуть обратному преобразованию – операции распаковки (unboxing). // операция упаковки // операция распаковки

Би бл ио т

По форме операция распаковка выглядит как приведение типов, однако таковой не является. Следующий код при выполнении генерирует исключение: object o = 123; short j = (short)o;

// операция упаковки литерала int // генерируется InvalidCastException

При распаковке необходимо указывать точный тип упакованного объекта: short j = (short)(int)o;

// распаковка, затем приведение типов

15. Структуры

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

Операция упаковки выполняется и в случае, когда переменной типа интерфейс присваивается переменная типа значения. Этот аспект будет разобран при рассмотрении интерфейсов. 19

44

модификаторы struct имя-структуры { элементы-структуры }

БГ УИ Р

При описании экземплярных полей структуры следует учитывать, что они не могут быть инициализированы при объявлении (для статических полей инициализация при объявлении возможна). Как и класс, структура может содержать конструкторы. В структуре можно объявить статический конструктор20 или экземплярный конструктор с параметрами, причём в теле конструктора необходимо инициализировать все поля структуры. Ещё одно отличие структуры от класса – в структуре указатель на экземпляр this доступен не только для чтения, но и для записи. Рассмотрим пример структуры для представления точки в пространстве: public struct Point3D { public readonly double X, Y, Z;

ек а

public Point3D(double x, double y, double z = 0.0) { X = x; Y = y; Z = z; }

Би бл ио т

public Point3D(Point3D point) { this = point; }

}

Если в типе объявляется поле-структура, все элементы структуры получат значения по умолчанию. Аналогичная ситуация будет при объявлении локальной переменной-структуры и вызове конструктора структуры без параметров21. Без вызова конструктора поля переменной-структуры не инициализированы. // поля p1 не инициализированы, их надо установить до использования Point3D p1; // поля p2 инициализированы значениями 0.0 Point3D p2 = new Point3D();

CLR гарантирует, что статический конструктор структуры будет запущен до первого обращения к статическому элементу структуры. Вызов экземплярного конструктора с параметрами также приводит к запуску статического конструктора. Однако статический конструктор не запускается при вызове экземплярного конструктора без параметров. 21 В отличие от классов, в структуре конструктор без параметров присутствует даже при объявлении пользовательского конструктора. 20

45

// поля p3 инициализированы значениями 2.0, 3.0, 0.0 Point3D p3 = new Point3D(2.0, 3.0);

Локальные переменные структурного типа размещаются в стеке приложения. Структурные переменные можно присваивать друг другу, при этом выполняется копирование данных структуры на уровне полей. Все структуры наследуются от класса System.ValueType. Класс ValueType переопределяет некоторые методы класса Object. В частности, переопределяется метод Equals() для сравнения объектов путём сравнения их полей.

16. Перечисления

БГ УИ Р

Перечисление – это тип, содержащий в качестве элементов именованные целочисленные константы. Рассмотрим синтаксис определения перечисления: модификаторы enum имя-перечисления [: тип-элемента] { элемент-перечисления-1 [= значение-элемента], . . . элемент-перечисления-N [= значение-элемента] }

Би бл ио т

ек а

Перечисление может предваряться модификатором доступа. Если указан тип-элемента, то он определяет тип каждого элемента перечисления. Допустимы типы byte, sbyte, short, ushort, int, uint, long, ulong (причём нужно использовать именно псевдоним типа C#). По умолчанию применяется тип int. Для элементов перечисления область видимости указать нельзя. Значением элемента перечисления должна быть целочисленная константа. Если значение не указано, элемент будет на единицу больше предыдущего элемента (первый элемент принимает значение 0). Заданные значения элементов могут повторяться. Приведём примеры перечислений: public enum Season { Winter, Spring, Summer, Autumn } public enum ErrorCode : byte { First = 1, Fourth = 4 }

После описания перечисления можно объявить переменную соответствующего типа: Season s = Season.Spring; Console.WriteLine(s); // выводит на печать Spring

Переменные перечисления поддерживают следующие операции: ==, !=, , =, бинарные + и – (с ограничением на тип операндов и результата), ^, &, |, 46

~, ++, --. При помощи явного преобразования типов переменной перечисления

можно присвоить значение, которое в перечислении не описано: Season p = Season.Winter + 3; int x = Season.Autumn - Season.Summer; Season r = Season.Autumn - 2; Season s = (Season)30;

Класс System.Enum является базовым для всех перечислений. Табл. 4 содержит описание некоторых методов класса System.Enum. Таблица 4

GetNames() GetUnderlyingType() GetValues() HasFlag() IsDefined() Parse()

Би бл ио т

TryParse()

Описание Возвращает строку с именем элемента для укастатический занного типа и значения перечисления Возвращает массив строк с именами элементов статический для указанного типа перечисления статический Получает тип элемента перечисления Возвращает массив значений элементов для статический указанного типа перечисления Возвращает true, если перечисление содержит экземплярный заданные флаги (т. е. набор значений) Возвращает true, если указанный элемент состатический держится в заданном типе перечисления Конвертирует строку с именем элемента в пестатический ременную перечисления Делает попытку конвертирования строки в пестатический ременную перечисления

ек а

GetName()

Категория

БГ УИ Р

Некоторые методы System.Enum Имя метода

17. Интерфейсы

Согласно общей парадигме ООП, интерфейс (interface) – это элемент языка, который служит для специфицирования услуг, предоставляемых классом. Класс может реализовывать интерфейс. Реализация интерфейса заключается в том, что в описании класса данный интерфейс указывается как реализуемый, а в коде класса обязательно определяются все методы, которые имеет интерфейс. Один класс может реализовать несколько интерфейсов одновременно. Возможно объявление переменных как имеющих тип-интерфейс. В такую переменную может быть записан экземпляр любого класса, реализующего интерфейс. Таким образом, с одной стороны, интерфейс – это «договор», который обязуется выполнить класс, реализующий его, с другой стороны, интерфейс – это пользовательский тип, потому что его описание достаточно чётко определяет характеристики объектов, чтобы наравне с классом типизировать переменные22. Интерфейс не является полноценным типом, т. к. он задаёт только внешнее поведение объектов. Внутреннюю структуру и реализацию поведения обеспечивает класс, реализующий интерфейс; именно поэтому «экземпляров интерфейса» в чистом виде не бывает, любая переменная типа «интерфейс» содержит экземпляры конкретных классов. 22

47

public interface IFlyable { void Fly(); double Speed { get; set; } }

БГ УИ Р

Использование интерфейсов – один из вариантов обеспечения полиморфизма в объектных языках. Все классы, реализующие один и тот же интерфейс, с точки зрения определяемого ими поведения, ведут себя внешне одинаково. Это позволяет писать обобщённые алгоритмы обработки данных, использующие в качестве типов параметры интерфейсов, и применять их к объектам различных типов, всякий раз получая требуемый результат. Язык C# следует представленной выше концепции интерфейсов. В C# интерфейсы могут реализовывать не только классы, но и структуры. Поддерживается множественное наследование интерфейсов. Для объявления интерфейса используется ключевое слово interface. Интерфейс содержит только заголовки методов, свойств и событий. Для свойства указываются только ключевые слова get и (или) set. При объявлении элементов интерфейса не могут использоваться следующие модификаторы: abstract, public, protected, internal, private, virtual, override, static. Считается, что все элементы интерфейса имеют public-уровень доступа:

// метод // свойство

Би бл ио т

ек а

Чтобы указать, что тип реализует некий интерфейс, используется синтаксис имя-типа : имя-интерфейса при записи заголовка типа. Если класс является производным от некоторого базового класса, то имя базового класса указывается перед именем реализуемого интерфейса. Элементы интерфейса допускают явную и неявную реализацию. При неявной реализации тип должен содержать открытые экземплярные элементы, имена и сигнатура которых соответствуют элементам интерфейса. При явной реализации элемент типа называется по форме имя-интерфейса.имя-элемента, а указание любых модификаторов для элемента при этом запрещается. public class Falcon : IFlyable { // неявная реализация интерфейса IFlyable public void Fly() { Console.WriteLine("Falcon flies"); } public double Speed { get; set; } } public class Eagle : IFlyable { // обычный метод public void PrintType() { Console.WriteLine("Eagle"); } // явная реализация интерфейса IFlyable void IFlyable.Fly() { Console.WriteLine("Eagle flies"); } double IFlyable.Speed { get; set; } } 48

Если тип реализует некоторые элементы интерфейса явно, то такие элементы будут недоступны через переменную типа. Допустимо объявить переменную интерфейса, которая может содержать значение любого типа, реализующего интерфейс (для структур будет выполнена операция упаковки). Через переменную интерфейса можно вызывать только элементы интерфейса. // // // //

обычное создание объекта у объекта доступен только этот метод переменная интерфейса получили доступ к элементам интерфейса

БГ УИ Р

Eagle eagle = new Eagle(); eagle.PrintType(); IFlyable x = eagle; x.Fly(); x.Speed = 60;

Все неявно реализуемые элементы интерфейса по умолчанию помечаются в классе как sealed. А значит, наследование классов не ведёт к прямому наследованию реализаций: public interface ISimple { void M(); }

ек а

public class Base : ISimple { public void M() { Console.Write("Base.M()"); } }

Би бл ио т

public class Descendant : Base { public void M() { Console.Write("Descendant.M()"); } } Base x = new Base(); Descendant y = new Descendant(); ISimple xi = x, yi = y; x.M(); y.M(); xi.M(); yi.M();

// // // //

печатает печатает печатает печатает

"Base.M()" "Descendant.M()" "Base.M()" "Base.M()"

Чтобы осуществить наследование реализаций, требуется при неявной реализации использовать модификаторы virtual и override (при явной реализации использование этих модификаторов невозможно): public class Base : ISimple { public virtual void M() { Console.Write("Base.M()"); } }

49

public class Descendant : Base { public override void M() { Console.Write("Descendant.M()"); } }

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

18. Универсальные шаблоны

БГ УИ Р

Универсальные шаблоны (generics) позволяют при разработке пользовательского типа или метода указать в качестве параметра тип, который конкретизируется при использовании. Универсальные шаблоны применимы к классам, структурам, интерфейсам, делегатам и методам.

ек а

18.1. Универсальные классы и структуры Поясним необходимость универсальных шаблонов на следующем примере. Пусть разрабатывается класс для представления структуры данных «стек». Чтобы не создавать отдельные версии стека для хранения данных определённых типов, программист выбирает базовый тип object как тип элемента:

Би бл ио т

public class Stack { private object[] _items; public void Push(object item) { . . . } public object Pop() { . . . } }

Класс Stack можно использовать для разных типов данных: var stack = new Stack(); stack.Push(new Customer()); Customer c = (Customer)stack.Pop(); var stack2 = new Stack(); stack2.Push(3); int i = (int)stack2.Pop();

Однако универсальность класса Stack имеет и отрицательные моменты. При извлечении данных из стека необходимо выполнять приведение типов. Для типов значений (например, int) при помещении данных в стек и при извлечении выполняются операции упаковки и распаковки, что отрицательно сказывается на производительности. И, наконец, неверный тип помещаемого в стек элемента может быть выявлен только на этапе выполнения, но не компиляции. var stack = new Stack(); stack.Push(1); 50

// планируем сделать стек чисел

stack.Push(2); stack.Push("three"); // вставили не число, а строку var sum = 0; for (var i = 0; i < 3; i++) { // код компилируется, но при выполнении на третьей итерации // будет сгенерирована исключительная ситуация sum += (int)stack.Pop(); }

БГ УИ Р

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

ек а

public class Stack { private T[] _items; public void Push(T item) { . . . } public T Pop() { . . . } }

Би бл ио т

Использовать универсальный тип «как есть» в клиентском коде нельзя, т. к. он является не типом, а, скорее, «чертежом» типа. Для работы со Stack необходимо использовать сконструированный тип (constructed type), указав в угловых скобках аргумент типа. Аргумент-тип может быть любым типом. Можно создать любое количество экземпляров сконструированных типов, и каждый из них может использовать разные аргументы типа. Stack stack = new Stack(); stack.Push(3); int x = stack.Pop();

Обратите внимание: при работе с типом Stack отпала необходимость в выполнении приведения типов при извлечении элементов из стека. Кроме этого, теперь компилятор отслеживает, чтобы в стек помещались только данные типа int. И ещё одна особенность: нет необходимости в упаковке и распаковке типа значения, а это приводит к росту производительности. Подчеркнём некоторые особенности сконструированных типов. Вопервых, сконструированный тип не связан отношением наследования с универсальным типом. Во-вторых, даже если классы A и B связаны наследованием, сконструированные типы на их основе этой связи лишены. В-третьих, статические поля и статический конструктор, описанные в универсальном типе, уникальны для каждого сконструированного типа.

51

При объявлении универсального шаблона можно использовать несколько параметров-типов. Приведём фрагмент описания класса для хранения пар «ключ-значение» с возможностью доступа к значению по ключу: public class Dictionary { public void Add(K key, V value) { . . . } public V this[K key] { . . . } }

БГ УИ Р

Сконструированный тип для Dictionary должен быть основан на двух аргументах-типах: Dictionary dict = new Dictionary();

В языке C# существует операция default, которая возвращает значение по умолчанию для переменной указанного типа. Эта операция может использоваться в тех методах, где возвращаемое значение задано как параметр типа:

Би бл ио т

ек а

public class Cache { // метод для поиска элемента по ключу public V LookupItem(K key) { // если элемент не найден, вернём значение по умолчанию return ContainsKey(key) ? GetValue(key) : default(V); } }

18.2. Ограничения на параметры универсальных типов Как правило, универсальные типы не просто хранят данные, но и вызывают методы у объекта, чей тип указан как параметр. Например, в классе Dictionary метод Add() может использовать метод CompareTo() для сравнения ключей: public class Dictionary { public void Add(K key, V value) { . . . if (key.CompareTo(x) < 0) { . . . } . . . } }

// ошибка компиляции!

Ошибка компиляции в этом примере возникает по следующей причине. Так как тип K может быть любым, то у параметра key можно вызывать только методы, определённые в object. Проблему можно решить, используя приведение типов: 52

// переписана строка, вызывавшая ранее ошибку компиляции if (((IComparable) key).CompareTo(x) < 0) { . . . }

Би бл ио т

ек а

БГ УИ Р

Недостаток такого подхода – многочисленность операций приведения. К тому же, если у сконструированного типа параметр K не поддерживает интерфейс IComparable, то при работе программы будет сгенерировано исключение InvalidCastException. C# допускает указание ограничений (constraints) для каждого параметра универсального типа. Только тип, удовлетворяющий ограничениям, может быть применён для записи сконструированного типа. Ограничения делятся на первичные ограничения, вторичные ограничения и ограничения конструктора. Первичное ограничение – это некий тип. Аргумент сконструированного типа должен приводиться к первичному ограничению. Для первичного ограничения нельзя использовать типы System.Object, System.Array, System.Delegate, System.MulticastDelegate, System.ValueType, System.Enum и System.Void. Первичное ограничение не может быть запечатанным типом. Существуют два особых первичных ограничения – class и struct. Ограничению class удовлетворяет любой ссылочный тип – класс, интерфейс, делегат. Ограничению struct удовлетворяет любой тип значения, за исключением типов с поддержкой null. Вторичное ограничение – это интерфейс. Вторичное ограничение требует, чтобы аргумент сконструированного типа реализовывал указанный интерфейс. Ограничение конструктора имеет вид new() и требует, чтобы аргумент сконструированного типа имел конструктор без параметров. Ограничения объявляются с использованием ключевого слова where, после которого указывается параметр универсального типа, двоеточие и список ограничений: – ноль или одно первичное ограничение; – ноль или несколько вторичных ограничений; – ноль или одно ограничение конструктора (если не задано первичное ограничение struct). В следующем примере демонстрируется использование ограничений на различные параметры универсального типа: public class EntityTable where K : IComparable, IPersistable where E : Entity, new() { public void Add(K key, E entity) { . . . if (key.CompareTo(x) < 0) { . . . } . . . } } 53

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

БГ УИ Р

void PushMultiple(Stack stack, params T[] values) { foreach (T value in values) { stack.Push(value); } }

Использование универсального метода PushMultiple позволяет работать с любым сконструированным типом на основе Stack. Stack stack = new Stack(); PushMultiple(stack, 1, 2, 3, 4);

ек а

В большинстве ситуаций компилятор способен самостоятельно сконструировать тип универсального метода на основе анализа типов фактических параметров. Это позволяет записывать вызов метода без указания типа:

Би бл ио т

var stack = new Stack(); PushMultiple(stack, 1, 2, 3, 4);

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

public class C { public static void M(string a, string b) { . . . } public static void M(T a, T b) { . . . } } int a = 10; byte b = 20; string s = "ABC"; C.M(a, b); C.M(s, s); C.M(s, s);

// C.M(a,b) // C.M(s,s) // C.M(s,s)

Универсальные методы могут заменить перекрытие методов в пользовательском типе, если алгоритмы работы различных версий перекрытых методов не зависят от типов параметров. Хотя свойства и индексаторы транслируются в вызов методов, в языке C# не существует универсальных свойств и универсальных индексаторов. 23

54

В первом случае компилятор сконструирует метод M() как M(), потому что к типу int приводится и переменная a, и переменная b. Второй вызов показывает, что при наличии альтернатив компилятор всегда выбирает версию метода без универсального параметра, если только метод не сконструирован явно.

ек а

БГ УИ Р

18.4. Ковариантность и контравариантность Определим понятия ковариантности и контравариантности для сконструированных типов данных. Для этого введём отношение частичного порядка на множестве ссылочных типов: 𝑇1 ≤ 𝑇2 ⇔ 𝑇1 наследуется (прямо или косвенно) от 𝑇2 . Если имеется тип C, а также типы T1 и T2 (T1 ≤ T2), то C назовём: – ковариантным, если C ≤ C; – контравариантным, если C ≤ C; – инвариантным, если не верно ни первое, ни второе утверждение. Понятия частичного порядка типов, ковариантности и контравариантности связаны с приведением типов. Тот факт, что тип T1 «меньше» типа T2, означает возможность неявного приведения переменной типа T1 к типу T2. Как указывалось ранее, массивы ковариантны для ссылочных типов (например, массив строк присваивается массиву объектов). Покажем на примере, что универсальные классы не могут быть ни ковариантными, ни контравариантными. Рассмотрим следующий код:

Би бл ио т

// два простых класса, связанных наследованием public class Person { public string Name; } public class Student : Person { public string University; }

// универсальный класс с открытым полем public class C { public T Field; } // использование классов var cp = new C(); var cs = new C {Field = new Student()}; // закомментировано присваивание, возможное при ковариантности // cp = cs; cp.Field = new Person(); 55

cs.Field.University = "MIT"; // закомментировано присваивание, возможное при контравариантности // cs = cp var s = cs.Field; s.University = "Berkeley";

ек а

public interface IOutOnly { T this[int index] { get; } }

БГ УИ Р

Данный код компилируется. Однако, если допустить ковариантность классов и разрешить присваивание cp = cs, то cs.Field == cp.Field, и объекту класса-потомка фактически выполняется присваивание объекта класса-предка (в выражении cp.Field = new Person()). Исходя из аналогичных соображений не может быть разрешена контравариантность классов, т. е. присваивание cs = cp24. Таким образом, универсальные классы (и структуры) инвариантны, однако универсальные интерфейсы могут быть описаны как ковариантные или контравариантные относительно некоего параметра-типа. Чтобы указать на ковариантность относительно параметра T, следует использовать ключевое слово out при описании параметра типа. На контравариантность указывает ключевое слово in при описании параметра типа.

Би бл ио т

public interface IInOnly { void Process(T x); }

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

19. Использование универсальных шаблонов В данной главе приведено несколько примеров использования универсальных шаблонов в элементах платформы .NET. 19.1. Кортежи В языках программирования кортежем (tuple) называется структура данных, содержащая фиксированный набор разнотипных значений. В платформе .NET для создания кортежей доступен набор универсальных классов вида

24

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

56

System.Tuple. Всего имеется восемь универсальных классов Tuple, которые

просто различаются количеством параметров типов. Tuple t = new Tuple("Hello", 4); Console.WriteLine("{0} - {1}", t.Item1, t.Item2);

Статический класс System.Tuple содержит восемь перегруженных версий метода Create() для конструирования кортежа с заданным числом элементов: Tuple t = Tuple.Create("Hello", 4);

БГ УИ Р

19.2. Типы, допускающие значение null Примером универсальных типов являются типы, допускающие значение null (nullable types). Эти типы являются экземплярами структуры System.Nullable, где T должен быть типом значения25. Структура Nullable имеет специальный флаг HasValue, указывающий на наличие значения, и свойство Value, содержащее значение. Попытка прочитать Value при HasValue, равном false, ведёт к генерации исключения. Также в Nullable определён метод GetValueOrDefault(). Язык C# предлагает компактную форму объявления типа, допускающего значение null. Для этого после имени типа указывается знак вопроса.

Би бл ио т

ек а

int? x = 123; int? y = null; if (x.HasValue) Console.WriteLine(x.Value); if (y.HasValue) Console.WriteLine(y.Value); int k = x.GetValueOrDefault(); int p = y.GetValueOrDefault(10);

Для типов значений определено неявное преобразование к соответствующему типу, допускающему значение null. Если для типа S возможно приведение к типу T, то такая возможность имеется и для типов S? и T?. Также возможно неявное приведение типа S к типу T? и явное приведение S? к T. В последнем случае возможна генерация исключительной ситуации – если значение типа S? не определено. int x = 10; int? z = x; double? w = z; double y = (double)z;

// неявное приведение int к int? // неявное приведение int? к double? // явное приведение int? к double

Хотя для структуры Nullable не определены арифметические операции и операции сравнения, компилятор способен «заимствовать» нужную операцию у соответствующего типа значения. При этом действуют следующие правила: Класс System.Nullable содержит методы сравнения значений типа, допускающего значение null, а также метод получения базового типа для типа, допускающего значение null. 25

57

– арифметические операции возвращают значение null, если хотя бы один из операндов равен null; – операции сравнения, кроме == и !=, возвращают значение false, если хотя бы один из операндов равен null; – операции равенства == и != считают две переменные, равные null, равными между собой; – если в операциях & и | участвуют операнды типа bool?, то null | true равняется true, а null & false равняется false.

bool? b = false; Console.WriteLine(null & b);

// // // // //

8 null true false true

БГ УИ Р

int? x = 3, y = 5, z = null; Console.WriteLine(x + y); Console.WriteLine(x + z); Console.WriteLine(x < y); Console.WriteLine(x < z); Console.WriteLine(null == z);

// false

ек а

С типами, допускающими значение null, связана операция ??. Результатом выражения a ?? b является a, если a содержит некое значение, и b – в противном случае. Таким образом, b – это значение, которое следует использовать, если a не определено. Тип выражения a ?? b определяется типом операнда b.

Би бл ио т

int? x = GetNullableInt(); int? y = GetNullableInt(); int? z = x ?? y; int i = z ?? -1;

Операцию ?? можно применить и для ссылочных типов: string s = GetStringValue(); Console.WriteLine(s ?? "Unspecified");

В этом фрагменте кода на консоль выводится или значение строки s, или "Unspecified", при s == null. 19.3. Прочие примеры универсальных шаблонов Структура System.ArraySegment является «обёрткой» над массивом, определяющей интервал элементов массива. Для одного массива можно создать несколько объектов ArraySegment, которые могут задавать даже перекрывающиеся интервалы. Работать с «обёрнутым» массивом можно, используя свойство ArraySegment.Array. int[] a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; var firstSeg = new ArraySegment(a, 2, 6); // первый сегмент var secondSeg = new ArraySegment(a, 4, 3); // второй сегмент firstSeg.Array[3] = 10; // изменяем четвёртый элемент массива a 58

Класс System.Lazy служит для поддержки отложенной инициализации объектов. Данный класс содержит булево свойство для чтения IsValueCreated и свойство для чтения Value типа T. Использование Lazy позволяет задержать создание объекта до первого обращения к свойству Value. Для создания объекта используется либо конструктор без параметров типа T, либо функция, передаваемая конструктору Lazy. // false // true

БГ УИ Р

Lazy lazy = new Lazy(); Console.WriteLine(lazy.IsValueCreated); Student s = lazy.Value; Console.WriteLine(lazy.IsValueCreated);

20. Делегаты

Делегат – это пользовательский тип, который инкапсулирует метод. В C# делегат объявляется с использованием ключевого слова delegate. При этом указывается имя делегата, тип и сигнатура инкапсулируемого метода: public delegate double Function(double x); public delegate void Subroutine(int i);

Би бл ио т

Function F; Subroutine S;

ек а

Делегат – самостоятельный пользовательский тип, он может быть как вложен в другой пользовательский тип (класс, структуру), так и объявлен отдельно. Так как делегат – это тип, то нельзя объявить в одной области видимости два делегата с одинаковыми именами, но разной сигнатурой. После объявления делегата можно объявить переменные этого типа:

Переменные делегата инициализируются конкретными методами при использовании конструктора делегата с одним параметром – именем метода (или именем другого делегата). Если делегат инициализируется статическим методом, требуется указать имя класса и имя метода, для инициализации экземплярным методом указывается объект и имя метода. При этом метод должен обладать подходящей сигнатурой: F = new Function(ClassName.SomeStaticFunction); S = new Subroutine(obj.SomeInstanceMethod);

Для инициализации делегата можно использовать упрощённый синтаксис без применения операции new(). F = ClassName.SomeStaticFunction; S = obj.SomeInstanceMethod;

59

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

БГ УИ Р

public static class ArrayHelper { public static int[] Transform(this int[] data, Transformer f) { var result = new int[data.Length]; for (int i = 0; i < data.Length; i++) { // альтернатива: result[i] = f.Invoke(data[i]); result[i] = f(data[i]); } return result; } }

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

ек а

public delegate int Transformer(int x);

Создадим класс, который использует ArrayHelper и Transformer:

Би бл ио т

public class MainClass { public static int TimesTwo(int i) { return i * 2; } public int AddFive(int i) { return i + 5; } public static void Main() { int[] a = {1, 2, 3}; Transformer t = TimesTwo; a = a.Transform(t); var c = new MainClass(); a = a.Transform(c.AddFive); }

}

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

60

// два класса, связанных наследованием public class Person { . . . } public class Student : Person { . . . } // делегат для обработки public delegate Person Register(Person p);

БГ УИ Р

// вспомогательный класс с методом обработки public class C { public static Student M(object o) { return new Student(); } } // присваивание возможно благодаря ко- и контравариантности Register f = C.M;

Делегаты могут быть объявлены как универсальные типы. При этом допустимо использование ограничений на параметр типа, а также указания на ковариантность и контравариантность параметров типа. public delegate TResult Transformer(T x);

Би бл ио т

ек а

public static class ArrayHelper { public static TResult[] Transform(this T[] data, Transformer f) { var result = new TResult[data.Length]; for (int i = 0; i < data.Length; i++) { result[i] = f(data[i]); } return result; } }

Ключевой особенностью делегатов является то, что они могут инкапсулировать не один метод, а несколько. Подобные объекты называются групповыми делегатами. При вызове группового делегата срабатывает вся цепочка инкапсулированных в нем методов. Групповой делегат объявляется таким же образом, как и обычный. Затем создаётся несколько объектов делегата, все они связываются с некоторыми методами. После этого используется операция + для объединения делегатов в один групповой делегат. Если требуется удалить метод из цепочки группового делегата, используется операция –. Если из цепочки удаляют последний метод, результатом будет значение null. Приведём пример использования группового делегата типа Transformer:

61

int[] a = {1, 2, 3}; var c = new MainClass(); Transformer x = TimesTwo, y = c.AddFive, z; z = x + y; // z – групповой делегат, содержит две функции a.Transform(z);

Любой пользовательский делегат можно рассматривать как наследник класса System.MulticastDelegate, который, в свою очередь, наследуется от System.Delegate. Элементы класса MulticastDelegate перечислены в табл. 5. Таблица 5

БГ УИ Р

Элементы класса MulticastDelegate Описание Clone() Метод создаёт копию объекта-делегата Статический метод, который объединяет в групповой делегат Combine() два объекта-делегата или массив таких объектов26 Перегруженный статический метод, который позволяет создаCreateDelegate() вать делегаты на основе информации о типе и методе Вызов метода, связанного с объектом-делегатом. Аргументы DynamicInvoke() метода передаются в виде набора объектов Метод возвращает массив объектов-делегатов, инкапсулироGetInvocationList() ванных в групповой делегат Свойство позволяет получить информацию о методе, связанMethod ном с объектом-делегатом Статические методы, которые удаляют указанный объектRemove() и RemoveAll() делегат из группового делегата Свойство содержит ссылку на объект, связанный с инкапсулиTarget рованным методом (для экземплярных методов или методов расширения) В пространстве имён System объявлено несколько полезных универсаль-

Би бл ио т

ек а

Имя элемента

ных делегатов. Имеются делегаты для представления функций и действий, содержащих от нуля до шестнадцати аргументов – Func и Action, делегаты для функций конвертирования Converter, сравнения Comparison и предиката Predicate.

21. Анонимные методы и лямбда-выражения Назначение анонимных методов (anonymous methods) заключается в том, чтобы сократить объём кода, который должен писать разработчик при использовании делегатов. Если рассмотреть примеры предыдущей главы, то очевидно, что даже для объектов-делегатов, содержащих минимум действий, необходимо создавать метод (а, возможно, и отдельный класс) и инкапсулировать этот метод в делегате. При применении анонимных методов формируется безымянный блок кода, который назначается объекту-делегату.

Делегаты относятся к неизменяемым типам. Поэтому методы Combine() и Remove() возвращают новые объекты-делегаты. 26

62

Синтаксис объявления анонимного метода включает ключевое слово delegate и список формальных параметров. Ковариантность и контравариантность делегатов работает и в случае применения анонимных методов. Дополнительным правилом является возможность описать анонимный метод без параметров, если параметры не используются в теле метода, а делегат не имеет out-параметров. Модифицируем фрагмент кода из предыдущей главы, используя анонимные методы:

БГ УИ Р

int[] a = {1, 2, 3}; Transformer t = delegate(int x) { return x * x; }; a.Transform(t);

Лямбда-выражения и лямбда-операторы – это альтернативный синтаксис записи анонимных методов. Начнём с рассмотрения лямбда-операторов. Пусть имеется анонимный метод:

ек а

Func f = delegate(int x) { int y = x - 100; return y > 0; };

При использовании лямбда-операторов список параметров отделяется от тела оператора символами =>, а ключевое слово delegate не указывается:

Би бл ио т

Func f = (int x) => { int y = x - 100; return y > 0; };

Более того, т. к. мы уже фактически указали тип аргумента лямбдаоператора слева при объявлении f, то его можно не указывать справа. В случае если у нас один аргумент, можно опустить обрамляющие его скобки27: Func f = x => { int y = x - 100; return y > 0; };

Когда лямбда-оператор содержит в своём теле единственный оператор return, он может быть записан в компактной форме лямбда-выражения: Func f_2 = x => x > 0;

Заметим, что для переменной, в которую помещается лямбда-оператор или лямбда-выражение, требуется явно указывать тип – var использовать нельзя. Рассмотрим пример, демонстрирующий применение лямбда-выражений. Используем метод Transform() из предыдущей главы, чтобы получить из числового массива массив булевых значений, а из него – массив строк. Обратите внимание на компактность получаемого кода. Если аргументов несколько, скобки нужно указывать. Когда лямбда-оператор не имеет входных аргументов, указываются пустые скобки. 27

63

int[] a = {1, 2, 3}; string[] s = a.Transform(x => x > 2).Transform(y => y.ToString()); // s содержит "False", "False", "True"

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

БГ УИ Р

// код, который написал пользователь public class MainClass { public static void Main() { System.Func f = x => x > 0; var result = f(1); } }

ек а

// приблизительный вид кода, созданного компилятором public class MainClass { public static void Main() { var cgc = new CompilerGeneratedClass(); System.Func f = cgc.CompilerGeneratedMethod; var result = f(1); }

Би бл ио т

private sealed class CompilerGeneratedClass { public bool CompilerGeneratedMethod(int x) { return x > 0; } }

}

Анонимные методы и лямбда-операторы способны захватывать внешний контекст вычисления. Если при описании тела анонимного метода применялась внешняя переменная, вызов метода будет использовать текущее значение переменной. Захват внешнего контекста иначе называют замыканием (closure). int[] a = {1, 2, 3}; int external = 0; // внешняя переменная Transformer t = x => x + external; // замыкание external = 10; // изменили переменную после описания t int[] b = a.Transform(t); // прибавляет 10 к каждому элементу

64

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

БГ УИ Р

// код, который написал пользователь public class MainClass { public static void Main() { int external = 0; System.Func f = x => x > external; external = 10; var result = f(1); } }

Би бл ио т

ек а

// приблизительный вид кода, созданного компилятором public class MainClass { public static void Main() { var cgc = new CompilerGeneratedClass(); cgc.external = 0; System.Func f = cgc.CompilerGeneratedMethod; cgc.external = 10; var result = f(1); } private sealed class CompilerGeneratedClass { public int external; public bool CompilerGeneratedMethod(int x) { return x > external; }

}

}

Эффектный приём использования замыканий – функции, реализующие мемоизацию. Мемоизация (memoization) – это кэширование результатов вычислений. В следующем примере описан метод расширения для строк, который возвращает функцию подсчёта встречаемости символа в строке. public static Func FrequencyFunc(this string text) { int[] freq = new int[char.MaxValue]; foreach (char c in text) 65

{ freq[c]++; } return ch => freq[ch]; } // использование частотной функции var f = "There is no spoon".FrequencyFunc(); Console.WriteLine(f('o'));

22. События

ек а

БГ УИ Р

Событийно-ориентированное программирование (СОП) – парадигма программирования, в которой выполнение программы определяется событиями – действиями пользователя (клавиатура, мышь), сообщениями других программ и потоков, событиями операционной системы. СОП, как правило, применяется при построении пользовательских интерфейсов и при программировании игр, в которых осуществляется управление множеством объектов. Работу с событиями можно условно разделить на три этапа: – объявление события (publishing); – регистрация получателя события (subscribing); – генерация события (raising). В языке C# событие можно объявить в пределах класса, структуры или интерфейса. Базовый синтаксис объявления события следующий: модификаторы event тип-делегата имя-события;

Би бл ио т

Ключевое слово event указывает на объявление события. При объявлении события требуется указать делегат, описывающий метод обработки события. Обычно этот делегат имеет тип возвращаемого значения void. Фактически, события являются полями типа делегатов. При объявлении события компилятор добавляет в класс или структуру private-поле с именем имя-события и типом тип-делегата. Кроме этого, для обслуживания события компилятор создаёт два метода add_Name() и remove_Name(), где Name – имя события. Эти методы содержат код, добавляющий и удаляющий обработчик события в цепочку группового делегата, связанного с событием. Если программиста по каким-либо причинам не устраивает автоматическая генерация методов add_Name() и remove_Name(), он может описать собственную реализацию данных методов. Для этого при объявлении события указывается блок, содержащий секции add и remove: модификаторы event тип-делегата имя-события { add { операторы } remove { операторы } };

66

БГ УИ Р

В блоке добавления и удаления обработчиков обычно размещается код, добавляющий или удаляющий метод в цепочку группового делегата. Поледелегат в этом случае должно быть явно объявлено в классе. Для генерации события в требуемом месте кода помещается вызов в формате имя-события(фактические-аргументы). Предварительно обычно проверяют, назначен ли обработчик события. Генерация события может происходить только в том же классе, в котором событие объявлено28. Приведём пример класса, содержащего объявление и генерацию события. Данный класс будет включать метод с целым параметром, устанавливающий значение поля класса. Если значение параметра отрицательно, генерируется событие, определённое в классе: public delegate void Handler(int val); public class ExampleClass { private int _field;

Би бл ио т

ек а

public int Field { get { return _field; } set { _field = value; if (value < 0) { // проверяем, есть ли обработчик if (NegativeValueSet != null) { NegativeValueSet(value); } } } } public event Handler NegativeValueSet;

}

Рассмотрим этап регистрации получателя события. Чтобы отреагировать на событие, его надо ассоциировать с обработчиком события. Обработчиком может быть метод, приводимый к типу события (делегату). В качестве обработчика может выступать анонимный метод или лямбда-оператор. Назначение и удаление обработчиков события выполняется при помощи операторов += и -=. Поведение, аналогичное событиям, можно получить, используя открытые поля делегатов. Ключевое слово event заставляет компилятор проверять, что описание и генерация события происходят в одном классе, и запрещает для события все операции, кроме += и -=. 28

67

Используем класс ExampleClass и продемонстрируем назначение и удаление обработчиков событий: public class MainClass { public static void Reaction(int i) { Console.WriteLine("Negative value = {0}", i); }

БГ УИ Р

public static void Main() { var c = new ExampleClass(); c.Field = -10; // нет обработчиков, нет реакции на событие // назначаем обработчик c.NegativeValueSet += Reaction; c.Field = -20; // вывод: "Negative value = -20"

// назначаем ещё один обработчик в виде лямбда-выражения c.NegativeValueSet += i => Console.WriteLine(i); c.Field = -30; // вывод: "Negative value = -30" и "-30"

} }

ек а

// удаляем первый обработчик c.NegativeValueSet -= Reaction;

Би бл ио т

Платформа .NET предлагает средства стандартизации работы с событиями. В частности, для типов событий зарезервированы следующие делегаты: public delegate void EventHandler(object sender, EventArgs e); public delegate void EventHandler(object sender, T e) where T : EventArgs;

Как видим, данные делегаты предполагают, что первым параметром будет выступать объект, в котором событие было сгенерировано. Второй параметр используется для передачи информации события. Это либо класс EventArgs, либо наследник этого класса с необходимыми полями. Сама генерация события обычно выносится в отдельный виртуальный метод класса. В этом методе проверяется, был ли установлен обработчик события. Также можно создать копию события перед обработкой (это актуально в многопоточных приложениях). Внесём изменения в код класса ExampleClass, чтобы работа с событиями соответствовала стандартам: public class MyEventArgs : EventArgs { public int NewValue { get; private set; } 68

public MyEventArgs(int newValue) { NewValue = newValue; } } public class ExampleClass { private int _field;

ек а

БГ УИ Р

public int Field { get { return _field; } set { _field = value; if (value < 0) { OnNegativeValueSet(new MyEventArgs(value)); } } }

Би бл ио т

protected virtual void OnNegativeValueSet(MyEventArgs e) { EventHandler local = NegativeValueSet; if (local != null) { local(this, e); } } public event EventHandler NegativeValueSet;

}

Создадим несколько полезных классов для упрощения работы с событиями. Часто для передачи информации события достаточно класса с единственным свойством. В этом случае можно использовать универсальный класс EventArgs. public class EventArgs : EventArgs { public T EventInfo { get; private set; } public EventArgs(T eventInfo) { EventInfo = eventInfo; } } 69

Вот пример использования EventArgs при описании события: public event EventHandler NegativeValueSet;

Поместим в класс EventHelper два метода расширения, упрощающих генерацию событий:

БГ УИ Р

public static class EventHelper { public static void Raise( this EventHandler handler, EventArgs args, object sender = null) { var local = handler; if (local != null) { local(sender, args); } }

Би бл ио т

ек а

public static void Raise(this EventHandler handler, EventArgs args, object sender = null) { var local = handler; if (local != null) { local(sender, args); } }

}

Вот пример использования метода расширения из класса EventHelper: protected virtual void OnNegativeValueSet(EventArgs e) { NegativeValueSet.Raise(e, this); }

23. Перегрузка операций

Язык C# позволяет организовать для объектов пользовательского класса или структуры перегрузку операций. Могут быть перегружены унарные операции +, -, !, ~, ++, --, true, false и бинарные операции +, -, *, /, %, &, |, ^, , ==, !=, >, =, и = и 0) || (a.Y > 0); }

}

ек а

public static bool operator false(Point a) { // этот метод должен возвращать true, // если семантика объекта соответствует false return (a.X == 0) && (a.Y == 0); }

Теперь возможно написать такой код (обратите внимание на оператор if):

Би бл ио т

var p = new Point {X = 10, Y = 20}; if (p) { Console.WriteLine("Point is positive"); } else { Console.WriteLine("Point has non-positive data"); }

Если класс или структура T перегружают true, false и операции & или |, то становится возможным вычисление булевых выражений по сокращённой схеме. В этом случае: – выражение x && y транслируется в T.false(x) ? x : T.&(x,y); – выражение x || y транслируется в T.true(x) ? x : T.|(x,y); Любой класс или структура могут перегрузить операции для неявного и явного приведения типов. При этом используется следующий синтаксис: public static implicit operator целевой-тип(приводимый-тип имя) public static explicit operator целевой-тип(приводимый-тип имя)

72

Ключевое слово implicit используется при перегрузке неявного приведения типов, а ключевое слово explicit – при перегрузке операции явного приведения. Либо целевой-тип, либо приводимый-тип должны совпадать с типом того класса, где выполняется перегрузка операций. Поместим две перегруженных операции приведения в класс Point:

БГ УИ Р

public class Point { . . . public static implicit operator Point(double x) { return new Point {X = x}; } public static explicit operator double(Point a) { return Math.Sqrt(a.X * a.X + a.Y * a.Y); } }

Вот пример кода, использующего преобразование типов:

ек а

var p = new Point {X = 3, Y = 4}; double y = 10; double x = (double) p; // явное приведение типов p = y; // неявное приведение типов

Би бл ио т

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

24. Анонимные типы

Анонимные типы (anonymous types), представленные в C# 3.0, позволяют создавать новый тип, не декларируя его заранее, а описывая непосредственно при создании переменной. Мотивом для введения анонимных типов в спецификацию языка послужила работа с коллекциями в технологии LINQ. При обработке коллекций тип элементов результата может отличаться от типа элементов исходной коллекции. Например, одна обработка набора объектов Student может привести к коллекции, содержащей имя студента и возраст. Другая обработка – к коллекции с именем и номером группы. В таких ситуациях в старых версиях C# нужно или заранее создать необходимое количество вспомогательных типов, или воспользоваться неким «мегатипом», содержащим все возможные поля результатов. Анонимные типы предлагают более элегантное решение. Объявление анонимного типа использует синтаксис инициализатора объектов, предварённый ключевым словом new. Тип полей не указывается, а выводится из начального значения.

73

var anonymous = new {a = 3, b = true, c = "string data"};

Если при объявлении анонимного типа в качестве значений полей применяются не константы, а элементы известного пользовательского типа или локальные переменные, то имя поля анонимного типа можно не указывать. Будет использовано имя инициализатора. int x = 10; // у анонимного типа будут поля x (со значением 10), b и c var anonymous = new {x, b = true, c = "string data"};

БГ УИ Р

Анонимный тип следует рассматривать как класс, состоящий из полей только для чтения. Кроме полей, других элементов анонимный тип содержать не может. Два анонимных типа считаются эквивалентными, если у них полностью (вплоть до порядка) совпадают поля (имена и типы). var anonymous = new {a = 3, b = true, c = "string data"}; var anotherAnonymous = new {a = 1, b = false, c = "data"}; anonymous = anotherAnonymous; // допустимое присваивание

ек а

Хотя анонимный тип задумывался как хранилище данных (концепция анонимных типов близка к концепции кортежей), действия в анонимный тип можно поместить, используя делегаты: Action m = x => Console.WriteLine(x); var anonymous = new {data = 1, method = m}; anonymous.method(3);

Би бл ио т

25. Пространства имён

Пространства имён служат для логической группировки пользовательских типов. Применение пространств имён обосновано в крупных программных проектах для снижения риска конфликта имён и улучшения структуры библиотек кода. Синтаксис описания пространства имён следующий: namespace имя-пространства-имён { [компоненты-пространства-имён] }

Компонентами пространства имён могут быть классы, интерфейсы, делегаты, перечисления, структуры и другие пространства имён. Само пространство имён может быть вложено только в другое пространство имён. Если в разных местах программы определено несколько пространств имён с одинаковыми именами, компилятор собирает компоненты из этих пространств в общее пространство имён. Для этого необходимо, чтобы одноимен-

74

ные пространства имён находились на одном уровне вложенности в иерархии пространств имён. Для доступа к компонентам пространства имён используется синтаксис имя-пространства-имён.имя-компонента. Для использования в программе некоего пространства имён служит директива using. Её синтаксис следующий: using имя-пространства-имён; using [имя-псевдонима =] имя-пространства[.имя-типа];

ек а

БГ УИ Р

Импортирование пространства имён позволяет сократить полные имена классов. Псевдоним, используемый при импортировании, – это обычно короткий идентификатор для ссылки на пространство имён (или на элемент из пространства имён) в тексте программы. Импортировать можно пространства имён как из текущего проекта, так и из подключённых к проекту сборок. Рассмотрим некоторые тонкости при работе с пространствами имён. Предположим, что создаётся проект, использующий внешние сборки A.dll и B.dll. Пусть сборка A.dll содержит пространство имён NS с классом C, и сборка B.dll содержит такое же пространство и класс. Как поступить для доступа к различным классам C в коде? Эту проблему решает операция :: и директива extern alias. Во-первых, сборкам A.dll и B.dll нужно назначить текстовые псевдонимы. В Visual Studio псевдоним для подключённой сборки можно установить в свойствах сборки. При использовании компилятора командной строки псевдоним указывается с опцией ссылки на сборку:

Би бл ио т

csc.exe program.cs /r:A=A.dll /r:B=B.dll

Затем с элементами сборок можно работать следующим образом: extern alias A; extern alias B;

public class Program { public static void Main() { var a = new A::NS.C(); var b = new B::NS.C(); } }

Существует предопределённый псевдоним с именем global для всех стандартных сборок платформы .NET.

75

26. Генерация и обработка исключительных ситуаций Опишем возможности по генерации и обработке исключительных ситуаций в языке C#. Для генерации исключительной ситуации используется оператор throw: throw [объект-класса-исключительной-ситуации];

Би бл ио т

ек а

БГ УИ Р

Объект, указанный после throw, должен быть объектом класса исключительной ситуации. В C# классами исключительных ситуаций являются класс System.Exception и все его наследники. В некоторых языках для .NET можно (хотя и не рекомендуется) генерировать исключения, не являющиеся производными от Exception. В таком случае CLR автоматически поместит объект исключения в оболочку класса RuntimeWrappedException, который наследуется от Exception. Класс Exception – это базовый класс для представления исключительных ситуаций. Основными элементами этого класса являются: – свойство только для чтения Message, содержащее строку с описанием ошибки; – перегруженный конструктор с одним параметром-строкой, записываемым в свойство Message; – строковое свойство StackTrace, описывающее содержимое стека вызова, в котором первым отображается самый последний вызов метода; – свойство InnerException – объект Exception, описывающий ошибку, вызывающую текущее исключение; – коллекция-словарь Data с дополнительной информацией об ошибке. В пространстве имён System содержится несколько классов для описания наиболее распространённых исключений. Упомянем некоторые из них: 1. ArgumentException – генерируется, когда методу передаётся недопустимый аргумент. 2. ArgumentNullException (наследник ArgumentException) – генерируется, когда методу передаётся аргумент, равный null. 3. ArgumentOutOfRangeException (наследник ArgumentException) – генерируется, если методу передаётся аргумент, выходящий за допустимый диапазон. 4. IndexOutOfRangeException – выбрасывается при попытке обратиться к элементу массива по индексу, который выходит за границы массива. 5. InvalidCastException – генерируется, когда явное преобразование типов завершается неудачей. 6. InvalidOperationException – сигнализирует о том, что состояние объекта препятствует выполнению метода (пример: запись в файл, который открыт только для чтения). 7. NotSupportedException – сигнализирует о том, что функциональная возможность не поддерживается. 8. NotImplementedException – сигнализирует о том, что функциональная возможность не реализована. 76

9. ObjectDisposedException – генерируется, когда метод вызывается у удалённого из памяти объекта. Разработчик может создать собственный класс для представления информации об исключительной ситуации. Единственным условием для этого класса в C# является прямое или косвенное наследование от класса Exception. Рассмотрим пример программы с генерацией исключительной ситуации:

public class ExampleClass { private int _field;

БГ УИ Р

using System;

Би бл ио т

}

ек а

public int Field { get { return _field; } set { if (value < 0) { // объект исключения создаётся "на месте" throw new ArgumentOutOfRangeException(); } _field = value; } }

public class MainClass { public static void Main() { var a = new ExampleClass(); a.Field = -3; // ИС генерируется, но не обрабатывается! } }

Так как в данном примере исключительная ситуация генерируется, но никак не обрабатывается, при работе приложения появится стандартное окно с сообщением об ошибке. Опишем возможности по обработке исключительных ситуаций. Для перехвата исключительных ситуаций служит операторный блок try – catch – finally. Синтаксис блока следующий: try { [операторы,-способные-вызвать-исключительную-ситуацию] } [один-или-несколько-блоков-catch] 77

[finally { операторы-из-секции-завершения }]

Операторы из части finally (если она присутствует) выполняются всегда, вне зависимости от того, произошла исключительная ситуация или нет. Если один из операторов, расположенных в блоке try, вызвал исключительную ситуацию, управление немедленно передаётся на блоки catch. Синтаксис отдельного блока catch следующий:

БГ УИ Р

catch [(тип-ИС [идентификатор-объекта-ИС])] { операторы-обработки-исключительной-ситуации }

ек а

Здесь идентификатор-объекта-ИС – это некая временная переменная, которая может использоваться для извлечения информации из объекта исключительной ситуации. Отдельно описывать эту переменную не надо. Если указать в блоке catch оператор throw без аргумента, это приведёт к тому, что обрабатываемая исключительная ситуация будет сгенерирована повторно. Модифицируем тело метода Main() из предыдущего примера, добавив блок перехвата ошибки:

Би бл ио т

var a = new ExampleClass(); try { Console.WriteLine("This line is printed always"); a.Field = -3; Console.WriteLine("This line is not printed if the error"); } catch (ArgumentOutOfRangeException ex) { Console.WriteLine(ex.Message); } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { Console.WriteLine("This line is printed always (finally)"); }

Если используется несколько блоков catch, то обработка исключительных ситуаций должна вестись по принципу «от частного – к общему», т. к. после выполнения одного блока catch управление передаётся на часть finally (при отсутствии finally – на оператор после try – catch). Компилятор C# не позво78

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

27. Директивы препроцессора Директивы препроцессора – это инструкции для компилятора, начинающиеся с символа # и расположенные в отдельной строке кода. В табл. 6 дано описание директив и их действия. Таблица 6 Директивы препроцессора #define символ #undef символ #if символ [операция символ_2]... #else #elif символ [операция символ_2]... #endif #warning текст #error текст

#region [имя] #endregion

ек а

#line [номер ["файл"] | hidden]

Действие Определяет указанный символ Удаляет определение символа Тестирует, определён ли символ или набор символов, связанных операциями ==, !=, &&, || Альтернативная ветвь для #if Комбинация #else и последующего #if Окончание блока #if Задаёт текст предупреждения, генерируемого компилятором Текст ошибки, генерируемой компилятором Позволят принудительно задать номер строки в исходном коде Начало региона кода, который можно «свернуть» в редакторе Окончание региона кода

БГ УИ Р

Директива

Би бл ио т

Наиболее часто применяются условные директивы, которые дают возможность включить или проигнорировать участок кода при компиляции. В следующем примере вызов Console.WriteLine() будет компилироваться, пока установлена директива #define DEBUG: #define DEBUG

public class MyClass { private int _x;

private void Foo() { #if DEBUG Console.WriteLine("Test: x = {0}", _x); #endif } }

Символы для условной компиляции можно описать не только с помощью #define, но и указав ключ компилятора командной строки /define или исполь79

зуя окно свойств проекта в Visual Studio (в этих случаях описание символа распространяется не на отдельный файл, а на всю сборку).

28. Документирование исходного кода

ек а

БГ УИ Р

Язык C# позволяет при написании программы снабжать исходный код особыми документирующими комментариями. Содержимое документирующих комментариев может затем быть выделено и обработано. Так реализуется концепция, при которой сам исходный код содержит необходимую документацию, описывающую его. Рассмотрим общие принципы документирования кода. Документирующие комментарии – это либо однострочные комментарии, начинающиеся с последовательности ///, либо блочные комментарии, начинающиеся с последовательности /**. Они могут располагаться в любом месте кода, но обычно их помещают перед описанием пользовательского типа или перед методом. Кроме собственно текста, комментарии могут содержать документирующие теги (табл. 7). Теги позволяют выделить некие особые составляющие комментария – например, имя метода, параметры, пример использования. Если в тексте комментария нужно использовать символы < и >, то они заменяются последовательностями < и >. В случае ссылок на универсальные шаблоны имя параметра-типа может быть записано в фигурных скобках { и }. Таблица 7

Документирующие теги

Тег и синтаксис

Би бл ио т

text content

Описание Позволяют вставить в комментарий текст, являющийся кодом. Второй тег применяется при необходимости вставить несколько строк кода

description description

Помечает части комментария, являющиеся примером. Обычно данный тег включает тег Показывает, какие исключения может сгенерировать метод. Атрибут cref указывает на существующий тип ИС

content

Используется для визуального оформления – выделяет параграф

description

Описывает параметр метода



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

description description

80

Содержит дополнительное описание Описание возвращаемого значения метода

Окончание табл. 7 Описание Устанавливают ссылки на существующий тип или элемент типа Содержит основное описание типа или элемента типа Позволяет указать описание generic-параметра Используется для описания свойства

БГ УИ Р

Тег и синтаксис description description property-description

Ниже приведён фрагмент кода с документирующими комментариями. /// /// Starts the network device. /// /// /// The start timeout. /// The session id. public bool Start(int startTimeout, int sessionId) { . . . }

Би бл ио т

ек а

Чтобы выделить документирующие комментарии из исходного кода, можно откомпилировать программу с ключом /doc:file, где file – это имя XMLфайла с комментариями. При работе в Visual Studio в свойствах проекта достаточно установить флаг Build | Output | Xml Documentation File. Заметим, что существуют самостоятельные проекты, которые расширяют возможности встроенной системы документирования кода. Упомянем такие проекты как NDoc и Sandcastle. Также достаточно популярным является GhostDoc – дополнение к Visual Studio, облегчающее генерирование документирующих комментариев.

81

Список использованных источников

Би бл ио т

ек а

БГ УИ Р

1. Албахари, Дж. C# 5.0. Справочник. Полное описание языка / Дж. Албахари, Б. Албахари ; пер. с англ. – 5-е изд. – М. : Изд. дом «Вильямс», 2013. – 1008 с. 2. Рихтер, Дж. CLR via C#. Программирование на платформе Microsoft .NET Framework 4.5 на языке C# / Дж. Рихтер. – 4-е изд. – СПб. : Питер, 2013. – 896 с. 3. Троелсен, Э. Язык программирования C# 5.0 и платформа .NET 4.5 / Э. Троелсен. – 6-е изд. – М. : Изд. дом «Вильямс», 2013. – 1312 с. 4. Хейлсберг, А. Язык программирования C#. Классика Computer Science / А. Хейлсберг [и др.]. – 4-е изд. – СПб. : Питер, 2012. – 784 с. 5. Цвалина, К. Инфраструктура программных проектов: соглашения, идиомы и шаблоны для многократно используемых библиотек .NET / К. Цвалина ; пер. с англ. – М. : Изд. дом «Вильямс», 2011. – 416 с.

82

Св. план 2017, поз. 30 Учебное издание

БГ УИ Р

Бережнов Даниил Евгеньевич

ек а

ОСНОВЫ ЯЗЫКА C# И ПЛАТФОРМЫ .NET FRAMEWORK

Би бл ио т

ПОСОБИЕ

Редактор Е. С. Юрец Компьютерная правка, оригинал-макет М. В. Касабуцкий

Подписано в печать 11.12.2017. Формат 60х84 1/16. Бумага офсетная. Гарнитура «Таймс». Отпечатано на ризографе. Усл. печ. л. 4,07. Уч.-изд. л. 5,0. Тираж 150 экз. Заказ 336. Издатель и полиграфическое исполнение: учреждение образования «Белорусский государственный университет информатики и радиоэлектроники». Свидетельство о государственной регистрации издателя, изготовителя, распространителя печатных изданий №1/238 от 24.03.2014, №2/113 от 07.04.2014, №3/615 от 07.04.2014. ЛП №02330/264 от 14.04.2014. 220013, Минск, П. Бровки, 6

83

Smile Life

When life gives you a hundred reasons to cry, show life that you have a thousand reasons to smile

Get in touch

© Copyright 2015 - 2024 AZPDF.TIPS - All rights reserved.