Вы находитесь на странице: 1из 336

УДК 004.42 ББК 32.

973

ISBN 978-5-907592-06-3

Евдокимов П. В., Дубовик Е. В.

СПРАВОЧНИК С#. Кратко, быстро, под рукой - СПб.: Издательство Наука


и Техника, 2023. - 336 с., ил.

Серия «Справочник»

Данный справочник содержит ключевую информацию о С# в удобной и


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

Рассмотрены основы синтаксиса С# и первые программы на С#. Отдель­


ное внимание уделено внимание таким темам, как: интерфейсы, струк­
туры и перечисления; обработка исключений; коллекции и итераторы;
объектно-ориентированное программирование на С#; работа с датой и
временем и файловый ввод/вывод.

Справочник будет полезен всем, кто использует или изучает С#: от на­
чинающих до профессионалов.

Все права защищены. HикalWI часть данной книm не может быть воспроизведена в какой бы то ни было форме без лисьмснно1u раэрешенИJ
впадельцев а!JfОрскн х прав.
Издпельство не несет ответственности за возможный ущерб, причиненный в ходе исполаювани.ч материалов данной книm, а таюке за
доступность мпсриалов, ссылки на которые вы можете найти в )ТОЙ книге. На момеm подrотовки книги к изданию все ссылки на интернет­
ресурсы были действующими.

ISBN 978-5-907592-06-3
Контактные телефон1,1 издательства:
(812) 412 70 26
Официальный сайт: www.nit.com.ru

© Евдокимов П. В.
9 J5 2063 >
© Издательство Наука и Техника (оригинал-макет)
Содержание
ГЛАВА 1. ВВЕДЕНИЕ В .NET...............................11
1.1. ЧТО ТАКОЕ .NET..........................................................................12

1.2. ИСТОРИЯ .NET..............................................................................16

1.3. ПОДДЕРЖИВАЕМЫЕ ОПЕРАЦИОННЫЕ СИСТЕМЫ...21

1.4. К АК ПРОГРАММИРОВАЛИ РАНЬШЕ ...................................21


1.4.1. Язык С и Windows API - традиционный подход ...... 21
1.4.2. Язык С++ и библиотека базовых классов ....................22
1.4.3. Visual Basic 6.О...............................................................22
1.4.4. Язык Java........................................................................23
1.4.5. Модель компонентных объектов..................................24

1.5. ЧТО ПРЕДЛ АГАЕТ Н АМ .NET.................................................26

1.6. ОСНОВНЫЕ КОМПОНЕНТЫ .NET........................................27


1.6.1. Три кита: CLR, CTS и CLS...........................................27
1.6.2. Библиотека базовых классов.........................................28
1.7. ЯЗЫК С#...........................................................................................28

1.8. СБОРКИ В .NET ............................................................................31

1.9. ПОДРОБНО О стs........................................................................33


1.9.1. Типыклассов..................................................................33
1.9.2. Типы интерфейсов.........................................................34
1.9.3. Типы структур................................................................35
1.9.4. Типы перечислений.......................................................35
1.9.5. Типы делегатов..............................................................36
1.9.6. Встроенные типы данных .............................................36
•to Справочник С#

1.10. ПОДРОБНО О CLS ............................................................................................. 37

1.11. ПОДРОБНО О CLR ............................................................................................39

1.12. ПРОСТРАНСТВА ИМЕН..................................................................................40

ГЛАВА 2. ПЕР ВАЯ ПРОГРАММА С#...................................43

2.1. РАЗВЕРТЫВАНИЕ У З АКАЗЧИКА ................................................................44

2.2. РАЗВЕРТЫВАНИЕ У ПРОГРАММИСТА. УСТАНОВКА VISUAL


STUD/O COMMUNITY ....................................................•....•..•.•..•.•..•.•.•..•.50

2.3. ПЕРВАЯ ПРОГРАММА С ИСПОЛЬЗОВАНИЕМ VISUALSTUDI0 .••.•.55

2.4. НОВЫЕ ШАБЛОНЫ ПРОЕКТОВ С#.............................................................58

ГЛАВА 3. ОСНОВЫ СИНТАКСИСА С# .......................................61


3.1. НОВЫЙ СТИЛЬ ШАБЛОНОВ КОНСОЛЬНЫХ ПРИЛОЖЕНИЙ .......62

3.2. ИССЛЕДОВАНИЕ ПРОГРАММЫ HELLO, WORLD!................................. 65


3.2.1. Пространства имен, объекгы, методы ................................................65
3.2.2. О методе Main() .................................................................................... 66
3.2.3. Обработка переданных параметров ....................................................69

3.3. КЛАСС SYSTEM.CONSOLE ............................................................................... 70

3.4. ТИПЫ Д АННЫХ И ПЕРЕМЕННЫЕ ..............................................................74


3.4.1. Системные типы данных .....................................................................74
3.4.2. Объявление переменных......................................................................76
3.4.3. Внутренние типы данных ....................................................................77
3.4.4. Члены типов данных ............................................................................78
3.4.5. Работа со строками ...............................................................................79
Члены класса System.String................................................................. 80
Базовые операции .................................................................................81
Содержание 1-t/J-
Сравнение строк ...................................................................................82
Поиск в строке ......................................................................................84
Конкатенация строк ..............................................................................86
Разделение и соединение строк ..........................................................87
Заполнение и обрезка строк ................................................................88
Вставка, удаление и замена строк .......................................................89
Получение подстроки...........................................................................90
Управляющие последовательности символов ...................................90
Строки и равенство .............................................................................. 91
Тип System. Text.StringBuilder ............................................................. 91
3.4.6. Области видимости переменных ........................................................93
3.4.7. Константы .............................................................................................95

3.5. ОПЕРАТОРЫ ..........................................................................................................96


3.5.1. Арифметические операторы................................................................96
3.5.2. Операторы сравнения и логические операторы ................................98
3.5.3. Операторы присваивания................................................................... 100
3.5.4. Поразрядные операторы ....................................................................101

3.6. ПРЕОБРАЗОВАНИЕ ТИПОВ ДАННЫХ......................................................102

3.7. НЕЯВНО ТИПИЗИРОВ АННЫЕ ЛОК АЛЬНЫЕ ПЕРЕМЕННЫЕ ......107

3.8. циклы ..................................................................................................................108


3.8.1. Цикл/оr •..•..•.••.•.•..•..•..•..•..•.••.••.•.••.•..•.•••.••.•..•.••.•..•.••.•..•.•..•.•.•..•.•..•.•.•. 109
3.8.2. Цикл/оrеасh ....................................................................................... 110
3.8.3. Циклы while и dolwhile •.••.•.••.••.••••.•.•..•.••.•.•..•.••...••.••.•.•.••.•.••.•.••.•.••••. 110

3.9. КОНСТРУКЦИИ ПРИНЯТИЯ РЕШЕНИЙ ................................................ 111

3.10. М АССИВЫ.......................................................................................................... 114


3.10.1. Одномерные массивы....................................................................... 114
3.10.2. Двумерные массивы ......................................................................... 116
3.10.3. Ступенчатые массивы ...................................................................... 117
_,о Справочник С#

3.10.4. Класс Array. Сортировка массивов................................................. 119


3.10.5. Массив как параметр ....................................................................... 121

3.11. КОРТЕЖИ ...........................................................................................................121

3.12. КАК ПОДСЧИТАТЬ КОЛИЧЕСТВО СЛОВ В ТЕКСТЕ .......................122

3.13. ВЫЧИСЛЯЕМ ЗНАЧЕНИЕ ФУНКЦИИ ....................................................124

3.14. ДЕЛАЕМ КОНСОЛЬНЫЙ КАЛЬКУЛЯТОР ............................................127

3.15. УГАДАЙ ЧИСЛО. ИГРА .................................................................................129

ГЛАВА 4. ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ
ПРОГРАММИРОВАННЕ ........................................131
4.1. основы ооп ....................................................................................................132

4.2. КЛАССЫ И ОБЪЕКТЫ ....................................................................................135


4.2.1. Члены класса .......................................................................................135
4.2.2. Ключевое слово class •.••.••....•.••..•.•.•..•.••.•.••.•.••.•..•.•.••.••.•..•.••.•.••.•..•.••• 137
4.2.3. Класс System.Object ...........................................................................141
4.2.4. Конструкторы......................................................................................142
4.2.5. Деструкгоры .......................................................................................144
4.2.6. Обращаемся сами к себе. Служебное слово this •.••.•..•••••••.•••••••.•.••. 145
4.2.7. Доступ к членам класса ..................................................................... 147
4.2.8. Модификаторы параметров ............................................................... 148
4.2.9. Необязательные параметры ............................................................... 153
4.2.1О. Именованные аргументы ................................................................. 154
4.2.11. Ключевое слово static.•..•..•.......•..•....•..•......•..•.......•.....•.••.••.•.••.••..•••. 155
4.2.12. Индексаторы .....................................................................................158
4.2.13. Свойства ............................................................................................161

4.3. ПЕРЕГРУЗКА ФУНКЦИЙ ЧЛЕНОВ КЛАССА .........................................162


Содержание 10•
4.3.1. Перегрузка методов ............................................................................162
4.3.2. Пере[l)узка методов............................................................................163
4.3.3. Пере[l)узка операторов ......................................................................164

4.4. НАСЛЕДОВАНИЕ И ПОЛИМОРФИЗМ ...................................................... 168


4.4.1. Введение в наследование...................................................................168
4.4.2. Защищенный доступ ..........................................................................170
4.4.3. Запечатанные классы. Ключевое слово sealed•.••.••.•..•.•.••.••.•.•....••••. 171
4.4.4. Наследование конструкторов ............................................................172
4.4.5. Сокрытие имен. Ключевое слово base ............................................. 173
4.4.6. Виртуальные члены............................................................................175
4.4.7. Абстрактные классы...........................................................................176

4.5. ОТЛОЖЕННАЯ ИНИЦИАЛИЗАЦИЯ. ТИП LAZY................................... 177

ГЛАВА 5. РАСШИРЕННЫЙ СИНТАКСИС.................... 181


5.1. ДЕЛЕГАТЫ ...........................................................................................................182
5.1.1. Определение делегатов ......................................................................182
5.1.2. Место определения делегата ............................................................. 184
5.1.3. Параметры и результат делеrата .......................................................185
5.1.4. Присвоение ссылки на метод ............................................................185
5.1.5. Соответствие методов делегату ........................................................ 186
5.1.6. Добавление методов в делегат .......................................................... 187
5.1.7. Объединение делеrатов......................................................................188
5.1.8. Вызов делеrата ....................................................................................189
5.1.9. Обобщенные делегаты .......................................................................191
5.1.10. Делегаты как параметры методов ...................................................192
5.1.11. Возвращение делегатов из метода .................................................. 192
5.1.12. Делегаты Action, Predicate и Func ................................................. 194

5.2. АНОНИМНЫЕ МЕТОДЫ ................................................................................ 196


•t•'il Справочник С#

5.3. ЛЯМБДЫ ...............................................................................................................199


5.3.1. Введение в лямбда-выражения .........................................................199
5.3.2. Параметры лямбды.............................................................................200
5.3.-3. Возвращение результата ....................................................................201
5.3.4. Лямбда-выражение как аргумент метода .........................................202
5.3.5. Лямбда-выражение как результат метода.........................................204

5.4. СОБЫТИЯ ............................................................................................................205


5.4.1. Введение в события ............................................................................205
5.4.2. Определение и вызов событий ..........................................................207
5.4.3. Добавление обработчика события ....................................................208
5.4.4. Добавление и удаление обработчиков ..............................................21О
5.4.5. Установка в качестве обработчика события ..................................... 211
5.4.6. Управление обработчиками ...............................................................211
5.4.7. Передача данных события .................................................................214

5.5. ЗАМЫКАНИЯ ......................................................................................................216

ГЛАВА 6. ИНТЕРФЕЙСЫ, СТРУКТУРЫ И


ПЕРЕЧИСЛЕНИЯ ....................................................221

6.1. ПОНЯТИЕ ИНТЕРФЕЙСА ..............................................................................222

6.2. КЛЮЧЕВЫЕ СЛОВА AS И /S......................................................................... 225

6.3. ИНТЕРФЕЙСНЫЕ СВОЙСТВА .................................................................... 226

6.4. ИНТЕРФЕЙСЫ И НАСЛЕДОВАНИЕ ..........................................................227

6.5. СТРУКТУРЫ........................................................................................................229

6.6. ПЕРЕЧИСЛЕНИЯ ..............................................................................................231

6.7. ВЛОЖЕННЫЕ типы .......................................................................................233


Содержание

ГЛАВА 7. ОБРАБОТКА ИСКЛЮЧЕНИЙ .......................235

7.1. ВВЕДЕНИЕ В ОБРАБОТКУ ИСКЛЮЧЕНИЙ ...........................................236

7.2. ПЕРЕХВАТ ИСКЛЮЧЕНИЙ. БЛОКИ TRY, САТСН, FINALLY ..........•.239

7.3. КЛАСС EXCEPTION .............................•....•.......•.....................•....•....................241

7.4. ИСКЛЮЧЕНИЯ УРОВНЯ СИСТЕМЫ .......................................................243

7.5. КЛЮЧЕВОЕ СЛОВО FINALLY......................................................................245

7.6. КЛЮЧЕВЫЕ СЛОВА CHECKED И UNCHECKED ...............................•.•. 246

ГЛАВА 8. КОЛЛЕКЦИИ И ИТЕРАТОРЫ ...........................249

8.1. ВВЕДЕНИЕ В КОЛЛЕКЦИИ ..........................................................................250

8.2. НЕОБОБЩЕННЫЕ КОЛЛЕКЦИИ ...............................................................253

8.3. ОБОБЩЕННЫЕ КОЛЛЕКЦИИ .....................................................................256

8.4. КЛАСС ARRA YLIST. ДИНАМИЧЕСКИЕ МАССИВЫ ............................259

8.5. ХЕШ-ТАБЛИЦА . КЛАСС HASHTABLE.......................................................264

8.6. СОЗДАЕМ СТЕК. КЛАССЫ STACK И SТАСК<Т> ..............•.....................267

8.7. ОЧЕРЕДЬ. КЛАССЫ QUEUE И QUEUE<Т>..•..................................•.........269

8.8. СВЯЗНЫЙ СПИСОК. КЛАСС LINKEDL/ST<Т>.••.•..•.••.•.•..•.•..•.•..•.•.•..•.••.271

8.9. СОРТИРОВАННЫЙ СПИСОК. КЛАСС SORTEDLIST<TКEY, ТVALUE>-274

8.10. СЛОВАРЬ. КЛАСС DICTIONARY<TKEY, TVALUE> .•..•.••.•.•..•....•.•.•..•.•..277

8.11. СОРТИРОВАННЫЙ СЛОВАРЬ: КЛАСС


SORTED D I CТIONARY<TKEY, TVALUE> ...............................•...........281

8.12. МНОЖЕСТВА: КЛАССЫ HASHSET<Т> И SORTEDSET<Т> .•.•.......•.•284

8.13. РЕАЛИЗАЦИЯ ИНТЕРФЕЙСА ICOMPARABLE •..•..•.•.••.•........•..•.•..•.•.•.286


Справочник С#

8.14. ПЕР ЕЧИСЛИТЕЛИ..........................................................................................287

8.15. РЕА ЛИЗАЦИЯ ИНТЕРФЕЙСОВIENUMERABLE И IENUMERATOR -289

8.16. ИТЕРАТОРЫ. КЛЮЧЕВОЕ СЛОВО YIELD.•.•.••.••.•.••••.•.•..•.•.••.•.•..••.•.••.•. 291

ГЛАВА 9. РАБОТА С ДАТОЙ И ВРЕМЕНЕМ ...................293

9.1. СТРУКТУРА DATETIME.....•.••.••.•.••.••...••.•.••.••••.•...•..••.•.•.••.•.••...••••....•.••.•.•..••.•294

9.2. ОПЕРАЦИИ С DATETIME.....•.••..•.........•.....•...•..•..•.....•••...•...•....••..•....•....•....••295

9.3. ФОРМАТИР ОВАНИЕ ДАТЫ И ВРЕМЕНИ................................................297

ГЛАВА 10. ФАЙЛОВЫЙ ВВОД/ВЫВОД.............................299


10.1. ВВЕДЕНИЕ В ПР ОСТРАНСТВО ИМЕН SYSTEM.I0••••.••.•.•..•.••.••••.•••• 300

10.2. КЛАССЫ ДЛЯ МАНИПУЛЯЦИИ С ФАЙЛАМИ И КАТАЛОГАМИ. 302


10.2.1. Использование класса Directorylnfo •••.•.......•....•.••.•.••.........•.••.•.••.• 303
10.2.2. Классы Directory и Drivelnfo. Получение списка дисков............. 304
10.2.3. Класс Filelnfo ......•...............•......•.....•............•.••.•....•..•....•............•... 307
10.2.4. Класс File ...•.......•..•..•....••.....•.•...••.•....••.•.••••.••.•.•••••.••.•.••.•..•.•..•.••••.•.. 31 I
10.2.5. Классы Stream и FileStream...........................................................• 312
10.2.6. Классы Stream Writer и StreamReader ............•...............................314
10.2.7. Классы BinaryWriter и ВinaryReader .•..•.••.•....••..•••.•••••••••••••••.••••.•. 316

10.3. СЕР ИА ЛИЗАЦИЯ ОБЪЕКТОВ.................................................................... 317

10.4. ВЫ ВОД СОДЕРЖИМОГО ФАЙЛА НА С# ............................................... 319

10.5. РАБОТА С ХМL-ФАЙЛОМ............................................................................ 323

10.6. АРХИВАЦИЯ ФАЙЛОВ НА С#..................................................................... 330

10.7. ПОДСЧЕТ КОЛИЧЕСТВА СЛОВ В ФАЙЛЕ.................•................•......... 332


ГЛАВА 1.

Введение в .NET
Справочник С#

1.1. Что такое .NET


Платформа .NET Framework - это технология, поддерживающая созда­
ние и выполнение нового поколения приложений и веб-служб.

При разработке платформы .NET Framework учитывались следующие


цели:

• Обеспечение среды выполнения кода, которая бы минимизирова­


ла конфликты при развертывании программного обеспечения и
управлении версиями.

• Обеспечение объектно-ориентированной среды программирова­


ния для локального сохранения и выполнения объектного кода
либо для удаленного/распределенного выполнения.

• Предоставление среды выполнения кода, гарантирующей безопас­


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

• Предоставление единых принципов разработки для разных типов


приложений, таких как приложения Windows и веб-приложения.

• Обеспечение среды выполнения кода, которая бы исключала


проблемы с производительностью сред выполнения сценариев
или интерпретируемого кода.

• Разработка взаимодействия на основе промышленных стандартов,


что позволяет интегрировать код платформы .NET Framework с
любым другим кодом.
Глава 1. Введение в .NET

Платформа .NET Framework состоит из общеязыковой среды выполне­


ния (среды CLR или Common Language Runtime) и библиотеки классов
.NET Framework.

Основа платформы .NET Framework - среда CLR. Ее можно считать


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

Основная задача среды выполнения - это управление кодом. Код, об­


ращающийся к среде выполнения, называют управляемым кодом, а код,
который не обращается к среде выполнения, называют неуправляемым
кодом. Библиотека классов является комплексной объектно-ориентиро­
ванной коллекцией типов, применяемых для разработки приложений -
начиная с консольных приложений, запускаемых из командной строки, и
заканчивая приложениями, использующими последние технологические
возможности ASP.NET, например, Web.Forms и веб-службы ХМL. Конеч­
но, с помощью .NET можно создавать и обычные Windоws-приложения с
интерфейсом пользователя (GUI).

Платформа .NET Framework может размещаться неуправляемыми ком­


понентами. Такие компоненты загружают среду CLR в собственные про­
цессы и запускают выполнение управляемого кода, что в итоге создает
программную среду, которая позволяет использовать средства управляе­
мого и неуправляемого выполнения.

Среда CLR управляет памятью, выполнением потоков, выполнением


кода, проверкой безопасности кода, компиляцией и другими системны­
ми службами. Все эти средства являются внутренними для управляемого
кода, который выполняется в среде CLR. Из соображений безопасности
управляемым компонентам присваивают разные степени доверия, кото­
рые зависят от многих факторов. Управляемый компонент может или не
может выполнять операции доступа к файлам, операции доступа к рее­
стру и другие важные функции.

Также среда выполнения обеспечивает управление доступом для кода.


Пользователи могут доверить исполняемому приложению, которое
Справочник С#

внедрено в веб-страницу, воспроизведение звука, но при этом не разре­


шить ему доступ к файловой системе и личным данным.

CLR реализует инфраструктуру строгой типизации и проверки кода, ко­


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

CLR может размещаться в серверных приложениях, таких как Microsoft


SQL Server и службы IIS (lntemet Information Services). Такая инфра­
структура позволяет использовать управляемый код для разработки соб­
ственной логики программ, пользуясь при этом высочайшей производи­
тельностью лучших серверов, которые поддерживают размещение среды
выполнения.

Теперь поговорим о библиотеке классов .NET Framework. Библиотека


классов платформы представляет собой коллекцию типов, которые тес­
но интегрируются со средой CLR. Понятно, что библиотека является
объектно-ориентированной.

Библиотека предоставляет типы, из которых управляемый код пользова­


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

Основные функции .NET следующие:

• Богатая функциональность. Платформа .NET Framework предо­


ставляет богатый набор функционала «из коробки». Она содержит
сотни классов, которые предоставляют функциональность, гото­
вую к использованию в ваших приложениях. Это означает, что раз­
работчику не нужно вникать в низкоуровневые детали различных
операций, таких как 1/0, сетевое взаимодействие и т.д.

• Простая разработка веб-приложений. ASP.NET - это техноло­


гия, доступная на платформе .NET для разработки динамических
11ава 1. Введение в .NET

веб-приложений. ASP.NET предоставляет управляемую события­


ми модель программирования (подобную Visual Basic 6, которая
упрощает разработку веб-страниц). ASP.NET предоставляет раз­
личные элементы пользовательского интерфейса (таблицы, сетки,
календари и т.д.), что существенно упрощает задачу программиста.

• Поддержка ООО. Преимущества объектно-ориентированного


программирования известны всем. Платформа.NET предоставля­
ет полностью объектно-ориентированное окружение. Даже при­
митивные типы вроде целых чисел и символов теперь считаются
объектами.
• Поддержка многоязычности. Как правило, в больших компаниях
есть программисты, пишущие на разных языках. Есть те, кто пред­
почитает С++, Java или Visual Basic. Чтобы переучить человека,
нужно потратить время и деньги. Платформа .NET позволяет че­
ловеку писать на том языке, к которому он привык.

• Автоматическое управление памятью. Утечки памяти -


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

• Совместимость с СОМ и СОМ+. Ранее, до появления .NET, СОМ


был стандартом де-факто для компонентизированной разработки
приложений..NET полностью совместима с СОМ и СОМ+.

• Поддержка XML. Платформа .NET предоставляет XML веб­


сервисы, которые основаны на стандартах вроде НТТР, XML и
SOPA.

• Простое развертывание и настройка. Развертывание Windоws­


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

• Безопасность. Windows-плaтфopмa всегда подвергалась критике


за плохую безопасность.Microsoft приложила огромные усилили
•со Справочник С#

и сделала платформу .NET безопасной для корпоративных прило­


жений. Безопасность типов, безопасность доступа к коду, аутенти­
фикация на основе ролей - все это делает приложения надежны­
ми и безопасными.

1.2. История .NET


В июле 2000 года на конференции PDC (Professional Developer
Conference) компания Microsoft анонсировала новый фреймворк для
разработки программного обеспечения - .NET Framework. Первая же
бета-версия .NET Framework SDK Beta l была опубликована на сайте
Microsoft 12 ноября 2000 года, однако она была настолько «сырой», что
Microsoft рекомендовала ее устанавливать только на компьютеры, пред­
назначенные для тестирования. Как говорится, первый блин всегда ко­
мом. И таким комом была первая бета-версия .NЕТ.

Наверное, вам будет интересно узнать, что изначально платформа должна


была называться Microsoft.Net, однако Билл Гейтс решил переименовать
ее просто в .NЕТ. Также он заявил, что «стратегия корпорации целиком
и полностью будет определяться платформой .Net» и что все продукты
компании со временем будут переписаны с учетом этой платформы.

Первая версия .NET появилась лишь два года спустя - l мая 2002 года.
В целом, таблица l. l содержит информацию обо всех версиях .NET, вы­
пущенных с 2002 года.

Таблица 1.1. Версии .NET

Visual Studio По умолчанию Заменяет


Версия Дата выхода в Windows версию
1.0 1 мая 2002 г. Visual Studio .NET - -

Visual Studio .NET Windows Server


1.1 1 апреля 2003 г. 1.0
2003 2003
Глава 1. Введение в .NET

Windows Vista,
Windows 7, Win-
2.0 11 июня 2005 г. Visual Studio 2005
dows Server 2008
R2

Windows Vista,
Windows Server
Visual Studio 2005
3.0 6 ноября 2006 г. 2008, Windows 7, 2.0
+ расширения
Windows Server
2008 R2

Windows 7, Win-
3.5 9 ноября 2007 г. Visual Studio 2008 dows Server 2008 2.0, 3.0
R2

Windows 8, Win-
4.0 12 апреля 2010 г. Visual Studio 2010
dows Server 2012

15 августа Windows 8, Win-


4.5 Visual Studio 2012 4.0
2012 г. dows Server 2012
Windows 8.1,
17 октября
4.5.1 Visual Studio 2013 Windows Server 4.0, 4.5
2013 г.
2012 R2
4.5.2 5 мая 2014 г. - - 4.0-

4.6 20 июля 2015 г. Visual Studio 2015 Windows 10 4.0-4.5.2

17 ноября Visual Studio 2015 Windows 10


4.6.1 4.0-4.6
2015 г. Update 1 v15 l l

Windows 10
4.6.2 20 июля 2016 г. - 4.0-4.6.1
v1607

Windows 10
4.7 5 апреля 20 l7 г. Visual Studio 2017 4.0-4.6.2
vl703
Windows 10
17 октября Visual Studio 2017
4.7.1 vl 709, Windows 4.0-4.7
2017 г. vl5.5
Server 2016
•1<:1 Справочник С#

Visual Studio 2017 Windows 10


4.7.2 30 апреля 2018 г. 4.0-4.7.1
vl5.8 vl803
Windows 10
v 1903, послед-
4.8 18 апреля 2019 г. - няя поддержива- 4.0-4.7.2
емая версия для
Windows 7 SPl

Примечание. На данный момент (вторая половина 2022 года)


более не выпускались новые версии .NET Framework, другими
словами - версия 4.8 является самой актуальной, и она же яв­
ляется последней версией, которая поддерживает Windows 7
SP1. Была выпущена сборка 4115 в мае 2021 года, но версии
4. 9 или более старшей пока не было. Следующие версии, кото­
рые будут выпускаться в будущем, уже не будут поддержи­
вать «семерку», а это означает, что современные приложе­
ния больше нельзя будет запускать в Windows 7. Конечно, на
это уйдет пара лет, а «семерка» и так долго прожила. Одна­
ко если кто еще до сих пор использует старые системы под
управлением Windows 7 - давно пора обновиться.

Первый релиз .NET Framework 1.0 предназначался для Windows 98, NT


4.0, 2000 и ХР. Поддержка ХР, а значит и .NET Framework 1.0, закон­
чилась в 2009 году. Версия 1.1 автоматически устанавливалась вместе с
Windows Server 2003, для других выпусков Windows она бьша доступна в
виде отдельного установочного файла. Обычная поддержка этой версии
.NET закончилась в 2008 году, а расширенная- в 2013-ом.
Версия 2.0 выпущена вместе с Visual Studio 2005. В этой версии была до­
бавлена поддержка обобщенных (generic) классов, анонимных методов,
а также полная поддержка 64-битных платформ х64 и IA-64. Поддержка
этой версии закончена в 2011 году, а расширенная заканчивается 12 апре­
ля 2016 года.
Интересно, что изначально версия .NET Framework 3.0 должна бьша на­
зываться WinFX, что должно бьшо отражать ее суть, а именно добавлен­
ные в ее состав компоненты:
Глава 1. Введение в .NET ею•
• Windows Presentation Foundation (WPF) - презентационная гра­
фическая подсистема, использующая XAML;

• Windows Communication Foundation (WCF) - программная мо­


дель межплатформенного взаимодействия;

• Windows Workflow Foundation (WF) - технология определения,


выполнения и управления рабочими процессами;

• Windows CardSpace - технология унифицированной идентифи-


кации.

Расширенной поддержки этой версии не было, а обычная уже закончи­


лась - еще в 2011 году.

Версия 3.5 поддерживает С# 3.0, VB.NET 9.0, в ней также расширена


функциональность WF и WCF (см. выше), добавлена поддержка ASP.
NET, добавлено новое пространство имен.

Версия 4.0 появилась в 2010 году вместе с Visual Studio 2010. Нововве­
дений довольно много:

• Полная поддержка F#, IronRuby, IronPython.

• Поддержка подмножеств .NET Frarnework и ASP.NET в варианте


Server Core.

• Библиотека параллельных задач (Task Parallel Library), предназна­


ченная для упрощения создания распределенных систем.

• Средства моделирования Oslo.

• Язык программирования М, используемый для создания предмет­


но-ориентированных языков.

Версия 4.5 появилась в августе 2012 года, и ее характерной особенно­


стью является отсутствие поддержки Windows ХР. Основные особенно­
сти этой версии:
•со Справочник С#

• Улучшенная подцержка сжатия ZIP.

• Подцержка UTF -16 в консоли.

• Уменьшение количества перезапусков системы посредством об­


наружения и закрытия приложений платформы .NET Framework
версии 4 во время развертывания.

• Фоновая компиляция по требованию (ЛТ), которая доступна на


многоядерных процессорах для повышения производительности
приложения.

• Подцержка огромных массивов ( размером более 2 Гб) на 64-раз­


рядных системах.

• Улучшенная производительность при извлечении ресурсов.

И это только начало. Нововведений очень много, и дополнительную ин­


формацию вы можете получить по адресу:

http:/lwww.codeproject.com/Articles/599756/Five-Great-NEТ-Framework-Features

Версия 4.5.1 является обновлением для 4.5 и выпущена в 2013 году вместе
с Visual Studio 2012. Данная версия поставляется вместе с 8.1 и Windows
Server 2012 R2, а минимально подцерживаемая версия - Windows Vista
SP 2. Версия 4.5.2 является обновлением для 4.5.1, 4.5 и 4.0, но может
быть установлена на одном компьютере вместе с версией 3.5 SPl.

Предпоследняя на момент написания этих строк версия - 4.6. Является


обновлением для версий 4.0 - 4.5.2. Устанавливается при необходимости
вместе с версией 3.5 SPl, поставляется вместе с Visual Studio 2015. В
версии 4.6 появились новый ЛТ-компилятор для 64-разрядных систем,
подцержка последней версии CryptoAPI от Microsoft, а также подцержка
TLS 1.1 и 1.2.

Версия 4.7 впервые была представлена вместе с Windows 1О Creators


Update (v l703), ее же можно установить в Windows 10 Anniversary
Update (v l607), 8.1, 7. Что же касается серверных ОС, то она подцержи­
вает Windows Server 2008R2 SP 1 - 2016.
Глава 1. Введение в .NET 10•
Самая последняя версия - 4.8, которая. появилась совсем недавно.
Является обновлением для версий 4.0 - 4.7.2 и поставляется вместе с
Windows vl903.

1.3. Поддерживаемые операционные


системы
Практически каждая новая версия .NET по умолчанию использовалась в
ближайшем следующем выпуске Windows. Однако это не означает, что
данная версия .NET поддерживает только этот выпуск Windows. Подроб­
ную информацию о поддерживаемых версиях можно получить на офи­
циальном сайте Microsoft:

https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/versions­
and-dependencies#net-framework-48

1.4. Как программировали раньше


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

1.4.1. Язык С и Windows API - традиционный подход


Данный подход можно назвать традиционным: разработка программы
ведется на С с использованием интерфейса Windows API (Application
Programming Interface - интерфейс прикладного программирования).
Данный подход проверен временем и с его использованием написано
очень много приложений.

Хотя этот подход и проверен временем, процесс создания приложений


с помощью одного только АРI-интерфейса является очень сложным за­
нятием. Основная проблема в том, что С сам по себе уж очень лаконич-
•to Справочник С#

ный язык. Любой С-программист вынужден мириться с необходимостью


«вручную» управлять памятью, иметь дело с указателями и ужасными
синтаксическими конструкциями. В современном мире все эти вещи су­
щественно упрощены, а язык (среда) сам управляет памятью, выделени­
ем и освобождением ресурсов и т.д. Но в традиционном С всего этого нет.
Кроме того, поскольку С - это структурный язык программирования,
ему не хватает преимуществ, которые обеспечиваются объектно-ориен­
тированным подходом. В современном мире программа, написанная на
С и Windows API, выглядит как динозавр - ужасно и устрашающе.
Неудивительно, что раньше Windоws-приложения часто «глючилю>.

1.4.2. Язык С++ и библиотека базовых классов


Появление С++ - огромный шаг вперед. Многие не считают С++ полно­
ценным языком программирования, а лишь объектно-ориентированной
надстройкой над С. Но это никак не уменьшает его преимуществ. С появ­
лением С++ программистам стало доступно объектно-ориентированное
программирование и его основные принципы - инкапсуляция, наследо­
вание и полиморфизм.

Но несмотря на поддержку ООП, программисты вынуждены мириться с


утомительными деталями языка С (выделение памяти вручную, ужасные
указатели и т.д. ). Поэтому бьшо решено упростить работу самим себе -
так появились платформы для программирования на С++. Одна из самых
популярных называется MFC (Microsoft Foundation Classes - библиоте­
ка базовых классов Microsoft).

Платформа MFS предоставляет исходный АРI-интерфейс Windows в


виде набора классов, макросов и множества средств для автоматической
генерации программного кода (мастеры, wizards). Хотя этот подход к
программированию более удачен, чем предыдущий, процесс программи­
рования на С++ остается очень трудным и велика вероятность возникно­
вения ошибок из-за его связи с языком С.

1.4.3. Visual Basic 6.0


Первым языком программирования, с которого многие начинают свою
карьеру программиста, является Basic (Beginner's All-purpose Symbolic
Глава l. Введение в .NET

Instruction Code). Данный язык специально был предназначен для но­


вичков, он очень прост, и обучение программированию часто начинают
именно с этого языка. Язык сам по себе довольно древний - он был раз­
работан в 1964 году, а вторую жизнь Basic получил с появлением Visual
Basic от Microsoft. Чтобы облегчить себе жизнь, многие программисты
перешли с С/С++ в мир простых и более дружественных языков вроде
Visual Basic 6.0 (VВ6). VВ6 предоставляет возможность создавать слож­
ные пользовательские интерфейсы, библиотеки программного кода (вро­
де СОМ-серверов) и логику доступа к базам данных. И все это на VB6
делается очень просто, в чем и заключается причина его популярности.

Основной недостаток языка VB6 в том, что он не является полностью


объектно-ориентированным. Можно сказать, что он просто «объект­
ный». Так, VB6 не позволяет программисту устанавливать между класса­
ми отношения «подчиненности» (т.е. прибегать к классическому насле­
дованию) и не обладает никакой внутренней поддержкой для создания
параметризованных классов. Также VB6 не позволяет создавать много­
поточные приложения, если программист не готов работать на уровне
Windows API (это сложно и довольно опасно).

Однако с появлением .NET все недостатки VB6 устранены, правда, но­


вый язык теперь имеет мало общего с VB6 и называется VB.NET. В этом
современном языке поддерживается переопределение операций (пе­
регрузка), классическое наследование, конструкторы типов, обобщения
и многое другое.

1.4.4. Язык Java


Первое, что мне запомнилось в свое время, когда впервые появился Java,
- это возможность написания кроссплатформенного кода. Но это не
единственное преимущество Java.

Java - это объектно-ориентированный язык программирования, кото­


рый по своему синтаксису похож на С++, но при этом Java не имеет мно­
гих из тех неприятных синтаксических аспектов, которые присутствуют
в С++, а как платформа - предоставляет в распоряжение програм-
Справочник С#

мистам большее количество готовых пакетов с различными определе­


ниями типов внутри. Благодаря этому программисты могут создавать на
Java приложения с возможностью подключения к БД, поддержкой обме­
на сообщениями, веб-интерфейсами - и все это используя возможности
только самого Java.

Сам по себе Java - очень элегантный язык, но у него есть одна потен­
циальная проблема: применение Java означает необходимость исполь­
зования Java в цикле разработки и для взаимодействия клиента с сер­
вером. Другими словами, Java очень сложно интегрировать с другими
языками, поскольку он задумывался изначально как единственный язык
программирования и единственная платформа для удовлетворения лю­
бой потребности. Смешать Java с другим языком будет очень сложно,
поскольку ограничен доступ к Java API.

1.4.5. Модель компонентных объектов


Вот мы и добрались до пред-.NЕТ решения. Модель СОМ (Component
Object Model - модель компонентных объектов) была предшествующей
платформой для разработки приложений, которая предлагалась Microsoft
перед .NET. Впервые СОМ появилась в 1993 году, так что модель сама
по себе уже является довольно древней, учитывая скорость развития тех­
нологий.

Модель СОМ позволяет строить типы в соответствии с правилами СОМ


и получать блок многократно используемого двоичного кода. Такие дво­
ичные блоки кода называют «серверами СОМ)). Одно из преимуществ
сервера СОМ в том, что к нему можно получить доступ, используя дру­
гой язык программирования. Например, кто-то может создать СОМ­
объект на С++, а вы можете использовать Delphi и подключаться к СОМ­
серверу.

Понятно, что подобная независимость СОМ от языка является немно­


го ограниченной. Например, нет способа породить новый СОМ-класс с
использованием уже существующего (СОМ не поддерживает классиче­
ское наследие). Но это уже нюансы. Если говорить о преимуществах,
то одно из преимуществ СОМ - прозрачность расположения. За счет
Глава 1. Введение в .NET 1-t-:J•
применения различных конструкций (прокси-серверы, заглушки, AppID)
программисты могут избежать необходимости иметь дело с низким
уровнем - сокетами, RРС-вызовами и т.д. Модель СОМ можно считать
успешной обьектной моделью, однако ее внутреннее устройство явля­
ется очень сложным для восприятия, именно поэтому программистам
требуются месяцы на изучение этой модели.

Для облегчения процесса разработки двоичных СОМ-объектов програм­


мисты могут использовать многочисленные платформы, поддерживаю­
щие СОМ. Так, в ATL (Active Template Library - библиотека активных
шаблонов) для упрощения процесса создания СОМ-серверов предо­
ставляется набор специальных классов, шаблонов и макросов на С++.
Во многих других языках сложная инфраструктура СОМ также скры­
вается из вида. Но поддержки одного только языка для сокрытия всей
сложности СОМ мало. Даже при выборе относительно простого языка с
поддержкой СОМ (пусть это будет VB6) все равно программисту нужно
бороться с записями о регистрации и многочисленными деталями раз­
вертывания. Все это называется адом DLL (DLL Hell).

Конечно, СОМ упрощает процесс создания приложений с помощью раз­


ных языков программирования. Но независимая от языка природа СОМ
не настолько проста, как нам бы этого хотелось. Вся эта сложность -
следствие того, что приложения, написанные на разных языках, полу­
чаются совершенно не связанными с точки зрения синтаксиса. Взять,
например, языки Java и С - их синтаксисы очень похожи, но вот VB6
вообще никак не похож на С. Его корни уходят в Basic. А СОМ-серверы,
созданные для выполнения в среде СОМ+ (она представляет собой
компонент ОС Windows, предлагающий общие службы для библиотек
специального кода, такие как транзакции, жизненный цикл объектов и
т.д. ), имеют совершенно другое поведение, чем ориентированные на ис­
пользование в веб-сети АSР-страницы, в которых они вызываются.

В результате получается очень запутанная смесь технологий. Каждый


АРI-интерфейс поставляется с собственной коллекцией уже готового
кода, а базовые типы данных не всегда интерпретируются одинаково.
У каждого языка своя собственная и уникальная система типов. Из-за
этого СОМ-разработчикам нужно соблюдать предельную осторожность
Справочник С#

при создании общедоступных методов в общедоступных классах СОМ.


Если вам, например, нужно создать метод на С++, который бы возвра­
щал массив целых чисел в приложение на VB6, то вам придется полно­
стью погрузиться в сложные вызовы АРI-интерфейса СОМ для создания
структуры безопасного массива. Ведь если разработчик на С++ просто
вернет собственный массив, приложение на VB6 просто не поймет, что
с ним делать.

1.5. Что предлагает нам .NET


Как видите, жизнь программиста Windows-пpилoжeниii раньше была
очень трудной. Но с появлением .NET Framework все стало гораздо про­
ще. Как уже отмечалось, NET Framework представляет собой программ­
ную платформу для создания приложений на базе семейства операци­
онных систем Windows, а также многочисленных операционных систем
разработки не Microsoft, таких как Мае OS Х и различные дистрибутивы
Unix и Linux.

Основные функциональные возможности .NET:

• Возможность обеспечения взаимодействия с существующим


программным кодом. Позволяет обеспечивать взаимодействие
существующих двоичных единиц СОМ с более новыми двоичны­
ми единицами .NET и наоборот. С появлением .NET 4.0 данная
возможность выглядит еще проще благодаря ключевому слову
dynamic (в книге мы о нем поговорим).

• Поддержка разных языков программирования (С#, Visual


Basic, F# и т.д.).

• Наличие общего исполняющего механизма, который использу­


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

• Тотальная интеграция языков. Поддерживается межъязыковое


наследование, обработка исключений, отладка кода.
Глава 1. Введение в .NET k-&:J•
• Огромная библиотека базовых классов. Она позволяет упро­
стить прямые вызовы к АРI-интерфейсу и предлагает согласован­
ную объектную модель, которую могут использовать все языки
.NET.

• Упрощенная модель развертывания. В .NET не нужно заботить­


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

Как видите, .NET не имеет ничего общего с СОМ, за исключением разве


что поддержки двоичных единиц СОМ.

1.6. Основные компоненты .NET


1.6.1. Три кита: CLR, CTS и CLS
Настало время познакомиться с тремя ключевыми компонентами .NET:
CLR, CTS и CLS. С точки зрения программиста .NET представляет собой
исполняющую среду и обширную библиотеку базовых классов. Уровень
исполняющей среды называется общеязыковой исполняющей средой
(Common Language Runtime) или средой CLR (такое название использу­
ется чаще).
Основная задача CLR - автоматическое обнаружение, загрузка и управ­
ление типами .NET. Теперь типами управляет .NET, а не программист.
Также среда CLR заботится о ряде низкоуровневых деталей - управле­
нии памятью, обработке потоков, выполнении разных проверок, связан­
ных с безопасностью.
Другой компонент .NET - общая система типов (Common Туре System),
или система CTS. Предоставляет полное описание всех возможных ти­
пов данных и программных конструкций, которые поддерживаются ис­
полняющей средой, а также способов, как все эти сущности могут взаи­
модействовать друг с другом. Нужно понимать, что любая возможность
CTS может не поддерживаться в отдельно взятом языке, совместимом с
.NET.
•t•'/.J Справочник С#

Именно поэтому существует третий компонент - CLS (Common


Language Specification), или спецификация CLS. В ней описано лишь то
подмножество общих типов и программных конструкций, которое спо­
собны воспринимать все .NЕТ-языки. Следовательно, если вы исполь­
зуете типы .NET только с функциональными возможностями, предус­
мотренными в CLS, можете быть уверены, что все совместимые с .NET
языки могут их и использовать. Если же вы используете тип данных,
которого нет в CLS, нет гарантии того, что с этим типом данных смо­
жет работать любой поддерживаемый .NET язык. К счастью, существует
способ указать компилятору С#, чтобы он проверял весь код на предмет
совместимости с CLS.

1.6.2. Библиотека базовых классов

Кроме среды CLR и спецификаций CTS и CLS, в составе платформы


.NET существует библиотека базовых классов. Она доступна для всех
языков, поддерживающих .NЕТ. Эта библиотека содержит определения
примитивов (потоки, файловый 1/0, системы графической визуализации,
механизмы для взаимодействия с разными внешними устройствами),
обеспечивает поддержку целого ряда служб, которые нужны в большин­
стве реальных приложений. В библиотеке базовых классов содержатся
определения типов, которые могут упростить процесс доступа к базам
данным, обеспечить безопасность, создание обычных, консольных и
веб-интерфейсов и т.д.

1.7. Язык С#
Поскольку платформа .NET радикально отличается от предыдущих тех­
нологий Microsoft, корпорация разработала специально для нее новый
язык программирования - С#. Синтаксис этого языка похож ... на Java.
Даже не на С++ (хотя язык называется С#), а именно на Java. В прин­
ципе, все эти языки - С, Objective С, С++, С# и Java - используют
похожий синтаксис. Все они являются членами семейства языков
программирования С.
Глава 1. Введение в .NET св•
Несмотря на схожесть с Java, многие синтаксические конструкции в С#
моделируются согласно различным особенностям Visual Basic 6.0 и С++.
Как и в VB6, в С# подцерживается понятие формальных свойств типов
(вместо традиционных методов get и set). Как и в С++, в С# допускается
перезагрузка операций, создание структур, перечислений и функций об­
ратного вызова.

В С# подцерживается целый ряд функциональных возможностей, кото­


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

Поскольку С# - смесь бульдога с носорогом, то есть гибридный язык,


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

• Не нужно никаких указателей. Наконец-то программисты из­


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

• Автоматическая сборка мусора (автоматическое управление па­


мятью). Именно поэтому ключевое слово delete в С# отсутствует.

• Формальные синтаксические конструкции для классов, интер­


фейсов, структур, перечислений и делегатов.

• Возможность перегрузки операторов для пользовательских ти­


пов (как в С++), но при этом не нужно заботиться о возврате *this
для обеспечения связывания.

• Возможность создания обобщенных типов и обобщенных эле­


ментов-членов.

• Поддержка анонимных методов, позволяющих предоставлять


встраиваемую функцию везде, где требуется использовать тип де­
легата.
Справочник С#

• С появлением NET 2.0 (с 2005 года) появились упрощения в мо­


дели «делегат-событие», в том числе появилась возможность при­
менения ковариантности 1 , контравариантности2 и преобразования
групп методов.

• Начиная с версии .NET 3.5, появилась поддержка для строго ти­


пизированных запросов (их также называются запросами LINQ),
которые используются для взаимодействия с разными видами данных.

• Подll.ержка анонимных типов, позволяющих моделировать фор­


му типа, а не его поведение.

С появлением .NET 4.0 язык С# снова бьш обновлен. Появился ряд но­
вых функциональных возможностей, а именно:

• Поддержка необязательных параметров в методах, а также имено­


ванных аргументов.

• Поддержка динамического поиска членов во время выполнения


посредством ключевого слова dynamic.

• Значительное упрощение обеспечения взаимодействия приложе­


ний на С# с унаследованными СОМ-серверами благодаря устра­
нению зависимости от сборок взаимодействия и предоставлению
поддержки необязательных аргументов ref

• Работа с обобщенными типами стала гораздо понятнее, благодаря


появлению возможности легко отображать обобщенные данные из
общих коллекций System.Object.

Ковариантность - это сохранение иерархии наследования исходных типов в


производных типах в том же порядке. Так, если класс Toyota наследует от класса Cars, то
естественно полагать, что перечисление IEnwneraЬle<Тoyota> будет потомком перечисления
IEnwneraЫe<Cars>.
2 Контравариантность - это обращение иерархии исходных типов на
противоположную в производных типах. Так, если класс String наследует от класса Object,
а делегат Action<Т> определен как метод, принимающий обьект типа Т, то Action<Object>
наследует от делегата Action<String>, а не наоборот. Действительно, если «все строки -
обьекты►>, то «всикий метод, оперирующий произвольными обьектами, может выполнить
операцию над строкой►>, но не наоборот. В таком случае говорllТ, что тип (в данном случае
обобщённый делегат) Action<Т> контравариантеи своему параметру-типу Т.
Глава 1. Введение в .NET

Наиболее важный момент, который вы должны знать, программируя на


С#: с помощью этого языка можно создавать только такой код, который
будет выполняться в исполняющей среде .NET (то есть использовать С#
для построения «классического» СОМ-сервера или неуправляемого при­
ложения с вызовами АРI-интерфейса и кодом на С и С++ нельзя).

Код, ориентируемый на выполнение в исполняющей среде .NET, назы­


вается управляемым кодом (managed code). Код, который не может об­
служиваться непосредственно в исполняющей среде .NET, называют
неуправляемым кодом (unmanaged code).

Примечание. Вы должны также понимать, что С# - это


не единственный язык, который можно использовать для
построения .NЕТ-приложений. При установке бесплатного
комплекта разработки Microsoft .NET 4.0 Framework SDK (как
и при установке Visual Studio) для выбора доступны пять язы­
ков: С#, Visual Basic, С++, JScript .Net, F#.

1.8. Сборки в .NET


Какой бы язык .NET вы бы не выбрали для программирования, важно по­
нимать, что двоичные .NЕТ-единицы имеют такие же расширение фай­
лов, как и двоичные единицы СОМ-серверов и неуправляемых программ
Win32 (.dll и .ехе), но внутри они устроены совершенно иначе.

Двоичные .NЕТ-единицы DLL не экспортируют методы для упрощения


взаимодействия с исполняющей средой СОМ, ведь .NET - это не СОМ.
Они не описываются с помощью библиотек СОМ-типов и не регистри­
руются в системном реестре. Самое важное заключается в том, что они
содержат не специфические, а наоборот, не зависящие от платформы ин­
струкции на промежуточном языке (Inteпnediate Language - IL), а также
метаданные типов.

Работает это так. Исходный код на Х проходит через компилятор Х, ко­


торый генерирует инструкции IL и метаданные. Другими словами, все
-НZI Справочник С#

В таблице 1.2 приводится краткий перечень характеристик, свойствен­


ных типам классов.

Таблица 1.2. Характеристики классов CTS

Характеристика Описание
Каждый класс должен настраиваться с атрибутом види-
мости (visibility). По сути, данный атрибут указывает,
Степень видимости должен ли класс быть доступным внешним сборкам или
его можно использовать только внутри определенной
сборки

Экземпляры абстрактных классов не могут создавать-


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

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


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

Интерфейс (interface) - это коллекция абстрактных


членов, которые обеспечивают возможность взаимодей-
Реализующие интер-
ствия между объектом и пользователем этого объекта.
фейсы
CTS позволяет реализовать в классе любое количество
интерфейсов

1.9.2. Типы интерфейсов


Интерфейсы представляют собой именованную коллекцию определений
абстрактных членов, которые могут поддерживаться в данном классе
или структуре. В С# типы интерфейсов определяются с помощью клю­
чевого слова interface. Пример:
puЫic interface IDrive

void Press();
Глава 1. Введение в .NET со•
От самих интерфейсов проку мало. Однако если они реализованы в клас­
сах или структурах, то позволяют получить доступ к дополнительным
функциональным возможностям за счет добавления просто ссьшки на
них в полиморфной форме.

1.9.3. Типы структур

В CTS есть понятие структуры. Если вы раньше программировали на


С, то будете приятно удивлены, что в современном мире .NET нашлось
место для вашего любимого типа данных. Структура может считаться
«облегченной>> версией класса. Обычно структуры лучше подходят для
моделирования математических данных. В С# структуры определяются
ключевым словом strnct:

struct Nums

puЫic int xs, ys;


puЫic Nums(int х, int у) { xs х; ys у; }
puЫic int Add()
{
return xs + ys;

Обратите внимание: структуры могут содержать конструкторы и методы


- подобно классам.

1.9.4. Типы перечислений

Перечисления (enumeration) - удобная программная конструкция,


позволяющая сгруппировать данные в пары «имя-значение». Например:
puЫic enum eNums
{
А 1,
В 2,
С 3
}
Справочник С#

По умолчанию для хранения каждого элемента вьщеляется блок памя­


ти, который соответствует 32-битному целому, но при необходимости
(например, если нужно экономить оперативную память при создании
приложений для устройств с небольшим объемом ОЗУ) это значение
можно изменить.

1.9.5. Типы делегатов


Делегаты (delegate) являются .NЕТ-эквивалентом безопасных в отноше­
нии типов указателей функций в стиле С. Основное отличие заключается
в том, что делегат в .NET представляет собой класс, который наследуется
от System.MulticastDelegate, а не просто указатель на какой-то конкрет­
ный адрес в памяти. Объявить делегат можно с помощью ключевого сло­
ва delegate:

puЬlic delegate int AddOp(int х, int у);

Делегаты удобно использовать, когда нужно обеспечить одну сущность


возможностью перенаправлять вызов другой сущности и образовывать
основу для архитектуры обработки событий .NЕТ.

1.9.6. Встроенные типы данных


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

Таблица 1.3. Встроенные типы данных в CTS

CTS С# С++ Visual Basic


System.ByteByte byte unsigned char Byte
System.SByteSByte SByte sbyte signed char

System.Intlб short short Short


Глава l. Введение в .NET t-a•
System.Int32 int int или \ong Integer

System.Int64 \ong -int64 Long

System.Ulntlб ushort unsigned short UShort

System.UintЗ2 uint unsigned int Ulnteger

System.Unit64 ulong unsigned_int64 ULong

System.Sing\eSingle float float Single

System.DoubleDouble douЬ\e douЬ\e DouЬ\e

System.ObjectObject object objectл Object

System.CharChar char wchar t Char

System.StringString String Stringл String

System.Decima\Deci-
decimal Decimal Decimal
mal
System.Boo\eanBool-
Ьоо\ Ьоо\ Boolean
ean

При этом в С# можно указать названия типов из CTS:

long х = О;
System.Int64 у = О;

1.10. Подробно о CLS


CLS (Common Language Specification - общая спецификация для языков
программирования) представляет собой набор правил, которые подроб­
но описывают минимальный и полный набор функциональных возмож­
ностей, которые должен обязательно поддерживать каждый отдельно
взятый .NЕТ-компилятор, чтобы генерировать такой программный код,
который мог бы обслуживаться CLR.
Справочник С#

и обнаружение запрашиваемого типа в двоичном файле за счет считыва­


ния содержащихся там метаданных. Также он размещает тип в памяти,
преобразует СIL-код в соответствующие платформе инструкции, произ­
водит проверки безопасности и выполняет программный код.

1.12. Пространства имен


Все обилие типов, которое можно использовать в .NET, содержится в
многочисленных пространствах (namespaces) имен .NET. Основное
пространство имен, с которого нужно начинать знакомство с простран­
ствами, называется System. В нем содержится набор ключевых типов,
которые любой разработчик .NET будет использовать снова и снова. Соз­
дание функционального приложения на С# невозможно без добавления
ссылки хотя бы на пространство имен System, поскольку все основные
типы данных (System.Int32, System.BooleanBoolean) определены именно
в нем. В таблице 1.4 приводятся некоторые пространства имен .NET.

Таблица 1.4. Некоторые пространства имен в .NET

Пространство имен Описание


Содержит много полезных типов, позволяющих рабо-
System тать с математическими вычислениями, генератором
случайных чисел, переменными среды и т.д.
Содержит ряд контейнерных типов, а также несколь-
System.Co\lections ко базовых типов и интерфейсов, позволяющих соз-
давать специальные коллекции
System.Data
System.Data.Common Используется для взаимодействия с базами данных
System.Data.EntityClient через ADO.NET

System.Data.SqlClient

System.IO Здесь содержится много типов, предназначенных для


System.10.Compression работы с операциями файлового ввода/вывода, ежа-
System.IO.Pons тия данных, портами
Глава 1. Введение в .NET
св•
System.Reflection Содержит типы, которые поддерживают обнаруже-
ние типов во время выполнения, а также динамиче-
System.Reflection.Emit ское создание типов
Содержит средства, позволяющие взаимодействовать
System.Runtime с неуправляемым кодом, точнее, эти средства нахо-
дятся в System.Runtime.InteropServices
System.Drawing Содержит типы, применяемые для построения на-
System.Windows.Forms стольных приложений (Windows Forms)

System.Windows
Является корневым для нескольких пространств
System.Windows.Controls имен, предоставляющих набор графических инстру-
System.Windows.Shapes ментов WPF (Windows P resentation Foundation)

Содержит типы, применяемые при выполнении про-


System.Linq
rраммирования с использованием API LINQ
System.Web Позволяет создавать веб-приложения ASP.NET

Позволяет создавать распределенные приложения с


System.ServiceModel помощью АРI-интерфейса. Windows Communication
Foundation (WCF)

System.Xml
System.Xml.Linq Здесь содержатся многочисленные типы, которые ис-
пользуются при работе с ХМL-данными
System.Data.
DataSetExtensions

Типы, имеющие дело с разрешениями, криптографи-


System.Security
ей и т.д.

System.Тhreading Средства для создания многопоточных приложений,


способных разделить нагрузку среди нескольких про-
System.Тhreading.Tasks цессоров

System.Workflow.Runtime Типы для построения поддерживающих рабочие


ПОТОКИ приложений с помощью АРl-интерфейса
System.Workflow.Activi-
ties Windows Workflow Foundation (WWF)
Справочник С#

Для подключения пространства имен в С# используется ключевое слово


using, например:

using System;
using System.Drawing;
using System.Windows.Forms;
ГЛАВА 2.

ПЕРВАЯ ПРОГРАММА С#
•t<-J Справочник С#

2.1. Развертывание у заказчика


Как узнать, какая версия .NET Framework установлена у заказчика?
Можно воспользоваться специальными программами для определения
версии .NET Framework. Одна из таких программ - ASoft .NET Version
Detector, скачать которую совершенно бесплатно можно по адресу:

http://net-framework.ru/soft/asoft-net-version-detector

IV.m'f. Рис. 2.1. Программа ASoft .NET


JV].[{'f Version Detector
1\1.m'f
К сожалению, эта утилита на
IV.NTT данный момент пока определяет
JVm максимальную версию как 4.6.2,
Unknown venlon�.a Cont.act ASonl
но в принципе номер версии она
М.xlmum versJon detКUIЫe Ьу this venlon: .NЕТ 4.6.2 выводит правильный - 4.8 (у
нас, как и полагается, установ­
лена самая последняя версия).
Так что можно сказать, что хоть
и с оговорками, но справляется
со своей задачей.
Глава 2. Первая программа С#

Внимание! Загружать .NET Framework нужно только с офи­


циального сайта. Что платформа .NET Framework, что пакет
разработчика Visua/ Studio Community Edition совершенно
бесплатны. Для их загрузки никакая peгucmpaцUJi не нужна.
Поэтому нет никакого смысла загружать их со сторонних
сайтов: очень часто таким образом распространяются виру­
сы и прочие вредоносные программы - вместе с загружаемым
продуктом вы получаете «в нагрузку» вирус.

Также для определения версии .NET Framework можно попытаться най­


ти другую программу, а можно заглянуть в реестр.

Гораздо проще открыть редактор реестра (regedit.exe) и обратиться к


ветке HКEY_LOCAL_MACHINE\SOFTWARE\ Microsoft\NET Framework
SetupWDP. Подразделы этой ветки с именами, начинающимися на «v.»,
позволяют судить об установленных версиях платформы .NET.
Посмотрим, так ли это. На рис. 2.2 показано, что установлены версии
2.0, 2.0, 2.5, 4.0. Кстати, это установка Windows 10 Enterprise по умсшча­
нию, безо всяких обновлений.

Кow/1.ioтep\HКEY_LQCALМACH1"4E\SOFТWARE\,Мk.to,on\NEТ F� Setup\NoP
j MpSigStuЬ " Имlll T•n
) J МSВuilcl
З,.11-.!f4.ef'
�)(По yt,taJ1<1•1-ttМO) REG_SZ ан...-ме roe щмс1ЮеtЮ)
> MSDE
> 1 MSDRМ
> JI MSDK
> J MSf
> МSIME
> J м�,ng
1-1 MSМQ
J I MSNApps
) :J MSSQl.SerYtt
> J МТF
f· ' ""'""""""'
>. MfFЩ)l.rtT�
> • МTTKeyt,o.�ing,
>• Muitttnedi•
>JI Мultivaii•nt
.., 1 NfТFr.amewott�t\ip
� i1Ч!!)
1 > J COF
"'ш
::
>:
: jl vЭ.S
1 > ...

!' v ;J 114.0
1
1
! L J Cl1tnt
1 0S lrllegr•tlon
:, """"

Рис. 2.2. Установленные версии .NET


Справочник С#

Вот только судить только по названиям веток неправильно. Откройте


подраздел v4\Full и посмотрите на значения параметров Release и
Version. Первый параметр означает номер релиза .NET Framework - по
нему можно более точно определить номер версии. Однако гораздо про­
ще взглянуть на параметр Version - на рис. 2.3 видно, что установлена
версия 4.8.03761.
о х

,.._,
KOW'!...OT!p\HO:EY_tocдLМACHINE\S,OnW�uosoft\,ЧEТ Fr,mewooc Setup\NDf'\"4\full\10]]

�=ub
11 ,r·: мsо,
т,n
REG_SZ (5к--��,
R[G,_D'№RD 0.0000000,(1)
;, ,1. МSОМ1 R E G [)W()W
_ 0-0000000, (1}
,.!мят:
REG_DWORD OxOOGSOeti1 (S28049'1
7;) MSf
> t MSIM[
� MSUc�nsing
R [ GO_ WORO
REG..SZ
REG_5Z
.....
� {О)

;.t мsма -4АО3761


> .\ MSNApщ
> J MSSQ.Serwr
> 1 MTf
J..J t,Шfuщfкtots
'>1 MTFlnpuff�
>.), t.AТFl.eytx:111rt1M111pP/119s
> ! Muftimedi1
> ). Multfv.юant
vt.NПFr.amewortSetup
: .., f NOP
' > l COF
! : j ! ::
�50727

1: � :�:...
1.1
1 1:: :·
:1· ! j i ) J Client
1 •

@1;,о;.kт Ш&t-iШ

Рис. 2.3. Правильное определение версии .NET


r, Mi(ГOSOft .NЕТ framewooc х
Устгноаu Jite 6у.QеТ усnешно эаверwена. СН. на.е оntонме nрмчмм.

.НЕТ Ffмnewort 4.8 млn более ncв:»tet o6ttoeмщte y,u �ы Н�11 пом ммnыаrере. Рис. 2.4. Установщик .NET
Framework 4. 6.1: установле­
на более новш, версия

ожно вообще ничего не


пределять, а просто ска-
ать инсталлятор последней
�ерсии (на данный момент
ы используем 4.6.1 ). В слу-
ДonoлtlиrUWМ! сееденю1 о ;so{inogюpнt,;fW)I: 'е(Чl'fl!Щ:ф( НЕТEr:Aшe:wortc48-
ае если установлена эта
ли более поздняя версия,
Глава 2. Первая программа С# l-t/1-
инсталлятор сообщит вам об этом (рис. 2.4). В коде программы опреде­
лить версию .NET можно так, как показано в листинге 2.1. Именно этот
код приводится на сайте: https://msdn.microsoft.com и рекомендуется для
определения версии .NET самой Microsoft.

Листинг 2.1. Определение версии .NET Framework на С#


using System;
using Micr osoft.Win32;

private static v oid GetV ersionFr omRegistry()

// Открываем раздел реестра и читаем версию .NET


using (RegistryKey ndpKey =
RegistryКey.(penRffil:)teВaseКey(RegistryHive.Local.Мachine, "").
OpenSuЬKey(@"SOFI'WARE\Мicrosoft\NET Framework Setup\NDP\ "))

// В качестве альтерна.ТИIЫ, если установлена версия 4.5 или ВЫIЕ,


// можно использовать следующий запрос
// (RegistryKey ndpKey =
// RegistryKey.Op e nBaseKey(RegistryHive.LocalMachine,
// Registry V iew.Registry32).
// crensuЬKey(@"SOFIWARE\Мicrosoft\NEТ Framework Setup\NDP\"))
f oreach (string versionKeyName in ndpKey.GetSubKeyNames() )

i f ( versionKeyName.StartsWith( " v " ) )

RegistryКey versionКey = ndpКey.CpenSuЬКey(version КeyNaire);


string папе= (string)versionКey.GetValue(''Version ", "");
string sp = versionКey.GetValue("SP", "").ToString();
string install = versionKey.GetValue("In sta l l " ,
'"') . ToString();
if (install = = "") //по install inf o, must Ье later.
Console.WriteLine(versionKeyName + " " + name);
e lse

if (sp != "" && in stall "1")


Справочник С#

Ccnsole.WriteLire('111,!rsionКeyNзre + " " + nапе + "


SP" + sp);

if (name != "")

continue ;

fo reach (string suЬКeyName in verэionКey.GetSuЬКeyNames())

Pegi stryКey suЬКеу = versionКey.�y(suЬКeyNaпe);


name = (string)subKey.GetVal ue("Version", '"');
if (name ! = "")
sp = subKey .GetVal ue ("SP", "") . To String ();
install = suЬKey.GetVal ue("Install ", "").To String();
if (install == "") //нет инфо об установке
Console.WriteLiлe(versionКeyName + " " + name);
else

if (sp != '"' && install == "l ")

Ccnsole.WriteL:irE(" " + � + " " + rare


+ " SP" + sp);

el se if (install == "1")

Console.WriteLiлe(" " + su ЬКеуNапе + " " +


name);

Если у заказчика Windows 10/11, то, скорее всего, обновлять .NET


Frarnework не придется, поскольку будет установлена одна из последних
Глава 2. Первая программа С#

версий. В более старых выпусках Windows могут быть установлены ста­


рые версии платформы, поэтому вполне вероятно, что придется обнов­
лять .NET Framework.

Как обновить .NET Framework? Для этого произведите поиск в Google по


поисковой фразе «.NET Framework 4. 6 download)) (вы же помните, что
загружать .NET Framework можно только с сайта Microsoft) или пере­
йдите по адресу:

https://go.microsoft. com/fwlink/?linkid=2088631

Данная ссылка загрузит автономный установщик (который можно уста­


новить без наличия Интернет-соединения). На рис. 2.5 изображена
страница загрузки оffiinе-инсталлятора. Будет загружен файл NDР461-
:КВЗ 102436-x86-x64-All0S-ENU.exe размером 66 Мб. Запустите его и
следуйте инструкциям мастера установки. По окончании установки по­
надобится перезагрузка компьютера.

Microsoft .NET Framework 4.8


автономный установщик для Windows
П<:iдrH•C:Kdд.l!it
�фф�l(T\.IS.t-1Qro·
\i(д◊1!1>ЮЫ1��$1
Введение , ·, !!pt?Met,j\<

О Microsoft .NET Fr·ame"vork 4.8

,..,,:,os<,l'r .r,;n r,,_..,,.� Н! - )N •..,.:,,,,.<•>f>,<t,:<.c•d<><- 00>+:)ОМ:""" ..� ...�,.. V<• .NП Fr11:'tl•-·� 4
1- }Hiitнiiil
4.5,<1.S.1,.is.�,4.6.•H,.1. ..i6.2,.i7. •.71 и-4.�.l.

А•т,:,,,-,u....,· '1�<.,Т UО»:,Ю ""'"°"""''°"•,>Tk О ,,...,.,...,,�><. .!А � ,,_ш, �,о 4Т 6_,•,. ,., ><>011>1
.,� J� <;1<у,с·101<11 ,ю.-км.,,..,.,.,,. ,,,,,,,r ·••i- 3'ют """"' б<-,ь,,.�. ,,_.;., y,,,.....,..w, , ,,.,
,.,_,.к,...,от • ..., �"��'"'· f'••o..,..'<,11· .,,,."' "" ,,. / ,:1•tю•щи� ""•�cr<> ,,.,,_,.,...,,о
уп,>,,.:,ощ.....а ,:,.,� ,,nтнм.мькоil п;ю.,�,к:;�,<ТС"J;Ь•«=<тм и тp�<JWl�><i,il � np<>�y(кж>it ,ncc<-�-IO<T"

Рис. 2.5. Страница заzрузки ojfl.ine-uнcmaлляmopa

Ясное дело, что когда у вас будет серьезный проект, вы создадите для
него инсталлятор, который будет автоматически загружать нужную вер­
сию .NET Framework. Поэтому данный раздел предназначен больше не
Справочник С#

для заказчика, а для вас - программиста. Ведь пока вы еще не создали


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

2.2. Развертывание у программиста.


Установка Visual Studio Community
На компьютере программиста вам нужно установить .NET Framework
SDK, а чтобы созданные вами приложения могли работать на компьютере
заказчика, на нем нужно установить .NET Framework, желательно той же
версии (или более новой, чем ваш SDK). По сути, .NET Framework SDK
- это единственное, что вам нужно установить для начала разработки
приложений .NET. Раньше можно бьmо скачать сам .NET Framework
SDK и не устанавливать какую-либо IDE. В результате вы могли писать
программы прямо в блокноте или любом другом текстовом редакторе и
компилировать с помощью компилятора CSC.EXE.

Конечно, для работы с серьезным проектом IDE понадобится - она


значительно упрощает управление файлами проекта, отладку и многие
другие процессы, происходящие при создании программного обеспече­
ния. Я уже молчу о возможности быстрой разработки форм. Разработать
форму графического приложения в блокноте будет очень сложно. По­
этому без среды все равно не обойтись. В Microsoft это понимают, и по­
этому сейчас загрузка .NET Framework SDK невозможна без установки
Visual Studio. Но ведь раньше можно было установить .NET Framework
SDK совершенно бесплатно, а как же сейчас? Дискриминация? Нет, в
Microsoft выпустили бесплатную версию Visual Studio - Community, с
которой вы познакомитесь чуть позже. Если вы уже установили Visual
Studio, то устанавливать .NET Framework SDK вам не нужно, так как он
уже входит в ее состав.

Перейдите по адресу http://msdn.microso.ft.com/en-uslvstudio/aa496123.


На этой же страничке можно скачать не только SDK, но и установщики
самой платформы разных версий (см. рис. 2.6).
Глава 2. Первая программа С# со•
• • о
1 •• > ftJ • '$1 •

1
8§1 Microservices
о.-�»�-'И'IФt�
..,1mн1:•·••<бttu\l\"1\lr>P<.�1:\ol

О Cloud � Game Oc-velopmeпt Р, lnternet ofтt,ings


Cut\;:ime"1�fn,i{k»;1И"� �W�I09-A'�ft'IIМ М-11,Т,!о.'111,,""'h�""'-"·.._...1
0<0.-.-.e,ll'l,d6":J!�'fй'6cмn. !!\C-$tp,»Мrcltsidops,ptl,:,nt\ f,)!'l�l'�P\.i,d,эm,:,;

Рис. 2.6. Страница загрузки .NET Framework

Сейчас нас больше интересует раздел .NET SDK Downloads. В нем вы


можете скачать бесплатную версию Visual Studio Community. В отличие
от полной версии, данная версия Visual Studio совершенно бесплатна.
Перейти сразу к загрузке Visual Studio Community можно по адресу:

http://www. visualstudio. com/productslvisual-studio-community-vs

Вы скачаете файл vs_community_ENU.exe. Инсталлятор очень неболь­


шого размера - ведь загрузка необходимых файлов осуществляется с
сервера Microsoft, поэтому не отключайте Интернет во время установки
Visual Studio.

Примечание. Поняттюе дело, что Visual Studio Community не­


сколько ограниченная версия. Не нужно быть гением, чтобы
догадаться. Но для начинающих программистов (а вы таким
и являетесь, иначе бы не читали эту книгу) и обучения про­
граммированию возможностей этой версии будет достаточ­
но. Более того, даже в этой книге не будут раскрыты все
возможности этой IDE (ведь она позволяет разрабатывать
Справочник С#

приложения не только для Windows, но и для других платформ),


поэтому смело устанавливайте Visual Studio Community и не
думайте ни о каких ограничениях! Когда вам понадобится
платная версия, вы об этом узнаете. А пока нет смысла тра­
тить деньги на те функции, которыми вы не будете пользо­
ваться.

На рис. 2.7 показан инсталлятор Visual Studio Community. Установка с


возможностью создания классических приложений .NET и классических
приложений на С++ займет 12.05 Гб. Если нужны только классические
приложения на С++, то установка займет 8.85 Гб. Вкладка Отдельные
компоненты позволяет выбрать различные устанавливаемые компонен­
ты, в том числе версии .NET Framework, для которых можно откомпили­
ровать приложение (целевые версии).

И.l;e-r�-VIШl!StudioCommunll)'I022f>f�-17дO�<l.1 Х
о
Раб чие нагрузки О,,де11ьные 1tомnонент"1 Яsыковые пакеты Расnопожения установ�си

х Сведения об установ1ее
• Р.uраtю-тк. кпассичеа:мх nрМJЮЖ•···
Кл&есические м мобильные приложения (S)
_, Qlet,CТN ра]р"6.лl(М КМCO''lfCIOOI npi'!A.�

1 � =.6::;:::;��:�:т
� (!)еДСТ.8 Pl)p86on:м A/ljll Ml!ФOt)O,lllil .Ne••

l!i (рем,_а рuр.боФ>.а11• .NЕТ


l!J C\,1'41-"!Npaw-6oni.м.NHf111'1�4..8
fJ, �Юfol•ViblJ1ISt\ldio
� � ИtI0py,.,et<"r"A/!llfl'l:ityl'rllmtWOrte6
м Crf-.11(.1'•1 npoфtM;olpmwf'� .NП
,, K�W1:np1'11'0>Кel<MA(IIOIO�IONH1fet,1('1,V<$1.>1il... �
f.il h>ti!lloCod,:,
1:!'JJIТ-от.,1.11чио:
.. . tl LмЯ�rе
1 f:-, ,..,.._,uu--u-••'"'••Coo " /!il MLN1:J � Build�•
½.,.J ��nрм,оожен1111С•�V11 ···· no�--r•A-'"'IWICD<�"­
Windows с��у№6о<ю IО!О(ТJ:УИе.• l. P!'amp!МIProtкtlon•Dctfuia:Df
r:-_: Cpцtl'l!<lirn,pМI01�".Nf'l'fr�t4.&.•.
с_·. п-,-,.......И1!��6м61-м....
1 WIIIOOW$ CommuniatlOf'I foundr.ion

Р«--.
C�,.l'rogritmF.-,,�ViWfl�l\PI....,.- ,1.,,_..,...._.
rlf)QIIOIOQ,.,_t!pl'__.)'(AOe,.•�AA•ew6�..н,;woaыnrcuViswl!it-.юio.Mw1.-::enpe,t,11,1,_��0<eщ1n.c Oбiiier>le06��12.0SГБ
�Studio.llP)f'Oenpofl)fW-�0мoJ.11�1"1J)\fe'J<:1101д1!/l""'°'Q«)'UЯIIOl-��.t\.��!�M�tl
<:OП)'m�JIIШC't<--np,;tAW\..:I"IJWY-np,,o-�ycМ)&ИJl,rю;."'4(i('1<3><i\.

Рис. 2. 7. Выбор рабочих нагрузок

На вкладке Языковые пакеты выберите только пакет для русского язы­


ка, а на вкладке Расположение установки можно узнать, куда именно
устанавливается Visual Studio, и при необходимости изменить диск, ис-
пользуемый для установки.
Глава 2. Первая программа С#

��1IOU.1-1,'!WМS!OO'OC,ommumyl022i>rtмw-17дll�4.I
Рабочи� наrрузки DтАел•ные 1Сомnож�нты Яэыкоеь1е f\31(e'f"8 Расгооложе,ния устаноеtеи

Сведения об установке
.NET • РаэраЬот1<11 класс:ичеа:им nрилож•...
.NЕТ ""•mewotl( 4.б f..-getil\9 РаС11:
ГJ .NЕТ F11mewoft.4.6.1 Tм�lin9 P.кlr. -, �AOU ра-6Qт�11 �IIXЩ)'!.�..
-, С�• l)f)paбoflCII -'l!Я Mil,�...N€_.
l"."_"j .NEТfr1�""-'П.t6.2T1�ЩP.к.t
_, О11�В.iс:
(..; .NEТfr�.a,т.-�ingPIO
•д«ю!!t�ТU�
: .NEff•1�.a7.1Т1rgellfl!iJl't<t J2 Cl)",V:,Ul),l,pюoootAМ,.N(Т
t2 .NHfr1�◄.8Tм9etirlgl'Ю 5 �-�и.NHfr.ameworl\4.0
Г\.NfТММXSOКfo,Android t!J � for 'tllsuм SШdio
rJ .NftМAUISCЖkWIOS r',,il � ....�[niityfr.-tЬ
[.. i .NfТMALIISO!(forMKC��1 rJ C;,,e.liCIU l'll)Oф>1ll"l)ONК,.. .NH
е'.,1 mt�i(ode
f',I J!Т-оr�цщ<•
["OJ .NЕТ SDK f.:.r Android
1!.) LМ"SMt'1!
\"_""j ,NE! SOK 1.х Andmi,J v.il.t• ЮТ
ISI ML.NEТМool!IBuildt'f
;__ ; .NH SOK lor IOS
[".""jn�я-Ff.L'IЯIUIКCl<<tКDUIA...
l.. : .NEТЯЖlorM,c:C�.tt,,d П Pr�Proi;e<:tion-Octtusct:or
f" .Nff�t:l'f'Ьuildlooll; !!Cpц,.re,1PJ�6oц,,.NEТFr-1t-t.6...
n

·-�
MLNEТModr!Buil<Ж : : r_,_-"-�!!МОЙ6"о6мL
Г' ПимIOICpv,.!'«!frМ!"l/l'\NO/t4.f>.1 ]Wi�Com""-"'Ulioofowю,,tioo,
.. ,,. --;:,.,t

C:\PIOgl'"ernГRS\Miao�V8-Jal Studiol,202� ИJr-.,,>fJ.._.


n�.._., м.1 flPI<""-� l!(lt.---�
.tt.raet-'И!t! ...,u l!W4>ЖltCfa ewn,,c-.111 lf!SШI SNdio. м" т....:е �n. t
\f,su1I �цstroe AIIOfpet,!W<QII o6«�1W'.Ol-,o,,,..._tf:t<""P"f'!'3<ПM,,�,Uo:")'ЦJIМQI ;,'11-'�Дr.u,t:llor\a.�l�>U!>I
(QfffТQ�11"-"-nPQAQ.\J<A"-'"'т...,_�.....-re)'(-*'""11,�il.

Рис. 2.8. Выбор отдельных компонентов

И,.l'"l"yct6НOIIO-V'1SUll9.\JdlQ�20Ul"l"t'>'iew-17.0.0�4.1 х
Рабочие нагрузки ОтдеJlьные комnон�нты Языковые п;1кеты РасnОJ1ожени,� усrановки

Иll"rerpмpoм1<�q>t.pp.1�Viw11Stud,0Q
Сведения об усrановке
::���;�:f�����i�� St���-��� .. 4.зrr.
• Раэработка к11ассических nриложе.,,
... i:IJJOO-Чe:>I()
V Ч)l!,11С"1"а1 �хмrюrк11 ц,с<.,.чесК11� n�.�-­
"' (Pf".IIC"fblfmpIOOШl�Mllrфcpl.<ы.Nf ...
,-��Proor�D·�-i,\М��ft\v���io\P� .. ,..
цrrr.

Z!'J СФ:DММn. ош :WP'r)IGI IIOCllt yc,"8tt08t,;i4


rJ �1p,jpl6orot4'Jl.!'�Et
С1 C�.itnupnpt6o00t.NEТf1�◄.S
Общме�IW.IIЖ�rv"nкетыSОК :;,, s elmdfOl""V"-.!Swdio
5 ��м�Емt)'f•�6
r;I Чlt'A(Тll��JIO
С1 !n�lliCOOfi
!()w(М(Тe,tlll,l!tIO"P)'utflТЫIIIIИfflolStЖC::�ltpoиtt""""11pкooю_11_ 1,8216 �J!f-t>Tllцч,t,!f::
&иvestia,r.
r.l MLNEТMoaelBuHder
. j /l,Q�.,.-1ta•-,,,.uF,A,�M�JIМt'>!"l!Cl.l<Jlf1.•.
• ,'refmptiYeProtec1iorl-OoФ..iкJ1or
CPМ(;IU �/iom!.NEJ fr�k.◄ 6..
-
�1 �� ne-petlOl;l!UOil 6"6Jlм_
W\ndoм Commt1118(1tiQ,, Fot,nd;i!!Qn

,_
C�tmГ�es\Microюt.�Sl.udio\2022\Pmw
�illl, ... npw!IIIAИ""e)'(№8>tll"l'\�A!111�1""1)'CQ�Stuala.Mwтea;enpeA/l.,._803/Ю:w:,,oc::n.<:f.8ЩT11,C О6ще,е lfeOбm,._ �IК1'8С 12,0S fБ
\l'&nl!St\ldio.4A1c,enpcrp.m11t№e �i<e.Oo40Jl�p)'e!OI " \)'t"�Q� )"(IШl(>l)U.t..,��1�;:;,,�Mlll>I
conym-1� /1.o!tJ;Ьm<"- Продоюа,�. IW тн:же r.p,,>1-Tt' )CIICIЖ• ,tюе /1.11Цеt\5!11.. Ytl"iJt1080"1И(QЦl<l.)ltlll>I � �tнOelfl"lt

Рис. 2.9. Расположение установки

Когда все будет готово, нажмите кнопку Установить и дождитесь у ста­


новки (рис. 2.10).
•10 Справочник С#

Visual Studio lnstaller

аО, Visual S1.udio C.01Т1munity 2022 Preview


.,,.�, '""'""n.<щ," r,PQиn,rк. l "4Ь..., ;цт rr, v;s,.нl $,:.>dio �оп р�� А 11 ,м;........A�ЬIII
w.,.,,.lt'>«I� \(>•f\ГOOVrn:tlhr. fni,rtt, р,� ,,._,

"-"·""''"°'"l·Nt:l6�-.<.:,ndid•1•·1
� <11\'! lwl>J')'to•·.-.e- ,Nl:1'6�11:.e CМ\tlidM...

&�noc:М!)'("l....,_"1'
iЬ-...ly.N!f,m,t,Y-:m"\,'wiltli-'mth.r.t:',,<(y�
"3-т..w о �._:пуаt.(1 :;,:,cts t� шVilt.<41SшdOl>�UU
v�<;IU(l>ozo.l2 lsne<• «ldl$ mo<•ninomia...
ц;ц:rv11t,1,�lfkl<

Рис. 2.10. Установка в процессе

Рис. 2.11. Предложение зареги­


Visual Studio стрироваться
Вход в Visual Studio
- Синхрони.1ация параметров между устройствами
В зависимости от растороп­
ности вашего компьютера,
- Совместная работа е реа11ыiо� еремен,1 с помощью live Share

- &есnроблемна11 мнrеrрация со сJrужбамr1 Azure


установка Visual Studio может
занимать от нескольких десят­
ков минут до пары часов. Мы
рекомендуем устанавливать
Visual Studio на SSD-диск -
так вы сократите не только
время установки продукта, но
и время компиляции ваших
программ. При первом запуске
среда предложит вам подклю­
читься к сервисам для разработчиков - вы можете получить собствен­
ный Git-репозитарий (систему управления версиями), синхронизиро­
вать свои настройки и друтие возможности (рис. 2.11).
Глава 2. Первая программа С#

х Вы можете зарегистрироваться
прямо сейчас, а можете при
Visual Studio следующем запуске IDE. Далее
Запуск в знакомом окружении
Visual Studio предложит
Параметры разрабо-тк:и:
выбрать тему (ее также можно
будет изменить позже) - синяя
Выберите цветовую схему
(по умолчанию), темная или
светлая (рис. 2.12). Хотя по
умолчанию используется синяя
тема, для этой книги с целью

1я.
О Сини� {Аоnолн�тепьн... () Синяя
улучшения иллюстраций будет

гс
--: - ~'

э� параметры можн1> будет юмен�,пь nозже.


использована светлая.

Рис. 2.12. Выбор темы оформ­


ления

2.3. Первая программа с использованием


Visual Studio
Настало время создать наше первое приложение на С#. При запуске вы
увидите окно, предлагающее начать работу (рис. 2.13). Выберите коман­
ду Создание проекта. Выберите Консольное приложение С# и нажми­
те кнопку Далее.

Visual Studio 2022


Открыть поv.едние Начало р-,боты

_,.,,...,;,.,.:r.,.,;i..,..,..,.,.,,..,,.,.,.. ,_,�...,,.,.м.,.,.,,_,'71/,:ff
c�o.<c��A••fu.oc-:,;oroдc;,l')'М
..
'1p,-мtn0.

�.,_,.• -,,,.,�,...,_-...,М< """"


· "»�"'"'�ue.....,,м.,,.0<.,,ut.i,...,.,.,,i,,e11,..-•1,.._, о
_,,.,.11-.,��""""

Рис. 2.13. Начало работы


Справочник С#
- а х

Создание проекта ,,.


Поспе,цние шаблоны nроекtов

lli�-"�"�
,),-r. .t�� <йW"><t< l'lf)I,\-�,.,� ��м 'Тl'КШt. ... ттмн-п! -
--
•"'""""11':...::<"•q,<,Ч ><H<,:.,..aW:-...S.L�IIIC>'W�OS

Рис. 2.14. Выбор типа проекта

Настроить новый проект


Консопьное nри11ожение с• 1;r,,.,,. � """"'"""'

Рис. 2.15. Окно создания проекта

Введите название проекта - Hello и на жмите кнопку Далее (рис. 2.15).


В следующем окне просто нажмите кнопку Создать. Созданный проект
показан на рис. 2.16. Среда подготовила основной код для нашего кон­
сольного приложения. Вам больше ничего не нужно делать, поскольку
среда Visual Studio 2022 сразу генерирует код приложения Hello, world!.
Глава 2. Первая программа С#

� �,1" fliu.- k G<t �,n С-5щ- o,,-.,.u " х


�•0,f!!8,:\ • Detlug AttyCN �\lf

(
""""
- •• х
: =!]t�
: �

Рис. 2.16. Сгенерированный проект

Откомпилируем проект. Для этого выполните команду Сборка, Постро­


ить Hello (Build, Build Hello) (если вы ввели другое имя проекта, то
вместо Hello будет введенная вами строка). В нижней части окна вы уви­
дите отчет компилятора, а в нем - местоположение результирующего
ехе-файла (рис. 2.17).
х

11
е ><
........... ..... ........:1....... ... ........ .. .. .... Н............��; :� � С tl 16 .-"El. ..
,:?���:;
!, 5,.., htti:,1.;.l :..Jц....1tS.lмr.::�11JЦfllA::1�l&.U i'<ar -:r,i, ir,f-,,..,,iti.<::>
C:»>м:'\.,o.1"rit.t..:i,-e(�КcL\o, W..r\.dt•);

1
.,.
i l:Yt:!:�:��:::r:��,. ,�'�t;"?rr·
�...... ··""''"i>\){iS>'
.,,д..•
i':•>.,;,::Jy.:.,.;:,_..,,
n�- CJtLF ;;

Рис. 2.17. Вывод компилятора


•10 Справочник С#

Для запуска проекта можно нажать кнопку ► на панели инструмен­


тов. Однако будет открыто окно командной строки, которое моментально
закроется, как только завершится выполнение программы, а поскольку
она выводит только одну строку, то вы и глазом не успеете моргнуть,
как окно закроется. Поэтому откройте командную строку и перейдите
в каталог проекта, содержащий исполнимый файл (в нашем случае это
с: \users \ <имя>\source \repos \ <название проекта>\ <название проекта>\
bln\Debug\netб.O). Введите команду Hello - она запустит исполнимый
файл. Результат выполнения нашей программы приведен на рис. 2.18.

� ., " 1' ., �! � нeilQ • Нello > Ьin > 0eЬug > net6.0 v r; j., :l..'H(")',nt"l.f•G
................................................. •
8Р.а6оч�,ilстол
_iз.,оу,ос,,
j;,Qoq�"''"'
".,J06pu:�.мi

{1Нei!o.1U11tfmecмf'9
•·QrleC)rille

) f!111ю6р11-�
>O�"'u
►-Р-6о<,14$11с,о,о

Рис. 2.18. Результат выполнения hello.exe

Вот теперь у вас есть понимание, что такое .NET Framework, и есть среда
для написания .NЕТ-приложений. Значит, вы полностью готовы к даль­
нейшему изучению .NЕТ-программирования.

2.4. Новые шаблоны проектов С#


Посмотрите на рис. 2.16. Наша программа состоит из одной строки. Если
до этого вы программировали на Visual Studio 2019 или более ранней
версии, то заметите, что что-то не так. Раньше код программы выглядел
примерно так, как показано в листинге 2.2.
Глава 2. Первая программа С#

Листинr 2.2. Приложение Hello, world! при


использовании шаблонов .NETS
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Hello

class Program

static void Main(sting[] str)

Console.WriteLine("Hello, world!");

Среда Visual Studio 2022 генерирует код шаблонов в стиле .NET 6. Хотя
это две одинаковые программы. Разница лишь в самих шаблонах. Вы
можете использовать как новый стиль, так и старый стиль. Чтобы ис­
пользовать старый стиль, сохранив целевой платформой новейшую .NET
6, введите команду:

dotnet new console --framework netS.O --target-framework-override netб.O

При создании нового консольного проекта в Visual Studio вам будет пред­
ложено выбрать целевую платформу. Выбрать 5.0 нельзя, но в файле
проекта нужно произвести следующие изменения:
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netS.0</TargetFramework>
+ <TargetFramework>netб.0</TargetFramework>
Справочник С#

<ImplicitUsings>enaЫe</ImplicitUsings>
<NullaЫe>enaЬle</NullaЫe>
</PropertyGroup>

</Project>

Нужно изменить целевую платформу с 6.0 на 5.0, как показано выше.


Новые шаблоны несколько упрощают разработку приложения - вам
нужно написать только тело метода Main(). Вам не нужно заботиться о
включении друтих элементов программы.
ГЛАВА 3.

ОСНОВЫ СИНТАКСИСА С#
•ro Справочник С#

В данной главе нашего справочника мы рассмотрим основные конструк­


ции языка С#, в которых вам нужно разобраться, чтобы успешно изучить
платформу .NET Framework. Будет показано, как создать объект прило­
жения, как создать структуру метода Main(), который является точкой
входа (entry point) в любой исполняемой программе. Также рассматри­
ваются основные типы данных в С#. Данная глава больше рассчитана на
чуть более опытных программистов, нежели совсем на новичков.

3.1. Новый стиль шаблонов консольных


приложений
В прошлой главе мы вкраще затронули новый стиль консольных при­
ложений .NET 6. В этой главе мы разберемся подробнее. Используя но­
вый стиль, можно создавать программы, состоящие всего лишь из од­
ной строчки, как бьшо показано в прошлой главе. Конечно, реальные
программы существенно отличаются от нашей Hello, но как бы там ни
было, вам не нужно писать и даже видеть лишний код - вы пишете толь­
ко содержимое метода Main(). Вам больше не нужно писать инструкции
using, объявлять класс, пространство имен и т.д.

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


ства имен, класса и метода для вашей основной программы. Вы можете
посмотреть на код нового приложения и представить, что он содержит
инструкции внутри метода Main, сгенерированные более ранними
шаблонами.
Глава 3. Основы синтаксиса С# 10•
Вы можете добавить в программу больше операторов, так же как вы мо­
жете добавить больше операторов в свой метод Main в традиционном
стиле. Вы даже можете добавлять функции. Они создаются как локаль­
ные функции, вложенные в созданный метод Main.

Новый стиль подразумевает неявные директивы using. Это означает, что


компилятор автоматически добавляет набор директив using в зависимо­
сти от типа проекта. Для консольных приложений в ваше приложение
неявно включены следующие директивы:

• using System;

• using System.IO;

• using System.Collections.Generic;

• using System.Linq;

• using System.Net.Нttp;

• using System.Threading;

• using System.Threading.Tasks.

Нужно понимать, что новый стиль не меняет синтаксис С#. Он не отме­


няет операторы объявления пространства имен, класса и т.д. Для простых
приложений, где можно «отделаться» только методом Main() и несколь­
кими локальными функциями, такой стиль оправдан. Также он оправдан
для совсем начинающих программистов, которые впервые видят С#, и
старая организация программы с классами, пространствами имен, кучей
директив using их пугает, поэтому они отправляются учить Python, где
все это не нужно в самом начале. В Microsoft это понимают и делают все
для популяризации С# - так он больше не будет пугать школьников и
студентов. Но синтаксис самого языка остается неизменным. Вы можете
взять старый код, написанный в традиционном стиле, поместить его в
проект, созданный на базе нового шаблона, и все будет работать. На рис.
3.1 видно, как компилятор безо всяких ошибок откомпилировал код в
традиционном стиле.
Справочник С#

- 1) х
[:j

• ..• ()l\.(_\�t!!•�"�
·+

� .. 'iэ-::a•,r;
.•.•• ' "'''"'�"""""''··· �•w"'""'
,.ь·1 р.
е:1 � ..
�,•1,,no· lr.poell,fМ' 1 � 1;

" j) ,!,,_ t:l.lSi >'.Nlf,�'.ul ... � ,Jc!i


( ► l/lll/З...,cw,uoc,1,

"1, llit"-tJ� .,oJd !hin(str.in9[J ·;tr) • �• PfOQrtrr...«


11

1
(
�.<'lt>';<>\� .W,,,.Ji·.t•.l.n•(•t,•\lo, ,rnrtdt•);
'"
,, t>
16

(,р, 16 с-. 2 TIOy�-�



(R�F
(�,ц .' .
(&�с,о:1 ......,н
..
1>··••·• (�,11 "•�•ч: ��: -'Мlto, �•�• 0.Ьw1 .,,у CN •···•·
1* -�•• ",.._.р,от.......,,_ Npo№ -Nil'• .-..М-••- (....,.. о,. '"',,,._. llttn·Jlev 15'4PV'fI,9!!':l:t

---- <�•: 1..,,_: ,, с_.,_. •· •.1 ._.._, ,, �;


1..,_11.о •• C1\U.1,s\l)tflt1\5aUl'(11\�.,os\�l)•V,tll.o\Ы.t1\0мol,,f:\.-.et.Ь.•\Н,tJl.o.,HJ
е •-·-··

Рис. 3.1. Среда VS 2022 безо всяких возражений приняла код в традици­
онном стиле

Какой стиль использовать? Если нужно написать простенькое приложе­


ние, можно смело использовать новый стиль. Но когда вам нужно исполь­
зовать все преимущество объектно-ориентированного программирова­
ния, забудьте о новом стиле, который напрочь убивает саму идею ООП
(взять только объявление локальных функций внутри метода Main()).
Зачем тогда вообще было идти к ООП, чтобы откатиться назад. Также
при использовании традиционного способа вы видите, какие директи­
вы using используются, и сами контролируете, что будет включаться в
проект, а что нет. Традиционный способ дает полное представление об
использовании других классов, пространстве имен и т.д. Да и если вам
понадобится добавить дополнительные методы в вашу программу, вы не
сможете этого сделать, поскольку они будут объявлены как локальные
функции. Так что используйте традиционный код и не забивайте себе
голову всяким Руthоn-образным шаблоном.

Далее мы разберем код самой простой программы Hello в традиционном


стиле.
Глава 3. Основы синтаксиса С# k<J•
3.2. Исследование программы Hello, world!
3.2.1. Пространства имен, объекты, методы
В предыдущей главе нами была написана очень простая программа, вы­
водящая строку Hello, world! Чтобы вы не листали книгу, привожу ее код
еще раз - в листинге 3.1.

Листинг 3.1. Простая программа


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Hello

class Program
{
static void Main(string[] str)

Console.WriteLine("Hello, world!");

Давайте разберемся, что есть что. В языке С# вся логика программы


должна содержаться внутри какого-то типа. Тип - это общий термин,
которым можно обозначить любой элемент множества { класс, интер­
фейс, структура, перечисление, делегат}. В отличие от других языков
программирования, в С# невозможно создать ни глобальную функцию,
ни глобальный элемент �анных. Вместо этого нужно, чтобы все данные
и методы находились внутри ·определения типа. Посмотрите на листинг
3.1. В нем созданное пространство имен Hello. Внутри него объявлен
класс Program, в котором есть метод Main(), выводящий строку на экран.
Для простых программ такой подход кажется запутанным, но все оку­
пится, когда приложения станут отнюдь не простыми.
•10 Справочник С#

Важным элементом любой программы являются комментарии. В С#


используются традиционные комментарии в стиле С, которые бывают
однострочными (/() и многострочными (/* .. *().

Пример:

// это однострочный комментарий


/* это
многострочный
комментарий */

В первом случае комментарием считаются символы, начинающиеся от //


и до конца строки. Во втором случае игнорируются все символы между
/* и */.

Комментарии могу�' быть встроенными, например:

SomeMethod (Width, /*Height*/ 200);

Такие комментарии использовать не рекомендуется, так как они могут


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

Console.WriteLine("//этa строка не будет считаться комментарием");

Не стесняйтесь писать комментарии - со временем вы будете благо­


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

3.2.2. О методе MainO


По умолчанию классу, в котором находится метод Main(), назначает­
ся имя Program. При желании его можно изменить. Однако в любом
Глава 3. Основы синтаксиса С#

С#-приложении должен быть хотя бы один класс, определяющий метод


Main(). Тип приложения (консольная программа, настольная программа,
служба и т.д.) при этом значения не имеет. Метод Main() используется
как точка входа в приложение (выполнение приложения начинается с
этого метода), поэтому без него никак. Обратите внимание: С# является
регистрозависимым языком, поэтому Main() и main() - разные иденти­
фикаторы. Об этом нужно помнить.

Класс, в котором есть метод Main(), называется объектом приложения.


В одном приложении может быть несколько таких объектов (это удобно
при тестировании), но при этом компилятору нужно обязательно явно
указать, какой из этих методов нужно использовать в качестве точки вхо­
да. В Visual Studio это можно сделать в окне редактора свойств проекта
(вызывается командой Проект, Свойства: <имя>) в разделе Приложение.
- " х

• 'х

р.

1.....,._, �4'i.<�•"'-"""'Y-�,;;,rov;JI ;\o�-)\'Qf


,_.,,
,.....
..,.,_·--�-\,СП
Q�ж,п:<»Щ�4<1to<.Mtll.W�'ICl,P>t1'V.j)'(.\IIWf:jY.......,_
�'"'°'-""�-1".�-i»<Щ.$ ЩМ1tfl •>:.Нl<U-,,0-,,U.I
1•��~"" »!М<�'{)ф "t.«,J;F.1".·JICКil�� <\()!!М,:ЩI <t',W!(,>lt"''l,,:11 Щ)>'
J.iol'�.•·.o,ni;«"Wv.>t1"11.liti.!�...-,,�)<>tJ1i.rA�'<1""""'.,..,,,,,._.
fi<•�JlfИ'l'f<:l!

..........
Рис. 3.2. Параметр <<Автоматически запускаемый объект" позволяет
задать объект, из которого будет вызван метод Main()

В описании метода Main(), в его сигнатуре, есть ключевое слово static.


Область действия статических членов охватывает уровень всего класса (а
не уровень отдельного объекта), поэтому они могут вызываться без пред­
варительного создания нового экземпляра класса. Метод Main() имеет
•1<1 Справочник С#

один параметр, представляющий собой массив строк (string[] args). В


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

Логика программы очень проста. Мы используем метод WriteLine() из


класса Console, который, в свою очередь, находится в пространстве имен
System. Данный метод просто выводит строку, заданную программистом,
и символ возврата каретки на стандартное устройство вывода. Помните,
в предыдущей главе я говорил, что среда добавляет избыточные про­
странства имен? Это так, можно удалить подключения всех пространств,
кроме System. Тогда и текст программы будет несколько короче.

Также в предыдущей главе рекомендовалось открыть окно командной


строки, чтобы увидеть результат выполнения программы. Если добавить
вызов метода ReadLine() после вызова метода WriteLine(), то программа
будет ожидать ввода пользоватеJIЯ, следовательно, не закроется, пока вы
не нажмете Enter:

Console.WriteLine("Hello, world!");
Console.ReadLine();

Такое поведение полезно при отладке программы, а в релизе нужно уда­


лить «лишние» вызовы. Хотя вряд ли вы в наши дни будете создавать
консольные приложения.

Метод Main() можно задать иначе, чем приведено в листинге 3.1. Глав­
ное, чтобы в программе был идентификатор Main() в одном из классов.
А как он будет определен, то есть какие параметры он будет принимать
и что будет возвращать, компилятору все равно. Рассмотрим несколько
вариантов этого метода:

static int Main()


{
return О;
Глава 3. Основы синтаксиса С# c-t-1•
static int Main(string[] args)

return О;

static void Main()

Первый метод Main() не принимает никаких параметров, возвращает О.


Второй принимает массив строк и возвращает О. Последний ничего не
принимает и ничего не возвращает. Некоторые программы раньше (это
касалось в основном консольных программ) обычно возвращали код
ошибки: О означал, что ошибок нет, а конкретное число, отличное от О,
- код ошибки. Используя оператор return метода Main(), можно реали­
зовать подобное поведение программы.

3.2.3. Обработка переданных параметров

Если в метод Main() передаются параметры, то обработать их можно


примерно так:
static void Main(string[] args)
{
for(int i = О; i < args.Length; i++)
Console.WriteLine ("Arg: {О}", args[i]);
Console.ReadLine ();

Данный код выводит все переданные приложению параметры и ожидает


ввод пользователя (нажатие Enter). Если вы знакомы с синтаксисом С/
Java, то этот код вам предельно знаком. Сначала мы проверяем свойство
Length массива args. Пока переменная i меньше Length, мы выводим па­
раметры.

Если использование цикла for кажется вам слишком древним подходом,


так оно и есть. В С# можно использовать циклfоrеасh, и код вывода па­
раметров можно переписать так:
- J.<J Справочник С#

foreach (string arg in args)


Console.WriteLine("Arg: {0}", arg);
Console.ReadLine();

Так получается компактнее и лаконичнее, но принцип тот же.

Из соображений отладки передать аргументы командной строки можно


с помощью окна свойств проекта. На вкладке Отладка нажмите ссылку
Открыть пользовательский интерфейс профилей запуска отладки
и задайте аргументы командной строки в поле Аргументы командной
строки (рис. 3.3).

11 ''""""''""·' ""' "''


� Ф,J�... i"f!,Нц e"..t (i<I n� (t,c,pq о,-..,ц_u r..:, д,,,..-..," <:P"№J.J Pм;:w��R �-"10 O,p�IOOI :,,,,-,н;(t-1•_;); ,Р il:l,':<11< ,,, б -,
ф• � • li{t В� � • Det,,19 �j /""?'.�� ... � • tч!!<р t,, J8 \,!.-.,�,� Ji! �

t! � --�!'!,ma;r;;:,,,. "'"'>'<"""

i np>11!()Ж!H<t<<!
r., ••
i
е i"J!k'J
СЩц

lf
-� fl3i«!•

11.;.:::
Ccxtf,ЩlfJ,$

"'"""= r
Иc---�-.,..o•-n-n,�
CJ У..uы,и: '"'-'Cl'k\t'<lll<-.�>u«ыы:11 "'"Р"'--«еу ttaYA��,,(>l,O OI0""1-Pte.

П.,pe--q>e,\W • • х
r"'j,_\<': ,·;,o-;;t,c .,,,,,�,;.-+<-«>(о>ОСZ,<М<� !��,.,_ '><'Yt"-" '\il!"'"'-Иt-1 :11,'1<'1(0
r;,,p,-..,,,.,...-"'P.•::it,,;,11,,...,.,.•.,_,..,.."i,;,,.,..,,.,,_.,,..,..P<",._,,.,...,..,,.,.__,,,....,,�._....-,.,.,,,.,
�><;,,vи,1 �'<lt�"''" ,.,.;_ 111,-.,,�_.,,:,. �'"t���-,�1 "-��, ✓,,,\>N'...,,J_,•.-,:,ж1. 3•>N,,,O<ll :< "IKJ�
Р�f.•••••::м .,..,,,,,., ,�,,,,_,., "'>"'-'«'>yt,-,.v,_,..,,,. < ,......,,,,.,._ю ,.,_� ...,n,.- ;,';

• Рис. 3.3. Задаем аргументы командной строки


'jфjфjj§Щ41;щ: м

3.3. Класс System. Console

Чтобы не углубляться в программирование графических интерфейсов


(по сути, это тема для отдельной книги), большая часть рассматривае­
мых в этой книге примеров будет строиться на базе консольных при­
ложений, поэтому без класса System.Console никак не обойтись. Такой
подход позволит больше внимания уделить именно синтаксису С#, а не
свойствам элементов графического интерфейса.
Глава 3. Основы синтаксиса С#

Подробно класс System.Console мы рассматривать не будем, так как все


мы понимаем, что приложения для консоли на сегодняшний день ред­
кость. Поэтому будут рассмотрены только те его методы, которые нам
пригодятся. Мы уже знакомы с методом WriteLine(), выводящим пере­
данный ему текст и символ возврата каретки. Если нужно вывести текст
без возврата каретки, используется метод Write().

Для чтения информации, вводимой пользователем, используются мето­


ды ReadLine() и Read(). Первый читает из потока все символы, пока не
будет нажата клавиша Enter. Второй читает из потока только один сим­
вол. Понятно, что метод Read() использовать неудобно.

Рассмотрим пример ввода данных:


Console.Write("Bвeдитe свое имя: ")
string usernarne = Console.ReadLine();
Console.WriteLine("Пpивeт {О}!", usernarne);
Console.ReadLine();

"

А
р ••�:�•,,М•\\•Nbl
·1---
: !\�,!::�:,?��-,:.�: � · •:.,
. Cll "-..i. 'H<IIIO' .,""'°"lfl"'; 1 ,_ Jj
-:;ф;'•-

И
1: �\..f;�A�=�:
� f.tc"t.ir; v<>id "".iн(,.тri.ng[] [f\·r)
f • �JJl'rtli:,.1111
11- -Jil••<=..-­
► t:•P,�f""'-"!i
{ //C<,r.�,)le.iolritll.'1...1"�(•«.,.\L<::, :.<.=1·\d! );
м

;���::::�:: �;::�:�1�;'Я/���j1�;;�����-!�;_;�;;
<<>n:<.,r,;!,,:.�.rH.,("!m•Jlмr♦ <::0<1• ,.,._n; •);
fitri.r.g \ls"r,,«,.., .. f: ....��l"'·.il"'"'<i'·.:1""�1• ,;,·
<мм:-1.<': .itri.t.,..L.i.n.,(�f!p1<м.- {е} !", u.serna.м)-;
<:1>"1-<'\s-.l<ei.1H..:i1>,..(};

Рис. 3.4. Ввод и вывод

Примечание. Чтобы русский текст корректно отображался


в консоли, раньше нужно было сначала установить кодировку
•rв Справочник С#

866 в качестве кодировки вывода по умолчанию. Сейчас же при


попытке вызвать метод GetEncoding(866) вы получите исклю­
чение, как показано на рис. 3.4. Не нужно настраивать консоль
в современном мире - все прекрасно работает по умолчанию.
Как ни крути, на дворе 2022 год и повсеместно используется
UTF-8.

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


и присваиваем его переменной userName типа string. После - выводим
методом WriteLine() приветствие. Внутри методов Write() и WriteLine()
могут быть метки, задающие номер параметра. Нумерация начинается с
О. В данном случае {О} будет заменен на значение переменной userName.

Последняя строка Console.ReadLine(); нужна, чтобы приложение сразу


не закрылось, и вы могли просмотреть его вывод. Если вы ранее про­
граммировали на С, то наверняка заметили сходство метода WriteLine()
с функцией printf(). Да, в .NET используется для форматирования строк
стиль, напоминающий стиль printf(). Давайте разберемся, что и к чему.

В качестве первого параметра методу WriteLine() всегда передается стро­


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

При необходимости метку можно повторять несколько раз в строке,


например:

Console.WriteLine("Cooбщeниe для {0)\nУважаемый {О)!", username);

Для форматирования числовых данных используются символы форма­


тирования. Они добавляются в метку так {номер:символ_форматирова­
ния}, например:

Console.WriteLine("A {О :d) ", А);


Глава З. Основы синтаксиса С# k<:J•
Символы форматирования приведены в таблице 3.1.

Таблица 3.1. Символы форматирования числовых данных в .NET


Символ
Описание
форматировании

С (или с) Форматирование денежных значений

D (или d) Форматирование десятичных чисел

Экспоненциальное представление. Регистр символа озна-


Е (или е) чает, в каком регистре будет выводиться экспоненциальная
константа - в верхнем (Е) или в нижнем (е)
Числа с фиксированной точкой. Число после символа озна-
F (или f) чает количество знаков после точки, например, { О:tз} выве-
дет 1.000, если в качестве О указать значение, равное 1

G (или g) Общий формат

N (или n) Базовое числовое форматирование (с запятыми)

Форматирование шестнадцатеричных чисел. Если указан Х,


Х (или х) то в hех-представлении символы будут в верхнем регистре,
например, l АН, а не 1 ah, если указан х

Примеры:
Console.WriteLine("Знaчeниe 123456 в разных форматах:");
Console.WriteLine ("d7: {0:d7}", 123456);
Console.WriteLine("c: {0:с}", 123456);
Console.WriteLine("n: {0:n}", 123456);
Console.WriteLine("f3: {O:f3}", 123456);

Результат будет таким, как показано на рис. 3.5.


Справочник С#

,.. C.'\WindOWs\SystemЗ2\cmd.e1e о х

Рис. 3.5. Значение в разных форматах

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


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

string Msg = string. Format ( "d7: {О: d7} ", 123456);


Windows.Forms.MessageBox.Show(Msg);

Метод string.Fonnat() понимает тот же формат форматирования, что и ме­


тод WriteLine(). Далее отформатированную строку можно использовать,
как вам будет нужно. В данном случае мы выводим ее в MessageBox.

3.4. Типы данных и переменные


3.4.1. Системные типы данных

В любом языке программирования есть собственный набор основных


(системных) типов данных. Язык С# в этом плане не исключение. Но в
отличие от С, в С# эти ключевые слова - не просто лексемы, распозна-
Глава 3. Основы синтаксиса С#

ваемые компилятором. Они представляют собой сокращенные варианты


обозначения полноценных типов из пространства имен System. Напри­
мер, тип Ьоо/ - это системный тип System.Boolean. Таблица 3.2 содер­
жит информацию о системных типах языка С#. Обратите внимание на
колонку CLS: она означает, отвечает ли тип требованиям общеязыковой
спецификации (CLS). Как уже упоминалось ранее, если вы в программе
используете типы данных, которые не соответствуют CLS, другие языки
не смогут их использовать.

Таблица 3.2. Системные типы данных С#


Системный
Тип в С# Диапазон CLS Описание
тип
Логическое (бу-
bool Syste.Boolean true, false Да
левое) значение
8-битное число
sbyte System.SByte -128... 127 Нет
со знаком
8-битное число
byte System.Byte 0... 255 Да
без знака
16-битное число
short System.Int16 -32 768... 32 767 Да
со знаком
16-битное число
ushort System.Int 16 0... 65 535 Нет
без знака
-2 147 483 648 ...
32-битное число
int System.Int32 Да
со знаком
2 147 483 647
32-битное число
uint System.Ulnt32 О" .4 294 967 295 Нет
без знака

-9 223 372 036 854


775 808
64-битное число
long System.Int64 ... Да
со знаком
9 223 372 036 854
775 807
•са Справочник С#

ulong System.Ulnt64
о... 18 446 744073
Нет
64-битное число
709 551 615 без знака

Один СИМВОЛ (16


char System.Char U+0000...U+FFFF Да
бит, Unicode)

32-битное число с
float System.Single +1,5х10-45 ... 3,4xl038 Да
плавающей точкой

decimal System.Decimal
±l,0xl0e·28 ... Да
96-битное число
-7,9х1028 со знаком

64-битное число с
douЫe System.DouЫe 5, Ох 10·324 ... 1, 7х 10308 Да
плавающей точкой
Ряд символов
Ограничен объемом
string System.String Да в кодировке
доступной памяти
Unicode
Используется для Базовый класс
object System.Object хранения любого Да для всех типов в
типа в памяти .NЕТ

3.4.2. Объявление переменных

Объявление переменной осуществляется так же, как и в С: сначала нуж­


но указать тип переменной, а затем - ее имя:

int Х;

После объявления переменной ее нужно инициализировать (присвоить


значение)- до первого использования:

х = О;

В случае использования локальной переменной до присваивания ей на­


чального значения компилятор сообщит об ошибке.
Глава 3. Основы синтаксиса С# t-ri:J-
Инициализировать переменную можно при объявлении:

int х = О;

При желании можно в одной строке объявить и инициализировать сразу


несколько переменных:

int х = 1; у = О;
int а, Ь, с;

3.4.З. Внутренние типы данных


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

• Для переменных типа bool - значение false;


• Для переменных числовых типов - значение О;
• Для типа string - один пустой символ;
• Для типа DateTime - 1/1/0001 12:00:00;

• Для объектных ссылок - значение null.

Примеры:

bool Ь = new bool(); // будет присвоено значение false


int х = new int(); // будет присвоено значение О

Обычно с помощью new создаются объекты в классическом их понима­


нии. Для числовых/булевых/строковых переменных обычно проще ука­
зать значения при инициализации, чем использовать new:
•иt1 Справочник С#

bool Ь = false;
int х = О;

3.4.4. Члены типов данных


Числовые типы в .NET поддерживают свойства MaxValue и MinValue,
позволяющие получить информацию о допустимом диапазоне значений
типа. Кроме свойств MaxValue и MinValue, тип DouЫe поддерживает
следующие свойства:

• Epsilon - эпсилон;
• Positivelnfinity - положительная бесконечность;

• Negativelnfinity - отрицательная бесконечность.

Пример:

int х = int.MaxValue;
douЫe d = douЫe.Epsilon;

Понятное дело, что тип данных System.Boolean не поддерживает свой­


ства MinValue и МахValue. Но зато он поддерживает свойства TrueString
и FalseString, которые, соответственно, содержат строки "True" и "False".
Пример:

Console.WriteLine("TrueString {О}", bool.TrueString);


Console.WriteLine("FalseString {О}", bool.FalseString);

Текстовые данные в С# представлены типами System.String (или


просто string) или System.Char (char). Оба типа хранят данные в коди­
ровке Unicode. Первый тип данных позволяет хранить строку, второй -
ТОЛЬКО ОДИН СИМВОЛ.

Тип char поддерживает следующие методы:


Глава 3. Основы синтаксиса С#

• IsDigit() - возвращает true, если переданный символ является де­


сятичной цифрой, в противном случае возвращаетсяfа/sе;

• IsWhiteSpace() - возвращает true, если переданный символ явля­


ется пробельным символом (пробел, табуляция и др.);

• IsPunctuation() - возвращает true, если переданный символ явля­


ется знаком пунктуации.

Пример:

Console.WriteLine("{O}", char.IsDigit('l'));

О типе string мы поговорим в следующем разделе, когда будем рас­


сматривать работу со строками.

3.4.5. Работа со строками


Строки в любом языке программирования являются одним из самых
важных типов данных. В С# для строк используется тип string (систем­
ный тип System.String), а для хранения одиночных символов исполь­
зуется тип char. В других языках программирования строки являются
массивами символов. В языке С# строки являются объектами, что будет
продемонстрировано далее.

Объявить строку можно так:

string <имя переменной > [= "значение"];

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


можно также использовать и массивы символов, например:

char[] carray = {'е', 'х', 'а', 'm', 'р', 'l', 'е'};

Превратить массив символов в тип данных string можно так:


•rю Справочник С#

string str = new string(carray);

Члены класса System.String

Настало время рассмотреть тип System.String, предоставляющий набор


различных методов для работы с текстовыми данными (см. табл. 3.3).
Далее эти методы будут рассмотрены подробно.

Таблица 3.3. Некоторые члены типа System.String

Член Описание

Length Свойство, содержащее длину текущей строки

Метод, позволяющий сравнить две строки. Статический


Compare()
метод
Метод, позволяющий определить, содержится ли в стро-
Contains()
ке определенная подстрока
Метод, позволяющий проверить, являются ли две строки
Equals()
эквивалентными
Метод, использующийся для форматирования строки.
Format()
Статический метод
Insert() Позволяет вставить строку внутрь другой строки

Позволяют дополнить строку какими-то символами,


PadLeft(), PadRight()
соответственно, слева и справа

Remove() Используется для удаления символов из строки

Replace() Замена символов в строке

Split() Разделение строк на подстроки

Удаляет все вхождения определенного набора символов с


Trim()
начала и конца текущей строки
Создают копию текущей строки, соответственно, в верх-
ToUpper(), ToLower()
нем и нижнем регистре
Глава 3. Основы синтаксиса С#

Базовые операции
Работа с членами System.String осуществляется довольно просто -
нужно объявить переменную типа string и получить доступ к методам
(членам) класса через операцию точки. Но имейте в виду, что некоторые
члены System.String представляют собой статические методы и потому
должны вызываться на уровне класса, а не объекта.

Рассмотрим некоторые примеры:

string h = "Hello";
Console.WriteLine("h = {0)", h);
Console.WriteLine("Длинa h = {О)", h.Length);
Console.WriteLine("h в верхнем регистре {О)", h.ToUpper());
Console.WriteLine("h {0)", h);
Console.WriteLine("h в нижнем регистре {О)", h.ToLower());
Console.WriteLine("h {О)", Ь);
Console.WriteLine("h содержит е? : {О)", h.Contains("e"));
Console.WriteLine("Зaмeнa {0)", h.Replace("lo", ""));
Console.WriteLine("h = {О)", h);
Console.ReadLine();

\ ■ C:\WindoWS\system32\cmd.exe • Hel10 □ х

Рис. 3. 6. Работа со строками


•rв Справочник С#

Обратите внимание на вывод этого кода (рис. 3.6). Методы ToLower(),


ToUpper(), Replace() и другие не изменяют строку, а работают с ее копи­
ей. Они возвращают измененную копию строки. Если вы хотите изме­
нить саму строку, это делается так:

h = h. ToUpper() ;

Методы ToUpper() и ToLower() объявлены так:

puЫic string ToUpper()


puЫic string ToLower()

Сравнение строк
Для сравнения строк используется метод Compare(). Существует много
разных форм вызова этого метода. Рассмотрим формы, использующиеся
для сравнения целых строк:

puЫic static int Compare(string strA, string strB)


puЬlic static int Compare(string strA, string strB, bool ignoreCase)
p.iЬlic static int Carpare(string strA, string strВ, StringCaтpariscn crnpariscnТype)
puЬlic static int Carpare(string strA, string strB, Ьооl ignoreCase, Cultureinfo
culture)

В данном случае метод сравнивает строку strA со строкой strB. Возвра­


щает положительное значение, если строка strA больше строки strB; от­
рицательное значение, если строка strA меньше строки strB; и нуль, если
строки strA и strB равны. Сравнение выполняется с учетом регистра и
культурной среды. Если параметр ignoreCase равен true, то при сравне­
нии не учитываются регистр символов. В противном случае (false) эти
различия учитываются.

Параметр comparisonType определяет конкретный способ сравне­


ния строк. Класс Culturelnfo определен в пространстве имен System.
Globalization. Используется для лучшей локализации программы.
Глава З. Основы синтаксиса С#

С помощью метода Compare() можно сравнивать фрагменты строк, а не


целые строки:

puЫic static int Cornpare(string strA, int indexA, string strB, int indexВ, int
length)
puЫic static int Cornpare(string strA, int indexA, string strB, int indexВ, int
length, Ьооl ignoreCase)
puЬlic static int Cornpare(string strA, int indexA, string strB, int indexВ, int
length, StringCornparison cornparisonType)
puЬlic static int Cornpare(string strA, int indexA, string strB, int indexВ, int
length, Ьооl ignoreCase, Cultureinfo culture)
puЬlic static int CompareOrdinal(string strA, string strB)
puЬlic static int CrnpareOrdinal(string strA, int indexA, string strВ, int indexВ, int
count)

Данная форма сравнивает фрагменты строк strA и strB. Сравнение на­


чинается со строковых элементов strA[indexA] и strВ[indexB] и включа­
ет количество символов, определяемых параметром length. Метод воз­
вращает положительное значение, если часть строки strA больше части
строки strB; отрицательное значение, если часть строки strA меньше
части строки strB; и нуль, если сравниваемые части строк strA и strB
равны. Сравнение выполняется с учетом регистра и культурной среды.
Аналогично, можно указать параметры ignoreCase и Culturelnfo, как в
предыдущем случае.

Кроме метода Compare(), есть и метод CompareOrdinal(), который рабо­


тает так же, как и Compare(), но не имеет параметра Culturelnfo() и ло­
кальных установок. Метод объявлен так:

puЬlic static int CompareOrdinal(string strA, string strB)


puЬlic static int CmpareOrdinal(string strA, int indexA, string strB, int indexВ,
int count)

Метод Equals() также используется для сравнения строк. Различные фор­


мы этого метода и их описания представлены в таблице 3.4.
•tW/1 Справочник С#

Таблица 3.4. Формы метода Equals()

Синтаксис Описание

Метод возвращает true, если вызывающая


строка содержит ту же последователь­
рuЫiс override bool Equals (object ность символов, что и строковое пред­
оЬj ) ставление объекта obj. Выполняется по­
рядковое сравнение с учетом регистра, но
параметры локализации не учитываются

Возвращает true, если вызывающая строка


puЫicboolEquals (stringvalue) содержит ту же последовательность сим­
волов, что и строка value. Выполняется
puЫic bool Equals (string порядковое сравнение с учетом регистра,
value, StringComparison но параметр локализации не используется.
comparisonType) Параметр comparisonType определяет кон­
кретный способ сравнения строк

Возвращает логическое значение true,


puЫic static bool если строка а содержит ту же последова-
Equals (string а, string Ь) тельность символов, что и строка Ь. Вы-
полняется порядковое сравнение с учетом
puЫic staticboolEquals (string регистра, но параметры локализации не
а, string Ь, StringComparison используются. Параметр comparisonType
comparisonType) определяет конкретный способ сравнения
строк

Поиск в строке
Для поиска в строке в С# есть множество методов. Начнем с метода
Contains(), который описан так:

puЫic bool Contains(string value)

Это самый простой метод, позволяющий определить, есть ли в строке


определенная подстрока.
Глава 3. Основы синтаксиса С#

Метод StartsWith() позволяет определить, начинается ли вызывающая


подстрока с подстроки value (если так, метод возвращает value, иначе
метод возвращает fa/se). Параметр comparisonType определяет конкрет­
ный способ выполнения поиска. Синтаксис следующий:

puЫic bool StartsWith(string value)


puЫic bool StartsWith(string value, StringComparison comparisonType)

Аналогично, существует метод EndsWith(), который возвращает true,


если вызывающая строка заканчивается подстрокой value:

puЫic bool EndsWith(string value)


puЫic bool EndsWith(string value, StringComparison comparisonType)

Для поиска первого вхождения заданной подстроки или символа исполь­


зуется метод IndexOf():

puЫic int IndexOf(char value)


puЫic int IndexOf(string value)

Если искомый символ или подстрока не обнаружены, то возвращается


значение -1. В противном случае возвращается позиция, с которой на­
чинается подстрока, или позиция, где впервые встречается заданный
символ. Если нужно начать поиск с определенной позиции Startlndex, то
синтаксис вызова метода будет немного другим:

puЬlic int IndexOf(char value, int startindex)


puЬlic int IndexOf(string value, int startindex)
puЬlic int IndexOf(char value, int startindex, int count)
puЬlic int IndexOf(string value, int startindex, int count)

Поиск начинается с элемента, который указывается индексом startlndex,


и охватывает число элементов, определяемых цараметром count (если
указан).
•t<A Справочник С#

Последнее вхождение символа/подстроки можно найти методом


LastlndexOf(). Параметры у этого метода такие же, как и у IndexOf(). Ус­
ложним задачу. Представим, что нужно найти не просто первое вхож­
дение какого-то символа, а первое вхождение одного из символов, на­
пример, есть строка Hello и нам нужно определить первое вхождение
символов / и о. Можно дв� раза вызвать метод IndexOf() - для символа
/ и для символа о, однако это неэффективно. Гораздо удобнее исполь­
зовать метод IndexOfAny(), которому можно передать массив искомых
символов:

puЫic int IndexOfAny(char[] anyOf)


puЬlic int IndexOfAny(char[] anyOf, int startindex)
puЬlic int IndexOfAny(char[] anyOf, int startindex, int count)

Первый параметр - это массив искомых символов, второй - начальная


позиция поиска, третий - счетчик символов. Метод возвращает индекс
первого вхождения любого символа из массива anyOf, обнаруженного в
вызывающей строке. Метод возвращает значение -1, если не обнаружено
совпадение ни с одним из символов из массива anyOf. Вернемся к наше­
му примеру. У нас есть строка Hello и мы хотим найти позицию символа
/ или символа о - какой встретится раньше. Обратите внимание, метод
возвращает одну позицию, а не массив позиций - каждого из приведен­
ных символов. Пример кода:

string s = "Hello";
char[] Ch = { '1', 'о' ) ;
if (s.IndexOfAny(Ch) != -1)
Console.WriteLine("Oдин из символов был найден в позиции {0)",
s.IndexOfAny(Ch));

Аналогично методу IndexOfAny(), существует метод LastlndexOfAny(),


который осуществляет поиск с конца.

Конкатенация строк
Переменные типа string можно соединить вместе, то есть выполнить
конкатенацию. Для этого используется оператор +. Компилятор С#
Глава 3. Основы синтаксиса С#

преобразует оператор + в вызов метода String.Concat(), поэтому вы мо­


жете или использовать + или метод Concat() - как вам больше нравится:

string s1 "sl";
string s2 "s2";
string s3 s1 + s2;

Метод Concat() объявлен так:

puЫic static string Concat(string strO, string strl);


puЬlic static string Concat(params string[] values);

Разделение и соединение строк


Представим, что есть строка, содержащая какой-то разделитель (сепара­
тор). С помощью метода Split() можно разделить эту строку на подстро­
ки. Метод, возвращающий массив string с присутствующими в данном
экземпляре подстроками внутри, которые отделяются друг от друга эле­
ментами из указанного массива separator:

puЬlic string[] Split(params char[] separator)


puЬlic string[] Split(params char[] separator, int count)

Если массив separator пуст или ссьшается на пустую строку, то в каче­


стве разделителя подстрок используется пробел. Во второй форме дан­
ного метода возвращается количество подстрок, заданное параметром
count.

Существуют и другие формы вызова метода Split():

puЫic string[] Split(params char[] separator, StringSplitOptions options)


puЫic string[] Split(string[] separator, StringSplitOptions options)
pililic striлg[] Split(pзrams char[] separator, int CXJ1.П1t, StringSplit(ptions options)
puЫic string[] Split(string[] separator, int count, StringSplit(ptions options)
Справочник С#

Разница, как видите, заключается в параметре options. В перечисле­


нии типа StringSplitOptions определяются только два значения: None
и RemoveEmptyEntries. Если параметр options принимает значение
None, то пустые строки включаются в конечный результат разделе­
ния исходной строки. А если параметр options принимает значение
RemoveEmptyEntries, то пустые строки исключаются из конечного ре­
зультата разделения исходной строки.

С помощью метода Join() можно решить обратную задачу, а именно


построить строку по массиву строк, разделив каждый элемент этого мас­
сива каким-то разделителем. Синтаксис такой:

puЬlic static string Join (string separator, string[] value)


puЫic st atic string Join (string separator, string[] value, int startindex,
int count)

В первой форме метода Join() возвращается строка, состоящая из


соединяемых подстрок из массива value. Во второй форме также воз­
вращается строка, состоящая из элементов массива value, но они соеди­
няются в определенном количестве count, начиная с элемента массива
value[startlndex]. В обеих формах каждая последующая строка отделяет­
ся от предыдущей разделителем, заданным separator.

Заполнение и обрезка строк


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

puЫic s tring Trim()


puЫic s tring Trim(params char[] trimChar s )

Первая форма удаляет из вызывающей строки начальные и конечные


пробелы. Во второй форме удаляются начальные и конечные вхождения
Глава 3. Основы синтаксиса С# k№:J•
в вызывающей строке символов из массива trimChars. В обеих формах
возвращается получающаяся в результате строка.

Методы PadLeft() и PadRight() позволяют дополнить строку символами


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

puЫic string PadLeft(int totalWidth)


puЫic string PadLeft(int totalWidth, char paddingChar)

Первая форма дополняет строку пробелами так, чтобы ее общая длина


стала равной значению tota!Width. Вторая форма позволяет указать сим­
вол заполнения. В обеих формах возвращается получающаяся в итоге
строка. Если значение параметра tota!Width меньше длины вызывающей
строки, то возвращается копия неизмененной строки.

Вставка, удаление и замена строк


Метод Insert() позволяет вставить строку value в вызывающую строку по
индексу startlndex:

puЫic string Insert(int startindex, string value)


В результате будет возвращена результирующая строка. Для удаления
части строки используется метод Remove():

puЫic string Remove(int startindex)


puЬlic string Remove(int startindex, int count)

Первая форма метода Remove() позволяет удалить часть строки, начина­


ющуюся с индекса startlndex и до конца строки. Вторая форма удаляет
count символов, начиная с позиции startlndex. Для выполнения замены в
строке используется метод Replace(), синтаксис которого приведен ниже:

puЫic string Replace(char oldChar, char �ewChar)


puЫic string Replace(string oldValue, string newValue)
•t-s Справочник С#

Первая форма заменяет все вхождения символа oldChar на символ


newChar. Вторая форма заменяет все вхождения подстроки oldValue на
newValue.

Получение подстроки
Получить подстроку можно методом Substring(), обладающим следую­
щим синтаксисом:

puЫic string Substring(int startindex)


puЬlic string Substring(int startindex, int length)

Первая форма возвращает подстроку, начинающуюся с позиции startlndex


и до конца строки, а вторая - подстроку длиной length символов, начи­
ная с позиции startlndex.

Управляющие последовательности символов


В языке С строковые литералы могут содержать различные управляю­
щие последовательности символов (escape characters). Язык С# - не
исключение. Управляющие последовательности позволяют уточнить то,
как символьные данные будут выводиться в выходном потоке.

Управляющая последовательность начинается с символа обратного сле­


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

Таблица 3.5. Управляющие последовательности

Послецовательность Описание

\' Используется для вставки символа одинарной кавычки

\" Позволяет вставить символ двойной кавычки

\\ Вставляет в строковой литерал символ обратной черты


Глава 3. Основы синтаксиса С#

Заставляет систему воспроизводить звуковой сигнал



(Ьеер)

\п Символ новой строки

\r Возврат каретки

\t Вставляет символ табуляции

Пример:
// Выведем две пустых строки и звуковой сигнал после слова Hello
Console.WriteLiпe("Hello\n\n\a");

Строки и равенство
В языке С# операция равенства предусматривает посимвольную провер­
ку строк с учетом регистра. Другими словами, строки Hello и hello не
равны. Проверку на равенство двух строк можно произвести или с по­
мощью метода Equals(), или с помощью оператора ==:

string s1 = "s1";
string s2 = "s2";
Console.WriteLine("s1 s2: {О)", s1 s2);

В результате будет выведена строка:

s1 == s2: false

Метод Equals() был описан ранее.

Тип System. Text.StringBuilder

Тип string может оказаться неэффективным в ряде случаев. Именно по­


этому в .NET предоставляется еще одно пространство имен - System.
Text. Внутри этого пространства имен находится класс по имени
•со Справочник С#

StringBuilder. В нем содержатся методы, позволяющие заменять и фор­


матировать сегменты. Для использования класса StringBuilder первым
делом нужно подключить пространство имен System.Text:

using System.Text;

При вызове членов StringBuilder производится непосредственное изме­


нение внутренних символьных данных объекта, а не получение копии
этих данных в измененном формате. При создании экземпляра String­
Builder начальные значения для объекта можно задавать с помощью не
одного, а нескольких конструкторов. Рассмотрим пример использования
StringBuilder:

StringBuilder sb = new StringBuilder("Oпepaциoнныe системы:");


sb.Append("\n");
sb.AppendLine("Windows");
sb.AppendLine("Linux");
sb.AppendLine("Mac OS х");
Console.WriteLine(sb.ToString());
Console.WriteLine("B sb {О) символов", sb.Length);
Console.ReadLine();

Сначала мы создаем объект sb и добавляем в него строку «Операцион­


ные системы:». Затем методом Append() мы добавляем символ перевода
строки. Можно было бы достичь того же эффекта, если бы мы создали
объект так:

StringBuilder sb = new StringBuilder("Oпepaциoнныe системы:\n")

После этого мы добавляем строки методом AppendLine(). Данный метод


автоматически добавляет символ \n. Для вывода общей строки мы ис­
пользуем метод ToString(), а свойство Length содержит количество сим­
волов в sb.
Глава 3. Основы синтаксиса С#

Г• C:\W!ndows\Systemll\cmcl.ex� - Hello о х

Рис. 3. 7. Пример использования StringBuilder

3.4.6. Области видимости переменных

Область видшюсти переменной ( ее еще называют контексто.м перемен­


ной) - это фрагмент кода, в пределах которого будет доступна данная
переменная.

Область видимости в С# определяется следующими правилами:

• Поле или переменная - член класса находится в области видимо­


сти до тех пор, пока в этой области находится содержащий это
поле класс.

• Локальная переменная находится в области видимости до тех пор,


пока закрывающая фигурная скобка не укажет конец блока опера­
торов или метода, в котором она объявлена.

• Локальная переменная, объявленная в операторах цикла.fоr, while


или подобных им, видима в пределах тела цикла.

Рассмотрим несколько примеров:


Справочник С#

for (int i = О; i < 5; i++)

Console.Write(" {0}",i);
// Здесь заканчивается область видимости i
int k = 5 * i; // Оператор не будет выполнен

После закрывающей скобки цикла for переменная i больше не будет


«видна)), следовательно, последний оператор не будет выполнен.

Еще один пример:

puЫic static int Main()


{
int m = 10;
for (int i = О; i < 10; i++)

int m = 20; // ошибка


Console.WriteLine(m + i);

return О;

Переменная т будет доступна в пределах всего метода Main(). Вы не


можете повторно объявить переменную т в теле цикла for. Это если
вкратце. Если более развернуто, то переменная т, определенная перед
началом цикла for, будет находиться в области видимости до тех пор,
пока не завершится метод Main(). Вторая переменная т якобы объявлена
в контексте цикла, но он является вложенным в контекст метода Main(),
поэтому компилятор не может различить эти две переменные и не допу­
стит объявления второй переменной с именем т.

В некоторых случаях два идентификатора с одинаковыми именами и


одинаковой областью видимости можно различить, при этом компиля­
тор допустит объявление второй переменной. Причина в том, что язык
С# делает различие между переменными, объявленными на уровне поля,
и переменными, объявленными в методах (локальными переменными).
Глава 3. Основы синтаксиса С#

В качестве примера рассмотрим следующий фрагмент кода:


using System;

namespace ConsoleApplicationl
{
class Program
{
static string uname = "den";

puЫic static void Main()


{
string uname = "john";
Console.WriteLine(uname);
return;

Обратите внимание, что в данном коде есть поле (член класса Program)
и локальная переменная с одинаковыми именами - ипате (объявлена в
методе Main()). Когда вы запустите этот код, вы увидите строку «johш>.
Приоритет у локальных переменных выше, чем у членов класса, поэто­
му вы увидите именно эту строку, а не строку «den». Об этом следует
помнить.

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

const int j = 100;

Особенности констант:

• Константы должны инициализироваться при объявлении, присво­


енные им значения никогда не могут быть и1менены.
Справочник С#

• Значение константы вычисляется во время компиляции. Поэтому


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

• Константы всегда являются неявно статическими. Но при этом не


нужно указывать модификатор static.

3.5. Операторы
3.5.1. Арифметические операторы
Арифметические операторы представлены в таблице 3.6. Операторы+,-,
* и / работают так, как предполагает их обозначение. Их можно приме­
нять к любому встроенному числовому типу данных. Думаю, в особых
комментариях данные операторы не нуждаются.

Таблица 3.6. Арифметические операторы в С#

Оператор Действие
+ Сложение
- Вычитание, унарный минус
* Умножение

/ Деление

% Деление по модулю
-- Декремент
++ Инкремент

Хотя эти операторы всем знакомы, нужно рассмотреть оператор /, а так­


же операторы инкремента и декремента. Когда / применяется к целому
числу, то любой остаток от деления отбрасывается. Остаток от этого де­
ления можно получить с помощью оператора деления по модулю (%),
Глава 3. Основы синтаксиса С# 1-t-J•
который иначе называется оператором вычисления остатка. В С# опе­
ратор % можно применять как к целочисленным типам данных, так и к
типам с плавающей точкой. В этом отношении С# отличается от языков
С и С++, где этот оператор(деление по модулю) разрешается только для
целочисленных типов данных.

Особенности есть и у операторов инкремента и декремента. Оператор


инкремента(++) увеличивает свой операнд на 1, а оператор декремента
(--) уменьшает операнд на 1. Фактически оператор:
х++;
аналогичен следующему:
х = х + 1;
Вот только нужно иметь в виду, что при использовании операторов
инкремента и декремента значение переменной х вычисляется только
один, а не два раза. Это сделано, чтобы повысить эффективность выпол­
нения программы.

Операторы инкремента и декремента можно указывать до операнда (в


префиксной форме) или же после операнда(в постфиксной форме). Опе­
рация инкремента/декремента в префиксной форме происходит раньше,
нежели в постфиксной форме. Пример:

int а = О;
int х = 1;
а = х++; // а = 1, х = 2
а = О; х 1; // исходные значения
а = ++х; // а = 2; х = 2;

Хочется немного отклониться от темы обсуждения и сделать небольшой


перерыв. Не всегда есть возможность получить доступ к среде Visual
Studio. Конкретный тому пример происходит сейчас - при написании
этих строк. Не всегда есть возможность написать главу за один день и за
одним компьютером. Вот и сейчас этот раздел пишется на компьютере, на
котором не установлена Visual Studio. Как делать скриншот результатов
выполнения? Как проверить свой собственный код на отсутствие ошибок
-R/,1 Справочник С#

(у человеческого мозга есть один недостаток - часто он не видит соб­


ственных ошибок)? Специально для этого есть оnlinе-компиляторы. Их
не нужно устанавливать на свой компьютер. Просто откройте браузер,
введите следующий URL, и у вас будет доступ к полноценному компиля­
тору того или иного языка программирования (в данном случае - С#):

https:/lwww.tutorialspoint.com/compile_csharp_online.php

Введите код, который вы хотите проверить, и нажмите кнопку Compile.


Если ее не нажать, то сайт сообщит, что не может открыть ехе-файл -
понятное дело, он еще не откомпилирован. После компиляции нажмите
кнопку Execute, и получите результат выполнения (рис. 3.8).

Рис. 3.8. Использование оп/iпе-компилятора

3.5.2. Операторы сравнения и логические операторы

Данные типы операторов обычно используются в условном операторе


iflelse, поэтому пока только перечислим операторы, а примеры кода вы
найдете далее.

Таблица 3. 7. Операторы сравнения


Глава 3. Основы синтаксиса С#

Оператор Значение
- Равно

!= Не равно
> Больше
< Меньше
>= Больше или равно
<= Меньше или равно

Таблица 3.8. Логические операторы

Оператор Значение

& и
1 или
л Исключающее ИЛИ

&& Укороченное И

11 Укороченное ИЛИ

! НЕ

Результатом операторов (как сравнения, так и логических) является зна­


чение типа bool- или true, илиfаlsе. В языке С# существует так называ­
емые укороченные варианты логических операторов И и ИЛИ. Оба эти
оператора предназначены для получения более эффективного кода.

Рассмотрим небольшой пример работы укороченных операций. Если


первый операнд операции И(&&) имеет ложное значение (false), то ее
результат будет иметь ложное (false) значение, независимо от значения
WИZI Справочник С#

второго операнда. Если же первый операнд логической операции ИЛИ


(11) имеет истинное значение (true), то ее результат будет иметь истинное
значение независимо от значения второго операнда. Поскольку значение
второго операнда вычислять не нужно, экономится время и повышает­
ся эффективность кода. Укороченные операции 11 и && отличаются от
обычных тем, что второй операнд вычисляется только по мере необходи­
мости. Обычно укороченные версии более эффективны. Но зачем тогда
нужны обычные операции? Иногда нужно вычислить значение обоих
операторов из-за неприятных побочных эффектов, которые могут воз­
никнуть.

Пример:
bool t = true;
int i = О;
// При использовании обычного оператора, в данной конструкции
// i будет увеличиваться
if (t 1 (++i < 5))
Console.WriteLine("i равно {О}", i); // i 1

i = О;
// При использовании укороченного оператора
// значение i останется прежним
if (t 1 1 (++i < 5))
Console.WriteLine("i равно (О)", i); // i О

3.5.3. Операторы присваивания


Оператор присваивания обозначается как знак равенства (=) и обычно
имеет форму:
имя переменной выражение
Пример:
х = 5;
Ь true;
Кроме этой простой формы в С# поддерживаются так называемые
составные операторы присваивания (см. табл. 3.9). Как правило, такие
Глава 3. Основы синтаксиса С# R/JMiiil
операторы довольно удобны, и они делают код компактнее, хотя они и
менее понятны начинающим программистам.

Таблица 3.9. Составные операторы присваивания

Оператор Пример кода Полная форма оператора


+= х += 1; х = х + 1;
-- х-= 1; х = х- 1;
*= х *= 1; х = x*l;
!= х /= 1; х = х/1;
%= х %= 1; х = x¾l;
1= х 1= 1; х = х 11;
л= Х л=
1; х = х лl;

3.5.4. Поразрядные операторы


В С# поддерживаются те же поразрядные операторы, что и в С/С++.
Работают данные операторы так же, как и в языках С/С++.

Таблица 3.10. Поразрядные операторы

Оператор Значение
& Поразрядное И
1 Поразрядное ИЛИ
л
Поразрядное исключающее ИЛИ
<< Сдвиг влево
>> Сдвиг вправо
~ Дополнение до 1 (унарный оператор НЕ)
Wt<J Справочник С#

3.6. Преобразование типов данных


В языке С# допускается преобразование типов данных с их автомати­
ческим сужением и расширением. Рассмотрим следующий пример
(листинг 3.2).

Листинг 3.2. Пример расширения типов данных


using System;

namespace Hello
{
class Program

static void Main(string[] args)

short а = 1000, Ь = 2000;


int с = Add(a, Ь);
Console.WriteLine("c = {0}", с);
Console.ReadLine();

static int Add(int а, int Ь)

return а + Ь;

Теперь рассмотрим, что произошло. В метод Add() мы передаем два зна­


чения типа short, хотя метод подразумевает прием параметров типа int.
На самом деле ничего страшного не происходит - тип short поглоща­
ется типом int (диапазон int значительно шире, чем short). Ошибки ком­
пиляции не будет - просто компилятор расширит значения типа short
до значений типа int. Расширение всегда происходит без потери данных,
поэтому вы увидите правильный результат:

с = 3000
Глава 3. Основы синтаксиса С# 1-F:1-
Теперь рассмотрим листинг 3.3, попытка скомпилировать который при­
ведет к ошибке компиляции. Измененные строки выделены жирным.

Листинг 3.3. Ошибка при преобразовании типов


using System;

namespace Hello

class Program

static void Main(string[] args)

short а = 20000, Ь = 20000;


short с = Add(a, Ь);
Console.WriteLine("c = (О}", с);
Console.ReadLine();

static int Add(int а, int Ь)

return а + Ь;

Во-первых, мы увеличили значения переменных а и Ь. Тем не менее, оба


значения все еще вписываются (даже с запасом) в диапазон типа short.
Во-вторых, переменная с теперь типа short, а не int. Разберемся, что про­
исходит. Переменные типа short передаются в качестве параметров ме­
тоду Add(). Компилятор может выполнить расширение до типа int, как
было отмечено ранее, но ему приходится возвращать ответ типа int, ко­
торый нужно поместить в переменную типа short. Выполнить сужение
типа без потери данных в этом случае невозможно. Ведь результат будет
40000, а максимальное значение для short - 32767. Поэтому вы увидите
ошибку компилятора:
Справочник С#

� VI!� (4:щ:.., .il'A Uit � 1.бо:>""' Q,,-.цц 1�� д.!ц� (;:,1Ц<m:<1 V�o,pe,t"" Q«:,ю Crr:»ti,I tJ c,,.t��:,i;.,,(Г, р №11◊ 0 )<
• 1111 � � 't,..щ �/· � .s ��Sh;,1� й Е"'�"Ш

.,t,,.tic "oid !'l;нn(Jtr.!'19{] •r;J'1')


(
',hort ,. "' i:ОН€1, t> * 2&&ее;
���:· :,:.:J}�{��J�� -� (в}�. ");
c»�•,;u\,,.R-,..,dL1r<r{);

�tatic ;i"t /1,:;1,;1(:l.rt 1t, �nt tr}


(

��,
;i;;
'0011. •;Q 01 A,J:>
•• х

кы 86 ФMJ9Мlfd§l§II В1Ы·,ФИМ11'1445%ФМ41

Рис. 3. 9. Ошибка компилятора

Чтобы заставить компилятор выполнить сужение типа с потерей данных,


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

short с = (short)Add(a, Ь)

8 C:\Windows\system32\cmd.exe - Hello о х

Р ис. 3.10. Вместо ожидаемого значения 40000 мы получили совсем дру­


гой результат
Глава 3. Основы синтаксиса С#

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


дет неправильным (рис. 3.10).

Помните, что операцию явного приведения типов нужно использовать с


осторожностью, поскольку она может привести к потере данных. Понят­
ное дело, во многих приложениях, особенно финансовых, такие потери
данных просто недопустимы. К счастью, в С# предлагаются ключевые
слова checked и unchecked, гарантирующие, что потеря данных окажется
незамеченной. Ведь в предыдущем примере мы просто получили непра­
вильный результат (20000 + 20000 = -25536) и никаких предупреждений
компилятора о том, что результат может быть неправильным.

Если оператор или блок операторов заключен в контекст checked, то


компилятор сгенерирует дополнительные СIL-инструкции, обеспечи­
вающие проверку на предмет условий переполнения, которые могут
возникать в результате сложения, умножения, вычитания или деления
двух числовых типов данных. В случае возникновения условия перепол­
нения во время выполнения будет генерироваться исключение System.
OverflowException. Обработать исключение можно, как обычно, с помо­
щью t,y/catch. Если вы не знакомы с обработкой исключений, то позже в
этой книге мы о них поговорим, а сейчас просто рассмотрим следующий
блок кода:

try
{
short с = checked((short)Add(a, Ь));
Console.WriteLine("c = {О)", с);

catch (OverflowException ех)

Console.WriteLine(ex.Message);

В случае если возникнет исключение, мы выведем его сообщение на кон­


соль.
Справочник С#

1 8 Выбрать C:\Windows\System32\cmd.exe • Hello о х :

Рис. 3.11. При выполнении арифметической операции возникло исклю­


чение

Из-за того, что действие флага checked распространяется на всю арифме­


тическую логику, в С# предусмотрено ключевое слово unchecked, кото­
рое позволяет отключить выдачу связанного с переполнением исключе­
ния в отдельных случаях. Применяется это ключевое слово похожим на
checked образом, поскольку может быть указано как для одного операто­
ра, так и для целого блока:

unchecked

short с = (short)Add(a, Ь);


Console.WriteLine("c = {О}", с);

В результате арифметическое переполнение будет явно проигнорирова­


но. По крайней мере, программист будет об этом знать. Использовать
ключевое слово unchecked можно только в тех случаях, где переполнение
является допустимым. Надеюсь, вы знаете, что делаете, раз используете
его.
Глава 3. Основы синтаксиса С# t-&JMiii
3.7. Неявно типизированные локальные
переменные
До этого момента мы везде явно указывали тип переменной - при ее
объявлении. Явное указание типа переменной считается хорошим сти­
лем, но в С# поддерживается неявная типизация. Создать неявно типи­
зированные локальные переменные можно с помощью ключевого слова
var, например:

var А О;
var s "String";

Ключевое слово var можно использовать вместо указания конкретного


типа данных. При этом компилятор автоматически определяет тип пере­
менной по типу первого значения, которое присваивается при инициа­
лизации. В нашем примере для переменной А будет выбран тип int
(System.Int32), а для s - string (System.String). На самом деле слово var
не является ключевым словом языка С#. С его помощью можно объяв­
лять переменные, параметры и поля и не получать никаких ошибок на
этапе компиляции. При использовании этой лексемы в качестве типа
данных она воспринимается компилятором как ключевое слово.

Понятное дело, использование неявно типизированных переменных


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

Также переменным, объявленным с помощью var, сразу должно быть


присвоено значение - при самом объявлении. При этом использовать
пи// в качестве значения не допускается, поскольку компилятор не смо­
жет определить тип переменной по ее значению:

var variaЫel; // Приведет к ошибке!


•10 Справочник С#

// Тоже приведет к ошибке


var variaЬle2;
variaЫe2 = О;

// Правильно
var variaЫeЗ О;

Что касается значения пи//, то их можно присваивать, но уже после того,


как переменной бьmо присвоено первоначальное значение определенно­
го типа:

var sb = new StringBuilder("Oпepaциoнныe системы:\n")


sb = null;

Значение неявно типизированной локальной переменной может быть


присвоено другим переменным, причем как неявно, так и явно типизи­
рованным:

// Ошибок нет
var Х = О;
var У = Х;
string str = "Hello!";
var world = str;

Стоит отметить, что особой пользы от неявно типизированных перемен­


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

3.8. Циклы
В любом языке программирования имеются итерационные конструкции,
использующиеся для повторения блоков кода до тех пор, пока не будет
Глава 3. Основы синтаксиса С# IWZIMiii
выполнено какое-то условие завершения. В языке С# поддерживаются
четыре таких кoнcmpyкцuu:for,foreach, while и do/while.

Если вы ранее программировали на других языках, все эти конструк­


ции (может быть, за исключением /оrеасh, хотя смотря на каком языке
вы программировали до этого) вам должны быть знакомы. Начнем мы с
классики - циклаfor.

3.8.1. Цикл/оr
Идеальное решение, если нужно выполнить какой-то блок кода фиксиро­
ванное количество раз. Операторfor позволяет указать, сколько раз дол­
жен повторяться блок кода, и задать условие завершения цикла. Первый
параметр оператора/оr задает оператор, который будет выполнен до пер­
вой инициализации цикла. Обычно здесь инициализируется переменная­
счетчик. Второй параметр задает условие выхода из цикла, а третий -
оператор, который будет выполнен после каждой итерации. Но, думаю,
вы все это знаете.

Небольшой пример:

for(int i = О; i < 9; i++)


{
Console.Write("{O)", i);

Будет выведена строка 012345678.

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


определять бесконечные циклы и использовать ключевые слова goto,
continue и break. Поскольку я подразумеваю, что вы знакомы с циклом
for, подробно мы его рассматривать не будем.

Скажу только пару слов о переменной-счетчике. Она (переменная i)


доступна только в теле цикла/оr. За пределами цикла (после выполнения
последней итерации) переменная i будет недостуnна.
WC№I Справочник С#

3.8.2. Цикл foreach

Цикл foreach удобно использовать при проходе по всем элементам мас­


сива или коллекции без проверки верхнего предела. Рассмотрим пример
прохода по массиву целых чисел с использованием циклаfоrеасh:

int[] digits = { 1, 2, 3, 4 };
foreach (int i in digits)
Console.WriteLine(i);

Сначала в foreach указывается переменная, которая должна быть такого


же типа, что и элементы массива. В эту переменную будет получен эле­
мент массива для его дальнейшей обработки (в нашем случае обработка
заключается в выводе на консоль). После ключевого слова in задается
название переменной массива. Истинная мощь этого оператора раскры­
вается отнюдь не при работе с массивами, а при работе с интерфейсами,
поэтому рассмотрение этого оператора откладывается до момента ваше­
го знакомства с интерфейсами. Тогда рассмотрениеforeach будет целесо­
образным в случае с С#.

3.8.3. Циклы while и do/while

В некоторой мере цикл/оr очень похож на цикл с предусловием (while),


так как сначала проверяется условие, а потом уже выполняется тело цик­
ла. Рассмотрим цикл с предусловием:

while ( логическоевыражение)
оператор;

Сначала цикл вычисляет значение логического выражения. Если оно ис­


тинно, происходит итерация цикла (выполняется тело цикла), иначе про­
исходит выход из цикла и выполнение следующего за циклом оператора.

Вот пример вывода строки 12345678910:


int i=O;
while(i++ < 10) Console.Write("{O}", i);
Глава 3. Основы синтаксиса С#

Если переменную i увеличивать в теле цикла после вывода предыдущего


значения i, мы получим строку 0123456789:
int i=0;
while(i < 10)
(
Console.Write(i);
i++;

В С# есть еще одна форма цикла - dolwhile. В отличие от цикла while,


здесь сначала выполняются операторы (тело цикла), а затем уже прове­
ряется условие. Если условие истинно, то начинается следующая итера­
ция. Получается, что тело цикла будет выполнено как минимум один раз.
Синтаксис цикла:

do
(
// тело цикла
}
while (условие);

Пример:

int i = 1;
do
Console.Write(i);
while (i++ < 10);

В результате будет выведена та же строка 12345678910.

3.9. Конструкции принятия решений


Как и в любом другом языке, кроме итерационных конструкций есть кон­
струкции принятия решений. В С# есть две таких конструкции - опера­
торы if/else и switch.
•1-rzм Справочник С#

Синтаксис оператора if/else такой же, как в языке С/С++. Но, в отличие
от С и С++, в С# этот оператор может работать только с булевскими вы­
ражениями, но не с произвольными значениями вроде -1 и О. Учитывая
этот факт, в операторе if/else можно использовать следующие операции
сравнения:

• = - возвращает true, если выражения одинаковые, например, if


(page = 5);
• != - возвращает true, если выражения не равны: if (page != 4);

• < (<=) - возвращает true, если выражение слева меньше или рав­
но, чем выражение справа: if (price < 100);

• > (>=) - возвращает true, если выражение слева больше или равно,
чем выражение справа, if (price > 200).

Раньше в С/С++ можно было использовать следующий код:

int k = 100;
if (k)
{
// do something

В С# такой код недопустим, поскольку k - это целое, а не булевое значе­


ние. В операторе ifможно использовать сложные выражения, и он может
содержать операторы else, что позволяет создать более сложные провер­
ки. Синтаксис похож на языки С/С++:

• && - условная операция AND (И), возвращает true, если все вы­
ражения истинны;

• 11 - условная операция OR (ИЛИ), возвращает true, если хотя бы


одно из выражений истинно;

• ! - условная операция NOT (НЕ), возвращает true, если выражение


ложно, иfalse- если истинно.
Глава 3. Основы синтаксиса С# IO!l'P
Пример:
if (page == 1)
{
Console.WriteLine("Пepвaя страница");

else

Console.WriteLine("Cтpaницa: {0}", page);

if (page == 1 && price < 100)


Console.WriteLine("Дeшeвыe продукты на первой странице");

Рассмотрим следующий не очень хороший пример кода:

if (page 1) Console.WriteLine("Пepвaя страница");


if (page 2) Console.WriteLine("Bтopaя страница");
if (page 3) Console.WriteLine("Tpeтья страница");
if (page 4) Console.WriteLine("Чeтвepтaя страница");
if (page 5) Console.WriteLine("Пятaя страница");
else Console.WriteLine("Cтpaницa {О}", page);

Данный код можно переписать с использованием оператора switch:


switch (page)

case 1: Console.WriteLine("Пepвaя страница");


break;
case 2: Console.WriteLine("Bтopaя страница");
break;
case 3: Console.WriteLine("Tpeтья страница");
break;
case 4: Console.WriteLine("Чeтвepтaя страница");
break;
case 5: Console.WriteLine("Пятaя страница");
default: Console.WriteLine("Cтpaницa {0}", page);
break;
•10 Справочник С#

В языке С# каждый блок case, в котором содержатся выполняемые опе­


раторы (default в том числе), должен завершаться оператором break или
goto, во избежание сквозного прохода.

У оператора switch в С# есть одна прекрасная особенность: помимо чис­


ловых данных он также позволяет производить вычисления и со строко­
выми данными. Рассмотрим пример:

string OS = "Linux";
switch {0S)

case "Windows": Console.WriteLine{"Xopoший выбор!");


break;
case "Linux" : Console.WriteLine{"OpenSource!");
break;
default : Console.WriteLine{"Мы не знаем такую систему!");
break;

3.1 О. Массивы
3.10.1. Одномерные массивы
Массив - это набор элементов данных одного типа, доступ к которым
осуществляется по числовому индексу и общему имени. В С# массивы
могут быть как одномерными, так и многомерными. Массивы служат са­
мым разным целям, поскольку они предоставляют удобные средства для
объединения связанных вместе переменных.

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


программирования, но у них есть одна особенность - они реализованы
в виде объектов. Чтобы воспользоваться массивом в программе, необхо­
димо сначала объявить переменную, которая может обращаться к масси­
ву, а после - создать экземпляр массива, используя оператор new.

Объявить массив можно так:

<тип>[] <имя>;
Глава 3. Основы синтаксиса С# 1-z1•
Пример:

int[J digits; // массив целых чисел


string[] strs; // массив строк
bool[] bools; // массив булевых значений

Обратите внимание: следующий код неправильный!

int ints[]; // ошибка компиляции

После объявления массива необходимо установить его размер с по­


мощью ключевого слова new, точно так же, как в Java. Пример:

ints = new int[5];

Установить размер можно и при объявлении массива:

int[] ints = new int[5];

Инициализировать массив можно двумя способами: либо заполнить его


поэлементно, либо использовать фигурные скобки:

ints[OJ 12;
ints[l] 15;
ints[2] 22;
ints[ЗJ 5;
ints[4] 122;

При заполнении массива вручную помните, что нумерация начинается с


О. Но гораздо удобнее использовать фигурные скобки:

int[] ints = new int[] {100,200,300,400,500};

// Синтаксис инициализации массива без использования


Справочник С#

// ключевого слова new


string[] user = {"1001", "den", "1234"};

// Используем ключевое слово new и желаемый размер массива


char[] symbol = new char[4] { 'А', 'В', 'С', 'О' };

Массивы могут быть неявно типизированными (тип элементов массива


-var):

var arrl = new[] { 1, 2, 3 };


Console. WriteLine { "Туре of array - {О}", arrl. GetType() ) ;

Ранее было сказано, что массив - это набор элементов одного типа. Но в
С# есть одна уловка, позволяющая помещать в массив элементы разных
типов. В С# поддерживаются массивы объектов. Вы можете объявить
массив типа object[] и поместить в него элементы самых разных типов.
Вот как это работает:

object[] arrOfObjects { true, 10, "Hello", 1. 7 1;

Формально определение массива соблюдается - все элементы типа


object. Но поскольку все элементарные типы данных в С# представлены
в виде объектов, то ими можно наполнить массив. С каждым массивом
в С# связано свойство Length, содержащее число элементов, из которых
может состоять массив. Работает это так:

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

for (int i = О; i < ints.Length; i++)


Console.WriteLine(ints[i]);

3.10.2. Двумерные массивы


Многомерный массив содержит два или больше измерений, причем
доступ к каждому элементу такого массива осуществляется с помощью
Глава 3. Основы синтаксиса С#

определенной комбинации двух или более индексов. Многомерный мас­


сив индексируется двумя и более целыми числами. Наиболее часто ис­
пользуются двумерные массивы. Это одновременно самый простой и
самый популярный тип многомерного массива. Местоположение любо­
го элемента в двумерном массиве обозначается двумя индексами. Такой
массив м:ожно представить в виде таблицы, на строки которой указывает
один индекс, а на столбцы - другой.

// Объявляем двумерный массив 4х5


int[,] RandomArr = new int[4, 5] ;

// Инициализируем генератор случайных чисел


Random ran = new Random();

// Инициализируем двумерный массив случайными числами


for (int i = О; i < 4; i++)

for (int j = О; j < 5; j++)

Ranchnдrr[i, j] = ran.Next(l, 100); // случайное число от 1 до 100


Console.Write("{0)\t", RandomArr[i, j]);

Console.WriteLine();

Если вам приходилось раньше проrраммировать на С, С++ или Java, то


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

3.10.3. Ступенчатые массивы


Как было отмечено ранее, двумерный массив представляет собой табли­
цу, в которой длина каждой строки неизменна. Но в С# можно создавать
так называемые ступенчатые массивы. Ступенчатый массив - это мас­
сив массивов, в котором длина каждого массива может быть разной.
•со Справочник С#

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


в которых указывается их размерность. Например, для объявления дву­
мерного ступенчатого массива служит следующая общая форма:
тип [][] имя массива = new тип[размер] [];

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

Листинг 3.4. Работа со ступенчатым массивом


int i = О;
// Объявляем ступенчатый массив
// В нем будет три массива длиной, соответственно,
// 3, 5 и 4 элемента
int[][] myArr = new int[3] [];
myArr[OJ new int[3];
myArr (1] new int[5];
myArr[2] new int[4];

// Инициализируем ступенчатый массив


for (; i < 3; i++)

myArr [О] [i] = i;


Console. Write("{О} \t", myArr[О][i]);

Console.WriteLine();
for (i = О; i < 5; i++)

myArr[l][i] = i;
Console. Write ("{О} \t", myArr[1][i]);

Console.WriteLine();
for (i = О; i < 4; i++)

myArr[2][i] = i;
Console.Write("{O}\t", myArr[2][i]);
Глава 3. Основы синтаксиса С#

3.10.4. Класс Array. Сортировка массивов

Для создания массива можно использовать еще и класс Апау. Класс Апау
является абстрактным, поэтому создать массив с использованием какого­
либо конструктора нельзя. Но вместо применения синтаксиса С# для соз­
дания экземпляров массивов также возможно создавать их с помощью
статического метода Createlnstance(). Это исключительно удобно, когда
заранее неизвестен тип элементов массива, поскольку тип можно пере­
дать методу Createlnstance() в параметре как объект Туре:

// Создаем массив типа string, длиной 3


Array strs = Array.Createinstance(typeof(string),3);

// Инициализируем первые два поля массива


strs.SetValue("Brand",0);
strs.SetValue("Model",l);

// Считываем данные из массива


string s = (string)strs.GetValue(l);

Гораздо удобнее использовать предлагаемый языком С# синтаксис, чем


использовать класс Апау, но поскольку мы изучаем С#, мы не могли
его не рассмотреть. Однако существуют ситуации, когда использование
Апау является оправданным. Примеры таких ситуаций - копирование
и сортировка массивов.

Массивы - это ссылочные типы, поэтому присваивание переменной


типа массива другой переменной создает две переменных, ссылающих­
ся на один и тот же массив. Для копирования массивов предусмотрена
реализация массивами интерфейса ICloneaЫe. Собственно, само клони­
рование выполняется методом Clone(), который определен в этом интер­
фейсе. Метод Clone() создает неглубокую копию массива. Если элементы
массива относятся к типу значений, то все они копируются, если массив
содержит элементы ссылочных типов, то сами эти элементы не копиру­
ются, а копируются лишь ссылки на них. В классе Апау также есть метод
копирования - Сору(). Он тоже создает неглубокую копию массива. Но
между Clone() и Сору() есть одно важное отличие: Clone() создает новый
•rв Справочник С#

массив, а Сору() требует наличия существующего массива той же раз­


мерности с достаточным количеством элементов.

Пример вызовов Clone() и Сору():


string[] arrl = (string[])rnyArr.Clone();
Array.Copy(rnyArr, arr2, rnyArr.Length);

В первом примере мы клонируем массив МуАтт в аттl , во втором-мас­


сив mуАп копируется в массив атт2. В класс Аттау встроен алгоритм бы­
строй сортировки (Quicksort) элементов массива. Простые типы вроде
System.String и System.Int32 реализуют интерфейс IComparaЫe, поэто­
му вы можете сортировать массивы, содержащие элементы этих типов.
Если вам нужно отсортировать элементы других типов, то они должны
реализовать интерфейс IComparaЫe.

С помощью разных вариантов метода Sort() можно отсортировать массив


полностью или в заданных пределах либо отсортировать два массива,
содержащих соответствующие пары «ключ-значение». Сортировка мас­
сива необходима, чтобы можно было осуществить эффективный поиск,
используя разные варианты метода BinarySearch().

Пример сортировки массива приведен в листинге 3.5.

Листинг 3.5. Сортировка массива

int[] myArr = { -5, 7, -13, 121, 45, -1, О, 77 };

Console.WriteLine("Дo сортировки: ");


foreach (int i in myArr)
Console.Write("\t{O}",i);

Array.Sort(rnyArr);

Console.WriteLine("\nПocлe сортировки:");
foreach (int i in myArr)
Console. Write("\t {О}",i);
Глава 3. Основы синтаксиса С# t<:JMHI
■ C.'\WindOWS\System32\Cmd.eic-e - �По о х

Рис. 3.12. Сортировка массива

3.10.5. Массив как параметр

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

static Array arrChange(Array Arr)

// Делаем что-то с массивом Arr


return Arr;

3.11. Кортежи
В отличие от массивов (которые содержат объекты одного типа), корте­
жи (tuple) могут содержать объекты самых разных типов. Кортежи часто
используются в языке F#, а с появлением .NET 4 кортежи доступны в
.NET Framework для всех языков .NЕТ.

В .NET 4 определены восемь обобщенных классов Tuple и один статиче­


ский класс Tuple, который служит «фабрикой» кортежей.
•tu Справочник С#

Существуют разные обобщенные классы Tuple для поддержки различно­


го количества элементов, например, Tuple<Tl> содержит один элемент,
Tuple<Tl, Т2> - два элемента и т.д. Элементы кортежа доступны че­
рез свойства ltem 1, ltem2. Если имеется более восьми элементов, кото­
рые нужно включить в кортеж, можно использовать определение класса
Tuple с восемью параметрами. Последний параметр называется TRest,
в котором должен передаваться сам кортеж. Поэтому есть возможность
создавать кортежи с любым количеством параметров.

Следующий метод возвращает кортеж из четырех элементов:

static Tuple<int, float, string, char > tup(int z, string name)


{
int х = 4 * z;
float у = (float)(Math.Sqrt(z));
string s = "Привет, " + name;
char ch = (char) (name[O]);

return Tuple.Create<int, float, string, char>(x, у, s, ch);

Работать с этим методом можно так:

var t = tup(S," Mapк");


Console.WriteLine("{0} {1} {2} {3}", t.ItemЗ, t.Iteml, t.Item2, t. Item4);

Далее мы рассмотрим несколько практических примеров - задачи, ко­


торые могут возникнуть у вас на практике.

3.12. Как подсчитать количество слов в


тексте
Представим, что есть какой-то текст и нам нужно подсчитать количество
символов. Код программы будет очень прост:
Глава 3. Основы синтаксиса С#

Console.WriteLine("Bвeдитe текст:");
string[] tArr;
string text = Console.ReadLiпe();
tArr = text.Split(' ');
Console.WriteLiпe("Koличecтвo слов:");
Console.WriteLiпe(tArr.Leпgth);
Console.ReadLiпe();

Все очень просто. Мы создаем массив строк textMass и простую строко­


вую переменную text. В переменную text считывается введенный поль­
зователем текст, а в массив tArr добавляются элементы из строки text,
расчлененные пробелом при помощи метода Split. Каждый элемент дан­
ного массива - это как раз одно слово, заключенное в тексте между про­
белов. Все, что осталось, - это вывести количество элементов массива.

8 C:\Windows\system32\cmd.exe • Hello о х

Рис. 3.13. Подсчет слов в тексте

Полный код этого приложения приведен в листинге 3.6.

Листинг 3.6. Подсчет количества слов в тексте


using System;
using System.Collections.Generic;
usiпg System.Liпq;
1-t/J Справочник С#

using System.Text;
using System.Threading.Tasks;

namespace Hello

class Program

static void Main(string[] args)

Console.WriteLine("Bвeдитe текст:");
string[] tArr;
string text = Console.ReadLine();
tArr = text.Split(' ');
Console.WriteLine("Koличecтвo слов:");
Console.WriteLine(tArr.Length);

Console.ReadLine();

3.13. Вычисляем значение функции


Теперь задача такая: нам нужно вычислить сумму значений, возвращае­
мых функцией f() от 1 до х. Функция будет такой:

Зх 3 - 2х2

Как мы знаем, в С для возведения в степень используется функция pow().


Конечно, в нашем простом примере мы могли бы использовать вот такой
оператор:

3 * х * х * х - 2 * х * х;
Но, согласитесь, это не совсем правильно. Думаю, читая эти строки, у
вас созревает лишь один вопрос: а зачем приводить столь простой при­
мер? Но этот пример не так прост, как вам кажется. Не стоит забывать,
Глава 3. Основы синтаксиса С# k№:JMW
что вы программируете не на С, а на С#, где все представлено в виде
объектов. Полный код приложения приведен в листинге 3.7, а результат
выполнения программы - на рис. 3.14.
r--···--··
' 8 C.'\Windows\system32\cmd.exe о х

Рис. 3.14. Результат выполнения программы

Листинг 3.7. Вычисляем значение функции


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Hello

class Program

static void Main(string[] args)

Console.WriteLine("Bвeдитe Х:");
string t = Console.ReadLine();

int х = Convert.Toint32(t);
int i;
•t-t/,1 Справочник С#

douЫe sum = О;

for (i = 1; i <= х; i++) sum = sum + f(i);

Console.WriteLine("Peэyльтaт: {О}", sum);

Console.ReadLine();

static douЫe f(douЫe х)

return 3 * Math.Pow(x, 3) - 2 * Math.Pow(x, 2);

Начнем с самого начала. Мы не можем использовать функцию pow(). В


С# она называется Math.Pow(). Обратите внимание на регистр символов.
Затем, просто так создать функцию f() вы не можете. Нужно создать член
класса. Давайте пока создадим лишь статический член класса, чтобы
можно бьшо вызывать функцию f() непосредственно, без указания имени
класса - так наш код будет больше похож на старый С-код:

static douЫe f(douЫe х)

return 3 * Math.Pow(x, 3) - 2 * Math.Pow(x, 2);

Поскольку Math.Pow() возвращает значение типа dоиЫе, то и наша функ­


ция будет возвращать значение типа dоиЫе. Далее пользователь должен
ввести Х, мы должны прочитать ввод и преобразовать его в целое чис­
ло. Для преобразования мы будем использовать метод Tolnt32() класса
Convert:
Console.WriteLine("Bвeдитe Х:");
string t = Console.ReadLine();
Глава 3. Основы синтаксиса С# k«/IM&I
int х = Convert.Toint32(t);
int i;

Ну а дальше - дело техники. В цикле for() пройтись от 1 дох и посчи­


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

3.14. Делаем консольный калькулятор


Сейчас мы напишем простенький калькулятор, который будет работать
в консоли. Программа будет работать так: пользователь вводит операн­
ды и оператор, а программа сообщает результат. Наш калькулятор будет
поддерживать только базовые операторы +, -, *, /. Вместо обработки ис­
ключения деления на О мы произведем проверку ввода - если второй
оператор будет равен О, мы просто не будем производить вычисление.
Готовая программа приведена в листинге 3.8 и изображена на рис. 3.15.

8 C.'\Windows\system32\cmd.exe • Hello о х

Рис. 3.15. Консольный кш�ькулятор


•са Справочник С#

Листинг 3.8. Консольный калькулятор


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Hello

class Program

static void Main(string[] args)

douЫe а;
douЫe Ь;
douЫe res О;
char oper;

Console.Write("Bвeдитe первое число:");


а = Convert.ToDouЫe(Console.ReadLine());

Console.Write("\nBвeдитe оператор:");
oper = Convert.ToChar(Console.ReadLine());

Console.Write("\nBвeдитe второе число:");


Ь = Convert.ToDouЫe(Console.ReadLine());

if (oper == '+')

res = а + Ь;

else if (oper '-')

.res а - Ь;

else if (oper '*')


Глава 3. Основы синтаксиса С# с-ш+м
res = а * Ь;

else if (oper == '/)'

if (Ь ! = 0)
res = а/ Ь;
else Console.WriteLine("Ha О делить нельзя!");

else

Console.WriteLine("Heизвecтный оператор.");

Console.WriteLine("\nPeзyльтaт: {О)", res);

Console.ReadLine();

3.15. Угадай число. Игра


В завершение этой главы мы разработаем простую программу - игру в
угадывание чисел. Компьютер загадает число от О до 9, выдаст подсказ­
ку - больше ли это число 5 или нет, затем сравнит введенное пользова­
телем число с загаданным. «Загадывание» осуществляется с помощью
генератора случайных чисел:

Random rand = new Random()


int i = rand.Next(lO);

В метод Next() нужно передать верхнюю границу диапазона, причем сама


граница в диапазон не входит. То есть если вы вызываете rand.Next( 1 О),
то число будет сгенерировано от О до 9.
•ro Справочник С#

Полный код программы приведен в листинге 3.9.

Листинг 3.9. Игра «Угадай число>>


using System;
using System.Collecti ons.Generic;
using System.Linq;
using System.Text;

namespace Gue s sTheNumЬer

cla s s Pr o gram

static voi d Main( string[] ar g s)

char again = 'у';


Rando m r and = new Random();

while (again == 'у') {

int i = rand.Next(lO);

Console.WriteLiлe("Кcмiыcrrep эагадал чист ar О др 9");

i f (i < 5) Console.WriteLine("Чиcлo меньше 5");


else Console.WriteLine("Чиcлo больше или равно 5");

int х = Convert.Toint32(Console.ReadLine());

if (i = х) O:nsole.WriteLire(''В,r �! �!");
else Console.WriteLine("K сожалению, вы проиграли.
Компьютер загадал число {0)", i);

Console.WriteLine("Попрхювать ЕЩ:!? (у = Да, n = Нет)");


again = Convert.ToChar(Console.ReadLine());
ГЛАВА 4.

ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ
ПРОГРАММИРОВАНИЕ
Wl<1 Справочник С#

4.1. Основы ООП


Объектно-ориентированное программирование (ООП) - это особый
подход к написанию программ. Чтобы понять, что такое ООП и зачем
оно нужно, необходимо вспомнить некоторые факты из истории разви­
тия вычислительной техники. Первые программы вносились в компью­
тер с помощью переключателей на передней панели компьютера - в то
время компьютеры занимали целые комнаты. Такой способ «написания»
программы, сами понимаете, был не очень эффективным - ведь боль­
шую часть времени (несколько часов, иногда - целый рабочий день) за­
нимали подключение кабелей и установка переключателей. А сами рас­
четы занимали считанные минуты. Вы только представьте, что делать,
если один из программистов (такие компьютеры программировались,
как правило, группами программистов) неправильно подключил кабель
или установил переключатель? Да, приходилось все перепроверять - по
сути, все начинать заново.

Позже появились перфокарты. Программа, то есть последовательность


действий, которые должен был выполнен компьютер, наносилась на пер­
фокарту. Пользователь вычислительной машины (так правильно было
называть компьютеры в то время) писал программу, оператор «записы­
вал» программу на перфокарту, которая передавалась оператору вычис­
лительного отдела. Через определенное время оператор возвращал поль­
зователю результат работы программы - рулон бумаги с результатами
вычислений. Мониторов тогда не было, а все, что выводил компьютер,
печаталось на бумаге. Понятно, если в расчетах была допущена ошибка
(со стороны пользователя, компьютеры ведь не ошибаются - они дела­
ют с точностью то, что заложено программой), то вся цепочка действий
(программист, оператор перфокарты, оператор вычислительной маши­
ны, проверка результатов) повторялась заново.
Глава 4. Объектно-ориентированное программирование

Следующий этап в программировании - это появление языка Ассембле­


ра. Этот язык программирования позволял писать довольно длинные для
того времени программы. Но Ассемблер - это язык программирования
низкого уровня, все операции проводятся на уровне «железа)). Если вы
не знаете, то сейчас я вам поясню. Чтобы в С# выполнить простейшее
действие, например, сложение, достаточно записать '$ = 2 + 2; '. На язы­
ке Ассемблера вам для выполнения этого же действия нужно было вы­
полнить как минимум три действия - загрузить в один из регистров
первое число (команда MOV), загрузить в другой регистр второе число
(опять команда MOV), выполнить сложение регистров командой ADD.
Результат сложения будет помещен в третий регистр. Названия реги­
стров я специально не указывал, поскольку они зависят от архитекту­
ры процессора, а это еще один недостаток Ассемблера. Если вам нужно
перенести программу на компьютер с другой архитектурой, вам нужно
переписать программу с учетом особенностей целевой архитектуры.

Требования к программным продуктам и к срокам их разработки росли


(чем быстрее будет написана программа, тем лучше), поэтому появились
языки программирования высокого уровня. Язык высокого уровня
позволяет писать программы, не задумываясь об архитектуре вашего
процессора. Нет, это не означает, что на любом языке высокого уров­
ня можно написать программу, которая в итоге станет работать на про­
цессоре с любой архитектурой. Просто при написании программы знать
архитектуру процессора совсем не обязательно. Вы пишете просто А= В
+С и не задумываетесь, в каком из регистров (или в какой ячейке опера­
тивной памяти) сейчас хранятся значения, присвоенные переменным В и
С. Вы также не задумываетесь, куда будет помещено значение перемен­
ной А. Вы просто знаете, что к нему можно обратиться по имени А. Пер­
вым языком высокого уровня стал FORTRAN (FORmula TRANslator).

Следующий шаг - это появление структурного программирования.


Дело в том, что программы на языке высокого уровня очень быстро ста­
ли расти в размерах, что сделало их нечитабельными из-за отсутствия
какой-нибудь четкой структуры самой программы. Структурное
программирование подразумевает наличие структуры программы и
программных блоков, а также отказ от инструкций безусловного пере­
хода (GOTO, JMP).
IUMID Справочник С#

После выделения структуры программы появилась необходимость в соз­


дании подпрограмм, которые существенно сокращали код программы.
Намного проще один раз написать код вычисления какой-то формулы и
оформить его в виде процедуры (функции) - затем для вычисления 10
результатов по этой формуле нужно будет 1 О раз вызвать процедуру, а не
повторять 1 О раз один и тот же код. Новый класс программирования стал
называться процедурным. Со временем процедурное программирование
постигла та же участь, что и структурное программирование - програм­
мы стали настолько большими, что их было неудобно читать. Нужен был
новый подход к программированию. Таким стало объектно-ориентиро­
ванное программирование (далее ООП).

ООП базируется на трех основных принципах: инкапсуляция, полимор­


физм, наследование. Разберемся, что есть что.

С помощью инкапсуляции вы можете объединить воедино данные и обра­


батывающий их код. Инкапсуляция защищает и код, и данные от вмеша­
тельства извне. Базовым понятием в ООП является класс. Грубо говоря,
класс - это своеобразный тип переменной. Экземпляр класса (пере­
менная типа класс) называется объектом. В свою очередь, объект - это
совокупность данных (свойств) и функций (методов) для их обработки.
Данные и методы обработки называются членами класса. Получается,
что объект - это результат инкапсуляции, поскольку он включает в себя
и данные, и код их обработки. Чуть дальше вы поймете, как это рабо­
тает, пока представьте, что объект - это эдакий рюкзак, собранный по
принципу «все свое ношу с собой)). Члены класса могут быть открытыми
или закрытыми. Открытые члены класса доступны для других частей
программы, которые не являются частью объекта. Закрытые члены
доступны только методам самого объекта.

Теперь поговорим о полиморфизме. Если вы программировали на язы­


ке С (на обычном С, не С++), то наверняка знакомы с функциями abs(),
fabs(), Iabs(). Все они вычисляют абсолютное значение числа, но каждая
из функций используется для своего типа данных. Если бы С поддер­
живал полиморфизм, то можно было бы создать одну функцию abs(), но
объявить ее трижды - для каждого типа данных, а компилятор бы уже
сам выбирал нужный вариант функции, в зависимости от переданного ей
Глава 4. Объек-rн�риеитированное программирование

типа данных. Данная практика называется перезагрузкой функций. Пе­


резагрузка функций существенно облегчает труд программиста - вам
нужно помнить в несколько раз меньше названий функций для написа­
ния программы. Полиморфизм позволяет нам манипулировать с объекта­
ми путем создания стандартного интерфейса для схожих действий.

Осталось поговорить о наследовании. С помощью наследования один


объект может приобретать свойства другого объекта. Заметьте, наследо­
вание - это не копирование объекта. При копировании создается точная
копия объекта, а при наследовании эта копия дополняется уникальными
свойствами (новыми членами). Наследование можно сравнить с рожде­
нием ребенка, когда новый человек наследует «свойства» своих родите­
лей, но в то же время не является точной копией одного из них.

4.2. Классы и объекты


4.2.1. Члены класса
Итак, у вас уже есть представление о том, что такое ООП, к ласс
и объект. Теперь настало время поговорить о классах подробнее. Класс
можно считать шаблоном, по которому определяется форма объекта.
В этом шаблоне указываются данные и код, который будет работать с
этими данными, - поля и методы. В С# используется спецификация
класса для построения объектов, которые являются экземплярами класса
(об этом уже говорилось в первом разделе этой главы). Следовательно,
класс является схематическим описанием способа построения объекта.
При этом нужно помнить, что класс является логической абстракцией.
Физическое представление класса появится в оперативной памяти лишь
после создания объекта этого класса. При определении класса нужно
определить данные и код, который будет работать с этими данными. Есть
и самые простые классы, которые содержат только код (или только дан­
ные). Однако большая часть настоящих классов содержит и то, и другое.

Данные содержатся в членах данных, а код - в функциях-членах (это не


только методы!). В языке С# предусмотрено несколько видов членов дан­
ных и функций членов. Все члены могут быть публичными (риЬ/iс) или
Справочник С#

приватными (private). Данные-члены могут быть статическими (static).


Член класса является членом экземпляра, если только он не объявлен
явно как static. Если вы не знаете, что такое private, риЬ/iс и static, об
этом мы поговорим чуть позже, всему свое время.

Рассмотрим члены данных:

• Поля - любые переменные, связанные с классом.

• Константы - ассоциируются с классом, как и поля. Константа


объявляется с помощью ключевого слова const. Если константа
объявлена публичной (риЫiс), то она становится доступной извне
класса.

• События - члены класса, позволяющие объектам уведомлять


вызывающий код о том, что произошло какое-то событие, напри­
мер, изменилось свойство класса или произошло взаимодействие
с пользователем.

Функции-члены - это члены, предназначенные для манипулирования


данными класса. В С# поддерживаются следующие функции-члены:

• Методы (method) - функции, связанные с определенным клас­


сом. Как и члены данных, они являются членами экземпляра. Они
могут быть объявлены статическими с помощью модификатора
static.
• Свойства (property) - представляют собой наборы функций, ко­
торые могут быть доступны так же, как и общедоступные поля
класса. В С# используется специальный синтаксис для реализации
чтения (Get) и записи (Set) свойств для классов, поэтому писать
собственные методы с именами, начинающимися на Set и Get, не
понадобится.
• Конструкторы (constructor) - функции, вызываемые автомати­
чески при инициализации объекта. Имена конструкторов совпада­
ют с именами классов, которым они принадлежат. Конструкторы
не имеют типа возврата и обычно используются для инициализа­
ции полей.
Глава 4. Обьектно-ориентированное проrраммирование

• Финализаторы (finalizer) - вызываются, когда среда CLR решает,


что объект больше не нужен. В других языках программирования
финализаторы называются деструкторами. Имеют то же имя, что
и класс, но с предшествующим символом тильды.
• Операторы (operator) - используются для перезагрузки
простейших действий вроде + и -. В С# можно указать, как эти
простейшие операции будут работать с пользовательскими класса­
ми. Такое явление называется перегрузка операции и подцержива­
ется не только в С, но и в других языках программирования (С++,
Java, РНР и т.д.).
• Индексаторы (indexer) - используется для индексирования
объектов.

4.2.2. Ключевое слово class


Для объявления класса используется ключевое слово class. Общая форма
этого оператора выглядит так:

class имя_класса {
// Объявление переменных экземпляра.
access type varl;
access type var2;
// . ..
access type varN;

// Объявление методов.
access return type methodl ([args]) {
// тело метода

access return type method2 ([args]) {


// тело метода
}
//. .
access return type methodN([args]) {
// тело метода
11-10 Справочник С#

Сначала объявляются переменные ЭJSЗемпляра var l ...varN. Специфика­


тор access задает тип доступа, например puЬlic. После этого модифика­
тора указывается тип переменной, а потом ее имя. При объявлении ме­
тода указывается спецификатор доступа (access), тип возвращаемого
значения (return_type) и название метода. В скобках - необязательный
список параметров (args).

Спецификатор доступа определяет порядок доступа к члену класса. Член


класса может быть приватным (private) и публичным (риЫiс). Приват­
ные члены доступны только в пределах класса, а публичные доступны из
друrих классов. Указывать спецификатор доступа необязательно. Если
он не указан, то член считается приватным.

Теперь пришло время разработать наш первый класс. Вместо простого


класса, выводящего что-то вроде Hello, world!, сейчас мы разработаем
класс, выводящий информацию о системе (лист. 4.1 ).

Листинr 4.1. Пример класса, выводящеrо информацию о


системе
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleAppl

class Program

class Sysinfo
{
puЬlic string win, net, cpu;
puЬlic string hostname, username;

puЬlic Sysinfo()
{
net = Environment.Version.ToString();
Глава 4. Объекrно-орнентированное программирование

win = Envi ron ment.OSVersion.ToSt ring();


cpu = Environment.Processo rCount.ToSt ring();
hostname = Envi ronment.MachineName.ToSt ring();
username = Envi ronment. UserName.ToString();

static voi d Main ( st ring[] args)

string р ;

Sysinfo info = new Sysinf o ();

if ( args. Lengt h > О) р = args[0];


else р = "null";

switch (р)
case "cpu":
Console.WriteLine("CPU count: {О}", info.cpu);
break;
case "win":
Console.WriteLine("Windows Version: [О}", info .win);
break;
case "net":
Console. WriteLine(".NET Version: {О}", info.net);
break;
case "host":
Console.WriteLine("Hostname : {0}", info.hostname);
break;
case "u ser":
Console.WriteLine("Username : {0}", info.username);
break;
default:
Coo.sole.WriteLire('"Usз::,г: sysinfo <q,..i I win I ret I h::бt I user>");
break;
Справочник С#

Теперь разберемся, что есть что. Сначала в методе Main() определяются


две переменные. Первая - это строковая переменная р, мы ее использу­
ем для облегчения работы с параметрами программы. В принципе, мож­
но было бы обойтись и без нее. Переменная info - это экземпляр класса
Syslnfo().

Посмотрим на сам класс. Класс содержит следующие поля:


puЫic string win, net, cpu;
puЬlic string hostname, username;

Поле win будет содержать версию ОС, net - версию .NET, СРИ - ко­
личество процессоров (ядер процессоров), hostname - имя узла, а
username - имя пользователя. Все поля публичные, то есть к ним можно
будет обращаться за пределами класса, что мы и будем делать в нашей
программе.

Рис. 4.1. Программа в действии

Конструктор класса (его имя совпадает с именем класса), используя


класс Environment, получает доступ к информации о системе. Обратите
внимание, поскольку каждое поле этого класса является объектом, то для
Глава 4. Объектно-ориентированное программирование

преобразования его в строку мы должны использовать метод ToString().


После того, как конструктор заполнит информацией поля, они станут
доступны для использования в нашей программе. Далее мы анализируем
переданные программе параметры. В зависимости от переданного пара­
метра мы выводим или соответствующее поле класса, или информацию
об использовании программы. Программа в действии изображена на рис. 4.1.

4.2.3. Класс System.Object


В языке С# имеется специальный класс System.Object (или просто
object), являющийся базовым классом для всех остальных классов и ти­
пов, в том числе типов значений. Это означает, что все остальные классы
и типы являются производными от object, следовательно, тоже являются
объектами. Все в С# - целые числа, строки, массивы и другие типы -
является объектами. На практике это означает то, что помимо определя­
емых вами методов и полей созданные вами классы поддерживают мно­
жество общедоступных и защищенных методов-членов, определенных в
классе System.Object (табл. 4.1).

Таблица 4.1. Методы класса System.Object()

Метод Описание
Возвращает символьную строку, содержащую описание объек-
та, для которого он вызывается. С этим методом мы уже знако-
мы из предыдущего раздела. ТаlОКе метод ToString() автомати-
ToString() чески вызывается при выводе содержимого объекта с помощью
метода Write()/WriteLine(). В своем классе вы можете переопре-
делить этот метод, чтобы созданные вами объекты могли быть
корректно преобразованы в строку
Используется при помещении объекта в структуру данных -
карту (map), которая также называется хэш-таблицей. Исполь-
зуется классами, которые манипулируют картами. Если вы
желаете использовать свой класс в таком контексте, то должны
GetНashCode() переопределить метод GetНashCode(). Однако это требуется до-
вольно редко, если вам когда-то и понадобится использовать
этот метод, подробности вы сможете узнать в документации по
.NET, поскольку существуют строгие требования перезагрузки
этого метода
Rl<J Справочник С#

Метод Equa\s(object) определяет, ссьшается ли вызывающий


объекr на тот же самый объекr, что и объекr, указываемый в ка-
Equa\s() честве аргумента этого метода. То есть он проверяет, являются
ли оба объекrа одинаковыми. Метод возвращает true, если срав-
пиваемые объекrы одинаковые, в противном случае -fa/se

Является деструктором и вызывается при очистке ресурсов,


занятых объектом. По умолчанию этот метод ничего не дела-
ет. Переопределять этот метод нужно, если ваш объекr владеет
Finalize()
неуправляемыми ресурсами, которые надо освободить при его
уничтожении. Во всех остальных случаях переопределять этот
метод не нужно
Возвращает экземпляр класса, унаследованный от System.Туре.
GetType() Может предоставить большой объем информации о классе, в
том числе базовый тип, методы, поля и т.д.
Clone() Создает копию объекrа и возвращает ссылку на эту копию

4.2.4. Конструкторы

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


конструктор для класса Syslnfo. Основная задача конструктора -
инициализировать объект при его создании. Наш конструктор заполнял
поля информацией, полученной от класса Environment. С точки зрения
синтаксиса определение конструктора подобно определению метода, но
у конструкторов нет возвращаемого типа. Общая форма определения
конструкторов приведена ниже:

access имя клacca(args)


// тело конструктора
1

Спецификатор доступа (access) обычно указывается риЬ/iс, поскольку


конструкторы зачастую вызываются в классе, а вот список параметров
args может быть как пустым, так и состоящим из одного или нескольких
Глава 4. Обьектно-ориеитироваииое программирование

параметров. Имя конструктора, как уже было отмечено, должно совпа­


дать с именем класса.

Каждый класс в С# по умолчанию оснащается конструктором, который


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

В листинге 4.2 приводится пример определения конструктора класса с


параметрами.

Листинг 4.2. Конструктор класса с параметрами


class Human

puЫic string Name;


puЫic byte Age;

// Устанавливаем параметры
puЫic Human(string n, byte а)
{
Name = n;
Age = а;

puЫic void Getlnfo()


{
Console. WriteLine("Narne: {О} \nAge: { 1 ) ", Narne, Age);

В нашем классе есть два поля, конструктор и метод Getlnfo(). Параметры


конструктору передаются при создании объекта:

Human John = new Human("John", 33);


John.Getlnfo();
Wkf'/J Справочник С#

4.2.5. Деструкторы
Обратите внимание, как создается новый объект. Перед указанием имени
вы указываете оператор new, который выделяет оперативную память для
создаваемого объекта. Понятное дело, что оперативная память не рези­
новая, поэтому свободная память рано или поздно закончится. Именно
поэтому одной из главных функций любой схемы динамического рас­
пределения памяти является освобождение памяти от неиспользуемых
объектов, чтобы сделать ее доступной для последующего выделения
(allocation).

В некоторых языках (например, в С++) освобождение выделенной ранее


памяти осуществляется вручную. Например, в С++ для этого использу­
ется оператор delete. Но в С# (и не только в С#, в РНР тоже) используется
процесс, названный сборкой мусора. Благодаря автоматической «сборке
мусора» в С# память освобождается от лишних объектов. Данный про­
цесс происходит незаметно и без всякого вмешательства со стороны
программиста. Если ссьшки на объект отсутствуют, то такой объект
считается ненужным, и занимаемая им память в итоге освобождается.
В итоге освобожденная память может использоваться для других объек­
тов. Процесс «сборки мусора» происходит полностью в автоматическом
режиме, и нельзя заранее знать или предположить, когда он произойдет.

В С# программист может определить метод, который будет вызываться


непосредственно перед окончательным уничтожением объекта. Такой
метод называется деструктором и гарантирует четкое окончание време­
ни жизни объекта. Синтаксис определения деструктора такой же, как и
конструктора, но перед именем класса указывается -:

access ~имя клacca(args)


// тело конструктора
1

Пример деструктора приведен в листинге 4.3.


Глава 4. Объекгно-ориентироваиное программирование

Листинr 4.3. Пример определения деструктора


class Human

puЫic string Name;


puЫic byte Age;

// Устанавливаем параметры
puЫic Human(string n, byte а)
{
Name = n;
Age = а;

puЫic -Human ()
{
Console.WriteLine("Object was destroyed");

puЫic void Getinfo()


{
Console. WriteLine("Name: { О } \nAge: { 1 } ", Name, Age) ;

Обычно деструкторы не нужны, поскольку все используемые объектом


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

4.2.6. Обращаемся сами к себе. Служебное слово this


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

Лучше всего продемонстрировать применение ключевого слова this на


примере, приведенном в листинге 4.4.
Справочник С#

Листинг 4.4. Использование ключевого слова this


class Human

puЫic string Name;


puЫic byte Age;

// Устанавливаем параметры
puЫic Human(string Name, byte Age)
{
// Что и чему присваивается?
// Name = Name;
// Age = Age;
this.Name = Name;
this.Age = Age;
1

Без использования служебного слова this было бы непонятно, что и чему


присваивается - то ли параметру присваивается значение поля, то ли
наоборот:
Name = Name;
Age = Age;

При использовании ключевого слова this все становится на свои места -


полям присваиваются значения параметров:
this.Name = Name;
this.Age = Age;

Использование this позволяет использовать более понятные имена пара­


метров методов. Ранее мы использовали имена параметров п и а, а
теперь в конструкторе мы можем смело указывать имена Name и Age и
не беспокоиться, что компилятор не поймет, что мы имели в виду.
Глава 4. Объектно-ориентированное программирование

4.2.7. Доступ к членам класса


Что дает нам инкапсуляция? Прежде всего, она связывает данные с ко­
дом. Но это далеко не все. Благодаря ей, класс предоставляет средства
для управления доступом к его членам. Именно об управлении доступом
мы сейчас и поговорим.

По существу, есть два типа членов класса - публичные (риЫiс) и


приватные (private). Также их еще называют открытыми и закрытыми.
Доступ к открытому члену возможен из кода, существующего за преде­
лами класса. А вот доступ к приватному члену возможен только методам,
которые определены в самом классе. Приватные члены позволяют орга­
низовать управление доступом.

Кроме известных спецификаторов (модификаторов) доступа риЬ/iс и


private, в С# поддерживаются еще два модификатора - protected и
internal. Как уже было отмечено, доступ к члену с модификатором private
невозможен из других классов. Если модификатор доступа не указан, то
считается, что член класса является приватным. Спецификатор protected
означает член класса, доступ к которому открыт в пределах иерархии
классов. Спецификатор internal используется в основном для сборок и
начинающим программистам не нужен.

Примеры:

puЬlic int а; // Открытый член


private int Ь; // Закрытый член
int с; // Закрытый член по умолчанию

При разработке собственных классов вы должны придерживаться следу­


ющих правил ограничения доступа:

• Члены, которые вы планируете использовать только в классе,


должны быть закрытыми (private).
• Если изменение члена приведет к последствиям, которые распро­
страняются за пределы области действия самого члена, этот член
должен быть закрытым.
нммса Справочник С#

• Методы, которые используются для получения и установки закры­


тых членов (полей) данных, должны быть публичными (риЬ/iс).

• Если доступ к члену допускается из производных классов, то та­


кой член можно объявить как защищенный (protected).

• Если какой-то член может нанести вред объекту, если он будет ис­
пользоваться неправильно, он должен быть закрытым.

• Если нет никаких оснований, чтобы та или иная переменная клас­


са бьmа закрытой, ее можно сделать публичной.

4.2.8. Модификаторы параметров


Как уже бьmо отмечено, метод может вообще не содержать параметров
вообще, а может содержать один или несколько параметров. У каждого
параметра может быть свой модификатор (см. табл. 4.2).

Таблица 4.2. Модификаторы параметров методов


Модификатор
Описание
параметра

Если модификатор параметра не задан, считается, что он дол-


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

Выходные параметры должны присваиваться вызываемым ме-


тодом (и, следовательно, передаваться по ссьшке). Если пара-
out
метрам с модификатором out в вызываемом методе значения не
присвоены, компилятор сообщит об ошибке

Эго значение первоначально присваивается вызывающим ко-


дом и при желании может повторно присваиваться в вызыва-
емом методе (поскольку данные также передаются по ссьшке).
ref
Если параметрам с модификатором ref в вызываемом методе
значения не присвоены, компилятор не будет генерировать со-
общение об ошибке
Глава 4. Объектно-ориентированное проrраммирование

Этот модификатор позволяет передавать в виде одноrо логи­


ческоrо параметра переменное количество аргументов. В каж­
дом методе может присутствовать только один модификатор
params params, и он должен обязательно указываться последним в спи­
ске параметров. В реальности необходимость в использовании
модификатора params возникает очень редко, но он применяет­
ся во многих методах внутри библиотек базовых классов

Наиболее часто используется модификатор ref, позволяющий изменять


значение параметра внутри метода и передавать его по ссьшке в вызыва­
ющий метод. Лучше всего продемонстрировать работу этого параметра
на примере (лист. 4.5).

Листинг 4.5. Использование модификатора ref


using System;
using System .Col lections.Gene r ic;
using System.Linq;
using System.Text;

namespace ConsoleAppl icationl

class Pr og r am

// Метод, изменяющий свой аргумент


static void changeNum(re f int n )

n = 100;

static void Main()

int х = О;
Coosole.WriteLire(''Value of х tefore caJ..l.irg marq:Nt.rn: {0}",х);
changeNum(r ef х ) ;
Coosole.WriteLire(''Value of х after callirg marq:Nt.rn: {0}", х);
Справочник С#

Console.ReadLine();

Вывод будет таким:


Value of х before calling changeNum: О
Value of х after calling changeNum: 100

Если бы параметр был объявлен без модификатора ref, метод не смог бы


изменить значение переменной х. Также обратите внимание, как пере­
менная х передается в сам метод. Модификатор параметра out похож на
ref, за одним исключением: он служит только для передачи значения за
пределы метода. Поэтому переменной, используемой в качестве параме­
тра out, не нужно присваивать какое-то значение. Более того, в методе
параметр out считается неинициализированным, т.е. предполагается, что
у него отсутствует первоначальное значение.

Рассмотрим пример использования модификатора out (лист. 4.6).

Листинг 4.6. Пример использования модификатора out


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplicationl

class Program

static int test(douЫe а, out douЫe Ь, out douЫe с)

int i (int) а;
Ь = а * а;
с = а * а * а;
Глава 4. Объекrно-ориентированное программирование

return i;

static void Main()

int i;
douЫe с, Ь, а = 4.5;

i = test(a, out Ь, out с);

Console.WriteLine("Original value: {0)\n


Int {l)\n
а " 2: {2)\n
а " 3: {3)\",a,i,b,c);

Console.ReadLine();

Вывод программы будет таким:

Original value: 4.5


Int 5
а " 2 30,25
а " 3 166,375

Использование модификатора out позволяет методу возвращать сразу


три числа - два через параметры с модификатором out и одно - через
return.

Модификатор params позволяет передавать методу переменное ко­


личество аргументов одного типа в виде единственного логического
параметра. Рассмотрим пример использования модификатора params
на примере передачи массива в качестве параметра метода. Наш метод
minElement будет искать минимальный элемент в массиве, который пере­
дан с модификатором params.
Справочник С#

Листинг 4.7. Использование модификатора params


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplicationl

class Program

static douЫe minElement(params douЬle[] arr)

douЫe min;
// Array is empty?
if (arr.Length == О)

Console.WriteLine("Empty array!");
return DouЫe.Negativeinfinity;

else
// Only 1 element in array
if (arr.Length == 1)

min = arr[0J;
return min;

min = arr[0J;
// Searcing for min
for (int j = 1; j < arr.Length; j++)
if (arr[j] < min)
min = arr[j];
return min;

static void Main()


Глава 4. Объектно-ориентированное программирование

douЬle result О;
douЫe [] arrl { 4.5, 7.6, 3.3, -11.7};

result = minElement(arrl);
Console.WriteLine("Min: {0}", result);

Console.ReadLine();

Результатом будет число -11. 7, как несложно было догадаться.

4.2.9. Необязательные параметры


Начиная с версии 4.0 языка С#, появились необязательные аргументы,
позволяющие определить значение по умолчанию для параметра метода.
Данное значение будет использоваться по умолчанию в том случае, если
для параметра не указан соответствующий аргумент при вызове метода.
Такие аргументы довольно давно существуют в других языках програм­
мирования, но в С# появились только в версии 4.0.

Основная цель, которую преследовали разработчики С#, - необходи­


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

Определяются данные параметры так:

puЫic int Prod3(int а, int Ь = 1, int с = 1)


{
return а * Ь * с;

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


но указываться справа от обязательных. Кроме обычных методов, необя­
зательные параметры можно применять в конструкторах, индексаторах
и делегатах.
11+1-rr:.1 Справочник С#

4.2.10. Именованные аргументы


Также в версии .NET 4.0 появилась поддержка так называемых имено­
ванных аргументов (named arguments). Обычно аргументы передаются в
том порядке, который должен совпадать с порядком указания аргументов
при объявлении метода. Вспомним наш конструктор класса Human:

puЫic Human(string Name, byte Age)


{
this.Name = Name;
this.Age = Age;

Вызывать его можно было только так:

Human Mark = new Human("Mark", 38);

Вы не можете вызвать его так:

Human Mark = new Human(38, "Mark");

Именованные аргументы позволяют указывать параметры в произволь­


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

Пример:

Human Mark = new Human(Age: 38, Name: "Mark");

При этом вносить изменения в описание самого метода не нужно. Просто


укажите компилятору, какому параметру какое значение вы передаете.
Глава 4. Объектно-ориентированное проrраммирование

4.2.11. Ключевое слово static


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

Самый часто используемый пример статического члена - метод Main(),


который объявляется таковым потому, что он должен вызываться опера­
ционной системой в самом начале выполняемой программы. Чтобы вос­
пользоваться членом типа static за пределами класса, достаточно указать
имя этого класса с оператором-точкой. Но создавать объект для этого не
нужно. Рассмотрим наш класс Hurnan, в который мы добавим статиче­
ский метод Hello():

Листинг 4.8. Статический метод


class Human

puЫic string Name;


puЫic byte Age;

// Устанавливаем параметры
puЬlic Human(string n, byte а)
{
Name = n;
Age = а;

puЬlic ~Human()
{
Console.WriteLine("Object was destroyed");
}
IWZI Справочник С#

puЬlic void Getinfo()


{
Console.WriteLine("Name: {0}\nAge : {1}", Name, Age);

static void Hello()

Console.WriteLine("Hel lo, wor ld!");

Теперь посмотрим, как использовать данный метод:

Human.Hello();

Заметьте, вызвать таким образом метод Getlnfo() не получится, посколь­


ку этот метод не является статическим и нужно сначала создать объект:

Human Mark = new Human("Mark", 32);


Mark.Getinfo();

Логика в следующем: для работы Getlnfo() нужно, чтобы бьmи инициа­


лизированы поля класса. Следовательно, его нельзя объявлять как стати­
ческий. А вот метод Hello() независим от остального класса - он всегда
будет выводить одну и ту же строку, поэтому его можно объявить стати­
ческим.

Конструктор можно также объявить как static. Статический конструктор


обычно используется для инициализации компонентов, применяемых ко
всему классу, а не к отдельному экземпляру объекта этого класса. Поэто­
му члены класса инициализируются статическим конструктором до соз­
дания каких-либо объектов этого класса. Причем в одном классе может
быть как статический, так и обычный конструктор. Пример объявления
двух конструкторов приведен в листинге 4.9.
Глава 4. Объекrно-орнентнрованное проrраммнрованне

Листинг 4.9. Статический и публичный конструкторы


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplicationl

class SampleClass

puЫic static int х;


puЫic int у;

// Статический конструктор
static SampeClass()
{
х = 1;

// Обычный конструктор
puЬlic SampleClass()
{
у = 12;

class Program

static void Main(string[] args)

Console.WriteLine("Access to х: " + SampleClass.x);

SampleClass obj = new SampleClass();


Console.WriteLine("Access to у: " + obj.y);
са Справочник С#

Более того, статическим можно объявить не только конструктор класса,


но и весь класс. Статические классы обладают двумя основными свой­
ствами:

1. Объекты статического класса создать нельзя.

2. Статический класс должен содержать только статические члены.

Объявляется статический класс так:

static class имя класса { // ...

4.2.12. Индексаторы
Любой программист хорошо знаком с процессом доступа к отдельным
элементам массива с помощью квадратных скобок[]. В языке С# имеется
возможность создавать специальные классы и структуры, которые мож­
но индексировать подобно обычному массиву посредством определения
индексатора. Другими словами, в С# можно обращаться к классу как к
массиву. Это позволяет создавать специальные типы коллекций.

Индексаторы бывают одномерными и многомерными. Последние ис­


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

тип_элемента this[int индекс] {


// Аксессор для получения данных,
get
// Возвращает значение, которое определяет индекс.
}
// Аксессор для установки данных,
set
// Устанавливает значение, которое определяет индекс.
} }

Рассмотрим, что есть что:


Глава 4. Объектно-ориентированное программирование

• Тип элемента - конкретный тип элемента индексатора. У каж­


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

В теле индексатора определяются два аксессора (от англ. accessor): get


и set. Первый используется для получения значения, второй - для его
установки. Аксессор похож на метод, но в нем не указываются тип
возвращаемого значения или параметры.

Аксессоры вызываются автоматически при использовании индексатора,


и оба получают индекс в качестве параметра. Аксессор set таюке получа­
ет неявный параметр value, который содержит значение, присваиваемое
по указанному индексу. Все это звучит очень сложно, пока не виден код.
Листинг 4.1 О демонстрирует, как изящно можно превратить класс в мас­
сив и работать с ним как с обычным массивом.

Листинг 4.10. Пример использования индексатора


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplicationl

class MyArr

int[] arr;
puЬlic int Length;

puЬlic MyArr(int Size)


{
•10 Справочник С#

arr = new int[Size];


Length = Size;

// Простой индексер
puЬlic int this[int index]
{
set // Устанавливаем элемент массива

arr[index] = value;

get
{
return arr[index];

class Program

static void Main()

MyArr arrl = new MyArr(lO);

// Инициализируем каждый индекс экземпляра класса arrl


for (int i = О; i < arrl.Length; i++)

arrl[i] = i * 2;
Console.Write("{O} ", arrl[i]);

В цикле for работа с переменной arr1 осуществляется как с массивом.


Однако arr1 - это объект типа МуАп, а не массив.
Глава 4. Объектно-ориентированное проrраммирование

4.2.13. Свойства
В С# есть еще одна диковинка - свойства. В других языках програм­
мирования, поддерживающих ООП, свойством называется член класса,
содержащий данные. Просто есть свойства и методы. Свойства - это
переменные, а методы - это функции. Грубо говоря, конечно. Но в С#
все немного иначе. Свойство сочетает в себе данные и методы доступа
к ним.

Свойства очень похожи на индексаторы. В частности, свойство состо­


ит из имени и аксессоров get и set. Аксессоры служат для получения и
установки значения переменной. Разница в том, что имя свойства может
быть использовано в выражениях и операторах присваивания аналогич­
но имени обычной переменной, но в действительности при обращении к
свойству автоматически вызываются методы get и set.

Синтаксис описания свойства выглядит так:

тип имя
get
{
// код аксессора для чтения из поля

set

// код аксессора для записи в поле

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

4.3. Перегрузка функций членов класса


4.3.1. Перегрузка методов
В С# допускается перегрузка методов. С помощью перегрузки методов
можно или переопределить ранее определенный метод (например, метод
ToString(), который неявно есть у каждого класса), чтобы он соответство­
вал вашему классу, или же определить несколько методов, которые будут
определяться по-разному. Например, вы можете создать методы с оди­
наковым названием, но разными типами возврата, а компилятор будет
выбирать нужный метод в зависимости от производимых вычислений.
Для перегрузки метода достаточно объявить разные его варианты, а об
остальном позаботится компилятор. Но при этом необходимо соблюсти
следующее важное условие: тип или число параметров у каждого метода
должны быть разными.

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


значений. Также они должны отличаться типами и/или числом своих
параметров. Если есть два метода с одинаковым списком параметров
(совпадает количество и ти'пы), но с разным типом возвращаемого значе­
ния, компилятор просто не сможет выбрать, какой из них использовать.
Хотя о типе возвращаемого значения будет сказано чуть позже.

Пример перегруженного метода Info() приведен в листинге 4.11.

Листинг 4.11. Пример перегруженного метода


clas s Car

// Перегружаем метод Info()


puЫic void Info()
{
Console.WriteLin e ("No bra n d s elec ted\n");

puЬlic void Info(string Bra n d)


{
Console.WriteLine("Brand: {0}\nNomodel selected", Brand};
Глава 4. Объекrно-орнентнрованное программирование

puЬlic void Info(string Brand, string Model)


{
Console. WriteLine("Brand: {О} Model: [ 1) ", Brand, Model) ;

В данном примере мы сначала объявляем метод Info(), а затем перегру­


жаем его два раза. Когда компилятор принимает решение о перегрузке
метода, то также учитываются модификаторы параметров ref и out.
Перегрузка методов поддерживает полиморфизм. В С# соблюдается
главный принцип полиморфизма: один интерфейс - множество мето­
дов. В языках, где не поддерживается перегрузка методов, каждому ме­
тоду должно быть присвоено уникальное имя. Типичный пример - язык
С, в котором есть функции abs(), fabs(), labs(). Все они вычисляют абсо­
лютное значение числа, но каждая из функций используется для своего
типа данных.

В С# есть понятие сигнатуры. Сигнатура обозначает имя и список пара­


метров. Поэтому в классе не должно быть двух методов не с одинаковы­
ми именами, а с одинаковыми сигнатурами. В сигнатуру не входит тип
возвращаемого значения, поскольку он не учитывается, когда компиля­
тор С# принимает решение о перегрузке метода. В сигнатуру не входит
также модификатор params.

4.3.2. Перегрузка методов


Язык С# позволяет перезагружать конструкторы. Это позволяет созда­
вать объекты самыми разными способами. Пример перегрузки конструк­
тора класса Car приведен в листинге 4.12.

Листинг 4.12. Перегрузка конструкторов


class Car

// Перегружаем конструктор
puЬlic void Car()
•1в Справочник С#

Console.WriteLine("No brand s electe d\n");

puЫic void Car(string Brand)


{
Console.WriteLine("Brand: {0}\nNo model selected", Brand);

puЬlic void Car(string Brand, string Model)


{
Console.WriteLine("Brand: {О} Mode l: {1}", Brand, Model);

Самая распространенная причина перегрузки конструкторов - необ­


ходимость инициализации объекта разными способами. При работе с
перезагружаемыми конструкторами есть возможность вызвать другой
конструктор. Делается это с помощью ключевого слова this, пример:
puЬlic Info() : this ( "None ", "None ")
{

puЬlic Info(Car obj)


: this(obj.Brand, obj.Model)

puЬlic Info(string Brand, string Model)


{
this.Brand Brand;
this.Model Model;

4.3.3. Перегрузка операторов


В С# перегружать можно не только методы и конструкторы, но и опера­
торы. Например, вы можете перегрузить оператор + и определить, как
будет выполняться операция сложения нескольких ваших объектов.
Глава 4. Объектно-ориентированное программирование

По умолчанию перегруженными являются все операторы, поскольку они


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

int а = 1, Ь = 2;
int с = а + Ь;
string n "user";
string s = "hello" + " " + user;

Сначала, как видите, оператор + используется для сложения двух целых


чисел, затем - для конкатенации трех строк. Не все операторы могут
быть перегружены. Таблица 4.3 содержит сведения о том, какие операто­
ры могут быть перегружены, а какие - нет.

Таблица 4.3. Возможность переzрузки операторов в С#

Оператор(ы) Возможность перегрузки

+, -, !, ++, --, true, fa\se Могуr быть перегружены

+, -, "', !, %, &, 1, л, <<, >> Могуr быть перегружены

=, !=, <, >, <=, >= Могуr быть перегружены, но нужно пере-
грузить все эти операторы
Не может быть перегружен. Но подобный
[]
функционал предлагают индексаторы

Не может быть перегружен. Подобный


о функционал обеспечивают специальные ме-
тоды преобразования
+=, -=, *=, /=, %=, &=, 1=, л=, <<=,
Не перезагружаются
>>=

Синтаксис перегрузки оператора отлич11ется для унарных и бинарных


операторов:
1-rll Справочник С#

// Для унарных операторов


puЬlic static возвращаемый_тип operator ор(тип_параметра операнд)
{
// тело

// Для би нарных операторов


puЬlic static возвращаемый_тип operator ор(тип_параметраl операндl,
тип параметра2 операнд2)
{
// тело

Разберемся, что есть что. Возвращаемый тип обозначает конкретный тип


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

Рассмотрим пример перегрузки операторов + и - для произвольного


класса.

Листинг 4.13. Перегрузка операторов + и -


usiпg System;
usiпg System.Collectioпs.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplicationl

class SampleClass

// Переменные
puЫic int х, у;

puЬlic SampleClass(int х = О, int у О)


{
this.x х;
Глава 4. Обьектио-ориеитироваииое программирование

this.y у;

//Перегрузка +
puЫic static SaпpleClass cpera tor +(SaпpleClass ol, SaпpleClass о2)
{
SampleClass res = new SampleClass();
res.x = ol.x + о2.х;
res.y = ol.y + о2.у;
return res;

//Перегрузка
puЫic static SaпpleClass cpera tor -(SaпpleClass ol, SaпpleClass о2)
{
SampleClass res = new SampleClass();
res.x = ol.x - о2.х;
res.y = ol.y - о2.у;
return res;

class Program

static void Main(string[] args)

SampleClass objl = new SampleClass(lOO, 64);


SampleClass obj2 = new SampleClass(-74, 28);
Console.WriteLine("First object: " +
objl.x + " " + objl.y);
Console.WriteLine("Second object: " +
obj2.x + " " + obj2.y);

SampleClass оЬjЗ objl + obj2;


Console.WriteLine("objl + obj2: "
+ оЬjЗ.х + " " + оЬjЗ.у);

оЬjЗ = objl - obj2;


ltMk<J Справочник С#

Console.WriteLine("objl - obj2: "


+ оЬjЗ.х + " " + оЬjЗ.у);

4.4. Наследование и полиморфизм


4.4.1. Введение в наследование
Наследование - один из трех основных принципов объектно-ориенти­
рованного программирования. Наследование допускает создание иерар­
хических классификаций. Благодаря ему можно создать общий класс, в
котором определены характерные особенности, которые свойственны
множеству связанных элементов. От этого класса могут создаваться про­
изводные классы (могут наследовать базовый), добавляя в общий класс
свои особенности.

Класс, который наследуется, называется базовым, а класс, который


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

class Имя производного класса Имя базового класса

Рассмотрим пример наследования классов (листинг 4.14). Мы опреде­


лим два класса- Parent (базовый) и Child (производный от базового). В
классе Parent объявлены два полях и у. В классе Child объявлено только
полеz.

Листинг 4.14. Наследование классов


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Глава 4. Обьектно-ориентированное программирование

using System.Threading.Tasks;

namespace ParentChild

class Parent

puЬlic int х, у;

puЬlic Parent ()
{
х = 10;
у 20;

class Chil d : Parent

puЫic int z;
puЫic Child()

30;
= 50;

class Program

static void Main(string[] args)

Parent р = new Parent();


Child с = new Child();

Console.WriteLine("Parent х, у: {О} {1}", р.х, р.у);


Console.WriteLire("Qri.ld х, у, z: {О} {1} {2}", с.х, с.у, c.z);

Console.ReadLine();
liJiMl-ril Справочник С#

Рис. 4.2. Вывод программы

Класс Child наследует все члены базового класса, следовательно, он


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

4.4.2. Защищенный доступ

В листинге 4.13 все члены классов были публичными (объявлены с моди­


фикатором доступа риЫiс) - так было нужно для простоты изложения.
Но, как мы знаем, члены могут быть приватными (private). Приватный
член базового класса недоступен для всего внешнего кода, в том числе
и для производного класса. Но иногда возникают ситуации, когда нужно
предоставить доступ к определенным членам только производным клас­
сам, чтобы все остальные классы (которые не являются производными)
использовать эти члены не могли. Такие члены называются защищенны­
ми (protected). Защищенный член является открытым в пределах иерар­
хии классов, но закрытым за пределами этой иерархии.
Глава 4. Обьектно-ориентированное программирование

Для объявления защищенного члена используется модификатор доступа


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

Пример:
class Parent

puЫic int х; // доступен для наследования и для внеuniего кода


protected int у; // дост. для наследования и производных Юiассов
private int z; //не доступен для наследования и произв. IOiacca
int а; //то же, что и private

puЫic Parent ()

х = 10;
у = 20;
z = 100;
а = х;

4.4.3. Запечатанные классы. Ключевое слово sealed


В С# можно создавать классы, недоступные для наследования. Такие
классы называются запечатанными (sealed). Для объявления запечатан­
ного класса используется ключевое слово sealed:

sealed class Parent

Если в листинге 4.14 объявить класс Parent как запечатанный, то компи­


лятор сообщит об ошибке (рис. 4.3):

'Child': cannot derive from sealed type 'Parent'


'Child': не может быть произ водным от запечатанного типа "Parent"
1-rr:1 Справочник С#

� • �rc�"�"""' ••х
_ __J_e_. _ ·---_,., с, ,11 \!> • z • с, ..,.EJ
.. !.:.t,,,-�.,.,....••,.._�11:-r,,,..._.:c:ti<•� p"j
!::l"!.--·�ncuiacr,�--, ..,,)

06,,�---pйt<>ll',wli�·и·м.,;· ,;;;.w --
с� ,.., ... ,х

,ас.�... . i!i.....�TfAOnp.q.,i,e--.--j 8Ом,l(� 1"J ,cбopu"1n1�1�


1. ri,-.,. ,,,., '"*'•)' ;JW"'6\'( .Р·(
·� ·к; ·,�-��;···
"(/\lld": ..e...c»o<.,.бon..
О С!�'� nроо1,ао.м,1,1мm 1'11..,,,ю,;,а
iст,и :(OC"lt>�;owrю,a•....... н;
,-,wщ,1•...oro1"1'1•-�f".
0(..Юl(» И,..•')'"'«11�•
.
с,.,..;о,сО<.:1,<боо< ew-.u

Рис. 4.3. Попытка наследования от запечатанного класса

4.4.4. Наследование конструкторов


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

Конструктор базового класса создает базовую часть объекта. В случае с


примером из листинга 4.14 конструктор класса Parent() задает начальные
значения полей х и у базового класса Parent. Конструктор производного
класса создает производную часть объекта. В нашем случае - задает
значение поля z и переопределяет значение у, определенное в базовом
классе. Логика следующая: базовому классу неизвестны (да и недоступ­
ны) элементы производного класса, следовательно, их создание должно
происходить раздельно.

Если же конструктор определен только в производном классе, то кон­


струируется производный класс, а базовая часть объекта создается кон­
структором по умолчанию.
Глава 4. Объекrно-ориентироваиное программирование

4.4.5. Сокрытие имен. Ключевое слово base


В производном классе могут быть определены члены с таким же име­
нем, как в производном классе. В этом случае член базового класса будет
скрыт в производном классе. Формально это не считается ошибкой, но
все же компилятор выдаст предупреждение (рис. 4.4). Рассмотрим при­
мер кода, в котором в классе Child определяется поле у - с таким же
именем, как в базовом классе.

Листинг 4.15. Сокрытие имен


class Parent

puЫic int х, у;

puЬlic Parent ()
{
х = 10;
у 20;

class Child : Parent

puЬlic int z;
puЫic int у;

puЬlic Child ()
{
у 30;
z = 50;

Как показано на рис. 4.4, компилятор сообщает о том, что Child.y скры­
вает унаследованный член Parent.y. Если член базового класса требуется
скрыть намеренно, то перед его именем следует указать ключевое слово
IMMIЮI Справочник С#

new, чтобы избежать появления подобного предупреждающего сообще­


ния (рис. 4.5):
puЫic new int у;

�•!Ш.2,_�__:_:.:,����- -=-=-=_-_..:_-_-J=:.::::::::::�::j
11 • 18;
,,cc.;,,�.j""'',c,c.,,......, �
._,,. �· 0,-,1C'S
,,,.,>� --:-~-
у. ::lt;
• -)-IIUO('f;!
� t:• ProQl'.:tm;;;,

.,�, •;�1r:· rr..1::����;'

-(-�
�- -
"Ctiid:.(aq:>t,IIИ'I
...:.�.а-��:,·.
•ACS&H)IJit:.:-apwr,w6t.oм) �
...

j
Рис. 4.4. Предупре:нсдение компилятора

и
'111! l'tpoel(t с6щжа Of'-UD Т«т Ан:w/,
�; · 1 ое�•< :,.ч��-

r
: : у �

i .. "
1·1
х:,18;
у 11 :18;

lf
,.
" cl.11•s Cl>i.J.f!: Vы-,,;r.t

m.
_f>'lb\!<:Ц!_;r.;
2,:,·• у;
:���� n•w
"" p<1btJc: t:h.11;,i()
{

Рис. 4.5. Никаких предупре:нсдений компилятора


Глава 4. Объектно-ориентированное программирование

Если таки нужно будет обратиться к полю у из базового класса, следует


использовать ключевое слово base:
base.y = 5;
4.4.6. Виртуальные члены
В данном разделе мы поговорим о виртуальных членах класса. С помо­
щью полиморфизма и переопределения методов подкласс может опре­
делить собственную версию метода, определенного в базовом классе. О
переопределении членов мы уже говорили, но нерассмотренными оста­
лись ключевые слова virtual и override.
В С# существует специальный тип методов - виртуальные. Метод,
определенный как virtual, реализуется в базовом классе. Виртуальный
метод может быть переопределен в одном или нескольких производных
классах. Следовательно, у каждого производного класса может быть
свой вариант виртуального метода. Виртуальные методы представляют
интерес тем, что происходит при их вызове по ссьшке на базовый класс.
Среда .NET определяет именно тот вариант виртуального метода, кото­
рый нужно вызвать, в зависимости от типа объекта, к которому проис­
ходит обращение по ссылке. Обратите внимание: именно среда .NET, а
не компилятор С#, поэтому выбор виртуального метода происходит при
выполнении, а не при компиляции.
Если базовый класс содержит виртуальный метод, и он наследуется про­
изводными классами, при обращении к разным типам объектов по ссьш­
ке на базовый класс выполняются разные варианты этого виртуального
метода. Объявить виртуальный метод можно с помощью ключевого сло­
ва virtual, которое указывается перед его именем. Для переопределения
виртуального метода в производном классе используется модификатор
override. Виртуальный метод не может быть объявлен как static или
abstract.

Процесс повторного определения виртуального метода в производном


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

Пример виртуального метода в базовом классе:

puЫic virtual string MyString(string s)


{
return "MyString " + s;

Пример переопределения метода в производном классе:

puЫic override string MyString(string s)


{
return "Overrided string " + s;

4.4.7. Абстрактные классы


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

При создании собственных библиотек классов вы можете убедиться, что


у метода часто отсутствует содержательное определение в контексте его
базового класса. Данную ситуацию можно решить двумя способами.
Первый из них заключается в выводе предупреждающего сообщения.
При отладке данный способ пригодится, но в production-вepcии кода он
недопустим. Нужен способ, гарантирующий, что в производном классе
действительно будут переопределены все необходимые методы. Такой
способ заключается в использовании абстрактного метода.

Абстрактный метод создается с помощью модификатора abstract. У


абстрактного метода отсутствует тело, и он не реализуется в базовом
классе. Данный метод должен быть реализован (переопределен) в произ­
водном классе, поскольку его вариант из базового класса просто непри­
годен для использования. Абстрактный метод автоматически становит-
Глава 4. Обьек-rно-ориентированное программирование

ся виртуальным, поэтому он не требует указания модификатора virtual.


Более того, совместное использование модификаторов virtual и abstract
приведет к ошибке.

Синтаксис определения абстрактного метода следующий:

abstract тип имя(список_параметров);

Пример определения абстрактного класса:

class Parent

puЫic int х, у;

puЬlic Parent ()

= 10;
20;

puЫic abstract int sum();


}

В производном классе абстрактный метод переопределяется так:

puЫic override string sum()


{
return х + у + z;

4.5. Отложенная инициализация. Тип Lazy


Приложение может использовать множество классов и объектов. Одна­
ко в больших приложениях есть вероятность, что не все создаваемые
объекты будут использованы. Например:
•кв Справочник С#

class Reader

Library library = new Library();


puЫic void ReadBook()
{
library.GetBook();
Console.WriteLine("Читaeм бумажную книгу");

puЫic void ReadEbook()


{
Console.WriteLine("Читaeм электронную книгу");

class Library

private string[] books new string[99];

puЬlic void GetBook()


{
Console.WriteLine("Bыдaeм книгу читателю");

Имеется класс Library, представляющий библиотеку и хранящий неко­


торый набор книг в виде массива. Есть класс читателя Reader, который
хранит ссылку на объект библиотеки, в которой он записан. У читате­
ля определено два метода: для чтения электронной книги и для чтения
обычной книги. Для чтения обычной книги необходимо обратиться к ме­
тоду класса Library, чтобы получить эту книгу.

Но что если читателю вообще не придется читать обычные книги, а толь­


ко электронные, например, в следующем случае:

Reader reader = new Reader();


reader.ReadEbook();
Глава 4. Объектно-ориентированное программирование

В этом случае объект library в классе читателя никак не будет исполь­


зоваться и будет только занимать место памяти. Хотя надобности в нем
не будет.

Для подобных случаев в .NET определен специальный класс Lazy<Г>.


Изменим класс читателя следующим образом:

class Reader

Lazy<Library> library = new Lazy<Library>();


puЬlic void ReadBook()
{
library.Value.GetBook();
Console.WriteLine("Читaeм обычную книгу");

puЬlic void ReadEbook()


{
Console.WriteLine("Читaeм электронную");

Класс Library остается прежним. Но теперь класс читателя содержит


ссылку на библиотеку в виде объекта Lazy<Library>. А чтобы обратиться
к самой библиотеке и ее методам, надо использовать выражение library.
Value - это и есть объект Library.

Что меняет в поведении класса Reader эта замена? Рассмотрим его при­
менение:

Reader reader = new Reader();


reader.ReadEbook();
reader.ReadBook();

Непосредственно объект Library задействуется здесь только на третьей


строке в методе reader.ReadВook(), который вызывает в свою очередь
Справочник С#

метод library.Value.GetBook(). Поэтому впл оть до третьей стро­


ки объект Library, используемый читателем, не будет создан. Если мы
не будем применять в программе метод reader.ReadBook(), то объект
библиотеки тогда вообще не будет создан, и мы избежим лишних затрат
памяти. Таким образом, Lazy<T> гарантирует нам, что объект будет
создан только тогда, когда в нем есть необходимость.
ГЛАВА 5.

РАСШИРЕННЫЙ
СИНТАКСИС
Справочник С#

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

5.1.1. Определение делегатов


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

delegate void Message();

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

Рассмотрим следующий код и разберемся, что к чему:

void Hello() => Console.WriteLine("Hello");


delegate void Message();

Message mes;
mes = Hello;
mes();
Глава 5. Расширенный синтаксис IOMU
Прежде всего, сначала необходимо определить сам делегат:

delegate void Message();

Для использования делегата создаем переменную этого делегата:

Message mes;

Далее в делегат передается адрес определенного метода (в нашем случае


метода Hello). Обратите внимание, что данный метод имеет тот же воз­
вращаемый тип и тот же набор параметров (в данном случае отсутствие
параметров), что и делегат.

mes = Hello;

Затем через делегат вызываем метод, на который ссылается данный де­


легат:

mes ();

Вызов делегата производится подобно вызову метода. При этом делега­


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

Message messagel Greeting.Print;


Message message2 new Hello() .Display;

messagel(); // Greeting
message2(); // Привет

delegate void Message();

class Greeting
Справочник С#

puЫic static void Print{) => Console.WriteLine("Greeting");

class Hello

puЫic void Display() => Console.WriteLine("Hello");

5.1.2. Место определения делегата

Если делегат определен в программе верхнего уровня, которую по умол­


чанию представляе т файл Program.cs начиная с версии С# 10, как в при­
мере ранее, то, как и другие типы, делегат определяется в конце кода. Но
в принципе делегат можно определять вну три класса:

class Program

delegate void Message();


static void Main()

Message mes;
mes = Hello;
mes();

void Hello() => Console.WriteLine("Hello");

Либо вне класса:

delegate void Message();


class Program

static void Main()

Message mes;
mes = Hello;
mes();
Глава 5. Расширенный синтаксис kit/.lMN
void Hel l o() => Console.WriteLine("Hel l o");

5.1.3. Параметры и результат делеrата


Рассмотрим определение и применение делегата, который принимает па­
раметры и возвращает результат:

Operation operation = Add; // Делегат указывает на метод Add


int result = operation(З, 8); // Фактически Add(З, 8)
Console.WriteLine(result); // 11

q:eration = t-W.tiply; // Теперь Ц=JЕГат у�<аЭ,JВаеТ на � t-W.tiply


result = operation(З, 8); // Фактически Multiply(З, 8)
Console.WriteLine(result); // 24

int Add(int х, int у) => х + у;

int Мultiply(int х, int у) => х * у;

delegate int Operation(int х, int у);

В данном случае делегат Operation возвращает значение типа int и име­


ет два параметра типа int. Поэтому этому делегату соответствует любой
метод, который возвращает значение типа int и принимает два параметра
типа int. В данном случае это методы Add и Multiply. То есть мы можем
присвоить переменной делегата любой из этих методов и вызывать.
Поскольку делегат принимает два параметра типа int, то при его вызове
необходимо передать значения для этих параметров: operation(З,8).

5.1.4. Присвоение ссылки на метод


Выше переменной делегата напрямую присваивался метод. Есть еще
один способ - создание объекта делегата с помощью конструктора, в
который передается нужный метод:
11'/.J Справочник С#

Operation operationl Add;


Operation operation2 new Operation(Add);

int Add(int х, int у) => х + у;

delegate int Operation(int х, int у);

Оба способа равноценны.

5.1.5. Соответствие методов делегату


Методы соответствуют делегату, если они имеют один и тот же возвра­
щаемый тип, и один и тот же набор параметров. Но надо учитывать, что
во внимание также принимаются модификаторы ref, in и out. Например,
пусть у нас есть делегат:

delegate void SomeDel(int а, douЫe Ь);

Этому делегату соответствует, например, следующий метод:

void SomeMethodl(int g, douЫe n) { )

А следующие методы НЕ соответствуют:

douЫe SomeMethod2(int g, douЫe n) return g + n;


void SomeMethodЗ(douЫe n, int g)
void SomeMethod4(ref int g, douЫe n)
void SomeMethodS(out int g, douЫe n) g 6; )

Здесь метод SomeMethod2 имеет другой возвращаемый тип, отличный


от типа делегата. SomeMethodЗ имеет другой набор параметров. Пара­
метры SomeMethod4 и SomeMethodS также отличаются от параметров
делегата, поскольку имеют модификаторы ref и out.
Глава 5. Расширенный синтаксис kfl ■iil
5.1.6. Добавление методов в делегат
В примерах выше переменная делегата указывала на один метод. В ре­
альности же делегат может указывать на множество методов, которые
имеют ту же сигнатуру и возвращаемые типы. Все методы в делегате
попадают в специальный список - список вызова, или invocation list.
И при вызове делегата все методы из этого списка последовательно вы­
зываются. И мы можем добавлять в этот список не один, а несколько
методов. Для добавления методов в делегат применяется операция +=:

Message message = Hello;


message += HowAreYou; // Теперь message указывает на два метода
message(); // Вызываются оба метода - Hello и HowAreYou

void Hello() => Coпsole.WriteLine("Hello");


void HowAreYou() => Console.WriteLine("How are уоu?Э);

delegate void Message();

В данном случае в список вызова делегата message добавляются два ме­


тода - Hello и HowAreYou. И при вызове message вызываются сразу оба
этих метода. Однако стоит отметить, что в реальности будет происходить
создание нового объекта делегата, который получит методы старой ко­
пии делегата и новый метод, и новый созданный объект делегата будет
присвоен переменной message.

При добавлении делегатов следует учитывать, что мы можем добавить


ссылку на один и тот же метод несколько раз, и в списке вызова делегата
тогда будет несколько ссылок на один и то же метод. Соответственно
при вызове делегата добавленный метод будет вызываться столько раз,
сколько он был добавлен:
Message message = Hello;
message += HowAreYou;
message += Hello;
message += Hello;

message();
Справочник С#

Консольный вывод будет таким:

Hello
How are you?
Hello
Hel lo

Подобным образом мы можем удалять методы из делегата с помощью


операций -=:

Message? message = Hello;


message += H owAreYou;
message(); // Вызываются все методы из message
message -= HowAreYou; // Удаляем метод HowAreYou
if (message != null) message(); // Бызывается метод Hello

При удалении методов из делегата фактически будет создаваться новый


делегат, который в списке вызова методов будет содержать на один ме­
тод меньше. Стоит отметить, что при удалении метода может сложить­
ся ситуация, что в делегате не будет методов, и тогда переменная будет
иметь значение null. Поэтому в данном случае переменная определена не
просто как переменная типа Message, а именно Message?, то есть типа,
который может представлять как делегат Message, так и значение null.

Кроме того, перед вторым вызовом мы проверяем переменную на зна­


чение null. При удалении следует учитывать, что если делегат содержит
несколько ссылок на один и тот же метод, то операция -= начинает поиск
с конца списка вызова делегата и удаляет только первое найденное вхож­
дение. Если подобного метода в списке вызова делегата нет, то операция
-= не имеет никакого эффекта.

5.1.7. Объединение делегатов


Делегаты можно объединять в другие делегаты. Например:

Message mesl Hell o;


Глава 5. Расширенный синтаксис са•
Мessage mes2 = HowAreYou;
Мessage mesЗ = mesl + mes2; // Объединяем делега'IЪ!
mesЗ(); // Вы:эываюrся все методы из mesl и mes2

void Hello() => Console.WriteLine("Hello");


void HowAreYou() => Console. WriteLine("How are you?");

delegate void Message();

В данном случае объект mesЗ представляет объединение делегатов mes 1


и mes2. Объединение делегатов значит, что в список вызова делегата
mesЗ попадут все методы из делегатов mes 1 и mes2. И при вызове деле­
гата mesЗ все эти методы одновременно будут вызваны.

5.1.8. Вызов делегата


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

Message mes = Hello;


mes ();
Operation ор = Add;
int n = ор(З, 4);
Console.WriteLine(n);

void Hello() = > Console.WriteLine("Hello");


int Add(int х, int у) => х + у;

delegate int Operation(int х, int у);


delegate void Message();

Другой способ вызова делегата представляет метод Invoke():

Message mes = Hello;


mes.Invoke(); // Hello
Operation ор = Add;
WIU Справочник С#

int n = op.Invoke(З, 4);


Console.WriteLine(n); // 7

void Hello() => Console.WriteLine("Hello");


int Add(int х, int у) => х + у;

delegate int Operation(int х, int у);


delegate void Message();

Если делегат принимает параметры, то в метод Invoke передаются значе­


ния для этих параметров. Следует учитывать, что если делегат пуст, то
есть в его списке вызова нет ссылок ни на один из методов (то есть деле­
гат равен Null), то при вызове такого делегата мы получим исключение,
как, например, в следующем случае:

Message? mes;
//mes(); // Ошибка: делегат равен null

Operation? ор = Add;
ор -= Add; // Делегат ор пуст
int n = ор(З, 4); // !Ошибка: делегат равен null

Поэтому при вызове делегата всегда лучше проверять, не равен ли он


Либо можно использовать метод Invoke и оператор условного пи//:
пи//.

Message? mes = null;


mes?.Invoke(); // Ошибки нет, делегат просто не вызывается

Operation? ор = Add;
ор -= Add; // Делегат ор пуст
int? n = op?.Invoke(З, 4); // Qnибки нет, депегат просто не ЕЬl'ЫВается, а n
= null

Если делегат возвращает некоторое значение, то возвращается значение


последнего метода из списка вызова (если в списке вызова несколько ме­
тодов). Например:
Глава 5. Расширенный синтаксис kif/JMФI
Operation ор = Subtract;
ор += Multiply;
ор += Add;
Console.WriteLine(op(7, 2)); // Add(7,2) 9

int Add(int х, int у) => х + у;


int Subtract(int х, int у) => х - у;
int Multiply(int х, int у) => х * у;

delegate int Operation(int х, int у);

5.1.9. Обобщенные делеrаты

Делегаты, как и другие типы, могут быть обобщенными, например:

Operation<decimal, int> squareOperation Square;


decimal resultl = squareOperation(5);
Console.WriteLine(resultl); // 25

Operation<int, int> douЬleOperation DouЬle;


int result2 = douЫeOperation(5);
Console.WriteLine(result2); // 10

decimal Square(int n) => n * n;


int DouЬle(int n) => n + n;

delegate Т Operation<T, К>(К val);

Здесь делегат Operation типизируется двумя параметрами типов. Пара­


метр Т представляет тип возвращаемого значения. А параметр К пред­
ставляет тип передаваемого в делегат параметра. Таким образом, этому
делегату соответствует метод, который принимает параметр любого типа
и возвращает значение любого типа. В программе мы можем определить
переменные делегата под определенный метод. Например, делегату
Operation<decimal, int> соответствует метод, который принимает число
int и возвращает число типа decimal. А делегату Operation<int, int> соот­
ветствует метод, который принимает и возвращает число типа int.
IU Справочник С#

5.1.10. Делегаты как параметры методов


Также делегаты могут быть параметрами методов. Благодаря этому один
метод в качестве параметров может получать действия - другие методы.
Например:

DoOperation(5, 4, Add); // 9
DoOperation(5, 4, Subtract); // 1
DoOperation(5, 4, Multiply); // 20

void DoOperation(int а, int Ь, Operation ор)

Console.WriteLine(op(a,Ь));

int Add(int х, int у) => х + у;


int Subtract(int х, int у) => х - у;
int Multiply(int х, int у) => х * у;

delegate int Operation(int х, int у);

Здесь метод DoOperation в качестве параметров принимает два числа и


некоторое действие в виде делегата Operation. Внутри метода вызываем
делегат Operation, передавая ему числа из первых двух параметров. При
вызове метода DoOperation мы можем передать в него в качестве третье­
го параметра метод, который соответствует делегату Operation.

5.1.11. Возвращение делегатов из метода


Делегаты можно возвращать из методов. То есть мы можем возвращать
из метода какое-то действие в виде другого метода. Например:

Operation operation = SelectOperation(OperationType.Add);


Console.WriteLine(operation(l0, 4)); // 14

operation = SelectOperation(OperationType.Subtract);
Console.WriteLine(operation(l0, 4)); // 6
Глава 5. Расширенный синтаксис k+rllMPII
operation = SelectOperation(OperationType.Multiply);
Console.WriteLine(operation(l0, 4)); // 40

Operation SelectOperation(OperationType орТуре)

switch (орТуре)
{
case OperationType.Add: return Add;
case OperationType.Subtract: return Subtract;
default: return Multiply;

int Add(int х, int у) => х + у;


int Subtract(int х, int у) => х - у;
int Multiply(int х, int у) => х * у;

enum OperationType

' Add, SuЬtract, Multiply

delegate int Operation(int х, int у);

В данном случае метод SelectOperation() в качестве параметра прини­


мает перечисление типа OperationType. Это перечисление хранит три
константы, каждая из которых соответствует определенной арифметиче­
ской операции. И в самом методе в зависимости от значения параметра
возвращаем определенный метод. Причем поскольку возвращаемый тип
метода - делегат Operation, то метод должен возвратить метод, кото­
рый соответствует этому делегату, - в нашем случае это методы Add,
Subtract, Multiply. То есть если параметр метода SelectOperation равен
OperationType.Add, то возвращается метод Add, который выполняет сло­
жение двух чисел:

case OperationType.Add: return Add;


При вызове метода SelectOpeгation мы можем получить из него нужное
действие в переменную operation:
•со Справочник С#

Operation operation Sele ctOperation(OperationType.Add);

И при вызове переменной operation фактически будет вызываться полу­


ченный из SelectOperation метод:

Operation operation = Sele ctOperation(OperationType.Add);


Console.W r i teLine(operation(200, 300)); // 500

5.1.12. Делеrаты Action, Predicate и Func


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

Делегат Action представляет некоторое действие, которое ничего не


возвращает, то есть в качестве возвращаемого типа имеет тип void:

puЫic delegate voi d Action()


puЫic delegate void Action<in Т>(Т obj)

Данный делегат имеет ряд перегруженных версий. Каждая версия при­


нимает разное число параметров: от Action<in Tl> до Action<in Tl,
in T2,....in Т lб>. Таким образом можно передать до 16 значений в метод.

Как правило, этот делегат передается в качестве параметра метода и


предусматривает вызов определенных действий в ответ на произошед­
шие действия. Например:

DoOperation(lO, 6, Add); // 10 + 6 16
DoOperation(lO, 6, Multiply ); // 10 * 6 60

void DoOperation(int а, int Ь, Action<int, int> ор ) => ор(а, Ь);

void Add(int х, int у) => Console.WriteLine($"{x) +{у)= {х + у)");


void Мultiply(int х, int у) => Console.WriteLine($"{x) * {у)= {х * у)");
Глава 5. Расширенный синтаксис 10•
Делегат Predicate<Т> принимает один параметр и возвращает значение
типа bool:

dele gate bool Predicate<in Т>(Т obj);

Как правило, используется для сравнения, сопоставления некоторого


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

Predi c ate<int> isPositive = (int х) => х > О;

Console.WriteLine(isPositive(20));
Console.WriteLine(isPositive(-20));

В данном случае возвращается true илиfаlsе в зависимости от того, боль­


ше нуля число или нет.

Еще одним распространенным делегатом является Func. Он возвращает


результат действия и может принимать параметры. Он также имеет раз­
личные формы: от Func<out Т>(), где Т - тип возвращаемого значения,
до Func<in Т1, in T2,...in Т16, out TResult>(), то есть может принимать до
16 параметров.

ТResult Func<out ТResult>()


ТResult Func<in Т, out ТResult>(T arg)
ТResult Func<in Tl, in Т2, out ТResult>(Tl argl, Т2 arg2)
'I'Result Func<in Tl, in Т2, in ТЗ, out 'I'Result>(Tl argl, Т2 arg2, ТЗ argЗ)
'I'Result Func<in Tl, in Т2, in ТЗ, in Т4, out 'I'Result>(Tl argl, Т2 arg2, ТЗ
argЗ, Т4 arg4)
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Данный делегат также часто используется в качестве параметра в методах:

int res u l t l = DoOperation(б, DouЬleNumЬer); // 12


kiZI Справочник С#

Console.WriteLine(resultl);

int result2 = DoOperation(б, SquareNumber); // 36


Console.WriteLine(result2);

int DoOperation(int n, Func<int, int> operation) => operation(n);


int DouЬleNumЬer(int n) => 2 * n;
int SquareNumber(int n) => n * n;

Метод DoOperation() в качестве параметра принимает делегат


Func<int, int>, то есть ссылку на метод, который принимает число int
и возвращает также значение int. При первом вызове метода DoOpera­
tion() ему передается ссылка на метод DouЫeNumber, который увеличи­
вает число в два раза. Во втором случае передается метод SquareNumber
- опять же метод, который принимает параметр типа int и возвращает
результат в виде значения int.

Еще один пример:


Func<int, int, string> createString = (а, Ь) => $"{а){Ь)";
Console.WriteLine(createString(l, 5)); // 15
Console.WriteLine(createString(7, 7)); // 77

Здесь переменная createStгing представляет лямбда-выражение, которое


принимает два числа int и возвращает строку, то есть представляет деле­
гат Func<int, int, stгing>.

5.2. Анонимные методы


С делегатами тесно связаны анонимные методы. Анонимные методы ис­
пользуются для создания экземпляров делегатов. Определение аноним­
ных методов начинается с ключевого слова delegate, после которого идет
в скобках список параметров и тело метода в фигурных скобках:

dеlеgаtе(параметры)
{
// Инструкции
Глава 5. Расширенный синтаксис IUMN
Например:

MessageHandler handler = delegate (string mes)


{
Console.WriteLine(mes);
} ;
handler("hello world!");

delegate void MessageHandler(string message);

Анонимный метод не может существовать сам по себе, он используется


для инициализации экземпляра делегата, как в данном случае перемен­
ная handler представляет анонимный метод. И через эту переменную де­
легата можно вызвать данный анонимный метод.

Другой пример анонимных методов - передача в качестве аргумента


для параметра, который представляет делегат:

ShowMessage("hello!", delegate (string mes)

Console.WriteLine(mes);
} );

static void ShowMessage(string message, MessageHandler handler)

handler(message);

delegate void MessageHandler(string message);

Если анонимный метод использует параметры, то они должны соответ­


ствовать параметрам делегата. Если для анонимного метода не требуется
параметров, то скобки с параметрами опускаются. При этом даже если
делегат принимает несколько параметров, то в анонимном методе можно
вовсе опустить параметры:
IFl<J Справочник С#

MessageHandler handler = delegate


{
Console.WriteLine("aнoнимный метод");
) ;
handler("hello world!Э); // Анонимный метод

delegate void MessageHandler(string rnessage);

То есть если анонимный метод содержит параметры, они обязательно


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

Так же, как и обычные методы, анонимные могут возвращать результат:

Operation operation delegate (int х, int у)

return х + у;
);
int result operation(4, 5);
Console.WriteLine(result); // 9

delegate int Operation(int х, int у);

При этом анонимный метод имеет доступ ко всем переменным, опреде­


ленным во внешнем коде:
int rn = 8;
Operation operation delegate (int х, int у)

return х + у + rn;
);
int result operation(4, 5);
Console.WriteLine(result); // 17

delegate int Operation(int х, int у);


Глава 5. Расширенный синтаксис CWlJMPPI
Анонимные методы используются в ситуациях, когда нам надо опреде­
лить однократное действие, которое не имеет много инструкций и нигде
больше не используется. В частности, их можно использовать для обра­
ботки событий, которые будут рассмотрены далее.

5.3. Лямбды
5.3.1. Введение в лямбда-выражения
Лямбда-выражения представляют упрощенную запись анонимных ме­
тодов. Лямбда-выражения позволяют создать емкие лаконичные методы,
которые могут возвращать некоторое значение и которые можно пере­
дать в качестве параметров в другие методы.

Лямбда-выражения имеют следующий синтаксис: слева от лямбда-опе­


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

(список_параметров) => выражение

С точки зрения типа данных лямбда-выражение представляет делегат.


Например, определим простейшее лямбда-выражение:

Message hello = () => Console.WriteLine("Hello");


hello(); // Hello
hello(); // Hello
hello(); // Hello

delegate void Message();

В данном случае переменная hello представляет делегат Message - то


есть некоторое действие, которое ничего не возвращает и не принимает
никаких параметров. В качестве значения этой переменной присваивает­
ся лямбда-выражение. Это лямбда-выражение должно соответствовать
делегату Message - оно тоже не принимает никаких параметров, поэто-
10 Справочник С#

му слева от лямбда-оператора идут пустые скобки. А справа от лямбда­


оператора идет выполняемое выражение - Console. WriteLine(«Hello» ).
Затем в программе можно вызывать эту переменную как метод.

Если лямбда-выражение содержит несколько действий, то они помеща­


ются в фигурные скобки:

Message hello = () =>


{
Console.Write("Hello ");
Console.WriteLine("World");
};
hello(); / / Hello World

Выше мы определили переменную hello, которая представляет делегат


Message. Но начиная с версии С# 1 О мы можем применять неявную ти­
пизацию (определение переменной с помощью оператора var) при опре­
делении лямбда-выражения:

var hello = () => Console.WriteLine("Hello");


hello(); / / Hello
hello(); / / Hello
hello(); / / Hello

Но какой тип в данном случае представляет переменная hello? При


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

5.3.2. Параметры лямбды


При определении списка параметров мы можем не указывать для них
тип данных:
Глава 5. Расширенный синтаксис k!fll-
Operation sum= (х, у) => Console.WriteLine($"{x} + {у}= {х + у}");
sum ( 1, 2) ; // 1 + 2 = 3
sum (22, 14 ); // 22 + 14 = 36

delegate vo i d Operati on(int х, int у );

В данном случае компилятор видит, что лямбда-выражение sum пред­


ставляет тип Operation, а, значит, оба параметра лямбды представляют
тип int. Поэтому никаких проблем не возникнет. Однако если мы при­
меняем неявную типизацию, то у компилятора могут возникнуть труд­
ности, чтобы вывести тип делегата для лямбда-выражения, например, в
следующем случае:

var sum = (х, у) => Console.WriteLine($"{xl + {у} {х + у}"); // СА!JИбка

В этом случае можно указать тип параметров:

var sum = (int х, int у) => Console.WriteLine($"{x} + {у} {х + у}");


sum(l, 2); // 1 + 2 = 3
sum(22 , 14 ); // 22 + 14 = 36

Если лямбда имеет один параметр, для которого не требуется указывать


тип данных, то скобки можно опустить:

PrintHandler print message => Console.WriteLine(message);


print(" Hell o "); // Hell o
print("Greeting"); // Greeting

delegate vo id PrintHandler(string message);

5.3.З. Возвращение результата


Лямбда-выражение может возвращать результат. Возвращаемый резуль­
тат можно указать после лямбда-оператора:
•1-r/.J Справочник С#

var sum = (int х, int у) => х + у;


int sumResult = sum(4, 5); // 9
Console.WriteLine(sumResult); // 9

Operation multiply = (х, у) => х * у;


int multiplyResult = multiply(4, 5); // 20
Console.WriteLine(multiplyResult); // 20

delegate int Operation(int х, int у);

Если лямбда-выражение содержит несколько выражений, тогда нужно


использовать оператор return, как в обычных методах:

var subtract = (int х, int у) =>

if (х > у) return х - у;
else return у - х;
};
int resultl = subtract(l0, 6); // 4
Console.WriteLine(resultl); // 4

int result2 = subtract(-10, 6); // 16


Console.WriteLine(result2); // 16

5.3.4. Лямбда-выражение как аргумент метода


Как и делегаты, лямбда-выражения можно передавать параметрам мето­
да, которые представляют делегат:

int[] integers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// Найдем сумму чисел больше 5


int resultl = Sum(integers, х => х > 5);
Console.WriteLine(resultl); // 30

// Найдем сумму четных чисел


int result2 = Sum(integers, х => х % 2 О);
Глава 5. Расширенный синтаксис J!f:J
Console.WriteLine(result2); //20

int Sum(int[] numЬers, IsEqual func)

int result = О;
foreach (int i in numЬers)

if (func(i))
result += i;

return result;

delegate bool IsEqual(int х);

Метод Sum принимает в качестве параметра массив чисел и делегат


IsEqual и возвращает сумму чисел массива в виде объекта int. В цикле
проходим по всем числам и складываем их. Причем складываем только
те числа, для которых делегат IsEqual func возвращает true. То есть де­
легат IsEqual здесь фактически задает условие, которому должны соот­
ветствовать значения массива. Но на момент написания метода Sum нам
неизвестно, что это за условие.

При вызове метода Sum ему передается массив и лямбда-выражение:

int resultl = Sum(integers, х => х > 5);

То есть параметр х здесь будет представлять число, которое передается


в делегат:

if (func(i))

А выражение х > 5 представляет условие, которому должно соответство­


вать число. Если число соответствует этому условию, то лямбда­
выражение возвращает true, а переданное число складывается с другими
числами.
Справочник С#

Подобным образом работает второй вызов метода Sum, только здесь уже
идет проверка числа на четность, то есть если остаток от деления на 2
равен нулю:

int result2 Sum(integers, х => х % 2 о) ;

5.3.5. Лямбда-выражение как результат метода


Метод также может возвращать лямбда-выражение. В этом случае воз­
вращаемым типом метода выступает делегат, которому соответствует
возвращаемое лямбда-выражение. Например:

Operation oper.tion = SelectOperation(OperationType.Add);


Console.WriteLine(operation(l0, 4)); // 14

operation = SelectOperation(OperationType.Subtract);
Console.WriteLine(operation(l0, 4)); // 6

operation = SelectOperation(OperationType.Multiply);
Console.WriteLine(operation(l0, 4)); // 40

Operation SelectOperation(OperationType орТуре)


{
switch (орТуре)
{
case OperationType.Add: return (х, у) => х + у;
case OperationType.Subtract: return (х, у) => х - у;
default: return (х, у) => х * у;

enum OperationType

Add, Subtract, Multiply

delegate int Operation(int х, int у);


Глава 5. Расширенный синтаксис

В данном случае метод SelectOperation() в качестве параметра принимает


перечисление типа OperationType. Это перечисление хранит три константы,
каждая из которых соответствует определенной арифметической опера­
ции. И в самом методе в зависимости от значения параметра возвращаем
определенное лямбда-выражение.

5.4. События
5.4.1. Введение в события
События сигнализируют системе о том, что произошло определенное
действие. И если нам надо отследить эти действия, то мы как раз и мо­
жем применять события. Например, рассмотрим следующий класс, кото­
рый описывает банковский счет:
class Account

// Сумма на счете
puЫic int Sum { get; private set; }
// В конструкторе устанавливаем начальную сумму на счете
puЬlic Account(int sum) => Sum = sum;
// Добавление средств на счет
puЫic void Put(int sum) => Sum += sum;
// Списание средств со счета
puЫic void Take(int sum)
{
if (Sum >= sum)

Sum sum;

В конструкторе устанавливаем начальную сумму, которая хранится в


свойстве Sum. С помощью метода Put мы можем добавить средства на
счет, а с помощью метода Take, наоборот, снять деньги со счета. Попро­
буем использовать класс в программе - создать счет, положить и снять
с него деньги:
IH Справочник С#

Account account = new Account(l00);


account.Put(20); // Добавляем на счет 20
Console.WriteLine($"Cyммa на счете: {account.Sum)Э);
account.Take(70); // Пытаемся снять со счета 70
Console.WriteLine($"Cyммa на счете: {account.Sum)");
account.Take(180); // Пытаемся снять со счета 180
Console.WriteLine($"Cyммa на счете: {account.Sum)");

Вывод будет таким:

Сумма на счете: 120


Сумма на счете: 50
Сумма на счете: 50

Все операции работают как и положено. Но что если мы хотим уведом­


лять пользователя о результатах его операций? Мы могли бы, например,
для этого изменить метод Put следующим образом:

puЫic void Put(int sum)


{
Sum += sum;
Console.WriteLine($"Ha счет поступило: {sum)");

Казалось, теперь мы будем извещены об операции, увидев соответствую­


щее сообщение на консоли. Но тут есть ряд замечаний. На момент опре­
деления класса мы можем точно не знать, какое действие мы хотим про­
извести в методе Put в ответ на добавление денег. Это может быть вывод
на консоль, а может быть, мы захотим уведомить пользователя по email
или sms. Более того, мы можем создать отдельную библиотеку классов,
которая будет содержать этот класс, и добавлять ее в другие проекты.
И уже из этих проектов решать, какое действие должно выполняться.
Возможно, мы захотим использовать класс Account в графическом при­
ложении и выводить при добавлении на счет в графическом сообщении,
а не консоль. Или нашу библиотеку классов будет использовать другой
разработчик, у которого свое мнение, что именно делать при добавлении
на счет. И все эти вопросы мы можем решить, используя события.
Глава 5. Расширенный синтаксис

· 5.4.2. Определение и вызов событий


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

delegate void AccountHandler(string rnessage);


event AccountHandler Notify;

В данном случае вначале определяется делегат AccountHandler, который


принимает один параметр типа string. Затем с помощью ключевого слова
event определяется собьпие с именем Notify, которое представляет деле­
гат AccountHandler. Название для собьпия может бьпь произвольным, но
в любом случае оно должно представлять некоторый делегат. Определив
событие, мы можем его вызвать в программе как метод, используя имя
события:

Nоtifу("Произошло действие");

Поскольку событие Notify представляет делегат AccountHandler, кото­


рый принимает один параметр типа string - строку, то при вызове со­
бытия нам надо передать в него строку. Однако при вызове событий мы
можем столкнуться с тем, что событие равно null в случае, если для него
не определен обработчик . Поэтому при вызове события лучше его всегда
проверять на null. Например, так:

if(Notify !=null) Notify("Sorne action");

Или так:

Notify?.Invoke("Sorne action");

В этом случае, поскольку событие представляет делегат, то мы можем


его вызвать с помощью метода Invoke(), передав в него необходимые зна­
чения для параметров.
HШMIW/J Справочник С#

Объединим все вместе - и создадим и вызовем событие:

class Account

puЫic delegate v oid AccountHan dler(st ring message);


puЫic event Accountнandler? Notify; // !.Определение
соеытия
puЫic Account(int sum) => Sum = sum;
puЫic int Sum { get; private set; 1
puЫic void Put(int sum)
{
Sum += sum;
Notify?.Invoke($"3aчиcneни: q:едсгв: {Sll11}"); // 2.Еыюв сосыrия

puЬlic void Take(int sum)


{
if (Sum >= sum)

Sum - sum;
Notify?.Invoke($"0iяn-E q::едсrв: {Sll11}"); // 2.Еыюв сххыrия

else

Notify?. Invoke($"�= Д::1-ЕГ. � бзJБнс: {Sun} ");

Теперь с помощью события Notify мы уведомляем систему о том, что


были добавлены средства и о том, что средства сняты со счета или на
счете недостаточно средств.

5.4.3. Добавление обработчика события


С событием может быть связан один или несколько обработчиков. Об­
работчики событий - это именно то, что выполняется при вызове со­
бытий. Нередко в качестве обработчиков событий применяются методы.
Каждый обработчик событий по списку параметров и возвращаемому
Глава 5. Расширенный синтаксис

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


Для добавления обработчика события применяется операция +=:

Not ify += обрабо т чик событи я;

Определим обработчики для события Notify, чтобы получить в програм­


ме нужные уведомления:

Account account = new Account(l000);


account.Notify += Sha.-.М:!ssage; // Дсбзвляем оеработчик для соеi,nия Notify
account.Put(300); // Добавляем на счет 300
Console.WriteLine($"Бaлaнc: {account.Sum}");
account.Take(l00); // Пытаемся снять со счета 100
Console.WriteLine($"Бaлaнc: (account.Sum}");
account.Take(2000); // Пытаемся снять со счета 2000
Console.WriteLine($"Бaлaнc: (account.Surn}");

void ShowМessage(string message) => Console.WriteLine(message);

В данном случае в качестве обработчика используется метод


ShowMessage, который соответствует по списку параметров и возвра­
щаемому типу делегату AccountHandler. В итоге при вызове события
Notify?.Invoke() будет вызываться метод ShowMessage, которому для
параметра message будет передаваться строка, которая передается в
Notify?.Invoke(). В ShowMessage просто выводим полученное от собы­
тия сообщение, но можно было бы определить любую логику. Если бы в
данном случае обработчик не был бы установлен, то при вызове события
Notify?.lnvoke() ничего не происходило, так как событие Notify бьmо бы
равно null.

Вывод программы:

За числе ние средств: 300


Сумма на счете: 1300
Сн ятие средств: 100
Сумма на счете: 1200
Справочник С#

Недостаточно денег на счете. Тек ущий б аланс: 1200


Сумм а на счете: 1200
Теперь мы можем выделить класс Account в отдельную библиотеку клас­
сов и добавлять в любой проект.

5.4.4. Добавление и удаление обработчиков


Для одного события можно установить несколько обработчиков и потом
в любой момент времени их удалить. Для удаления обработчиков при­
меняется операция-=. Например:

Account account = new Account(l00);


account.Notify += ShowМessage; // Добавляем обработчик ShowМessage
account.Notify += Displa�ssage; // ДОбавляем ос:работчик Sh�ssage
account.Put(20); // Добавляем на счет 20
account.Notify -= DisplayRedМessage; // Удаляем обработчик
DisplayRedМessage
account.Put(50); // Добавляем на счет 50

void ShowМessage(string message) => Console.WriteLine(message);


void DisplayRedМessage(string message)

// У станавливаем красный цвет символов


Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
// Сбрасываем настройки цвета
Console.ResetColor();

Вывод программы:
Зачисление средств: 20
Зачисление ср едств: 20
Зачисление ср едств: 50

В качестве обработчиков могут использоваться не только обычные ме­


тоды, но также делегаты, анонимные методы и лямбда-выражения. Ис­
пользование делегатов и методов:
Глава 5. Расширенный синтаксис 10
Account асс = new Account(lOO);
// Установка делегата, который указывает на метод ShowMessage
acc.Notify += new Account.AccountHandler(ShowМessage);
// Установка в качестве обработчика метода ShowМessage
acc.Notify += ShowМessage; // Добавляем обработчик ShowМessage

acc.Put(20); // Добавляем на счет 20

void ShowМessage(string message) => Console.WriteLine(message);

В данном случае разницы между двумя обработчиками никакой не будет.

5.4.5. Установка в качестве обработчика события

Установка в качестве обработчика анонимного метода:

Account асс = new Account(lOO);


acc .Notify + = deleg ate (string mes)
{
Console.WriteLine(mes);
};
acc.Put(20);

Установка в качестве обработчика лямбда-выражения:

Account account = new A ccount(lOO);


account.Notify += message => Console.WriteLine (message);
account.Put(20);

5.4.6. Управление обработчиками


С помощью специальных аксессоров add/remove мы можем управлять
добавлением и удалением обработчиков. Как правило, подобная функци­
ональность редко требуется, но тем не менее мы ее можем использовать.
Например:
Справочник С#

class Account

puЫic delegate void AccountHandler(string message) ;


AccountHandler? notify;
puЬlic event AccountHandler Notify

add

notify += value;
Console.WriteLine($"{value.Method.Name} добавлен");

remove

notify -= value;
Console.WriteLine($ "{ value.Method.Name} удален" ) ;

puЫic Account(int sum) => Sum = sum;


puЫic int Sum { get; private set; }
puЫic void Put(int sum)
{
Sum += sum;
notify?.Invoke($"Нa счет поступило: {sum}"); // 2.Еызов соеыrия

puЬlic void Take( i nt sum)


{
if (Sum >= sum)

Sum -= sum;
notify?.Invoke($"Cнятиe средсrв: {sum}"); // 2.1:ыэов соеьnия

else

rюtify?. Iпvoke($"Не,�:юсrа= q:едств. т� баJВнс: {St.m) ");


Глава 5. Расширенный синтаксис

Теперь определение события разбивается на две части. Вначале просто


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

Acc ountHandler notify;

Во второй части определяем аксессоры add и remove. Аксессор add


вызывается при добавлении обработчика, то есть при операции+=. До­
бавляемый обработчик доступен через ключевое слово value. Здесь мы
можем получить информацию об обработчике (например, имя метода че­
рез value.Method.Name) и определить некоторую логику. В данном слу­
чае для простоты просто выводится сообщение на консоль:

add

notify += value;
Console.WriteLine($"{value.Method.Name} добавлен");

Блок remove вызывается при удалении обработчика. Аналогично здесь


можно задать некоторую дополнительную логику:

remove

notify -= value;
Console.WriteLine($"{value.Me thod.Name} удален");

Внутри класса событие вызывается также через переменную notify. Но


для добавления и удаления обработчиков в программе используется как
раз Notify:

Account асс = ne w Account(lOO);


acc.Notify += ShowМessage; // Добавляем обработчик ShowМessage
acc.Put(20); // Добавляем на счет 20
HIMk<J Справочник С#

acc.Notify -= ShowMessage; // Удаляем обработчик ShowMessage


acc.Put(20) ; // Добавляем на счет 20

void ShowMessage(string message) => Console.WriteLine(message);


Консольный вывод программы:
ShowMessage добавлен
На счет поступило: 20
ShowMessage удален

5.4.7. Передача: данных события


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

class AccountEventArgs

// Сообщение
puЫic string Message{get;}
// Сумма, на которую изменился счет
puЫic int Sum {get;}
puЬlic AccountEventArgs(string message, int sum)

Message = message;
Sum = sum;

Данный класс имеет два свойства: Message - для хранения выводимого


сообщения и Sum - для хранения суммы, на которую изменился счет.
Теперь применим класс AccoutEventArgs, изменив класс Account следу­
ющим образом:
class Account

puЫic delegate void AccountНandler(Ac=unt sender, Ac=untEventArgs е);


puЫic event Accountнandler? Notify;
Глава 5. Расширенный синтаксис IDMM
puЬlic int Sum { get; private set; }

puЬlic Account (int sum) => Sum = sum;

puЬlic void Put (int sum)


{
Sum += sum;
Notify?.Invoke(this, I16-l l\ca:JuntEventArgs($"нa счет rкxлyrn,uю {SU'!1}",
SU'!1) ) ;
}
puЬlic void Take(int sum)
{
if (Sum >= sum)

Sum -= sum;
Notify?.Invoke(this, new AccountEventArgs($"Cyм,.,в {sum} снята
со счеrа", sum));

else

Notify?.Invoke(this, I16-l l\ca:JuntEventArgs("№дJсга=-ю данег на


C<Ere"' SU'!1) ) ;
}

По сравнению с предыдущей версией класса Account здесь изменилось


только количество параметров у делегата и соответственно количество
параметров при вызове события. Теперь делегат AccountHandler в каче­
стве первого параметра принимает объект, который вызвал событие, то
есть текущий объект Account. А в качестве второго параметра принимает
объект AccountEventArgs, который хранит информацию о событии, полу­
чаемую через конструктор.

Теперь изменим основную программу:

Account асс = new Account (lOO);


10 Справочник С#

acc.Notify += ShowMessage;
acc.Put(20);
acc.Take(70);
acc.Take(lSO);

void ShowMessage(Account sender, AccountEventArgs е)


{
Console.WriteLine($"Cyммa транзакции: {e.Sum}");
Console.WriteLine(e.Message);
Console.WriteLine($"Teкyщaя сумма на счете: {sender.Sum}");

По сравнению с предыдущим вариантом здесь мы только изменяем ко­


личество параметров и их использования в обработчике ShowMessage.
Благодаря первому параметру в методе можно получить информацию
об отправителе события - счете, с которым производится операция. А
через второй параметр можно получить информацию о состоянии опе­
рации.

5.5. Замыкания
Замыкание (closure) представляет объект функции, который запоминает
свое лексическое окружение даже в том случае, когда она выполняется
вне своей области видимости.

Технически замыкание включает три компонента:

• Внешняя функция, которая определяет некоторую область видимо­


сти и в которой определены некоторые переменные и параметры
- лексическое окружение;
• Переменные и параметры (лексическое окружение), которые
определены во внешней функции;
• Вложенная функция, которая использует переменные и параметры
внешней функции.

В языке С# реализовать замыкания можно разными способами - с по­


мощью локальных функций и лямбда-выражений.
Глава 5. Расширенный синтаксис

Рассмотрим создание замыканий через локальные функции:

var fn = Outer(); // fn = Inner, так как метод Outer возвраш,э.ет фун1ЩИЮ


Inner
// вызываем внутреннюю функцию Inner
fn (); // 6
fn (); // 7
fn (); // 8

Action Outer () // Метод или внешняя функция

int х = 5; // Лексическое окружение - локальная переменная


void Inner () // Локальная функция

х++; // Операции с лексическим окружением


Console.WriteLine(x);

return Inner; // Возвращаем локальную функцию

· Здесь метод Outer в качестве возвращаемого типа имеет тип Action, то


есть метод возвращает функцию, которая не принимает параметров и
имеет тип void.

Action Outer ()

,Внутри метода Outer определена переменная х - это и есть лексическое


(окружение для внутренней функции:

1int х = 5;

fТакже внутри метода Outer определена внутренняя функция - локаль-


, ая функция Inner, которая обращается к своему лексическому окруже­
:нию - переменной х - увеличивает ее значение на единицу и выводит
а консоль:
Справочник С#

void Inner ()

х++;
Console.WriteLine(x);

Эта локальная функция возвращается методом Outer:

return Inner;

В программе вызываем метод Outer и получаем в переменную fn локаль­


ную функцию Inner:

var fn = Outer ();

Переменная/пи представляет собой замыкание, то есть объединяет две


вещи: функцию и окружение, в котором функция была создана. И
несмотря на то, что мы получили локальную функцию и можем ее вызы­
вать вне ее метода, в котором она определена, тем не менее она запомни­
ла свое лексическое окружение и может к нему обращаться и изменять,
что мы увидим по консольному выводу:

fn (); // 6
fn (); // 7
fn (); // 8

С помощью лямбд можно сократить определение замыкания:

var outerFn = () =>

int х = 10;
var innerFn = () => Console.WriteLine(++x);
return innerFn;
};
Глава 5. Расширенный синтаксис

var fn = outerFn(); // fn = innerFn, так как outerFn возвращает innerFn


// вызываем innerFn
fn(); // 11
fn(); // 12
fn(); // 13

Кроме внешних переменных, к лексическому окружению также относят­


ся параметры окружающего метода. Рассмотрим использование пара­
метров:

var fn = Multiply(5);

Console.WriteLine(fn(5)); // 25
Console.WriteLine(fn(б)); // 30
Console.WriteLine(fn(7)); // 35

Operation Multiply(in t n)
{
int Inner(int m)

return n * m;

return Inner;

delegate int Operation(int n);

Здесь внешняя функция - метод Multiply возвращает функцию, которая


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

delegate int Operation(int n);

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


Func<int, int>.

Вызов метода Multiply() возвращает локальную функцию, которая соот­


ветствует сигнатуре делегата Operation:
•rп Справочник С#

int Inner(int m)

return n * m;

Эта функция запоминает окружение, в котором она была создана, в


частности значение параметра п. Кроме того, сама принимает параметр
и возвращает произведение параметров п и т.

В итоге при вызове метода Multiply определяется переменнаяfп, которая


получает локальную функцию Inner и ее лексическое окружение -
значение параметра п:

var fn = Multiply(5);

В данном случае параметр п равен 5. При вызове локальной функции,


например, в случае:

Console.WriteLine(fn(б)); // 30

Число 6 передается для параметра т локальной функции, которая


возвращает произведение п ит, то есть 5 * 6 = 30.

Также можно бьmо бы сократить весь этот код с помощью лямбд:

var multiply = (int n) => (int m) => n * m;

var fn = multiply(5);

Console.WriteLine(fn(5)); // 25
Console.WriteLine(fn(б)); // 30
Console.WriteLine(fn(7)); // 35
IW/J Справочник С#

6.1. Понятие интерфейса


Первым делом нужно познакомиться с понятием интерфейса (inteiface).
Интерфейс представляет собой именованный набор абстрактных членов.
Абстрактные методы, с которыми мы познакомились в прошлой главе, не
имеют никакой стандартной реализации. Конкретные члены, определя­
емые интерфейсом, зависят от того, какое поведение моделируется с его
помощью. Интерфейс выражает поведение класса или структуры.

В библиотеках: базовых классов .NET поставляются сотни предопреде­


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

Чтобы реализовать интерфейс в классе, нужно указать тела методов,


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

Интерфейс объявляется с помощью ключевого слова inteiface. Рассмо­


трим синтаксис объявления:
Глава 6. Интерфейсы, струкrуры и перечисления

interface имя{
return type method name 1 (args);
return type method name 2 (args);
// . . .
return type method name N (args);

Здесь имя - имя конкретного интерфейса, return_type - возвращаемый


тип, method_пате_N - имя метода, args - список аргументов. По суще­
ству все эти методы являются абстрактными. Как видите, в интерфейсе
нет никакой реализации. Просто список методов. В интерфейсе все ме­
тоды неявно считаются публичными, поэтому риЬ/iс явно указывать не
нужно.

Кроме методов в интерфейсах могут быть указаны свойства, индексато­


ры и события. Но интерфейсы не могут содержать члены данных. Также
в интерфейсах нельзя определить конструкторы и деструкторы. Ни один
из членов интерфейса не может быть статическим. После определения
интерфейса его можно использовать в одном или нескольких классах.
Чтобы реализовать интерфейс, нужно указать его имя после имени клас­
са, аналогично тому, как вы это делали при указании базового класса:

class имя класса : имя_интерфейса


// тело класса

В классе можно реализовать несколько интерфейсов. Для этого нужно


указать список интерфейсов через запятую. В классе можно наследовать
базовый класс и реализовать один или более интерфейсов. В этом случае
имя базового класса должно быть указано перед списком интерфейсов,
разделяемых запятой. Методы, реализующие интерфейс, должны быть
объявлены как риЬ/iс. Возвращаемый тип и сигнатура реализуемого ме­
тода должны точно соответствовать возвращаемому типу и сигнатуре,
которые указаны при определении интерфейса.

Понимаю, что сейчас совсем ничего не понятно, но все прояснится, как


только мы рассмотрим пример, приведенный в листинге 6.1.
Справочник С#

Листинr 6.1. Пример определения интерфейсов


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplicationl

// Описываем интерфейс
puЫic interface ITest
{
// Определяем набор абстрактных методов
int Testl();
int Test2();

// Данный класс реализует интерфейс ITest


class MyClass ITest
{
int Му_х;

// Пример get-тepa и set-тepa закрытого поля х


puЫic int х
{
set Му_х = value; }
get return Му_х;

puЫic MyClass() { }

// Реализуем методы интерфейса ITest


puЫic virtual int Testl()

return 1;

puЬlic int Test2()


{
return 2;
Глава 6. Интерфейсы, структуры и перечисления

class Program

static void Main()

MyClass objl = new MyClass();


int tl objl.Testl();
int t2 = objl.Test2();

Console.WriteLine("{O} {1}", tl, t2);

Console.ReadLine();

6.2. Ключевые слова as и is


Ключевое слово as позволяет определить, поддерживает ли данный тип
тот или иной интерфейс. Если объект удается интерпретировать как ука­
занный интерфейс, то возвращается ссылка на интересующий интер­
фейс, а если нет, то ссылка пи//. Следовательно, перед продолжением в
коде необходимо предусмотреть проверку на null. Рассмотрим неболь­
шой пример:

ITest obj = objl as ITest;


if (obj != null)
Console.WriteLine("Ecть поддержка интерфейса ITest");
else
Console.WriteLine("ITest не поддерживается");

Проверить, был ли реализован нужный интерфейс, можно с помощью


ключевого слова is. Если запрашиваемый объект не совместим с указан-
liZI Справочник С#

ным интерфейсом, возвращается значение false, а если совместим, то


можно спокойно вызывать члены этого интерфейса:
if (objl is ITest)
Console.WriteLine("Дa");
else
Console.WriteLine("Heт");

6.3. Интерфейсные свойства


Подобно методам, свойства указываются в интерфейсе вообще без тела.
Синтаксис объявления интерфейсного свойства выглядит так:
// Интерфейсное свойство
тип имя{
get;
set;

В определении интерфейсных свойств, доступных только для чтения


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

Листинг 6.2. Пример определения интерфейсных свойств


using System;

namespace ConsoleApplicationl
{
interface ITest

string Str
{
get;
set;

class MyClass ITest


Глава 6. Интерфейсы, структуры и перечисления

string myStr;

puЫic string Str


{
set

myStr value;

get
{
return myStr;

class Program

static void Main()

MyClass objl = new MyClass();


userl.Str = "Hello";

Console.ReadLine();

6.4. Интерфейсы и наследование


Подобно классам, один интерфейс может наследовать другой, при этом
синтаксис наследования интерфейсов такой же, как у классов. Если в
классе реализуется один интерфейс, наследующий другой, в нем долж­
ны быть реализованы все члены, определенные в цепочке наследования
интерфейсов.Интерфейсы могут быть организованы в иерархии. Если
какой-то интерфейс расширяет существующий, он наследует все аб­
страктные члены своих родителей. В отличие от классов, производные
интерфейсы никогда не наследуют саму реализацию. Вместо этого они
Mk<I Справочник С#

просто расширяют собственное определение за счет добавления до­


полнительных абстрактных членов. Пример наследования интерфейсов
приведен в листинге 6.3.

Листинг 6.3. Пример наследования интерфейсов


using Systern;

narnespace ConsoleApplicationl
{
puЬlic interface А
{
int TestA();

// Интерфейс В наследует интерфейс А


puЬlic interface В : А
{
int TestB();

// Класс MyClass наследует интерфейс В


// и реализует методы интерфейсов А и В
class MyClass : В
{
puЬlic int TestA()
{
return 1;

puЬlic int TestB()


{
return 2;

class Prograrn

static void Main()


Глава 6. Интерфейсы, структуры и перечисления

6.5. Структуры
Объекты конкретного класса доступны по ссылке (поскольку классы от­
носятся к ссылочным типам данных), в отличие от значений обычных
типов, которые доступны непосредственно. В некоторых ситуациях для
повышения эффективности программы полезно иметь прямой доступ к
объектам, как к значениям простых типов. Ведь каждый доступ к объ­
екту (даже к самому небольшому) требует дополнительные системные
ресурсы (оперативную память и процессорное время).
Для решения подобных задач используются структуры. Структуры по­
добны классам, но они относятся не к ссьшочным типам данным. Струк­
туры отличаются от классов способом хранения в памяти и способом
доступа к ним. Классы размещаются в куче, а структуры - в стеке. Это
и есть самая большая разница. Из соображений эффективности програм­
мы структуры полезно использовать для небольших типов данных. Син­
таксически описание структуры похоже на классы. Основное отличие в
том, что класс определяется с помощью служебного слова c/ass, а струк­
тура - с помощью служебного слова struct. Синтаксис следующий:
struct имя : интерфейсы {
// объявления членов

Как и у класса, у структуры могут быть методы, поля, индексаторы,


свойства, методы и события. В структурах даже можно объявлять
конструкторы, но не деструкторы. Однако в структуре нельзя опреде­
лить конструктор по умолчанию (конструктор без параметров). Посколь­
ку структуры не поддерживают наследование, их члены нельзя указы­
вать как abstract, virtual или protected.
Объект структуры можно создать с помощью оператора new - как вы
это делали для класса. Но обычно в этом нет необходимости, поскольку
при использовании оператора new вызывается конструктор по умолча-
НJiMI-F/.J Справочник С#

нию, а если вы не используете оператор new, то объект все равно созда­


ется, но не инициализируется. Пример объявления структуры приведен
в листинге 6.4.

Листинг 6.4. Пример структуры


using System;

namespace ConsoleApplicationl

// Создадим структуру
struct Carinfo

puЫic string Brand;


puЫic string Model;

puЫic Carinfo(string Brand , string Model)


{
this.Brand Brand;
this.Model Model;

puЫic void GetCarinfo()


{
Console.WriteLine("Бpeнд: {О}, Модель: {1}", Brand, Мodel);

class Program

static void Main()

Carinfo carl = new Carinfo("Audi" , "Аб");


Carinfo car2 = new Carinfo("BMW", "XS");
Console.Write("car 1: ");
carl.GetCarinfo();
Console.Write("car 2: ");
car2.GetCarinfo();
Глава 6. Интерфейсы, струкrуры и перечисления

carl = car2;
car2.Brand = "Toyota";
car2.Model = "Camry";
Console.Write("carl: ");
car.GetCarinfo();
Console.Write("car2: ");
car2.GetCarinfo();

Console.ReadLine();

Вывод программы будет таким:

carl: Бренд Audi Модель Аб


car2: Бренд ВМW Модель XS
carl: Бренд BMW Модель XS
car2: Бренд Toyota Модель Camry
Если одна структура присваивается другой, то при этом создается копия
ее объекта. Это главное отличие структуры от класса. Когда ссьшка на
один класс присваивается ссьшке на другой класс, в итоге ссьшка в ле­
вой части оператора присваивания указывает на тот же самый объект,
что и ссьшка в правой его части. А когда переменная одной структуры
присваивается переменной другой структуры, создается полная копия
объекта структуры из правой части оператора присваивания. Если бы
вместо структуры Carlnfo мы бы использовали класс с таким же именем,
то вывод программы был бы таким:
carl: Бренд Audi Модель Аб
car2: Бренд BMW Модель xs
carl: Бренд Toyota Модель Camry
car2: Бренд Toyota Модель Camry

6.6. Перечисления
Перечисление (enumeration) - это определяемый пользователем цело­
численный тип. Когда вы объявляете перечисление, то указываете набор
Справочник С#

допустимых значений, которые могут принимать экземпляры перечисле­


ний. Этим значениям можно присвоить имена, понятные человеку. Если
присвоить экземпляру перечисления значение, не входящее в список ра­
нее определенных, компилятор выдаст ошибку.

Синтаксис определения перечисления такой:


enum имя {список перечисления};
Листинг 6.5 содержит пример определения и использования перечисле­
ния.

Листинг 6.5. Использование перечислений


using System;

namespace ConsoleApplicationl

// Создать перечисление
enum car : long { Brand, Model, Year, Engine 1

class Program

static void Main()

car carl;
for (carl = car.Brand; carl <= car.Engine; carl++)
Console.WriteLine("Ключ: \"(01\", значение (11",
carl, (int)carl);

Console.ReadLine();

Каждая символически обозначаемая константа в перечислении имеет


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

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


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

При желании можно самостоятельно назначит1(значения элементам пе­


речисления:
enurn car { Brand = 1, Model 2, Year 3, Engine 4 }

6. 7. Вложенные типы
Вложенным является тип, который обновляется внутри другого типа:
puЫic class UpLevel
{
puЫic class NestedType {)
puЫic enum Font { Arial, Times, Courier )

Вложенный тип обладает следующими возможностями:

• Он может получить доступ к закрытым членам верхнего типа и


всему остальному, что верхний тип может «достать».
• Вы можете объявить его с полным набором модификаторов досту­
па, а не просто риЫiс или interna/.
• Доступность по умолчанию для вложенного типа - private, а не
interna/.
• Для доступа к вложенному типу из-за пределов верхнего типа тре­
буется квалификация с именем включающего типа (например, как
при доступе к статическим членам).

Например, чтобы получить доступ к Font.Arial за пределами класса


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

UpLevel.Font font = UpLevel.Font.Arial;


Все типы (классы, структуры, интерфейсы, делегаты и т.д.) могут быть
вложенными. Рассмотрим пример доступа к приватному члену типа из
вложенного типа:
•ru Справочник С#

puЬlic class UpLevel


{
static int а;
class Nested

static void АЬс() { Console.WriteLine(UpLevel.a); }

Также нужно рассмотреть пример применения модификатора доступа


protected к вложенному типу:
puЫic class UpLevel
{
protected class Nested { }

puЫic class SubUpLevel : UpLevel


{
static void Foo() ( new UpLevel.Nested();

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


но так:
puЫic class UpLevel
(
puЫic class Nested { }

class Test

UpLevel.Nested а;

Вложенные типы активно используются самим компилятором, когда он


создает закрытые классы, фиксирующие состояние для таких конструк­
ций, как итераторы и анонимные методы.
•со Справочник С#

7.1. Введение в обработку исключений


От ошибок никто не застрахован. Но не всегда ошибки происходят по
вине разработчика. В некоторых случаях ошибку могут сгенерировать
действия пользователя, например, недопустимый ввод. Также ошибка
может произойти из-за отсутствия того или иного драйвера, который ис­
пользует приложение и который или отсутствует, или работает некор­
ректно на конечной системе. Самый простой пример - калькулятор.
Все знают, что на О делить нельзя, но пользователь по тем или иным
причинам может указать О в качестве второго операнда (или же О будет
сгенерирован в результате вычисления какой-то формулы, если вы раз­
рабатываете сложный математический калькулятор). В этом случае про­
изойдет ошибка деления на О и будет сгенерировано исключение System.
DivideByZeroException. Программа будет остановлена, а пользователь
увидит окно с ошибкой.

Несмотря на то, что ошибка произошла по вине пользователя, ответ­


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

Существует две тактики обработки исключений. Первая заключается в


том, чтобы не допустить появление исключения как такового. В случае
Глава 7. Обработка исключений (-t/,1
с калькулятором приложение должно проверить второй операнд до вы­
полнения самой операции деления и сообщить пользователю о том, что
он собирается выполнить недопустимую операцию. Аналогично, при ра­
боте с файлами приложение сначала должно проверить существование
файла, а уже потом открывать ero для чтения.

Вторая тактика основана на предположении тоrо, что какой-то код может


вызвать исключение. Этот код заключается в блок try/catch, и произво­
дится обработка ожидаемого исключения.

Первая тактика считается предпочтительной, но она возможна не всегда.


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

Прежде чем мы перейдем к самим исключениям, нужно поговорить о


типах ошибок. Всего существует три типа ошибок: ошибки программи­
ста, ошибки пользователя и исключения. К первому типу ошибок отно­
сятся ошибки в логике программы. Например, ошибка неучтенной едини­
цы (off-by-one error). Встречается, когда, например, количество итераций
в цикле на единицу больше или меньше количества обрабатываемого
массива. Программист может забыть, что нумерация массива начинается
с О, и начать с единицы. Если в цикле используется условие останов­
ки при достижении значения, определенного в свойстве Length, то цикл
просто не обработает один элемент - первый (с индексом О). Если же
количество элементов в цикле задано жестко, например 1 О, то произой­
дет нарушение границ массива. Это только один из примеров ошибок
программиста. Их существуют сотни, если не тысячи.

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


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

ввод, то пользователь вместо обычной информации может указать в поле


ввода SQL-кoд, который удалит всю базу данных. Такая угроза называ­
ется SQL lnjection.

Исключение (exception), или исключительная ситуация - аномалия,


которая может возникнуть во время выполнения (runtirne) и которую
трудно предусмотреть во время программирования. Примеры исключи­
тельных ситуаций - открытие поврежденного файла, попытка чтения из
таблицы базы данных, которая уже не существует или же ее структура из­
менена. Если с ошибкой деления на О или с открытием файла все просто
- достаточно или проверить второй операнд, или существование самого
файла, то с такими ошибками все не так однозначно. Приложение откры­
вает файл и ожидает, что он будет в определенном формате, например в
XML. Проверить принадлежность к тому или иному формату можно при
открытии файла - если приложение откроет не ХМL-файл, а обычный
текстовый файл, то сразу сообщит об этом пользователю. Но если прило­
жение таки открывает ХМL-файл, оно начинает чтение. По мере чтения
оказывается, что файл поврежден. Сразу проверить, поврежден ли файл
или нет, практически невозможно. Например, размер файла может быть
довольно большим и если проверять корректность файла перед каждой
обработкой, то работать программа будет неэффективно. Ведь ошибки
происходят довольно редко, а проверять корректность придется каждый
раз при открытии файла.

Однако программист может предусмотреть, что такая исключительная


ситуация может произойти. Он должен перехватить ее и обработать. До
появления .NET обработка ошибок в Windows была довольно сложной.
Многим программистам приходилось включать собственную логику об­
работки ошибок в приложение. Кроме этих приемов, которые бьmи разра­
ботаны самим разработчиками, в АРI-интерфейсе Windows определены
сотни кодов ошибок с помощью #define и HRESULT, а также множество
вариаций простых булевских значений (bool, BOOL, VARIANT BOOL
и т.д.). В случае с СОМ-приложениями, написанными на языках VB6 и
С++, все еще сложнее.

Основная проблема со всеми этими подходами - отсутствие симме­


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

В .NET поддерживается стандартная методика для генерации и выявле­


ния ошибок в исполняющей среде. Данная методика позволяет разра­
ботчикам использовать в области обработки ошибок унифицированный
подход, являющийся общим для всех .NЕТ-языков. Благодаря этой мето­
дике, С#-программист может обрабатывать ошибки точно таким же об­
разом, как и VВ-программист.

7 .2. Перехват исключений. Блоки try, catch,


fi,nally
Для обработки исключений в С# используются следующие блоки:
• try - содержит код, который потенциально может столкнуться с
исключительной ситуацией;
• catch - содержит код, обрабатывающий ошибочные ситуации,
происходящие в коде блока try;
• finally - содержит код, очищающий любые ресурсы или выпол­
няющий другие действия, которые обычно нужно выполнить в
конце блоков try или catch. Данный код выполняется независимо
от того, сгенерировано исключение или нет.

В основном используются блоки try/catch. Синтаксис следующий:


try {
// Блок кода, проверяемый на наличие ошибок.

catch (Excepl ехОЬ) {


// Обработчик исключения Excepl
}
catch (Ехсер2 ехОЬ) {
// Обработчик исключения Ехсер2
}
@jj-(.Щ Справочник С#

Здесь Ехсер - это тип исключения. Если исключение сгенерировано


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

При перехвате исключения переменная ехОЬ получает свое значение.


Вообще указывать переменную ехОЬ необязательно, если обработчику
исключений не требуется доступ к объекту исключения, что на самом
деле бывает очень часто - ведь достаточно просто знать тип исключе­
ния. Если исключение не генерируется, то блок оператора try завершает­
ся как обычно, а все блоки catch пропускаются. Выполнение программы
возобновляется с первого оператора, который следует после завершаю­
щего оператора catch. Другими словами, содержимое catch выполняется,
только если генерируется исключение.

Посмотрим, как можно обработать деление на О, о котором мы так много


говорили:
int х = 10, у О, z;
try
{
z = х / у;

catch (DivideByZeroException)
{
Console.WriteLine("Дeлeниe на 0");

Преимущество обработки исключений в том, что ваша программа не


будет завершаться аварийно. В данном случае программа отобразит
сообщение и продолжит обычное выполнение. Правда, что произойдет
далее - зависит от самой программы. При необходимости можно ука­
зать несколько блоков catch:
try
{
// Некоторые вычисления
Глава 7. Обработка исключений C-&JMil
catch (OverflowException)
{
Console.Write("Пepeпoлнeниe");

catch (DivideByZeroException)
{
Console.WriteLine("Дeлeниe на 0");

catch (IndexOutOfRangeException)
{
Console.WriteLine("Bыxoд за пределы диапазона");

Каждый блок catch будет реагировать только на свой класс исключения.

7 .3. Класс Exception


Все определяемые исключения всегда наследуются от базового клас­
са System.Exception, который, в свою очередь, наследуется от класса
System.Object. Класс System.Exception содержит свойства и методы.
Полное описание этого класса представлено по ссылке:

https://msdn.microsoft.com/ru-rn/librarylsystem.exception%28v = vs.110%29.aspx

Мы же рассмотрим основные свойства этого класса, которые использу­


ются чаще всего (табл. 7.1).

Таблица 7.1. Некоторые свойства класса System.Exception

Свойство Описание

Позволяет извлекать коллекцию пар «ключ/значе-


ние» (представленную объектом, реализующим
интерфейс IDictionary), которая предоставляет до-
Data полнительную информацию об исключении. По
умолчанию данная коллекция пуста и должна опре-
деляться программистом. Свойство доступно только
для чтения
Справочник С#

Позволяет получать или устанавливать URL-aдpec,


HelpLink по которому доступен справочный файл или веб-
сайт с подробным описанием ошибки

Используется для получения информации о преды-


дущем исключении или исключениях, которые по-
lnnerException
служили причиной возникновения текущего исклю-
чения. Свойство доступно только для чтения

Содержит сообщение об ошибке. Доступно только


Message
для чтения
Позволяет получать или устанавливать имя сборки
Source
или объекта, который привел к вьщаче исключения
Содержит строку с описанием последовательности
вызовов, которая привела к возникновению ис-
StackTrace ключения. Доступно только для чтения. Полезно
использовать во время отладки. Содержимое этого
поля можно сохранять в журнале ошибок
Возвращает объект MethodВase с описанием много-
TargetSite численных деталей метода, который привел к вьща-
че исключения. Доступно только для чтения

Рассмотрим пример использования объекта Exception:


int х = 10, у = О, z;
try
{
z = х / у;

catch (DivideByZeroException ех)


{
Console.WriteLine("Дeлeниe на 0");
Console.WriteLine(ex.Мessaqe);
Глава 7. Обработка исключений IOMII
Отличия от предыдущего примера выделены жирным. В данном приме­
ре мы выводим дополнительную информацию из свойства Message.

Самостоятельно породить исключение можно с помощью оператора


throw. Иногда это полезно для отладки. Пример:
Exception ехсер = new Exception();
excep.HelpLink = "google.corn";
excep.Data.Add("Tirne: ", DateTirne.Now);
excep.Data.Add("Reason: ", "Wrong input");
throw ехсер;
Рассмотрим пример блока catch, который выводит очень подробную ин­
формацию об ошибке:
catch (Exception ех)

Console.Write("Oшибкa: ");
Console.Write(ex.Message + "\n\n");
Console.Write("Meтoд: ");
Console.Write(ex.TargetSite + "\n\n");
Console.Write("Cтeк: ");
Console.Write(ex.StackTrace + "\n\n");
Console.Write("Пoдpoбнocти: ");
Console.Write(ex.HelpLink + "\n\n");
if (ex.Data != null)

Console.WriteLine("Пoдpoбнocти: \n");
foreach (DictionaryEntry d in ex.Data)
Console.WriteLine("-> {О} {l}",d.Key, d.Value);

7.4. Исключения уровня системы


В библиотеке классов .NET содержится множество классов, которые
наследуются от System.Exception. Так, в пространстве имен System опре­
делены ключевые классы исключений - StackOverflowException (исклю­
чение переполнения стека), lndexOutOfRangeException (исключение
1-t/J Справочник С#

выхода за диапазон) и др. В других пространствах имен также опреде­


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

Исключения, генерируемые самой платформой .NЕТ, называются


исключениями уровня систе.мы. Такие исключения считаются также фа­
тальными ошибками. Они наследуются прямо от базового класса System.
SystemException, который, в свою очередь, наследуется от System.
Exception.

Кроме исключений уровня системы есть еще и исключения уровня при­


ложений (класс System.AppicationException). Если вам нужно создать
собственные исключения, предназначенные для конкретного прило­
жения, их нужно наследовать от System.AppicationException. В классе
System.AppicationException никаких членов, кроме набора конструк­
торов, не пре�агается. Единственная цель этого класса - указание
на источник ошибки. Если произошло исключение, унаследованное от
System.ApplicationException, программист может смело полагать, что
исключение было вызвано кодом функционирующего приложения, а не
библиотекой базовых классов .NET.

Если вы планируете создавать свои классы исключений, то вы должны


придерживаться рекомендаций .NET, а именно пользовательские классы
должны:

• Наследоваться от ApplicationException;

• Содержать атрибут [System.SerializaЫe];

• Иметь конструктор по умолчанию;

• Иметь конструктор, который устанавливает значение унаследо­


ванного свойства Message;

• Иметь конструктор для обработки «внутренних исключений));

• Иметь конструктор для обработки сериализации типа.


Глава 7. Обработка исключений

Рассмотрим «болванку» такого класса (лист. 7.1).

Листинг 7.1. Болванка класса пользовательского


исключения
[System.SerializaЫe]
puЫic class МуЕхс : ApplicationException

puЫic МуЕхс () { }
puЬlic МуЕхс (string message) : base(message) ( }
puЫic МуЕхс (string message, Exception ех) : base(message) { }
protected МуЕхс (System.Runtime.Serialization.Serializationinfo info,
System.Runtime.Serialization.StreamingContext context)
: base( info, context) { }

Данный класс, хотя ничего и не делает, но полностью соответствует ре­


комендациям .NET.

7 .5. Ключевое слово finally


Осталось нерассмотренным ключевое слово finally. Если вам нужно
определить код, который будет выполняться после выхода из блока try/
catch, вам нужно использовать блок finally. Использование этого блока
гарантирует, что некоторый набор операторов будет выполняться всегда,
независимо от того, возникло исключение (любого типа) или нет.

Синтаксис следующий:
try {
// Блок кода, проверяемый на наличие ошибок.

catch (Excepl ехОЬ) {


// Обработчик исключения Excepl
}
catch (Ехсер2 ехОЬ) {
// Обработчик исключения Ехсер2
kW/1 Справочник С#

finally {
// Этот код будет выполнен после обработки исключений

Блок finally выполняется каждый раз, когда происходит выход из блока


try/catch, независимо от причин, которые привели к этому блоку. Если
блок try завершается нормально или по причине исключения, то послед­
ним выполняется код, определяемый в блоке finally. Блок finally выпол­
няется и в том случае, если любой код в блоке try или в связанных с ним
блоках catch приводит к возврату из метода.

Пример:

int х = 10, у о, z;
try
[
z = х / у;

catch (DivideByZeroException)
{
Console.WriteLine("Дeлeниe на 0");

finally
{
Console.WriteLine("Koнeц программы");

7 .6. Ключевые слова checked и uncliecked


Ранее я обещал, что скоро мы рассмотрим ключевые слова checked и
unchecked. В языке С# можно указывать, будет ли в коде сгенерировано
исключение при переполнении. Для этого используются эти ключевые
слова . Если нужно указать, что выражение будет проверяться на пере­
полнение, нужно использовать ключевое слово checked. Если нужно
проигнорировать переполнение - ключевое слово unchecked.
Глава 7. Обработка исключений

Синтаксис такой:

checked {
// операторы
}

Если вычисление проверяемого выражения приводит к переполнению,


то будет сгенерировано исключение OverflowException. Синтаксис
unchecked такой же:

unchecked {
// операторы
}

Рассмотрим пример кода:

byte х, у, res;

try
{
Console.Write("Bвeдитe х:");
х = unchecked((byte)int.Parse(Console.ReadLine()));
Console.Write("Bвeдитe у:");
у = unchecked((byte)int.Parse(Console.ReadLine()));

checked

res = (byte)(х + у);


Console.WriteLine("res {О}", res);

catch (OverflowException)

Console.WriteLine("Пepeпoлнeниe");
Справочник С#

Здесь специально используется тип byte (диапазон 0.. 255), чтобы про­
ще было вызвать переполнение. Потенциально опасный код заключен в
checked. Обратите внимание: код, получающий значения х и у, заключен
в unchecked. Для одиночного выражения допускается использование
круглых скобок вместо фигурных.
•1-а Справочник С#

8.1. Введение в коллекции


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

С помощью коллекций существенно упрощается программирование


многих задач, поскольку они предлагают готовые решения для создания
целого ряда типичных, но иногда очень трудных для разработки струк­
тур данных. Так, в среду .NET встроены коллекции для поддержки хеш­
таблиц, динамических массивов, очередей, стеков, связных списков.
Коллекции заслуживают внимания всех С#-программистов. Зачем
изобретать колесо заново?

Ранее существовали только классы необобщенных коллекций. Но позже


появились обобщенные классы и интерфейсы. Благодаря этому общее
количество классов удвоилось. Вместе с библиотекой TPL (используется
для распараллеливания задач) появился ряд новых классов коллекций,
предназначенных для применения в тех случаях, когда доступ к коллек­
ции осуществляется из нескольких потоков. Интерфейс Collections API
настолько важен, что он занимает огромную часть всей среды .NET.

Все коллекции разработаны на основе набора четко определенных ин­


терфейсов. Некоторые встроенные реализации таких интерфейсов, в том
числе AпayList, HashtaЫe, Stack и Queue, могут применяться в исход­
ном виде - без изменений. При желании программист может создать
собственную коллекцию, но, учитывая богатый набор коллекций, такая
необходимость возникает редко.
Глава 8. Коллекции и итераторы 1-t-JMI
Примечание. Если вы ранее програ.ммировШ1и на С++, то
вам будет интересно знать, что классы коллекций по своей
сути подобны классам стандартной библиотеки шаблонов
(Standard Template Library - SТL), определенной в С++. Кол­
лекция в терминологии С++-это не что иное, как контейнер.

В .NET поддерживаются пять типов коллекций:

• Необобщенные - коллекции, реализующие ряд основных струк­


тур данных, включая динамический массив, стек, очередь, а так­
же словари. Об этом типе коллекций нужно помнить следующее:
все они работают с типом данных object. Поэтому необобщенные
коллекции могут служить для хранения данных любого типа, при­
чем в одной коллекции допускается наличие разнотипных данных.
Классы и интерфейсы необобщенных коллекций находятся в про­
странстве имен System.Collections.
• Специальные - работают с данными конкретного типа. Имеются
специальные коллекции для символьных строк, а также специаль­
ные коллекции, в которых используется однонаправленный спи­
сок. Такие коллекции объявляются в пространстве имен System.
Collections.Specialized.
• Поразрядная коллекция - такая коллекция одна, но она не попа­
дает ни под один другой тип коллекций. Она определена в System.
Collections и называется BitAпay. Коллекция поддерживает пораз­
рядные операции, т.е. операции над отдельными двоичными раз­
рядами, например, И, ИЛИ, исключающее ИЛИ.
• Обобщенные - такие коллекции обеспечивают обобщенную реа­
лизацию нескольких стандартных структур данных, включая связ­
ные списки, стеки, очереди и словари. В силу своего обобщенного
характера такие коллекции являются типизированными. Объявля­
ются в пространстве имен System.Collections.Generic.
• Параллельные - поддерживают многопоточный доступ к кол­
лекции. Определены в пространстве имен System.Collections.
Concuпent.

Основным для всех коллекций является понятие перечислителя, ко­


торый поддерживается в необобщенных интерфейсах IEnumerator и
Справочник С#

IEnumeraЫe, а также в обобщенных интерфейсах IEnumerator<Т> и


IEnumeraЫe<T>. Перечислитель предоставляет стандартный способ по­
очередного доступа к элементам коллекции. Другими словами, он пере­
числяет содержимое коллекции. В каждой коллекции реализована обоб­
щенная или необобщенная форма интерфейса IEnumeraЫe, элементы
любого класса коллекции должны быть доступны с помощью методов,
которые определены в интерфейсе IEnumerator или IEnumerator<T>. Для
поочередного обращения к содержимому коллекции в цикле foreach ис­
пользуется перечислитель.

С перечислителями тесно связаны итераторы. Итератор упрощает про­


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

В таблице 8.1 приведены интерфейсы, реализуемые в коллекциях С#.

Таблица 8.1. Интерфейсы, реализуемые в коллекциях С#

Интерфейс Описание

Интерфейс, реализованный классами 0606-


щенных коллекций. Позволяет получить ко-
личество элементов в коллекции (свойство
ICollection<Т> Count), скопировать коллекцию в массив (ме-
тод СоруТо()). Также позволяет добавлять
и удалять элементы из коллекции (методы
Add(), Remove(), Clear())

Необходим, когда с коллекцией используется


оператор foreach. Этот интерфейс определяет
IEnumeraЫe<Т>
метод GetEnumerator(), возвращающий пере-
числитель, который реализует IEnumerator
Впервые появился в версии .NET 4. Реали-
зуется множествами. Позволяет комбини-
ровать различные множества в обьедине-
ISet<Т>
ния, а также проверять, не пересекаются
ли два множества. ISet<Т> унаследован от
ICollection<Т>
Глава 8. Коллекции и итераторы

Предназначен для создания списков, элемен-


ты которых доступны по своим позициям.
Определяет индексатор, а также способы
IList<T>
вставки и удаления элементов в определен-
ные позиции (методы lnsert() и Remove()).
IList<Т> унаследован от ICollection<Т>

Реализован компаратором и используется


IComparer<Т> для сортировки элементов внутри коллекции
с помощью метода Compare()

Реализуется обобщенными классами кол-


лекций, элементы которых состоят из ключа
и значения. С помощью этого интерфейса
IDictionary<TKey, TValue> можно получать доступ ко всем ключам и
значениям, извлекать элементы по индекса-
тору типа ключа, а также добавлять и уда-
лять элементы

Похож на IDictionary<TKey, TValue> и под-


держивает ключи и значения. Однако в этом
ILookup<TKey, TValue>
случае коллекция может содержать множе-
ственные значения для одного ключа

Появился в .NET 4. Используется для под-


IProducerConsumerCo\lection<Т> держки новых, безопасных в отношении по-
токов классов коллекций
Реализован компаратором, который может
быть применен к ключам словаря. Через этот
IEqualityComparer<Т> интерфейс объекты могут быть проверены
на предмет эквивалентности друr друrу.
Поддерживается массивами и кортежами

8.2. Необобщенные колдекции


Необобщенные коллекции вошли в состав среды первыми. Они появи­
лись в самой первой версии .NET, поэтому считаются самыми давними.
e+t<:1 Справочник С#

Необобщенные коллекции определяются в пространстве имен System.


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

В способе их манипулирования объектами заключается их основное пре­


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

Необобщенные коллекции определены во многих интерфейсах и клас­


сах, которые реализуют эти интерфейсы. Множество необобщенных
коллекций определено в пространстве имен System.Collections. Интер­
фейсы, являющиеся фундаментом для необобщенных коллекций, пред­
ставлены в таблице 8.2.

Таблица 8.2. Интерфейсы, используемые в необобщенных коллекциях

Интерфейс Описание

Определяет элементы, которые должны иметь все не-


ICollection
обобщенные коллекции
Определяет метод Compare() для сравнения объектов,
IComparer
хранящихся в коллекции
Определяет коллекцию, состоящую ИЗ пар «ключ-
IDictionary
значение» (словарь)
Определяет перечислитель для коллекции, реализующей
IDictionaryEnumerator
интерфейс IDictionary
Определяет метод GetEnumerator(), предоставляющий
IEnumeraЬ\e
перечислитель для любого класса коллекции
Глава 8. Коллекции и итераторы

Предоставляет методы, позволяющие получать содер-


IEnumerator
жимое коллекции по очереди

IEqualityComparer Сравнивает два объекта на предмет равенства

IНashCodeProvider Устарел. Используйте интерфейс IEqualityComparer

Определяет коллекцию, доступ к которой можно полу-


IList
чить с помощью индексатора

Содержит метод CompareTo(), применяемый для струк-


IStructura!Comparable
турноrо сравнения
Содержит метод Equals(), применяемый для выяснения
1Structura!EquataЫe структурного, а не ссылочного равенства. Кроме того,
определяет метод GetНashCode()

Особое место в программировании занимают словари. Интерфейс


IDictionary определяет коллекцию, которая состоит из пар «ключ­
значение». В пространстве имен Systern.Collections определена струк­
тура DictionaryEntry. Необобщенные коллекции пар «ключ-значение»
сохраняют эти пары в объекте типа DictionaryEntry. В структуре
DictionaryEntry определяются два следующих свойства:

puЫic object Кеу { get; set; }


puЫic object Value { get; set;

Данные свойства используются для доступа к ключу или значению, свя­


занному с элементом коллекции. Построить объект типа DictionaryEntry
можно с помощью следующего конструктора:

puЫic DictionaryEntry(object key, object value)

Параметр key - это ключ, value - это значение.

Таблица 8.3 содержит классы необобщенных коллекций.


Справочник С#

Таблица 8.3. Классы необобщенных коллекций

Класс Описание
Определяет динамический массив. Динамические
AпayList массивы при необходимости могут увеличивать свой
размер
Используется для работы с хеш-таблицами для пар
HashtaЫe
«ключ-значение>>

Queue Очередь или список, построенный по принципу FIFO


- первым зашел, первым вышел
SortedList Отсортированный список пар «ключ-значение»
Стек или список, построенный по принципу LIFO -
Stack
последним зашел, первым вышел

8.3. Обобщенные коллекции


Как уже было отмечено, благодаря введению обобщений, количество
коллекций существенно увеличилось. Все обобщенные коллекции
объявляются в пространстве имен System.Collections.Generic.

Обычно классы обобщенных кшшекций являются не более чем обоб­


щенными эквивалентами классов необобщенных коллекций. В некото­
рых случаях одни и те же функции существуют параллельно в классах
обобщенных и необобщенных коллекций, хотя и под разными именами.
Примером может послужить обобщенный вариант класса HashTaЫe, ко­
торый называется Dictionary. Аналогично, обобщенный класс List - это
аналог необобщенного класса AпayList. Вообще, практически все, что
вы знаете о необобщенных коллекциях, применимо и к обобщенным
коллекциям.

Обобщенные коллекции из пространства имен System.Collections.Generic


определены в таблице 8.4.
Глава 8. Коллекции и итераторы C-t-:JMI
Таблица 8.4. Интерфейсы обобщенных коллекций

Интерфейс Описание

Здесь определены основные свойства


/Collection<Т>
обобщенных коллекций
Определяет обобщенный метод
/Comparer<Т> Compare() для сравнения объектов, хра-
нящихся в коллекции
Определяет словарь, то есть обобщенную
/Dictionary<Tkey, TValue> коллекцию, состоящую из пар «ключ-
значение))
Содержит обобщенный метод
/ЕпитеrаЫе<Т> GetEnumerator(), предоставляющий пере-
числитель для любого класса коллекции
Предоставляет методы, позволяющие по-
/Enumerator<Т>
лучать содержимое коллекции по очереди
Компаратор. Сравнивает два объекта на
IEqualityComparer<Т>
предмет равенства
Определяет обобщенную коллекцию,
/List<Т> доступ к которой осуществляется с помо-
щью индексатора

Аналогично, для работы со словарем в пространстве имен System.


Collections.Generic определена структура KeyValuePair<TKey, TValue>.
В этой структуре определяются два следующих свойства:

puЫic ТКеу Кеу { get; );


puЫic TValue Value { get; );
Для создания объекта типа KeyValuePair<TKey, TValue> используется
конструктор:

puЫic KeyValuePair(TKey key, TValue value)


Mk№I Справочник С#

В таблице 8.5 приведены основные классы обобщенных коллекций,


определенные в пространстве имен System.Collections.Generic.

Таблица 8.5. Основные классы обобщенных коллекций

Класс Описание

Dictionary<Tkey, TValue> Словарь. Используется для хранения пары


«ключ-значение». Функции такие же, как и у
необобщенного класса HashTaЫe
Используется для хранения уникальных зна-
HashSet<Т>
чений с использованием хеш-таблицы

LinkedList<Т> Сохраняет элементы в двунаправленном списке

Создает динамический массив. Функцио-


List<Т> нал такой же, как и у необобщенного класса
ArrayList

Очередь. Функционал такой же, как у необоб-


Qиеие<Т>
щенного класса Queue
Используется для создания отсортированного
SortedDictionary< ТКеу, TValue>
списка из пар «ключ-значение»
Создает отсортированный список. Функци-
SortedList<TKey, TValue> онал такой же, как у необобщенного класса
SortedList

SortedSet<Т> Создает отсортированное множество

Стек. Функционал такой же, как у необоб-


Stack<Т>
щенного класса Stack

В пространстве имен System.Collections.Generic определены и другие


классы, но они используются реже. Далее мы рассмотрим описанные ра­
нее классы. Не все, а только лишь те, которые, на мой взгляд, являются
наиболее интересными. С остальными вы сможете познакомиться в до­
кументации по С# на официальном сайте Microsoft.
Глава 8. Коллекции и итераторы 1-F/J
8.4. Класс ArrayList. Динамические массивы
В С# подцерживаются динамические массивы, то есть такие массивы,
которые расширяются и сокращаются по мере необходимости. Стандарт­
ные массивы имеют фиксированную длину, которая не может изменяться
во время выполнения программы. Другими словами, при использовании
стандартных массивов программист должен задать длину массива зара­
нее. Но количество элементов иногда неизвестно до момента выполне­
ния программы. Например, вам нужно создать массив строк, загрузив
его из файла. Вы не знаете, сколько строк будет в файле. Именно для
таких ситуаций и предназначен классAпayList. В нем определяется мас­
сив переменной длины, состоящий из ссылок на объекты и динамически
увеличивающий и уменьшающий свой размер.

Динамический массив AпayList создается с первоначальным размером.


В случае превышения размера массив будет автоматически расширен. А
при удалении объектов из такого массива он автоматически сокращается.
В С# коллекции клaccaAпayList широко используются. В клacceAпay­
List реализуются интерфейсы ICollection, IList, IEnumeraЫe и ICloneaЫe.

Рассмотрим конструкторы класса AпayList:


puЫic ArrayList()
puЫic ArrayList(ICollection с)
puЫic ArrayList(int capacity)
Первый конструктор используется для создания пустой коллекции класса
AпayList. Емкость такой коллекции равна О. При добавлении в динами­
ческий элемент массивов он будет автоматически расширяться. Второй
конструктор создает коллекцию типа AпayList с количеством иници­
ализируемых элементов, которое определяется параметром с и равно
первоначальной емкости массива. Третий конструктор позволяет указать
емкость массива целым числом. Емкость коллекции типа AпayList уве­
личивается автоматически по мере добавления в нее элементов.

Как вы уже догадались, в классе AпayList определены собственные


методы. Например, произвести двоичный поиск в коллекции можно с
помощью метода BinarySearch(). Но перед этим коллекцию желательно
Справочник С#

отсортировать методом Sort(). В таблице 8.6 приведены некоторые мето­


ды класса AпayList.

Таблица 8.6. Некоторые методы класса ArrayList

Метод Описание

Добавляет диапазон значений из одной коллекции в конец


AddRange()
вызывающей коллекции типа ArrayList

Двоичный поиск значения в вызывающей коллекции. Воз-


вращает индекс найденного элемента или отрицательное
BinarySearch() значение, если искомое значение не найдено. Перед ис-
пользованием этого метода нужно отсортировать список
методом Sort()

Копирует содержимое вызывающей коллекции в массив.


СоруТо() Массив должен быть одномерным и совместимым по типу с
элементами коллекции

Заключает коллекцию в оболочку типа ArrayList с фиксиро-


FixedSize()
ванным размером и возвращает результат
Возвращает индекс первого вхождения объекта в вызываю-
IndexOf()
щей коллекции или -1, если объект не найден

Вставляет элементы коллекции в вызывающую коллекцию,


/nsertRange()
начиная с элемента, указываемого по индексу

Создает коллекцию, доступную только для чтения, и возвра-


Readonly()
щает результат
Удаляет часть вызывающей коллекции, начиная с элемента,
RemoveRange() заданного параметром index. Количество элементов задается
параметром count

Sort() Сортирует вызывающую коллекцию по возрастанию

Огромное значение в классе ArrayList имеет свойство Capacity, позволя­


ющее получать и устанавливать емкость вызывающей коллекции типа
AпayList. Свойство определено так:
Глава 8. Коллекции и итераторы CW:1MI
puЫic virtual int Capacity { get; set; }

Свойство Capacity позволяет получать и устанавливать емкость вызыва­


ющей коллекции типа AпayList. Свойство содержит количество элемен­
тов, которое может содержать коллекция до очередного изменения раз­
мера. Поскольку ArrayList расширяется автоматически, задавать емкость
вручную не нужно. Но это можно сделать, если количество элементов
коллекции известно заранее. Благодаря этому исключаются издержки на
выделение дополнительной памяти. Иногда требуется уменьшить размер
коллекции. Для этого достаточно установить меньшее значение свойства
Capacity. Обратите внимание: оно должно быть не меньше значения
свойства Count. Свойство Count определено в интерфейсе ICollection
и содержит количество объектов, хранящихся в коллекции на данный
момент. Если вы попытаетесь установить значение свойства Capacity
меньше значения свойства Count, будет сгенерировано исключение
ArgumentOutOfRangeException. Чтобы получить такое количество эле­
ментов коллекции типа AпayList, которое содержится в ней на данный
момент, нужно установить значение свойства Capacity равным значению
свойства Count. Также вы можете использовать метод TrimToSize().

Настало время рассмотреть практический пример (листинг 8.1 ).

Листинг 8.1. Работа с коллекциями


using System;
using System.Collections;

namespace WorkingWithCollection

class IntCollection

puЫic static ArrayList NewCollection(int i)


{
Random ran = new Random();
ArrayList arr = new ArrayList();

for (int j = О; j < i; j++)


arr.Add(ran.Next(l, 100));
Wt-rzl Справочник С#

return arr;

puЫic static void Re!!DveEleпent(int i, iлt j, ref ArrayList arr)

arr.RemoveRange(i, j);

puЬlic static void AddElement(int i, ref ArrayList arr)


{
Random ran = new Random();
for (int j = О; j < i; j++)
arr.Add(ran.Next(l, 100));

puЫic static void Write(ArrayList arr)


{
foreach (int а in arr)
Console.Write("{0}\t", а);
Console.WriteLine("");

class Program

static void Main()

// Создадим новую коллекцию чисел длиной 4


ArrayList MyCol = IntCollection.NewCollec tion(4);
Console.OutputEncoding = Encoding.GetEncoding(866);
Console.WriteLine("Moя коллекция: ");
IntCollection.Write(MyCol);

// Добавим еще несколько элементов


IntCollection.AddElement(4, ref MyCol);
Console.WriteLine("Пocлe добавления элементов: ");
IntCollection.Write(MyCol);

// Удалим пару элементов


Глава 8. Коллекции и итераторы сп•
IntCollection.RemoveElement(3, 2, ref MyCol);
Console.WriteLine("Пocлe удаления элементов: ");
IntCollection.Write(MyCol);

// Отсортируем теперь коллекцию


MyCol.Sort();
Console.WriteLine("Пocлe сортировки: ");
IntCollection.Write(MyCol);

Вывод этой программы будет примерно таким (примерно - потому что


мы используем случайные элементы в диапазоне от 1 до 100 для фор­
мирования нашей коллекции, поэтому каждый запуск программы будет
давать другие результаты):
Моя коллекция:
97 11 26 96
После добавления элементов:
97 11 26 96 72 33 94 31
После удаления элементов:
97 11 26 33 94 31
После сортировки:
11 26 31 33 94 97

Изначально была создана коллекция с числами 97, 11, 26 и 96. Для созда­
ния коллекции мы использовали метод NewCollection(), которому пере­
дали число 4 - количество элементов в новой коллекции. Сами же эле­
менты коллекции создаются генератором случайных чисел:

Random ran = new Random();


ArrayList arr = new ArrayList();

for (int j = О; j < i; j++)


arr.Add(ran.Next(l, 100));
return arr;
•r-s Справочник С#

Добавление элементов осуществляется методом Add(). За один раз


в коллекцию можно добавить один элемент. Мы же создали метод
AddElement(), который добавляет в динамический массив указанное ко­
личество случайных элементов. Мы добавили 4 элемента. Далее мето­
дом RemoveRange() мы удаляем 2 элемента, начиная с третьего. Элемен­
ты 96 и 72 удалены.

В заключение этого примера мы сортируем массив по возрастанию.


Результат сортировки приведен выше.

8.5. Хеп1-таблица. Класс HashTahle


Информация в хеш-таблице хранится с помощью механизма, называ­
емого хешированием. Для создания хеш-таблицы используется класс
HashTaЫe. При хешировании для определения уникального значения,
называемого хеш-кодом, используется информационное содержимое
специального ключа. В результате хеш-код служит в качестве индекса,
по которому в таблице хранятся искомые данные, соответствующие за­
данному ключу.

Сам хеш-код недоступен программисту, а преобразование ключа в хеш­


код осуществляется автоматически. Преимущество данного способа
(хеширования) в том, что оно обеспечивает постоянство времени выпол­
нения операций поиска, извлечения и установки значений независимо от
размера массива данных.

В классе HashTaЬle реализованы следующие интерфейсы:

• ICloneaЫe
• ICollection
• IDictionary
• IDeserializationCallback
• IEnumeraЫe

Конструкторы класса HashTaЫe выглядят так:


Глава 8. Коллекции и итераторы 1-F:JMI
puЫic HashtaЬle()
puЬlic HashtaЬle(IDictionary d)
puЬlic HashtaЫe(int capacity)
puЬlic HashtaЫe(int capacity, float loadFactor)
Первая форма создает объект класса HashTaЬ\e по умолчанию. Во второй
форме объект типа HashTaЬ\e инициализируется элементами из коллек­
ции d. Третья форма создает объект, инициализируемый с учетом емко­
сти коллекции, заданной параметром capacity. Четвертая форма созда­
ет объект типа HashtaЬ\e, который инициализируется с учетом емкости
capacity и коэффициента заполнения loadFactor.

Параметр loadFactor может принимать значения от 0.1 до 1.0. Он опре­


деляет степень заполнения хеш-таблицы до увеличения ее размера. В
частности, таблица расширяется, если количество элементов оказывает­
ся больше емкости таблицы, умноженной на коэффициент заполнения.
Конструкторы, не принимающие параметр loadFactor, считают, что этот
параметр равен 1.0. В классе HashtaЬ\e определяется ряд собственных
методов, помимо тех, что уже объявлены в интерфейсах, которые в нем
реализуются. Рассмотрим некоторые часто используемые методы (табл.
8.7).

Таблица 8. 7. Некоторые часто используемые методы класса НashtaЫe

Метод Описание

Если в вызывающей коллекции типа HashTaЫe содержится


ContainsKey()
ключ, метод возвращает trne, в противном случае -false

Если в вызывающей коллекции типа HashTaЫe содержится


ContainsValue()
значение, метод возвращает trne, в противном случае -false
Возвращает для вызывающей коллекции типа HashtaЫe пере-
GetEnumerator()
числитель типа IDictionaryEnumerator
Возвращает синхронизированный вариант коллекции типа
Synchronized()
HashtaЫe, которая передана как параметр

Особую роль в классе HashTaЬ\e играют свойства Keys и Values, содер­


жащие ключи и значения, соответственно:
Справочник С#

puЫic virtual ICollection Keys { get; }


puЫic virtual ICollection Values { get;
Теперь давайте рассмотрим пример создания и использования хеш­
таблицы (лист. 8.2).

Листинг 8.2. Создание и использование хеш-таблицы


using System;
using System.Collections;

namespace ConsoleApplicationl

class Program

static void Main()

// Создаем хеш-таблицу
HashtaЫe ht = new HashtaЫe();

// Добавим несколько записей


ht.Add("den", "98765456546");
ht.Add("user", "45837685768");
ht.Add("root", "ddfdf3545");

// Получаем коллекцию ключей


ICollection keys = ht.Keys;

foreach (string s in keys)


Console.WriteLine(s + "· " + ht[s]);

Console.ReadLine();

Рис. 8.1. Пример работы с хеш-таблицей


Глава 8. Коллекции и итераторы

8.6. Создаем стек. Классы Stack и Stack<Т>


Наверное, нет ни одного программиста, который не бьш бы знаком со
стеком. Стек - это контейнер, работающий по принципу LIFO (Last /п
First Out), то есть последним зашел, первым вышел. Добавление элемен­
та в стек осуществляется методом Push(), а извлечение последнего до­
бавленного элемента - методом Рор().

В С# определен класс коллекции с именем Stack. Он-то и реализует стек.


Конструкторы этого класса определены так:
puЫic Stack()
puЫic Stack(int initialCapacity)
puЫic Stack(ICollection col)
Первая форма создает пустой стек, вторая форма - тоже создает пустой
стек, но задает его первоначальный размер (параметр initialCapacity).
Третья форма создает стек, содержащий элементы коллекции со/.
Емкость этого стека равна количеству элементов в коллекции со/.

В классе Stack определены различные методы. С методами Рор() и Push()


вы уже знакомы. Но поговорим о них подробнее. Метод Push() помещает
элемент на вершину стека. А для того чтобы извлечь и удалить объект из
вершины стека, вызывается метод Рор(). Если же объект требуется толь­
ко извлечь, но не удалить из вершины стека, то вызывается метод Peek().
А если вызвать методы Рор() или Peek(), когда вызывающий стек пуст, то
сгенерируется исключение InvalidOperationException. Кроме описанных
методов класс Stack содержит свойство Count и метод Contains(). Свой­
ство Count возвращает количество элементов в стеке, а метод Contains()
проверяет наличие элемента в стеке и возвращает true, если элемент на­
ходится в стеке.

Класс Stack<Т> является обобщенным вариантом класса Stack. В


этом классе реализуются интерфейсы Collection, IEnumeraЫe и
IEnumeraЫe<Т>. Кроме того, в классе Stack<Т> непосредственно реа­
лизуются методы Clear(), Contains() и СоруТо(), определенные в интер­
фейсе ICollection<Т>. А методы Add() и Remove() в этом классе не под­
держиваются, как, впрочем, и свойство IsReadOnly. Коллекция класса
MtWIJ Справочник С#

Stack<Т> имеет динамический характер, расширяясь по мере необходи­


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

В листинге 8.3 содержится пример работы со стеком.

Листинг 8.3. Пример использования класса Stack


using System;
using System.Collections.Generic;

namespace ConsoleApplicationl

class Program

static void Main()

var MyStack = new Stack<char>();


MyStack.Push('А');
MyStack.Push('B');
MyStack.Push('C');

Console.OutputEncoding = Encoding.GetEncoding(866);
Console.WriteLine("Coдepжимoe стека: ");

foreach (char s in MyStack)


Console.Write(s);
Console.WriteLine("\n");

while (MyStack.Count > О)


(
Console.WriteLine(MyStack.Pop());

if (MyStack.Count == О)
Console.WriteLine("Cтeк пуст!");
Глава 8. Коллекции и итераторы C<JM
Вывод программы будет таким:
Му Stack contains:
СВА
с
в
А
Stack is empty

8. 7. Очередь. Классы Queue и Qиеие<Т>


Очередь - это контейнер, работающий по принципу FIFO (First In First
Out), то есть первым вошел, первым вышел. Элемент, вставленный в
очередь первым, первым же и читается. Примером очереди в програм­
мировании может послужить любая очередь в реальном мире. Если вы
пришли первым и заняли очередь, то первым и будете обслужены.

Очередь реализуется с помощью классов Queue из пространства имен


System.Collections и Queue<Т> из пространства имен System.Collections.
Generic.

Конструкторы класса Queue выглядят так:


puЫic Queue()
puЫic Queue (int capacity)
puЫic Queue (int capacity, float growFactor)
puЫic Queue (ICollection col)
Как и в случае со стеком, первая форма создает пустую очередь, вторая
- тоже пустую, но задает ее первоначальный размер. Третья форма
позволяет указать начальную емкость очереди и фактор роста (допусти­
мые значения от 1.0 до 10.0). Четвертая форма создает очередь из эле­
ментов коллекции со/. Все конструкторы, не позволяющие задать пара­
метр growFactor, считают, что фактор роста равен 2.0.

Класс Queue<Т> содержит такие конструкторы:


puЫic Queue()
puЫic Queue(int capacity)
puЬlic Queue(IEnumeraЬle<T> collection)
•10 Справочник С#

Первая форма создает пустую очередь, емкость очереди выбирается по


умолчанию. Вторая форма позволяет задать емкость создаваемой очере­
ди. Третья форма создает очередь, содержащую элементы заданной кол­
лекции collection.

Члены класса Queue представлены в таблице 8.8.

Таблица 8.8. Члены класса Queue

Член класса Описание


Свойство Count возвращает количество элементов
Count
очереди
Enqueue() Метод добавляет элемент в конец очереди

Читает и удаляет элемент из головы очереди. Если на


Dequeue() момент вызова метода очередь пуста, генерируется
исключение InvalidOperationException

Peek() Читает элемент из головы очереди, но не удаляет его

Изменяет емкость очереди. Метод Dequeue() удаля-


ет элемент из очереди, но не изменяет ее емкости.
TrimExcess()
TrimExcess() позволяет избавиться от пустых элемен-
тов в начале очереди

Пример работы с очередью приведен в листинге 8.4.

Листинг 8.4. Работаем с очередью


using System;
using System.Collections.Generic;

namespace ConsoleApplicationl

class Program

static void Main()


Глава 8. Коллекции и итераторы

Queue<int> MyQueue = new Queue<int>();


Random ran new Random();

for (int i О; i < 20; i++)


MyQueue.Enqueue(ran.Next(l, 100));

Console.WriteLine("Moя очередь:");
foreach (int i in MyQueue)
Console.Write("{0} ", i);

Console.ReadLine();

8.8. Связный список. Класс LinkedList<Т>


Рассмотрим рис. 8.2, на котором изображен типичный двухсвязный спи­
сок - структура, часто использующаяся в программировании. У каждо­
го элемента списка есть два указателя - на следующий (Next) и преды­
дущий элемент (Prev). Если следующего (или предъщущего) элемента
нет, то указатель содержит значение пи//. Кроме указателей каждый эле­
мент содержит значение (value).

-
;� 1 1- 1
Newt Next
1

Vвlue

Prev Prev 1 PreY

Рис. 8.2. Связный список

Если в языке С нужно было создавать структуру связного списка вруч­


ную, то сейчас же достаточно использовать уже готовый класс LinkedList
и его методы.
Справочник С#

Преимущество связных списков в том, что операция вставки элемента в


середину выполняется очень быстро. При этом только ссьmки Next (сле­
дующий) предыдущего элемента и Previous (предыдущий) следующего
элемента должны быть изменены так, чтобы указывать на вставляемый
элемент. В классе List<T> при вставке нового элемента все последующие
должны быть сдвинуты. К недостаткам связных списков можно отнести
довольно медленный поиск элементов, находящихся в центре списка.

Класс LinkedList<T> содержит члены First (используется для доступа к


первому элементу списка), Last (доступ к последнему элементу), а также
методы для работы с элементами. В классе LinkedList<Т> реализуются
интерфейсы ICollection, ICollection<T>, IEnumeraЫe, IEnumeraЫe<T>,
ISerializaЫe и IDeserializationCallback. В двух последних интерфейсах
поддерживается сериализация списка. В классе LinkedList<Т> определя­
ются два конструктора:
puЬlic LinkedList()
puЫic LinkedList(IEnumeraЬle<T> collection)

Первый конструктор создает пустой связный список, второй - список,


который инициализируется элементами из заданной коллекции.

Методы класса LinkedList<Т> представлены в таблице 8.9.

Таблица 8.9. Методы класса LinkedList<'Г>

Метод Описание

Добавляют элемент, соответственно, в начало


AddFirst(), AddLast()
и конец списка
Добавляет в список узел непосредственно
после указанного узла. Указываемый узел не
AddAfter()
должен бьпь пустым (null). Метод возвращает
ссьшку на узел, содержащий значение

Аналогичен AddAfter(), но добавляет узел до


AddВefore()
указанного узла
Глава 8. Коллекции и итераторы

Возвращает ссылку на первый узел в списке,


имеющий передаваемое значение. Если иско-
Find()
мое значение отсутствует в списке, то возвра-
щается null
Используется для удаления из списка перво-
го узла, который содержит переданное методу
Remove() значение value. Возвращает true, если узел со
значением был обнаружен и удален, в против-
ном случае возвращается значение false

Как обычно, настало время для примера. В листинге 8.5 приводится при­
мер создания связного списка и вывода его в двух направлениях - в
прямом и обратном. Обратите внимание, как построены циклыfоr. В ка­
честве начального значения при прямом обходе мы используем First, а за­
тем присваиваем узлу значение Next (идем вперед). При обратном обходе
мы начинаем с Last и узлу при каждой итерации присваиваем значение
Previous, то есть идем назад.

Листинг 8.5. Пример прямого и обратного обхода


связного списка
using System;
using System.Collections.Generic;

namespace ConsoleApplicationl

class Program

static void Main()

// Создадим связный список


LinkedList<string> 1 = new LinkedList<string>();

// Добавим несколько элементов


l.AddFirst("Apple");
l.AddFirst("Banana");
l.AddFirst("Pear");
w+ro Справочник С#

l.AddFirst("Orange");
l.AddFirst("Peach");

// Элементы в прямом порядке


LinkedListN ode<string> node;
Console.WriteLine("Direct Order: ");
for (node = l.First; node != null; node node.Next)
Console.Write(node.Value + "\ t");

Console.WriteLine( );
// Элементы в обратном порядке
Consol e.WriteLine("Rev erse order: ");
for (node = l.Last; node != null; node = node.Previous)
Console.Write(node.Value + "\t");

Console.ReadLine( );

Рис. 8.3. Пример прямого и обратного обхода связного списка

8.9. Сортированный список. Класс


SortedList<TKey, TValue>
Еще один вариант списка - сортированный по ключу список. Для его
создания можно воспользоваться классом SortedList<TKey, TValue>.
Данный класс сортирует элементы на основе значения ключа. При этом
вы можете использовать любые типы значения и ключа.

В классе SortedList<ТKey, ТValue> реализуются следующие интерфейсы:


Глава 8. Коллекции и итераторы

• IDictionary
• IDictionary<TKey, TValue>
• ICollection
• ICollection<KeyValuePair<TKey, TValue>>
• IEnumeraЫe
• IEnumeraЫe<KeyValuePair<TKey, TValue>

Размер сортированного списка увеличивается автоматически - по


мере необходимости. Класс SortedList<ТKey, TVa\ue> похож на класс
SortedDictionary<TKey, TValue>, но он более эффективно расходует па­
мять.

Рассмотрим конструкторы класса SortedList<TKey, TValue>:

puЫic SortedList()
puЫic SortedList(IDictionary<TKey, TValue> dictionary)
puЫic SortedList(int capacity)
puЫic SortedList(IComparer<TK> comparer)

Первая форма создает пустой список. Вторая - создает отсортирован­


ный список, элементы берутся из словаря dictionaty. Третья форма с
помощью параметра capacity задает емкость отсортированного списка.
Последняя форма позволяет указать способ сравнения объектов, которые
находятся в списке (для этого используется параметр comparer).

Методы и прочие члены класса SortedList<TKey, TValue> приведены в


таблице 8.10.

Таблица 8.10. Методы класса SortedList<TKey, TValue>

Член класса Описание

Добавляет в список пару «ключ-значение>>. Если


Add() ключ уже находится в списке, то генерируется
исключение ArgumentException
Справочник С#

Возвращает значение true, если вызывающий


ContainsKey() список содержит объект key в качестве ключа, в
противном случае -false
Возвращает значение true, если вызывающий
ContainsValue() список содержит значение value; в противном
случае -false

Возвращает перечислитель для вызывающего


GetEnumerator()
словаря

Возвращает индекс ключа или первого вхождения


lndexOfКey(), IndexONalue() значения в вызывающем списке. Если ключ или
значение не найдены, возвращается значение -1
Удаляет из списка пару «ключ-значение)) по ука-
Remove() занному ключу key. Если удаление удачно, воз-
вращает true, если нет -false
Сокращает избыточную емкость вызывающей
TrirnExcess()
коллекции в виде отсортированного списка
Capacity Получает или устанавливает емкость списка
Comparer Задает метод сравнения для вызывающего списка

Keys Коллекция ключей

Values Коллекция значений

Пример программы, работающей с отсортированным списком, приведен


в листинге 8.6.

Листинг 8.6. Работа с отсортированным списком


using System;
using System.Collections.Generic;

namespace ConsoleApplicationl

class Program
Глава 8. Коллекции и итераторы C-&JM•i
static void Main()

// Создадим коллекцию сортированного списка

SortedLi st<string, string> Carlnfo = new SortedList<string,


string>();

// Добавление элементов
Carinfo.Add("Audi ", "2015");
Carinfo.Add("Toyota", "2016");
Carinfo.Add("BMW", "2015");
Carinfo.Add("Renault", "2014");
Carinfo.Add("L exus ", "2016");

// Коллекция ключей
ICo llection<string> keys = Carinfo.Keys;

// Теперь используем ключи для получения значений


foreach (string s in keys)
Console.WriteLine("Мapкa: {О}, Год: (11", s, Carlnfo[s));

Con sole.ReadLine();

8.10. Словарь. Класс Dictionary<TKey,


TValue>
Словарь (dictionary) - это сложная структура данных, обеспечиваю­
щая доступ к элементам по ключу. Основная особенность словаря -
быстрый поиск элемента по ключу. Также вы можете быстро добавлять и
удалять элементы, подобно тому, как вы это делаете в списке List<T>, но
гораздо эффективнее, поскольку нет необходимости смещения последу­
ющи х элементов в памяти.
Тип, используемый в качестве ключа словаря, должен переопределять
метод GetHashCode() класса Object. Всякий раз, когда класс словаря дол­
жен найти местоположение элемента, он вызывает метод GetHashCode().
RMIU Справочник С#

К методу GetHashCode() есть следующие требования:


• Не должен генерировать исключений;
• Должен выполняться быстро, не требуя значительных вычисли­
тельных затрат;
• Разные объекты могут возвращать одно и то же значение;
• Один и тот же объект должен возвращать всегда одно и то же зна­
чение;
• Должен использовать как минимум одно поле экземпляра;
• Хеш-код не должен изменяться на протяжении времени существо­
вания объекта.

Помимо реализации GetHashCode() тип ключа также должен реализовы­


вать метод IEquataЫe<Т>.Equals() либо переопределять метод Equals()
класса Object. Поскольку разные объекты ключа могут возвращать один
и тот же хеш-код, метод Equals() используется при сравнении ключей
словаря.

Конструкторы класса Dictionary<TKey, TValue> выглядят так:


puЫic Dictionary()
puЫic Dictionary(IDictionary<TKey, TValue> dictionary)
puЫic Dictionary(int capacity)
Первый конструктор создает пустой словарь, его емкость выбирается по
умолчанию. Второй конструктор создает словарь с указанным количе­
ством элементов dictionary. Третий конструктор позволяет указать ем­
кость словаря (параметр capacity).

В классе Dictionary<ТКey, ТValue> реализуются следующие интерфейсы:


• IDictionary
• IDictionary<TKey, TValue>
• ICollection
• ICollection<KeyValuePair<TKey, TValue>>
Глава 8. Коллекции и итераторы C-t-:J•
• IEnurneraЫe
• IEnurneraЫe<KeyValuePair<TKey, TValue>>
• ISerializaЫe
• IDeserializationCallback

Члены класса Dictionary<TKey, ТValue> приведены в таблице 8.11.

Таблица 8.11. Члены класса Dictionary<TKey, TValue>

Член класса Описание


Добавляет в словарь пару «ключ-значение)). Пара определя-
ется параметрами key и va/ue. Если ключ key уже находится в
Add()
словаре, то генерируется исключение ArgumentException. Зна-
чение самого же ключа не изменяется

Возвращает true, если вызывающий словарь содержит объект


ContainsKey()
key в качестве ключа, иначе - возвращает fa/se

Возвращает логическое значение true, если вызывающий ело-


ContainsValue()
варь содержит значение va/ue; в противном случае -fa/se

Удаляет ключ key из словаря. В случае успеха возвращается


Remove()
true, иначе -fa/se (если ключ отсутствует в словаре)

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

Keys Получает коллекцию ключей

Values Получает коллекцию значений

В классе Dictionary<TKey, TValue> реализуется приведенный ниже ин­


дексатор, определенный в интерфейсе IDictionary<TKey, ТValue>:
puЫic TValue this[TKey key] { get; set; }

Данный индексатор служит для получения и установки значения


элемента коллекции, а также для добавления в коллекцию нового
1-ril Справочник С#

элемента. В качестве индекса в данном случае служит ключ элемента,


а не сам индекс. При перечислении коллекции типа Dictionary<TKey,
TValue> из нее возвращаются пары «ключ-значение» в форме структуры
KeyValuePair<TKey, TValue>. В этой структуре определяются два поля:

puЫic ТКеу Кеу;


puЫic TValue Value;

Пример программы, использующей словарь, приведен в листинге 8.7.

Листинг 8.7. Работа со словарем


using System;
using System.Collections.Generic;

namespace ConsoleApplicationl

c lass Carinfo

// Реализуем словарь
puЫic static D ictionary<int , string> MyDic(in t i)

Dictionary<int, string> dic = new Dictionary<int,string>();

Console.WriteLi ne("Bвeдитe марку машины: \n");

stri ng s;

for (int j = О; j < i; j++)

Console.Write("Mapкa{OI --> ",j);


s = Console.Read Line();
d ic.Add{j, s);

return dic;

c lass Program
Глава 8. Коллекции и итераторы

static void Main()

Console.OutputEncoding = Encoding.GetEncoding(866);
Console.Write("Cкoлькo машин добавить? ");
try

int i = int.Parse(Console.Read Line());


Dictionary<int , string> dic = Carinfo.My Dic(i);

// Получаем ключи
ICollection<int> keys = dic.Keys;

Console.WriteLine("Cлoвapь: ");
foreach (int j in keys)
Console.WriteLine("ID -> {О} Мэрка -> {l}",j, dic[j));

catch (FormatException)

Console.WriteLine("Oшибкa! Исключение формата");

Console.ReadLine();

8.11. Сортированный словарь: класс


SortedDictionary<TKey, TValue>
Отсортированный вариант словаря представляет собой дерево бинарно­
го поиска, в котором все элементы отсортированы по ключу. Класс на­
зывается SortedDictionary<TKey, Tvalue>.

Ключ, точнее его тип, должен реализовать интерфейс IComparaЫe<TKey>.


Если тип ключа не относится к сортируемым, вы можете реализовать
IComparer<TKey> и указать его в качестве аргумента конструктора
сортированного словаря.
10 Справочник С#

Классы Sorted.Dictionary<TKey, Tvalue> и SortedList<TKey, TValue> до­


вольно похожи. Но SortedList<TKey, TValue> реализован в виде списка,
основанного на массиве, а Sorted.Dictionary<TKey, Tvalue> реализован
как словарь. Именно поэтому эти классы обладают разными характери­
стиками, а именно SortedList<TKey, TValue> использует меньше памя­
ти, чем SortedDictionary<TKey, TValue>. Также Sorted.Dictionary<TKey,
TValue> быстрее вставляет и удаляет элементы.

В классе Sorted.Dictionary<TKey, ТValue> предоставляются также следу­


ющие конструкторы:
puЫic SortedDictionary()
puЬlic SortedDictionary(IDictionary<TKey, TValue > d ictionary)
p uЫic SortedDictionary(IComparer<TKey > comparer )
puЬlic SortedDictionary(IDictionary<ТКey, 'IValue> dictionary, ICarparer<ТКey>
crnparer)

Первый конструктор создает пустой словарь, второй - словарь с указан­


ным количеством элементов dictionary. Третий конструктор позволяет
указывать с помощью параметра comparer типа IComparer способ срав­
нения, используемый для сортировки. Четвертая форма конструктора по­
зволяет инициализировать словарь, помимо указания способа сравнения.
В классе Sorted.Dictionary<TKey, TValue> реализуются следующие ин­
терфейсы:

• IDictionary
• IDictionary<ТKey, TValue>
• ICollection
• ICollection<KeyValuePair<TKey, TValue>>
• IEnumeraЫe
• IEnumeraЫe<KeyValuePair<TKey, TValue>>

Методы отсортированного словаря аналогичны методам обычного, а


именно поддерживаются методы Add(), ContainsKey(), ContainsValue(),
Remove().
Глава 8. Коллекции и итераторы

В классе SortedDictionary<ТKey, TValue> реализуется приведенный ниже


индексатор, определенный в интерфейсе IDictionary<ТKey, ТValue>:

puЫic TValue this[TKey key] ( get; set; }

Пример использования отсортированного словаря приведен в листинге


8.8.

Листинr 8.8. Использование отсортированноrо словаря


using System;
using System.Collections.Generic;

namespace ConsoleApplicationl

class Carinfo

puЫic static SortedDictionary<string, string> MyDic(int i)

SortedDictionary<string, string> dic = new


SortedDictionary<string,string>();

string s2, sl;

for (int j = О; j < i; j++)

Console. Write("Ключ: ");


sl = Console.ReadLine();
Console.WriteLine("Mapкa: ");
Console.Write("Mapкa{O} --> ",j);
s2 = Console.ReadLine();
dic.Add(sl, s2);

return dic;

class Program
WCW/J Справочник С#

static void Main()


Console.OutputEncoding = Encoding.GetEncoding(866);
Console.Write("Cкoлькo машин добавить? ");
try

int i = int.Parse(Console.ReadLine());

SortedDictionary<string, string> d = Carinfo.MyDic(i);

// Получить коллекцию ключей


ICollection <string> keys = d.Keys;

Console.WriteLine("Oтcopтиpoвaнный словарь: ");


foreach (string j in keys)
Console.WriteLine("ID -> {01 Марка -> {11",j,d[j]);

catch (FormatException)

Console.WriteLine("Oшибкa!");

Console.ReadLine();

8.12. Множества: классы HashSet<'J'> и


SortedSet<'J'>
Настало время поговорить о множествах (set). В составе .NET имеются
два класса - HashSet<Т> и SortedSet<Т>. Оба они реализуют интерфейс
ISet<Т>. Класс HashSet<Т> содержит неупорядоченный список разли­
чающихся элементов, а в SortedSet<Т> элементы упорядочены. То есть
первый класс - зто просто множество, а второй - отсортированное
множество.

Интерфейс ISet<Т> предоставляет методы, используемые для выполне­


ния основных операций над множеством.
Глава 8. Коллекции и итераторы

В классе HashSet<T> определены следующие конструкторы:


puЬlic HashSet ()
puЫic HashSet(IEnurneraЬle<T> collection)
puЬlic HashSet(IEqualityCompare comparer)
puЬlic HashSet(IEnurneraЬle<T> collection, IEqualityCompare comparer)

Первая форма создает пустое множество, вторая форма - создает мно­


жество, состоящее из элементов коллекции collection. Третий конструк­
тор позволяет указывать способ сравнения, указав параметр comparer.
Последняя форма создает множество, состоящее из элементов заданной
коллекции collection, и использует метод сравнения comparer.

Конструкторы класса SortedSet<T> такие же:


puЫic SortedSet()
puЫic SortedSet(IEnumeraЬle<T> collection)
puЫic SortedSet(IComparer comparer)
puЫic SortedSet(IEnumeraЫe<T> collection, IComparer comparer)

В SortedSet<T>, помимо прочих свойств, определены дополнительные


свойства:
puЫic IComparer<T> Comparer { get; }
puЫic Т Мах get;
puЫic Т Min { get;
Пример использования коллекции:

SortedSet<int> ss = new SortedSet<int>();


ss.Add(7);
ss.Add(б);
ss.Add( 9);
ss.Add(l);

foreach (int j in ss)


Console.Write(j + " ");
Для объединения множеств используется метод UnionWith(), в качестве
параметра ему нужно передать имя второго множества. Для вычитания
множеств используется метод ExceptWith(). Оставить в двух множествах
IU Справочник С#

только уникальные элементы (чтобы не было пересечения этих мно­


жеств) можно методом SymmetricExceptWith(). Рассмотрим, как работа­
ют эти методы:
// Создадим второе множество
SortedSet<int> sb = new SortedSet<int>();

sb.Add(7);
sb.Add(2);
sb.Add(З);
sb.Add(l);

// Пересечение
ss.ExceptsWith(sb);
foreach (int j in ss)
Console.Write(j + " ");

// Объединение
ss.UnionWith(sb);
foreach (int j in ss)
Console.Write(j + " ");

// Исключаем одинаковые элементы


ss.SymrnetricExceptWith(sb);
foreach (int j in ss)
Console.Write(j + " ");

8.13. Реализация интерфейса /Comparahle


Представим, что вы создали некий собственный класс и создали коллек­
цию, содержащую объекты этого класса. Что, если вам понадобилось
отсортировать эту коллекцию? Для сортировки коллекции, заполнен­
ной объектами пользовательского типа, нужно реализовать интерфейс
IComparaЫe, то есть указать компилятору, как сравнивать объекты.

Если требуется отсортировать объекты, хранящиеся в необобщенной


коллекции, то для этой цели придется реализовать необобщенный вари­
ант интерфейса IComparaЫe. В этом варианте данного интерфейса опре-
Глава 8. Коллекции и итераторы IOMI
деляется только один метод, CompareTo(), который определяет порядок
выполнения самого сравнения. Ниже приведена общая форма объявле­
ния метода CornpareTo():

int CompareTo(object obj)

Данный метод должен возвращать положительное значение, если значе­


ние вызывающего объекта больше, чем у объекта, с которым производит­
ся сравнение. В противном случае, если значение вызывающего объекта
меньше, - возвращается отрицательное значение. Если объекты равны,
то возвращается О. Представим, что в классе есть поле price, по которому
мы и будем сравнивать объекты:
puЬlic int CompareTo(SomeClass obj)

if (this.price > obj.price)


return 1;
if (this.price < obj.price)
return -1;
else
return О;

Если нужно отсортировать объекты, хранящиеся в обобщенной коллек­


ции, то для этой цели придется реализовать обобщенный вариант интер­
фейса ICornparaЫe<Т>. В этом варианте интерфейса ICornparaЫe опре­
деляется приведенная ниже обобщенная форма метода CornpareTo():

int CompareTo(T other)

8.14. Перечислители
К элементам коллекции иногда приходится обращаться циклически -
ранее приводились примеры циклаfоrеасh как один из способов цикли­
чески обратиться ко всем элементам коллекции. Второй способ -
использование перечислителя.
Перечислитель - это объект, реализующий необобщенный интерфейс
/Enumerator или обобщенный /Enumerator< Т>.
RMIIOJ Справочник С#

В интерфейсе IEnumerator определяется одно свойство, Current. Такое


же свойство есть и в обобщенной версии интерфейса. Свойства Current
определяются так:

object Current get;


object Current get;

В обеих формах свойства Current получается текущий перечисляемый


элемент коллекции. Свойство Current доступно только для чтения.

Доступные методы (как в необобщенной, так и в обобщенной версиях):

• MoveNext() - смещает текущее положение перечислителя к сле­


дующему элементу коллекции. Возвращает true, если следующий
элемент коллекции доступен, илиfаlsе, если был достигнут конец.
• Reset() - после вызова метода перечислитель перемещается в на­
чало коллекции.

Прежде чем получить доступ к коллекции с помощью перечислителя,


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

Рассмотрим, как использовать перечислитель:


List<int> MyList = new List<int>();
Random ran = new Random();

// Заполняем список случайными значениями от 1 до 100


for (int i = О; i < 20; i++)
MyList.Add(ran.Next(l, 100));

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


IEnumerator<int> enum = MyList.GetEnumerator();
while (enum.MoveNext())
Console.Write(num.Current + " ");
Глава 8. Коллекции и итераторы

Если нужно вывести список повторно, нужно сбросить перечислитель


методом Reset и снова вывести список циклом while:

enum.Reset();

8.15. РеаJiизация интерфейсов /Enumerahle и


/Enumerator
Представим, что вам нужно создать класс, объекты которого будут пе­
ребираться в цикле foreach, в классе нужно реализовать интерфейсы
IEnumerator и IEnumeraЫe. То есть для того чтобы обратиться к объекту
определяемого пользователем класса в цикле foreach, необходимо ре­
ализовать интерфейсы IEnumerator и IEnumeraЫe в их обобщенной
или необобщенной форме. Реализовать данные интерфейсы довольно
просто, что и показано в листинге 8.9.

Листинг 8.9. Реализация интерфейсов IEnumerator и


IEnumeraЫe
using System;
using System.Collections;

namespace ConsoleApplicationl

class MyBytes : IEnumeraЫe, IEnumerator

byte [ ] bytes = { 1, 3, 5, 7 } ;
byte index = -1;

// Интерфейс IEnumeraЬle
puЬlic IEnumerator GetEnumerator()

return this;

// Интерфейс IEnumerator
puЫic bool MoveNext()
{
if (index == bytes.Length - 1)
bliMl-0 Справочник С#

Reset();
return false;

index++;
return true;

// Метод, сбрасывающий перечислитель


puЫic void Reset()
{
index = -1;

puЫic object Current


{
get
{
return bytes[index];

class Program

static void Main()

MyBytes mЬ = new MyBytes();

foreach (int j in mЬ)


Console.Write(j+» «);
Глава 8. Коллекции и итераторы t№JMI
8.16. Итераторы. Ключевое слово yield
В прошлом разделе мы реализовали интерфейсы IEnumerator и
IEnumeraЫe. Как было показано, это совсем несложно. Но еще проще
воспользоваться итератором.

Итератор - это метод, оператор или аксессор, который по очереди


возвращает члены последовательности объектов - с ее начала до кон­
ца. После того, как вы реализуете итератор, вы сможете обращаться к
объектам определяемого пользователем класса в цикле foreach.

Создать итератор в С# можно с помощью ключевого слова yield. Данное


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

Синтаксис итератора следующий:

puЬlic IEnurneraЬle имя_итератора(список_параметров)


// . . .
yield return obj;

Здесь имя_итератора - конкретное имя метода, список_параметров -


параметры, передаваемые методу итератора, obj - следующий объект, воз­
вращаемый итератором. Как только именованный итератор будет создан,
его можно будет использовать везде, где он нужен, например, в цикле
Joreach.

Пример итератора:

class MyClass {
int limit = О;
puЫic MyClass(int limit) { this.limit limi t; }
Справочник С#

puЬlic IEnurneraЬle<int> CountFrorn(int start)

for (int i = start; i <= lirnit; i++)


yield return i;

Для выхода из итератора по какому-нибудь условию может использо­


ваться такая конструкция:

if (условие) yield break;


ГЛАВА 9.

РАБОТА С ДАТОИ И
ВРЕМЕНЕМ
HfMl-0 Справочник С#

9.1. Структура DateTime


Для работы с датами и временем в .NET предназначена структура Date­
Time. Она представляет дату и время от 00:00:00 1 января ООО 1 года до
23:59:59 31 декабря 9999 года. Для создания нового объекта DateTime
также можно использовать конструктор. Пустой конструктор создает на­
чальную дату:
DateTime dateTime = new DateTime();
Console.WriteLine( dateTime); // 01.01.0001 0:00:00
То есть мы получим минимально возможное значение, которое также
можно получить следующим образом:

Console.WriteLine(DateTime.MinValue);

Чтобы задать конкретную дату, нужно использовать один из конструкто­


ров, принимающих параметры:
DateTime datel = new DateTime(2022, 7, 20); // год - месяц - день
Console.WriteLine(datel); // 20.07.2022 0:00:00
Установка времени:
// год - месяц - день - час - минута - секунда
DateTime datel = new DateTime(2022, 6, 21, 19, 30, 35);
Console.WriteLine(datel); // 21.06.2022 19:30:35

Если необходимо получить текущую время и дату, то можно использо­


вать ряд свойств DateTime:
Console.WriteLine(DateTime.Now);
Console.WriteLine(DateTime.UtcNow);
Console.WriteLine(DateTime.Today);
Глава 9. Работа с датой и временем

Вывод будет таким:


06.08.2022 12:43:33
06.08.2022 9:43:33
06.08.2022 0:00:00
Свойство DateTime.Now берет текущую дату и время компьютера,
DateTime.UtcNow - дату и время относительно времени по Гринвичу
(GMT) и DateTime.Today - только текущую дату. При работе с датами
надо учитывать, что по умолчанию для представления дат применяется
григорианский календарь. Но что будет, если мы, например, захотим по­
лучить день недели для 7 июня 2022 года:

DateTime someDate = new DateTime(2022, 6, 7);


Console.WriteLine(someDate.DayOfWeek);

Консоль высветит значение Tuesday, то есть вторник .

9.2. Операции с DateTime


Основные операции со структурой DateTime связаны со сложением или
вычитанием дат. Например, надо к некоторой дате прибавить или, наобо­
рот, отнять несколько дней.

Для добавления дат используется ряд методов:

• Add(TimeSpan value)- добавляет к дате значение TimeSpan;


• AddDay s(douЫe value) - добавляет к текущей дате несколько
дней;
• AddНours(douЫe value)- добавляет к текущей дэ:rе несколько часов;
• AddMinutes( douЫe value) - добавляет к текущей дате несколько
минут;
• AddМonths(int value) - добавляет к текущей дате несколько ме­
сяцев;

• AddYears(int value)- добавляет к текущей дате несколько лет.


Справочник С#

Например, добавим к некоторой дате 3 часа:


DateTime datel = new DateTime(2022, 7, Об, 18, 30, 25);
Console.WriteLine(datel.AddHours(3)); // 06.07.2022 21:30:25
Для вычитания дат используется метод Subtract(DateTime date):

DateTirne datel = new DateTirne(2022, 7, 20, 18, 30, 25);


DateTirne date2 = new DateTirne(2022, 7, 20, 15, 30, 25);
Console.WriteLine(datel.Subtract(date2)); // 03:00:00

Здесь даты различаются на три часа, поэтому результатом будет дата


«03:00:00)). Метод Subtract не имеет возможностей для отдельного вы­
читания дней, часов и так далее. Но это и не надо, так как мы можем
передавать в метод AddDays() и другие методы добавления отрицатель­
ных значений:

// Вычтем три часа


DateTirne datel = new DateTime(2022, 7, 20, 18, 30, 25);
Console.WriteLine(datel.AddHours(-3)); // 20.07.2022 15:30:25

Кроме операций сложения и вычитания еще есть ряд методов формати­


рования дат:

DateTime datel = new DateTirne(2022, 6, 20, 18, 30, 25);


Console.WriteLine(datel.ToLocalTime()); // 20.06.2022 21:30:25
Console.WriteLine(datel.ToUniversalTime()); // 20.06.2022 15:30:25
Console.WriteLine(datel.ToLongDateString()); // 20 июня 2022 г.
Console.WriteLine(datel.ToShortDateString()); // 20.07.2022
Console.WriteLine(datel.ToLongTimeString()); // 18:30:25
Console.WriteLine(datel.ToShortTimeString()); // 18:30

Метод ToLocalТime() преобразует время UTC в локальное время, добав­


ляя смещение относительно времени по Гринвичу. Метод ToUniversal­
Time(), наоборот, преобразует локальное время во время UTC, то есть
вычитает смещение относительно времени по Гринвичу. Остальные ме­
тоды преобразуют дату к определенному формату.
Глава 9. Работа с датой и временем 18':IMI
9.3. Форматирование даты и времени
Для форматирования вывода дат и времени применяется ряд строковых
форматов:

Таблица 9.1. Модификаторы формата времени и даты

Модификатор Описание

D Полный формат даты. Например, 17 июля 2022 г.

d Краткий формат даты. Например, 17.07.2022

Полный формат даты и времени. Например, 17 июля 2022 г.


F
17:04:43
Полный формат даты и краткий формат времени. Например,
f
17 ИЮЛЯ 2022 Г. 17:04
Краткий формат даты и полный формат времени. Например,
G
17.07.2022 17:04:43

g Краткий формат даты и времени. Например, 17.07.2022 17:04

М.т Формат дней месяца. Например, 17 июля

Формат обратного преобразования даты и времени. Вывод


даты и времени в соответствии со стандартом ISO 8601 в фор-
о. о мате «yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ftППfzzz». Например,
2022-07-17Т17:04:43 .4092892+03 :00

R. r Время по Гринвичу. Например, Fri, 17 Ju\ 2022 17:04:43 GMT

Сортируемый формат даты и времени. Например, 2022-07-


17Т\ 7:04:43

т Полный формат времени. Например, 17:04:43

t Краткий формат времени. Например, 17:04

Полный универсальный полный формат даты и времени. На-


и пример, 17 июля 2022 г. 17:04:43
Справочник С#

Краткий универсальный полный формат даты и времени.


Например, 2022-07-17 l 7:04:43Z

У, у Формат года. Например, Июль 2022

Чтобы попробовать данные модификаторы в действии, выполните сле­


дующий код:
DateTime now = DateTime.Now;

Console.WriteLine($"D: {now.ToString("D") )");


Console.WriteLine($"d: {now.ToString("d"))");
Console.WriteLine($"F: {now.ToString("F"))");
Console.WriteLine($"f: {now:f}");
Console.WriteLine($"G: {now:G}");
Console.WriteLine($"g: {now:g}");
Console.WriteLine($"M: {now:M)");
Console.WriteLine($"O: {now:O}");
Console.WriteLine($"o: {now:o}");
Console.WriteLine($"R: {now:R}");
Console.WriteLine($"s: {now:s)");
Console.WriteLine($"T: {now:T)");
Console.WriteLine($"t: {now:t)");
Console.WriteLine($"U: {now:U)");
Console.WriteLine($"u: {now:u)");
Console.WriteLine($"Y: {now:Y)");

Также можно создавать собственные форматы, например:

DateTime now = DateTime.Now;


Console.WriteLine(now.ToString("hh:mm:ss"));
Console.WriteLine(now.ToString("dd.ММ.yyyy"));

Вывод:
07:46:38
08.06.2022
Справочник С#

До этого мы рассматривали в нашем справочнике примеры, обрабаты­


вающие какие-то данные. Как правило, сами данные или вводил пользо­
ватель, или же они были заданы в приложении. Но в реальном мире все
немного сложнее - приложению нужно получать данные извне, а после
их обработки часто нужно не просто вывести, но и сохранить резуль­
тат. Именно поэтому просто необходима глава, объясняющая файловый
ввод/вывод в С#. Кроме того, мы рассмотрим сериализацию объектов,
то есть приведение объекта в форму, в которой можно его сохранить в
обычном файле, а затем загрузить по мере необходимости.

В .NET существует очень много классов и методов, связанных с файло­


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

В этой главе, как было уже отмечено, рассматриваются две темы. Первая
просто связана с файловым вводом/выводом. Соответствующие типы вы
можете найти в пространстве имен System.1O. Вторая тема - это сериа­
лизация, то есть представление в символьном виде различных объектов.
После того как объект сериализирован, его можно сохранить в файл (или
в базу данных) или передать на удаленную машину посредством техно­
логии WCF (Windows Communication Foundation).

10.1. Введение в пространство имен System.1O


Все, что так или иначе связано с вводом/выводом, хранится в простран­
стве имен System.1O. В .NET это пространство имен посвящено служ­
бам файлового ввода-вывода, а также ввода-вывода из памяти. В этом
Глава 10. Файловый ввод/вывод C-F/IMШI
пространстве определен набор классов, интерфейсов, перечислений,
структур и делегатов, большинство из которых физически находятся в
mscorlib.dll. В таблице 10.1 представлены основные классы из простран­
ства имен System.1O.

Таблица 10.1. Основные классы из пространства имен System.10

Класс Описание

Позволяют читать и записывать элементарные


BinaryReader, BinaryWriter типы данных (целочисленные, булевские, строко-
вые и т.п.) в двоичном виде

Предоставляет временное хранилище для потока


BufferedStream байтов, которые могут затем быть перенесены в
постоянные хранилища

Directory, Directorylnfo Используются для манипуляций с каталогами

Предоставляет подробную информацию о диско-


Drivelnfo
вых устройствах

File, Filelnfo Используются для манипуляций с файлами

Обеспечивает произвольный доступ к файлу с


FileStream
данными, представленными в виде потока байт

Обеспечивает произвольный доступ к данным,


MemoryStream
хранящимся в памяти, а не в физическом файле

Предоставляет информацию о пути к файлу/катало-


Path
гу
Используются для чтения и записи текстовой ин-
StreamReader, StreamWriter формации из файла. Не поддерживают произволь-
ного доступа к файлу
Также используются для чтения и записи тек-
стовой информации из файла. Однако в качестве
StringReader, StringWriter
хранилища они используют строковый буфер, а не
физический файл
IMl-&1 Справочник С#

10.2. Классы для манипуляции с файлами и


каталогами
В System.1O определено четыре класса для манипуляции с файлами и
каталогами - File, Filelnfo, Directory, Directorylnfo. Классы Directory и
File используются для создания, удаления, копирования и перемещения
каталогов и файлов соответственно. Классы Directorylnfo и Filelnfo так­
же используются для манипуляций с каталогами и файлами, но предла­
гают свою функциональность в виде методов уровня экземпляра - они
должны размещаться в памяти с помощью ключевого слова new.

Классы Directorylnfo и Filelnfo берут от абстрактного базового класса


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

• Attributes - получает или устанавливает ассоциированные с те­


кущим файлом атрибутi.1 (только чтение, системный, скрытый,
сжатый).
• CreationТime - получает/устанавливает время создания файла/
каталога.
• Exists - используется для определения, существует ли данный
файл/каталог.
• Extension - извлекает расширение файла.
• Ful/Name - полный путь к файлу/каталогу.
• LastAccessТime - получает/устанавливает время последнего
доступа к текущему файлу или каталогу.
• LastWriteТime - получает/устанавливает время последней записи
в файл/каталог.
• Name - содержит имя файла/каталога.
Глава 10. Файловый ввод/вывод

10.2.1. Использование класса Directorylnfo


Для работы с каталогами удобнее всего использовать класс Directorylnfo.
Рассмотрим основные члены этого класса:

• Create(), CreateSubdirectory() - создают каталог или дерево ката­


логов по заданному имени.
• Delete() - удаляет каталог вместе со всем содержимым. Обратите
внимание, в отличие от некоторых системных функций, удаляю­
щих только пустой каталог, этот метод удаляет непустой каталог,
что очень удобно - не нужно извлекать обход дерева каталогов.
• GetDirectory() - возвращает массив объектов Directorylnfo, пред­
ставляющий собой все подкаталоги заданного (текущего) каталога.
• GetFiles() - извлекает массив объектов Filelnfo, представляющий
собой все файлы из текущего каталога.
• СоруТо() - копирует каталог со всем содержимым.
• MoveTo() - перемещает весь каталог (с файлами и подкаталогами).
• Parent - содержит родительский каталог.
• Root - содержит корневой каталог.

Чтобы начать работу с Directorylnfo, нужно создать новый объект этого


типа, указав каталог, с которым вы будете работать:
Directoryinfo d = new Directoryinfo("C:\Files");
Указать можете как существующий, так и несуществующий каталог.
Если вы пытаетесь привязаться к несуществующему каталогу, то будет
сгенерировано исключение System.1O.DirectoryNotFoundException. Если
нужно создать несуществующий каталог, то укажите перед его именем
знак@, например:
Directoryinfo d = new Directoryinfo(@"C:\Files\Mark");
d.Create(); // Создаем новый каталог
Для создания подкаталогов используется метод CreateSubdirectory().
Пример:
MiiMC-t-:J Справочник С#

Directoryinfo d = new Directoryinfo(@"C:\Files\Mark");


d.CreateSubdirectory("Data");
d.CreateSubdirectory("Help\Html");

В результате в каталоге C:\Files\Mark будут созданы подкаталоги Data и


Help\Нtml. Обратите внимание на то, что данный метод удобно исполь­
зовать для создания целого дерева подкаталогов. После того, как мы соз­
дали объект Directorylnfo, можно исследовать его содержимое, используя
любое свойство, которое было унаследовано от класса FileSystemlnfo.
Пример:
Directorylnfo d = new Directorylnfo(@"C:\Test");

Console.WriteLine("FullName: {0)", d.FullName);


Console.WriteLine("Name: {О)", d.Name);
Console.WriteLine("Parent: {О)", d.Parent);
Console.WriteLine("Creation: {О)", d.CreationTime);
Console.WriteLine("Attributes: {О}", d.Attributes);
Console.WriteLine("Root: {0}", d.Root);

Получить содержимое каталога (список файлов) можно с помощью мето­


да GetFiles() класса Directorylnfo. Данному классу нужно передать маску
файлов и опции поиска. В качестве результата метод возвращает массив
объектов типа Filelnfo, каждый из которых используется для предостав­
ления информации о конкретном файле. Пример кода:
Directory d = new Directoryinfo(@"C:\test");
Fileinfo files = d.GetFiles("*.doc", SearchOption.AllDirectories);
Console.WriteLine("Found {О) documents", files.Length);
// Выводим файлы:
foreach (Fileinfo f in files)

Console.WriteLine("Filename: {0)", f.Name);


Console.WriteLine("Size: {О)", f.Length);
Console.WriteLine("Attributes: {О}", f.Attributes);

10.2.2. Классы Directory и Drivelnfo. Получение списка дисков


После рассмотрения класса Directorylnfo можно приступить к классу
Directory. Члены этого класса обычно возвращают строковые данные, а
Глава 1 О. Файловый ввод/вывод

не типизированные объекты типов Filelnfo/Directorylnfo - в этом ос­


новная разница между этими двумя классами. Сейчас мы, используя
этот класс, создадим более интересное приложение, состоящее всего из
четырех строк рабочего кода, а именно выведем список дисков вашего
компьютера:
Console.WriteLine("Baши диски:");
string[] drives = Directory.GetLogicalDrives();
foreach (string s in drives)
Console.WriteLine("{O}", s);

Во-первых, обратите внимание, что метод GetLogica!Drives() возвращает


массив строк, а не массив объектов Directorylnfo. Во-вторых, метод вы­
зывается без предварительного создания объекта оператором new.

Рис. 10.1. Использование метода GetLogicalDrives()

Метод GetLogica!Drives() не информативен. Он просто сообщает буквы


логических дисков, при этом невозможно понять, где и какой диск. Если
нужно получить больше информации о дисках, нужно использовать тип
Drivelnfo:
Drivelnfo[] drvs = Drivelnfo.GetDrives();

foreach (Drivelnfo d in drvs)


Справочник С#

Console.WriteLine("ДИcк: {О} Туре {1}", d.Name, d.DriveType);


if (d. IsReady) {
Console.WriteLine("Cвoбoднo: {0}", d.TotalFreeSpace);
Console.WriteLine("Фaйлoвaя система: {0}", d.DriveFormat);
Console.WriteLine("Meткa: {0}", d.VolumeLabel);

Console.WriteLine();

Полный код приложения приведен в листинге 10.1.

Листинг 10.1. Информация о дисках


using System;
using System.IO;

namespace MyDriveinfo

class Program

static void Main(string[] args)

// Изменяем кодировку консоли для вывода текста


Console.WriteLine("Baши диски:");
string[] drives = Directory.GetLogicalDrives();
foreach (string s in drives)
Console.WriteLine("{0}", s);

Driveinfo[] drvs = Driveinfo.GetDrives( );

foreach ( Driveinfo d in drvs)

Console.WriteLine(",!Jj,D{: {О} ТИп {1}", d.№rre, d.Drive'I'ype);


if ( d. IsReady)

Console.WriteLine("Cвoбoднo: {0}", d.TotalFreeSpace);


Console.WriteLine( "Файловая система: {О}",
d.DriveFormat);
Глава 1 О. Файловый ввод/вывод ИZ1
Console.WriteLine("Meткa: {0}", d.VolumeLabel);
Console.WriteLine();

Console.ReadLine();

Вывод этого приложения показан на рис. 10.2. Видно, что сначала мы


просто выводим список дисков методом GetLogicalDrives(). Далее мы
используем метод GetDrives() и получаем информацию о дисках - как
минимум мы получаем тип. Если диск смонтирован, что проверяется
свойством IsReady, то мы выводим свободное пространство, тип файло­
вой системы и метку диска. Для диска Е: информация не выводится,
поскольку он является зашифрованным диском BitLocker.

Рис. 10.2. Пример использования метода GetDrives()

10.2.3. Класс Filelnfo

Подобно классу Directorylnfo, класс Filelnfo используется для манипуля­


ции с файлами. В таблице 10.2 приведены его основные члены.
iiMl-&:J Справочник С#

Таблица 10.2. Основные члены класса Filelnfo

Член Описание

Создает новый файл и возвращает объекr FileStream, который


Create()
используется для взаимодействия с созданным файлом
Создает объекr StreamWriter, который используется для созда-
CreateText()
ния текстового файла

СоруТо() Копирует существующий файл в другой файл

AppendText() Создает объекr StreamWriter для добавления текста в файл

Delete() Удаляет файл, связанный с экземпляром Filelnfo

Directory Используется для получения эюемпляра родительского каrалога

DirectoryName Содержит полный путь к родительскому каталогу

Length Получает размер текущего файла или каталога

MoveTo() Используется для перемещения файла

Name Содержит имя файла

Open() Оrкрывает файл с различными разрешениями чтения/записи

OpenRead() Создает доступный только для чтения объекr FileStream


Создает объекr StreamReader для чтения информации из тек-
OpenText()
стового файла
OpenWrite() Создает объекr FileStream, доступный только для записи

Теперь рассмотрим примеры использования класса Filelnfo. Начнем с


примеров использования метода Create():
Fileinfo myFile = new Fileinfo(@"C:\temp\file.vdd");
FileStream fs = myFile.Create();
// Производим какие-либо операции с fs
// Закрываем поток
fs. Close();
Глава 1 О. Файловый ввод/вывод

Итак, мы создали поток типа FileStream, позволяющий производить ма­


нипуляции с содержимым файла, например, читать из него данные, за­
писывать в него данные. Позже этот поток будет подробно рассмотрен,
пока разберемся с созданием и открытием файлов. Для открытия файла
используется метод Open(), позволяющий как открывать существующие
файлы, так и создавать новые. Причем этот метод принимает несколь­
ко параметров, в отличие от метода Create(), что позволяет более гибко
управлять процессом открытия/создания файлов. В результате выполне­
ния метода Open() создается объект типа FileStream, как и в предыдущем
случае:
Fileinfo mf = new Fileinfo(@"C:\temp\file.vdd");
FileStream fs = mf.Cpen(FileМ:xJe.CpenOrCreate, FileAcrees.Read'lrite, FileShare.None);

Методу Open() нужно передать три параметра. Первый из них - тип


запроса ввода/вывода из перечисления типа FileMode:
puЫic enum FileMode

CreateNew,
Create,
Орел,
OpenOrCreate,
Truncate,
Append

Рассмотрим члены этого перечисления:


• CreateNew - создать новый файл; если он существует, будет сге­
нерировано исключение IOException.
• Create - создать новый файл; если он существует, он будет пере­
записан.
• Ореп - открыть существующий файл. Если он не существует, бу­
дет сгенерировано исключение FileNotFoundException.
• OpenOrCreate - открывает файл, если он существует. Если файл
не существует, он будет создан.
• Truncate - открывает файл и усекает его до нулевой длины. По
сути, удаляет все его содержимое.
Справочник С#

• Append - открывает файл и переходит в самый его конец. Этот


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

Второй параметр метода Open() - одно из значений перечисления


FileAccess. Используется для определения операций чтения/записи:
puЫic enum FileAccess

Read,
Write,
ReadWrite

Думаю, значения членов перечисления FileAccess в комментариях не


нуждаются. Третий параметр метода Open() - член перечисления
FileShare, задающий тип совместного доступа к этому файлу:
puЬlic enum FileShare

Delete,
InheritaЫe,
None,
Read,
ReadWrite,
Write

Методы OpenRead() и OpenWrite() используются для создания потоков,


доступных только для чтения и только для записи. Данные методы
возвращают поток FileStream, сконфигурированный соответствующим
образом. В принципе, можно было бы обойтись только методом Open(),
но использовать эти методы немного удобнее - ведь вам не нужно при­
менять различные значения из перечислений. Примеры:
Fileinfo f = new Fileinfo(@"C:\temp\l.dat");
using (FileStream ro = f.OpenRead())
{
// Выполняем операции чтения из файла

Fileinfo f2 = new Fileinfo(@"C:\temp\2.dat");


Глава 10. Файловый ввод/вывод

using (FileStream rw f2 .OpenWrite())


{
// Записываем в файл

Для работы с текстовыми файлами пригодятся методы OpenText(),


CreateText() и AppendText(). Первый метод возвращает экземпляр типа
StreamReader (в отличие от методов Create(), Open() и OpenRead/Write(),
которые возвращают тип FileStream).

Пример открытия текстового файла:


Fileinfo txt = new Fileinfo(@"program.log");
using(StreamReader txt_reader = txt.OpenText ())
{
// Читаем данные из текстового файла
}

Методы CreateText() и ApendText() возвращают объект типа StreamWriter.


Использовать эти методы можно так:
Fileinfo f = new Fileinfo(@"C:\temp\l.txt");
using(StreamWriter sw = f.CreateText())
{
// Записываем данные в файл ...

Fileinfo f = new Fileinfo(@"C:\temp\2.txt");


using(StreamWriter swa = f.AppendText())
{
// Добавляем данные в текстовый файл

10.2.4. Класс File


Тип File поддерживает несколько полезных методов, которые пригодятся
при работе с текстовыми файлами. Например, метод ReadAllLines() по­
зволяет открыть указанный файл и прочитать из него все строки - в
результате будет возвращен массив строк. После того, как все данные из
файла прочитаны, файл будет закрыт.
Справочник С#

Аналогично, метод ReadAIIBytes() читает все байты из файла, возвраща­


ет массив байтов и закрывает файл. Метод ReadAIIText() читает все со­
держимое текстового файла в одну строку и возвращает ее. Как обычно,
файл после чтения будет закрыт. Существуют и аналогичные методы за­
писи WriteAIIBytes(), WriteAIILines() и WriteAlltext(), которые записыва­
ют в файл, соответственно, массив байтов, массив строк и строку. После
записи файл закрывается.

Пример:
string[] myFriends = {"Jane", "Мах", "John" };

// Записать все данные в файл


File.WriteAllLines(@"C:\temp\friends.txt", myFriends);

// Прочитать все обратно и вывести


foreach (string friend in File.ReadAllLines(@"C:\temp\friends.txt"))

Console.WriteLine("{O}", friend);

10.2.5. Классы Stream и FileStream


На данный момент вы знаете, как открыть и создать файл, но как прочи­
тать из него информацию? Как сохранить информацию в файл? Методы
класса File хороши, но они подходят только в самых простых случаях.
При работе с текстовыми файлами их вполне достаточно, а вот при ра­
боте с двоичными файлами методов ReadAIIBytes() и WriteAIIBytes() бу­
дет маловато. В абстрактном классе System.1O.Stream определен набор
членов (см. табл. 10.3), которые обеспечивают поддержку синхронного
и асинхронного взаимодействия с хранилищем (например, файлом или
областью памяти).

Таблица 10.3. Члены абстрактного класса System.1O.Stream

Член Описание

CanRead, CarWrite, Определяют, поддерживает ли поток чтение, запись, поиск


CanSeek соответственно
Глава 10. Файловый ввод/вывод

Close() Метод закрывает поток и освобождает все ресурсы

Обновляет лежащий в основе источник данных. Например,


позволяет перечитать источник или же сбросить содержи-
Flush()
мое буферов ввода/вывода на диск. Если поток не реализу-
ст буфер, этот метод ничего не делает

Length Возвращает длину потока в байтах

Position Определяет текущую позицию в потоке

Читает последовательность байтов или один байт соответ-


Read(), ReadByte() ственно из текущего потока и перемещает позицию потока
на количество прочитанных байтов

Seek() Устанавливает позицию в текущем потоке

SetLength() Устанавливает длину текущего потока

Записывает последовательность байтов или одиночный


Write(), WriteByte() байт в текущий поток и перемещает текущую позицию на
количество записанных байтов

Класс FileStream предоставляет реализацию абстрактного члена Stream


для потоковой работы с файлами. Это элементарный поток, и он может
записывать или читать только один байт или массив байтов. Однако
программисты редко используют FileStream непосредственно, гораздо
чаще они используют оболочки потоков, облегчающие работу с тексто­
выми данными или типами .NET. Но в целях иллюстрации мы рассмо­
трим возможности син'хронного чтения/записи типа FileStream (лист.
10.2).
Листинг 10.2. Использование типа FileStream
// Получаем объект типа FileStream
using(FileStream fStream = File.Cpen(@"C:\teпp\test.dat", Fil&Ьde.Create))

// Кодируем строку в виде массива байтов


string txt = "Test!";
byte[] txtByteArr ay = Encoding.Default.GetBytes(txt);
•r-rr:, Справочник С#

// Записываем массив в файл


fStream.Write(txtByteArray, О, txtByteArray.Length);

// Сбрасываем position
fStream.Position = О;

// Читаем из файла и выводим на консоль

byte[] bytesFromFile = new byte[txtByteArray.Length];


for (int i = О; i < txtByteArray.Length; i++)

bytesFromFile[i] = (byte)fStream.ReadByte();
Console.Write(bytesFromFile[i]);

Console.WriteLine();

// Декодируем сообщение и выводим.


Console.Write("Декодированное сообщение: " ) ;
Console.WriteLine(Encoding.Default.GetString(bytesFromFile));

Пример показывает не только наполнение файла тестовыми данными,


но и демонстрирует основной недостаток при работе с типом FileStream:
вам необходимо выполнять низкоуровневое кодирование и декоди­
рование информации, которую вы записываете и которую вы читаете.
Это очень неудобно. Но иногда ситуация этого требует. Например, ког­
да нужно записать поток байтов в область памяти (используется класс
MemoryStream) или в сетевое соединение (класс NetworkStream из
пространства имен System.Net.Sockets).

10.2.6. Классы Stream Writer и StreamReader


Классы StreamWriter и StreamReader удобно использовать во всех слу­
чаях, когда нужно читать или записывать символьные данные (напри­
мер, строки). Оба типа работают по умолчанию с символами Unicode, но
кодировку можно изменить предоставлением правильно сконфигуриро­
ванной ссьшки на объект System.Text.Encoding.
Глава 10. Файловый ввод/вывод ro
Класс StreamReader унаследован от абстрактного класса TextReader, ос­
новные методы которого следующие:

• Close() - закрывает объект и освобождает ресурсы.


• Flush() - очищает все буферы объекта-писателя и записывает все
буферизированные данные на устройство, но объект при этом не
закрывается.
• NewLine - свойство, позволяющее задать константу перевода
строки. В Windows используется \r\n.
• Write() - позволяет записывать данные в текстовый поток без до­
бавления константы новой строки.
• WriteLine() - записывает данные в текстовый поток с добавлени­
ем константы новой строки.

Рассмотрим пример записи в файл с использованием StreamWriter:


using (StreamWnter writer = File.CreateText("friends.txt"))

writer.WriteLine("Bacя");
writer.WriteLine("Иpa");
writer.WriteLine("Oлeг");

Для чтения информации из текстового файла используется StreamReader,


который унаследован от абстрактного класса TextReader, обладающего
следующими методами:
• Peek() - возвращает следующий доступный символ, не изменяя
текущей позиции читателя. Если достигнут конец потока, возвра­
щается -1.
• Read() - читает данные из входного потока.
• ReadLine() - читает строку символов из текущего потока. Если
достигнут конец потока, возвращается null.
• ReadToEnd() - читает все символы, начиная с текущей позиции
и до конца потока. Прочитанные символы возвращаются в виде
одной строки.
Справочник С#

Пример:
using(StreamReader readerl File.OpenText("friends.txt"))

string s = null;
while ((s = readerl.ReadLine()) != null)
{
Console.WriteLine(s);

10.2.7. Классы BinaryWriter и BinaryReader


Для работы с двоичными (не текстовыми) данными используются клас­
сы BinaryWriter и BinaryReader. Оба эти класса унаследованы от System.
Object и позволяют читать и записывать данные в потоки в двоичном
формате. Для записи у BinaryWriter используется метод Write(), а для
чтения - метод Read() у BinaryReader. Метод Close (есть у обоих клас­
сов) позволяет закрыть поток. Метод Flush() у BinaryWriter позволяет
сбросить буфер двоичного потока на носитель.

Пример использования:
Fileinfo f = new Fileinfo(@"file.dat");
using(BinaryWriter bw = new BinaryWriter(f.OpenWrite()))
{
// Данные для записи
douЫe а = 12310.56;
int Ь = 123456;
string s = "123456";
// Записать данные.
bw.Write(a);
bw.Write(b);
bw.Write(s);

// Читаем данные
using(BinaryReader br = new BinaryReader(f.OpenRead())

Console.WriteLine(br.ReadDouЬle());
Глава 10. Файловый ввод/вывод

Console.WriteLine(br.Readlnt32());
Console.WriteLine(br.ReadString());

10.3. Сериализация объектов


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

Представим, что вам нужно сохранить настройки приложения в файле.


Можно просто создать текстовый файл (например, program.ini), записать
в него настройки и при запуске приложения обрабатывать этот файл, чи­
тая из него тот или иной конфигурационный параметр. Можно пойти по
другому пути и записать в файл целый класс, отвечающий за хранение
параметров пользователя. Тогда достаточно при загрузке программы вы­
полнить десериализацию этого класса (восстановить настройки), а при
завершении работы - сериализовать класс в файл (сохранить настройки).

Представим, что у нас есть класс UserSettings, содержащий параметры,


установленные пользователем. Чтобы этот класс можно было сериализо­
вать, его нужно снабдить атрибутом SerializaЫe:

[SerializaЬle]
puЬlic class UserSettings

puЬlic string WorkDir = "C:\temp";


puЫic int TimeOut = 2000;

Теперь давайте посмотрим, как сериализовать этот класс. Первым делом


нужно создать объект класса:
IMl№J Справочник С#

UserSettings currentSettings = new UserSettings();


currentSettings.TimeOut = 1000; // Устанавливаем конкретное значение

Для сериализации нужно создать объект типа BinaryFoпnatter (опреде­


лен в пространстве имен System.Runtime):
using System.Runtime;

BinaryFormatter bf = new BinaryFormatter();


using (Stream fs = new FileStream("settings.dat", FileMode.Create,
FileAccess.Write, FileShare.None))

bf.Serialize(fs, currentSettings);

Для десериализации нужно выполнить метод Deserialize():


FileStream fs = new FileStream("settings.dat", FileMode.Open);
try
{
BinaryFormatter bf = new BinaryFormatter();
currentSettings = (UserSettings)bf.Deserialize(fs);

catch (SeriaizationException е)

Console.WriteLine("Oшибкa. При�ина: " + e.M essage);


throw;

finally
{
fs. C lose();

Посмотрите, как мы выполняем десериализацию объекта. Первым делом


мы открываем файл для чтения. Далее мы создаем новый объект класса
BinaryFoпnatter и десериализируем поток fs. Если в результате возникнет
исключение, то мы выводим об этом сообщение. Блок finally позволяет в
любом случае закрыть поток fs и освободить ресурсы.

Нужно отметить, что BinaryFonnatter - это не единственный класс для


сериализации объекта. Он сохраняет объект в двоичном формате.
Глава 10. Файловый ввод/вывод to
Однако если вам нужно сохранить объект в формате XML или в формате
SOAP, то вам нужно использовать, соответственно, классы XmlSerializer
или SoapFormatter. Тип SoapFormatter сохраняет состояние объекта в
виде сообщения SOAP (стандартный ХМL-формат для передачи и прие­
ма сообщений от веб-служб). Этот тип определен в пространстве имен
System.Runtime. Serialization.Formatters.Soap, находящемся в отдельной
сборке. Поэтому для преобразования объектов в формат SOAP необхо­
димо сначала установить ссьшку на System.Runtime.Serialization.Format­
ters.Soap.dll, используя диалоговое окно Add Reference в Visual Studio и
затем указать следующую директиву using:

// Нужна ссылка на System.Runtime.Serialization.Foпnatters.Soap!


using System.Runtime.Serialization.Formatters.Soap;

Для преобразования объекта в формат XML имеется тип XmlSerializer.


Чтобы использовать этот тип, нужно указать директиву using для
пространства имен System.Xml.Serialization и установить ссылку на
сборку System.Xml.dll. Однако шаблоны проектов Visual Studio автома­
тически ссылаются на System.Xml.dll, поэтому достаточно просто ука­
зать соответствующее пространство имен:

u sing System.Xml.Serialization;

Дополнительная информация может быть получена по адресу:

https://msdn.microsoft.com/en-usllibrarylsystem.runtime.serialization.
formatters.blnary.blnaryformatter%28v = vs.110%29.aspx

10.4. Вывод содержимого файла на С#


Ранее мы познакомились с классом StreamReader, сейчас же рассмотрим
небольшой пример его практического использования, а именно - вывод
на консоль содержимого текстового файла. Первым делом нужно создать
сам файл. При сохранении файла в Блокноте нужно выбрать кодировку
Юникод, поскольку приложение научилось по умолчанию сохранять в
UTF-8 только в Windows 11 (рис. 10.3), а в Windows 10 Блокнот пред-
Справочник С#

лагал по умолчанию кодировку ANSI, из-за чего многие меняли его на


приложение Notepad2, которое по умолчанию использовало UTF-8.
!;д,;\!..<Ш!Ш!....!!1&...Ш:.....!.l!'Зiilll-l-�-.d!S!....D!llw.,ilf;SU-,llllillla----..Ji51!!L..м!W--,•f: м-;t,·is.;., (1 х
lf.� 1;.,..sщ.,,. /1(

i
11
1 °" 1 .,,_, 1 "'--

Рис. 10.3. В Wuu/ows 11 поумолчш,ию в Блоююте испапьзrется l<Одиров,ш UТF�

Вообще, Windows в плане различных кодировок проявляет высшую сте­


пень идиотизма. Это вам не Linux, где все взяли в один момент и переш­
ли на UTF-8. В конечном итоге в UTF-8 оказались все файлы, настроена
консоль и графический интерфейс, переделаны файлы локализации (вы­
вод осуществляется тоже в UTF-8). В Windows все как-то не так, как у
людей. С одной стороны, поддерживается Юникод, с другой стороны,
консоль почему-то до сих пор живет в кодировке СР-866, которая впер­
вые появилась в MS DOS 4.01.
На секундочку: эта операционная система была выпущена в декабре
1988 года! Кодировке, которая до сих пор используется в командной
строке Windows 1О, уже более 30 лет. При этом блокнот почему-то сохра­
няет файлы в кодировке ANSI (СР-1251). Зачем это все делать? Обратная
совместимость? Кто будет запускать приложения 20-летней давности?
Все это - риторические вопросы, поэтому возвращаемся к нашему при­
ложению. Само приложение очень простое, но, как говорится, есть
нюансы. Первый нюанс - это установка кодировки вывода:
Console.OutputEncoding = Encoding.GetEncoding(866);
Глава 1 О. Файловый ввод/вывод

Примечание. В Windows 11 и при программировании для .NЕТб


этого делать не нужно.

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


консоли увидите много вопросительных знаков. И это понятно: нужно
указать StreamReader, в какой кодировке нужно читать файл. У нас это
Юникод - именно в этой кодировке мы сохранили файл:

StrearnReader sr = n ew StrearnReader("c:\\users\\Mark\\file.txt",
System.Text.Encoding.Default);

Обратите внимание: по умолчанию, значит, используется Юникод


(System.Text.Encoding.Default). Так почему же эта кодировка по умолча­
нию не используется в приложения? В том же Блокноте? Третий момент,
на который стоит обратить внимание, - это на экранирование символа \
при указании пути к файлу. Об этом уже говорилось, но все равно хочет­
ся акцентировать еще раз ваше внимание. Далее все просто - мы читаем
строки из StreamReader и построчно выводим их на консоль. Полный код
приложения приведен в листинге 10.3.

Листинr 10.3. Чтение и вывод файла на консоль


using System;
usi ng System.Collections.Generic;
using System.Linq;
using System.Text;
using System .Threading.Tasks;
using System .IO;

names pace StreamReaderExample

class Program

static void Main(string[] a rgs)


{
StreamReader sr = new StreamReader("c:\\users\\мark\\file.txt",
System.Text.Encoding.Default);
Справочник С#

string s;

while (sr.EndOfStream != true)

s = sr.ReadLine();

Console.WriteLine(s);

sr. Close();
Console.ReadLine();

На рис. 10.4 изображена среда Visua!Studio, содержимое текстового фай­


ла в блокноте и запущенное приложение.

Рис. 10.4. Программа в действии

Обратите внимание, что изменен цвет консоли - черные символы на


белом фоне. Если хотите изменить цвета консоли, щелкните правой
кнопкой мыши на заголовке консоли, выберите команду Свойства и на
вкладке Цвет установите нужные цвета.
Глава 10. Файловый ввод/вывод

10.5. Работа с ХМL-файлом


В прошлом разделе была продемонстрирована простая программа чте­
ния обычного текстового файла. Сейчас же мы создадим приложение
Windows Foпns для чтения данных из ХМL-файла. Подробно останав­
ливаться на формате XML мы не будем по двум причинам. Во-первых,
формат XML прост и интуитивно понятен и с ним многие уже знакомы.
Во-вторых, XML посвящено много информации в Интернете и найти ее
не составляет труда. Лучше сконцентрируемся на главном - на чтении
информации из XML. В качестве исходного файла мы будем. использо­
вать файл с информацией о сотрудниках небольшой компании, см. лист.
10.4.
Листинг 10.4. Исходный ХМL-файл
<?xml version="l.0" encoding="utf-8" ?>
<root>
<Employee name= "Evgeniy Orlov">
<Age>35</Age>
<Programmer>True</Programmer>
</Employee>

<Employee name ="Sergey Pertov">


<Age>25</Age>
<Programmer>False</Programmer>
</Employee>

<Employee name ="Mark Landau">


<Age>ЗO</Age>
<Programmer>True</Programmer>
</Employee>

<Employee name="Vika Orlova">


<Age>Зl</Age>
<Programmer>False</Programmer>
</Employee>
</root>
Наш файл содержит фамилию и имя сотрудника, возраст, а также флаг
Programmer, свидетельствующий о том, что тот или иной человек явля­
ется программистом.
Справочник С#

c;;;;:
-,.�'?.'i... �.;►l)J(.i,•l>�•DiC,,,,
'8 Фаi!!� � � 6'1 1'1ром1' C6oflц о._ Хмt.•- ,К\' � � "•tw�,.. с�� ,_ ··р) llml..,..tder о х
Ф· �-llif!81t}•(.,'•· 6'�siw. 1r ...,_.
OoG,o

;�==�;,.••н-.1t-••1.e•t1oco,H"g •ut-••r:,, +
:-
_....:....!•·~-��-•• ·••
rf" ";Q,IJ Ъ·Z(.){I., .. ,..е3
11 --�• •mЩ�•- ��--
a
·

•f::;��1�:;:::::::9:::::
� i�;��::;::zi;·�,)
r

l 1f 1


</EIC!fl1.0),.•"' ► ;'�
!

'[ ....,.,...._..,.,.., ..,,....


l ► lifCa.,,""
"Ai•"2�/Aoie'"' ,v ApQ..(on!!g

11,.
"Ptчщ1r_r.,.J"a\и<1Pt-og•·-,.. ,. 18 fo,rnt..:.,
: c/E8f>\OJf"� ► t:.lfOf"'l.�n«.a

1
� ' "�::::.'"

;k �tc:,•• n-••Vilta Or\o.,••• ,06<.aoeи......__.,._·· �Git


1 ::::;:.��•1.аес/�1'-1"> ,Qом-1

·-
.. '

,. ,
t 1@::;;.:�":::р
1 "/nюt><
·с:�1
Е "110•;>\•�V {�хм�.

в-

!r�r.-"""""'
� u.kocte(VП-8)

i•им..
: ,..�,,_,,т n�aм:>NIIMR XSL.

Рис. 10.5. Исходный ХМL-файл в среде Visual Studio

Создать ХМL-файл можно или в любом текстовом редакторе, или в среде


Visual Studio (команда Файл, Создать, Файл, ХМL-файл), см. рис. 10.6.
х
А Усt;,ноеАеНные

....
rlc"ac:.((JJI•�)
Teir.croвыA фait-1 • ТИn; Обшие
Общие
n�XML--Фзl'l.n.

,_..
.�
HTML-cтptttt14цa
Vi$1Jaj с·•·•
Оцмnт .D Общ••

Ви1и
D' �i\11J.waScript Общ..,

Т,16J!ицвстw��

(!:)

n.
( >
(xet,1a1X:Мl 00.Ц,,е

:1.. хsп-..� ОО.Ц,,е

набор npa1SМ1t соое AnatvsiJ


00 00.Ц,,е

� Ф&ЙАюцеЧЖ)f'Орv:<уМ"'8 Об-

Фail11t3t1�<Ж3
[1 Общие

� Ф.111:J! l()'JXOpa Общ,е


..=.

Рис. 10.6. Создание ХМL-файла в Visual Studio

Создайте новый проект Windows Foпns (рис. 10.7). Далее расположите


на форме компоненты ListBox и PropertyGrid (найти эти элементы можно
на вкладке Панель элементов), чтобы наша форма была такой, как
Глава 1О. Файловый ввод/вывод CW/J-
показано на рис. 10.8. В ListBox мы будем загружать имена сотрудников,
в PropertyGrid - «свойства>► сотрудников.

Создание проекта ,,.

__
Посл•дние шаблоны проектов

........=�..
�-•�W!nclowl.�)"lll'OМ'--�'1<,e,,o

г.........J

-
Рис. 10. 7. Создание нового проекта Windows Forms Application

е. •


aw � k G<t

••е�в111 '? •
rtac-т

• -~-- · ""-�
t6ot:ca О,11� �т rcr А,q_щ

-t ►R)o::·b�·

�·:, s;
� Q,ж1
•·11-•r•-ii.,a:►j1
� ·�-�

l.�
" х

lf
ii .""""
.,..�.... -
-··-·-•""- ....�-''""""""'�'---�.,..-� --� .. •
iiil-lMЧti/ИЩ.1-
х
��-м�-1

,-:::::::::::.::-::i
_
. • :.:::-
1r!
► 611/Ct
..,_

► t) fonn1.Dt1io,,e,.o
"Qlofml.m-

I
► С'• ll'fogr-a

S,Stem.Wtndowvo,ttnJl\opertyG,lo

Рис. 10.8. Форма нашего прш�ожения

Для работы с данными из ХМL-файла нам нужно создать вспомогатель­


ный класс. Выполните команду Проект, Добавить к.ласе. Данный класс
Справочник С#

очень зависит от формата ХМL-файла - данные, читаемые из ХМL­


файла, должны быть помещены в этот класс, поэтому если вы создали
отличный от приведенного ХМL-файл, вам придется изменить и этот
класс. Код класса Employee приведен в листинге 10.5.
8м11.• nu- � Gil J'k,oкJ "6с:1$О 0,-..ца � Tf!C" A,i,1.""' Срц,;,м Р� � � r1uto р·, ---� - {') Х

-�---�? ►!}ус-11·Р.·"'·tж·:,� !C:""'t�f-t- 1�u.oe� R ,uw:w

ТМ: ).-....ewn,iVi,u,tH:,

Пустое�меu.сс:,

1
1 *i
-�

i
i.
ISX·фlllл 1�-,16:rial

M<К1'it1el«i"'Н!O�(�l,.N(Т}

Runt!n-,elatTe<npl&�

• • '•• 1 • (> 1• •• I ••

Рис. 10.9. Создание нового класса

Листинг 10.5. Вспомогательный класс Employee


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace xml reader

internal class Employee

puЬlic string Name { get; private set;


puЬlic int Age { get; private set;
puЬlic bool Programmer { get; private set;

puЬlic Employee(string name, int age, bool programmer)


Глава 1 О. Файловый ввод/вывод кв•
Name = name;
Age = age;
Programmer = programmer;

p uЬlic override string ToString()


{
return Name;

Как видите, класс очень простой. Однако, тем не менее, без него -
никак. Конструктор класса просто инициализирует свойства класса. Так­
же мы переопределили метод ToString() - в качестве строкового значе­
ния будет возвращаться значение свойства Name. Теперь приступаем к
редактированию файла кода формы - Fonnl .cs (в области Обозреватель
решений для этого выделите файл Fonnl .cs, щелкните правой кнопкой
мыши и выберите команду Перейти к коду или нажмите F7). Первым
делом нужно добавить метод чтения из ХМL-файла:
private void LoadEmployees ( )

XmlDocument doc = new XmlDocument();


doc.Load ("xmltext.xml");

foreach(XmlNode node in doc.DocumentElement)

string name = node.Attributes[O] .Value;


int age = int.Parse (node["Age"] .InnerText);
Ьооl prograrnrer = Ьool.Par se(node["Prograrnrer"] .InnerТext);
listBoxl.Items.Add(new Employee(name, age, pr ogramner ));

Метод Load() загружает ХМL-файл. Убедитесь, что указали правильное


имя. Далее в цикле мы читаем все элементы ХМL-файла и загружаем
в listbox 1. Обратите внимание, как мы читаем имена сотрудников и их
характеристики. Для доступа к элементам Age и Programmer мы исполь­
зуем имена элементов.
FNl-t/A Справочник С#

Чтобы класс XmlDocument бьш доступен, нужно добавить директиву:


using S ystem.Xml;

В метод Foпnl() нужно добавить вызов метода чтения из ХМL-файла,


чтобы он загружался при открытии формы:
puЬlic Forml()
{
InitializeComponent();
LoadEmployees();

Также нам надо сделать так, чтобы при выборе имени в ListBox все дан­
ные выводились в PropertyGrid. Для этого дважды щелкните левой кноп­
кой мыши на ListВox в форме и в появившемся блоке кода введите:
private void listBoxl_SelectedindexChanged(object sender, EventArgs е)

if (listBoxl.Selectedlndex != -1)

propertyGridl.SelectedObject = listBoxl.Sel�ctedltem;

Полный код приложения приведен в листинге 10.6.

Листинг 10.6. Код файла Forml.cs


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.T h reading.Tasks;
using System.Wind ows.Forms;
using System.Xml;

namespace xml reader


Глава 1 О. Файловый ввод/вывод 1-zl-
puЫic partial c lass Forml Form

puЫic Forml ()
{
InitializeComponent();
LoadEmployees();

private void LoadEmployees()

XmlDocument doc = new XmlDocument();


doc.Load("xmltext.xml");

foreach(XmlNode node in doc .DocumentElement)

strin g name = node.Attributes[0] .Value;


i nt age = i nt.Parse(node["A ge"] .InnerText);
Ьооl programner = Ьool.Parse(node["P rogramner"].InnerТext) ;
listВoxl.Items.Add(new Employee(name, age, programner));

private void listВoxl_Selecte dindexChanged(object sender, EventArgs


е)

if (listBoxl.Selectedindex != -1)

propertyGridl.SelectedObject = listBoxl.Selecteditem;

Примечание. Файл xmltext.xml нужно скопировать в каталог


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

Работающая программа изображена на рис. 10.1 О.


С-&1 Справочник С#

ijiForm1 о х

SergeyPertov
Mark Larкfau
VikзOdova )'5
E-,;g;;miy Or!ov

Рис. 10.10. Готовое приложение

10.6. Архивация файлов на С#


Для архивации файлов на С# обычно используется библиотека Ionic.Zip,
скачать которую можно по адресу:

https:/larchive.codeplex. com/?p=dotnetzip
https://documentation. help/DotNetZipl

Примечание. Вторая ссылка - это достаточно подробная до­


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

Данная библиотека позволяет выполнять все операции с архивами в фор­


мате Zip - архивацию файлов, распаковку архивов, добавление файлов
в архив и т.д. В этом разделе мы покажем, как создать приложение, ар­
хивирующее целую папку (архивация одного файла - малополезная
операция). Итак, приступим. Загрузите Ionic.Zip.dll в каталог приложе­
ния или в любой другой удобный вам. После этого создайте приложение
Windows Fonns и создайте форму, состоящую из одного текстового поля
и двух кнопок. Первая кнопка будет вызывать диалог выбора папки, а
вторая - диалог сохранения файла, в нем нужно будет ввести название
архива.
Глава 10. Файловый ввод/вывод

После создания формы щелкните правой кнопкой мыши на элементе


Ссылки в области Обозреватель решений и выберите кнопку Доба­
вить ссылку. В появившемся окне нажмите кнопку Browse и укажите
путь к библиотеке Ionic.Zip. Посмотрите на рис. 10.11 - на нем не толь­
ко отображается созданная форма, но и путь к DLL.
---------------�
j с,:) lf s 100

Рис.10.11. В процессе создания приложения

После этого в файле Fonn 1.cs нужно указать, что мы будем использовать
подключенную библиотеку:
using Ionic.Zip;
Обработчик нажатия первой кнопки (Выберите папку) будет таким:

FolderBrowserDialog fo = new FolderBrowserDialog();

private void buttonl_Click(object sender, EventArgs е)

if (fo.ShowDialog() == DialogResult.OK)

textBoxl.Text = fo.SelectedPath;

Здесь мы открываем диалог выбора папки, а выбранный путь заносим


в textbox 1 - текстовое поле. Обработчик второй папки тоже довольно
Справочник С#

простой. Мы открываем диалог сохранения файла и, если пользователь


не нажал Отмена (то есть DialogResult.OK), начинается процесс архи­
вации:
SaveFileDialog sfd = new SaveFileDialog();
sfd.Filter = "Zip files (*.zip) l*.zip";
if (textвoxl.Text!="" && sfd.ShowDialog() = DialogResult.OK)

ZipFile zf = new ZipFile(sfd.FileName);


zf.AddDirectory(fo.SelectedPath);
zf. Save();
№ssageВox.Show( "Архиващ,�я пр:uта успешю.", "В,mалнено");

Полный листинг Foпnl.cs для экономии бумаги и ваших денег приво­


дить не буду - он очень прост. Программа в действии отображена на
рис. 10.12.

l:IJ Ар:н1еащ,1я - О Х

IC:\Usm\den\Desktop"2jpl)ing 1

Рис. 10.12. Результат

1 О. 7. Подсчет количества слов в файле

В заключение этой главы рассмотрим небольшой пример - программу,


подсчитывающую количество слов в тексте. Исходный код этой програм­
мы приведен в листинге 10.7.
Глава 1 О. Файловый ввод/вывод 10•
Листинг 1 О.7. Подсчет слов в тексте
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace wordscount

class Program

static void Main(string[] args)

string s = "" ;
string[] txt;
StreamReader sr = new StreamReader("file.txt",
System.Text.Encoding.Default);

while (sr.EndOfStream != true)

s = sr.ReadLine();

txt s.Split(' ');


Console.WriteLine("Koличecтвo слов:");
Console.WriteLine(txt.Length);

sr.Close();
Console.ReadLine();

Мы читаем слова из файла (словом, считается все, что отделено пробе­


лом) в массив, а потом выводим количество элементов (длину) в мас­
сиве. Алгоритм, может быть, и не очень хороший, но зато простой. Для
•10 Справочник С#

больших файлов, возможно, его придется переделать, а именно - читать


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

Уважаемые авторы!
Приглашаем к сотрудничеству по изданию книг по IТ-технолоrням, электронике,
медицине, педагогике.
Издательство сушествует в книжном пространстве более 20 лет и имеет большой
практический опыт.
Наши преимущества:
- Большие тиражи (в сравнении с аналогичными изданиями других издательств);
- Наши книги регулярно переиздаются, а автор автоматически получает гонорар с
каждого издания;
- Индивидуальный подход в работе с каждым автором;
- Лучшее соотношение цена-качество, влияющее на объемы и сроки продаж, и, как
следствие, на регулярные переиздания;
- Ваши книги будут представлены в крупнейших книжных магазинах РФ и ближнего
зарубежья, библиотеках вузов, ссузов, а также на площадках ведущих маркетплейсов.
Ждем Ваши предложения:
тел. (812) 412-70-26 / эл. почта: nitmail@nit.com.ru
Будем рады сотрудничеству!

Для заказа кннr:


► интернет-маrазин: www.nit.com.ru / БЕЗ ПРЕДОПЛАТЫ по ОПТОВЫМ ценам
более 3000 пунктов выдачи на территории РФ, доставка 3-5 дней
более 300 пунктов выдачи в Санкт-Петербурге и Москве, доставка 1-2 дня
тел. (812) 412-70-26
эл. почта: nitmail@nit.com.ru

► маrазин издательства: г. Санкт-Петербург, пр. Обуховской обороны, д.107


метро Елизаровская, 200 м за ДК им. Крупской
ежедневно с 10.00 до 18.30
справки и заказ: тел. (812) 412-70-26

► крупнейшие книжные сети и маrазины страны


Сеть магазинов «Новый книжный►► тел. (495) 937-85-81, (499) 177-22-11
------------------------------------------------------------------------------
► маркетплейсы ОЗОН, Wildberries, Яндекс.Маркет, Myshop и др.

Вам также может понравиться