КулЛиб - Классная библиотека! Скачать книги бесплатно 

C# на примерах [Петр Валентинович Евдокимов] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Евдокимов П. В.

на примерах
4-е издание

Санкт-Петербург

У Д К 004.438
Б Б К 32.973.2
ISBN 978-5-94387-782-7
Евдокимов П. В.

C# на примерах. 4-е издание

(переработанное и обновленное) —

СПб.: Наука и Техника, 2019. — 320 с., ил.

Серия "На примерах”

Эта книга является превосходным учебным пособием для изучения
язы ка программирования C # на примерах. Изложение ведется
последовательно: от развертывания .NET и написания первой
программы, до многопоточного программирования, создания клиентсерверных приложений и разработки программ для мобильных
устройств. По ходу книги даются все необходимые пояснения и
комментарии. В четвертом издании был частично переработан
текст по ходу изложения всей книги, а также обновлены некоторые
примеры.
Книга написана простым и доступным языком. Лучший выбор для
результативного изучения С #. Начните сразу писать программы на
С #!

ISBN 978-5-94387-782-7

Контактные телефоны издательства:
(812)412 70 26
Официальный сайт: www.nit.com.ru

Евдокимов П. В., ПРОКДИ РГ
9 78- 5- 94387- 782- 7

Наука и Техника (оригинал-макет)

Содержание
Глава 1.

введение

в . n e t ............................................ ю

1.1. ЧТО ТАКОЕ .NET........................................................................................11
1.2. ИСТОРИЯ .N E T .........................................................................................14
1.3. ПОДДЕРЖИВАЕМЫЕ ОПЕРАЦИОННЫЕ СИСТЕМ Ы ............................... 17
1.4. КАК ПРОГРАММИРОВАЛИ РАНЬШЕ........................................................18
1.4.1. Язык Си и Windows API - традиционный подход.................. 19
1.4.2. Язык C++ и библиотека базовых к л а с со в ............................ 19
1.4.3. Visual Basic 6 .0 ........................................................................ 20
1.4.4. Язык Jav a.................................................................................20
1.4.5. Модель компонентных объектов.......................................... 21

1.5. ЧТО ПРЕДЛАГАЕТ НАМ .N ET.................................................................... 22
1.6. ОСНОВНЫЕ КОМПОНЕНТЫ .NET............................................................ 23
1.6.1. Три кита: CLR, CTS и C L S ...................................................... 23
1.6.2. Библиотека базовых кл ассов ................................................24

1.7. ЯЗЫ КС#...................................................................................................24
1.8. CBOPKHB.NET.........................................................................................27
1.9. ПОДРОБНО О C T S ....................................................................................29
1.9.1. Типы к л а ссо в .......................................................................... 29
1.9.2. Типы интерф ейсов................................................................. 30
1.9.3. Типы структур.........................................................................30
1.9.4. Типы перечислений............................................................... 31
1.9.5. Типы делегатов.......................................................................31
1.9.6. Встроенные типы данных...................................................... 31

1.10. ПОДРОБНО О C L S ................................................................................. 32
1.11. ПОДРОБНО О C L R ................................................................................. 34
1.12. ПРОСТРАНСТВА ИМЕН.......................................................................... 34

Глава 2. р а з в е р т ы в а н и е .n e t
И ПЕРВАЯ ПРОГРАММА....................................... 37
2.1.

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

2.2. РАЗВЕРТЫВАНИЕ У ПРОГРАММИСТА. УСТАНОВКА VISUAL STUDIO
COMMUNITY...................................................................................... 43
2.3. ПЕРВАЯ ПРОГРАММА С ИСПОЛЬЗОВАНИЕМ VISUAL STUDIO...............46

Глава 3. ОСНОВНЫЕ КОНСТРУКЦИИ ЯЗЫКА С#.........50
3.1.

ИССЛЕДОВАНИЕ ПРОГРАММЫ HELLO, WORLD1................................. 51
3.1.1. Пространства имен, объекты, м е т о д ы ................................ 51

3.3. ТИПЫ ДАННЫХ И ПЕРЕМЕННЫЕ............................................................ 54
3.3.1. Системны е типы данны х....................................................... 54
3.3.2. Объявление переменны х...................................................... 55
3.3.3. Внутренние типы д а н н ы х...................................................... 56
3.3.4. Члены типов д а н н ы х .............................................................. 56
3.3.5. Работа со стр о к а м и ............................................................... 57
Члены класса System .String...................................................... 58
Базовые операции.................................................................... 58
Сравнение строк........................................................................ 59
Поиск в ст р о к е ........................................................................... 61
Конкатенация ст р о к .................................................................. 63
Разделение и соединение с т р о к .................

63

Заполнение и обрезка ст р о к .................................................... 65
Вставка, удаление и замена с т р о к .......................................... 65
Получение подстроки................................................................ 66
Управляющие последовательности сим вол ов........................66
Строки и равенство................................................................... 67
Тип System.Text.StringBuilder.................................................... 67
3.3.6. Области видимости перем енны х......................................... 68
3.3.7. Констан ты .............................................................................. 70

3.4. ОПЕРАТОРЫ............................................................................................. 70
3.4.1. Арифметические операторы .................................................70
3.4.2. Операторы сравнения и логические операторы ................. 72
3.4.3. Операторы присваивания..................................................... 74
3.4.4. Поразрядные операторы ...................................................... 74

3.5. ПРЕОБРАЗОВАНИЕ ТИПОВ ДАННЫХ...................................................... 75
3.6. НЕЯВНО ТИПИЗИРОВАННЫЕ ЛОКАЛЬНЫЕ ПЕРЕМЕННЫЕ.................. 79
3.7. ЦИКЛЫ..................................................................................................... 80

3.7.1. Цикл f o r ...................................................................................81
3.7.2. U n io ifo re a ch ........................................................................... 81
3.7.3. Циклы while и do/while............................................................82

3.8. КОНСТРУКЦИИ ПРИНЯТИЯ РЕШЕНИЙ....................................................83
3.9. МАССИВЫ................................................................................................ 85
3.9.1. Одномерные м а с с и в ы ...........................................................85
3.9.2. Двумерные м асси в ы .............................................................. 87
3.9.3. Ступенчатые м ассивы ............................................................88
3.9.4. Класс Array. Сортировка м асси в ов....................................... 89
3.9.5. М ассив - как п а р а м е т р ..........................................................91

3.10. КОРТЕЖИ...............................................................................................91
3.11. КАК ПОДСЧИТАТЬ КОЛИЧЕСТВО СЛОВ В ТЕКСТЕ...............................92
3.12. ВЫЧИСЛЯЕМ ЗНАЧЕНИЕ ФУНКЦИИ..................................................... 93
3.13. ДЕЛАЕМ КОНСОЛЬНЫЙ КАЛЬКУЛЯТОР............................................... 95
3.14. ГРАФИЧЕСКИЙ КАЛЬКУЛЯТОР..............................................................97
3.15. "УГАДАЙ ЧИСЛО". ИГРА..................................................................... 100

Глава 4. ФАЙЛОВЫЙ ВВОД/ВЫВОД.......................... 103
4.1. ВВЕДЕНИЕ В ПРОСТРАНСТВО ИМЕН SYSTEM.Ю ............................... 104
4.2. КЛАССЫ ДЛЯ МАНИПУЛЯЦИИ С ФАЙЛАМИ И КАТАЛОГАМИ............ 105
4.2.1. Использование класса Directorylnfo................................... 106
4.2.2. Классы Directory и Drivelnfo. Получение списка дисков ...108
4.2.3. Класс F ile ln fo ........................................................................ 110
4.2.4. Класс F ile ...............................................................................113
4.2.5. Классы Stream и FileStream ................................................. 114
4.2.6. Классы StreamWriter и Stream Reader..................................116
4.2.7. Классы BinaryWriter и BinaryR eader.................................... 117

4.3. СЕРИАЛИЗАЦИЯ ОБЪЕКТОВ............................................................... 118
4.4. ВЫВОД СОДЕРЖИМОГО ФАЙЛА НА C#.............................................. 120
4.5. РАБОТА С XML-ФАЙЛОМ..................................................................... 123
4.6. АРХИВАЦИЯ ФАЙЛОВ НА C # ............................................................... 129
4.7. ПОДСЧЕТ КОЛИЧЕСТВА СЛОВ В ФАЙЛЕ............................................. 131

Глава 5. о б ъ е к т н о -о р и е н т и р о в а н н о е
ПРОГРАММИРОВАНИЕ..................................... 133
5.1. ОСНОВЫ О О П ...................................................................................... 134
5.2. КЛАССЫ И ОБЪЕКТЫ........................................................................... 137
5.2.1. Члены класса........................................................................ 137
5.2.2. Ключевое слово cla ss ........................................................... 138
5.2.3. Класс S ystem .O bject............................................................ 141
5.2.4. Конструкторы ....................................................................... 143
5.2.5. Д еструкторы......................................................................... 144
5.2.6. О бращ аемся сами к себе. Служебное слово th is .............. 145
5.2.7. Доступ к членам к л а с с а .......................................................146
5.2.8. Модификаторы параметров................................................ 147
5.2.9. Необязательные парам етры ............................................... 152
5.2.10. Именованные аргум енты .................................................. 152
5.2.11. Ключевое слово s ta tic ........................................................153
5.2.12. И ндексаторы ...................................................................... 155
5.2.13. С в о й с т в а .............................................................................158

5.3. ПЕРЕГРУЗКА ФУНКЦИЙ ЧЛЕНОВ КЛАССА.......................................... 158
5.3.1. Перегрузка м е то д о в ............................................................ 158
5.3.2. Перегрузка конструкторов.................................................. 160
5.3.3. Перегрузка оп ераторов.......................................................161

5.4. НАСЛЕДОВАНИЕ И ПОЛИМОРФИЗМ................................................. 163
5.4.1. Введение в наследование................................................... 163
5.4.2. Защищенный доступ............................................................ 165
5.4.3. Запечатанные классы. Ключевое слово se a le d ................. 166
5.4.4. Наследование конструкторов............................................. 167
5.4.5. Сокрытие имен. Ключевое слово b a s e ...............................167
5.4.6. Виртуальные чл ен ы ............................................................. 169
5.4.7. Абстрактные к л а с сы ............................................................ 170

Глава 6 . и н т е р ф е й с ы , с т р у к т у р ы
И ПЕРЕЧИСЛЕНИЯ............................................ 172
6.1. ПОНЯТИЕ ИНТЕРФЕЙСА..........................................

173

6.2. КЛЮЧЕВЫЕ СЛОВА AS И IS................................................................. 175
6.3. ИНТЕРФЕЙСНЫЕ СВОЙСТВА............................................................... 176

6.4. ИНТЕРФЕЙСЫ И НАСЛЕДОВАНИЕ...................................................... 177
6.5. СТРУКТУРЫ.......................................................................................... 178
6.6. ПЕРЕЧИСЛЕНИЯ................................................................................... 181

Глава 7. ОБРАБОТКА ИСКЛЮЧЕНИЙ.......................... 183
7.1. ВВЕДЕНИЕ В ОБРАБОТКУ ИСКЛЮЧЕНИЙ.......................................... 184
7.2. ПЕРЕХВАТ ИСКЛЮЧЕНИЙ. БЛОКИ TRY, CATCH, FINALLY................... 186
7.3. КЛАСС EXCEPTION............................................................................... 188
7.4. ИСКЛЮЧЕНИЯ УРОВНЯ СИСТЕМЫ.......................................................190
7.5. КЛЮЧЕВОЕ СЛОВО FINALLY................................................................ 191
7.6. КЛЮЧЕВЫЕ СЛОВА CHECKED И UNCHECKED.................................... 192

Глава 8.

коллекции и и те р а т о р ы

.......................... 194

8.1. ВВЕДЕНИЕ В КОЛЛЕКЦИИ................................................................... 195
8.2. НЕОБОБЩЕННЫЕ КОЛЛЕКЦИИ.......................................................... 198
8.3. ОБОБЩЕННЫЕ КОЛЛЕКЦИИ................................................................ 200
8.4. КЛАСС ARRAYLIST. ДИНАМИЧЕСКИЕ МАССИВЫ .................................202
8.5. ХЕШ-ТАБЛИЦА. КЛАСС HASHTABLE..................................................... 206
8.6. СОЗДАЕМ СТЕК. КЛАССЫ STACK И STACK.................................... 209
8.7. ОЧЕРЕДЬ. КЛАССЫ QUEUE И QUEUE.............................................210
8.8. СВЯЗНЫЙ СПИСОК. КЛАСС LINKEDLIST........................................212
8.9. СОРТИРОВАННЫЙ СПИСОК. КЛАСС SORTEDLISTCTKEY, TVALUE>... 215
8.10. СЛОВАРЬ. КЛАСС DICTIONARY.................................217
8.11. СОРТИРОВАННЫЙ СЛОВАРЬ:
КЛАСС SORTEDDICTIONARYCTKEY, TVALUE>................................ 221
8.12. МНОЖЕСТВА: КЛАССЫ HASHSET
И SORTEDSET............................................................................223
8.13. РЕАЛИЗАЦИЯ ИНТЕРФЕЙСА ICOMPARABLE..................................... 225
8.14. ПЕРЕЧИСЛИТЕЛИ ............................................................................... 226

8.15. РЕАЛИЗАЦИЯ ИНТЕРФЕЙСОВ ENUMERABLE И ENUMERATOR..... 227
8.16. ИТЕРАТОРЫ. КЛЮЧЕВОЕ СЛОВО YIELD.............................................228

Глава 9. конфигурация сборок .net.................... 230
9.1. СПЕЦИАЛЬНЫЕ ПРОСТРАНСТВА ИМЕН............................................... 231
9.2. УТОЧНЕННЫЕ ИМЕНА ИЛИ КОНФЛИКТЫ НА УРОВНЕ ИМЕН............ 233
9.3. ВЛОЖЕННЫЕ ПРОСТРАНСТВА ИМЕН.
ПРОСТРАНСТВО ПО УМОЛЧАНИЮ................................................ 234
9.4. СБОРКИ .N ET......................................................................................... 235
9.4.1. Зачем нужны сб о р к и ? ..........................................................235
9.4.2. Ф орм ат сб о р о к .....................................................................237
9.4.3. Однофайловые и многофайловые сб ор ки .........................238

9.5. СОЗДАНИЕ СБОРКИ (DLL).................................................................... 239
9.6. СОЗДАНИЕ ПРИЛОЖЕНИЯ, ИСПОЛЬЗУЮЩЕГО СБОРКУ................... 243

Глава

10. многопоточность и параллельное
ПРОГРАММИРОВАНИЕ...................................... 246

10.1. ПАРАЛЛЕЛЬНЫЕ КОЛЛЕКЦИИ............................................................247
10.2. БИБЛИОТЕКА РАСПАРАЛЛЕЛИВАНИЯ ЗАДАЧ................................... 250
10.3. КЛАСС TASK......................................................................................... 251
10.4. ОЖИДАНИЕ ЗАДАЧИ........................................................................... 255
10.5. КЛАСС TASKFACTORY.......................................................................... 258
10.6. ПРОДОЛЖЕНИЕ ЗАДАЧИ.................................................................... 259
10.7. ВОЗВРАТ ЗНАЧЕНИЯ ИЗ ЗАДАЧИ.......................................................259

Глава

11. СЕТЕВОЕ ПРОГРАММИРОВАНИЕ................ 261

11.1. ПРОСТРАНСТВО ИМЕН SYSTEM.NET..................................................262
11.2. КЛАСС URI........................................................................................... 263
11.3. ЗАГРУЗКА ФАЙЛОВ (HTTP И FTP).......................................................264
11.4. КЛАСС DNS. РАЗРЕШЕНИЕ ДОМЕННЫХ И М ЕН ................................ 268

11.5. СОКЕТЫ............................................................................................... 269
11.5.1. Типы с о к е то в ......................................................................269
11.5.2. П орты .................................................................................. 270
11.5.3. Классы для работы с сокетами......................................... 271

11.6. КОНВЕРТЕР ВАЛЮТ.............................................................................272
11.7. ПРОСТОЙ СКАНЕР ПОРТОВ................................................................ 274

Глава 12. СОЗДАНИЕ ПРИЛОЖЕНИЯ
КЛИЕНТ/СЕРВЕР............................................... 277
12.1. ПРИНЦИП РАБОТЫ ПРИЛОЖЕНИЯ.................................................... 278
12.2. РАЗРАБОТКА СЕРВЕРНОЙ ЧАСТИ.......................................................278
12.3. ПРИЛОЖЕНИЕ-КЛИЕНТ.......................................................................281
12.4. МНОГОПОТОЧНЫЙ С Е Р В Е Р ............................................................... 285

Глава 13. р а з р а б о т к а п р и л о ж е н и й
ДЛЯ ПЛАНШЕТА
ПОД УПРАВЛЕНИЕМ WINDOWS 10 .................... 293
13.1. ПОДГОТОВКА К СОЗДАНИЮ МОБИЛЬНЫХ ПРИЛОЖЕНИЙ.............. 294
13.2. ПРОЕКТИРОВАНИЕ ГРАФИЧЕСКОГО ИНТЕРФЕЙСА........................ 298
13.3. НАПИСАНИЕ КОДА ПРИЛОЖЕНИЯ..................................................... 302
13.4. КОМПИЛЯЦИЯ И ЗАПУСК ПРИЛОЖЕНИЯ.......................................... 303

Глава 14. РАБОТА С БАЗАМИ ДАННЫХ..................... 305

Глава 1.

Введение в .NET

коллекцией типов, применяемых для разработки приложений - начиная с
консольных приложений, запускаемых из командной строки, и заканчивая
приложениями, использующими последние технологические возможности
ASP.NET, например, Web.Forms и веб-службы XML. Конечно, с помощью
.NET можно создавать и обычные Windows-приложения с интерфейсом
пользователя (GUI).
Платформа .NET Framework может размещаться неуправляемыми компо­
нентами. Такие компоненты загружают среду CLR в собственные процессы
и запускают выполнение управляемого кода, что в итоге создает программ­
ную среду, которая позволяет использовать средства управляемого и не­
управляемого выполнения.
Среда CLR управляет памятью, выполнением потоков, выполнением кода,
проверкой безопасности кода, компиляцией и другими системными служ­
бами. Все эти средства являются внутренними для управляемого кода, ко­
торый выполняется в среде CLR. Из соображений безопасности управляе­
мым компонентам присваивают разные степени доверия, которые зависят
от многих факторов. Управляемый компонент может или не может выпол­
нять операции доступа к файлам, операции доступа к реестру и другие важ­
ные функции.
Также среда выполнения обеспечивает управление доступом для кода.
Пользователи могут доверить исполняемому приложению, которое внедре­
но в веб-страницу, воспроизведение звука, но при этом не разрешить ему
доступ к файловой системе и личным данным.
CLR реализует инфраструктуру строгой типизации и проверки кода, кото­
рую называют системой общих типов (CTS, Common Type Systyem). Такая
система обеспечивает самоописание всего управляемого кода. В результате
все это приводит к надежности кода.
CLR может размещаться в серверных приложениях, таких как Microsoft
SQL Server и службы IIS (Internet Information Services).
Такая инфраструктура позволяет использовать управляемый код для раз­
работки собственной логики программ, пользуясь при этом высочайшей
производительностью лучших серверов, которые поддерживают размеще­
ние среды выполнения.
Теперь поговорим о библиотеке классов .NET Framework. Библиотека клас­
сов платформы представляет собой коллекцию типов, которые тесно инте­
грируются со средой CLR. Понятно, что библиотека является объектноориентированной.

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


Богатая функциональность. Платформа .NET Framework предостав­
ляет богатый набор функционала “из коробки”. Она содержит сотни
классов, которые предоставляют функциональность, готовую к ис­
пользованию в ваших приложениях. Это означает, что разработчику
не нужно вникать в низкоуровневые детали различных операций,
таких как I/O, сетевое взаимодействие и т.д.



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



Поддержка ООП. Преимущества объектно-ориентированного про­
граммирования известны всем. Платформа .NET предоставляет пол­
ностью объектно-ориентированное окружение. Даже примитивные
типы вроде целых чисел и символов теперь считаются объектами.



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



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



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



Поддержка XML. Платформа .NET предоставляет XM L веб-сервисы,
которые основаны на стандартах вроде HTTP, XM L и SOPA.



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



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

1.2. История .NET
В июле 2000 года на конференции PDC (Professional Developer Conference)
компания Microsoft анонсировала новый фреймворк для разработки про­
граммного обеспечения - .NET Framework. Первая же бета-версия .NET
Framework SDK Beta 1 была опубликована на сайте Microsoft 12 ноября
2000 года, однако она была настолько “сырой”, что Microsoft рекомендовала
ее устанавливать только на компьютеры, предназначенные для тестирова­
ния. Как говорится, первый блин всегда комом. И таким комом была первая
бета-версия .NET.
Наверное, вам будет ийтересно узнать, что изначально платформа должна
была называться Microsoft.Net, однако Билл Гейтс решил переименовать ее
просто в .NET. Также он заявил, что "стратегия корпорации целиком и пол­
ностью будет определяться платформой .Net" и что все продукты компании
со временем будут переписаны с учетом этой платформы.
Первая версия .NET появилась лишь два года спустя - 1 мая 2002 года. В
целом, таблица 1.1 содержит информацию обо всех версиях .NET, выпущен­
ных с 2002 года.
Таблица 1.1. Версии .NET
Версия

Дата выхода

1.0

1 мая 2002 г.

1.1

Visual Studio
Visual Studio
.NET

По умолчанию

Заменяет

в Windows

версию

-

-

1 апреля 2003

Visual Studio

Windows Server

г.

.NET 2003

2003

1.0

*

Windows Vista,
2.0

11 июня 2005 г.

Visual Studio

Windows 7,

2005

Windows Server

-

2008 R2
Windows Vista,
3.0

6 ноября 2006
г.

Visual Studio

Windows Server

2005 + расш и­

2008, Windows

рения

7, Windows Serv­

2.0

er 2008 R2
3.5

4.0

4.5

9 ноября 2007

Visual Studio

г.

2008

12 апреля 2010 «

Visual Studio

г.

2010

15 августа

Visual Studio

2012 г.

2012

17 октября

Visual Studio

2013 г.

2013

4.5.2

5 мая 2014 г.

-

4.6

20 июля 2015 г.

4.5.1

Windows Server
Windows 8,
Windows Server

4.0

2012
Windows 8.1,
Windows Server

4.0, 4.5

2012 R2
-

4 .0 -4 .5 .1

Windows 10

4 .0 -4 .5 .2

2015 Update 1

sion 1511

CD

Windows 10 Ver­

5 апреля 2017

-

2012

Visual Studio

4.7

4.7.2

Windows 8,

г.

20 июля 2016

2.0, 3.0

2008 R2

17 ноября 2015

4.6.2

4.7.1

2015

Windows Server

i
o

4.6.1

Visual Studio

Windows 7,

4.0-4.6.1
Visual Studio

Windows 10

2017

v1703

4.0-4.6.2

Windows 10

17 октября

Visual Studio

2017

2017 v15.5

30 апреля

Visual Studio

Windows 10

2018

2017 v15.8

v1803

v1709, Windows

4.0-4.7

Server 2016
4.0-4.7.1

Первый релиз .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 должна была назы­
ваться W nFX, что должно было отражать ее суть, а именно добавленные в
ее состав компоненты:


Windows Presentation Foundation (W PF) —презентационная графи­
ческая подсистема, использующая XAML;



Windows Communication Foundation (WCF) —программная модель
межплатформенного взаимодействия;



Windows Workflow Foundation (W F) — технология определения,
выполнения и управления рабочими процессами;



Windows CardSpace — технология унифицированной идентифика­
ции.

Расширенной поддержки этой версии не было, а обычная уже закончилась
- еще в 2011 году.
Версия 3.5 поддерживает C# 3.0, VB.NET 9.0, в ней также расширена функ­
циональность WF и WCF (см. выше), добавлена поддержка ASP.NET, до­
бавлено новое пространство имен.
Версия 4.0 появилась в 2010 году вместе с Visual Studio 2010. Нововведений
довольно много:


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



Поддержка подмножеств .NET Framework и ASP.NET в варианте
Server Core;



Библиотека параллельных задач (Task Parallel Library), предназна­
ченная для упрощения создания распределенных систем;



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



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

Версия 4.5 появилась в августе 2012 года, и ее характерной особенностью
является отсутствие поддержки Windows ХР. Основные особенности этой
версии:


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



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



Уменьшение количества перезапусков системы посредством обна­
ружения и закрытия приложений платформы .NET Framework вер­
сии 4 во время развертывания;



Фоновая компиляция по требованию О IT), которая доступна на
многоядерных процессорах для повышения производительности
приложения;



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



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

И это только начало. Нововведений очень много и дополнительную инфор­
мацию вы можете получить по адресу:

http://www.codeproject.com/Articles/599756/Five-Great-NETFramework-Features
Предпоследняя на момент написания этих строк версия - 4.6. Она являет­
ся обновлением для версии 4.0 - 4.5.2. Устанавливается при необходимости
вместе с версией 3.5 SP1, поставляется вместе с Visual Studio 2015.
В версии 4.6 появились новый J IT -компилятор для 64-разрядных систем,
поддержка последней версии CryptoAPI от Microsoft, а также поддержива­
ются TLS 1.1 и 1.2.
Самая последняя версия - 4.7.2 которая появилась относительно недавно.
Является обновлением для версий 4.0 - 4.7.1 и поставляется вместе с Visual
Studio 2017.

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

Таблица 1.2. Поддержка ОС
Версия
Windows

1.0

Windows 98

+

Windows NT

+

Windows

1.1 2.0 3.0

4.0

4.5 4.5.2

4.6

4.6.1

4.7 4.7.1.

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

Windows 8

+

+

+

+

+

+

+

+

+

Windows 8.1

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

2000
Windows XP
Windows
Server 2003
Windows
Server 2008
Windows
Vista
Windows 7
Windows
Server 2008
R2
Windows
Server 2012

Windows
Server 2012
R2
Windows 10

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

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

Первым языком программирования, с которого многие начинают свою
карьеру программиста, является Basic (Beginner’s All-purpose Symbolic In­
struction Code). Данный язык специально был предназначен для новичков,
он очень прост, и обучение программированию часто начинают именно с
этого языка. Язык сам по себе довольно древний - он был разработан в 1964
году, а вторую жизнь Basic получил с появлением Visual Basic от Microsoft.
Чтобы облегчить себе жизнь, многие программисты перешли с C /C + + в
мир простых и более дружественных языков вроде Visual Basic 6.0 (VB6).
VB6 предоставляет возможность создавать сложные пользовательские ин­
терфейсы, библиотеки программного кода (вроде COM-серверов) и логику
доступа к базам данных. И все это на VB6 делается очень просто, в чем и
заключается причина его популярности.
Основной недостаток языка VB6 - в том, что он не является полностью объ­
ектно-ориентированным. Можно сказать, что он просто “объектный”. Так,
VB6 не позволяет программисту устанавливать между классами отношения
“подчиненности” (т.е. прибегать к классическому наследованию) и не об­
ладает никакой внутренней поддержкой для создания параметризованных
классов. Также VB6 не позволяет создавать многопоточные приложения,
если программист не готов работать на уровне Windows API (это сложно и
довольно опасно).
Однако с появлением .NET все недостатки VB6 устранены, правда, новый
язык теперь имеет мало общего с VB6 и называется VB.NET. В этом совре­
менном языке поддерживается переопределение операций (перегрузка),
классическое наследование, конструкторы типов, обобщения и многое дру­
гое.
1.4.4. Язык Java

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

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

Вот мы и добрались к пред-.NET решению. Модель COM (Component
Object Model - модель компонентных объектов) была предшествующей
платформой для разработки приложений, которая предлагалась Microsoft
перед .NET. Впервые СОМ появилась в 1993 году, так что модель сама по
себе уже является довольно древней, учитывая скорость развития техно­
логий.
Модель СОМ позволяет строить типы в соответствии с правилами СОМ
и получать блок многократно используемого двоичного кода. Такие дво­
ичные блоки кода называют “серверами СО М ”. Одно из преимуществ
сервера СОМ в том, что к нему можно получить доступ, используя дру­
гой язык программирования. Например, кто-то может создать СОМобъект на C ++, а вы можете использовать Delphi и подключаться к
СОМ-серверу.
Понятно, что подобная независимость СОМ от языка является немного
ограниченной. Например, нет способа породить новый COM-класс с ис­
пользованием уже существующего (СОМ не поддерживает классическое
наследие). Но это уже нюансы.
Если говорить о преимуществах, то одно из преимуществ СОМ - прозрач­
ность расположения. За счет применения различных конструкций (проксисерверы, заглушки, AppID) программисты могут избежать необходимости
иметь дело с низким уровнем - сокетами, RPC-вызовами и т.д.

Модель СОМ можно считать успешной объектной моделью, однако ее вну­
треннее устройство является очень сложным для восприятия, именно по­
этому программистам требуются месяцы на изучение этой модели.
Для облегчения процесса разработки двоичных CO M -объектов про­
граммисты могут использовать многочисленные платформы, поддер­
живающие СОМ. Так, в ATL (Active Template Library — библиотека ак­
тивных шаблонов) для упрощения процесса создания СОМ-серверов
предоставляется набор специальных классов, шаблонов и макросов на
C ++. Во многих других языках сложная инфраструктура СОМ также
скрывается из вида. Но поддержки одного только языка для сокрытия
всей сложности СОМ - мало. Даже при выборе относительно простого
языка с поддержкой СОМ (пусть это будет V B6) все равно програм­
мисту нужно бороться с записями о регистрации и многочисленными
деталями развертывания. Все это называется адом D LL (D L L Hell).
Конечно, СОМ упрощает процесс создания приложений с помощью
разных языков программирования. Но независимая от языка природа
СОМ не настолько проста, как нам бы этого хотелось. Вся эта слож­
ность - следствие того, что приложения, написанные на разных язы ­
ках, получаются совершенно не связанными с точки зрения синтаксиса.
Взять, например, языки Ja v a и Си - их синтаксисы очень похожи, но
вот VB6 вообще никак не похож на Си. Его корни уходят в Basic. А
CO M -серверы, созданные для выполнения в среде СО М + (она пред­
ставляет собой компонент ОС Windows, предлагающий общие службы
для библиотек специального кода, такие как транзакции, жизненный
цикл объектов и т.д.), имеют совершенно другое поведение, чем ориен­
тированные на использование в веб-сети A SP -страницы, в которых они
вызываются.
В результате получается очень запутанная смесь технологий. Каждый
A PI-интерфейс поставляется с собственной коллекцией уже готового
кода, а базовые типы данных не всегда интерпретируются одинаково.
У каждого языка своя собственная и уникальная система типов. Изза этого CO M -разработчикам нужно соблюдать предельную осторож­
ность при создании общедоступных методов в общедоступных классах
СОМ. Если вам, например, нужно создать метод на C ++, который бы
возвращал массив целых чисел в приложение на VB6, то вам придется
полностью погрузиться в сложные вызовы A PI-интерфейса СОМ для
создания структуры безопасного массива. Ведь если разработчик на
C + + просто вернет собственный массив, приложение на VB6 просто не
поймет, что с ним делать.
*

1.5. Что предлагает нам .NET
Как видите, жизнь программиста Windows-приложений раньше была очень
трудной. Но с появлением .NET Framework все стало гораздо проще. Как
уже отмечалось, NET Framework представляет собой программную плат­
форму для создания приложений на базе семейства операционных систем
Windows, а также многочисленных операционных систем разработки не Mi­
crosoft, таких как Mac OS X и различные дистрибутивы Unix и Linux.
Основные функциональные возможности .NET:


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



Поддержка разных языков программирования (С#, Visual Basic, F#
и Т.Д.).



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



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



Огромная библиотека базовых классов. Она позволяет упростить
прямые вызовы к API-интерфейсу и предлагает согласованную объ­
ектную модель, которую могут использовать все языки .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 Type System) или
система CTS. Предоставляет полное описание всех возможных типов дан­
ных и программных конструкций, которые поддерживаются исполняющей
средой, а также способов, как все эти сущности могут взаимодействовать
друг с другом. Нужно понимать, что любая возможность CTS может не под­
держиваться в отдельно взятом языке, совместимом с .NET.
Именно поэтому существует третий компонент - CLS (Common Language
Specification) или спецификация CLS. В ней описано лишь то подмноже­
ство общих типов и программных конструкций, каковое способны воспри­
нимать все .NET языки. Следовательно, если вы используете типы .NET
только с функциональными возможностями, предусмотренными в CLS,
можете быть уверены, что все совместимые с .NET языки могут их и ис­
пользовать. Если же вы используете тип данных, которого нет в CLS, нет
гарантии того, что с этим типом данных сможет работать любой поддержи­
ваемый .NET язык. К счастью, существует способ указать компилятору С#,
чтобы он проверял весь код на предмет совместимости с CLS.
1.6.2. Библиотека базовых классов

Кроме среды CLR и спецификаций CTS и CLS, в составе платформы .NET
существует библиотека базовых классов. Она доступна для всех языков,
поддерживающих .NET. В этой библиотеке содержатся определения при­
митивов (потоки, файловый I/O, системы графической визуализации, ме­
ханизмы для взаимодействия с разными внешними устройствами), предо­
ставлена поддержка целого ряда служб, которые нужны в большинстве
реальных приложений.
В библиотеке базовых классов содержатся определения типов, которые мо­
гут упростить процесс доступа к базам данным, обеспечить безопасность,
создание обычных, консольных и веб-интерфейсов и т.д. На рис. 1.1 показа­
на связь между библиотекой базовых классов и компонентами .NET.
?

Рис. 1.1. Взаимосвязь между компонентами .NET и библиотекой базовых классов

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

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


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



Поддержка динамического поиска членов во время выполнения по­
средством ключевого слова dynamic.



Значительное упрощение обеспечение взаимодействия приложе­
ний на C# с унаследованными COM-серверами благодаря устране­
нию зависимости от сборок взаимодействия и предоставлению под­
держки необязательных аргументов ref.



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

Наиболее важный момент, который вы должны знать, программируя на С#:
с помощью этого языка можно создавать только такой код, который будет
выполняться в исполняющей среде .NET (то есть использовать C# для по­
строения “классического” COM-сервера или неуправляемого приложения с
вызовами API-интерфейса и кодом на Си и C ++ нельзя).
Код, ориентируемый на выполнение в исполняющей среде .NET, называет­
ся управляемым кодом (managed code). Код, который не может обслужи­
ваться непосредственно в исполняющей среде .NET, называют неуправляе­
мым кодом (unmanaged code).
Примечание. Вы должны также понимать, что C# - это не единственный язык,
который можно использовать для построения .NET-приложений. При установке
бесплатного комплекта разработки Microsoft .NET 4.0 Framework SDK (как и при
установке Visual Studio) для выбора доступны пять языков: С#, Visual Basic, C++,
JScript .Net, F#.

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

жат не специфические, а наоборот, не зависящие от платформы инструкции
на промежуточном языке (Intermediate Language — IL), а также метаданные
типов.
Работает это так. Исходный код на X проходит через компилятор X, кото­
рый генерирует инструкции IL и метаданные. Другими словами, все ком­
пиляторы всех поддерживаемых .NET языков генерируют одинаковые ин­
струкции IL и метаданные.
При создании DLL- или ЕХЕ-файла с помощью .NET-компилятора получа­
емый большой двоичный объект называется сборкой (assembly). Подробно
сборки будут рассмотрены в других главах этой книги, а сейчас нужно рас­
сказать хотя бы об основных свойствах этого формата файлов.
В сборке содержится CIL-код, который похож на байт-код Java тем, что не
компилируется в ориентированные на определенную платформу инструк­
ции до тех пор, пока это не становится действительно необходимым. Обыч­
но этот момент наступает, когда к какому-то блоку С IL-инструкций выпол­
няется обращение для его выполнения в среде .NET.
Кроме CIL-инструкций, в сборках есть также метаданные, описывающие
особенности каждого имеющегося внутри данной двоичной .NET-единицы
“типа”.
Сами сборки, кроме CIL и метаданных типов, также описываются с помо­
щью метаданных, называемых манифестом (manifest). В каждом манифе­
сте содержатся информация о текущей версии сборки, сведения о культуре
(применяемые для локализации строковых и графических ресурсов) и пе­
речень ссылок на все внешние сборки, которые требуются для правильного
функционирования (зависимости).
Сборки бывают однофайловыми и многофайловыми. В большинстве слу­
чаев между сборками .NET и файлами двоичного кода (.dll или .ехе) со­
блюдается простое соответствие “один к одному”. Поэтому при построении
DLL-библиотеки .NET можно полагать, что файл двоичного кода и сборка
- это одно и то же. Однако это не совсем так. С технической точки зрения,
сборка, состоящая из одного модуля (.dll или .ехе), называется однофайло­
вой. В такой сборке все необходимые C IL-инструкции, метаданные и мани­
фесты содержатся в одном автономном, четко определенном пакете.
Многофайловые сборки состоят из множества файлов двоичного кода .NET,
каждый из которых называется модулем (module). При построении много­
файловой сборки в одном из ее модулей содержится манифест всей самой
сборки, а во всех остальных - манифест, CIL-инструкции и метаданныети­
пов, охватывающие уровень только соответствующего модуля. В главном

модуле содержится описание набора требуемых дополнительных модулей
внутри манифеста сборки. Сборки будут рассмотрены в главе 9.

1.9. Подробно о CTS
В каждой конкретной сборке может содержаться любое количество самых
разных типов. В мире .NET "тип" - это просто общий термин, который мо­
жет использоваться для обозначения любого элемента из множества (класс,
интерфейс, структура, перечисление, делегат).
При построении решений .NET, скорее всего, придется взаимодействовать
со многими из этих типов. Так, в сборке может содержаться один класс, ре­
ализующий определенное количество интерфейсов, метод одного из кото­
рых может принимать в качестве параметра перечисление, а возвращать массив.
CTS представляет собой формальную спецификацию, в которой описано
то, как должны быть определены типы для того, чтобы они могли обслу­
живаться в CLR-среде. Всем .NET-разработчикам важно уметь работать
на предпочитаемом ими языке с пятью типами из CTS. Далее приводится
краткий обзор этих типов.
1.9.1. Типы классов

В каждом совместимом с .NET языке поддерживается, как минимум, по­
нятие типа класса (class type), которое играет центральную роль в объек­
тно-ориентированном программировании (далее - ООП). Каждый класс
может содержать произвольное количество членов (конструкторы, свой­
ства, методы и события) и точек данных (полей). В С#, как и в других
языках программирования, для объявления класса используется ключе­
вое слово class:
class Car
{
public int Run()
{ return 1; }

}
В таблице 1.3 приводится краткий перечень характеристик, свойственных
типам классов.

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

Х ар актер и сти ка
Степень видимости

Абстрактные и
кретные классы

кон­

Запечатанные

Реализующие
фейсы

интер­

О писание
Каждый класс должен настраиваться с атрибутом ви­
димости (visibility). По сути, данный атрибут указывает,
должен ли класс быть доступным внешним сборкам
или его можно использовать только внутри опреде­
ленной сборки.
Экземпляры абстрактных классов не могут со зд а­
ваться напрямую и предназначены для определения
общих аспектов поведения для произвольных типов.
Экземпляры конкретных классов могут создаваться
напрямую.
Запечатанные (sealed) классы не могут выступать в
роли базовых для других классов, то есть не поддер­
живают наследия.
Интерфейс (interface) - это коллекция абстрактных
членов, которые обеспечивают возможность взаи­
модействия между объектом и пользователем этого
объекта. CTS позволяет реализовать в классе любое
количество интерфейсов.

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

Интерфейсы представляют собой именованную коллекцию определений
абстрактных членов, которые могут поддерживаться в данном классе или
структуре. В C# типы интерфейсов определяются с помощью ключевого
слова interface. Пример:
p u b l i c i n t e r f a c e I D r iv e
{
vo id P r e s s ( ) ;

}
От самих интерфейсов проку мало. Однако, если они реализованы в клас­
сах или структурах, они позволяют получить доступ к дополнительным
функциональным возможностям за счет добавления просто ссылки на них
в полиморфной форме.
1.9.3. Типы структур

В CTS есть понятие структуры. Если вы раньше программировали на Си, то
будете приятно удивлены, что в современном мире .NET нашлось место для
вашего любимого типа данных. Структура может считаться "облегченной"
версией класса. Обычно структуры лучше подходят для моделирования
математических данных. В C# структуры определяются ключевым словом
struct:
s t r u c t Nums

{
p u b li c i n t x s , y s ;
p u b li c N u m s(in t x , i n t y)
p u b li c i n t Add()

( x s = x ; ys = y;

}

{
r e tu r n x s + y s ;

}
}
Обратите внимание: структуры могут содержать конструкторы и методы подобно классам.
1.9.4. Типы перечислений

Перечисления (enumeration) - удобная программная конструкция, позволя­
ющая сгруппировать данные в пары "имя-значение". Например:
p u b l i c enum eNums
{
А = 1,
В = 2,
С = 3
}

По умолчанию для хранения каждого элемента выделяется блок памяти,
который соответствует 32-битному целому, но при необходимости (напри­
мер, если нужно экономить оперативную память при создании приложений
для устройств с небольшим объемом ОЗУ) это значение можно изменить.
Кроме того, в CTS сделано так, чтобы перечисляемые типы наследовались
от общего базового класса System.Enum.
1.9.5. Типы делегатов

Делегаты (delegate) - являются .NET-эквивалентом безопасных в отноше­
нии типов указателей функций в стиле Си. Основное отличие заключается
в том, что делегат в .NET представляет собой класс, который наследуется
от System.MulticastDelegate, а не просто указатель на какой-то конкрет­
ный адрес в памяти. Объявить делегат можно с помощью ключевого слова
delegate:
p u b l i c d e l e g a t e i n t A d d O p (in t x ,

in t y ) ;

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

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

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

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

C#

C++

Visual Basic

System. ByteByte

byte

unsigned char

Byte

System .SByteSByte

SByte

sbyte

signed char

System. Int 16

short

short

Short

System. Int32

int

int или long

Integer

System. Int64

long

_int64

Long

System.Ulnt16

ushort

unsigned short

UShort

System.Ulnt32

uint

unsigned int

Ulnteger

System.Unit64

ulong

unsigned_int64

ULong

System .SingleSingle

float

float

Single

System. DoubleDouble

double

double

Double

System .ObjectObject

object

object"

Object

System .CharChar

char

wchar_t

Char

System .Stringstring

String

String"

String

System.Decim alDecim al

decimal

Decimal

Decimal

System .Boolean Boolean

bool

bool

Boolean

При этом в C# можно указать названия типов из CTS:
long х=0;
System.Int64 у = 0;

1.10. Подробно о CLS
CLS (Common Language Specification — общая спецификация для языков
программирования) представляет собой набор правил, которые подробно
описывают минимальный и полный набор функциональных возможностей,
которые должен обязательно поддерживать каждый отдельно взятый .NET-

компилятор, чтобы генерировать такой программный код, который мог бы
обслуживаться CLR.
CLS можно считать просто подмножеством всех функциональных возмож­
ностей, определенных в CTS. В конечном итоге CLS является своего рода
набором правил, которых должны придерживаться создатели компилято­
ров, если они хотят чтобы их продукты могли без особых проблем функци­
онировать в мире .NET.
У каждого такого правила есть простое название, и оно описывает, как
именно его действие касается тех, кто создает компиляторы, и тех, кто будет
с ними взаимодействовать.
Самое главное правило в CLS гласит, что правила CLS касаются только тех
частей типа, которые делаются доступными за пределами сборки, в которой
они определены. Отсюда следует, что все остальные правила в CLS не рас­
пространяются на логику, применяемую для построения внутренних рабо­
чих деталей типа .NET.
Самое интересное, что в самом C# есть целый ряд программных конструк­
ций, не соответствующих правилам CLS. Однако компилятор C# можно за­
ставить выполнять проверку программного кода на предмет соответствия
правилам CLS с помощью атрибута .NET:
[assem b ly : S y ste m .C L S C o m p lia n t(tru e )]

ны нарушения каких-либо правил CLS, компилятор выдаст ошибку и опи­
сание вызвавшего ее кода. На рис. 1.2 показано, что атрибут [CLSCompliant]
задан для сборки System.Dll. Утилиту Reflector можно скачать по адресу
http://www.red-gate.com/products/reflector.

1.11. Подробно о CLR
CLR (Common Language Runtime) - общеязыковая исполняющая среда.
CLR можно расценивать как коллекцию внешних служб, которые необхо­
димы для выполнения скомпилированной единицы программного кода.
Например, при использовании платформы MFC для создания нового при­
ложения программист осознает, что приложению понадобится библиотека
времени выполнения MFC (то есть mfc40.dll).
Аналогично и с другими языками. VB-программист понимает, что дол­
жен привязываться к одному или двум модулям исполняющей среды
(msvbvm60.dll), а Java-программисты привязаны к виртуальной машине
Java (JVM ).
В составе .NET есть еще одна исполняющая среда. Основное отличие меж­
ду исполняющей средой .NET и вышеупомянутыми средами заключается в
том, что исполняющая среда .NET обеспечивает единый четко определен­
ный уровень выполнения. Этот уровень выполнения способны использо­
вать все совместимые с .NET языки и платформы.
Физически основной механизм CLR представляет собой библиотеку под
названием mscoree.dll. Название M SCOREE - это аббревиатура от Microsoft
Common Object Runtime Execution Engine - общий механизм выполнения
исполняемого кода объектов.
Когда добавляется ссылка на сборку, предварительно автоматически загру­
жается библиотека mscoree.dll, а уже после этого загружается сборка в па­
мять и происходит ее выполнение.
Механизм исполняющей среды отвечает за выполнение целого ряда задач.
Первым делом отвечает за определение места расположения сборки и обна­
ружение запрашиваемого типа в двоичном файле за счет считывания содер­
жащихся там метаданных. Также он размещает тип в памяти, преобразует
C IL-код в соответствующие платформе инструкции, производит проверки
безопасности и запрашивает программный код.

1.12. Пространства имен
Все обилие типов, которое можно использовать в .NET, содержится в много­
численных пространствах (namespaces) имен .NET. Основное пространство

О ............................................*

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

System
System.Collections

System. Data
System. 10

System. Reflection

System.Runtime
System.Drawing
System .Windows .Forms
System.Windows
System. Linq
System.Web
System.ServiceModel
System.Xml
System.Security

Описание

Содержит много полезных типов, позволяющих рабо­
тать с математическими вычислениями, генератором
случайных чисел, переменными среды и т.д.
Содержит ряд контейнерных типов, а также несколько
базовых типов и интерфейсов, позволяющих созда­
вать специальные коллекции
Используется для взаимодействия с базами данных
через ADO.NET
Здесь содержится много типов, предназначенных для
работы с операциями файлового ввода/вывода, сжа­
тия данных, портами
Содержит типы, которые поддерживают обнаружение
типов во время выполнения, а также динамическое
создание типов
Содержит средства, позволяющие взаимодейство­
вать с неуправляемым кодом, точнее, эти средства
находятся в System.Runtime.InteropServiceS
Содержит типы, применяемые для построения настольных приложений (Windows Forms)
Является корневым для нескольких пространств имен,
предоставляющих набор графических инструментов
WPF (Windows Presentation Foundation)
Содержит типы, применяемые при выполнении про­
граммирования с использованием API LINQ
Позволяет создавать веб-приложения ASP.NET
Позволяет создавать распределенные приложения
с помощью API-интерфейса Windows Communication
Foundation (WCF)
Здесь содержатся многочисленные типы, которые ис­
пользуются при работе с XML-данными
Типы, имеющие дело с разрешениями, криптографи­
ей и т.д.

System.Threading

System.Workflow

Средства для создания многопоточных приложений,
способных разделить нагрузку среди нескольких про­
цессоров
Типы для построения поддерживающих рабочие по­
токи приложений с помощью API-интерфейса Windows
Workflow Foundation (WWF)

Для подключения пространства имен в C# используется ключевое слово
using, например:
u sin g
u sin g
u sin g

System ;
S y ste m .D ra w in g ;
S y ste m .W in d o w s. F o rm s;

На этом все. В следущей главе мы поговорим о развертывании среды .NET
и о создании приложений на языке С#.

?

Глава 2.

Развертывание .NET и
первая программа

2.1. Развертывание у заказчика
Как узнать, какая версия .NET Framework установлена у заказчика? Мож­
но воспользоваться специальными программами для определения версии
.NET Framework. Одна из таких программ - ASoft .NET Version Detector,
скачать которую совершенно бесплатно можно по адресу:
http://net-framework.ru/soft/asoft-net-version-detector

Рис. 2.1. Программа ASoft .NET Version Detector
Внимание! Загружать .NET Framework нужно только с официального сайта. Что

платформа .NET Framework, что пакет разработчика Visual Studio Community Edi­
tion - совершенно бесплатны. Для их загрузки никакая регистрация не нужна. По­
этому нет никакого смысла загружать их со сторонних сайтов - очень часто таким
образом распространяются вирусы и прочие вредоносные программы - вместе с
загружаемым продуктом вы получаете “в нагрузку’ вирус.

Однако текущая версия этой программы сообщает, что номер версии .NET
не определен, поскольку она просто не умеет определять версии выше 4.6
RC. Можно попытаться найти другую программу, а можно заглянуть в ре­
естр.
Гораздо проще открыть редактор реестра (regedit.exe) и обратиться к вет­
ке Н КЕ Y_LO C A L M АС Н INE\SOFTW ARE\ Microsoft\NET Framework
Setup\NDP. Подразделы этой ветки с именами, начинающимися на “v.”, по­
зволяют судить об установленных версиях платформы .NET.
Посмотрим, так ли это. На рис. 2.2 показано, что установлены версии 2.0,
2.0, 2.5, 4.0. Кстати, это установка Windows 10 Enterprise по умолчанию,
безо всяких обновлений.

Вот только судить только по названиям веток неправильно. Откройте под­
раздел v4\Full и посмотрите на значения параметров Release и Version.
Первый параметр означает номер релиза .NET Framework - по нему можно
более точно определить номер версии. Однако гораздо проще взглянуть на
параметр Version - на рис. 2.3 видно, что установлена версия 4.6.01038.
Можно вообще ничего не определять, а просто скачать инсталлятор послед­
ней версии (на данный момент это 4.7.1). В случае, если установлена эта
или более поздняя версия, инсталлятор сообщит вам об этом (рис. 2.4).

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

Рис. 2.4. Установщик .NET Framework 4.6.1: установлена более новаа версии

*

В коде программы определить версию .NET можно так, как показано в ли­
стинге 2.1. Именно этот код приводится на сайте https://msdn.microsoft.com
и рекомендуется для определения версии .NET самой Microsoft.

Листинг 9 1 Птцущрргсние версии .WET Framework на C#
using System;
using Microsoft.Win32;
private static void GetVersionFromRegistry()

<
// Открываем раздел реестра и читаем версию .NET
using (RegistryKey ndpKey =
RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, "").
OpenSubKey(0"SOFTWARE\Microsoft\NET Framework Setup\NDP\"))

(
// В качестве альтернативы, если установлена версия 4.5 или выше,
// можно использовать следующий запрос
// (RegistryKey ndpKey =
// RegistryKey.OpenBaseKey(RegistryHive.LocalMachine,
// RegistryView.Registry32).
// OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\"))
foreach (string versionKeyName in ndpKey.GetSubKdyNames())

{
if (versionKeyName.StartsWith C'v"))

{
RegistryKey versionKey = ndpKey.OpenSubKey(versionKeyName);
string name = (string)versionKey.GetValue("Version", "");
string sp = versionKey.GetValue("SP", "").ToString();
string install = versionKey.GetValue("Install", "") .ToString () ;
if (install == "") //no install info, must be later.
Console.WriteLine(versionKeyName + " " + name);
else

<
if (sp != "" SS install == "1")

(
Console.WriteLine(versionKeyName + "

" + name + "

SP" + sp) ;

)
)
if (name != "")

(
continue;

>
foreach (string subKeyName in versionKey.GetSubKeyNames())

{
RegistryKey subKey = versionKey.OpenSubKey(subKeyName);
name = (string)subKey.GetValue("Version", "");
if (name != "")
sp = subKey.GetValue("SP", "").ToString();
install = subKey.GetValue("Install", "").ToString();
if (install == "") //нет инфо об установке
Console.WriteLine(versionKeyName + " " + name);

else

{
if (sp != "" && install == "1")

(
Console.WriteLine("

" + subKeyName + "

" + name + "

SP” + sp) ;

)
else if (install == "1")

{
Console.WriteLine("

)

)

)

)

)

)

" + subKeyName + "

" + name);

)

Если у заказчика Windows 10, то, скорее всего, обновлять .NET Framework
не придется, поскольку будет установлена одна из последних версий. В бо­
лее старых выпусках Windows могут быть установлены старые версии плат­
формы, поэтому вполне вероятно, что придется обновлять .NET Framework.
Как обновить .NET Framework? Для этого произведите поиск в Google по
поисковой фразе ’’.NET Framework 4.6 download" (вы же помните, что за­
гружать .NET Framework можно только с сайта Microsoft) или перейдите по
одному из следующих адресов:

https://www.microsoft.com/ru-ru/download/details.aspx?id=48130
https://www.microsoft.com/ru-ru/download/details.aspx?id=49982
Первый адрес - это веб-инсталлятор платформы. Вы загружаете небольшой
файл, запускаете его, а все необходимые файлы он получает с Интернета.
Второй адрес - это offline-установщик. На рис. 2.5 изображена страница
загрузки offline-инсталлятора. Будет загружен файл NDP461-KB3102436x86-x64-AllOS-ENU.exe размером 66 Мб. Запустите его и следуйте ин­
струкциям мастера установки. По окончанию установки понадобится пере­
загрузка компьютера.
Ясное дело, что когда у вас будет серьезный проект, вы создадите для него
инсталлятор, который будет автоматически загружать нужную версию
.NET Framework. Поэтому данный раздел предназначен больше не для за­
казчика, а для вас - программиста. Ведь пока вы еще не создали этот самый
серьезный проект, то и инсталлятор вам еще не нужен. Но вам нужно проте­
стировать свою программу на другой машине (отличной от той, на которой
вы ведете разработку), а без .NET Framework она не запустится. Поэтому
вы должны знать, что и откуда можно скачать, чтобы ваша программа за­
работала.

Рис. 2.5. Страница загрузки offline-инсталлятора

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

процессов, происходящих при создании программного обеспечения. Я уже
молчу о возможности быстрой разработки форм. Разработать форму гра­
фического приложения в блокноте будет очень сложно. Поэтому без среды
все равно не обойтись. В Microsoft это понимают, и поэтому сейчас загрузка
.NET Framework SDK невозможна без установки Visual Studio.
Но ведь раньше можно было установить .NET Framework SD K совершенно
бесплатно, а как же сейчас? Дискриминация?! Нет, в Microsoft выпустили
бесплатную версию Visual Studio - Community, с которой вы познакомитесь
чуть позже. Если вы уже установили Visual Studio, то устанавливать .NET
Framework SDK вам не нужно, так как он уже входит в ее состав.
Перейдите по адресу http://msdn.microsoft.com/en-us/vstudio/aa496123.
На этой же страничке можно скачать не только SDK, но и установщики са­
мой платформы разных версий (см. рис. 2.6).

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

*

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

http://www.visualstudio.com/products/visual-studio-community-vs
Вы скачаете файл vs_community_ENU.exe. Инсталлятор очень небольшо­
го размера - ведь загрузка необходимых файлов осуществляется с серве­
ра Microsoft, поэтому не отключайте Интернет во время установки Visual
Studio.
Примечание. Понятное дело, что Visual Studio Community - несколько ограничен­

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

На рис. 2.7 показан инсталлятор Visual Studio Community. Установка по
умолчанию занимает 8 Гб - это минимальная установка. Можно установить
ее, а можно выбрать Custom и установить дополнительные компоненты
(рис. 2.8).

Рис. 2.7. Инсталлятор Visual Studio
Community

Рис. 2.8. Выбираем компоненты Visual
Studio Community
_____

............................................................ d

В зависимости от расторопности вашего компьютера, установка Visual
Studio может занимать от нескольких десятков минут до пары часов. При
первом запуске среда предложит вам подключиться к сервисам для раз­
работчиков - вы можете получить собственный Git-репозитарий (систем
управления версиями), синхронизировать свои настройки и другие воз­
можности (рис. 2.9). Вы можете зарегистрироваться прямо сейчас, а можете
- при следующем запуске IDE.

Рис. 2.9. Предложение зарегистрироваться

Рис. 2.10. Выбор темы оформления

Далее Visual Studio предложит выбрать тему (ее также можно будет изме­
нить позже) - синяя (по умолчанию), темная или светлая (рис. 2.10). Хотя
по умолчанию используется синяя тема, для этой книги с целью улучшения
иллюстраций будет использована светлая.

2.3. Первая программа с использованием
Visual Studio
Настало время создать наше первое приложение на С#. Основное окно
Visual Studio изображено на рис. 2.11. Для создания нового проекта вос­
пользуйтесь ссылкой New Project или выберите команду меню File * New
*• Project. В окне New Project выберите тип приложения. Наиболее часто
используемый тип Windows Forms Application - обычное приложение с

Рис. 2.11. Основное окно

графическим интерфейсом (формой). Но для первого приложения выбери­
те Console Application.
Введите название проекта - Hello и нажмите кнопку ОК. Созданный проект
показан на рис. 2.12. Среда подготовила основной код для нашего консоль-

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

*

ного приложения. Изменим его так, как показано в листинге 2.2. Особо это
приложение мы изменять не станем, а лишь добавим вывод строки "Hello,
world!" (не будем изменять традиции) на консоль.
Листинг 2.2. Приложение Hello, world!
using
using
using
using
using

S y st e m ;
System .Collections.Generic;
System.L inq;
System.Text;
System.Threading.Tasks;

namespace H e l l o
{
c l a s s Program

<
s t a t i c v o i d M a i n ( s t i n g [] s t r )

{
Console.WriteLine("Hello, w orld!" ) ;

}
}
t

Примечание. По умолчанию среда подключает избыточные пространства имен.
Для нашего очень "сложного" проекта хватило бы одного пространства имен System.

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

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

Для запуска проекта можно нажать кнопку Start на панели инструментов.
Однако будет открыто окно командной строки, которое моментально за­
кроется, как только завершится выполнение программы, а поскольку она
выводит только одну строку, то вы и глазом не успеете моргнуть, как окно
закроется. Поэтому откройте командную строку и перейдите в каталог про­
екта, содержащий исполнимый файл (в нашем случае это c:\users\\
documents\visual studio 2015\Projects\Hello\Hello\bin\Debug\). Результат
выполнения нашей программы приведен на рис. 2.15.

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

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

?

Глава 3.

Основные конструкции
языка C #

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

3 .1 . Исследование программы Hello,
w orld!
3.1.1. Пространства имен, объекты, методы

В предыдущей главе нами была написана очень простая программа, выво­
дящая строку Hello, world! Чтобы вы не листали книгу, привожу ее код еще
раз - в листинге 3.1.
Листинг 3.1. Простая программа
using
using
using
using
using

S yst em;
System .Collections.Generic;
System.Linq;
Syste.Text;
System.Threading.Tasks;

namespace H e l l o
{
c l a s s Program
{
s t a t i c v o i d M a i n ( s t i n g [] s t r )

(
Console.W riteLine("Hello, w orld!" ) ;

}

)

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

Посмотрите на листинг 3.1. В нем создано пространство имен Hello. Вну­
три него объявлен класс Program, в котором есть метод Main(), выводящий
строку на экран. Для простых программ такой подход кажется запутанным,
но все окупится, когда приложения станут отнюдь не простыми.
Важным элементом любой программы являются комментарии. В C# ис­
пользуются традиционные комментарии в стиле Си, которые бывают одно­
строчные ( / / ) и многострочные ( /* .. */).
Пример:
//
/*

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

В первом случае комментарием считаются символы, начинающиеся от / / и
до конца строки. Во втором случае игнорируются все символы между / * и
7Комментарии могут быть встроенными, например:
Som eM ethod

(W idth ,

/*H e ig h t*/

200);

Такие комментарии использовать не рекомендуется, так как они могут
ухудшить читабельность кода.
Если символы комментария включены в строковой литерал, то они счита­
ются обычным кодом программы, например:
C o n so le .W rite L in e ( " / / э т а

с т р о к а не б у д ет с ч и т а т ь с я к о м м ен тари ем ");

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

О писание

С (или с)

Форматирование денежных значений.

Э(илиб)

Форматирование десятичных чисел.

Е (или е)

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

F (nanf)

Числа с фиксированной точкой. Число после символа
означает количество знаков после точки, например,
{0:f3} выведет 1.000, если в качестве 0 указать значе­
ние, равное 1.

G (или д)

Общий формат.

N (или п)

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

X (или х)

Форматирование шестнадцатеричных чисел. Если ука­
зан X, то в hex-представлении символы будут в верхнем
регистре, например, 1АН, а не 1ah, если указан х.

Примеры:
Console.WriteLine
Console.WriteLine
Console.WriteLine
Console.WriteLine
Console.WriteLine

("Значение 123456 в разных форматах:")
Cd7: {0 :d 7 )", 123456);
("с: {0:с )", 123456);
("п: {0:п )", 123456);
("f3: {0:f3}", 123456);

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

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

Не нужно думать, что приведенные сведения, раз они предназначены для
консоли, то не пригодятся вам при работе с графическим интерфейсом. От­
форматировать можно любую строку, а потом вывести ее средствами графи­
ческого интерфейса:
string Msg = string.Format("d7: (0:d7}", 123456);
Windows.Forms.MessageBox.Show(Msg);

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

3.3. Типы данных и переменные
3.3.1. Системные типы данных

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

Систем ны й
тип

Д иапазон

C LS

bool

Syste.Boolean

true, false

Да

sbyte

System .SByte

-128...127

Нет

byte

System. Byte

0...255

Да

short

System.Inti 6

-32 768...32 767

Да

ushort

System.Inti 6

0...65 535

Нет

Описание

Логическое (булевое)
значение
8-битное число со
знаком
8-битное число без
знака
16-битное число со
знаком
16-битное число без
знака

-2 147 483 648
int

System. Int32

Да

32-битное число со
знаком

Нет

32-битное число без
знака

Да

64-битное число со
знаком

Нет

64-битное число без
знака

2 147 483 647
uint

System.Ulnt32

0...4 294 967
295
-9 223 372 036
854 775 808

long

System. Int64
9 223 372 036
854 775 807

ulong

System.Ulnt64

0... 18 446
744073 709 551
615

Да

Один символ (16 бит,
Unicode)

+ I,5 x l0 '45...
3,4x1038

Да

32-битное число с пла­
вающей точкой

System. Decimal

± l, 0 x l0 e 28...
-7 ,9 x l0 28

Да

96-битное число со
знаком

System.Double

5 ,0 x l0 '324 ...
l J x l O 308

Да

64-битное число с пла­
вающей точкой

Да

Ряд символов в коди­
ровке Unicode

Да

Базовый класс для
всех типов в .NET

char

System .Char

float

System .Single

decimal
double

string

System.String

object

System. Object

3.3.2.

U+0000...

U+FFFF

Ограничен
объемом до­
ступной па­
мяти
Используется
для хранения
любого типа в
памяти

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

Объявление переменной осуществляется так же, как и в Си: сначала нужно
указать тип переменной, а затем - ее имя:
i n t X;

После объявления переменной ее нужно инициализировать (присвоить
значение) - до первого использования:
х = 0;

В случае использования локальной переменной до присваивания ей на­
чального значения компилятор сообщит об ошибке.
Инициализировать переменную можно при объявлении:
i n t х = 0;

При желании можно в одной строке объявить и инициализировать сразу
несколько переменных:
i n t х = 1 ; у = 0;
in t а,

Ь,

с;

3.3.3.

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

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


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



Для переменных числовых типов - значение 0;



Для типа string - один пустой символ;



Для типа DateTime - 1/1/0001 12:00:00;



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

Примеры:
b o o l b = new b o o l О ;

/ / б у д е т п р и с во е н о з н а ч е н и е f a l s e

i n t х = new i n t ( ) ;

/ / б у д е т п р и с во е н о з н а ч е н и е 0

Обычно с помощью new создаются объекты в классическом их понимании.
Для числовых/булевых/строковых переменных обычно проще указать зна­
чения при инициализации, чем использовать new:
bool b = f a ls e ;
i n t x = 0;

3.3.4.

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

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


Epsilon - эпсилон;



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



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

Пример:
i n t х = int.M axValue;
double d = d o u b le .E p silo n ;

Понятное дело, что тип данных System.Boolean не поддерживает свойства
MinValue и MaxValue. Но зато они поддерживают свойства TrueString и

FalseString, которые, соответственно, содержат строки “True” и “False”.
Пример:
Console.W riteLine("T rueString {0 }", b o o l. T ru e S trin g );
C on sole.W riteLin e("FalseStrin g (0 )", b o o l. F a ls e S t r in g ) ;

Текстовые данные в C# представлены типами System. String (или про­
сто string) или System.Char (char). Оба типа хранят данные в кодировке
Unicode. Первый тип данных позволяет хранить строку, второй - только
один символ.
Тип char поддерживает следующие методы:


IsDigit() - возвращает true, если переданный символ является деся­
тичной цифрой, в противном случае возвращается false;



IsWhiteSpace() - возвращает true, если переданный символ является
пробельным символом (пробел, табуляция и др);



IsPunctuation() * возвращает true, если переданный символ является
знаком пунктуации.

Пример:
C onsole.W riteLine( " { 0 } " ,

char. IsD igit ( '1 ') ) ;

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

Строки в любом языке программирования являются одним из самых важ­
ных типов данных. В C# для строк используется тип string (системный тип
System.String), а для хранения одиночных символов используется тип char.
В других языках программирования строки являются массивами симво­
лов. В языке C# строки являются объектами, что будет продемонстриро­
вано далее.
Объявить строку можно так:
s t r i n g [= " з н а ч е н и е " ] # ’

Значение строковой переменной указывается в двойных кавычках.
В C# можно также использовать и массивы символов, например:
chart]

c a r r a y = { ' е 1,

'х ',

'a ',

'm',

'р ',

'1 ',

' е ' }#’

Превратить массив символов в тип данных string можно так:
s t r i n g s t r = new s t r i n g ( c a r r a y ) ;

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

Настало время рассмотреть тип System.String, предоставляющий набор раз­
личных методов для работы с текстовыми данными (см. табл. 3.3). Далее
эти методы будут рассмотрены подробно.
Таблица 3.3. Некоторые члены типа System.String
Член

Описание

Length

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

Compare()

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

Contains()

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

Equals()

Метод, позволяющий проверить, являются ли две стро­
ки эквивалентными

FormatO

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

InsertO

Позволяет вставить строку внутрь другой строки

PadLeftO,
PadRight()

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

Removed

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

Replaced

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

Split»

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

Trimd

Удаляет все вхождения определенного набора символов
с начала и конца текущей строки

Tollpperd,
ToLowerd

Создаю т копию текущей строки, соответственно, в верх­
нем и нижнем регистре

Базовые операции

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

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

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

Обратите внимание на вывод этого кода (рис. 3.5). Методы ToLower(),
ToUpper(), Replace() и другие не изменяют строку, а работают с ее копией.
Они возвращают измененную копию строки. Если вы хотите изменить саму
строку, это делается так:
h = h.ToUpper() ;

Методы ToUpperQ и ToLowerQ объявлены так:
public string ToUpper ()
public string ToLowerO
Сравнение строк

Для сравнения строк используется метод Compare(). Существует много
разных форм вызова этого метода. Рассмотрим формы, использующиеся
для сравнения целых строк:
p u b lic
s t a t i c in t
p u b lic
s t a t i c in t
p u b lic
s t a t i c in t
comparisonType)
p u b lic s t a t i c in t

C o m p a r e (str in g s tr A , s t r i n g
C o m p a r e (str in g s tr A , s t r i n g
C o m p a r e (str in g s tr A , s t r i n g
C o m p are(strin g strA ,

strB )
s t r B , b o o l ig n o r e C a se )
s t r B , S trin g C o m p arison

s t r i n g s tr B , bool ig n o re -

Case, Culturelnfo culture)

В данном случае метод сравнивает строку strA со строкой strB. Возвра­
щает положительное значение, если строка strA больше строки strB; от­
рицательное значение, если строка strA меньше строки strB; и нуль, если
строки strA и strB равны. Сравнение выполняется с учетом регистра и
культурной среды.
Если параметр ignoreCase равен true, то при сравнении не учитывается ре­
гистр символов. В противном случае (false) эти различия учитываются.
Параметр comparisonType определяет конкретный способ сравнения строк.
Класс Culturelnfo определен в пространстве имен System.Globalization. Ис­
пользуется для лучшей локализации программы.
С помощью метода Сотраге() можно сравнивать фрагменты строк, а не це­
лые строки:
public static int Compare(string strA, int indexA, string strB, int
indexB, int length)
public static int Compare(string strA, int indexA, string strB, int
indexB, int length, bool ignoreCase)
public static int Compare(string strA, int indexA, string strB, int
indexB, int length, StringComparison comparisonType)
public static int Compare(string strA, int indexA, string strB, int
indexB, int length, bool ignoreCase, Culturelnfo culture)
public static int CompareOrdinal(string strA, string strB)
public static int CompareOrdinal(string strA,

int indexA,

string

strB, int indexB, int count)

Данная форма сравнивает фрагменты строк strA и strB. Сравнение начина­
ется со строковых элементов strA[indexA] и strB[indexB] и включает коли­
чество символов, определяемых параметром length.
Метод возвращает положительное значение, если часть строки strA боль­
ше части строки strB; отрицательное значение, если часть строки strA
меньше части строки strB; и нуль, если сравниваемые части строк strA и
strB равны. Сравнение выполняется с учетом регистра и культурной сре­
ды. Аналогично, можно указать параметры ignoreCase и Culturelnfo, как в
предыдущем случае.
Кроме метода Сошраге(), есть и метод CompareOrdinal(), который работает
так же, как и Сошраге(), но не имеет параметра CultureInfo() и локальных
установок. Метод объявлен так:
?

public static int CompareOrdinal(string strA, string strB)
public static int CompareOrdinal(string strA, int indexA,
string strB, int indexB, int count)

Метод Equals() также используется для сравнения строк. Различные фор­
мы этого метода и их описания представлены в таблице 3.4.
Таблица 3.4. Формы метода EqualsQ
Синтаксис

Описание

p u b l i c o v e r r i d e b o o l E q u a l s ( o b j e c t ob j)

М етод возвращает true, если
вызывающая строка со д ер ­
жит ту же последователь­
ность символов, что и стро­
ковое представление объекта
obj. Выполняется порядковое
сравнение с учетом регистра,
но параметры локализации
не учитываются

p u b lic bool E q u a ls ( s t r in g value)
pub lic bool E q u a ls ( s t r in g value,
S tr in gCo mp ar is on comparisonType)

pub lic s t a t i c bool E q u a ls ( s t r in g a,
s t r i n g b)
p u b lic s t a t i c bool E q u a ls ( s tr in g
a , s t r i n g b, S tr in gCo mp ar is on
comparisonType)

Возвращ ает true, если вы­
зывающая строка содержит
ту же последовательность
символов, что и строка value.
Выполняется порядковое
сравнение с учетом регистра,
но параметр локализации
не используется. Параметр
comparisonType определяет
конкретный способ сравне­
ния строк
Возвращ ает логическое зна­
чение true, если строка а с о ­
держит ту же последователь­
ность символов, что и строка
Ь. Выполняется порядковое
сравнение с учетом регистра,
но параметры локализации
не используются. Параметр
comparisonType определяет
конкретный способ сравне­
ния строк

Поиск в строке

Для поиска в строке в C# есть множество методов. Начнем с метода
ContainsQ, который описан так:

p u b lic bool C o n ta in s(strin g value)

Это самый простой метод, позволяющий определить, есть ли в строке опре­
деленная подстрока.
Метод StartsWith() позволяет определить, начинается ли вызывающая под­
строка с подстроки value (если так, метод возвращает value, иначе метод
возвращает false). Параметр comparisonType определяет конкретный способ
выполнения поиска. Синтаксис следующий:
p u b lic bool S ta r tsW ith (strin g value)
p ub lic bool S tartsW ith (strin g value,
comparisonType)

StringComparison

Аналогично, существует метод EndsWithQ, который возвращает true, если
вызывающая строка заканчивается подстрокой value:
p u b lic bool EndsW ith(string value)
p u b lic bool EndsW ith(string valu e,
comparisonType)

StringComparison

Для поиска первого вхождения заданной подстроки или символа использу­
ется метод IndexOf():
p u b lic i n t IndexOf(char value)
p u b lic in t In d e x O f(strin g value)

Если искомый символ или подстрока не обнаружены, то возвращается зна­
чение -1. В противном случае возвращает позицию, с которой начинается
подстрока или позицию, где впервые встречается заданный символ.
Если нужно начать поиск с определенной позиции Startlndex, то синтаксис
вызова метода будет немного другим:
public
public
public
public

in t IndexOf(char v a lu e , in t sta r tln d e x )
in t IndexO f(string value, in t startlndex)
i n t I n d e x O f( c h a r v a l u e , i n t s t a r t l n d e x , i n t count)
i n t I n d e x O f ( s t r i n g v a l u e , i n t s t a r t l n d e x , i n t count)

Поиск начинается с элемента, который указывается индексом startlndex, и
охватывает число элементов, определяемых параметром count (если указан).
Последнее вхождение символа/подстроки можно найти методом
LastIndexOf(). Параметры у этого метода такие же, как и у IndexOf().
Усложним задачу. Представим, что нужно найти не просто первое вхожде­
ние какого-то символа, а первое вхождение одного из символов, например,
есть строка Hello и нам нужно определить первое вхождение символов 1и о.
Можно два раза вызвать метод IndexOfQ - для символа 1и для символа о, од*

нако это неэффективно. Гораздо удобнее использовать метод IndexOfAny(),
которому можно передать массив искомых символов:
p u b l i c i n t I n d e x O f A n y ( c h a r [] anyOf)
p u b l ic i n t I n d e x O f A n y ( c h a r [] a n y O f ,
p u b l i c i n t I n d e x O f A n y ( c h a r [] a n y O f ,

in t startlndex)
int startlndex,

i n t count)

Первый параметр - это массив искомых символов, второй - начальная пози­
ция поиска, третий - счетчик символов. Метод возвращает индекс первого
вхождения любого символа из массива anyOf, обнаруженного в вызываю­
щей строке. Метод возвращает значение -1, если не обнаружено совпадение
ни с одним из символов из массива anyOf.
Вернемся к нашему примеру. У нас есть строка Hello и мы хотим найти по­
зицию символа 1 или символа о - какой встретится раньше. Обратите вни­
мание, метод возвращает одну позицию, а не массив позиций - каждого из
приведенных символов. Пример кода:
s tr in g s = "H ello";
c h a r [] Ch = { ' 1 ' , ' о ' } ;
i f ( s . IndexOfAny(Ch) ! = -1)
C o n s o l e . W r i t e L i n e ("Один и з символов был найден в позиции ( 0 ) " ,
s . IndexOfAny( Ch) ) ;

Аналогично методу IndexOfAnyf), существует метод LastIndexOfAny(), ко­
торый осуществляет поиск с конца.
Конкатенация строк

Переменные типа string можно соединить вместе, то есть выполнить конка­
тенацию. Для этого используется оператор +. Компилятор C# преобразует
оператор + в вызов метода String.Concat(), поэтому вы можете использо­
вать + или метод Concat() - как вам больше нравится:
strin g si = " s i" ;
s t r i n g s2 = " s 2 " ;
s t r i n g s3 = s i + s 2 ;

Метод Concatf) объявлен так:
p u b lic s t a t i c s t r i n g C o n c a t(s tr in g strO, s t r i n g s t r l ) ;
p u b l i c s t a t i c s t r i n g C o n c a t ( p a r a m s s t r i n g [] v a l u e s ) ;

Разделение и соединение строк

Представим, что есть строка, содержащая какой-то разделитель (сепара­
тор). С помощью метода SplitQ можно разделить эту строку на подстроки.

Метод, возвращающий массив string с присутствующими в данном экзем­
пляре подстроками внутри, которые отделяются друг от друга элементами
из указанного массива separator:
public str in g !]
public strin g !]

Sp lit(p aram s char[]
S p l i t ( p a ra m s c h a r [ ]

separator)
separator,

i n t count)

Если массив separator пуст или ссылается на пустую строку, то в качестве
разделителя подстрок используется пробел. Во второй форме данного мето­
да возвращается количество подстрок, заданное параметром count.
Существуют и другие формы вызова метода Split():
p u b lic s t r i n g ! ] S plit(param s chart] sep a ra to r,
S trin g S p litO p tio n s options)
p u b l i c s t r i n g ! ] S p l i t ( s t r i n g [] s e p a r a t o r , S t r i n g S p l i t O p t i o n s
options)
p u b l i c s t r i n g ! ] S p l i t ( p a r a m s c h a r t ] s e p a r a t o r , i n t count,
S trin g S p litO p tio n s options)
p u b l i c s t r i n g ! ] S p l i t ( s t r i n g [] s e p a r a t o r , i n t c o u n t ,
Strin gSp litO p tio n s options)

Разница, как видите, заключается в параметре options. В перечисле­
нии типа StringSplitOptions определяются только два значения: None и
RemoveEmptyEntries. Если параметр options принимает значение None,
то пустые строки включаются в конечный результат разделения исходной
строки. А если параметр options принимает значение RemoveEmptyEntries,
то пустые строки исключаются из конечного результата разделения исход­
ной строки.
С помощью метода Join() можно решить обратную задачу, а именно по­
строить строку по массиву строк, разделив каждый элемент этого массива
каким-то разделителем. Синтаксис такой:
public s t a t i c str in g Jo in (str in g separator,
public s t a t i c str in g Jo in (str in g separator,
i n t s t a r t l n d e x , i n t count)

strin g!]
strin g!]

value)
value,

В первой форме метода Join() возвращается строка, состоящая из соеди­
няемых подстрок из массива value. Во второй форме также возвращается
строка, состоящая из элементов массива value, но они соединяются в опре­
деленном количестве count, начиная с элемента массива value[startlndex].
В обеих формах каждая последующая строка отделяется от предыдущей
разделителем, заданным separator.

Заполнение и обрезка строк

В РНР есть удобные функции заполнения и обрезки строк. Подобные функ­
ции есть и в С#. Например, в C# есть метод Trim(), позволяющий удалять
все вхождения определенного набора символов с начала и конца текущей
строки:
p u b l i c s t r i n g Trim()
p u b l i c s t r i n g Trim(params c h a r [ ]

trimChars)

Первая форма удаляет из вызывающей строки начальные и конечные про­
белы. Во второй форме удаляются начальные и конечные вхождения в вы­
зывающей строке символов из массива trimChars. В обеих формах возвра­
щается получающаяся в результате строка.
Методы PadLeft() и PadRight() позволяют дополнить строку символами
слева и справа соответственно. Синтаксис обоих методов одинаковый, от­
личаются только названия:
p u b lic s t r i n g P a d L e ft( in t totalW idth)
p u b lic s t r i n g P a d L e ft(in t totalW idth,

ch a r paddingChar)

Первая форма дополняет строку пробелами так, чтобы ее общая длина ста­
ла равной значению totalWidth. Вторая форма позволяет указать символ
заполнения. В обеих формах возвращается получающаяся в итоге строка.
Если значение параметра totalWidth меньше длины вызывающей строки, то
возвращается копия неизмененной строки.
Вставка, удаление и замена строк

Метод Insert() позволяет вставить строку value в вызывающую строку по
индексу startlndex:
public strin g In se rt(in t startln d ex,

s t r i n g value)

В результате будет возвращена результирующая строка.
Для удаления части строки используется метод Remove():
p u b l i c s t r i n g Remove(int s t a r t l n d e x )
p u b l i c s t r i n g Remove(int s t a r t l n d e x ,

i n t count)

Первая форма метода Remove() позволяет удалить часть строку, начинаю­
щуюся с индекса startlndex, и до конца строки. Вторая форма удаляет count
символов, начиная с позиции startlndex.
Для выполнения замены в строке используется метод Replace(), синтаксис
которого приведен ниже:

p u b l i c s t r i n g R e p l a c e ( c h a r o l d C h a r , c h a r newChar)
p u b l i c s t r i n g R e p l a c e ( s t r i n g o l d V a l u e , s t r i n g newValue)

Первая форма заменяет все вхождения символа oldChar на символ newChar.
Вторая форма заменяет все вхождения подстроки oldValue на newValue.
Получение подстроки

Получить подстроку можно методом Substring(), обладающим следующим
синтаксисом:
public str in g S u b strin g(in t startlndex)
public strin g Su bstrin g(in t startln d ex ,

in t length)

Первая форма возвращает подстроку, начинающуюся с позиции startlndex
и до конца строки, а вторая - возвращает подстроку длиной length симво­
лов, начиная с позиции startlndex.
Управляющие последовательности символов

В языке Си строковые литералы могут содержать различные управляющие
последовательности символов (escape characters). Язык C# - не исключе­
ние. Управляющие последовательности позволяют уточнить то, как сим­
вольные данные будут выводиться в выходном потоке.
Управляющая последовательность начинается с символа обратного слеша,
после которого следует управляющий знак. В таблице 3.5 приведены наи­
более популярные управляющие последовательности.
Таблица 3.5. Управляющие последовательности
П осл едователь­
ность

О писание

Используется для вставки символа одинарной кавычки

V
V
w

Вставляет в строковой литерал символ обратной черты

\a

Заставляет систем у воспроизводить звуковой сигнал
(beep)

\n

Символ новой строки

V
\t

Возврат каретки

Позволяет вставить символ двойной кавычки

Вставляет символ табуляции

Пример:
/ / Выведем д в е пустых с троки и з в у к о в о й сиг нал после с л о в а H e l l o
Console.W riteLine("H ello\n\n\a");

Строки и равенство

В языке C# операция равенства предусматривает посимвольную проверку
строк с учетом регистра. Другими словами, строки Hello и hello не равны.
Проверку на равенство двух строк можно произвести или с помощью мето­
да Equals(), или с помощью оператора ==:
strin g si = " s i" ;
s t r i n g s2 = " s 2 " ;
C o n s o l e . W r i t e L i n e ( " s i == s 2 :

{0}",

s i = = -s2);

В результате будет выведена строка:
s i == s 2 :

false

Метод Equals() был описан ранее.
Тип System.Text.StringBuilder

Тип string может оказаться неэффективным в ряде случаев. Именно поэто­
му в .NET предоставляется еще одно пространство имен - System.Text. Вну­
три этого пространства имен находится класс по имени StringBuilder. В нем
содержатся методы, позволяющие заменять и форматировать сегменты.
Для использования класса StringBuilder первым делом нужно подключить
пространство имен System.Text:
usin g System .Text;

При вызове членов StringBuilder производится непосредственное измене­
ние внутренних символьных данных объекта, а не получение копии этих
данных в измененном формате. При создании экземпляра StringBuilder на­
чальные значения для объекта можно задавать с помощью не одного, а не­
скольких конструкторов. Рассмотрим пример использования StringBuilder:
S t r i n g B u i l d e r s b = new S t r i n g B u i l d e r ("Операционные с и с т е м ы : " ) ;
sb.A ppend("\n");
sb.AppendLine("W indows");
sb .A p p en dL in e(" L in u x " );
s b . A p p e n d L i n e ( " M a c OS x " ) ;
Console.W riteLine(sb.ToString) ;
C o n s o l e . W r i t e L i n e ( " B sb {0} символов", s b . L e n g t h ) ;
C o n so le . ReadLine () ;

Сначала мы создаем объект sb и добавляем в него строку "Операционные
системы:". Затем методом Append() мы добавляем символ перевода строки.
Можно было бы достичь того же эффекта, если бы мы создали объект так:

S t r i n g B u i l d e r s b = new S t r i n g B u i l d e r ( "Операционные
си с тем ы :\n ")

После этого мы добавляем строки методом AppendLine(). Данный метод ав­
томатически добавляет символ \п. Для вывода общей строки мы использу­
ем метод ToString(), а свойство Length содержит количество символов в sb.

Р и с . 3.6. Пример использования StringBuilder

3.3.6.

Области видимости переменных

Область видимости переменной (ее еще называют контекстом перемен­
ной) - это фрагмент кода, в пределах которого будет доступна данная пере­
менная.
Область видимости в C# определяется следующими правилами:
Поле или переменная-член класса находится в области видимости до тех
пор, пока в этой области находится содержащий это поле класс.
Локальная переменная находится в области видимости до тех пор, пока за­
крывающая фигурная скобка не укажет конец блока операторов или мето­
да, в котором она объявлена.
Локальная переменная, объявленная в операторах цикла for, while или по­
добных им, видима в пределах тела цикла
Рассмотрим несколько примеров:
fo r

( in t i = 0;

i < 5;

i++)

{
C o n s o le .W r it e (" { 0 } " , i ) ;
}
/ / з д е с ь з а к а н ч и в а е т с я о б л а с т ь видим ости i
in t k = 5 * i ;
/ / о п е р а т о р не б у д е т выполнен

После закрывающей скобки цикла for переменная i больше не будет "вид­
на", следовательно, последний оператор не будет выполнен.
Еще один пример:
p u b l i c s t a t i c i n t Main()

<
int m = 1 0 ;
f o r ( i n t i = 0; i < 10; i + + )

{
i n t m = 2 0;
/ / ошибка
Console.WriteLine(m + i ) ;

}
r e t u r n 0;

>

Переменная m будет доступна в пределах всего метода Main(). Вы не мо­
жете повторно объявить переменную m в теле цикла for. Это если вкрат­
це. Если развернуто, то переменная ш, определенная перед началом цикла
for, будет находиться в области видимости до тех пор, пока не завершится
метод Main(). Вторая переменная m якобы объявлена в контексте цикла,
но он является вложенным в контекст метода Main(), поэтому компилятор
не может различить эти две переменные и не допустит объявления второй
переменной с именем ш.
В некоторых случаях два идентификатора с одинаковыми именами и оди­
наковой областью видимости можно различить, при этом компилятор до­
пустит объявление второй переменной. Причина в том, что язык C# делает
различие между переменными, объявленными на уровне поля, и перемен­
ными, объявленными в методах (локальными переменными).
В качестве примера рассмотрим следующий фрагмент кода:
u s in g System;
namespace C o n s o l e A p p l i c a t i o n l
{
c l a s s P ro gr a m
{
s t a t i c s t r i n g uname = " d e n " ;
p u b l i c s t a t i c v o i d Main()

{
s t r i n g uname = " j o h n " ;
Console.W riteLine(uname);
return;

}

Обратите внимание, что в данном коде есть поле (член класса Program) и
локальная переменная с одинаковыми именами - uname (объявлена в мето­
де Main()). Когда вы запустите этот код, вы увидите строку "john". Приори­
тет у локальных переменных выше, чем у членов класса, поэтому вы увиди­
те именно эту строку, а не строку "den". Об этом следует помнить.
3.3.7. Константы

Константы - это переменные, значение которых нельзя изменить во время
выполнения программы. Константа объявляется с помощью служебного
слова const, после которого следует тип константы:
c o n s t i n t j = 100;

Особенности констант:


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



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



Константы всегда являются неявно статическими. Но при этом не
нужно указывать модификатор static.

3.4. Операторы
3 .4 .1 . Арифметические операторы

Арифметические операторы представлены в таблице 3.6. Операторы +,-,*
и / работают так, как предполагает их обозначение. Их можно применять к
любому встроенному числовому типу данных. Думаю, в особых коммента­
риях данные операторы не нуждаются.
Таблица 3.6. Арифметические операторы в C#
О ператор

Д ей ствие

+

Сложение
Вычитание, унарный минус
Умножение
Деление
Деление по модулю
Декремент
Инкремент

-

*
/
%


++

Хотя эти операторы всем знакомы, нужно рассмотреть оператор /, а также
операторы инкремента и декремента.
Когда / применяется к целому числу, то любой остаток от деления отбра­
сывается. Остаток от этого деления можно получить с помощью оператора
деления по модулю (%), который иначе называется оператором вычисления
остатка. В C # оператор % можно применять как к целочисленным типам
данных, так и к типам с плавающей точкой. В этом отношении C# отличает­
ся от языков Си и C++, где этот оператор (деление по модулю) разрешается
только для целочисленных типов данных.
Особенности есть и у операторов инкремента и декремента. Оператор ин­
кремента (+ + ) увеличивает свой операнд на 1, а оператор декремента (—)
уменьшает операнд на 1. Фактически, оператор:
х++;

аналогичен следующему:
х = х + 1;

Вот только нужно иметь в виду, что при использовании операторов инкре­
мента и декремента значение переменной х вычисляется только один, а не
два раза. Это сделано, чтобы повысить эффективность выполнения про­
граммы.
Операторы инкремента и декремента можно указывать до операнда (в пре­
фиксной форме) или же после операнда (в постфиксной форме). Операция
инкремента/декремента в префиксной форме происходит раньше, нежели в
постфиксной форме. Пример:
in t
in t
а =
а =
а =

а = 0;
х = 1;
х++;
0 ; х = 1;
++х;

/ / а = 1, х = 2
/ / исходны е зн ач ен и я
/ / а = 2; х = 2;

Хочется немного отклониться от темы обсуждения и сделать небольшой
перерыв. Не всегда есть возможность получить доступ к среде Visual Studio.
Конкретный тому пример происходит сейчас - при написании этих строк.
Не всегда есть возможность написать главу за один день и за одним ком­
пьютером. Вот и сейчас этот раздел пишется на компьютере, на котором не
установлена Visual Studio. Как делать скриншот результатов выполнения?
Как проверить свой собственный код на отсутствие ошибок (у человеческо­
го мозга есть один недостаток - часто он не видит собственных ошибок)?
Специально для этого есть Online-компиляторы. Их не нужно устанав­
ливать на свой компьютер. Просто откройте браузер, введите следующий

URL, и у вас будет доступ к полноценному компилятору того или иного
языка программирования (в данном случае - С#):

http://www.tutorialspoint.com/compile_csharp_online.php
Введите код, который вы хотите проверить, и нажмите кнопку Compile.
Если ее не нажать, то сайт сообщит, что не может открыть ехе-файл - понят­
ное дело, он еще не откомпилирован. После компиляции нажмите кнопку
Execute и получите результат выполнения (рис. 3.7).

Рис. 3.7. Использование online-компилятора

3.4.2.

Операторы сравнения и логические операторы

Данные типы операторов обычно используются в условном операторе if/
else (см. раздел 3.8), поэтому пока только перечислим операторы, а примеры
кода вы найдете далее.
Результатом операторов (как сравнения, так и логических) является значе­
ние типа bool - или true, или false.

Ш

I

*

Таблица 3.7. Операторы сравнения
Оператор
==
!=
>
<
>=
(> = ) - возвращает true, если выражение слева больше или равно,
чем выражение справа, if (price > 200)

В С и /С ++ можно использовать следующий код:
i n t к = 100;
i f (к)

{
//do

something

}
В C# такой код недопустим, поскольку к - это целое, а не булевое значение.
В операторе if можно использовать сложные выражения, и он может содер­
жать операторы else, что позволяет создать более сложные проверки. Син­
таксис похож на языки С и/С++:



условная операция AND (И), возвращает ture, если все выра­
жения истинны.



I - условная операция OR (ИЛИ), возвращает true, если хотя бы
одно из выражений истинно.



I - условная операция NOT (НЕ), возвращает true, если выражение
ложно, и false - если истинно.

&& -

Пример:
if

( p a g e = = 1)

{
C o n s o l e . W r i t e L i n e ("П ервая с т р а н и ц а " ) ;

}
else

{
C o n s o le .W r i t e L in e ("Страница:

(0}",

page);

}
if

( p a g e = = 1 && p r i c e < 1 00 )
C o n s o l e . W r i t e L i n e ( " Де ше вые п р о д у к т ы н а п е р в о й с т р а н и ц е " ) ;

Рассмотрим следующий не очень хороший пример кода:
if
if
if
if
if

( p a g e = = 1) C o n s o l e . W r i t e L i n e ( " П е р в а я с т р а н и ц а " ) ;
( p a g e = = 2) C o n s o l e . W r i t e L i n e ( " В т о р а я с т р а н и ц а " ) ;
( p a g e = = 3) C o n s o l e . W r i t e L i n e ( " Т р е т ь я с т р а н и ц а " ) ;
( p a g e = = 4) C o n s o l e . W r i t e L i n e ( " Ч е т в е р т а я с т р а н и ц а " ) ;
( p a g e = = 5) C o n s o l e . W r i t e L i n e ( " П я т а я с т р а н и ц а " ) ;
e l s e C o n s o l e . W r i t e L i n e ( " Ст р а н и ц а ( 0 ) " , p a g e ) ;

Данный код можно переписать с использованием оператора switch:
switch

(page)

{
c a s e 1:
break;
c a s e 2:
break;
c a s e 3:
break;
c a s e 4:
break;
c a s e 5:
default:
break;

C o n s o l e . W r i t e L i n e ("Первая с т р а н и ц а ") ;
C o n s o le .W r i t e L in e ("Вторая стр ан и ц а");
C o n so le .W r ite L in e ("Третья стран ица");
C o n so le .W r ite L in e ("Четвертая стр ан ица");
C o n s o l e . W r i t e L i n e ("Пятая с т р а н и ц а ") ;
C o n s o l e . W r i t e L i n e ( " С т ра н иц а ( 0 ) " , p a g e ) ;

*

В языке C# каждый блок case, в котором содержатся выполняемые опера­
торы (default в том числе), должен завершаться оператором break или goto,
во избежание сквозного прохода.
У оператора switch в C# есть одна прекрасная особенность: помимо число­
вых данных он также позволяет производить вычисления и со строковыми
данными. Рассмотрим пример:
s t r i n g OS = " L i n u x " ;
s w i t c h (OS)

{
c a s e " Wi nd o ws" : C o n s o l e . W r i t e L i n e ( "Хороший в ы б о р ! " ) ;
break;
c a s e "Lin u x" : C o n s o le .W r i t e L in e ("O penSource!") ;
break;
d e f a u l t : C o n s o l e . W r i t e L i n e ("Мы не з н а е м такую с и с т е м у ! " ) ;
break;

)

3.9. Массивы
3 .9 .1 . Одномерные массивы

Массив - это набор элементов данных одного типа, доступ к которым осу­
ществляется по числовому индексу и к общему имени. В C# массивы могут
быть как одномерными, так и многомерными. Массивы служат самым раз­
ным целям, поскольку они предоставляют удобные средства для объедине­
ния связанных вместе переменных.
Массивы в C# можно использовать почти так же, как и в других языках
программирования, но у них есть одна особенность - они реализованы в
виде объектов.
Чтобы воспользоваться массивом в программе, необходимо сначала объ­
явить переменную, которая может обращаться к массиву, а после - создать
экземпляр массива, используя оператор new.
Объявить массив можно так:
< т и п > []

;

Пример:
int[] d ig its;
strin g[] s tr s ;
b o o l [] b o o l s ;

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

Обратите внимание: следующий код неправильный!

int ints[];

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

После объявления массива необходимо установить его размер с помощью
ключевого слова new, точно так же, как в Java. Пример:
ints = new int[5];

Установить размер можно и при объявлении массива:
int[] ints = new int[5];

Инициализировать массив можно двумя способами: можно заполнить его
поэлементно, а можно использовать фигурные скобки:
ints[0]
ints[l]
ints[2]
ints[3]
ints[4]

=
=
=
=
=

12;
15;
22;
5;
122;

При заполнении массива вручную помните, что нумерация начинается с 0.
Но гораздо удобнее использовать фигурные скобки:
int[] ints = new int [] {100,200,300,400,500};
// Синтаксис инициализации массива без использования
// ключевого слова new
string[] user = {"1001", "den", "1234"};
// Используем ключевое слово new и желаемый размер массива
char[] symbol = new char[4] { 'A','В ','С ','D ' };

Массивы могут быть неявно типизированными (тип элементов массива var):
var arrl = new[] { 1, 2, 3 };
Console.WriteLine("Type of array - {0}",arrl.GetType());

Ранее было сказано, что массив - это набор элементов одного типа. Но в C#
есть одна уловка, позволяющая помещать в массив элементы разных типов.
В C# поддерживаются массивы объектов. Вы можете объявить массив типа
objectj] и поместить в него элементы самых разных типов. Вот как это ра­
ботает:
object[] arrOfObjects = { true, 10, "Hello", 1.7 };
*

Формально определение массива*соблюдается - все элементы типа object.
Но поскольку все элементарные типы данных в C# представлены в виде
объектов, то ними можно наполнить массив.
С каждым массивом в C# связано свойство Length, содержащее число эле­
ментов, из которых может состоять массив. Работает это так:
i n t []
for

i n t s = { 1,

2,

3,

4,

5 };

( i n t i = 0; i < i n t s . L e n g t h ;
C onsole.W riteLine(ints [ i ] );

i++)

3.9.2. Двумерные массивы

Многомерный массив содержит два или больше измерений, причем доступ
к каждому элементу такого массива осуществляется с помощью определен­
ной комбинации двух или более индексов. Многомерный массив индекси­
руется двумя и более целыми числами. Наиболее часто используются дву­
мерные массивы. Это одновременно самый простой и самый популярный
тип многомерного массива. Местоположение любого элемента в двумерном
массиве обозначается двумя индексами. Такой массив можно представить
в виде таблицы, на строки которой указывает один индекс, а на столбцы —
другой.
/ / Объявляем двумерный м а с с и в 4x5
i n t [ , ] RandomArr = new i n t [ 4, 5 ] ;
/ / Инициализируем г е н е р а т о р случайных ч исел
Random r a n = new Random( ) ;
/ / Инициализируем двумерный м а с с и в случайными числами
f o r ( i n t i = 0; i < 4; i++)

{
for

( i n t j = 0;

j < 5;

j++)

{
RandcmArr[i, j ] = ran.Next(1, 100); / / случайное число от 1 до 100
C o n s o l e . W r i t e ( " { 0 ) \ t " , RandomArr[i, j ] ) ;

)
C o n s o l e . W r i t e L i n e () ;

)
Если вам приходилось раньше программировать на Си, C ++ или Java, то
будьте осторожны при работе с многомерными массивами в С#. В этих язы­
ках программирования размеры массива и индексы указываются в отдель­
ных квадратных скобках, тогда как в C# они разделяются запятой.

3.9.3. Ступенчатые массивы

Как было отмечено ранее, двумерный массив представляет собой таблицу,
в которой длина каждой строки неизменна. Но в C# можно создавать так
называемые ступенчатые массивы. Ступенчатый массив - это массив масси­
вов, в котором длина каждого массива может быть разной.
Ступенчатые массивы объявляются с помощью ряда квадратных скобок, в
которых указывается их размерность. Например, для объявления двумер­
ного ступенчатого массива служит следующая общая форма:
тип [][] имя массива = new тип[размер]

[];

Здесь размер обозначает число строк в массиве, а память для самих строк
распределяется индивидуально, и поэтому длина строк может быть разной.
Пример работы со ступенчатым массивом приведен в листинге 3.4.
Листинг 3.4. Работа со ступенчатым массивом
i n t i = 0;

// Объявляем ступенчатый массив
// В нем будет три массива длиной, соответственно,
/ / 3, 5 и 4 эл ем ен та
i n t [ ] [ ] ш у А гг = new i n t [ 3 ] [ ] ;
m y A r r [ 0 ] = new i n t [ 3 ] ;
m y A r r [1 ] = new i n t [ 5 ] ;
m y A r r [2 ] = new i n t [ 4 ] ;

// Инициализируем ступенчатый массив
for

(;

i < 3;

i++)

{
myArr[ 0 ] [ i ] = i ;
C onsole.W rite( " { 0 } \t",m y A rr[ 0 ] [ i ] );

}
Console.W riteLine();
f o r ( i = 0; i < 5; i++)

{
myArr[ 1 ] [i] = i ;
C on sole.W rite("{0)\t",

myArr[ 1 ] [ i ] ) ;

}
Console.W riteLine();
f o r ( i = 0; i < 4; i++)

{
myArr [2] [ i ] = i ;
C on sole.W rite("{0)\t",

}

m y A r r [2] [ i ] ) ;

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

Для создания массива можно использовать еще и класс Array. Класс Array
является абстрактным, поэтому создать массив с использованием како­
го-либо конструктора нельзя. Но вместо применения синтаксиса C# для
создания экземпляров массивов также возможно создавать их с помощью
статического метода Createlnstance(). Это исключительно удобно; когда
заранее неизвестен тип элементов массива, поскольку тип можно передать
методу Createlnstance() в параметре как объект Туре:
/ / С о з д а е м м а с с и в т ипа s t r i n g , длиной 3
Array s t r s = A r r a y .C r e a t e l n s t a n c e ( t y p e o f ( s t r i n g ) , 3 ) ;
/ / Инициализируем первые д в а поля м а с с и в а
str s.S e tV a lu e ("B r a n d ",0);
s t r s . SetV alue("M odel",1);
/ / Считываем данные и з м а с с и в а
s t r i n g s = (st r in g ) s t r s .G e t V a lu e (1);

Гораздо удобнее использовать предлагаемый языком C# синтаксис, чем ис­
пользовать класс Array, но поскольку мы изучаем С#, мы не могли его не
рассмотреть. Однако существуют ситуации, когда использование Array яв­
ляется оправданным. Примеры таких ситуаций - копирование и сортиров­
ка массивов.
Массивы - это ссылочные типы, поэтому присваивание переменной типа
массива другой переменной создает две переменных, ссылающихся на один
и тот же массив. Для копирования массивов предусмотрена реализация
массивами интерфейса ICloneable. Собственно, само клонирование вы­
полняется методом С1опе(), который определен в этом интерфейсе. Метод
С1опе() создает неглубокую копию массива. Если элементы массива отно­
сятся к типу значений, то все они копируются, если массив содержит эле­
менты ссылочных типов, то сами эти элементы не копируются, а копируют­
ся лишь ссылки на них.
В классе Array также есть метод копирования - Сору(). Он тоже создает не­
глубокую копию массива. Но между С1опе() и Сору() есть одно важное от­
личие: С1опе() создает новый массив, а Сору() требует наличия существу­
ющего массива той же размерности с достаточным количеством элементов.
Пример вызовов С1опе() и Сору():
s t r i n g ! ] a r r l = ( s t r i n g [ ] ) myArr. C l o n e ( ) ;
A rray.C opy(myArr, a r r 2 , myArr. L e n g t h ) ;

В первом примере мы клонируем массив МуАгг в arrl, во втором - массив
шуАгг копируется в массив агг2.
В класс Array встроен алгоритм быстрой сортировки (Quicksort) элементов
массива. Простые типы вроде System.String и System.Int32 реализуют ин­
терфейс IComparable, поэтому вы можете сортировать массивы, содержа­
щие элементы этих типов. Если вам нужно отсортировать элементы других
типов, то они должны реализовать интерфейс IComparable.
С помощью разных вариантов метода Sort() можно отсортировать массив
полностью или в заданных пределах либо отсортировать два массива, со­
держащих соответствующие пары “ключ-значение”. Сортировка массива
необходима, чтобы можно было осуществить эффективный поиск, исполь­
зуя разные варианты метода BinarySearch().
Пример сортировки массива приведен в листинге 3.5.
Листинг 3.5. Сортировка массива
int[] myArr = { -5, 7, -13, 121, 45, -1, 0, 77 };
Console.WriteLine("До сортировки: ") ;
foreach (int i in myArr)
Console.Write("\t{0}",i);
Array.Sort(myArr);
Console.WriteLine("ХпПосле сортировки:");
foreach (int i in myArr)
Console.Write("\t{0)",i);

Рис. 3.11. Сортировка массива

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

Массив может выступать как параметр метода. Также метод может возвра­
щать целый массив. Рассмотрим небольшой пример описания метода, при­
нимающего массив в качестве параметра и возвращающего также массив:
static Array arrChange(Array Arr)

{
// делаем что-то с массивом Arr
return Arr;

}

3.10. Кортежи
В отличие от массивов (которые содержат объекты одного типа), кортежи
(tuple) могут содержать объекты самых разных типов. Кортежи часто ис­
пользуются в языке F#, а с появлением .NET 4 кортежи доступны в .NET
Framework для всех языков .NET.
В .NET 4 определены восемь обобщенных классов Tuple и один статический
класс Tuple, который служит "фабрикой" кортежей.
Существуют разные обобщенные классы Tuple для поддержки различно­
го количества элементов, например, Tuple содержит один элемент,
Tuple - два элемента и т.д. Элементы кортежа доступны через свой­
ства Iteml, Item2. Если имеется более восьми элементов, которые нужно
включить в кортеж, можно использовать определение класса Tuple с восе­
мью параметрами. Последний параметр называется TRest, в котором дол­
жен передаваться сам кортеж. Поэтому есть возможность создавать корте­
жи с любым количеством параметров.
Следующий метод возвращает кортеж из четырех элементов:
static Tuplecint, float, string, char> tup(int z, string name)

{
int x = 4 * z;
float у = (float) (Math.Sqrt (z)) ;
string s = "Привет, " + name;
char ch = (char)(name[0]);
return Tuple.Createcint, float, string, char>(x, y, s, ch) ;

}
Работать с этим методом можно так:
v a r t = t u p (5," М а р к " );
Console.WriteLine("{0} (1) (2) (3)", t.Item3, t.Iteml,
t.Item2, t.Item4);

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

3.11. Как подсчитать количество слов в
тексте
Представим, что есть какой-то текст и нам нужно подсчитать количество
символов. Код программы будет очень прост:
C o n s o l e . W r i t e L i n e ("Введите т е к с т : " ) ;
s t r i n g [] t A r r ;
s t r in g te x t = C on sole.ReadLine();
tArr = t e x t . S p l i t ( ' ' ) ;
C o n s o l e . W r i t e L i n e ("Количество с л о в : " ) ;
C onsole.W riteLine(tA rr. Length);
C on sole. ReadLine();

Все очень просто. Мы создаем массив строк textMass и простую строко­
вую переменную text. В переменную text считывается введенный пользо­
вателем текст, а в массив tArr добавляются элементы из строки text, рас­
члененные пробелом при помощи метода Split. Каждый элемент данного
массива - это как раз одно слово, заключенное в тексте между пробелов.
Все, что осталось, - это вывести количество элементов массива.

Рис. 3.12. Подсчет слов в тексте

Полный код этого приложения приведен в листинге 3.6.
Листинг 3.6. Подсчет количества слов в тексте
using
using
using
using
using

System;
S y st e m .C o lle c tio n s . Generic;
System .Linq;
System .Text;
System .Threading.Tasks;

namespace T e s t l l

Г Л А В А 3 О с н о в н ы е коне ф у к ц и и я з ы к и С

#

{
class

P r og r am

{
s t a t i c v o i d M a i n ( s t r i n g [] a r g s )

{
Console.OutputEncoding = E ncoding.G etE ncoding(866);
C o n so le . InputEncoding = Encoding.GetEncoding (866);
C o n s o l e . W r i t e L i n e ("Введите т е к с т : " ) ;
s t r in g !] tArr;
s t r i n g t e x t = C o n s o l e . ReadLine ( ) ;
tArr = t e x t . S p l i t C ') ;
C o n s o l e . W r i t e L i n e ( "Количество с л о в : " ) ;
C on sole.W riteLine(tA rr.Length);
C on sole. ReadLine();
C o n s o l e . ReadLine ( ) ;

}
)
)

3.12. Вычисляем значение функции
Теперь задача такая: нам нужно вычислить сумму значений, возвращаемых
функцией f() от 1 до х. Функция будет такой:
Зх3 - 2х2
Как мы знаем, в Си для возведения в степень используется функция pow().
Конечно, в нашем простом примере мы могли бы использовать вот такой
оператор:
3 * х * х ,,,х - 2 * х * х ;
Но, согласитесь, это не совсем правильно. Думаю, читая эти строки, у вас
созревает лишь один вопрос: а зачем приводить столь простой пример? Но
этот пример не так прост, как вам кажется. Не стоит забывать, что вы про­
граммируете не на Си, а на С#, где все представлено в виде объектов. Пол­
ный код приложения приведен в листинге 3.7, а результат выполнения про­
граммы - на рис. 3.13.

Рис. Э.1Э. Результат выполнения программы

Листинг 3.7. Вычисляем значение функции
using
using
using
using
using

System;
S y ste m .C o lle c tio n s . Generic;
System .Linq;
System .Text;
System .Threading.Tasks;

namespace T e s t l l

{
class Program

{
s t a t i c d o u b l e f ( d o u b l e x)

{
r e t u r n 3 * Math.Pow(x,

3)

- 2 * Math.Pow(x,

2);

}
s t a t i c v o i d M a i n ( s t r i n g [] a r g s )

{
C o n s o l e . O u t p u t E n c o d i n g = E n c o d i n g . G e t E n c o d i n g (866) ;
C o n s o le . InputEncoding = E nco d in g.G etE n co d in g(866);
C o n s o le .W r i t e L in e ("Введите X ; " ) ;
s t r i n g t = C onsole. ReadLine();
in t x = Convert. T o In t3 2 (t);
int i;
double sum = 0;
for

( i = 1;

i 0) p = a r g s f O ] ;
else p = "null";
s w i t c h (p) {
case "cpu":
Console.W riteLine("CPU count: { 0 } " , i n f o .c p u ) ;
break;
case "win":
Console.W riteLine("W indows V e rsio n : ( 0 ) " , i n f o .w i n ) ;
break;
case "net":
Console.W riteLineC.N ET V ersion: ( 0 ) " , i n f o .n e t ) ;
break;
case "host":
Console.W riteLine("H ostnam e: ( 0 ) " , i n f o . hostnam e);
break;
case "u ser":
Console.W riteLine("U sernam e: ( 0 ) " , i n f о . usernam e);
break;
d efau lt:
Console.Wri te Line ("Usage: s y s i n f о < cp u |w in |n e t| h os t I us e r > " ) ;
break;

}
}
}
}
Теперь разберемся, что есть что. Сначала в методе Main() определяются две
переменные. Первая - это строковая переменная р, мы ее используем для
облегчения работы с параметрами программы. В принципе, можно было бы
обойтись и без нее. Переменная info - это экземпляр класса SysInfo().
Посмотрим на сам класс. Класс содержит следующие поля:
p u b l i c s t r i n g win, n e t ,
p u b l i c s t r i n g hostname,

cpu;
username;
*

Поле win будет содержать версию ОС, net - версию .NET, CPU - количе­
ство процессоров (ядер процессоров), hostname - имя узла, a username - имя
пользователя. Все поля публичные, то есть к ним можно будет обращаться
за пределами класса, что мы и будем делать в нашей программе.
Конструктор класса (его имя совпадает с именем класса), используя класс
Environment, получает доступ к информации о системе. Обратите внима­
ние: поскольку каждое поле этого класса является объектом, то для преоб­
разования его в строку мы должны использовать метод ToString(). После
того как конструктор заполнит информацией поля, они станут доступны
для использования в нашей программе.
Далее мы анализируем переданные программе параметры. В зависимости
от переданного параметра мы или выводим соответствующее поле класса,
или выводим информацию об использовании программы. Программа в дей­
ствии изображена на рис. 5.1.

Рис. 5.1. Программа в действии

5.2.3. Класс System.Object

В языке C# имеется специальный класс System.Object (или просто ob­
ject), являющийся базовым классом для всех остальных классов и типов, в
том числе типов значений. Это означает, что все остальные классы и типы
являются производными от object, следовательно, тоже являются объек­
тами. Все в C # - целые числа, строки, массивы и другие типы - является
объектами.

На практике это означает то, что помимо определяемых вами методов и по­
лей, созданные вами классы поддерживают множество общедоступных и
защищенных методов-членов, определенных в классе System.Object (табл.
5.1).
Таблица 5.1. Методы класса System.ObjectQ
М етод

О писание

T o S t r i n g ()

Возвращ ает символьную строку, содержащую описание
объекта, для которого он вызывается. С этим методом
мы уже знакомы из предыдущего раздела. Также метод
ToString() автоматически вызывается при выводе сод ер ­
жимого объекта с помощью метода Write()/WriteLine(). В
своем классе вы можете переопределить этот метод, что­
бы созданные вами объекты могли быть корректно преоб­
разованы в строку.

G e t H a s h C o d e ()

Используется при помещении объекта в структуру данных
- карту (та р ), которая также называется хеш-таблицей.
Используется классами, которые манипулируют картами.
Если вы желаете использовать свой класс в таком контек­
сте, то вы должны переопределить метод GetHashCode().
Однако это нужно довольно редко, если вам когда-то и по­
надобится использовать этот метод, подробности вы с м о ­
жете узнать в документации по .NET, поскольку сущ еству­
ют строгие требования перезагрузки этого метода.

E q u a l s ()

М етод Equals(object) определяет, ссылается ли вызыва­
ющий объект на тот же самый объект, что и объект, ука­
зываемый в качестве аргумента этого метода. То есть он
проверяет, являются ли оба объекта одинаковыми. М етод
возвращает true, если сравниваемые объекты одинако­
вые, в противном случае - false.

F i n a l i z e ()

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

G e t T y p e ()

Возвращает экземпляр класса, унаследованный от System.
Туре. Может предоставить большой объем информации о
классе, в том числе базовый тип, методы, поля и т.д.

C l o n e ()

Создает копию объекта и возвращает ссылку на эту копию.

5.2.4. Конструкторы

С конструктором мы уже знакомы, поскольку недавно реализовали кон­
структор для класса Syslnfo. Основная задача конструктора - инициализи­
ровать объект при его создании. Наш конструктор заполнял поля информа­
цией, полученной от класса Environment.
С точки зрения синтаксиса определение конструктора подобно определе­
нию метода, но у конструкторов нет возвращаемого типа. Общая форма
определения конструкторов приведена ниже:
a c c e ss имя_класса(args)
/ / тело конструктора
}

{

Спецификатор доступа (access) обычно указывается public, поскольку кон­
структоры зачастую вызываются в классе, а вот список параметров args
может быть как пустым, так и состоящим из одного или нескольких па­
раметров. Имя конструктора, как уже было отмечено, должно совпадать с
именем класса.
Каждый класс в C# по умолчанию оснащается конструктором, который вы
при желании можете переопределить, что мы и сделали в нашем классе (см.
лист. 5.1).
Конструктор также может принимать один или несколько параметров. В
конструктор параметры передаются таким же образом, как и в метод. Для
этого достаточно объявить их в скобках после имени конструктора.
В листинге 5.2 приводится пример определения конструктора класса с па­
раметрами.
Листинг 5.2. Конструктор класса с параметрами
c l a s s Human
{
p u b l i c s t r i n g Name;
p u b l i c b y t e Age ;
/ / У с т а н а в л и в а е м п а ра ме тр ы
p u b l i c H u m a n ( s t r i n g n, b y t e a)

{
Name = n;
Age = a ;

}
p u b lic void GetlnfoO
{
Console.W riteLine("Name:

)

{0}\nAge:

(1}",

Name, A g e ) ;

}
о нашем классе есть два поля, конструктор и метод GetInfo(). Параметры
конструктору передаются при создании объекта:
Human J o h n = new Human( " J o h n " , 3 3 ) ;
John. G etlnfо ();

5.2.5. Деструкторы

Обратите внимание, как создается новый объект. Перед указанием имени
вы указываете оператор new, который выделяет оперативную память для
создаваемого объекта.
Понятное дело, что оперативная память - не резиновая, поэтому свободная
память рано или поздно закончится.
Именно поэтому одной из главных функций любой схемы динамическо­
го распределения памяти является освобождение памяти от неиспользуе­
мых объектов, чтобы сделать ее доступной для последующего выделения
(allocation).
В некоторых языках (например, в C ++) освобождение выделенной ранее
памяти осуществляется вручную. Например, в C ++ для этого используется
оператор delete. Но в C# (и не только в С#, в РНР тоже) используется про­
цесс, названный сборкой мусора.
Благодаря автоматической “сборке мусора” в C# память освобождается
от лишних объектов. Данный процесс происходит незаметно и без всяко­
го вмешательства со стороны программиста. Если ссылки на объект отсут­
ствуют, то такой объект считается ненужным, и занимаемая им память в
итоге освобождается. В итоге освобожденная память может использоваться
для других объектов.
Процесс “сборки мусора” происходит полностью в автоматическом режиме
и нельзя заранее знать или предположить, когда он произойдет.
В C# программист может определить метод, который будет вызываться не­
посредственно перед окончательным уничтожением объекта. Такой метод
называется деструктором и гарантирует четкое окончание времени жизни
объекта.
Синтаксис определения деструктора такой же, как и конструктора, но перед
именем класса указывается
a c c e ss ~имя_класса(args)
/ / тело конструктора
}

{

Пример деструктора приведен в листинге 5.3.
Листинг 5.3. Пример определения деструктора
c l a s s Human
{
p u b l i c s t r i n g Name;
p u b l i c b y t e Age;
/ / У с т а н а в л и в а е м па ра ме тр ы
p u b l i c H u m a n ( s t r i n g n, b y t e a)

{
Name = n;
Age = a ;

}
p u b l i c ' H u m an ()

{
C o n s o l e . W r i t e L i n e ( " O b j e c t was d e s t r o y e d " ) ;

)
p u b lic void G etlnfoO
{
Console.W riteLine("Nam e: {0}\nAge:

(1)",

Name, A g e ) ;

)
}
Обычно деструкторы не нужны, поскольку все используемые объектом ре­
сурсы автоматически освобождаются при его уничтожении. Но при жела­
нии вы можете освободить ресурсы явно, например, закрыть используемые
файлы и сетевые соединения.
5.2.6.

Обращаемся сами к себе. Служебное слово this

В C # есть ключевое слово this, обеспечивающее доступ к текущему экзем­
пляру класса. Данное ключевое слово используется для разрешения не­
однозначности, когда один из параметров назван так же, как поле данных.
Конечно, чтобы такой неоднозначности не было, лучше придерживаться
правил именования переменных, параметров, полей и т.д.
Лучше всего продемонстрировать применение ключевого слова this на при­
мере, приведенном в листинге 5.4.
Листинг 5.4. Использование ключевого слова this
c la s s

Hum an

{
p u b l i c s t r i n g Name;
p u b l i c b y t e Age ;

/ / У с т а н а в л и в а е м па ра ме тр ы
p u b l i c H u m a n ( s t r i n g Name, b y t e Age)
{
/ / Что и чему п р и с в а и в а е т с я ?
/ / Name = Name;
/ / Age = Age ;
t h i s . N a m e = Name;
t h i s . A g e = A ge ;

)
}
Без использования служебного слова this было бы непонятно, что и чему
присваивается - то ли параметру присваивается значение поля, то ли наобо­
рот:
Name = Name;
Age = A ge ;

При использовании ключевого слова this все становится на свои места - по­
лям присваиваются значения параметров:
t h i s . N a m e = Name;
t h i s . A g e = A ge ;

Использование this позволяет использовать более понятные имена параме­
тров методов. Ранее мы использовали имена параметров п и а, а теперь в
конструкторе мы можем смело указывать имена Name и Age и не беспоко­
иться, что компилятор не поймет, что мы имели в виду.
5.2.7. Доступ к членам класса

Что дает нам инкапсуляция? Прежде всего, она связывает данные с кодом.
Но это далеко не все. Благодаря ей, класс предоставляет средства для управ­
ления доступом к его членам. Именно об управлении доступом мы сейчас
и поговорим.
По существу, есть два типа членов класса - публичные (public) и приватные
(private). Также их еще называют открытыми и закрытыми. Доступ к от­
крытому члену возможен из кода, существующего за пределами класса. А
вот доступ к приватному члену возможен только методам, которые опреде-

лены в самом классе. Приватные члены позволяют организовать управле­
ние доступом.
Кроме известных спецификаторов (модификаторов) доступа public и
private, в C# поддерживаются еще два модификатора - protected и internal.
Как уже было отмечено, доступ к члену с модификатором private невозмо­
жен из других классов. Если модификатор доступа не указан, то считается,
что член класса является приватным.
Спецификатор protected означает член класса, доступ к которому открыт в
пределах иерархии классов. Спецификатор internal используется в основ­
ном для сборок, и начинающим программистам он не нужен.
Примеры:
public in t а;
p r iv a te in t b;
in t с;

/ / Открытый член
/ / Закрытый член
/ / Закрытый член по умолчанию

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


Члены, которые выпланируете использовать только в классе, долж­
ны быть закрытыми (private).



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



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



Если доступ к члену допускается из производных классов, то такой
член можно объявить как защищенный (protected).



Если какой-то член может нанести вред объекту, если он будет ис­
пользоваться неправильно, он должен быть закрытым.



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

Как уже было отмечено, метод может не содержать параметров вообще, а
может содержать один или несколько параметров: У каасдого параметра мо­
жет быть свой модификатор (см. табл. 5.2).

Таблица 5.2. Модификаторы параметров методов
М одиф икатор
парам етра

О писание

(отсутствует)

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

out

Выходные параметры должны присваиваться вызываемым
методом (и, следовательно, передаваться по ссылке). Если
параметрам с модификатором out в вызываемом методе
значения не присвоены, компилятор сообщ ит об ошибке

ref

Это значение первоначально присваивается вызывающим
кодом и при желании может повторно присваиваться в вы­
зываемом методе (поскольку данные также передаются по
ссылке). Если параметрам с модификатором re f в вызывае­
мом методе значения не присвоены, компилятор не будет ге­
нерировать сообщ ение об ошибке.

params

Этот модификатор позволяет передавать в виде одного ло­
гического параметра переменное количество аргументов. В
каждом методе может присутствовать только один модиф и­
катор p ara m s, и он должен обязательно указываться послед­
ним в списке параметров. В реальности необходимость в и с­
пользовании модификатора p a ra m s возникает очень редко,
но он применяется во многих методах внутри библиотек ба­
зовых классов

Наиболее часто используется модификатор ref, позволяющий изменять
значение параметра внутри метода и передавать его по ссылке в вызыва­
ющий метод. Лучше всего продемонстрировать работу этого параметра на
примере (лист. 5.5).
Листинг 5.5. Использование модификатора ref
using
using
using
using

System;
System .C ollections.G eneric;
System .Linq;
System .Text;

namespace C o n s o l e A p p l i c a t i o n l
{
c l a s s P rog ra m
{
/ / Метод, изменяющий с в о й а р г у м е н т

s t a t i c v o i d c h a n g e N u m ( r e f i n t n)

{
n = 100;

)
s t a t i c v o i d M ai n ( )

(
i n t x = 0;
C o n s o le . Wr i te L in e (" Va l ue o f x b e f o r e c a l l i n g changeNum: ( 0 } " , x ) ;
changeNum(ref x ) ;
C o n s o le . Wr i te L in e (" Va l ue o f x a f t e r c a l l i n g changeNum: { 0 } " , x ) ;
C onsole. ReadLine();

}
}
}
Вывод будет таким:
V a l u e o f x be f o r f e c a l l i n g changeNum: 0
V a l u e o f x a f t e r c a l l i n g changeNum: 100

Если бы параметр был объявлен без модификатора ref, метод не смог бы из­
менить значение переменной х. Также обратите внимание, как переменная
х передается в сам метод.
Модификатор параметра out похож на ref, за одним исключением: он слу­
жит только для передачи значения за пределы метода. Поэтому переменной,
используемой в качестве параметра out, не нужно присваивать какое-то
значение. Более того, в методе параметр out считается неинициализирован­
ным, т.е. предполагается, что у него отсутствует первоначальное значение.
Рассмотрим пример использования модификатора out (лист. 5.6).
Листинг 5.6. Пример использования модификатора out
using
using
using
using

System;
S y s t e m .C o l le c t i o n s . Generic;
System .Linq;
System .Text;

namespace C o n s o l e A p p l i c a t i o n l
{
c l a s s P rog ra m
{
s t a t i c in t te s t( d o u b le a,

out dou ble b,

o u t d o u b l e c)

{
int i = (in t)a;
b = a * a;
c = a * a * a;
return

i;

)
static

v o i d M ain()

{
int i;
double
i

Int
:
а л 2:
а л 3:

c,

b,

= t e s t (a,

a = 5.5;
out b,

out

c);

C on sole.W riteL in e("O rigin al
{l)\n
{2 } \ n
{3 }\",a ,i,b ,c );

value:

{0}\n

C on sole.R eadLine();

}
}
}

Вывод программы будет таким:
O rigin al value:
Int
: 5
a A 2 : 30,25
a A 3 : 166,375

5.5

Использование модификатора out позволяет методу возвращать сразу три
числа - два через параметры с модификатором out и одно - через return.
Модификатор params позволяет передавать методу переменное количество
аргументов одного типа в виде единственного логического параметра. Рас­
смотрим пример использования модификатора params на примере переда­
чи массива в качестве параметра метода. Наш метод minElement будет ис­
кать минимальный элемент в массиве, который передан с модификатором
params.
Листинг 5.7. Использование модификатора params
using
using
using

System ;
Sy stem .C o llectio n s.G en eric;
System .Ling;

4

usin g System .Text;
namespace C o n s o l e A p p l i c a t i o n l
{
c l a s s Pr og r am

{
s t a t i c d o u b l e m i n E l e m e n t ( p a r a m s d o u b l e [] a r r )

{
if

d o u b l e min;
/ / A r r a y i s e mp ty?
( a r r . L e n g t h = = 0)

{
C o n s o l e . W r i t e L i n e ( "Empty a r r a y ! " ) ;
r e t u r n Do ub le . N e g a t i v e l n f i n i t y ;

)
else
{
/ / Only 1 e l e m e n t i n a r r a y
i f ( a r r . L e n g t h = = 1)

{
min = a r r [ 0 ] ;
r e t u r n min;

}
}
min = a r r [ 0 ] ;
/ / S e a r c i n g f o r min
f o r ( i n t j = 1; j < a r r . L e n g t h ;
i f ( a r r [ j ] < min)
min = a r r [ j ] ;
r e t u r n min;

j++j

}
s t a t i c v o i d M ai n ( )

{
d o u b l e r e s u l t = 0;
doublet] a r r l = { 5.5,
r e s u lt = m inElem ent(arrl);
Console.W riteLine("M in: { 0 } " ,

7.6,

3.3,

-11.7);

resu lt);

C on sole. ReadLine();

}
}
}
Результатом будет число -11.7, как несложно было догадаться.

5.2.9. Необязательные параметры

Начиная с версии 4.0 языка С#, появились необязательные аргументы, по­
зволяющие определить значение по умолчанию для параметра метода. Дан­
ное значение будет использоваться по умолчанию в том случае, если для
параметра не указан соответствующий аргумент при вызове метода. Такие
аргументы довольно давно существуют в других языках программирова­
ния, но в C# появились только в версии 4.0.
Основная цель, которую преследовали разработчики С#, - необходимость в
упрощении взаимодействия с объектами СОМ, многие из которых были на­
писаны давно и рассчитаны на использование необязательных параметров.
Определяются данные параметры так:
p u b lic in t Prod3(int a,

{

i n t b = 1,

i n t c = 1)

retu rn a * b * c;

}
Нужно отметить, что все необязательные аргументы должны непременно
указываться справа от обязательных. Кроме обычных методов, необяза­
тельные параметры можно применять в конструкторах, индексаторах и де­
легатах.
5.2.10. Именованные аргументы

Также в версии .NET 4.0 появилась поддержка так называемых именован­
ных аргументов (named arguments). Обычно аргументы передаются в том
порядке, который должен совпадать с порядком указания аргументов при
объявлении метода. Вспомним наш конструктор класса Human:
p u b l i c H u m a n ( s t r i n g Name, b y t e Age)

{
t h i s . N a m e = Name;
t h i s . A g e = Age ;

}
Вызывать его можно было только так:
Human Den = new Huma n( "Den" ,

32);

Вы не можете вызвать его так:
Human Den = new Human(32,

"Den");

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

что и какому параметру вы присваиваете, поскольку при передаче аргумен­
тов указываются имена параметров. Для указания аргументов по имени ис­
пользуется форма:
имя_параметра : значение

Пример:
Human Den = new Human(Age:

3 2 , Name:

"Den");

При этом вносить изменения в описание самого метода не нужно. Просто
укажите компилятору, какому параметру какое значение вы передаете.
5 .2.11. Ключевое слово static

Бывают ситуации, когда нужно определить член класса, который будет ис­
пользоваться независимо от всех остальных объектов этого класса. Доступ
к члену класса будет осуществляться посредством объекта этого класса, но
в то же время можно создать член класса для самостоятельного примене­
ния без ссылки на конкретный экземпляр объекта. Такие члены называются
статическими и для их определения используется ключевое слово static.
Ключевое слово static можно использовать как для переменных, так и для
методов. Переменные, объявляемые как static, по существу, являются гло­
бальными.
Самый часто используемый пример статического члена - метод Main(), ко­
торый объявляется таковым потому, что он должен вызываться операцион­
ной системой в самом начале выполняемой программы.
Чтобы воспользоваться членом типа static за пределами класса, достаточ­
но указать имя этого класса с оператором-точкой. Но создавать объект для
этого не нужно. Рассмотрим наш класс Human, в который мы добавим ста­
тический метод Не11о():
Листинг 5.8. Статический метод
c l a s s Human
{
p u b l i c s t r i n g Name;
p u b l i c b y t e Age;
/ / У с т а н а в л и в а е м п а ра ме тр ы
p u b l i c H u m a n ( s t r i n g n, b y t e a)

{
Name - n;
Age = a ;

}
p u b l i c ' Hu ma n()

<
C o n s o l e . W r i t e L i n e ( " O b j e c t was d e s t r o y e d " ) ;

)
p u b lic void G etlnfoO
{
Console.W riteLine("Name:

{0}\nAge:

{1}",

Name, A g e ) ;

)
s t a t i c v o i d H e l l o ()

{

Console.W riteLine("H ello,

w orld!");

)
}
Теперь посмотрим, как использовать данный метод:
Human. H e l l o ( ) ;

Заметьте, вызвать таким образом метод GetInfo() не получится, поскольку
этот метод не является статическим и нужно сначала создать объект:
Human Den = new HumanC'Den” , 3 2 ) ;
Den. G e t l n f o ( ) ;

Логика в следующем: для работы GetInfo() нужно, чтобы были инициали­
зированы поля класса. Следовательно, его нельзя объявлять как статиче­
ский. А вот метод Не11о() независим от остального класса - он всегда будет
выводить одну и ту же строку, поэтому его можно объявить статическим.
Конструктор можно также объявить как static. Статический конструктор
обычно используется для инициализации компонентов, применяемых ко
всему классу, а не к отдельному экземпляру объекта этого класса. Поэтому
члены класса инициализируются статическим конструктором до создания
каких-либо объектов этого класса. Причем в одном классе может быть как
статический, так и обычный конструктор. Пример объявления двух кон­
структоров приведен в листинге 5.9.
Листинг 5.9. Статический и публичный конструкторы
u s in g System;
u sin g S y s t e m .C o l l e t t i o n s . Generic;
u sin g Systdm .Lind;

«В»................................................... <

usin g System .Text;
namespace C o n s o l e A p p l i c a t i o n l

{
c l a s s Sam pleClass

{
public s t a t i c
p u b l i c i n t y;

in t x;

/ / Статический конструктор
s t a t i c SampeClassO

{
x = 1;

}
/ / Обычный к о н с т р у к т о р
p u b l i c S a m p l e C l a s s ()

{
У = 12 ;

}
}
c l a s s P ro gr a m
{
s t a t i c v o i d M a i n ( s t r i n g [] a r g s )

{
C on sole.W riteLine("A ccess to x:

" + Sam pleC lass.x);

S a m p l e C l a s s o b j = new S a m p l e C l a s s ( ) ;
C o n so le .W r ite L in e ("A c cess to y: " + o b j . y ) ;

}
}
}
Более того, статическим можно объявить не только конструктор класса, но
и весь класс. Статические классы обладают двумя основными свойствами:
• Объекты статического класса создать нельзя.
• Статический класс должен содержать только статические члены.
Объявляется статический класс так:
static

c l a s s имя к л а с с а

{ //

5.2.12.

...

Индексаторы

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

индексировать подобно обычному массиву посредством определения ин­
дексатора. Другими словами, в C# можно обращаться к классу как к масси­
ву. Это позволяет создавать специальные типы коллекций.
Индексаторы бывают одномерными и многомерными. Последние исполь­
зуются редко, поэтому остановимся только на одномерных. Синтаксис
определения одномерного индексатора такой:
т и п _ э л е м е н т а t h i s [ i n t инде кс] {
/ / А к с е с с о р для получения данных,
get {
/ / Возвращает значение, которое определяет индекс.

}
/ / А к с е с с о р для у с т а н о в к и данных,
set {
/ / Устанавливает значение, которое определяет индекс.

}}
Рассмотрим, что есть что:


тип_элемента - конкретный тип элемента индексатора. У каждого
элемента, доступного с помощью индексатора, должен быть опреде­
ленный тип элемента. Этот тип соответствует типу элемента мас­
сива.



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

В теле индексатора определяются два аксессора (от англ, accessor): get и set.
Первый используется для получения значения, второй - для его установки.
Аксессор похож на метод, но в нем не указываются тип возвращаемого зна­
чения или параметры.
Аксессоры вызываются автоматически при использовании индексатора, и
оба получают индекс в качестве параметра. Аксессор set также получает не­
явный параметр value, который содержит значение, присваиваемое по ука­
занному индексу.
Все это звучит очень сложно, пока не виден код. Листинг 5.10 демонстри­
рует, как изящно можно превратить класс в массив и работать с ним как с
обычным массивом.

Листинг 5.10. пример использования индексатора
using
using
using
using

System;
S y s t e m .C o l le c t i o n s . Generic;
System .Linq;
System .Text;

namespace C o n s o l e A p p l i c a t i o n l

{
c l a s s MyArr

{
int[] arr;
p u b l i c i n t Length;
p u b l i c MyArr(int Siz e)

{
a r r = new i n t [ S i z e ] ;
Length = S i z e ;

}
/ / Простой и н д е к с а т о р
p u b l i c i n t t h i s [ i n t index]

{
set

/ / У с т а н а в л и в а е м мас сим э л е м е н т о в

{
arr[index]

= value;

}
get

{
return a r r [in d e x ];

}
}
}
class

Pr og r am

{
s t a t i c v o i d M ai n ( )

{
MyArr a r r l = new M y A r r ( 1 0 ) ;
/ / Инициализируем каждый и нд е кс э к з е м п л я р а к л а с с а a r r l
f o r ( i n t i = 0; i < a r r l . L e n g t h ; i++)

{
a r r l [ i ] = i * 2;
C onsole.W rite("{0}

)
}

",

arrl[i]);

}
}
В цикле for работа с переменной arrl осуществляется как с массивом. Од­
нако arrl - это объект типа МуАгг, а не массив.
5.2.13. Свойства

В C# есть еще одна диковинка - свойства. В других языках программирова­
ния, поддерживающих ООП, свойством называется член класса, содержа­
щий данные. Просто есть свойства и методы. Свойства - это переменные, а
методы - это функции. Грубо говоря, конечно. Но в C# все немного иначе.
Свойство сочетает в себе данные и методы доступа к ним.
Свойства очень похожи на индексаторы. В частности, свойство состоит из
имени и аксессоров get и set. Аксессоры служат для получения и установки
значения переменной. Разница в том, что имя свойства может быть исполь­
зовано в выражениях и операторах присваивания аналогично имени обыч­
ной переменной, но в действительности при обращении к свойству автома­
тически вызываются методы get и set.
Синтаксис описания свойства выглядит так:
тип имя {
get
{
//

код а к с е с с о р а для ч те н ия и з поля

}
set

{
//

код а к с е с с о р а для з а п и с и в поле

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

5.3. Перегрузка функций членов класса
5.3.1. Пеиегоузка методов

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

ToString(), который неявно есть у каждого класса), чтобы он соответство­
вал вашему классу, или же определить несколько методов, которые будут
определяться по-разному. Например, вы можете создать методы с одинако­
вым названием, но разными типами возврата, а компилятор будет выбирать
нужный метод, в зависимости от производимых вычислений.
Для перегрузки метода достаточно объявить разные его варианты, а об
остальном позаботится компилятор. Но при этом необходимо соблюсти
следующее важное условие: тип или число параметров у каждого метода
должны быть разными.
Недостаточно, чтобы методы отличались только типами возвращаемых
значений. Также они должны отличаться типами и/или числом своих пара­
метров. Если есть два метода с одинаковым списком параметров (совпада­
ют количество и типы), но с разным типом возвращаемого значения, ком­
пилятор просто не сможет выбрать, какой из них использовать. Хотя о типе
возвращаемого значения будет сказано чуть позже.
Пример перегруженного метода Info() приведен в листинге 5.11.
Листинг 5.11. Пример Пвр а гр у т А М м л т ч в т м
c la ss

С аг

{

/ / Пе рег ружа ем м е т о д I n f o ( )
p u b l i c v o i d I n f o ()

{
C o n so le .W rite L in e ("N o b ran d

se le c te d \n ");

}
p u b lic

v o id

In fo (strin g

Brand)

{
Console.W riteLine("Brand:

{ 0 } \ n N o model s e l e c t e d " , B r a n d ) ;

)
p u b l i c v o id I n f o ( s t r i n g Brand,

s t r i n g Model)

{
Console.W riteLine("Brand:

{ 0 } M od e l:

(1)",

Brand, Model);

)
}

В данном примере мы сначала объявляем метод Info(), а затем перегружаем
его два раза. Когда компилятор принимает решение о перегрузке метода, то
также учитываются модификаторы параметров ref и out.

Перегрузка методов поддерживает полиморфизм. В C# соблюдается глав­
ный принцип полиморфизма: один интерфейс - множество методов. В язы­
ках, где не поддерживается перегрузка методов, каждому методу должно
быть присвоено уникальное имя. Типичный пример - язык Си, в котором
есть функции abs(), fabs(), labs(). Все они вычисляют абсолютное значение
числа, но каждая из функций используется для своего типа данных.
В C# есть понятие сигнатуры. Сигнатура обозначает имя и список пара­
метров. Поэтому в классе не должно быть двух методов не с одинаковыми
именами, а с одинаковыми сигнатурами. В сигнатуру не входит тип воз­
вращаемого значения, поскольку он не учитывается, когда компилятор C#
принимает решение о перегрузке метода. В сигнатуру не входит также мо­
дификатор params.
5.3.2. Перегрузка конструкторов

Язык C# позволяет перезагружать конструкторы. Это позволяет создавать
объекты самыми разными способами. Пример перегрузки конструктора
класса Саг приведен в листинге 5.12.
Листинг 5.12. Перегрузка конструкторов
c l a s s Саг
{
/ / Перегружаем к о н с т р у к т о р
p u b l i c v o i d C a r ()

(
Console.W riteLine("N o brand s e l e c t e d \ n " );

}
p u b l i c v o i d C a r ( s t r i n g Br a nd )

(
Console.W riteLine("Brand:

( 0) \ nN o model s e l e c t e d " , B r a n d ) ;

)
p u b li c v o id C a r ( s t r i n g Brand,

s t r i n g Model)

(
Console.WriteLine("Brand:

{0} Model:

( 1 ) " , Br and, M o de l) ;

}
}
Самая распространенная причина перегрузки конструкторов - необходи­
мость инициализации объекта разными способами. При работе с перезагру­
жаемыми конструкторами есть возможность вызвать другой конструктор.
Делается это с помощью ключевого слова this, пример:

public Info() : this("None",

"None")

{
}
public Info(Car obj)
: this (obj.Brand, obj.Model)

{
}
public Info(string Brand, string Model)

{
this.Brand = Brand;
this.Model = Model;

)

5.3.3. Перегрузка операторов
В C# перегружать можно не только методы и конструкторы, но и операто­
ры. Например, вы можете перегрузить оператор + и определить, как будет
выполняться операция сложения нескольких ваших объектов.
По умолчанию перегруженными является все операторы, поскольку они
позволяют работать с данными разных типов. Например, тот же + можно
использовать для сложения чисел и строк:
int а = 1, b = 2;
int с = а + b;
string n = "user";
string s = "hello" + " " + user;

Сначала, как видите, оператор + используется для сложения двух целых чи­
сел, затем - для конкатенации трех строк.
Не все операторы могут быть перегружены. Таблица 5.3 содержит сведения
о том, какие операторы могут быть перегружены, а какие - нет.
Таблица 5.3. Возможность перегрузки операторов в C#

II

A

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

if
V

+, -,!, ++, —, true, false

л"

Возможность перегрузки

V
jf
if
II

Оператор(ы)

[]

0
+=.-=. *=,/=,%=,&=, |=,
A=, « = , » =

Не перезагружаются

Синтаксис перегрузки оператора отличается для унарных и бинарных опе­
раторов:
/ / Для унарных о п е р а т о р о в
p u b l i c s t a t i c в оз вр аща емый_ т ип o p e r a t o r o p ( т и п _ п а р а м е т р а
опера нд)

{
//

тело

}
/ / Для бинарных о п е р а т о р о в
p u b l i c s t a t i c в оз вр аща емый_ т ип o p e r a t o r o p ( т и п _ п а р а м е т р а 1
операнд1,
т и п _ п а р а м е т р а 2 опер анд2 )

{
//

тело

}
Разберемся, что есть что. Возвращаемый тип обозначает конкретный тип
значения, которое будет возвращаться операцией. Обычно возвращаемый
тип такой же, как и у класса, для которого перегружают оператор. Вместо
ор подставляется оператор, который нужно перегрузить, например, + или -.
Рассмотрим пример перегрузки операторов + и - для произвольного класса
(лист. 5.13).
Листинг 5.13. Перегрузка операторов + и using
using
using
using

System;
System .C ollections.G eneric;
System .Linq;
System .Text;

namespace C o n s o l e A p p l i c a t i o n l

{
class

Sam pleClass

{
/ / Переменные
p u b l i c i n t x , y;
p u b l i c S a m p l e C l a s s ( i n t x = 0,

i n t у = 0)

{
t h i s . x = x;
t h i s . у = у;

}
/ / Перегрузка +
p u b l i c s t a t i c S a m p l e C l a s s o p e r a t o r + ( Sa mp le Cl ass o l , S a mp l e C l a ss o2)
{

S a m p l e C l a s s r e s = new S a m p l e C l a s s ( ) ;
r e s .x = o l .x + o2.x;
r e s . у = o l .y + o2.y;
return re s;

}
II Перегрузка p u b l i c s t a t i c S a mp le C la ss o p e r a t o r - ( S a m p l e C l a s s o l , S a mp l e C l a ss o2)

{
S a m p l e C l a s s r e s = new S a m p l e C l a s s ( ) ;
r e s .x = o l .x - o2.x;
r e s . у = o l .y - o2.y;
return re s;

}
}
class

P ro gr a m

{
s t a t i c v o i d M a i n ( s t r i n g []

args)

{
S a m p l e C l a s s o b j l = new S a m p l e C l a s s ( 1 0 0 ,
S a m p l e C l a s s o b j 2 = new S a m p l e C l a s s ( - 7 4 ,
C onsole.W riteLine("First ob ject: " +
o b jl.x + " " + o b jl.y );
Console.W riteLine("Second o b je c t : " +
obj2.x + " " + ob j2 .y );
Sam pleClass obj3 = o b jl + o b j2 ;
C on sole.W riteLin e("objl + obj2:
+ obj3.x + " " + o b j3 .y );

64);
28);

"

obj3 = objl - obj2;
Console.WriteLine("objl - obj2: "
+ obj3.x + " " + obj3.y);

}
}
}

5.4. Наследование и полиморфизм
5.4.1.

Введение в наследование

Наследование - один из трех основных принципов объектно-ориентиро­
ванного программирования. Наследование допускает создание иерархиче­
ских классификаций. Благодаря ему можно создать общий класс, в котором
определены характерные особенности, которые свойственны множеству
связанных элементов. От этого класса могут создаваться производные клас­
сы (могут наследовать базовый), добавляя в общий класс свои особенности.

Класс, который наследуется, называется базовым, а класс, который насле­
дует, называется производным. В других языках программирования может
использоваться другая терминология, например, базовый класс может на­
зываться родительским, а производный - дочерним.
Для объявления производного класса используется следующий синтаксис:
class Имя_производного_класса : Имя_базового_класса
{

Рассмотрим пример наследования классов (листинг 5.14). Мы определим
два класса - Parent (базовый) и Child (производный от базового). В классе
Parent объявлены два поля х и у. В классе Child объявлено только поле z.
Листинг 5.14. Наследование классов
using
using
using
using
using

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

namespace ParentChild
{
class Parent
{
public int x, y;
public Parent()

{
x = 10;
У = 20;

}
}
class Child : Parent
{
public int z;
public Child()

{
у = 30;
z = 50;

}
}
class Program
{

ГЛАВА 5. Объектно-ориентированное программирована

static void Main(string[] args)

{
Parent p = new Parent ();
Child c = new ChildO;
Console.WriteLine("Parent x, y: (0} {1}", p.x, p.y);
Console.WriteLine("Child x, у, z: {0} {1} (2}", c.x, c.y, c.z);
Console.ReadLine();

}
}
}
Класс Child наследует все члены базового класса, следовательно, он унас­
ледует поля х н у . Посмотрите, как конструктор класса Child изменяет зна­
чение поля у, которое не было у него определено. Далее в программе мы
обращаемся к полям х и у класса Child, которые не были изначально опре­
делены.
5.4.2. Защищенный доступ

В листинге 5.14 все члены классов были публичными (объявлены с моди­
фикатором доступа public) - так было нужно для простоты изложения. Но,
как мы знаем, члены могут приватными (private).
Приватный член базового класса недоступен для всего внешнего кода, в том
числе и для производного класса. Но иногда возникают ситуации, когда
нужно предоставить доступ к определенным членам только производным
классам, а чтобы все остальные классы (которые не являются производны­
ми) использовать эти члены не могли. Такие члены называются защищен­
ными (protected). Защищенный член является открытым в пределах иерар­
хии классов, но закрытым за пределами этой иерархии.
Для объявления защищенного члена используется модификатор доступа
protected. Используя protected, можно создать члены класса, являющиеся
закрытыми для своего класса, но все же наследуемыми и доступными для
производного класса.
Пример:
class Parent
{
public int x;
protected int у;
private int z;
int а;
?

// доступен для наследования и для внешнего кода
// дост. для наследования и производных классов
// не доступен для наследования и произв. класса
// то же, что и private

p u b l i c P a r e n t ()

{
х = 1 0;
У = 20;
z = 100;
а = х;

}
}
5.4.3.

Запечатанные классы. Ключевое слово sealed

В C# можно создавать классы, недоступные для наследования. Такие клас­
сы называются запечатанными (sealed). Для объявления запечатанного
класса используется ключевое слово sealed:
sealed c la ss

Parent

Если в листинге 5.14 объявить класс Parent как запечатанный, то компиля­
тор сообщит об ошибке (рис. 5.3):
'C h ild ':

c a n n o t d e r i v e fr om s e a l e d t y p e

'P arent'

5.4.4. Наследование конструкторов

В предыдущем примере у каждого из классов был свой конструктор. Как
было отмечено, при наследовании наследуются все члены, в том числе и
конструкторы. Но в иерархии классов допускается, чтобы у базовых и про­
изводных классов были свои собственные конструкторы. Как понять, какой
конструктор отвечает за производный класс - конструктор базового, произ­
водного класса или же оба?
Конструктор базового класса создает базовую часть объекта. В случае с при­
мером из листинга 5.14 конструктор класса Parent() задает начальные зна­
чения полей х и у базового класса Parent. Конструктор производного класса
создает производную часть объекта. В нашем случае - задает значение поля
z и переопределяет значение у, определенное в базовом классе.
Логика следующая - базовому классу неизвестны (да и недоступны) эле­
менты производного класса, следовательно, их создание должно происхо­
дить раздельно.
Если же конструктор определен только в производном классе, то констру­
ируется производный класс, а базовая часть объекта создается конструкто­
ром по умолчанию.
5.4.5. Сокрытие имен. Ключевое слово base

В производном классе могут быть определены члены с таким же именем,
как в производном классе. В этом случае член базового класса будет скрыт
в производном классе. Формально это не считается ошибкой, но все же ком­
пилятор выдаст предупреждение (рис. 5.4). Рассмотрим пример кода, в ко­
тором в классе Child определяется поле у - с таким же именем, как в базовом
классе.

Листинг 5.15. Сокрытие имен
c l a s s Parent
{

public int x, у;
p u b l i c P a r e n t ()

{
x = 10 ;
У = 20;

}
}
c l a s s C hild : Parent
{
p u b l i c i n t z;

public int у;
p u b l i c C h i l d ()
{
у = 30;
z - 50;

}
}

Рис. 5.4. Предупреждение компилятора

Как показано на рис. 5.4, компилятор сообщает о том, что Child.y скрывает
унаследованный член Parent.y. Если член базового класса требуется скрыть
намеренно, то перед его именем следует указать ключевое слово new, чтобы
избежать появления подобного предупреждающего сообщения (рис. 5.5):
p u b l i c new i n t

у;

Рис. 5.5. Никаких предупреждений компилятора
*

Если таки нужно будет обратиться к полю у из базового класса, нужно ис­
пользовать ключевое слово base:
b a s e . у = 5;

5.4.6. Виртуальные члены

В данном разделе мы поговорим о виртуальных членах класса. С помощью
полиморфизма и переопределения методов подкласс может определить
собственную версию метода, определенного в базовом классе. О переопре­
делении членов мы уже говорили, но нерассмотренными остались ключе­
вые слова virtual и override.
В C# существует специальный тип методов - виртуальные. Метод, опреде­
ленный, как virtual, в базовом классе. Виртуальный метод может быть пере­
определен в одном или нескольких производных классах. Следовательно,
у каждого производного класса может быть свой вариант виртуального ме­
тода.
Виртуальные методы представляют интерес тем, что происходит при их вы­
зове по ссылке на базовый класс. Среда .NET определяет именно тот вари­
ант виртуального метода, который нужно вызвать, в зависимости от типа
объекта, к которому происходит обращение по ссылке. Обратите внимание:
именно среда .NET, а не компилятор С#, поэтому выбор виртуального мето­
да происходит при выполнении, а не при компиляции.
Если базовый класс содержит виртуальный метод, и он наследуется произ­
водными классами, при обращении к разным типам объектов по ссылке на
базовый класс выполняются разные варианты этого виртуального метода.
Объявить виртуальный метод можно с помощью ключевого слова virtual,
которое указывается перед его именем. Для переопределения виртуального
метода в производном классе используется модификатор override. Вирту­
альный метод не может быть объявлен как static или abstract (см. след, раз­
дел).
Процесс повторного определения виртуального метода в производном
классе называется переопределением метода. При переопределении мето­
да имя, возвращаемый тип и сигнатура переопределяющего метода должны
быть точно такими же, как и у того виртуального метода, который переопре­
деляется.
Пример виртуального метода в базовом классе:
public v ir tu a l

s t r i n g M y S t r i n g ( s t r i n g s)

{
retu rn "MyString " + s ;

}
Пример переопределения метода в производном классе:
public override string MyString(string s)

{
return "Overrided string " + s;

)
5.4.7. Абстрактные классы

Иногда нужно создать базовый класс, где определяется самая общая фор­
ма для всех его производных классов. Предполагается, что наполнять эту
форму деталями будут производные классы. В таком общем классе опреде­
ляется лишь характер методов, грубо говоря, в нем определяются функциизаглушки. Реализовывать данные методы будут производные классы.
При создании собственных библиотек классов вы можете убедиться, что у
метода часто отсутствует содержательное определение в контексте его ба­
зового класса. Данную ситуацию можно решить двумя способами. Первый
из них заключается в выводе предупреждающего сообщения. При отладке
данный способ пригодится, но в production-версии кода он недопустим. Ну­
жен способ, гарантирующий, что в производном классе действительно бу­
дут переопределены все необходимые методы. Такой способ заключается в
использовании абстрактного метода.
Абстрактный метод создается с помощью модификатора abstract. У аб­
страктного метода отсутствует тело и он не реализуется в базовом классе.
Данный метод должен быть реализован (переопределен) в производном
классе, поскольку его вариант из базового класса просто непригоден для ис­
пользования. Абстрактный метод автоматически становится виртуальным,
поэтому он не требует указания модификатора virtual. Более того, совмест­
ное использование модификаторов virtual и abstract приведет к ошибке.
Синтаксис определения абстрактного метода следующий:
abstract тип имя(список_параметров);

Пример определения абстрактного класса:
class Parent
{

public int x, у;
public Parent()

{

t

х = 10;
У = 20;

}
public abstract int sum();

}
В производном классе абстрактный метод переопределяется так:
public override string sum()

{
return x + у + z;

}
Следующая глава будет не менее интересной. В ней мы поговорим об интер­
фейсах, структурах и перечислениях.

Глава 6.

Интерфейсы, структуры и
перечисления

6.1. Понятие интерфейса
Первым делом нужно познакомиться с понятием интерфейса (interface).
Интерфейс представляет собой именованный набор абстрактных членов.
Абстрактные методы, с которыми мы познакомились в прошлой главе, не
имеют никакой стандартной реализации. Конкретные члены, определяе­
мые интерфейсом, зависят от того, какое поведение моделируется с его по­
мощью. Интерфейс выражает поведение класса или структуры.
В библиотеках базовых классов .NET поставляются сотни предопределен­
ных типов интерфейсов, которые реализуются в разных классах.
В интерфейсе тело ни одного из методов не определяется. Другими слова­
ми, в интерфейсе вообще нет никакой реализации. В нем только указано,
что следует сделать, но не написано, как это сделать. После определения
интерфейс может быть реализован в любом количестве классов. А в одном
классе можно реализовать любое количество интерфейсов. Благодаря под­
держке интерфейсов в C# полностью реализован основной принцип поли­
морфизма: один интерфейс - множество методов.
Чтобы реализовать интерфейс в классе, нужно указать тела методов, опи­
санных в этом интерфейсе. Каждый класс может как угодно определять
свою собственную реализацию интерфейса. А это означает, что в двух раз­
ных классах интерфейс может быть реализован по-разному. Но в каждом
из них должен поддерживаться один и тот же набор методов данного ин­
терфейса.
Интерфейс объявляется с помощью ключевого слова interface. Рассмотрим
синтаксис объявления:
i n t e r f a c e имя{
r e t u r n _ t y p e met ho d_ na me _ l
r e t u r n _ t y p e method_name_2

П

(args);
(args);

...

r e t u r n _ t y p e method_name_N

(args);

)
Здесь имя - имя конкретного интерфейса, return_type - возвращаемый тип,
method_name_N - имя метода, args - список аргументов. По существу все
эти методы являются абстрактными.

Как видите, в интерфейсе нет никакой реализации. Просто список методов.
В интерфейсе все методы неявно считаются публичными, поэтому public
явно указывать не нужно.
Кроме методов в интерфейсах могут быть указаны свойства, индексато­
ры и события. Но интерфейсы не могут содержать члены данных. Также в
интерфейсах нельзя определить конструкторы и деструкторы. Ни один из
членов интерфейса не может быть статическим.
После определения интерфейса его можно использовать в одном или не­
скольких классах. Чтобы реализовать интерфейс, нужно указать его имя
после имени класса, аналогично тому, как вы это делали при указании базо­
вого класса:
c l a s s и м я _ к л а с с а : имя_ ин те рфей са {
/ / тело к л асса

}
В классе можно реализовать несколько интерфейсов. Для этого нужно ука­
зать список интерфейсов через запятую. В классе можно наследовать базо­
вый класс и реализовать один или более интерфейсов. В этом случае имя
базового класса должно быть указано перед списком интерфейсов, разделя­
емых запятой.
Методы, реализующие интерфейс, должны быть объявлены как public. Воз­
вращаемый тип и сигнатура реализуемого метода должны точно соответ­
ствовать возвращаемому типу и сигнатуре, которые указаны при определе­
нии интерфейса.
Понимаю, что сейчас совсем ничего не понятно, но все прояснится, как
только мы рассмотрим пример, приведенный в листинге 6.1.
Листинг 6.1. Пример определения интерфейсов
using
using
using
using

System;
System .C ollections.G eneric;
System .Linq;
System .Text;

namespace C o n s o l e A p p l i c a t i o n l
{
/ / Описываем инт ерфейс
public in te rfa ce ITest
{
/ / Определяем наб ор а б с т р а к т н ы х м е т о д о в
in t T estl ();
in t Test2 ();

}
/ / Данный к л а с с р е а л и з у е т и нт е рф ей с I T e s t
c l a s s MyClass : I T e s t
{
i n t My_x;
/ / Пример g e t - т е р а и s e t - т е р а з а к р ы т о г о поля х
public in t х
{
s e t { My_x = v a l u e ; }
g e t { r e t u r n My_x; }

}
p u b l i c M y C l a s s ()

{ }

/ / Р е а л и з у е м методы и н т е р ф ей с а I T e s t
public v ir tu a l in t T estlO

{
r e t u r n 1;

}
p ub lic in t Test2()

{
r e t u r n 2;

}
}
class

P ro gr a m

{
s t a t i c v o i d Main()

{
M y C l a s s o b j 1 = new M y C l a s s ( ) ;
in t t l = o b j1 .T estl ();
i n t t2 = o b j 1 . T est2 ( );
Console.W riteLine ("{0 }

{1}",

tl,

t2);

C on sole.ReadLine();

}
}
}

6.2. Ключевые слова as и is
Ключевое слово as позволяет определить, поддерживает ли данный тип тот
или иной интерфейс. Если объект удается интерпретировать как указанный

интерфейс, то возвращается ссылка на интересующий интерфейс, а если
нет, то ссылка null. Следовательно, перед продолжением в коде необходимо
предусмотреть проверку на null. Рассмотрим небольшой пример:
IT e st obj = o b jl a s I T e s t ;
i f (obj != n u l l )
C o n s o l e . W r i t e L i n e ( " Е с т ь поддержка и н т е р ф ей с а I T e s t " ) ;
else
C o n s o l e . W r i t e L i n e ( " I T e s t не п о д д е р ж и в а е т с я " ) ;

Проверить, был ли реализован нужный интерфейс, можно с помощью клю­
чевого слова is. Если запрашиваемый объект не совместим с указанным ин­
терфейсом, возвращается значение false, а если совместим, то можно спо­
койно вызывать члены этого интерфейса:
if

(objl i s ITest)
Console.W riteLine("Д а");
else
Console.W riteLine("Н ет");

6.3. Интерфейсные свойства
Подобно методам, свойства указываются в интерфейсе вообще без тела.
Синтаксис объявления интерфейсного свойства выглядит так:
/ / Интерфейсное с в о й с т в о
тип и м я {
get;
set;

)
В определении интерфейсных свойств, доступных только для чтения или
только для записи, должен присутствовать единственный аксессор: get или
set соответственно.
Пример определения интерфейсных свойств приведен в листинге 6.2.
Листинг 6.2. Пример определения интерфейсных свойств
u s i n g System;
namespace C o n s o l e A p p l i c a t i o n l
{
in terfa ce ITest
{
strin g Str
{
get;

set;

}
}
c l a s s MyClass : I T e s t
{
s t r i n g myStr;
public strin g Str
{
set

{
myStr = v a l u e ;

}
get

{
re tu r n myStr;

}
}
}
c l a s s P rog ra m
{
s t a t i c v o i d M ai n ( )

{
M y C l a s s o b j 1 = new M y C l a s s ( ) ;
u s e r l.S t r = "H ello";
C on sole. ReadLine();

}
}
}

6.4. Интерфейсы и наследование
Подобно классам, один интерфейс может наследовать другой, при этом син­
таксис наследования интерфейсов такой же, как у классов. Если в классе
реализуется один интерфейс, наследующий другой, в нем должны быть ре­
ализованы все члены, определенные в цепочке наследования интерфейсов.
Интерфейсы могут быть организованы в иерархии. Если какой-то интер­
фейс расширяет существующий, он наследует все абстрактные члены своих
родителей. В отличие от классов, производные интерфейсы никогда не на­
следуют саму реализацию. Вместо этого они просто расширяют собствен­
ное определение за счет добавления дополнительных абстрактных членов.
Пример наследования интерфейсов приведен в листинге 6.3.
Листинг 6.3. Пример наследования интерфейсов
u s in g System;

n am espace C o n s o le A p p lic a tio n l
{
p u b lic

in te rfac e

A

<
in t

T estA O ;

}
/ / И нтерфейс В н а с л е д у е т и н тер ф ей с А
p u b lic in te r fa c e В : А
{
in t

T estB O ;

}
/ / К ласс M y C lass н а сл е д у е т ин терф ей с В
/ / и р е а л и зу е т методы ин терф ей сов А и В
c l a s s M y C lass : В
{
p u b lic

in t

T estA O

{
retu rn

1;

}
p u b lic

in t

T estB O

{
retu rn

2;

}
}
c la ss

P rogram

{
sta tic

v o i d M a i n ()

{
}
}
}

6.5. Структуры
Объекты конкретного класса доступны по ссылке (поскольку классы отно­
сятся к ссылочным типам данных), в отличие от значений обычных типов,
которые доступны непосредственно. В некоторых ситуациях для повыше­
ния эффективности программы полезно иметь прямой доступ к объектам
как к значениям простых типов. Ведь каждый доступ к объекту (даже к са­
мому небольшому) требует дополнительные системные ресурсы (оператив­
ную память и процессорное время).
Для решения подобных задач используются структуры. Структуры подоб­
ны классам, но они относятся не к ссылочным типам данным. Структуры
отличаются от классов способом хранения в памяти и способом доступа к

ним. Классы размещаются в куче, а структуры - в стеке. Это и есть самая
большая разница. Из соображений эффективности программы структуры
полезно использовать для небольших типов данных.
Синтаксически описание структуры похоже на классы. Основное отличие
в том, что класс определяется с помощью служебного слова class; а струк­
тура - с помощью служебного слова struct. Синтаксис следующий:
struct имя : интерфейсы {
// объявления членов
}

Как и у класса, у структуры могут быть методы, поля, индексаторы, свой­
ства, методы и события. В структурах даже можно объявлять конструкто­
ры, но не деструкторы. Однако в структуре нельзя определить конструктор
по умолчанию (конструктор без параметров).
Поскольку структуры не поддерживают наследование, их члены нельзя
указывать как abstract, virtual или protected.
Объект структуры можно создать с помощью оператора new - как вы это
делали для класса. Но обычно в этом нет необходимости, поскольку при ис­
пользовании оператора new вызывается конструктор по умолчанию, а если
вы не используете оператор new, то объект все равно создается, но не ини­
циализируется. Пример объявления структуры приведен в листинге 6.4.
Листинг 6.4. Пример структуры
using System;
namespace ConsoleApplicationl
{
// Создадим структуру
struct Carlnfo

{
public string Brand;
public string Model;
public Carlnfo(string Brand, string Model)

{
this.Brand = Brand;
this.Model = Model;

}
public void GetCarlnfoO

<

Brand,

C o n s o l e . W r i t e L i n e ("Бренд:
M odel);

(0),

Модель:

{1}",

}
}
class

{

Pr og r am

s t a t i c v o i d M a i n( )

{
C a r l n f o c a r l = new
C a r l n f o c a r 2 = new
C onsole.W rite("car
c a r l . G etCarlnfо ();
Console.W rite("car
c a r 2 . G etCarlnfо ();

C a rln fо ("Audi", "A6");
C a r l n f о ( "BMW", " X 5 " ) ;
1: " ) ;
2:

");

c a rl = car2;
car2.Brand = "Toyota";
ca r 2 .M o d e l = "Camry";
C o n sole.W rite("carl: ");
c a r . G etCarlnfо ();
C o n s o le .W r i t e (" c a r 2 : " ) ;
c a r 2 . GetCarlnfо ();
C o n so le . ReAdLine();

}
}
}
Вывод программы будет таким:
carl:
car2:
carl:
с а г 2:

Бренд
Бренд
Бренд
Бренд

Audi Модель А6
BMW Модель Х5
BMW Модель Х5
T o y o t a Модель Camry

Если одна структура присваивается другой, то при этом создается копия ее
объекта. Это главное отличие структуры от класса. Когда ссылка на один
класс присваивается ссылке на другой класс, в итоге ссылка в левой части
оператора присваивания указывает на тот же самый объект, что и ссылка в
правой его части. А когда переменная одной структуры присваивается пере­
менной другой структуры, создается полная копия объекта структуры из

правой части оператора присваивания. Если бы вместо структуры Carlnfo
мы бы использовали класс с таким же именем, то вывод программы былбы
таким:
carl:
саг2:
carl:
саг2:

Бренд
Бренд
Бренд
Бренд

Aud i Модель Аб
BMW Модель Х5
T o y o t a Модель Camry
T o y o t a Модель Camry

6.6. Перечисления
Перечисление (enumeration) — это определяемый пользователем целочис­
ленный тип. Когда вы объявляете перечисление, то указываете набор до­
пустимых значений, которые могут принимать экземпляры перечислений.
Этим значениям можно присвоить имена, понятные человеку. Если присво­
ить экземпляру перечисления значение, не входящее в список ранее опреде­
ленных, компилятор выдаст ошибку.
Синтаксис определения перечисления такой:
епшп имя { с п и с о к _ п е р е ч и с л е н и я } ;

Листинг 6.5 содержит пример определения и использования перечисления.
Листинг 6.5. Использование перечислений
u s i n g System;
namespace C o n s o l e A p p l i c a t i o n l
{
/ / С о з д а т ь п е ре ч ис ле н ие
enum c a r : l o n g { B r a n d , M o d e l,

Year,

Engine

}

c l a s s P ro gr a m

{
s t a t i c v o i d M ai n ( )

I
car ca rl;
f o r ( c a r l = c a r . B r a n d ; c a r l {0} { l ) " , d . K e y , d . V a l u e ) ;

)
}

7.4. Исключения уровня системы
В библиотеке классов .NET содержится множество классов, которые насле­
дуются от System. Exception. Так, в пространстве имен System определены
ключевые классы исключений - StackOverflowException (исключение пере­
полнения стека), IndexOutOfRangeException (исключение выхода за диа­
пазон) и др. В других пространствах имен также определены исключения
- например, исключения, возникающие при вводе/выводе, исключения, ко­
торые связаны с базами данных и т.д.
Исключения, генерируемые самой платформой .NET, называются ис­
ключениями уровня системы. Такие исключения считаются также фа­
тальными ошибками. Они наследуются прямо от базового класса System.
SystemException, который, в свою очередь, наследуется от System.Exception.
Кроме исключений уровня системы есть еще и исключения уровня при­
ложений (класс System.AppicationException). Если вам нужно создать соб­
ственные исключения, предназначенные для конкретного приложения, их
нужно наследовать от System.AppicationException.
В классе System.AppicationException никаких членов, кроме набора кон­
структоров, не предлагается. Единственная цель этого класса - указание
на источник ошибки. Если произошло исключение, унаследованное от
System.ApplicationException, программист может смело полагать, что ис­
ключение было вызвано кодом функционирующего приложения, а не би­
блиотекой базовых классов .NET.
Если вы планируете создавать свои классы исключений, то вы должны при­
держиваться рекомендаций .NET, а именно, пользовательские классы долж­
ны:



наследоваться от ApplicationException;



содержать атрибут [System.Serializable];



иметь конструктор по умолчанию;



иметь конструктор, который устанавливает значение унаследован­
ного свойства Message;



иметь конструктор для обработки "внутренних исключений";



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

Рассмотрим "болванку" такого класса (лист. 7.1).
Листинг 7.1. Болванка класса пользовательского исключения
[System .Serializable]
p u b l i c c l a s s MyExc : A p p l i c a t i o n E x c e p t i o n

{
p u b l i c MyExc
p u b l i c MyExc
p u b l i c MyExc

() { }
( s t r i n g message)
( s t r in g message,

: base(m essage) { )
E x c e p t io n ex) : b a s e ( m e s s a g e )

< }
pr ot ec te d MyExc ( S y s t o n . R u n ti me . Se r ia l iz at i on .S e ri a l iz a ti o nl nf o info.
S yst em. Run ti me . S e r i a l i z a t i o n . S t re am in g Co n te xt c o n t e x t )
: base (info, context) { }

}
Данный класс, хотя ничего и не делает, но он полностью соответствует ре­
комендациям .NET.

7.5. Ключевое слово finally
Осталось нерассмотренным ключевое слово finally. Если вам нужно опреде­
лить код, который будет выполняться после выхода из блока try/catch, вам
нужно использовать блок finally. Использование этого блока гарантирует,
что некоторый набор операторов будет выполняться всегда, независимо от
того, возникло исключение (любого типа) или нет.
Синтаксис следующий:
try {
/ / Блок к о д а ,

проверяемый на наличие ошибок.

>
c a t c h ( E x c e p t exOb) {
/ / Обработ чик исключения E x c e p t

}
c a t c h ( Ех се р2 exOb) {
/ / Обработчик исключения Е х се р 2
}
finally {
/ / Эт от код б у д е т выполнен п о с л е о б р а б о т к и исключений
}

Блок finally выполняется каждый раз, когда происходит выход из блока
try/catch, независимо от причин, которые привели к этому блоку. Если блок
try завершается нормально или по причине исключения, то последним вы­
полняется код, определяемый в блоке finally. Блок finally выполняется и в
том случае, если любой код в блоке try или в связанных с ним блоках catch
приводит к возврату из метода.
Пример:
int х = 1 0 , у = 0 ,
try
{
z = х / у;

z;

}
catch

(DivideByZeroException)

{
C o n s o l e . W r i t e L i n e ( "Деление на О " ) ;

}
finally

{
C o n s o l e . W r i t e L i n e ( "Конец п р о г р а м м ы " ) ;

}

7.6. Ключевые слова checked и unchecked
Ранее я обещал, что в этой главе мы рассмотрим ключевые слова checked и
unchecked. В языке C# можно указывать, будет ли в коде сгенерировано ис­
ключение при переполнении. Для этого используются эти ключевые слова.
Если нужно указать, что выражение будет проверяться на переполнение,
нужно использовать ключевое слово checked. Если нужно проигнорировать
переполнение - ключевое слово unchecked.
Синтаксис такой:
checked {
/ / о п е ра т ор ы
}

Если вычисление проверяемого выражения приводит к переполнению, то
будет сгенерировано исключение OverflowException. Синтаксис unchecked
такой же:
unchecked {
// операторы

)
Рассмотрим пример кода:
byte х, у, res;
try
{
Console.Write("Введите x:");
x = unchecked((byte)int.Parse(Console.ReadLine()));
Console.Write("Введите у:");
у = unchecked((byte)int.Parse(Console.ReadLine()));
checked

{
res = (byte)(x + y);
Console.WriteLine("res = (0)", res);

)
}
catch (OverflowException)

{
Console.WriteLine("Переполнение");

}
Здесь специально используется тип byte (диапазон 0..255), чтобы про­
ще было вызвать переполнение. Потенциально опасный код заключен в
checked. Обратите внимание: код, получающий значения х и у, заключен
в unchecked. Для одиночного выражения допускается использование кру­
глых скобок вместо фигурных.

Глава 8.

Коллекции и итераторы

8.1. Введение в коллекции
Коллекция - это совокупность объектов. В среде .NET существует множе­
ство интерфейсов и классов, в которых определяются и реализуются раз­
личные типы коллекций.
С помощью коллекций существенно упрощается программирование мно­
гих задач, поскольку они предлагают готовые решения для создания целого
ряда типичных, но иногда очень трудных для разработки структур данных.
Так, в среду .NET встроены коллекции для поддержки хеш-таблиц, динами­
ческих массивов, очередей, стеков, связных списков. Коллекции заслужи­
вают внимания всех С#-программистов. Зачем изобретать колесо заново?
Ранее существовали только классы необобщенных коллекций. Но позже
появились обобщенные классы и интерфейсы. Благодаря этому общее ко­
личество классов удвоилось. Вместе с библиотекой TPL (используется для
распараллеливания задач) появился ряд новых классов коллекций, пред­
назначенных для применения в тех случаях, когда доступ к коллекции осу­
ществляется из нескольких потоков. О многопоточности мы поговорим в
последней главе этой книги.
Интерфейс Collections API настолько важен, что он занимает огромную
часть всей среды .NET.
Все коллекции разработаны на основе набора четко определенных ин­
терфейсов. Некоторые встроенные реализации таких интерфейсов, в том
числе ArrayList, Hashtable, Stack и Queue, могут применяться в исходном
виде - без изменений. При желании программист может создать собствен­
ную коллекцию, но учитывая богатый набор коллекций, такая необходи­
мость возникает редко.
Примечание. Если вы ранее программировали на C++, то вам будет интересно
знать, что классы коллекций по своей сути подобны классам стандартной библио­
теки шаблонов (Standard Template Library — STL), определенной в C++. Коллекция
в терминологии C++ - это не что иное, как контейнер.

В .NET поддерживаются пять типов коллекций:



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

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



Специальные - работают с данными конкретного типа. Имеются
специальные коллекции для символьных строк, а также специаль­
ные коллекции, в которых используется однонаправленный спи­
сок. Такие коллекции объявляются в пространстве имен System.
Collections.Specialized.



Поразрядная коллекция - такая коллекция одна, но она не попа­
дает ни под один другой тип коллекций. Она определена в System.
Collections и называется BitArray. Коллекция поддерживает пораз­
рядные операции, т.е. операции над отдельными двоичными разря­
дами, например И, ИЛИ, исключающее ИЛИ.



Обобщенные - такие коллекции обеспечивают обобщенную реали­
зацию нескольких стандартных структур данных, включая связные
списки, стеки, очереди и словари. В силу своего обобщенного харак­
тера такие коллекции являются типизированными. Объявляются в
пространстве имен System.Collections.Generic.



Параллельные - поддерживают многопоточный доступ к коллекции.
Определены в пространстве имен System.Collections.Concurrent.

Основным для всех коллекций является понятие перечислителя, который
поддерживается в необобщенных интерфейсах IEnumerator и IEnumerable,
а также в обобщенных интерфейсах IEnumerator и IEnumerable.
Перечислитель предоставляет стандартный способ поочередного доступа к
элементам коллекции. Другими словами, он перечисляет содержимое кол­
лекции.
В каждой коллекции реализована обобщенная или необобщенная фор­
ма интерфейса IEnumerable, элементы любого класса коллекции должны
быть доступны с помощью методов, которые определены в интерфейсе
IEnumerator или IEnumerator. Для поочередного обращения к содер­
жимому коллекции в цикле foreach используется перечислитель.
С перечислителями тесно связаны итераторы. Итератор упрощает процесс
создания классов коллекций, например специальных, поочередное обраще­
ние к которым организуется в цикле foreach.
В таблице 8.1 приведены интерфейсы, реализуемые в коллекциях С#.

Таблица 8.1. Интерфейсы, реализуемые в коллекциях C#
Интерф ейс

Описание

ICollection

Интерфейс, реализованный классами обоб­
щенных коллекций. Позволяет получить ко­
личество элементов в коллекции (свойство
Count), скопировать коллекцию в массив
(метод СоруТоО). Также позволяет добав­
лять и удалять элементы из коллекции (м е­
тоды Add(), Remove(), Clear()).

IEnumerable

Необходим, когда с коллекцией использует­
ся оператор foreach. Этот интерфейс опре­
деляет метод GetEnumerator(), возвращ а­
ющий перечислитель, который реализует
lEnumerator.

ISet

Впервые появился в версии .NET 4. Реали­
зуется множествами. Позволяет комбини­
ровать различные множества в объедине­
ния, а также проверять, не пересекаются
ли два множества. ISet унаследован от
ICollection.

IList

Предназначен для создания списков, эле­
менты которых доступны по своим позициям.
Определяет индексатор, а также способы
вставки и удаления элементов в определен­
ные позиции (методы lnsert() и RemoveO).
IList унаследован от ICollection.

I C o m pa r er < T >

Реализован компаратором и используется
для сортировки элементов внутри коллек­
ции с помощью метода Compare().

IDictionary

TValue>

IProducerConsumerCollection

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

IEqualityComparer

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

8.2. Необобщенные коллекции
Необобщенные коллекции вошли в состав среды первыми. Они появились
в самой первой версии .NET, поэтому считаются самыми давними. Необоб­
щенные коллекции определяются в пространстве имен System.Collections.
Такие коллекции представляют собой структуры данных общего назначе­
ния, оперирующие ссылками на объекты. Позволяют манипулировать объ­
ектом любого типа, хотя и не типизированным способом.
В способе их манипулирования объектами заключается их основное пре­
имущество и их основной недостаток. Необобщенные коллекции опери­
руют ссылками на объекты, в них можно хранить разнотипные данные. В
некоторых ситуациях это удобно. Например, если вам нужно манипулиро­
вать совокупностью разнотипных объектов или же когда типы хранящихся
в коллекции объектов заранее неизвестны. Однако, если коллекция пред­
назначена для хранения объекта конкретного типа, то здесь вам придется
столкнуться с одним неприятным нюансом: необобщенные коллекции не
обеспечивают типовую безопасность, которая обеспечивается обобщенны­
ми коллекциями.
Необобщенные коллекции определены во многих интерфейсах и классах,
которые реализуют эти интерфейсы.
Множество необобщенных коллекций определено в пространстве имен
System.Collections. Интерфейсы, являющиеся фундаментом для необоб­
щенных коллекций, представлены в таблице 8.2.
Таблица 8.2. Интерфейсы, используемые в необобщенных коллекциях
Интерфейс

Описание

ICollection

Определяет элементы, которые должны иметь все
необобщенные коллекции

IComparer

Определяет метод Compare() для сравнения объ­
ектов, хранящихся в коллекции

1Dictionary

Определяет коллекцию, состоящую из пар "ключзначение" (словарь)

ЯП

*

IDictionaryEnumerator

Определяет перечислитель для коллекции, реали­
зующей интерфейс IDictionary

Enum erable

Определяет метод GetEnumerator(), предоставля­
ющий перечислитель для любого класса коллек­
ции

Enum erator

Предоставляет методы, позволяющие получать
содержимое коллекции по очереди

EqualityCom parer

Сравнивает два объекта на предмет равенства

IHashCodeProvider

Устарел. Используйте интерфейс EqualityCom parer

IList

Определяет коллекцию, доступ к которой можно
получить с помощью индексатора

IStructuralComparable

Содержит метод CompareTo(), применяемый для
структурного сравнения

IStructuralEquatable

Содержит метод Equals(), применяемый для вы­
яснения структурного, а не ссылочного равенства.
Кроме того, определяет метод GetHashCodeO

Особое место в программировании занимают словари. Интерфейс
IDictionary определяет коллекцию, которая состоит из пар “ключзначение”. В пространстве имен System.Collections определена структура
DictionaryEntry. Необобщенные коллекции пар “ключ-значение” сохраня­
ют эти пары в объекте типа DictionaryEntry. В структуре DictionaryEntry
определяются два следующих свойства:
p u b l i c o b j e c t Key { g e t ; s e t ; }
p u b l i c o b j e c t Value { g e t ; s e t ; }

Данные свойства используются для доступа к ключу или значению, связан­
ному с элементом коллекции. Построить объект типа DictionaryEntry мож­
но с помощью следующего конструктора:
p u b l i c D i c t i o n a r y E n t r y ( o b j e c t k e y,

o b je c t value)

Параметр key - это ключ, value - это значение.
Таблица 8.3 содержит классы необобщенных коллекций.

Таблица 8.3. Классы необобщенных коллекций
Класс
A rrayList
H ashtable

Описание
Определяет динамический массив. Динамические массивы при
необходимости могут увеличивать свой размер
Используется для работы с хеш-таблицами для пар "ключзначение"

Queue
SortedList
Stack

Очередь или список, построенный по принципу FIFO - первым
зашел, первым вышел
Отсортированный список пар "ключ-значение"
Стек или список, построенный по принципу LIFO - последним
зашел, первым вышел

8.3. Обобщенные коллекции
Как уже было отмечено, благодаря введению обобщений, количество кол­
лекций существенно увеличилось. Все обобщенные коллекции объявляют­
ся в пространстве имен System.Collections.Generic.
Обычно классы обобщенных коллекций являются не более чем обобщенны­
ми эквивалентами классов необобщенных коллекций. В некоторых случаях
одни и те же функции существуют параллельно в классах обобщенных и
необобщенных коллекций, хотя и под разными именами. Примером может
послужить обобщенный вариант класса HashTable, который называется
Dictionary. Аналогично, обобщенный класс List - это аналог необобщенного
класса Array List. Вообще, практически все, что вы знаете о необобщенных
коллекциях, применимо и к обобщенным коллекциям.
Обобщенные коллекции из пространства имен System.Collections.Generic
определены в таблице 8.4.
Таблица 8.4. Интерфейсы обобщенных коллекций
Интерф ейс

Описание

З де сь определены основные свойства обоб­
щенных коллекций

ICollection
IComparer

IDictionary

IEnumerable
IEnumerator
IEqualityComparer
IList

Определяет обобщенный метод Com pared
для сравнения объектов, хранящихся в кол­
лекции
Определяет словарь, то есть обобщ ен­
ную коллекцию, состоящую из пар "ключзначение"
Содержит обобщенный метод
GetEnumeratorQ, предоставляющий пере­
числитель для любого класса коллекции
Предоставляет методы, позволяющие полу­
чать содержимое коллекции по очереди
Компаратор. Сравнивает два объекта на
предмет равенства
Определяет обобщенную коллекцию, д о ­
ступ к которой осущ ествляется с помощью
индексатора

Аналогично, для работы со словарем в пространстве имен System.
Collections.Generic определена структура KeyValuePair. В
этой структуре определяются два следующих свойства:
p u b l i c ТКеу Key { g e t ; } ;
p u b l i c T V al u e V a l u e { g e t ;

};

Для создания объекта типа KeyValuePair используется кон­
структор:
p u b l i c K e y V a l u e P a i r (ТКеу k e y ,

T V al u e v a l u e )

В таблице 8.5 приведены основные классы обобщенных коллекций, опреде­
ленные в пространстве имен System.Collections.Generic.

Таблица 8.5. Основные классы обобщенных коллекций
Класс
DictionaryCTkey,

Описание
T V al u e >

Словарь. Используется для хранения
пары "ключ-значение". Функции та­
кие же, как и у необобщенного класса
HashTable

HashSet

Используется для хранения уникаль­
ных значений с использованием хэштаблицы

LinkedList

Сохраняет элементы в двунаправлен­
ном списке

List

Создает динамический массив. Ф унк­
ционал такой же, как и у необобщенно­
го класса ArrayList

Queue

Очередь. Функционал такой же, как у
необобщенного класса Queue

SortedDictionary

Используется для создания отсортиро­
ванного списка из пар "ключ-значение"

SortedList

SortedSet

Создает отсортированное множество

Stack

Стек. Функционал такой же, как у не­
обобщенного класса Stack

В пространстве имен System.Collections.Generic определены и другие клас­
сы, но они используются реже. Далее мы рассмотрим описанные ранее клас­
сы. Не все, а только лишь те, которые, на мой взгляд, являются наиболее

интересными. С остальными вы сможете познакомиться в документации по
C# на официальном сайте Microsoft.

8.4. Класс ArrayList. Динамические
массивы
В C# поддерживаются динамические массивы, то есть такие массивы, ко­
торые расширяются и сокращаются по мере необходимости. Стандартные
массивы имеют фиксированную длину, которая не может изменяться во
время выполнения программы. Другими словами, при использовании стан­
дартных массивов программист должен задать длину массива заранее. Но
количество элементов иногда неизвестно до момента выполнения програм­
мы. Например, вам нужно создать массив строк, загрузив его из файла. Вы
не знаете, сколько строк будет в файле. Именно для таких ситуаций и пред­
назначен класс ArrayList. В нем определяется массив переменной длины,
состоящий из ссылок на объекты и способный динамически увеличивать и
уменьшать свой размер.
Динамический массив ArrayList создается с первоначальным размером. В
случае превышения размера массив будет автоматически расширен. А при
удалении объектов из такого массива он автоматически сокращается. В C#
коллекции класса ArrayList широко используются. В классе ArrayList реа­
лизуются интерфейсы ICollection, IList, IEnumerable и ICloneable.
Рассмотрим конструкторы класса ArrayList:
p u b l i c A r r a y L i s t ()
p u b l i c A r r a y L i s t ( I C o l l e c t i o n c)
pub lic A rra y L ist(in t capacity)

Первый конструктор используется для создания пустой коллекции класса
ArrayList. Емкость такой коллекции равна 0. При добавлении в динамиче­
ский элемент массивов он будет автоматически расширяться.
Второй конструктор создает коллекцию типа ArrayList с количеством ини­
циализируемых элементов, которое определяется параметром с и равно
первоначальной емкости массива. Третий конструктор позволяет указать
емкость массива целым числом. Емкость коллекции типа ArrayList увели­
чивается автоматически по мере добавления в нее элементов.
Как вы уже догадались, в классе ArrayList определены собственные мето­
ды. Например, произвести двоичный поиск в коллекции можно с помощью
метода BinarySearch(). Но перед этим коллекцию желательно отсортиро­
вать методом SortQ. В таблице 8.6 приведены некоторые методы класса
ArrayList.

Таблица 8.6. Некоторые методы класса Array List
М етод

Описание

A d d R a n g e ()

Добавляет диапазон значений из одной коллекции в ко­
нец вызывающей коллекции типа ArrayList.

B i n a r y S e a r c h ()

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

С о р у Т о ()

Копирует содержимое вызывающей коллекции в м а с­
сив. М ассив должен быть одномерным и совместимы м
по типу с элементами коллекции.

F i x e d S i z e ()

Заключает коллекцию в оболочку типа ArrayList с ф икси­
рованным размером и возвращает результат. Возвра­
щает часть вызывающей коллекции типа ArrayList. М ето­
ду нужно передать индекс элемента (параметр index), с
которого начинается часть возвращаемой коллекции, и
количество элементов (параметр count). Возвращаемый
объект ссылается на те же элементы, что и вызывающий
объект.

I n d e x O f ()

Возвращает индекс первого вхождения объекта в вызы­
вающей коллекции или -1, если объект не найден.

I n s e r t R a n g e ()

Вставляет элементы коллекции в вызывающую коллек­
цию, начиная с элемента, указываемого по индексу.

R e a d o n l y ()

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

R em o v e R a ng e ()

Удаляет часть вызывающей коллекции, начиная с эле­
мента, заданного параметром index. Количество эле­
ментов задается параметром count.

S o r t ()

Сортирует вызывающую коллекцию по возрастанию.

Огромное значение в классе ArrayList имеет свойство Capacity, позволя­
ющее получать и устанавливать емкость вызывающей коллекции типа
ArrayList. Свойство определено так:
public v ir tu a l

in t Capacity

{ get;

set;

}

Свойство Capacity позволяет получать и устанавливать емкость вызываю­
щей коллекции типа ArrayList. Свойство содержит количество элементов,
которое может содержать коллекция до очередного изменения размера. По­
скольку ArrayList расширяется автоматически, задавать емкость вручную
не нужно. Но это можно сделать, если количество элементов коллекции из­
вестно заранее. Благодаря этому исключаются издержки на выделение до­
полнительной памяти.

Иногда требуется уменьшить размер коллекции. Для этого достаточно уста­
новить меньшее значение свойства Capacity. Обратите внимание: оно долж­
но быть не меньше значения свойства Count. Свойство Count определено
в интерфейсе ICollection и содержит количество объектов, хранящихся в
коллекции на данный момент.
Если вы попытаетесь установить значение свойства Capacity меньше значе­
ния свойства Count, будет сгенерировано исключение ArgumentOutOfRangeException. Чтобы получить такое количество элементов коллекции типа
ArrayList, которое содержится в ней на данный момент, нужно установить
значение свойства Capacity равным значению свойства Count. Также вы Мо­
жете использовать метод TrimToSize().
Настало время рассмотреть практический пример (листинг 8.1).
Листинг 8.1. Работа с коллекциями
u s in g System;
using S ystem .C ollection s;
namespace W orkingW ithC ollection
{
c la s s IntC ollection
{
p u b lic s t a t i c A r r a y L is t N ew C o lle ctio n (in t i)

{
Random r a n = new Random( ) ;
A r r a y L i s t a r r = new A r r a y L i s t ( ) ;
for

( i n t j = 0; j < i ; j ++)
a r r . A d d ( r a n . N e x t ( 1, 1 0 0 ) ) ;
return a r r ;

}
p ubli c s t a t i c void RemoveElement(int i , i n t j , r e f ArrayList arr)

{
arr.RemoveRange(i,

j);

}
p u b li c s t a t i c v o id AddElement(int i ,

re f A rrayList arr)

(
Random r a n = new Random( ) ;
f o r ( i n t j = 0; j < i ; j + + )
a r r . A d d ( r a n . N e x t (1, 1 0 0 ) ) ;

)
*

p u b lic s t a t i c void W rite(A rrayList arr)

{
foreach (in t a in arr)
Console.W rite( " { 0 } \ t " ,
Console.W riteLine( " " ) ;

a);

}
}
class

Pr og r am

{
s t a t i c v o i d M ai n ( )

{
/ / Соз дадим новую коллекцию ч исел длиной 4
A r r a y L i s t MyCol = I n t C o l l e c t i o n . N e w C o l l e c t i o n ( 4 ) ;
C o n s o le . OutputEncoding = E n co d in g . G etE ncodin g(866);
C o n s o l e . W r i t e L i n e ("Моя к о л л ек ц ия : " ) ;
IntCollection.W rite(M yCol);
/ / Добавим еще н е с к о л ь к о э л е м е н т о в
I n t C o l l e c t i o n . A d d E l e m e n t (4, r e f M y Co l );
C o n s o l e . W r i t e L i n e ( " Посл е д о б а в л е н и я э л е м е н т о в :
IntCollection.W rite(M yCol);

") ;

/ / Удалим п а р у э л е м е н т о в
I n t C o l l e c t i o n . R e m o v e E l e m e n t (3, 2 , r e f M y Co l ) ;
C o n s o l e . W r i t e L i n e ( " Посл е уд а л ен ия э л е м е н т о в : " ) ;
IntCollection.W rite(M yC ol);
/ / От со рт ир уе м т е п е р ь коллекцию
M yCo l . S o r t ( ) ;
C o n s o l e . W r i t e L i n e ( " По сл е с о р т и р о в к и :
IntCollection.W rite(M yCol);

");

)
}
)
Вывод этой программы будет примерно таким (примерно - потому что мы
используем случайные элементы в диапазоне от 1 до 100 для формирования
нашей коллекции, поэтому каждый запуск программы будет давать другие
результаты):
Моя к о л л е к ц и я :
97
11
26
96
После д о б а в л е н и я э л е м е н т о в :

97

11

26

96

72

33

33

94

31

33

94

97

94

31

После удаления элементов:
97

11

26

После сортировки:
11

26

31

Изначально была создана коллекция с числами 97, 11, 26 и 96. Для созда­
ния коллекции мы использовали метод NewCollection(), которому переда­
ли число 4 - количество элементов в новой коллекции. Сами же элементы
коллекции создаются генератором случайных чисел:
Random ran = new Random();
ArrayList arr = new ArrayListO;
for (int j = 0; j < i; j++)
arr.Add(ran.Next(1, 100));
return arr;

Добавление элементов осуществляется методом Add(). За один раз в кол­
лекцию можно добавить один элемент. Мы же создали метод AddElement(),
который добавляет в динамический массив указанное количество случай­
ных элементов. Мы добавили 4 элемента.
Далее методом RemoveRange() мы удаляем 2 элемента, начиная с третьего.
Элементы 96 и 72 удалены.
В заключение этого примера мы сортируем массив по возрастанию. Резуль­
тат сортировки приведен выше.

8.5. Хеш-таблица. Класс HashTable
Информация в хеш-таблице хранится с помощью механизма, называемого
хешированием. Для создания хеш-таблицы используется класс HashTable.
При хешировании для определения уникального значения, называемого
хеш-кодом, используется информационное содержимое специального клю­
ча. В результате хеш-код служит в качестве индекса, по которому в таблице
хранятся искомые данные, соответствующие заданному ключу.
Сам хеш-код недоступен программисту, а преобразование ключа в хеш-код
осуществляется автоматически. Преимущество данного способа (хеширо­
вания) в том, что оно обеспечивает постоянство времени выполнения опе­
раций поиска, извлечения и установки значений независимо от размера
массива данных.

В классе HashTable реализованы следующие интерфейсы:


ICloneable;



ICollection;



IDictionary;



IDeserializationCallback;



IEnumerable.

Конструкторы класса HashTable выглядят так:
public
public
public
public

HashtableO
Hashtable(IDictionary d)
Hashtable(int capacity)
Hashtable(int capacity, float loadFactor)

Первая форма создает объект класса HashTable по умолчанию. Во второй
форме объект типа HashTable инициализируется элементами из коллек­
ции d. Третья форма создает объект, инициализируемой с учетом емкости
коллекции, заданной параметром capacity. Четвертая форма создает объект
типа Hashtable, который инициализируется с учетом емкости capacity и ко­
эффициента заполнения loadFactor.
Параметр loadFactor может принимать значения от 0.1 до 1.0. Он определя­
ет степень заполнения хеш-таблицы до увеличения ее размера. В частности,
таблица расширяется, если количество элементов оказывается больше ем­
кости таблицы, умноженной на коэффициент заполнения. Конструкторы,
не принимающие параметр loadFactor, считают, что этот параметр равен 1.0.
В классе Hashtable определяется ряд собственных методов, помимо тех, что
уже объявлены в интерфейсах, которые в нем реализуются. Рассмотрим не­
которые часто используемые методы (табл. 8.7).
Таблица 8.7. Некоторые часто используемые методы класса Hashtable
Метод
ContainsKey()

ContainsValue()
GetEnumerator()
Synchronized()

Описание
Если в вызывающей коллекции типа HashTable сод ер ­
жится ключ, метод возвращает true, в противном случае
- false.
Если в вызывающей коллекции типа HashTable сод ер ­
жится значение, метод возвращает true, в противном
случае - false.
Возвращает для вызывающей коллекции типа Hashtable
перечислитель типа IDictionaryEnumerator
Возвращает синхронизированный вариант коллекции
типа Hashtable, которая передана как параметр

Особую роль в классе HashTable играют свойства Keys и Values, содержа­
щие ключи и значения, соответственно:
public virtual ICollection Keys { get; }
public virtual ICollection Values { get; }

Теперь давайте рассмотрим пример создания и использования Hashтаблицы (лист. 8.2).
Листинг 8.2. Создание и использование Hash-таблицы
using System;
using System.Collections;
namespace ConsoleApplicationl

{
class Program

{
static void Main()

{
// Создаем хеш-таблицу
Hashtable ht = new Hashtable();
// Добавим несколько записей
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.6. Создаем стек. Классы Stack и
Stack
Наверное, нет ни одного программиста, который не был бы знаком со сте­
ком. Стек - это контейнер, работающий по принципу LIFO (Last In First
Out), то есть последним зашел, первым вышел.
Добавление элемента в стек осуществляется методом Push(), а извлечение
последнего добавленного элемента - методом Рор().
В C# определен класс коллекции с именем Stack. Он-то и реализует стек.
Конструкторы этого класса определены так:
p u blic
p u blic
p u blic

S t a c k ()
Stack(int in itialC ap acity)
S t a c k (I C o lle c tio n col)

Первая форма создает пустой стек, вторая форма - тоже создает пустой стек,
но задает его первоначальный размер (параметр initialCapacity). Третья
форма создает стек, содержащий элементы коллекции col. Емкость этого
стека равна количеству элементов в коллекции col.
В классе Stack определены различные методы. С методами Рор() и Push()
вы уже знакомы. Но поговорим о них подробнее. Метод Push() помещает
элемент на вершину стека. А для того чтобы извлечь и удалить объект из
вершины стека, вызывается метод Рор(). Если же объект требуется только
извлечь, но не удалить из вершины стека, то вызывается метод Реек(). А
если вызвать метод Рор() или Реек(), когда вызывающий стек пуст, то сгенерируется исключение InvalidOperationException.
Кроме описанных методов класс Stack содержит свойство Count и метод
Contains(). Свойство Count возвращает количество элементов в стеке, а ме­
тод Contains() проверяет наличие элемента в стеке и возвращает true, если
элемент находится в стеке.
Класс Stack является обобщенным вариантом класса Stack. В этом
классе реализуются интерфейсы Collection, IEnumerable и IEnumerable.
Кроме того, в классе Stack непосредственно реализуются методы
С1еаг(), Contains() и СоруТо(), определенные в интерфейсе ICollection.
А методы Add() и Remove() в этом классе не поддерживаются, как, впрочем,
и свойство IsReadOnly. Коллекция класса Stack имеет динамический
характер, расширяясь по мере необходимости, чтобы вместить все элемен­
ты, которые должны в ней храниться.
В листинге 8.3 содержится пример работы со стеком.

Листинг 8.3. Пример использования класса Stack
u s in g System;
u sin g S y s t e m .C o l le c t i o n s . Generic;
namespace C o n s o l e A p p l i c a t i o n l

{
class

P ro gr a m

{
s t a t i c v o i d Main()

{
v a r M y S t a c k = new S t a c k < c h a r > ( ) ;
M y S t a c k . Push ( ' A ' ) ;
MyStack. P u sh ( ' В ' ) ;
MyStack. P u sh ( ' C' ) ;
Console.OutputEncoding = Encoding.G etEncoding(866);
C o n s o l e . W r i t e L i n e ( "Содержимое с т е к а : " ) ;
f o r e a c h ( c h a r s i n MyS t ac k)
C onsole.W rite(s);
C onsole.W riteLine("\n");
while

( M y S t a c k . C o u n t > 0)

{
Console.W riteLine(M yStack.Pop( ) ) ;

>
if

( M y S t a c k . C o u n t = = 0)
C o n s o l e . W r i t e L i n e ("Стек п у с т ! " ) ; ‘

}
}
>
Вывод программы будет таким:
Му S t a c k c o n t a i n s :
СВА
С
В
А
S t a c k i s empty

8.7. Очередь. Классы Queue и Queue
Очередь - это контейнер, работающий по принципу FIFO (Fist In First Out),
то есть первым вошел, первым вышел. Элемент, вставленный в очередь пер­
вым, первым же и читается. Примером очереди в программировании может

послужить любая очередь в реальном мире. Если вы пришли первым и за­
няли очередь, то первым и будете обслужены.
Очередь реализуется с помощью классов Queue из пространства имен
System.Collections и Queue из пространства имен System.Collections.
Generic.
Конструкторы класса Queue выглядят так:
public
public
public
public

Queue ()
Queue ( i n t c a p a c i t y )
Queue ( i n t c a p a c i t y , f l o a t g r o w F a c t o r )
Queue ( I C o l l e c t i o n c o l )

Как и в случае со стеком, первая форма создает пустую очередь, вторая тоже пустую, но задает ее первоначальный размер. Третья форма позволяет
указать начальную емкость очереди и фактор роста (допустимые значения
от 1.0 до 10.0). Четвертая форма создает очередь из элементов коллекции
col. Все конструкторы, не позволяющие задать параметр growFactor, счита­
ют, что фактор роста равен 2.0.
Класс Queue содержит такие конструкторы:
p u b l i c Q u e u e ()
p u b lic Queue(int ca p a city )
p u b l i c Queue(IEnumerable c o l l e c t i o n )

Первая форма создает пустую очередь, емкость очереди выбирается по
умолчанию. Вторая форма позволяет задать емкость создаваемой очереди.
Третья форма создает очередь, содержащую элементы заданной коллекции
collection.
Члены класса Queue представлены в таблице 8.8.
Таблица 8.8. Члены класса Queue
Член к л а с с а
Count
Enque ue ()
D e q u e u e ()
P e e k ()
T r i m E x c e s s ()

Описание
Свойство Count возвращает количество элементов очере­
ди
Метод добавляет элемент в конец очереди
Читает и удаляет элемент из головы очереди. Если на м о ­
мент вызова метода очередь пуста, генерируется исклю­
чение InvalidOperationException.
Читает элемент из головы очереди, но не удаляет его
Изменяет емкость очереди. М етод Dequeue() удаляет эле­
мент из очереди, но не изменяет ее емкости. TrimExcess()
позволяет избавиться от пустых элементов в начале оче­
реди.

Пример работы с очередью приведен в листинге 8.4.
Листинг 8.4. Работаем с очередью
u s in g System;
using System .C ollections.G eneric;
namespace C o n s o l e A p p l i c a t i o n l

{
class

P rog ra m

{
s t a t i c v o i d M ai n ( )

{

Q u e u e < i n t > MyQueue = new Q u e u e < i n t > ( ) ;
Random r a n = new Random( ) ;
for

( i n t i = 0; i < 2 0 ; i + + )
M y Q u e u e . E n g u e u e ( r a n . N e x t (1,

100) ) ;

C o n s o le . OutputEncoding = E n co d in g .G e tE n co d in g (866);
C o n s o l e . W r i t e L i n e ( "Моя о ч е р е д ь : " ) ;
f o r e a c h ( i n t i i n MyQueue)
C onsole.W rite("(0) ", i ) ;
C onsole. ReadLine();

)

8.8. Связный список. Класс LinkedList
Рассмотрим рис. 8.2, на котором изображен типичный двухсвязный спи­
сок - структура, часто использующаяся в программировании. У каждого
элемента списка есть два указателя - на следующий (Next) и предыдущий
элемент (Prev). Если нет следующего (или предыдущего) элемента, то
указатель содержит значение null. Кроме указателей каждый элемент со­
держит значение (value).

Рис. 8.2. Связный список

Ранее, в языке Си, нужно было создавать структуру связного списка вруч­
ную. Сейчас же достаточно использовать уже готовый класс LinkedList и
его методы.
Преимущество связных списков в том, что операция вставки элемента в се­
редину выполняется очень быстро. При этом только ссылки Next (следую­
щий) предыдущего элемента и Previous (предыдущий) следующего элемен­
та должны быть изменены так, чтобы указывать на вставляемый элемент.
В классе List при вставке нового элемента все последующие должны
быть сдвинуты.
К недостаткам связных списков можно отнести довольно медленный поиск
элементов, находящихся в центре списка.
Класс LinkedList содержит члены First (используется для доступа к
первому элементу списка), Last (доступ к последнему элементу), а также
методы для работы с элементами. В классе LinkedList реализуются ин­
терфейсы ICollection, ICollection, IEnumerable, IEnumerable, ISerializable и IDeserializationCallback. В двух последних интерфейсах поддер­
живается сериализация списка. В классе LinkedList определяются два
конструктора:
p u b l i c L i n k e d L i s t ()
p u b lic LinkedList(IEnumerable c o lle c tio n )

Первый конструктор создает пустой связный список, второй - список, кото­
рый инициализируется элементами из заданной коллекции.
Методы класса LinkedList< Т > представлены в таблице 8.9.
Таблица 8.9. Методы класса LinkedList
Метод

*

Описание

A ddFirst (),
A d d L a s t ()

Добавляют элемент, соответственно, в начало и конец списка

A d d A f t e r ()

Добавляет в список узел непосредственно после
указанного узла. Указываемый узел не должен быть
пустым (null). Метод возвращает ссылку на узел, с о ­
держащий значение.

A d d B e f o r e ()

Аналогичен AddAfter(), но добавляет узел до указан­
ного узла.

F i n d ()

Возвращает ссылку на первый узел в списке, имею ­
щий передаваемое значение. Если искомое значе­
ние отсутствует в списке, то возвращается null.

R e m ov e ()

Используется для удаления из списка первого узла,
который содержит переданное методу значение
value. Возвращает true, если узел со значением был
обнаружен и удален, в противном случае возвраща­
ется значение false.

Как обычно, настало время для примера. В листинге 8.5 приводится пример
создания связного списка и вывода его в двух направлениях - в прямом и
обратном. Обратите внимание, как построены циклы for. В качестве началь­
ного значения при прямом обходе мы используем First, а затем присваива­
ем узлу значение Next (идем вперед). При обратном обходе мы начинаем с
Last и узлу при каждой итерации присваиваем значение Previous, то есть
идем назад.
Листинг 8.5. Пример п р я м о го

и

обратного обхода связного списка

u sin g System;
u sin g System . C o l l e c t i o n s . Generic;
namespace C o n s o l e A p p l i c a t i o n l

{
class

P ro gr a m

{
s t a t i c v o i d Main()

{
/ / Соз дадим св я зн ый с п и со к
L i n k e d L i s t < s t r i n g > 1 = new L i n k e d L i s t < s t r i n g > ( ) ;
/ / Добавим н е с к о л ь к о э л е м е н т о в
l.A d d F irst("A p p le");
1 .A ddFirst("B anana");
l.A d d F irst("P ear");
1 .A ddFirst("O range");
1 .A ddFirst("Peach");

/ / Элементы в прямом п о ря дк е
L i n k e d L i s t N o d e < s t r i n g > node;
C o n s o le .W r i t e L i n e ( " D i r e c t Order: " ) ;
f o r (node = 1 . F i r s t ; node ! = n u l l ; node = n o d e . N e x t )
Console.W rite(node.V alue + ” \ t " ) ;
Console.W riteLine ();
/ / Элементы в о бр а т н о м п о ря дк е
Console.W riteLine("R everse order: " ) ;
f o r (node = l . L a s t ; node ! = n u l l ; node = n o d e . P r e v i o u s )

Console.W rite(node.V alue + " \ t " ) ;
Console. ReadLine();

}
}
}

Рис. 8.3. Пример прямого и обратного обхода связного списка

8.9. Сортированный список. Класс
SortedList
Еще один вариант списка - сортированный по ключу список. Для его соз­
дания можно воспользоваться классом SortedList. Данный
класс сортирует элементы на основе значения ключа. При этом вы можете
использовать любые типы значения и ключа.
В классе SortedList реализуются следующие интерфейсы:


IDictionary;



IDictionaryCTKey,



IC ollection;



ICollection.

Размер сортированного списка увеличивается автоматически - по
мере необходимости. Класс SortedList похож на класс
SortedDictionary, но он более эффективно расходует па­
мять.

Рассмотрим конструкторы класса SortedList:
public
public
public
public

SortedListt)
S o r t e d L i s t ( I D i c t i o n a r y < T K e y , TValue> d i c t i o n a r y )
S o r te d L is t (in t capacity)
S o r te d L ist (I C o m p a r e r < T K > comparer)

Первая форма создает пустой список. Вторая - создает отсортированный
список, элементы берутся из словаря dictionary. Третья форма с помощью
параметра capacity задает емкость отсортированного списка. Последняя
форма позволяет указать способ сравнения объектов, которые находятся в
списке (для этого используется параметр comparer).
Методы и прочие члены класса SortedList приведены в та­
блице 8.10.

Таблица 8.10. Методы класса SortedList
Член класса

Add ()

C o n t a i n s K e y ()

C o n t a i n s V a l u e ()
G e t E n u m e r a t o r ()
I n d e x O f K e y () ,
I n d e x O f V a l u e ()

R e m ov e ()

T r i m E x c e s s ()
Capacity
Co mp ar er
Ke ys
Values

Описание

Добавляет в список
пару "ключзначение". Если ключ уже находится в
списке, то генерируется исключение
ArgumentException
Возвращает значение true, если вызыва­
ющий список содержит объект key в ка­
честве ключа, в противном случае - false
Возвращает значение true, если вызыва­
ющий список содержит значение value; в
противном случае - false
Возвращает перечислитель для вызыва­
ющего словаря
Возвращает индекс ключа или первого
вхождения значения в вызывающем сп и ­
ске. Если ключ или значение не найдены,
возвращается значение -1
Удаляет из списка пару "ключ-значение"
по указанному ключу key. Если удаление
удачно, возвращает true, если нет - false
Сокращ ает избыточную емкость вызыва­
ющей коллекции в виде отсортированно­
го списка.
Получает или устанавливает емкость
списка
Задает метод сравнения для вызываю­
щего списка
Коллекция ключей
Коллекция значений

Пример программы, работающей с отсортированным списком, приведен в
листинге 8.6.
Листинг 8.6. Работа с отсортированным списком
u s i n g System;
using S ystem .C ollection s.G en eric;
namespace C o n s o l e A p p l i c a t i o n l

{
class

P rog ra m

{
s t a t i c v o i d M ai n ( )

<
/ / Соз дадим коллекцию с о р т и р о в а н н о г о с п и с ка
SortedList C a r l n f o = new

/ / Доб ав ле ние э л е м е н т о в
C a r I n fо . Add("Audi", " 2 0 1 5 ");
C arlnfo.A dd("Toyota", "2016");
CarInfo.Add("BMW", " 2 0 1 5 " ) ;
C arInfo.A dd("R enault", "2014");
C arlnfo.A d d C 'L exu s", "2016") ;
/ / Коллекция ключей
I C o l l e c t i o n < s t r i n g > keys = C a r l n f o .K e y s ;
Console.OutputEncoding = Encoding.G etEncoding(866);
/ / Тепе рь и с п о л ь з у е м ключи для получения з н ач ен ий
f o r e a c h ( s t r i n g s in keys)
C o n s o l e . W r i t e L i n e ("Марка: ( 0 ) , Год: { 1 } " , s , C a r l n f o [ s ] ) ;
C on sole. ReadLine();

}
}
}

8.10. Словарь. Класс DictionaryCTKey,
TValue>
Словарь (dictionary) - это сложная структура данных, обеспечивающая до­
ступ к элементам по ключу. Основная особенность словаря - быстрый поиск
элемента по ключу. Также вы можете быстро добавлять и удалять элементы,

подобно тому, как вы это делаете в списке List, но гораздо эффективнее,
поскольку нет необходимости смещения последующих элементов впамяти.
Тип, используемый в качестве ключа словаря, должен переопределять ме­
тод GetHashCode() класса Object. Всякий раз, когда класс словаря должен
найти местоположение элемента, он вызывает метод GetHashCode().
К методу GetHashCode() есть следующие требования:


Не должен генерировать исключений.



Должен выполняться быстро, не требуя значительных вычислитель­
ных затрат.



Разные объекты могут возвращать одно и то же значение.



Один и тот же объект должен возвращать всегда одно и то же зна­
чение.



Должен использовать как минимум одно поле экземпляра.



Хеш-код не должен изменяться на протяжении времени существо­
вания объекта.

Помимо реализации GetHashCode() тип ключа также должен реализовы­
вать метод IEquatable.Equals() либо переопределять метод Equals()
класса Object. Поскольку разные объекты ключа могут возвращать один и
тот же хеш-код, метод Equals() используется при сравнении ключей слова­
ря.
Конструкторы класса Dictionary выглядят так:
p u b l i c D i c t i o n a r y ()
p u b li c D i c t i o n a r y (IDictionary d i c t i o n a r y )

Первый конструктор создает пустой словарь, его емкость выбирается по
умолчанию. Второй конструктор создает словарь с указанным количеством
элементов dictionary. Третий конструктор позволяет указать емкость сло­
варя (параметр capacity).
В классе Dictionary реализуются следующие интерфейсы:


IDictionary;



IDictionary ;



IEnumerable ( ) ;

C o n s o l e . W r i t e L i n e ( " В в е д и т е м ар ку машины:

\n");

strin g s;
for

( i n t j = 0;

j < i;

j++)

{
C o n s o l e . W r i t e ( "Марка{0}
s = Console. ReadLine();
die.A d d (j, s ) ;

—> ",j);

}
return d ie;

}
}
class

P rog ra m

{
s t a t i c v o i d M ai n ( )

{
Console.OutputEncoding = Encoding.G etEncoding(866);
C o n s o l e . W r i t e ( " С к о л ь к о машин д о б а в и т ь ? " ) ;
try
{
in t i = i n t . P arse(C onsole.R eadLine());
D ictionary die = Carlnfo.M yD ic(i);
/ / Получаем ключи
I C o l l e c t i o n < i n t > keys = d i e . K e y s ;
C on sole.W riteLin e("Словарь: " ) ;
f o r e a c h ( i n t j in keys)
C o n s o l e . W r i t e L i n e ( " I D - > {0} Марка - > ( l ) " , j ,

d ic[j]);

}
catch

(FormatException)

{
C o n s o l e . W r i t e L i n e ( "Ошибка ! Исключение ф о р м а т а " ) ;

}

C o n so le . R ead L in e();

}
}
}

8.11. Сортированный словарь: класс
SortedDictionary
Отсортированный вариант словаря представляет собой дерево бинарного
поиска, в котором все элементы отсортированы по ключу. Класс называется
Sorted Dictionary .
Ключ, точнее, его тип, должен реализовать интерфейс IComparable.
Если тип ключа не относится к сортируемым, вы можете реализовать
IComparer и указать его в качестве аргумента конструктора сорти­
рованного словаря.
Классы SortedDictionary и SortedList до­
вольно похожи. Но SortedList реализован в виде списка,
основанного на массиве, a SortedDictionary реализован как
словарь. Именно поэтому эти классы обладают разными характеристика­
ми, а именно SortedList использует меньше памяти, чем
SortedDictionary. Также SortedDictionary
быстрее вставляет и удаляет элементы.
В классе SortedDictionary предоставляются также следую­
щие конструкторы:
p u b l i c S o r t e d D i c t i o n a r y ()
p u b l i c S o r t e d D i c t i o n a r y ( I D i c t i o n a r y < T K e y , T V a lu e > d i c t i o n a r y )
p u b l i c S o r te d D ic tio n a ry (IC o m p a r e r < T K e y > com parer)
p u b l i c S o r t e d D i c t i o n a r y ( I D i c t i o n a r y < T K e y , T V a lu e > d i c t i o n a r y ,
IC o m p a rer< T K ey > c o m p a r e r )

Первый конструктор создает пустой словарь, второй - словарь с указанным
количеством элементов dictionary. Третий конструктор позволяет ука­
зывать с помощью параметра comparer типа IComparer способ сравнения,
используемый для сортировки. Четвертая форма конструктора позволяет
инициализировать словарь, помимо указания способа сравнения.
В классе SortedDictionary реализуются следующие интер­
фейсы:


ID ictio n ary ;



ID ictio n a ry < T K e y ,

T V a lu e > ;



IC o lle c tio n ;



IC o lle ctio n < K e y V alu e P air< T K e y ,



IE n um erable;



I E n u m e r a b l e < K e y V a l u e P a ir < T K e y ,

T V alue>>;

T V alue>>.

Методы отсортированного словаря аналогичны методам обычного, а
именно поддерживаются методы Add(), ContainsKey(), ContainsValue(),
Remove().
В классе SortedDictionary реализуется приведенный ниже
индексатор, определенный в интерфейсе IDictionary:
p u b l i c T V alu e t h i s [ T K e y key]

{ get;

set;

}

Пример использования отсортированного словаря приведен в листинге 8.8.
Листинг 8.8. Использование отсортированного словаря
u s in g System ;
u sin g S y ste m .C o lle c tio n s.G e n e r ic ;
nam espace C o n s o le A p p lic a t io n l

{
c l a s s C arln fo

{
p u b l i c s t a t i c S o r t e d D i c t i o n a r y < s t r i n g , s t r i n g > M yD ic(in t i)

{
S o rte d D ictio n a ry < strin g ,
ic t io n a r y < s t r in g ,s t r in g > ();
s tr in g s2,
fo r

s t r i n g > d i e = new S o r t e d D

si;

(in t j = 0;

j < i;

j+ + )

{
C o n s o l e . W r i t e ("Ключ: " ) ;
s i = C o n so le .R e a d L in e O ;
C o n s o l e . W r i t e L in e ( "М арка: " ) ;
Console.Write("Марка{0( --> ",j);
s2 = Console.ReadLineO;
die.Add (si, s2);

}
retu rn d ie ;

}
}
c l a s s P rog ram

{
s t a t i c v o i d M a in ()
{
C o n so le .O u tp u tE n c o d in g = E n c o d in g . G e tE n c o d in g (8 6 6 );
C o n s o l e . W r i t e (" С к о л ь к о машин д о б а в и т ь ? " ) ;
try
{
in t i = i n t . P a rse (C o n so le .R e a d L in e ( ) ) ;
S o rte d D ictio n a ry < strin g , s t r i n g > d = C arlnfo.M yD ic(i);
/ / Получить коллекцию ключей
I C o l le c t io n < s t r i n g > keys = d .K e y s;
C o n s o l e . W r i t e L i n e ("О тсортированный с л о в а р ь : " ) ;
fo r e a c h ( s t r i n g j in keys)
Console.WriteLine("ID -> (0) Марка -> { 1 } " , j , d [ j ] ) ;

)
catch

(F o rm atE xcep tion )

{
C o n s o l e . W r i t e L i n e ("О ш и б к а !" ) ;

}
C o n so le .R e a d L in e ();

}
}
}

8.12. Множества: классы HashSet и
SortedSet
Настало время поговорить о множествах (set). В составе .NET имеются
два класса - HashSet и SortedSet. Оба они реализуют интерфейс
ISet. Класс H ashSet содержит неупорядоченный список различаю­
щихся элементов, а в SortedSet элементы упорядочены. То есть первый
класс - это просто множество, а второй - отсортированное множество.
Интерфейс ISet предоставляет методы, используемые для выполнения
основных операций над множеством.
В классе HashSet определены следующие конструкторы:
p u b l i c H a s h S e t ()
p u b li c H ashSet(IE n u m erable c o l l e c t i o n )
p u b l i c H ash S et (IE q u a lity C o m p a re com parer)
p u b li c H ashSet(IE n u m erable c o l l e c t i o n ,
com parer)

IE q u ality C o m p are

Первая форма создает пустое множество, вторая форма - создает множе­
ство, состоящее из элементов коллекции collection. Третий конструктор
позволяет указывать способ сравнения, указав параметр comparer. По­
следняя форма создает множество, состоящее из элементов заданной кол­
лекции collection, и использует метод сравнения comparer.
Конструкторы класса SortedSet такие же:
public SortedSetO
p u b l i c S o r t e d S e t (IEnumerable c o l l e c t i o n )
p u b l i c S o r t e d S e t ( I C o m p a r e r comparer)
p u b lic SortedSet(IEnum erable c o ll e c t io n ,
comparer)

IComparer

В SortedSet, помимо прочих свойств, определены дополнительные
свойства:
p u b l i c I C om p a r e r < T > Co mp ar e r { g e t ;
p u b l i c T Max { g e t ; )
p u b l i c T Min { g e t ; )

)

Пример использования коллекции:
S o r t e d S e t < i n t > s s = new S o r t e d S e t < i n t > ( ) ;
s s . A d d (7) ;
s s .Add(6);
s s .Add(9);
s s .Add(1);
foreach

(int j

in ss)
C onsole.W rite(j

+ " ");

Для объединения множеств используется метод UnionWith(), в качестве
параметра ему нужно передать имя второго множества. Для вычитания
множеств используется метод ExceptWith(). Оставить в двух множествах
только уникальные элементы (чтобы не было пересечения этих множеств)
можно методом SymmetricExceptWith(). Рассмотрим, как работают эти ме­
тоды:
/ / с о з д а д и м в т о р о е множество
S o r t e d S e t < i n t > s b = new S o r t e d S e t < i n t > ( ) ;
sb.A d d (7);
sb.A d d (2);
sb.A d d (3);
sb.A d d (1);
/ / П ер есе че ние

ss.E xceptsW ith (sb);
fo reach (in t j in ss)
Console.W rite(j

+ " ");

/ / Объединение
ss.U nionW ith(sb);
foreach (in t j in ss)
Console.W rite(j

+ " ");

/ / Исключаем одинаковые элементы
s s . Sym metricExceptW ith(sb);
foreach (in t j in ss)
Console.W rite(j + " " ) ;

8.13. Реализация интерфейса
IComparable
Представим, что вы создали некий собственный класс и создали кол­
лекцию, содержащую объекты этого класса. Что, если вам понадобилось
отсортировать эту коллекцию? Для сортировки коллекции, заполнен­
ной объектами пользовательского типа, нужно реализовать интерфейс
IComparable, то есть указать компилятору, как сравнивать объекты.
Если требуется отсортировать объекты, хранящиеся в необобщенной кол­
лекции, то для этой цели придется реализовать необобщенный вариант ин­
терфейса IComparable. В этом варианте данного интерфейса определяется
только один метод, СотрагеТо(), который определяет порядок выполне­
ния самого сравнения. Ниже приведена общая форма объявления метода
СотрагеТо():
i n t СошрагеТо( o b j e c t o b j )

Данный метод должен возвращать положительное значение, если значе­
ние вызывающего объекта больше, чем у объекта, с которым производит­
ся сравнение. В противном случае, если значение вызывающего объекта
меньше - возвращается отрицательное значение. Если объекты равны, то
возвращается 0. Представим, что в классе есть поле price, по которому мы
и будем сравнивать объекты:
p u b l i c i n t СошрагеТо(SomeClass o b j )

{
if

(th is.p ric e > obj.price)
r e t u r n 1;
i f (th is.p ric e < obj.price)
return -1;
else
t

r e t u r n 0;

}
Если нужно отсортировать объекты, хранящиеся в обобщенной коллекции,
то для этой цели придется реализовать обобщенный вариант интерфейса
IComparable. В этом варианте интерфейса IComparable определяется
приведенная ниже обобщенная форма метода CompareTo():
i n t C om pareTo(T o t h e r )

8.14. Перечислители
К элементам коллекции иногда приходится обращаться циклически - ра­
нее приводились примеры цикла foreach как один из способов циклически
обратиться ко всем элементам коллекции. Второй способ - использование
перечислителя. Перечислитель - это объект, реализующий необобщенный
интерфейс IEnumerator или обобщенный IEnumerator.
В интерфейсе IEnumerator определяется одно свойство, Current. Такое же
свойство есть и в обобщенной версии интерфейса. Свойства Current опре­
деляются так:
o b je c t C urrent
o b je c t C urrent

{ get;
{ get;

}
}

В обеих формах свойства Current получается текущий перечисляемый эле­
мент коллекции. Свойство Current доступно только для чтения.
Доступные методы (как в необобщенной, так и в обобщенной версиях):


MoveNext() - смещает текущее положение перечислителя к следу­
ющему элементу коллекции. Возвращает true, если следующий эле­
мент коллекции доступен, или false, если был достигнут конец.



Reset() - после вызова метода перечислитель перемещается в начало
коллекции.

Прежде чем получить доступ к коллекции с помощью перечислителя, не­
обходимо получить его. В каждом классе коллекции для этой цели предо­
ставляется метод GetEnumerator(), возвращающий перечислитель в начало
коллекции. Используя этот перечислитель, можно получить доступ к любо­
му элементу коллекции по очереди.
Рассмотрим, как использовать перечислитель:
L i s t < i n t > M y L is t = new L i s t < i n t > ( ) ;
Random r a n = new R andom ( ) ;
*

/ / З апол н яем с п и с о к случайными зн ач ен иям и о т 1 до 100
f o r ( in t i = 0; i < 20; i++)
M y L i s t . A d d ( r a n . N e x t (1 , 1 0 0 ) ) ;
/ / И сп о л ьзуе м п е р е ч и с л и т е л ь дл я вы во да э л е м е н т о в с п и ск а
I E n u m e r a t o r < i n t > enum = M y L i s t . G e t E n u m e r a t o r ( ) ;
w h i l e (e n u m .M o v eN e x t( ) )
C o n so le .W rite (n u m .C u rre n t + " " ) ;

Если нужно вывести список повторно, нужно сбросить перечислитель ме­
тодом Reset и снова вывести список циклом while:
e n u m .R e se t( ) ;

8.15. Реализация интерфейсов
lEnumerable и lEnumerator
Представим, что вам нужно создать класс, объекты которого будут переби­
раться в цикле foreach, в классе нужно реализовать интерфейсы lEnumerator
и lEnumerable. То есть для того чтобы обратиться к объекту определяемого
пользователем класса в цикле foreach, необходимо реализовать интерфей­
сы lEnumerator и lEnumerable в их обобщенной или необобщенной форме.
Реализовать данные интерфейсы довольно просто, что и показано в листин­
ге 8.9.
Листинг 8.9. Реализация интерфейсов lEnumerator и lEnumerable
u s in g System ;
u sin g S y ste m .C o lle c tio n s;
nam espace C o n s o le A p p lic a t io n l

{
c l a s s M yB ytes : l E n u m e r a b l e ,

lE n u m erato r

{
b y t e [ ] b y t e s = { 1,
b y te in d ex = - 1 ;

3,

5,

7 };

/ / И нтерфейс l E n u m e r a b l e
p u b l i c l E n u m e r a t o r G e t E n u m e r a t o r ()

(
retu rn t h i s ;

}
/ / Интерфейс l E n u m e r a t o r
p u b l i c b o o l M oveN extO
*

{
if

( i n d e x == b y t e s . L e n g t h - 1)

{
R e s e t();
return f a l s e ;

}
index++;
return tru e;

}
/ / Метод, сбрасывающий п е р е ч и с л и т е л ь
p u b l i c v o i d R e s e t ()

{
index = -1;

}
p u b lic o b je c t Current

{
get

<
r e tu rn b y t e s [i n d e x ] ;

}
}
}
c l a s s Pr og r am
{
s t a t i c v o i d Main()

{
MyBytes mb = new M y B y t e s O ;
f o r e a c h ( i n t j i n mb)
C onsole.W rite(j+"

");

}
}
)

8.16. Итераторы. Ключевое слово yield
В прошлом разделе мы реализовали интерфейсы IEnumerator и IEnumerable.
Как было показано, это совсем несложно. Но еще проще воспользоваться
итератором. Итератор - это метод, оператор или аксессор, который по оче­
реди возвращает члены последовательности объектов - с ее начала до конца.
После того как вы реализуете итератор, вы сможете обращаться к объектам
определяемого пользователем класса в цикле foreach.
Создать итератор в C# можно с помощью ключевого слова yield. Данное
ключевое слово является контекстным, то есть оно имеет специальное зна-

чение только в блоке итератора. Вне этого блока оно может быть исполь­
зовано аналогично любому другому идентификатору. Следует особо под­
черкнуть, что итератор не обязательно должен опираться на массив или
коллекцию другого типа. Он должен просто возвращать следующий эле­
мент из совокупности элементов.
Синтаксис итератора следующий:
p u b l i c IE n u m erable и м я_и тер ато р а(сп и со к _п ар ам етр ов)

И

{

...

y ie ld retu rn o b j;

>
Здесь имя итератора - конкретное имя метода, список параметров - пара­
метры, передаваемые методу итератора, obj - следующий объект, возвра­
щаемый итератором. Как только именованный итератор будет создан, его
можно будет использовать везде, где он нужен, например, в цикле foreach.
Пример итератора:
c l a s s M y C la s s {
i n t l i m i t = 0;
p u b lic M y C lass(in t lim it)

{ th is .lim it = lim it;

)

p u b l i c IE n u m e ra b le < in t> C o u n tF ro m (in t s t a r t )

{
fo r

(in t i = s t a r t ;
y ie ld retu rn i ;

i .

Обычно используется ключевое слово using, чтобы избежать лишнего вво­
да с клавиатуры - вы только подумайте, если отказаться от using, то везде
придется “таскать” префикс “MyPets.”. В CIL-коде всегда используются
уточненные имена типов, но программисту гораздо удобнее использовать
ключевое слово using.
Тем не менее использование уточненных имен может понадобиться, а имен­
но при возникновении конфликтов имен - когда в двух разных простран­
ствах определены типы с одинаковыми именами. Например, у вас может
быть два пространства имен с одинаковыми типами внутри - MyPets и
JohnPets (животные Джона). Когда вы вызываете конструктор Cat(), нуж­
но уточнить, какое пространство имен нужно использовать:
MyPets.Cat Bagira = new MyPets.Cat ();
JohnPets.Dog Bobby = new JohnPets.Dog();

Есть и еще один способ разрешения конфликтов имен - использование
псевдонимов. Определить псевдоним можно так:

u s i n g = с П р о с т р а н с т в о . Тип>;

Пример:
u s i n g Jo hn Dog = J o h n P e t s . Dog;
J ohnDog Bobby = new J o h n D o g ( ) ;
Внимание! He злоупотребляйте использованием псевдонимов! Злоупотребление

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

9.3. Вложенные пространства имен.
Пространство по умолчанию
Язык C# позволяет определять пространства имен внутри других про­
странств, то есть создавать вложенные пространства. Вспомните, внутри
пространства System находятся пространства имен Text (операции с тек­
стом), IO (ввод/вывод), Threading (создание многопоточных приложений).
Определить вложенное пространство имен очень просто - нужно опреде­
лить еще один блок namespace:
n a m e s p a c e MyPets {
namespace C a t s {
namespace S iam ese {
p u b l i c c l a s s Cat
{
p u b l i c s t r i n g Name;
p u b l i c b y t e A ge ;
p u b l i c b y t e Weight;

}
}
}
}
Создадим объект типа Cat:
M y P e t s . C a t s . S i a m e s e . C a t B a g i r a = new M y P e t s . C a t s . S i a m e s e . C a t ( ) ;

При использовании using создание объекта будет компактнее:
u sin g MyPets. C a t s . Siamese;
C a t B a g i r a = new C a t ( ) ;

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

кода командой Project *■ Add New Item все соответствующие типы автома­
тически помещаются в это пространство имен.
Если вы после создания проекта желаете изменить название пространства
имен, то откройте окно свойств проекта (Project ► Properties) и на
вкладке Application измените значение поля Default namespace (рис. 9.1).

9.4. Сборки .NET
9.4.1 .Зачем нужны сборки?

Первым делом нужно разобраться, зачем нужны сборки .NET и что это во­
обще такое. Сборка - это двоичный файл, обслуживаемый CLR (Common
Language Runtime). Несмотря на то, что у сборок такие же расширения, как
у обычных Windows'-приложений (.exe, .dll), внутри они устроены иначе.
Прежде чем двигаться дальше, давайте рассмотрим преимущества, предла­
гаемые сборками:


Сборки повышают удобство повторного использования кода



Сборки являются единицами, поддерживающими версии



Сборки определяют границы типов



Сборки можно настраивать

Теперь давайте рассмотрим эти основные преимущества подробнее и нач­
нем с самого важного - с первого. Всю эту книгу мы создавали простые кон­
сольные приложения и читателю могло показаться, что это приложение
построено по принципу "все свое ношу с собой". Однако попробуйте запу­
стить одно из разрабатываемых приложений на компьютере, где не установ­
лена платформа .NET (если, конечно, такой найдете) - оно не запустится,
поскольку на самом деле приложение обращается к различным DLL. Про­
стые консольные приложения будут требовать наличия mscorlib.dll. Более
сложные графические - System.Windows.Forms.dll.
Библиотека кода представляет собой файл с расширением .dll (хотя это не
всегда так, но как правило), в котором содержатся типы, которые можно
использовать во внешних приложениях. Данные типы можно использовать
независимым от языка образом. То есть кто-то может использовать Visual
Basic для создания библиотеки кода, которую вы будете использовать в сво­
ем С#-приложении (и наоборот!).
Важно понять, что вы можете разбить монолитный код вашего исполняе­
мого файла на несколько сборок, которые вы можете использовать в других
своих проектах, или кто-то еще может использовать их в собственных про­
ектах. При этом не важно, какой язык использует другой программист.
Теперь переходим ко второму преимуществу. Сборкам среда .NET при­
сваивает версии в формате .... По умолчанию используется номер 1.0.0.0,
если вы не измените его. Изменить номер сборки можно в свойствах про­
екта. Для этого на вкладке Application нужно нажать кнопку Assembly
Information и указать номер версии (рис. 9.2).

Сборки определяют границы типов. Мы только что говорили об уточнен­
ных именах. Такие имена получаются за счет добавления префикса с назва­
нием пространства имен, к которому относится тип. Сборка, содержащая
тип, определяет его идентичность. Так, сборки с разными именами, напри­
мер, MyPets.dll и JohnPets.dll, в которых определены классы Cats, будут
выглядеть совершенно разными типами - MyPets.Cats и JohnPets.Cats. Это
еще одно преимущество сборок.
Сборки можно настраивать. Сборки могут быть приватными (private) и со­
вместно используемыми (shared). Приватные сборки находятся в том же
каталоге, что и ваше приложение, которое их использует.
Совместно используемые сборки представляют собой библиотеки, пред­
назначенные для использования в разных приложениях на одной маши­
не, поэтому они помещаются в каталог, который называется GAC (Global
Assembly Cache) или глобальный кэш сборок.
Независимо от типа развертывания (private или shared), для сборок можно
создавать специальные конфигурационные файлы в формате XML. Цель
этих файлов - указать CLR-среде, какую версию той или иной сборки за­
гружать для определенного приложения, где искать сборки и т.д.
9.4.2. Формат сборок

Любая сборка .NET содержит в себе следующие элементы:


Заголовок Windows;



Заголовок CLR;



C IL-код;



Метаданные типов;



Манифест сборки;



Дополнительные встроенные ресурсы, например, значки.

Вместе с Visual Studio поставляется утилита dumpbin.exe, позволяющая
просмотреть те или иные заголовки. Так, параметр /headers позволяет про­
смотреть заголовок Windows:
dumpbin / h e a d e r s L i b r a r y . d l l

Просмотреть CLR-заголовок можно с помощью параметра clrheader:
dumpbin / c l r h e a d e r L i b r a r y . d l l

Стоит отметить, что заголовки генерируются автоматически компилято­
ром, и программисту в 99.99% случаев не нужно беспокоиться о тонкостях
деталей заголовков. Главное понимать, что находится внутри сборки.
В основе любой сборки находится C IL-код, который представляет собой
промежуточный код, не зависящий ни от платформы, ни от языка. Во вре­
мя выполнения внутренний CIL-код компилируется в инструкции, соот­
ветствующие определенной архитектуре (процессору) и операционной си­
стеме. Благодаря такому подходу сборки .NET могут выполняться в рамках
разных архитектур, устройств и операционных систем.
В любой сборке содержатся метаданные, описывающие формат находящих­
ся внутри нее типов, а также внешних типов, которые она использует. Кро­
ме того, в любой сборке также находится связанный с ней манифест (мета­
данные сборки). В нем описаны каждый входящий в состав сборки модуль,
версия сборки, а также любые внешние сборки, на которые ссылается теку­
щая сборка. Манифест также применяется во время определения места, на
которое указывают представляющие внешние ссылки.
Наконец, в любой сборке может содержаться любое количество дополни­
тельных ресурсов, например, значков приложения, звуковых файлов и т.д.
Платформа .NET также поддерживает создание так называемых подчинен­
ных сборок, которые содержат только локализированные ресурсы (напри­
мер, звуковые файлы на болгарском, английском, французском языках).
Построение таких сборок выходит за рамки этой книги.
9.4.3. Однофайловые и многофайловые сборки

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

ют высокоэффективный способ выполнения загрузки содержимого. Пред­
ставим, что есть машина, которая ссылается на удаленную многофайловую
сборку, состоящую из трех модулей, главный из которых установлен на кли­
енте. Если клиенту будет нужен какой-то тип из второстепенного удален­
ного модуля *.netmodule, CLR-среда по его требованию немедленно загру­
зит на локальную машину (в каталог, который называется кэшем загрузки
или download cache) только соответствующий двоичный файл. Если размер
каждого модуля составляет 10 Мб, то преимущество сразу будет заметно нужно будет загрузить один модуль размером 10 Мб вместо загрузки одно­
го большого модуля размером 30 Мб.
Это было первое существенное преимущество многофайловых сборок. Вто­
рое - то, что разные модули могут быть написаны на разных языках. Напри­
мер, вам удобно писать приложение на Языке С#, но в вашей команде есть
другие программисты, для которых более удобно использовать языки VB
или Java. Получается, что главное приложение может быть написано на С#,
а модули - на языках VB и Java.
Поскольку книга ориентирована на начинающих программистов, много­
файловые сборки мы рассматривать не будем. Однако мы рассмотрим соз­
дание однофайловой сборки, а именно сначала мы создадим библиотеку
PetsLibrary, а потом покажем, как подключить ее к приложению и вызвать
метод, описанный в одном из классов этой библиотеки.

9.5. Создание сборки (DLL)
Настало время приступить к практике. Сейчас будет рассмотрен процесс
создания однофайловой сборки .dll с именем PetsLibrary, в которой будет
небольшой набор общедоступных типов (классов). Для создания библиоте­
ки кода нужно выполнить команду File » New Project и создать проект типа
Class Library (рис. 9.3).
Добавим в нашу библиотеку абстрактный класс с именем Cat. Определим в
нем различные свойства кошки. Также определим один абстрактный метод
MeowQ (см. лист. 9.1).
Листинг 9.1. Библиотека классов PetsLibrary
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PetsLibrary

{

Рис. 9.3. Создание библиотеки кода
p u b l i c a b s t r a c t c l a s s Cat
{
p u b l i c s t r i n g Name;
p u b l i c b y t e A ge ;
p u b l i c b y t e W ei g h t;
p u b l i c a b s t r a c t s t r i n g Meow();
p u b lic Cat() { )
p u b l i c C a t ( s t r i n g n, b y t e a g e , b y t e w)

{
Name = n;
Age = a g e ;
Weight = w;

}
}
}
Теперь создадим непосредственного потомка класса Cat, в котором мы
переопределим метод Meow(). Мяукать, конечно, метод не будет, но зато
он будет отображать диалог с надписью “МяуГ. Для отображения диало­
га мы будем использовать класс MessageBox, который находится в сборке
System.Windows.Fonns.dll. Чтобы использовать эту сборку, нужно в н а ш
проект PetsLibrary добавить ссылку на System.Windows.Forms.dll. Для это-

го выполните команду меню P roject, Add R eference. В появившемся окне
(рис. 9.4) установите галку напротив System.Windows.Forms.dll и нажмите
кнопку ОК.

Рис. 9.4. Добавление ссылки на сборку

Поскольку мы создаем консольное приложение, то можно было бы обой­
тись и без класса MessageBox, но мы его добавили, чтобы продемонстриро­
вать процесс добавления ссылки на внешнюю сборку.
Внимание! В окне выбора сборки будут далеко не все зарегистрированные в ва­
шей системе сборки. Если нужной вам сборки нет в списке, используйте кнопку
Browse для выбора .dll или .ехе файла.

Теперь выполните команду Project * Add New Item и создайте новый файл
классов с именем MyCat.cs (рис. 9.5). Обратите внимание на болванку клас­
са, которую создала среда Visual Studio (рис. 9.6), а именно на пространство
имен - PetsLibrary.
Созданный файл классов приведен в листинге 9.2.
Листинг 9.2. Файл MyCat.cs
using System;
using System.Collections.Generic;

Рис. 9.5. Добавление нового файла классов

Рис. 9.6. Создан новый файл класса

using
using
using
using

System .Linq;
System .Text;
System .Threading.Tasks;
S y s t e m . W i n d o w s . Fo rms ;

namespace P e t s L i b r a r y
{
p u b l i c c l a s s MyCat : C a t
{
p u b l i c M y C a t ()

{
Name = " B a g i r a " ;

}
p u b l i c o v e r r i d e s t r i n g Meow()

{
M e s s a g e B o x . Sho w( " Мяу! " ) ;
r e t u r n Name;

}
}
}

9.6. Создание приложения,
использующего сборку
Теперь создадим приложение, которое будет использовать созданную ранее
сборку. Первым делом, если вы этого еще не сделали, выполните команду
Project *■ Build для компиляции нашей сборки. Как обычно, после ком­
пиляции среда сообщит вам каталог, в который была помещена созданная
сборка PetsLibrary.dll (рис. 9.7).
Создайте приложение-с названием PetsClient (рис. 9.8). После создания
проекта выполните команду Project * Add Reference и в появившемся окне
(рис. 9.9) выберите созданную нами библиотеку. Путь к ней показан на пре­
дыдущем рисунке (если вы делаете все, как по книге, то единственное, что у
вас будет отличаться, - это имя пользователя).
Примечание. Созданная ранее библиотека может быть найдена в каталоге С:\

Users\\Documents\Visual Studio 2015\Projects\PetsLibrary\PetsLibrary\bin\
Debug.

Код нашего приложения приведен в листинге 9.3.
Листинг 9.3. Код приложения, вызывающего DLL
u s i n g System;
using PetsLib rary ;

/ / выз ыв ае м нашу б иб ли от е ку

р и с

. и.и.

иоэдание нового проекта

Рис. 9.9. Добавление ссылки на библиотеку PetsLibrary.dll

namespace PetsClient

{
class Program

{
static void Main(string[] args)

{
MyCat Bagira = new MyCat ();
Bagira.Meow();
// Метод Meow() из DLL
Console.ReadLine();

}
}
}
Результат выполнения нашей очень сложной программы приведен на рис.
9.10. Метод Meow(), как и класс MyCat, физически находятся в библиотеке
PetsLibrary, которую мы разработали ранее.

Рис. 9.10. Программа в действии
?

Глава 10.

Многопоточность
и параллельное
программирование

Многопоточность и параллельное программирование - довольно серьез­
ная тема и, несмотря на то, что книга предназначена для новичков, хоте­
лось бы ее осветить. Начнем мы с рассмотрения параллельных коллекций
из пространства имен System.Collections.Concurrent. Параллельные кол­
лекции являются потокобезопасными и специально предназначены для
параллельного программирования. А это означает, что их можно безопас­
но использовать в многопоточной программе, где возможен доступ к кол­
лекции со стороны двух или больше параллельно работающих потоков.

10.1. Параллельные коллекции
Для безопасного доступа к коллекциям предназначен интерфейс
IProducerConsumerCollection. У него много методов, но самые важные
- это TryAdd() и ТгуТаке().
Первый метод пытается (именно поэтому есть "try" в его названии) доба­
вить элемент в коллекцию. Однако добавление может не получиться, если
коллекция заблокирована от добавления элементов. Метод возвращает
булевское значение, сообщающее об успехе или неудаче операции. Метод
TryTake() пытается вернуть элемент из коллекции.
В таблице 10.1 перечислены классы из System.Collections.Concurrent с
кратким описанием их функциональности.
Таблица 10.1. Параллельные коллекции
К ласс

О писание

ConcurrentQueue

Параллельная версия очереди. Для доступа
к элементам очереди применяются методы
Enqueue(), TryDequeue() и TryPeek(). Имена
этих методов похожи на уже известные м е­
тоды Queue, но с добавлением префикса
Try к некоторым из них (к тем, которые могут
вызвать ошибку в параллельной среде).

ConcurrentStack

Параллельная версия стека. Данный класс
определяет методы Push(), PushRangeO,
TryPeek(), ТгуРорО и TryPopRangeQ. Внутри
этот класс использует связный список для
хранения элементов.

ConcurrentBag

He определяет никакого порядка для добав­
ления или извлечения элементов. Реализует
концепцию отображения потоков на исполь­
зуемые внутренне массивы, при этом он
старается избежать блокировок. Для досту­
па к элементам применяются методы Add(),
TryPeek() и TryTake().

C o n c u rr e nt Di c ti o na r y< TK e y,
TValue>

Параллельная версия словаря. Для доступа
к членам в неблокирующем режиме служат
методы TryAdd(), TryGetValue(), TryRemove()
и Tryllpdate().

BlockingCollection

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

Далее мы рассмотрим пример использования класса BlockingCollection.
Это реальный пример многопоточного приложения. В качестве структуры
данных была выбрана BlockingCollection. Также пример реального много­
поточного приложения будет приведен в главе 12 - мы разработаем много­
поточный сервер, позволяющий обработать запросы нескольких клиентов.
В листинге 10.2 будет приведен пример использования двух потоков - ос­
новного и дополнительного.
Листинг 10.1. Многопоточное приложение. Использование
BlockingCollection
using
using
using
using

System;
S y s t e m .C o l le c t i o n s . Concurrent;
System .Threading;
System .Threading.Tasks;

namespace C o n s o l e A p p l i c a t i o n l

{
c l a s s Pr og ra m
{
s t a t i c B lo c k in g C o lle c t io n < in t > be;
s t a t i c v o i d p r o d u c e r ()

{
for

( i n t i = 0;

i < 100;

i++)

{
b e .A d d (i * i ) ;
C o n so le .W rite L in e ("P ro d u c e r " + i * i ) ;

}
b e . C o m p le te A d d in g ( ) ;

}
s t a t i c v o i d c o n s u m e r ()

{
in t i ;
w h ile ( !b c .I sC o m p le te d )

{
if

(b c .T ry T ak e (out i ) )
C o n so le .W rite L in e ("C o n su m e r: " + i ) ;

}
}
s t a t i c v o i d M a in ()

{
b e = new B l o c k i n g C o l l e c t i o n < i n t > ( 4 ) ;
/ / З а д а ч и п о ста вщ и к а и п о т р е б и т е л я
T a s k P r o d u c e r T a s k = new T a s k ( p r o d u c e r ) ;
T a s k C o n su m e r T a sk = new T a s k ( c o n s u m e r ) ;
//

З а п у сти м з а д а ч и
P ro d u c e rT a sk .S ta rt ();
C onsum erT ask. S t a r t ( ) ;
try

{
T ask .W aitA ll(C o n su m e rT a sk ,

t
catch

( E x c e p t i o n e x)

(
C o n so le .W rite L in e (e x );

)
fin a lly

{
C on su m erT ask. D isp o se ( ) ;
P ro d u cerT ask . D isp o se ();
b e . D isp o se ( ) ;

)
C o n so le . R ead L in e();

}
}
}

P rodu cerT ask);

Сначала мы создаем задачи поставщика и потребителя, а потом в блоке try
мы ждем завершения этих задач.

10.2. Библиотека распараллеливания
задач
Современные компьютеры имеют многоядерные процессоры (самые сла­
бые из них обладают двумя ядрами). Не запуская нескольких потоков, вы
не сможете использовать на полную мощь современные процессоры. По­
этому параллельное программирование так важно и рассматривается в этой
книге.
Главным нововведением в .NET Framework 4.0 является библиотека (TPL,
Task Parallel Library). Данная библиотека упрощает создание и применение
многих потоков. Да, потоки и задачи можно было создавать и в более ран­
них версиях .NET, но с TPL создание многопоточных приложений суще­
ственно упрощается. К тому же она позволяет автоматически использовать
несколько процессоров. Именно поэтому библиотека TPL рекомендуется
в большинстве случаев к применению для организации многопоточной об­
работки.
Также, начиная с версии 4.0, .NET поддерживает параллельный язык инте­
грированных запросов (PLINQ). Язык PLINQ дает возможность составлять
запросы, для обработки которых автоматически используется несколько
процессоров.
Язык PLINQ довольно простой, с его помощью очень просто запросить па­
раллельную обработку запроса. Следовательно, используя PLINQ, вы без
особых проблем сможете внедрить параллелизм в свою программу.
Библиотека TPL определена в пространстве имен System.Threading.Tasks.
Однако для работы с ней также нужно подключить класс System.Threading,
поскольку он поддерживает синхронизацию и другие средства многопоточ­
ной обработки, в том числе и те, что входят в класс Interlocked.
Хотя есть библиотека TPL и язык PLINQ, организовать многопоточную
обработку можно и средствами класса Thread, хотя этот подход считает­
ся устаревшим и вместо него рекомендуется использовать как раз TPL и
PLINQ.
Применяя TPL, добавить параллелизм в программу можно двумя основ­
ными способами. Первый способ - это параллелизм данных: одна операция
над совокупностью данных разбивается на два (или больше) параллельно
выполняемых потока, в каждом из которых обрабатывается часть данных.

Такой подход может привести к значительному ускорению обработки дан­
ных по сравнению с последовательным подходом.
Параллелизм данных был доступен и раньше с помощью класса Thread, но
его использование требовало немалых усилий и времени. Но с появлением
TPL все изменилось, и теперь вы можете создавать масштабируемые реше­
ния без особого труда.
Второй способ - параллелизм задач: две (или более) операции выполня­
ются параллельно. Параллелизм задач представляет собой разновидность
параллелизма, который достигался в прошлом средствами класса Thread.
Благодаря TPL у программиста есть возможность автоматически масшта­
бировать исполнение кода на несколько процессоров. Библиотека TPL по­
зволяет автоматически распределять нагрузку приложений между доступ­
ными процессорами в динамическом режиме, используя пул потоков CLR.
Также она занимается распределением работы, планированием потоков,
управлением состоянием и прочими низкоуровневыми деталями.

10.3. Класс Task
В основе TPL лежит класс Task, то есть при использовании TPL нужно соз­
давать объекты типа Task, а не Thread. Ранее, в листинге 10.1, уже был по­
казан пример использования Task, правда, без объяснения подробностей.
Класс Task отличается от Thread тем, что он является абстракцией, пред­
ставляющей асинхронную операцию. А в классе Thread инкапсулируется
поток исполнения. Понятно, что на системном уровне поток по-прежнему
остается элементарной единицей исполнения, которую можно планировать
средствами операционной системы. К тому же - исполнением задач занима­
ется планировщик задач, работающий с пулом потоков. Это означает, что
несколько задач могут разделять один и тот же поток. Класс Task определен
в пространстве имен System.Threading.Tasks.
Для создания новой задачи нужно просто создать новый объект класса Task
и начать его выполнение. Для создания объекта используется конструктор
Task(), а для запуска - метод Start(). Чаще всего используется следующая
форма конструктора:
p u b l i c T a s k ( A c t io n действие)

Здесь действие - это точка входа в код, который будет представлять задачу.
Форма делегата Action следующая:
p u b l i c d e l e g a t e v o i d A c t i o n ()

t

Как видите, точкой входа служит метод, который не принимает параметров
и не возвращает никаких значений. Однако позже будет показано, как пере­
дать делегату данные.
После создания задачу можно запустить методом Start(), что и было по­
казано в листинге 10.1. После вызова этого метода планировщик задач за­
планирует выполнение задачи. Обратите внимание: задача будет запущена
не сразу, она будет добавлена в очередь планировщика заданий. Она будет
выполнена, как только до нее дойдет очередь. В простых демонстрационных
приложениях задача будет выполнена мгновенно, но в сложных больших
приложениях возможна определенная задержка между вызовом Start() и
началом выполнения задачи. В листинге 10.2 приводится еще один пример
основного и дополнительного потока.
Листинг 10.2. Основной и дополнительный поток
u s i n g System;
usin g System .Threading;
using System .Threading.Tasks;
namespace C o n s o l e A p p l i c a t i o n l
{
c l a s s P rog ra m
{
/ / Метод, который б у д е т запущен в к а ч е с т в е з а д а ч и
s t a t i c vo id TaskActionO
{
C onsole.W riteLine("TaskA ction s t a r t e d " ) ;
for

( i n t count = 0;

co u n t < 5;

count++)

{
/ / Ждем 1 с е к у н д у (1000 мс)
/ / Ожидание имит ирует бурную д е я т е л ь н о с т ь п о т о к а
T h r e a d .S le e p (1000);
Console.W riteLine("Count:

" + count);

}
}
s t a t i c v o i d Main()

{
Console.W riteLine("M ain thread s t a r t e d " ) ;
T a s k t a s k = new T a s k ( T a s k A c t i o n ) ;
//

Запустить задачу
?

t a s k . S t a r t ();
for

( i n t i = 0;

i < 20;

i++)

<
C onsole.W rite( " . " ) ;
T h r e a d .S le e p (500);

)
C o n s o l e . W r i t e L i n e ( " Main t h r e a d s h u t d o w n " ) ;
C onsole. ReadLine();

)
)
}
На рис. 10.1 приводится результат выполнения этой программы. Как вид­
но из вывода программы, главный и дополнительный поток выполняются
одновременно. Основной поток выводит точки, а дополнительный - значе­
ние счетчика.
Нужно помнить, что как только завершается основной поток, то заверша­
ются и все дополнительные (созданные классом Task задачи). Поэтому в
основном потоке мы используем метод Thread.Sleep(), чтобы сохранять ос­
новной поток активным, пока не завершится выполнение дополнительного
потока.

Рис. 10.1. Результат выполнения программы из листинга 10.2

Если вы ранее использовали класс Thread, то наверняка заметили, что в
классе Task нет свойства Name, которое задавало имя задачи. Зато в нем
есть свойство Id - идентификатор задачи, но оно типа int и доступно только
для чтения.
При запуске каждой задаче назначается собственный идентификатор. Зна­
чения Id уникальны, но не упорядочены. Поэтому если вы запустили две
задачи, то значение Id второй задачи не обязательно будет больше значения
Id первой задачи.
Получить идентификатор текущей задачи можно с помощью свойства
Currentld. Данное свойство тоже доступно только для чтения:

Изменим немного программу из листинга 10.2. Во-первых, мы добавим вто­
рую задачу, а во-вторых, сделаем вывод Id задачи в процессе ее выполнения.
Также мы немного изменим счетчики - для большей наглядности. Изменен­
ный вариант приведен в листинге 10.3.
Листинг 10.3. Пример использования свойства Currentld
u s in g System;
usin g System .Threading;
usin g System .Threading.Tasks;
namespace C o n s o l e A p p l i c a t i o n l

{
class

P rog ra m

{
/ / Метод, который б у д е т запущен в к а ч е с т в е з а д а ч и
s t a t i c vo id TaskActionO
{
Console.WriteLine("TaskAction s t a r t e d , t a s k { 0 } " , T ask .C u rren tld );
for

( i n t c o u n t = 0;

c o u n t < 10; co unt+ +)

{
/ / Ждем 1 с е к у н д у ( 10 0 0 мс)
/ / Ожидание имит ир уе т бурную д е я т е л ь н о с т ь п о т о к а
T hread .Sleep (1000);
Co nso le .Wr ite Li ne ("T ask ( 0 ) , Count (1) " , T a s k. C u rr en t ld , co un t) ;

}
)
s t a t i c v o i d Main()

{
Console.W riteLine("M ain th read s t a r t e d " ) ;
T a s k t a s k l = new T a s k ( T a s k A c t i o n ) ;
T a s k t a s k 2 = new T a s k ( T a s k A c t i o n ) ;
/ / Запустить задачу
t a s k l . S ta r t ();
t a s k 2 . S ta r t ();
for

( i n t i = 0;

i < 25;

{
Console.W rite( " . " ) ;
T hread.Sleep (500);

i++)

}
Console.W riteLine("M ain th re ad shutdown");
C on sole. ReadLine();

}
}
}
Проанализируем вывод программы (см. рис. 10.2). Было запущено две зада­
чи. Первая, как видите, получила идентификатор 2, вторая 1. Первая задача
(с Id 2) завершилась также первой.

Рис. 10.2. Две задачи, демонстрация свойства Currentld

10.4. Ожидание задачи
В двух предыдущих примерах основной поток выполнения (то есть метод
Main()) мог завершиться раньше, чем выполнение запущенных задач. Что­
бы этого не произошло, мы использовали метод Thread.Sleep(), но это, мяг­
ко говоря, неправильно по двум причинам:
Выполнение метода Main() все равно может завершиться раньше, если про­
граммист не угадает задержку для Thread.SleepO и укажет ее меньше, чем
нужно для выполнения всех запущенных задач.
Как видно из рис. 10.2, основной поток продолжает выполнение, хотя за­
дача уже выполнена - в этом случае программист указал задержку больше,
чем нужно. Выходит, программа должна уже завершиться, но она все еще
продолжает работу.

Далеко не всегда можно спрогнозировать выполнение каждой запущенной
задачи, поэтому использование метода Thread.SleepO является неправиль­
ным.
В классе Task есть метод Wait(), позволяющий организовать завершение за­
дач более совершенным образом. При выполнении этого метода могут быть
сгенерированы два исключения:


ObjectDisposedException - генерируется, если задача освобождена
посредством вызова метода Dispose().



AggregateException - генерируется, если задача сама генерирует ис­
ключение или же отменяется.

Перепишем наш пример с использованием метода Wait(). Данный метод
мы будем вызывать из метода Main(), чтобы дождаться выполнения задач
taskl и task2.
Листинг 10.4. Использование метода WaitO
u s in g System;
usin g System .Threading;
using System .Threading.Tasks;
namespace C o n s o l e A p p l i c a t i o n l
{
c l a s s P rog ra m
{
/ / Метод, который б у д е т з апущен в к а ч е с т в е з а д а ч и
s t a t i c vo id TaskActionO
{
Console.WriteLine("TaskAction s t a r t e d , t a s k { 0 } " , T ask .C u rren tld );
for

( i n t co u n t = 0;

c o u n t < 10;

count++)

{
/ / Ждем 1 с е к у н д у (1000 мс)
/ / Ожидание имитирует бурную д е я т е л ь н о с т ь п о т о к а
T h r e a d .S le e p (1000);
Console. WriteLine("Task ( 0 ) , Count (1) " , T as k. Cur rent ld, count);

)
Console.W riteLine("End o f t a s k

)
s t a t i c v o i d M ai n ( )
{

(0)",

T ask.Currentld);

Console.WriteLine("Main thread started");
Task taskl = new Task(TaskAction);
Task task2 = new Task(TaskAction);
// Запустить задачу
taskl.Start ();
task2.Start ();
Console.WriteLine("Waiting...") ;
taskl.Wait();
task2.Wait();
Console.WriteLine("Main thread shutdown");
Console.ReadLine();

}
)
}

Рис. 10.3. Использование метода Wait()

Как видите (рис. 10.3), теперь основной поток выполняется ровно столько,
сколько нужно - он завершается сразу после завершения последней задачи.
Теперь нет необходимости вызывать метод Thread.Sleep() и нет этих ужас­
ных точек, свидетельствующих о выполнении главного потока.
Иногда вместо использования метода Wait() удобнее использовать метод
WaitAll(), которому передаются задачи, выполнения которых нужно до­
ждаться. Пример применения этого метода был приведен в листинге 10.1:

try

{
Task.W aitAll(ConsumerTask,

ProducerTask);

}
catch

( E x c e p t i o n e x)

{
Console.W riteLine(ex);

}
finally

{
C o n s u m e r T a s k . D i s p o s e () ;
ProducerTask.D isp o se ();
b e . D isp o se ();

}
В блоке try/catch/finally происходит ожидание выполнения задач
ConsumerTask и ProducerTask.
Метод DisposeQ реализуется в классе Task, освобождая ресурсы, использу­
емые этим классом. Как правило, ресурсы, связанные с классом Task, осво­
бождаются автоматически во время "сборки мусора" (или по завершении
программы). Но если эти ресурсы требуется освободить еще раньше, то для
этой цели служит метод Dispose(). Это особенно важно в тех программах,
где создается большое число задач, оставляемых на произвол судьбы. В ли­
стинге 10.1 мы использовали вызов этого метода в блоке finally, хотя имен­
но в том конкретном случае особой нужды использовать данный метод не
было.

10.5. Класс TaskFactory
Ранее мы сначала создавали задачу, а потом запускали ее выполнение. При
желании, используя класс TaskFactory, можно сразу создать и запустить на
выполнение задачу.
По умолчанию объект класса TaskFactory может быть получен из свойства
Factory, доступного только для чтения в классе Task. Используя это свой­
ство, можно вызвать любые методы класса TaskFactory. Метод StartNew()
существует во множестве форм. Самая простая форма метода StartNew()
выглядит так:
p u b li c Task StartN ew (A ction a c tio n )

В нашем случае можно было бы переписать код так (с использованием
TaskFactory):
/ / И с п о л ь з о в а н и е фабрики з а д а ч
T a s k F a c t o r y t f = new T a s k F a c t o r y ( ) ;
*

? as k t l
Ч

= t f . StartN ew (TaskA ction);

А можно с о з д а т ь и з а п у с т и т ь з а д а ч у т а к :

Task t 2

= T a s k .F a c to r y . StartN ew (TaskA ction) ;

Как видите, можно использовать один из двух способов создания и запуска
задач с помощью TaskFactory.

10.6. Продолжение задачи
Иногда бывают ситуации, когда нужно запустить следующую зада­
чу сразу по завершению текущей. Для этого в TPL используется метод
ContinueWith(), определенный в классе Task. Ниже приведена простейшая
форма его объявления:
p u b l i c T a s k C o n t i n u e W i t h ( A c t i o n < T a s k > д е йс тв ие_ п род ол жен ия)

10.7. Возврат значения из задачи
Задача может возвращать значение. Ранее мы не рассматривали такие за­
дачи, но вы должны знать, что такая возможность имеется. Возвращение
значения из задачи удобно по нескольким причинам. Прежде всего, задача
может вычислять некоторый результат и возвращать в основной поток вы­
численное значение. Таким образом, в C# можно организовать параллель­
ные вычисления. Также вызывающий процесс окажется блокированным до
тех пор, пока не будет получен результат. Это означает, что для организации
ожидания результата не требуется никакой особой синхронизации, в том
числе и метода Wait().
Чтобы вернуть результат из задачи, нужно создать эту задачу, используя
обобщенную форму Task класса Task. Ниже приведены два кон­
структора этой формы класса Task:
p u b l i c T a s k ( F u n c < T R e s u l t > функция)
p u b l i c T a s k ( F u n c < O b j e c t , T R e s u l t > функция, O b j e c t с о с т о я н и е )

Здесь функция обозначает выполняемый делегат. Обратите внимание на то,
что в этом случае он должен быть типа Func, а не Action. Тип Func исполь­
зуется именно в тех случаях, когда задача возвращает результат. В первой
форме конструктора создается задача без аргументов, а во втором - задача,
принимающая аргумент типа Object, передаваемый как состояние. В доку­
ментации по C# вы также найдете другие формы конструктора класса Task.
Также имеются и другие формы метода StartNew(), доступные в обоб­
щенной форме класса TaskFactory и поддерживающие возврат

результата из задачи. Рассмотрим те их них, которые применяются парал­
лельно с только что рассмотренными конструкторами класса Task:
p u b l i c T a s k < T R e s u l t > S t a r t N e w ( F u n c < T R e s u l t > функция)
p u b l i c T a s k < T R e s u l t > S t a r t N e w ( F u n c C O b j e c t , T R e s u l t > функция,
O b je c t состояние)

Значение, возвращаемое задачей, может быть получено из свойства Result
в классе Task:
p u b lic TResult R e su lt

( get;

internal

set;

}

Аксессор set является внутренним для данного свойства, и поэтому во
внешнем коде оно доступно только для чтения. Следовательно, задача по­
лучения результата блокирует вызывающий код до тех пор, пока результат
не будет вычислен.
Мы только что рассмотрели основные возможности по созданию парал­
лельных задач в С#. Если этого вам оказалось мало, вы заинтересовались
и готовы продолжить изучение параллельного вычисления, тогда обрати­
те свое внимание на класс Parallel, который поддерживает набор методов,
которые позволяют выполнять итерации по коллекции данных (точнее, по
объектам, реализующим IEnumerable) в параллельном режиме. Опи­
сание этого класса вы найдете в документации по .NET Framework на сайте
Microsoft:

https://msdn.microsoft.com/ru-ru/library/system.threadmg.tasks.
parallel%28v=vs.H0%29.aspx

t

Глава 11.

Сетевое программирование

Современный компьютер сложно представить без доступа к Сети - будь то
локальной или же к Интернету. Поэтому и книгу по программированию
сложно представить без описания хотя бы основ сетевого программирова­
ния. В этой главе будут представлены такие основы, и начнем работу с се­
тью мы с пространства имен System.Net.

11.1. Пространство имен System.Net
Пространство имен System.Net содержит сетевые классы для поиска IPадресов, сетевой аутентификации, разрешений, отправки и получения дан­
ных. Для большего удобства все классы из System.Net мы отсортируем по
группам и рассмотрим.
Начнем с поиска имен. Для преобразования IP-адреса в символьное имя и
обратно используется система доменных имен (DNS, Domain Name System).
Платформа .NET содержит следующие классы для разрешения имен/1Радресов:


Dns - используется для разрешения символьных имен в IP-адреса и
обратно.



Dn s Ре rmi s s i on - представляет разрешение, необходимое для поиска

имени.


D n s P e r m i s s i o n A t t r i b u t e - позволяет отмечать сборки, классы
и методы, нуждающиеся в полномочиях, определяемых классом
DnsPermission.

Обработка IP-адресов производится в классе IPAddress. Сейчас некоторые
узлы содержат более одного IP-адреса и более одного имени (используются
псевдонимы). Информация о дополнительных IP-адресах и псевдонимах
находится в классе IPHostEntry. Когда мы ищем имя, класс Dns возвращает
объект типа IPHostEntry.
Для
аутенти ф икаци и
и
авторизации
используется
класс
AuthenticationManager. Данный класс обращается к этим модулям, чтобы
идентифицировать пользователя. Модули аутентификации получают ин­
формацию запроса и данные о личности пользователя с помощью интер­
фейса ICredentials и возвращают объект Authorization для авторизованных
пользователей, которые могут использовать тот или иной ресурс. Прило-

жение-клиент может использовать класс NetworkCredential, передающий
данные о пользователе на сервер.
Различные сетевы е запросы можно осуществить посредством абстракт­
ных классов WebRequest и WebResponse. В System.Net имеется несколь­
ко специальных реализаций этих классов для HTTP и доступа к файлам:
HttpWebRequest, HttpWebResponse, FileWebRequest, FileWebResponse.
Класс компонентов WebClient облегчает использование WebRequest и
WebResponse из Visual Studio .NET.
Для управления соединениями используются классы ServicePoint и
ServicePointManager. Если же вы хотите использовать сокеты , то вам при­
годится пространство имен System.Net.Sockets. Для программирования с
использованием сокетов вам нужно знать тот или иной сетевой протокол.
Однако использование сокетов - это хоть и более низкоуровневый способ
взаимодействия, он позволяет наиболее гибко организовать связь.
Далее будут рассмотрены самые важные классы из пространства System.
Net.

11.2. Класс Uri
Класс Uri предназначен для работы с универсальными идентификаторами
ресурса (URI, Uniform Resource Identifier). Мы каждый день используем
URI и даже не задумываемся над этим, - когда обращаемся к Web-страницам,
FTP-серверам и т.д. URI можно использовать не только для обращения к
ресурсам Web и FTP, но и к сетевым и даже локальным файлам.
Рассмотрим пример URI и разберемся, из чего он состоит:

http://www.example.com:3128/public/index.php?page=about&lang=:en
Первая часть, содержащая название протокола (http://), называется схемой
(scheme). После ограничителя схемы ( / / ) указывается имя или IP-адрес
узла, в нашем случае www.example.com.
После имени узла через двоеточие указывается номер порта. Если номер
порта не указывается, то и двоеточие тоже не указывается.
Далее следует путь, определяющий каталог и страницу нужного нам ресур­
са. В нашем случае - это /public/index.php.
После символа ? указываются параметры, передаваемые ресурсу. В терми­
нологии URI эта часть называется запросом (query). В нашем случае запрос
выглядит так: index.php?page=about&lang=en.
Для работы с URI используется, как уже было отмечено, класс Uri:

Uri site = new Uri("http://www.example.com:3128/public/index.
php?page=about&lang=en");
Далее, используя свойства класса, можно получить доступ к компонентам
Uri:


Scheme - содержит первую часть URI - схему. Для протокола HTTP
это будет значение http.



H o s t - имя хоста.



P o r t - номер порта, если он задан.



A b s o l u t e P a t h - абсолютный путь к ресурсу, в примере выше это /

public/index.php.


Query

- содержит строку после пути, в нашем случае это
?page=about&lang=en.



PathAndQuery - путь и запрос, в нашем случае /public/index.
php?page=about&lang=en.



F r a g m e n t - если после пути следует фрагмент, он возвращается в
свойстве Fragment. За путем могут следовать только строка запроса
или фрагмент. Фрагмент идентифицируется символом #.



U s e r i n f o - если указано имя пользователя, например, ftp://user@

ftp.example.com, то это свойство будет содержать строку user.
В этом классе есть и другие свойства, но они используются гораздо реже.
Важно помнить, что после создания конструктором экземпляр класса Uri не
может больше изменяться. Свойства класса Uri доступны только на чтение.
Для динамического изменения URI можно использовать класс UriBuilder:
U r i B u i l d e r myURI = new U r i B u i l d e r ( " h t t p " , " w ww. e xamp le. com" ,
3 12 8 ,
"/public/index.php?page=about& lang=en", "#team ");
Uri s i t e = myURI.Uri;

11.3. Загрузка файлов (HTTP и FTP)
Одна из довольно распространенных задач - загрузка файлов, например,
приложение закачивает обновление с сервера разработчика или же обраба­
тывает какую-то веб-страницу.
Для загрузки файла может использоваться класс WebClient. Вот как легко
и непринужденно можно загрузить файл (лист. 11.1):
*

Листинг 11.1. Загрузка файлов. Пример использования WebClient
u s in g System;
using S ystem .C ollection s.G en eric;
usin g System.Net;
namespace C o n s o l e A p p l i c a t i o n l
{
c l a s s Pr og r am
{
s t a t i c v o i d Main()

{
C o n s o le . W r ite L in e ("Downloading. . . P l e a s e w a i t . . . " ) ;
W e b C l i e n t wc = new W e b C l i e n t ( ) ;
wc. D o w n l o a d F i l e ( " h t t p : / / d k w s . o r g . u a / p r o x y / p r o x y .
z ip " , " c : W tempW proxy. z ip ") ;
C o n s o l e . W r i t e L i n e ( "Down loa d c o m p l e t e ! " ) ;
Console.R eadLine();

}
}
}
Метод DownloadFile() принимает два параметра: первый - это путь к файлу,
который нужно скачать, а второй - локальное имя файла. Обратите внима­
ние: я указал не только имя, но и путь, папка C:\temp. Символ \ требует
экранирования, поэтому он указан как \ \ . Также папка C:\temp должна су­
ществовать на момент запуска программы. Результат работы программы
изображен на рис. 11.1 - видно, что программа завершила загрузку файла в
папку C:\temp.
Теперь представим другую ситуацию. Допустим, нам нужно загрузить
не файл, а нам нужно получить какую-то веб-страницу в строку для после­
дующей ее обработки. Связываться для этого с файлами не хочется. Вопервых, в реальном приложении придется производить обработку кор­
ректности записи файла, иметь дело с правами доступа и т.д. Обычно
программа устанавливается в C:\Program Files, а права записи к этому
каталогу есть не у всех пользователей. Значит, записывать придется в до­
машний каталог пользователя. Во-вторых, сама процедура неэффектив­
ная - сначала мы загружаем страницу в файл, потом читаем текст из этого
файла. Гораздо удобнее использовать метод OpenReadQ:
W e b C l i e n t wc = new W e b C l i e n t ( ) ;
S tream s t r = wc. OpenRead("h t t p : / / d k w s . o r g . u a / " );

Рис. 11.1. Файл успешно загружен

Содержимое страницы будет загружено в переменную str. После этого мож­
но использовать класс StreamReader для обработки потока str
S t r e a m R e a d e r s r = new S t r e a m R e a d e r ( s t r ) ;
strin g s;
while ( (s = sr.ReadLine()) != null)
Console.WriteLine(s);

В классе WebClient также имеются методы UploadFile() и UploadData().
Первый используется, чтобы отправить на сервер файл, а второй - данные
HTML-формы. Стоит отметить, что второй метод выгружает на сервер дво­
ичные данные, представленные в виде массива байт, по указанному URI
(есть и метод DownloadDataQ, предназначенный для извлечения массива
байтов из URI).
Нужно отметить, что выгрузка (upload) файлов по протоколу HTTP осу­
ществляется довольно редко, поскольку такое действие на реальных сер­
верах запрещено настройками безопасности. Как правило, загрузка фай­
лов на сервер осуществляется по протоколу FTP, а для этого WebClient не
подходит, поскольку его возможности весьма ограничены. Причина в том,

что WebClient — класс общего назначения, предназначенный для рабо­
ты с любым протоколом, позволяющим отправлять запросы и получать
ответы (вроде HTTP и FTP). Он не может обработать никаких средств,
специфичных для какого-то одного протокола, например, сокеты, ко­
торые специфичны для HTTP. Чтобы написать более сложное сетевое
приложение, нужно использовать другие классы, а именно WebRequest
и Web Response. В листинге 11.2 приведен пример использования этих
классов для загрузки файла с FT P -сервера, требующего аутентификации.

Листинг 11.2. Загрузка файла с FTP-сервера
u sin g
u sin g
u sin g
u sin g

System ;
S y ste m .10;
S y ste m .N e t;
S y ste m .T e x t;

n a m e s p a c e FTPDowload

{
p u b l i c c l a s s W eb R e q u estG e tE x a m p le

{
p u b l i c s t a t i c v o i d Main

()

{
/ / Получаем о б ъ е к т , который будем и с п о л ь з о в а т ь для
св я зи с сервером
Ftp W e b R eq u est r e q u e s t = (F tp W e b R e q u e s t) W e b R e q u e s t.
C r e a te (" f t p : //e x a m p le .c o m /re p o rt.tx t" ) ;
/ / Выбираем м е т о д з а г р у з к и файла
r e q u e s t . Method = W e b R e q u e s tM e th o d s . F t p .
D o w n lo ad F ile ;
/ / З а д а е м имя п о л ь з о в а т е л я и п ар ол ь
r e q u e s t . C r e d e n t i a l s = new N e t w o r k C r e d e n t i a l
(" d e n " ,"1 2 34567");
/ / Формируем о б ъ е к т для о т в е т а
F tp W ebR esp on se r e s p o n s e =
G etR esp o n se();

(F tp W eb R esp on se)requ est.

/ / Создаем поток о т в е т а
S tream re sp o n se S tre a m = r e s p o n s e .
G etR esp o n seStream ( ) ;
/ / И сп о л ьзуе м S t r e a m R e a d e r для ч тен и я о т в е т а
S t r e a m R e a d e r r e a d e r = new
Stream R ead er(resp on seStream );

// Поскольку файл текстовый, просто выводим его на консоль
// для других типов файлов нужно сохранить поток в файл
Console.WriteLine(reader.ReadToEnd());
Console.WriteLine("Загрузка завершена. Статус
{0}", response.StatusDescription);
reader.Close ();
response.Close ();

// закрываем reader
// закрываем ответ

}
}
}

11.4. Класс Dns. Разрешение доменных
имен
В этом разделе мы поговорим о преобразовании IP-адресов в символьные
имена и обратно. Начнем с класса IPAddress, который представляет IPадрес. Сам адрес доступен в виде свойства GetAddressBytes и может быть
преобразован в десятичный формат с разделителями-точками с помощью
метода. ToString().
IPAddress ip = IPAddress.Parse("192.168.1.9") ;
byte[] adress = i p .GetAddressBytes();
string ipString = ip.ToString();

Для разрешения имен в IP-адреса используется класс Dns. Рассмотрим не­
большой пример:
using System.Net;
string hostname = "www.mail.ru", ips="", aliases="";
IPHostEntry entry = Dns.GetHostEntry(hostname);
foreach (IPAddress a in entry.AddressList)
ips += a.ToString () + "\n";
foreach (string aliasName in entry.Aliases)
aliases += aliasName + "\n";
Console.WriteLine("Псевдонимы:\n" + aliases);
Console.WriteLine("\пСписок IP:\n" + ips);

Как видите, у www.mail.ru нет псевдонимов, зато есть два IP-адреса.
?

Рис. 11.2. Разрешение доменного имени

11.5. Сокеты
11.5.1. Типы сокетов
Изначально сокеты появились в операционной системе UNIX. Сокет - это
один конец двустороннего канала связи между двумя программами, рабо­
тающими в сети. Используя два сокета, можно передавать данные между
разными процессами (как локальными, так и удаленными). На локальной
машине сокет может использоваться как средство межпроцессного взаимо­
действия. В сети сокет может использоваться для передачи данных между
двумя программами, запущенными на разных компьютерах.
Прежде чем использовать сокет, его нужно открыть, задав надлежащие раз­
решения. Как только ресурс открыт, из него можно считывать данные и/
или в него можно записывать данные. После использования сокета нужно
вызвать метод Close() для его закрытия.
Существует два типа типа сокетов — потоковые сокеты (stream sockets) и
дейтаграммные (datagram socket).
Потоковый сокет —это сокет с установленным соединением, состоящий из
потока байтов, который может быть двунаправленным, то есть через такой
сокет можно передавать и принимать данные.
Потоковый сокет гарантирует доставку передаваемых данных в целости и
сохранности, он сохраняет последовательность данных. Если вам нужно не
просто доставить данные, а доставить их в правильной последовательности,
то нужно использовать потоковый сокет. Потоковые сокеты используют в
качестве транспортного протокола TCP (Transmission Control Protocol), ко­
торый и контролирует доставку данных.

Дейтаграммные сокеты не подразумевают установку соединения. Сообще­
ние отправляется указанному сокету, и на этом все. Никакая доставка или
хотя бы ее контроль не гарантируется. Если все хорошо, сообщение будет
доставлено получателю, если нет, вы даже не узнаете об этом. Дейтаграмм­
ные сокеты использует для доставки сообщений протокол UDP (User Da­
tagram Protocol).
Когда нужна гарантированная доставка данных, нужно использовать по­
токовые сокеты. Если же надежное соединение с сервером не требуется,
можно использовать дейтаграммные сокеты. Ведь на установление на­
дежного соединения с сервером требуется время, которое просто вносит
задержки в обслуживание - иногда проще и быстрее отправить несколько
U D P-пакетов.
Есть еще и сырые сокеты (raw sockets). Задача таких сокетов заключается
в обходе механизма, с помощью которого компьютер обрабатывает Т С Р /
IP. Сырой сокет — это сокет, который принимает пакеты, обходит уровни
TCP и UDP в стеке T C P/IP и отправляет их непосредственно приложению.
Подходит, если вы хотите реализовать собственный транспортный прото­
кол, в противном случае проще использовать TCP или UDP.
11.5.2. Порты

Представим себе взаимодействие двух компьютеров - Web-сервера, пусть
у него будет IP-адрес 192.168.1.1, и компьютера пользователя с IP-адресом
192.168.1.100. Компьютер пользователя отправляет серверу запрос на по­
лучение какой-то страницы. Сервер получает запрос, обрабатывает его и
отправляет пользовательскому компьютеру ответ. Но на сервере может
быть запущено несколько программ для обработки запросов, например,
веб-сервер Apache, почтовый сервер Exim, FTP-сервер ProFTPD и др. Как
система определит, какому именно приложению нужно передать запрос от
пользовательского компьютера?
В заголовке TC P-пакета передается номер порта. По сути, номер порта за­
дает номер приложения на сервере, которому будут отправлены данные.
Стандартные номера портов определены организацией IANA (Internet As­
signed Numbers Authority). Так, порт с номером 80 используется для веб­
сервера, 22 - для SSH, 110 - для РОРЗ-протокола, 25 - для SM TP (отправка
почты), 21 - для FTP и т.д.
Получив TC P-пакет, система “смотрит” на номер порта и передает этот па­
кет соответствующему приложению. Аналогично приложение веб-сервер,
получив запрос, отправляет клиентскому компьютеру ответ. Клиентский
компьютер может отправить несколько запросов - один к FTP-серверу, дру-

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

В таблице 11.1 перечислены классы, предназначенные для работы с со­
кетами.
Таблица 11.1. Классы для работы с сокетами
Класс

Описание

M ulticastO ption

Устанавливает значение IP-адреса для присоединения к
IP-группе или для выхода из нее

NetworkStream

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

Socket

Обеспечивает базовую функциональность приложения
сокета

SocketException

Задает исключения при работе с сокетами

TcpClient

Класс TcpClient построен на классе Socket, чтобы обе­
спечить TC P-обслуживание на более высоком уровне.
TcpClient предоставляет несколько методов для отправки
и получения данных через сеть

TcpListener

Построен на низкоуровневом классе Socket. Его основ­
ное назначение — серверные приложения. Он ожидает
входящие запросы на соединения от клиентов и уведом­
ляет приложение о любых соединениях

UdpClient

Содержит методы для отправки и получения данных по­
средством протокола UDP

Основной класс - это класс Socket. Он обеспечивает базовую функциональ­
ность для работы с сокетами. На его базе построены некоторые другие клас­
сы. Члены класса Socket приведены в таблице 11.2.
Таблица 11.2. Члены класса Socket
Член

Описание

A c c e p t ()

Создает новый сокет для обработки входящего запро­
са на соединение

AddressFamily

Предоставляет семейство адресов сокета — значение
из перечисления Socket.AddressFam ily

A vailable

Возвращает объем доступных для чтения данных

B i n d ()

Связывает сокет с локальной конечной точкой для
ожидания входящих запросов на соединение

Blocking

Дает или устанавливает значение, показывающее, на­
ходится ли сокет в блокирующем режиме

C l o s e ()

Закрывает сокет

C o n n e c t ()

Устанавливает соединение с удаленным узлом

Connected

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

G e t S o c k e t O p t i o n ()

Возвращает значение SocketOption.

I O C o n t r o l ()

Устанавливает для сокета низкоуровневые режимы
работы

L i s t e n ()

Помещает сокет в режим прослушивания (ожидания).
Используется только на сервере

LocalEndPoint

Сообщ ает локальную конечную точку

P o l l ()

Определяет состояние сокета

ProtocolType

Содержит тип протокола сокета

R e c e i v e ()

Получает данные из сокета

R e m ot e En d Po i n t

Сообщ ает удаленную конечную точку сокета

S e l e c t ()

Проверяет состояние одного или нескольких сокетов

S e n d ()

Отправляет данные соединенному сокету

S e t S o c k e t O p t i o n ()

Устанавливает опцию сокета

S h u t d o w n ()

Запрещ ает операции отправки и получения данных на
сокете

SocketType

Содержит тип сокета

11.6. Конвертер валют
В качестве примера реального приложения мы сейчас попробуем соз­
дать конвертер валют, который будет получать актуальные курсы валют у
Google. Теоретически, можно, немного изменив его код, настроить на работу
с любым другим сервисом, но Google, в отличие от других сервисов, будет
всегда. Уже готовая программа изображена на рис. 11.3.
Создайте форму, расположив на ней компоненты, как показано на рис. 11.3.
В качестве списков используйте listBox, заполнив их элементами, как по­
казано на рис. 11.4.
*

К онвертер валю т

Количество

: т м
i USD
Из i EUR
i GBP
I UAH

1000

и м



RUB
В

EUR
GBP
UAH

Перевести

1000 Russian Ruble equate

15.400 US Dollar
Рис. 11.3. Конвертер валют в действии
String Collection Editor

?

X

E n te r th e strin g s in th e c o lle c t io n (o n e p e r line):

Рис. 11.4. Элементы списков валют

Компонент внизу формы - это WebBrowser. Данный компонент позволяет
получить содержимое любой веб-страницы. По сути, это уже готовый веб­
браузер. Используя его метод Navigate(), можно загрузить любую страницу:
w eb B ro w se rl.N av ig a te ("h t t p s : //ww w .google. r u / s e a r c h ? q = " +
t e x t B o x l . T e x t + " " + fr om + " %D0%B2 " + t o ) ;

Мы пытаемся осуществить поиск Google по следующему запросу:
Ссодержимое т е к с т о в о г о поля> и с х о д н а я _ в а л ю т а %D0%B2
р е з у л ь т ир ую щ ая _ ва л ют а

from - это выбранная валюта в первом списке, a to - выбранная валюта во
втором списке. Обработчик кнопки Перевести будет таким:

p riv a te void b u tto n l_ C lick (o b ject sender,

E v e n t A r g s e)

{
s t r i n g f ro m,

to;

from = l i s t B o x l . S e l e c t e d l t e m . T o S t r i n g ( ) ;
to = list B o x 2 .S e le c te d lte m .T o S tr in g ();
i f (from==to)

{
M e s s a g e B o x . S h o w ( " Э т о одна и т а же в а л ю т а ! " ,

"Внимание!");

}
e ls e i f (textB oxl.T ext=="")

{
M e s s a g e B o x . S h o w (" Вв едит е ко личе ст в о в а лю т ы! " ,

"Внимание!");

)
else

{
w e b B r o w s e r l . N a v i g a t e ( " h t t p s : / /www. g o o g l e . r u / s e a r c h ? q = " +
t e x t B o x l . T e x t + " " + f r o m + " %D0%B2 " + t o ) ;

}
}
Как видите, кроме перевода валюты, мы предусмотрели "защиту от дурака"
- мы проверяем ввод пользователя и выбор валюты.

11.7. Простой сканер портов
Для закрепления материала о сокетах разработаем простейший сканер пор­
тов. Приложение будет очень простым и "сырым" - никакой обработки не­
правильного ввода, никакой обработки исключений. При необходимости
вы всегда сможете сами "модернизировать" этот пример. Работающее при­
ложение изображено на рис. 11.5.

Принцип работы сканера портов прост: вы указываете IP-адрес, который
нужно просканировать, вводите диапазон портов и нажимаете кнопку С ка­
нировать. Сканер поочередно подключается к каждому из портов диапазо­
на. Если соединение удалось, программа сообщает, что порт открыт, в про­
тивном случае - что закрыт.
Поместите на форме текстовое поле (имя tIPAddress), два компонента NumericUpDown с номерами nBeginPort и nEndPort. Основной рабочий ком­
понент называется listViewl - в него будут помещены результаты сканиро­
вания. В процессе сканирования индикатор прогресса (progressBarl) будет
сообщать о ходе сканирования.
Обработчик кнопки Сканировать выглядит, как показано в листинге 11.3.
Листинг 11.3. Обработчик нажатия кнопки Сканировать
void B u tto n lC lic k (o b je c t sender,

E v e n t A r g s e)

{
in t BeginPort = C o n vert.T oInt32(n B eginP ort.V alu e);
EndPort = C o n v e r t . T o I n t 3 2 ( n E n d P o r t . V a l u e ) ;
int i ;
p r o g r e s s B a r l . M a x i m u m = E n d P o r t - B e g i n P o r t + 1;
p r o g r e s s B a r l . V a l u e = 0;
listV ie w l. Item s.C le a r ();
IPAddress addr = IP A d d re ss. P a r s e ( t I P A d d r e s s . T e x t ) ;
for

(i = BeginPort;

i -1)

{
Console.WriteLine("Соединение завершено.");
break;

}
s .Shutdown(SocketShutdown.Both);
s .Close ();

}
}
catch (Exception ex)

{
Console.WriteLine(ex.ToString());

}
finally

{
Console.ReadLine ();

}
}
}
}

12.3. Приложение-клиент
Метод Main() будет очень прост:
try

{

C om m u n icate("localh ost", 8888);

}
catch

( E x c e p t i o n ex)

{
Console.W riteLine(ex.T oString());

}
finally

{
C o n s o l e . ReadLine ( ) ;

}
Мы просто запустим метод Communicate, передав ему имя сервера и номер
порта, с которым нужно взаимодействовать. Сам метод Communicate() бу­
дет более сложным. В нем нужно первым делом подключиться к удаленно­
му серверу:
IPHostEntry ipHost = Dns. G etH ostE n try (h o stn am e);
IPA ddress ipAddr = i p H o s t . A d d r e s s L i s t [ 0 ];
I P E n d P o i n t i p E n d P o i n t = new I P E n d P o i n t ( i p A d d r , p o r t ) ;
S o c k e t s o c k = new S o c k e t ( i p A d d r . A d d r e s s F a m i l y ,
Stream, P r o to c o lT y p e . T c p );

SocketType.

/ / Подключаемся к с е р в е р у
sock.Connect(ipEndPoint);

Код должен быть вам уже знаком. Первым делом мы подготавливаем ко­
нечную точку для сокета, затем создаем сокет (параметры такие же, как в
случае с сервером, иначе клиент и сервер просто не поймут друг друга), а
после вместо метода Listen() мы вызываем метод Connect() для подключе­
ния к серверу.
Далее все просто: получаем данные от пользователя (которые нужно пере­
дать серверу), кодируем их и отправляем:
in t bytesSent = sock. Sen d(data);

Мы также рекурсивно вызываем метод Communicate(), чтобы передать сер­
веру следующее сообщение:
if

( m e s s a g e . I n d e x O f ( " < T h e E n d > " ) = = - 1)
Communicate(hostname, p o r t ) ;

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

не будет необходимости. В нем будет поле ввода и кнопка Send. Для от­
правки следующего сообщения серверу нужно будет ввести его в поле вво­
да и нажать кнопку Send. Здесь рекурсия нужна, чтобы пользователь смог
отправить несколько сообщений серверу без перезапуска приложения
клиента - если хотите, из соображений отладки кода. Чтобы завершить ра­
боту клиента и сервера, просто закройте их окна.
Полный код клиентского приложения приведен в листинге 12.2.
Листинг 12.2. Код приложения клиента
using
using
using
using

System;
System .Text;
System.Net;
System .N et. Sockets;

namespace c l i e n t

{
c l a s s Pr og r am

{
s t a t i c v o i d M a i n ( s t r i n g [] a r g s )

{
Console.OutputEncoding = Encoding.G etE ncoding(866);
try
{
Com m unicate("localhost", 8888);

}
catch

( E x c e p t i o n e x)

{
C onsole.W riteLine(ex.ToString ());

}
finally

{
C on sole.ReadLine();

}
)
s t a t i c v o i d C o m m u n ic a te ( st r in g hostname,

in t port)

{
/ / Буфер для входящих данных
b y t e [] b y t e s = new b y t e [ 1 0 2 4 ] ;
/ / Соед иняемся с удаленным с е р в е р о м
/ / У с т а н а в л и в а е м удаленную т оч к у ( с е р в е р ) для с о к е т а
IPHostEntry ipHost = Dns. G etH ostEntry(hostnam e);
IPA ddress ipAddr = i p H o s t . A d d r e s s L i s t [ 0 ] ;

I PEndPoi nt i p E n d P o i n t = new I P E n d P o i n t ( i p A d d r , p o r t ) ;
S o c k e t s o c k = new S o c k e t ( i p A d d r . A d d r e s s F a m i l y ,
So ck etT y p e . Stream, P ro to c o lT y p e . T c p ) ;
/ / Подключаемся к с е р в е р у
sock.Connect(ipEndPoint);
C o n s o l e . W r i t e ( " В в е д и т е сооб щение : " ) ;
s t r i n g message = C o n so le.R ead Lin e();
C o n s o l e . W r i t e L i n e ("Подключаемся к п о р т у { 0 }
s o c k . RemoteEndPoint. T o S t r i n g ( ) ) ;
byte[]

",

d a t a = E n c o d i n g . UTF8. G e t B y t e s ( m e s s a g e ) ;

/ / Получаем к - в о от пр ав л енн ых б а й т о в
in t bytesSent = so ck . Sen d(data);
/ / Получаем о т в е т от с е р в е р а , b y t e s R e c - к - в о принятых б а й т о в
in t bytesRec = s o c k .R e c e iv e (b y te s );
C o n s o l e . W r i t e L i n e ( " ХпОтвет с е р в е р а :
E n c o d i n g . U T F 8 . G e t S t r i n g ( b y t e s , 0, b y t e s R e c ) ) ;

(0)\n\n",

/ / Вызываем C o m m u n i c a t e () еще
i f ( m e s s a g e . I n d e x O f ( " < T h e E n d > " ) == - 1)
Communicate(hostname, p o r t ) ;
/ / Освобождаем с о к е т
sock.Shutdown(SocketShutdown. B o th ) ;
so c k .C lo se ();

)
)
)
Теперь посмотрим, что у нас получилось (рис. 12.1). Запустите сначала сер­
вер, а затем - клиент. Введите строку, которую вы хотите передать серверу и
посмотрите, какой ответ вы получите от сервера. Сервер отправляет клиен­
ту количество символов в сообщении, полученном от клиента. Если клиент
передает сообщение , то это означает конец сеанса и сервер раз­
рывает соединение с клиентом.

Рис. 12.1. Процесс взаимодействия клиента и сервера

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

Рис. 12.2. Неудачная попытка подключения второго клиента

В реальном мире к одному серверу может подключаться несколько клиен­
тов и он должен успевать обслуживать все из них.
В этом разделе мы напишем многопоточный сервер. Многопоточность будет
реализована через ThreadPool. Первым делом нужно настроить ThreadPool:
/ / Максимальное к о л и ч е с т в о п о т о к о в - по 4 на п р о ц е с с о р
i n t M ax T h r e a d s C o u n t = E n v i r o n m e n t . P r o c e s s o r C o u n t * 4;
/ / Уст а нов им м ак с им а ль но е к о л и ч е с т в о р а б о ч и х п о т о к о в
T h r e a d P o o l . SetMaxThreads(MaxThreadsCount, M axThreadsCount);
/ / Уст а нов им минимальное к о л и ч е с т в о р а б о ч и х п о т о к о в
T h r e a d P o o l . S e t M i n T h r e a d s (2, 2 ) ;

Принимать новых клиентов будем в бесконечном цикле. После того как
клиент был принят, он передается в новый поток (ClientThread) с исполь­
зованием пула потоков. Бесконечный цикл будет выглядеть так:
while

(true)

{
C o n s o l e . W r i t e ("\пОжидание с о е д и н е н и я . . . " ) ;
ThreadPool. QueueUserW orkltem(Clientprocessing,
s e r v e r .A cceptTcpClient( ));
counter++;
C o n s o l e . W r i t e ("ХпСоединение №" + c o u n t e r . T o S t r i n g () + " ! " ) ;

}
Сам же сервер создается так:
I n t 3 2 p o r t = 9 59 5 ;
IPAddress localA ddr = IP A d d re ss. P a r s e ( " 1 2 7 . 0 . 0 . 1 " ) ;
s e r v e r = new T c p L i s t e n e r ( l o c a l A d d r , p o r t ) ;
s e r v e r . S t a r t ();

Метод ClientProcessing() получает запрос клиента и возвращает его в верх­
нем регистре. В отличие от предыдущего варианта, мы не будем выводить
запрос клиента на консоль, чтобы не захламлять вывод сервера - ведь кли­
ентов на этот раз будет много. Зато клиент, который мы немного изменим
для этого случая, будет выводить, как запрос, так и ответ сервера.
/ / Буфер для принимаемых данных.
B y t e [] b y t e s = new B y t e [ 2 5 6 ] ;
S trin g data = nu ll;
TcpClient c l i e n t = c lie n t_ o b j
data = n u ll;

as TcpClient;

/ / Получаем информацию о т к ли е н т а
NetworkStream s tr e a m = c l i e n t . G e t S t r e a m O ;
int i;
/ / Принимаем данные о т кл и е н та
w h i l e ( ( i = s t r e a m . R e a d ( b y t e s , 0, b y t e s . L e n g t h ) )

! = 0)

{
/ / П р е о б р а з у е м данные в A SC I I s t r i n g ,
data = System .Text.Encoding.ASCII.GetString(bytes,

0, i ) ;

/ / П р е о б р аз у е м с т р о к у в вер хн ий р е г и с т р
data = data.ToU pper();

byte[]

/ / П р е о б р а з у е м полученную с т р о к у в м а с с и в б а й т
msg = S y s t e m . T e x t . E n c o d i n g . A S C I I . G e t B y t e s ( d a t a ) ;
/ / От пр ав ляем о т в е т к лие нт у
s t r e a m . W r i t e ( m s g , 0, m s g . L e n g t h ) ;

}
/ / З а к р ыв ае м с о е д и н е н и е .
c lie n t.C lo s e ();

Полный исходный код приложения-клиента приведен в листинге 12.3.
Листинг 12.3. Многопоточный сервер
using
using
using
using
using
using
using
using

System;
S y st e m .C o lle c tio n s . Generic;
System .Linq;
System .Text;
S y s t e m . 1 0;
System.Net;
System .N et. Sockets;
System .Threading;

namespace M u lt iT h r e a d S e r v e r

{
c l a s s ExampleTcpListener

{
s t a t i c v o i d M a i n ( s t r i n g [] a r g s )

{
TcpListener serv er = n u ll;
try
{
i n t MaxThreadsCount = E n vi r o n m e n t . P r o c e s s o r C o u n t * 4;
/ / Уст а нов им м а к с им а ль но е к о л и ч е с т в о р а б о ч и х п о т о к о в

...........................................................................................................................

T h r e a d P o o l . S e t M a x T h r e a d s ( M a x T h r e a d s C o u n t, M a x T h r e a d s C o u n t ) ;
/ / У стан овим минимальное к о л и ч е с т в о р а б о ч и х п о т о к о в
T h r e a d P o o l . S e t M i n T h r e a d s (2, 2 ) ;
/ / У с т а н а в л и в а е м по р т для T c p L i s t e n e r = 9 5 9 5 .
In t3 2 p o r t = 9595;
IP A d d re s s l o c a l A d d r = I P A d d r e s s . P a r s e ( " 1 2 7 . 0 . 0 . 1 " ) ;
i n t c o u n t e r = 0;
s e r v e r = new T c p L i s t e n e r ( l o c a l A d d r , p o r t ) ;
C o n so le .O u tp u tE n c o d in g = E n c o d in g . G e tE n c o d in g (8 6 6 );
C o n so le .W r ite L in e ("Конфигурация многопоточного с е р в е р а : " ) ;
C o n s o l e . W r i t e L i n e ("
IP-адрес
: 127.0.0.1");
C o n s o l e . W r i t e L i n e (" Порт
: " + p o rt.T o S trin g O );
C o n s o l e . W r i t e L i n e ("
Потоки
: " +
M axT hreadsC ount. T o S t r i n g ( ) ) ;
C o n s o le .W r i t e L in e ( " \пС ервер з а п у щ е н \п " ) ;
/ / З а п уск аем T c p L i s t e n e r и начинаем слушать к л и ен тов,
se rv e r. S ta rt ();
/ / Принимаем к л и е н то в в б ес к он е ч н ом ц и к л е,
w h ile (tru e )

{
C o n s o l e . W r i t e ("ХпОжидание с о е д и н е н и я . . .

");

T h re a d P o o l. Q u e u e U serW o rk lte m (C lie n tp ro ce ssin g ,
se rv e r.A c c e p tT c p C lie n t( ) ) ;
/ / Выводим информацию о подключении.
co u n ter++;
C o n s o l e . W r i t e ("ХпСоединение №" + c o u n t e r . T o S t r i n g () + " ! " ) ;

}
)
catch

( S o c k e t E x c e p t i o n e)

{
C o n so le .W rite L in e ("S o c k e tE x c e p tio n :

}
fin ally

{
/ / О станавливаем сер вер
s e r v e r . S to p ();

}
C o n s o l e . W r i t e L i n e ("ХпНажмите E n t e r . . . " ) ;
C o n so le . R ead ();

}

{0}",

e);

s t a t i c v o id C lie n tp r o c e s s in g ( o b je c t c lie n t_ o b j)

{
/ / Буфер для принимаемых данных.
B y t e [] b y t e s = new B y t e [ 2 5 6 ] ;
S trin g d ata = n u ll;
T cp C lien t c l i e n t = c lie n t _ o b j a s T c p C lie n t;
d ata = n u ll;
/ / Получаем информацию о т к л и е н та
N e t w o r k S tr e a m s t r e a m = c l i e n t . G e t S t r e a m ( ) ;
in t i ;
/ / Принимаем данные от клиента в цикле, пока не дойдем до конца,
w h i l e ( ( i = s t r e a m . R e a d ( b y t e s , 0, b y t e s . L e n g t h ) ) != 0)

{
/ / П р е о б р азу е м данные в A SC II s t r i n g .
d a t a = S y ste m .T e x t.E n c o d in g .A S C II .G e tS tr in g (b y te s , 0, i ) ;
/ / П р е о б р азу е м с т р о к у к в е р х н е м у р е г и с т р у ,
d a ta = d a t a . ToU pper( ) ;
/ / П р е о б р а зу е м полученную с т р о к у в м а с с и в б а й т .
b y te [] msg = S y stem .T ex t.E n co d in g .A SC II.G e tB y tes(d ata);
/ / О тправл яем данные о б р а т н о к л и е н ту
s t r e a m . W r i t e ( m s g , 0, m s g . L e n g t h ) ;

(о твет),

}
/ / З а кр ы ваем с о е д и н е н и е ,
c lie n t.C lo se ();

}
}
}
Теперь немного переделаем наш клиент. Что такое клиент? Это приложе­
ние, вызвавшее конструктор класса TcpListener. Мы переделаем наш кли­
ент так, чтобы в нем создавалось несколько объектов класса TcpListener, то
есть устанавливалось сразу несколько подключений. В результате вам не
придется вручную запускать несколько клиентов. В цикле вы будете ре­
гулировать количество клиентов и смотреть, как их обслуживает сервер.
Ведь взять тот же браузер - в его окне можно открыть несколько вкладок
и подключиться к веб-серверу. Каждая вкладка создаст новое соединение к
серверу и для сервера будет выглядеть, по сути, как отдельный клиент. То
же самое мы воссоздадим и в нашем случае. В листинге 12.4 представлена

модифицированная версия клиента, создающая несколько подключений к
нашему серверу.
Листинг 12.4. Клиент для многопоточного сервера
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g

System ;
S y ste m .C o lle c tio n s.G e n e ric ;
S y ste m .L in q ;
S y stem .T ex t;
S y s t e m .10;
S y ste m .N e t;
S y ste m .N e t.S o c k e ts;

n a m e s p a c e N e w C lie n t

{
class

Program

{
s t a t i c v o i d M a i n ( s t r i n g [] a r g s )

{
C o n s o l e . O u tputE n coding = E n c o d in g .G e tE n c o d in g (8 6 6 );
f o r ( in t i = 0; i < 5; i+ + )

{
C o n s o le .W r i t e L in e ( " \ n Соединение # " + i . T o S t r i n g ( ) + " \ n " ) ;
C o n n e c t ( " 1 2 7 . 0 . 0 . 1 " , " H e l l o W orld! # " + i . T o S t r i n g ( ) ) ;

}
C o n s o l e . W r i t e L i n e ( " \ n Нажмите E n t e r . . . " ) ;
C o n so le . R ead ();

}
s t a t i c v o id C o n n ect(S trin g s e r v e r ,

S t r in g m essage)

{
try

{
/ / С о зд аё м T c p C l i e n t .
/ / Для с о з д а н н о г о в предыдущем п р о е к т е T c p L i s t e n e r
/ / Настраиваем е г о на IP нашего се р в е р а и т о т же порт.
In t3 2 p o r t = 9595;
T c p C l i e n t c l i e n t = new T c p C l i e n t ( s e r v e r ,

p o rt);

/ / Переводим наше сообщение в ASCII, а затем в массив Byte.
B y te [] d a t a = S y s t e m .T e x t .E n c o d i n g .A S C I I .G e t B y t e s ( m e s s a g e ) ;
/ / Получаем п о т о к для ч тен и я и з а п и с и данных.
N e t w o r k S tr e a m s t r e a m = c l i e n t . G e t S t r e a m ( ) ;

/ / О тправл яем сообщение нашему с е р в е р у ,
s t r e a m . W r i t e ( d a t a , О, d a t a . L e n g t h ) ;
C o n s o le .W r it e L in e ("О тправлено: ( 0 ) " , m e s s a g e );
//

Получаем о т в е т о т с е р в е р а .

/ / Буфер для х р ан ен и я п р и н я т о го м а с с и в а b y t e s ,
d a t a = new B y t e [ 2 5 6 ] ;
/ / С тр о к а для хр ан е н и я полученных A SC II данных.
S t r i n g r e s p o n s e D a t a = S t r i n g . Em pty;
/ / Читаем первый п а к е т о т в е т а с е р в е р а .
/ / Можно ч и т а т ь в с ё сообщ ен ие.
/ / Для этого надо организовать чтение в цикле как на сервере.
I n t 3 2 b y t e s = s t r e a m . R e a d ( d a t a , 0, d a t a . L e n g t h ) ;
re sp o n se D ata = S y ste m .T e x t.E n c o d in g .A S C II.
G e t S t r i n g ( d a t a , 0, b y t e s ) ;
C o n s o l e . W r i t e L i n e ("П о л у ч ен о : ( 0 ) " , r e s p o n s e D a t a ) ;
//

З а к р ы в ае м в с ё .
stre a m .C lo se ();
c lie n t.C lo se ();

}
catch

( A r g u m e n t N u l l E x c e p t i o n e)

{
C o n so le .W rite L in e ("A rg u m e n tN u llE x c e p tio n :

{0}",

e);

)
catch

( S o c k e t E x c e p t i o n e)

{
C o n so le .W rite L in e ("S o c k e tE x c e p tio n :

(0)",

e);

}
}
}
}
В цикле вы можете установить другое количество клиентов, по умолчанию
у нас будет 5 клиентов. Метод Connect() занимается всей обработкой про­
цесса отправки запроса и получения ответа от сервера. Он подключается
к серверу, отправляет запрос, получает ответ и выводит его. Код довольно
простой и уже должен быть понятен вам.
Теперь посмотрим, что же у нас получилось. Запустите сервер и клиент. Ре­
зультат изображен на рис. 12.3. Как видите, к серверу поступило 5 соедине­
ний, все они были обработаны.

Рис. 12.3. Многопоточный сервер в работе

Глава 13.

Разработка приложений
для планшета под
управлением
Windows 10

13.1. Подготовка к созданию мобильных
приложений
С помощью Visual Studio можно разработать самые разнообразные при­
ложения - консольные приложения, классические Windows-приложения
(Windows Forms), Android-приложения и др. Конечно же, с помощью Visual
Studio можно разработать приложения, работающие на планшетах под
управлением Windows 10. Создание такого приложения и будет рассмотре­
но в этой главе.
Забегая наперед, хочется рассказать о разработке приложений под Windows
10. Мои впечатления можно описать одним словом: Восторг! У меня есть
опыт разработки Android-приложений в Android Studio. Совсем недавно
я для собственных нужд разработал простенькое приложение в Android
Studio, вычисляющее расход топлива автомобиля. Приложение очень про­
стое, но, учитывая торможение Android Studio на моем не очень слабом
компьютере, на его разработку ушел час. А вот в Visual Studio подобное
приложение было разработано за 20 минут, причем я даже успевал делать

Рис. 13.1. Средства разработчика мобильных приложений не установлены

скриншоты для этой главы. Расположение элементов на форме, удобная
разметка, простой код. В общем, я серьезно задумался, что следующее мое
Android-приложение будет написано на С#.
Однако перед началом разработки мобильных приложений нужно подго­
товить ваш компьютер к этому процессу. Откройте окно создания нового
проекта (File *■ New * Project/Solution) и выберите шаблон Visual C# »
Windows * Universal (рис. 13.1). Вы увидите, что средства разработки не
установлены. Выберите Install Universal Windows Tools и нажмите кнопку
OK. В появившемся окне нажмите кнопку Install (рис. 13.2)

Рис. 13.2. Нажмите кнопку Install

Появится окно установщика Visual Studio. Вы уже видели его при установ­
ке IDE (рис. 13.3). Нажмите кнопку Update. Если вы не хотите по оконча­
нию установки перезагружать компьютер, завершите работу Visual Studio
(рис. 13.4) и нажмите кнопку Retry.

Рис. 13.3. Нажмите кнопку Update

t

Рис. 13.4. Завершите работу Visual
Studio и нажмите кнопку Retry

Далее вам будет предложено выбрать необходимые компоненты. Нам по­
надобятся все. Просто нажмите кнопку Next (рис. 13.5). Кстати, на вашем
жестком диске понадобится примерно 9 Гб пространства - довольно много,
но ничего с этим не поделаешь. В следующем окне нажмите кнопку Update.

Рис. 13.5. Выбор устанавливаемых
компонентов

Рис. 13.6. Нажмите кнопку Update

Начнется мучительный процесс загрузки и установки программного обе­
спечения. Ожидание всегда мучительно. Время зависит от скорости вашего
интернет-соединения (рис. 13.7).

Рис. 13.7. Загрузка и установка ПО

Рис. 13.8. Установка завершена

Рано или поздно вы увидите заветное сообщение (рис. 13.8).
Запустите Visual Studio и откройте окно создания нового проекта. Теперь в
разделе Visual C# *• Windows * Universal будут новые шаблоны (рис. 13.9).

Рис. 13.9.' Окно создания нового проекта

Далее вы увидитесообщение, что нужно перевести ваше устройство в ре­
жим разработчика (рис. 13.10). Для этого нажмите ссылку settings for
developers.

Рис. 13.10. Нужно перевести ваше устройство в режим разработчика

Откроется окно Settings. Выберите режим Developer mode. В появившем­
ся окне нужно нажать Yes, чтобы подтвердить включение режима разработ­
чика.

Убедитесь, что режим разработчика включен, и закройте окно Settings. Вы
вернетесь в окно, изображенное на рис. 13.10. Нажмите кнопку ОК.
Будет создан новый проект. Вот теперь можно приступить к разработке ва­
шего первого мобильного приложения.

13.2. Проектирование графического
интерфейса
Поскольку приложение у нас будет графическим, то и разработку его сле­
дует начать с разработки графического интерфейса. В области Solution
Explorer дважды щелкните на MainPage.xaml. Откроется экран проектиро­
вания интерфейса (рис. 13.11). Обратите внимание, что вы можете редакти­
ровать разметку приложения, как графически, располагая на нем элементы,
так и с помощью редактирования XAML-файла разметки, подобно тому, как
это принято делать в Android Studio (там тоже есть два режима разработки
интерфейса). Вот только если в Android Studio лично мне удобнее работать
с XM L-файлом разметки, то в Visual Studio есть превосходный графиче­
ский редактор формы.
В верхнем левом углу выберите устройство, для которого вы проектируете
приложение, в данный момент выбран планшет с размером экрана 8" (рис.

Рис. 13.11. Режим проектирования страницы приложения

13.11). Далее откройте панель Toolbox для выбора элементов графического
интерфейса (рис. 13.12).
Чтобы вам было проще и удобнее, увеличьте масштаб страницы (Ctrl + ко­
лесико мыши). Расположите на странице компонент textBlock (рис. 13.13).
Вместо тривиального приложения “Hello, world” мы разработаем програм­
му вычисления расхода автомобиля.

t

Справа, как и при проектировании приложения Windows Forms, находится
панель Properties, где вы можете изменять свойства компонентов. Измени­
те свойство Text нашей надписи - “Километраж”. На вкладке Text (панель
Properties) установите размер шрифта -16 pt (рис. 13.14).

Р и с . 1 3 .1 4 . П а н е л ь с в о й с т в к о м п о н е н т а

Рис. 13.15. Расположение компонентов на странице

Расположите компоненты textBlock (надпись) и textBox (поле ввода) так,
как показано на рис. 13.15. Также добавьте кнопку (компонент Button) с
текстом Вычислить (свойство Content). Обратите внимание, как удобно
организовано выравнивание компонентов на страницу. Android Studio да­
леко до такого удобства. Расположение каждого компонента существенно
упрощается направляющими, которые напоминают направляющие Adobe
Photoshop.
Теперь задайте значения по умолчанию для полей ввода - 100, 10 и 10, как
показано на рис. 13.16. Для первого поля ввода установите имя (свойство
Name) - probeg, для второго - litres, для третьего - rashod.

Рис. 13.16. Значения полей по умолчанию

В листинге 13.1 приведен код файла разметки MainPage.xaml.
Листинг 13.1. Файл MainPafle.xaml.


< G r i d B a c k g r o u n d = " { T h e m e R e so u rce A p p l i c a t i o n P a g e B a c k g r o u n d
T h e m e B r u s h }" >
< T ex tB lo ck x :N a m e = "te x tB lo c k "
H o riz o n ta lA lig n m e n t= "L e ft" M a rg in = "5 5 ,1 0 ,0 ,0 "
T e x tW r a p p in g = "W r a p " T e x t = " Километраж" V e r t i c a l A l i g n m e n t = " T o p "
F o n tS iz e = "2 1 .3 3 3 "/>
< T e x tB o x x :N a m e = " p r o b e g " H o r i z o n t a l A l i g n m e n t = " L e f t "
H e i g h t = " 2 8 " M a r g i n = " 1 9 0 , 1 0 , 0 , 0 " T e x tW r a p p in g = " W r a p " T e x t = " 1 0 0 "
V e r tic a lA lig n m e n t = " T o p " W id th = "1 5 1 "/>
< T ex tB lo ck x :N a m e = "te x tB lo c k l"
H o riz o n ta lA lig n m e n t= "L e ft" M a rg in = "5 5 ,4 9 ,0 ,0 "
T e x tW r a p p in g = "W r a p " Т ехГ ="Л итр ы " V e r t i c a l A l i g n m e n t = " T o p "
F o n tS iz e = "2 1 . 3 3 3 "/>
< T e x tB o x x : N a m e = " l i t r e s " H o r i z o n t a l A l i g n m e n t = " L e f t "
H e i g h t = " 2 8 " M a r g i n = " 1 9 0 , 4 9 , 0 , 0 " T e x tW r a p p in g = " W r a p " T e x t = " 1 0 "
V e r tic a lA lig n m e n t = " T o p " W id th = "1 5 1 "/>
< T e x tB o x x : N a m e = " r a s h o d " H o r i z o n t a l A l i g n m e n t = " L e f t "
M a r g i n = " 1 9 0 , 8 6 , 0 , 0 " T e x tW r a p p in g = " W r a p " T e x t = " 1 0 "
V e r t i c a l A l i g n m e n t = ,,T op " W i d t h = " 8 6 " / >
< T ex tB lo ck x : N am e= "tex tB lock 2 "
H o riz o n ta lA lig n m en t= "L e ft" M arg in = "5 5 ,9 0 ,0 ,0 "
T e x tW r a p p in g = " W r a p " T e x t = " P a c x o f l " V e r t i c a l A l i g n m e n t = " T o p "
F o n tS iz e = "2 1 . 3 3 3 "/>
< T ex tB lo ck x : N am e= "tex tB lock 3 "
H o riz o n ta lA lig n m e n t= "L e ft" M arg in = "2 8 8 ,9 8 ,0 ,0 "
T e x tW r a p p in g = " W r a p " T e x t = ' ^ / 1 0 0 к м . " V e r t i c a l A l i g n m e n t = " T o p " / >
< B u t t o n x : N a m e = " b u t t o n " C o n te n t= " B ы ч и c л и т ь "
H o riz o n ta lA lig n m en t= "L e ft" M arg in = "5 2 ,1 3 5 ,0 ,0 "
V e rtic alA lign m en t= "T o p " C lic k = " b u t to n _ C lic k " />
< /G rid >


13.3. Написание кода приложения
Осталось написать обработчик для единственной кнопки. Дважды щелкни­
те на кнопке и введите следующий код:
d o u b le dP robeg = D o u b le .P a r s e ( p r o b e g .T e x t ) ;
d o u b le d L i t r e s = D o u b le . P a r s e ( l i t r e s . T e x t ) ;
d o u b l e d R es = 100 * d L i t r e s / d P r o b e g ;
ra sh o d .T e x t = d R es. T o S t r in g ( " # # .# # " ) ;

Сначала мы получаем значения километража и литров. Сразу используем
тип double, чтобы избежать лишнего преобразования типов. Затем мы вы­
числяем расход топлива и отображаем его в поле rashod, попутно преобра­
зовав его в тип string. Полный код приложения приведен в листинге 13.2.

Листинг 13.2. Полный код приложения
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g
u sin g

System ;
S y ste m .C o lle c tio n s.G e n e ric ;
S y ste m .10;
S y ste m .L in q ;
S y s t e m . R u n t i m e . I n t e r o p S e r v i c e s . W indowsRuntim e;
Windows. F o u n d a t i o n ;
Windows. F o u n d a t i o n . C o l l e c t i o n s ;
Windows. U I . X a m l;
W in d o w s.U I.X am l.C o n tro ls;
W in d o w s.U I. X a m l. C o n t r o l s . P r i m i t i v e s ;
W in d o w s .U I .X a m l. D a t a ;
W in d o w s .U I .X a m l. I n p u t ;
W indows. U I . X a m l.M e d i a ;
W in d o w s.U I.X am l.N a v ig a tio n ;

n a m e s p a c e A ppl

{
p u b l i c s e a l e d p a r t i a l c l a s s M ain P ag e

: Page

{
p u b l i c M a i n P a g e ()

{
t h i s . In itia liz e C o m p o n e n t ( ) ;

}
p r iv a t e v o id b u tto n _ C lic k (o b je c t sen d er,

R o u t e d E v e n t A r g s e)

{
d o u b le dProbeg = D o u b le .P a r s e ( p r o b e g .T e x t ) ;
d o u b le d L i t r e s = D o u b le . P a r s e ( l i t r e s . T e x t ) ;
d o u b l e d R es = 100 * d L i t r e s / d P r o b e g ;
ra sh o d .T e x t = d R e s.T o S trin g ( " # # .# # " ) ;

}
}
}

13.4. Компиляция и запуск приложения
Осталось откомпилировать и запустить приложение. Сначала откомпили­
руем приложение (меню Build). В результате успешной компиляции будет
выведено сообщение, изображенное на рис. 13.17.

*

Рис. 13.17. Успешная компиляция проекта

Далее нажмите кнопку Local Machine (также на ней изображена кнопка
Play зеленого цвета) для запуска приложения. На рис. 13.18 изображено
наше готовое приложение. В верхнем левом углу находятся координаты
указателя, при запуске на планшете их не будет.
Поздравляю! Вы только что создали свое первое приложение для планше­
та, работающего под управлением Windows 10!

Рис. 13.18. Готовое приложение

*

Глава 14 . Работаем с базой данны х

Visual Studio содержит компоненты для работы с базами данных Microsoft
Access и Microsoft SQL Server. В этой главе будет показано, как подклю­
читься к базе данных Microsoft Access.
Прежде чем продолжить, нужно отметить, что Visual Studio умеет подклю­
чаться к базам данных Access как в новом формате (ACCDB-файл), так и в
старом формате (M D B-файл).

14.1. Подключение источника данных
Для работы с базой данных первым делом нужно подключить источник
данных. Мы считаем, что приложение Windows Forms у вас уже создано
(рис. 14.1).

Рис. 14.1. Создание приложения Windows Forms

Поместите на форму компонент DataGridView. Вы сразу увидите область
DataGridView Задачи (рис. 14.2). Откройте список Выберите источник
данных и нажмите ссылку Добавить источник данных проекта (рис 14.3).
Есть и альтернативный способ добавления источника данных: нужно вы­
брать команду Вид > Другие окна > Источники данных. Появится ссылка
Добавить новый источник данных. При ее нажатии откроется мастер до­
бавления источника данных, который можно будет потом выбрать в обла­
сти DataGridView > Задачи.

Рис. 14.4. Мастер настройки источника данных

?

Поставщик данных .NET Framework. Обратите внимание: здесь можно
выбрать и другой источник данных, например, Microsoft SQL Server, База
данных Oracle и др. Мы же рассматриваем пример подключения к БД
Access, поэтому нам нужно выбрать .
После нажатия кнопки Продолжить нужно из списка Поставщик OLE DB
выбрать Microsoft Office 15.0 Access Data Prodiver (версия MS Office у
вас может отличаться), а в поле Имя сервера или файла введите имя файла
базы данных MS Access (рис. 14.8). Вы можете нажать кнопку Проверить

Рис. 14.7. Выбор поставщика данных

Рис. 14.8. Имя файла базы данных

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

Затем вы увидите имя базы данных, а также название провайдера данных в
строке подключения (рис. 14.9). Нажмите кнопку Далее.

Если база данных находится не в каталоге проекта, Visual Studio спросит
вас, хотите ли вы скопировать базу данных в каталог проекта (рис. 14.10).
Здесь решение принимать только вам. Удобнее, конечно, когда база данных
находится в одном каталоге с проектом, особенно это хорошо, когда вы бу­
дете создавать инсталлятор - все файлы проекта будут в одном месте, и вы
ничего не забудете.

Рис. 14.10. Добавить базу данных в каталог проекта?

Введите название подключения - под этим названием оно будет доступно в
вашем проекте - и нажмите кнопку Далее. Вам осталось выбрать таблицы
и представления, которые вы будете использовать в своей программе. Вы­
берите таблицу contacts и все ее поля (столбцы). Нажмите кнопку Готово.
Все, компонент DataGridView будет успешно добавлен в ваш проект.
Запустите программу. Да, прямо сейчас. Вы увидите, что компонент
DataGridView автоматически заполнен данными из базы данных (рис.
14.14)!

Рис. 14.13. Компонент DataGridVIew настроен

Рис. 14.14. Программа запущена

На самом деле автоматическая загрузка данных из БД осуществляется бла­
годаря вызову метода Fill() из события Load формы:
p r i v a t e v o i d F o r m l _ L o a d ( o b j e c t s e n d e r , E v en tA rg s e)

{
/ / TODO: данная ст р о к а кода п о зв о л я е т з а г р у з и т ь данные в таблицу
« m y c o n t a c t s D a t a S e t . c o n t a c t s » . При необходимости она может быть
перемещена или у д а л е н а .
t h i s . c o n ta c tsT a b le A d ap te r.F ill(th is.m y c o n tac tsD a ta S e t. c o n ta c ts ) ;

>

Но данные строки за нас написал VisualStudio.

14.2. Сохранение данных

Рис. 14.15. Пользователь редактирует строку

Программа запущена и работает. Вы можете не только просматривать, но
и редактировать строки (рис. 14.15). И заметьте: мы еще не написали ни
одной строчки кода.
Все хорошо в нашей программе, но она не сохраняет измененные данные.
Редактировать данные вы можете, а вот сохранять - нет. Давайте добавим
на форму кнопку с надписью Сохранить. Посмотрите на рис. 14.16. Кнопка

Рис. 14.16. Кнопка и служебные компоненты

уже добавлена. Ниже отображаются служебные компоненты, которые были
добавлены автоматически при добавлении источника данных:
• mycontactsDataSet - источник данных;
• contactsBindingSource - используется для привязки к источнику данных;
• contactsTableAdapter - адаптер таблицы, создается для каждой таблицы
базы данных. Если вы добавили базу данных с несколькими таблицами,
то компонент TableAdapter будет создан для каждой из таблиц.
Установите имя кнопки SaveButton. Дважды щелкните по ней, чтобы от­
крыть редактор кода и установить обработчик нажатия кнопки:
p r i v a t e v o i d b u t t o n l _ C l i c k ( o b j e c t s e n d e r , E v e n t A r g s e)
{
c o n ta c tsT a b le A d a p te r.U p d a te (m y c o n ta c tsD a ta S e t);

}
Запустите программу и попробуйте сохранить данные, закройте программу
и запустите ее снова. Убедитесь, что данные сохранены (рис. 14.17).

14.3. Изменение заголовков столбцов таблицы
Наша программа запускается, позволяет редактировать и сохранять данные.
Уже неплохо. Особенно, если учесть, что мы написали всего одну строчку

Рис. 14.17. Изменена одна строка, добавлена новая строка

кода. Но по-прежнему режут глаз названия полей таблицы. Для их редак­
тирования щелкните по компоненту DataGridView и откройте свойство
Columns (щелкните на кнопку ... напротив названия свойства). Откроется
окно редактирования столбцов таблицы (рис. 14.18).

Рис. 14.18. Редактирование столбцов таблицы

Выделите столбец и установите его свойства. Вот некоторые самые полез­
ные из них:


HeaderText - задает название столбца.



Visible - будет ли заголовок виден или нет.



Width - ширина столбца.



Resizable - можно ли изменять ширину столбца.
?



SortMode - режим сортировки:
о

NotSortable - без сортировки;

о

Automatic - автоматически;

о

Programmatic - программная сортировка (определяется
программистом).
На рис. 14.19 показано, что заголовки столбцов теперь на русском языке, а
столбец img скрыт (Visible = false).

14.4. Защита
от случайного
удаления
Для удаления записи
пользователю нужно вы­
делить запись (строку) и
нажать Delete. При этом
произойдет
событие
UserDeletingRow
ком­
понента DataGridView,
а после удаления
UserDeletedRow. В об­
работчике
события
UserDeletingRow М О Ж Рис. 14.19. Заголовки столбцов на русском
но вызвать диалог под­
тверждения
удаления
записи. В этом случае мы защитим данные от случайного удаления поль­
зователем.
Перейдите к компоненту DataGridView, откройте список событий и устано­
вите обработчик для события UserDeletingRow:
p r i v a t e v o i d d a t a G r i d V ie w l _ U s e r D e l e t in g R o w ( o b je c t s e n d e r ,
DataG ridView Row C ancelEventA rgs e)

{
D i a l o g R e s u l t d r = M e ssa g e B o x .S h o w ("Удалить з а п и с ь ? " ,
"У дал ен ие",
M e s s a g e B o x B u t t o n s . OKCancel,
M essa g eB o x Ic o n .W a rn in g ,
M essageB oxD efau ltB u tton . B u tto n 2 ) ;
i f (dr == D i a l o g R e s u l t . C a n c e l )
{

е.Cancel = true;
}

}

Рис. 14.20. Диалог при попытке удаления строки

14.5. Добавление данных
Теперь напишем код добавления данных в нашу таблицу. Сейчас добав­
ление осуществляется средствами самого DataGridView, но это несколько
неправильно, нужно знать, как запрограммировать добавление данных. За­
одно рассмотрим взаимодействие между формами в С#.
Добавьте новую форму в проект командой Проект *■ Добавить форму
Windows (рис. 14.21). Выберите Форма Windows Forms и установите имя
AddForm.es.

На новой форме разместите компоненты согласно таблице 14.1.
Таблица 14.1. Свойства компонентов
Описание
Форма

Имя компонента
AddForm

Название свойства
StartPosition
Text

Значение
CenterParent
Форма
добавления
записи

Фаска
Надписи

groupBoxI
label 1 - Iabel4

Text
Text

Добавление записи
Имя, Телефон, E-mail и

(Name)

Фото соответственно
tbName, tbPhone, tbMail,

Text
Text

tbPhoto соответственно
Добавить
Закрыть

Поля ввода
Кнопка добавления
Кнопка закрытия

AddBtn
CloseBtn

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

Рис. 14.22. Форма добавления записи

Теперь на главную форму добавьте две кнопки - Найти и Добавить. Пер­
вая будет использоваться для поиска записи (см. след, пункт), а вторая для добавления новой записи программным путем, а не через форму.
Обработчик кнопки Добавить будет выглядеть так:
AddForm a f = new A ddForm ( ) ;
a f .O w n e r = t h i s ;
a f . Sh o w () ;

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

Обратите внимание, что используется метод Show(), а не ShowDialog(),
благодаря чему родительская форма не будет заблокирована и можно бу­
дет производить в ней какие-то операции. Метод ShowDialog() блокирует
родительскую форму. Используя Show(), мы можем добавлять записи и на­
блюдать, как они появляются в сетке данных.
Теперь настало время написать обработчики для кнопок CloseBtn и AddBtn.
Начнем с CloseBtn - ее обработчик предельно прост:
p riv ate

void

C lo seB tn _C lick (o bject

sender,

EventA rgs

e)

{
C lo se ();

}
Обработчик нажатия кнопки AddBtn будет немного сложнее. Чтобы код
работал, как и ожидается, установите модификаторы доступа компонен­
тов mycontactsDataSet, contactsBindingSource, contactsTableAdapter и
dataGridViewl в Public.
Весь код, который обращается к родительской форме, нужно поместить во­
внутрь следующего оператора if:
Forml main = t h is .O w n e r
i f (m ain != n u l l )

as

Form l;

{
//

Обращаемся к р о д .

форме

}

Преимущества следующего подхода в следующем:
• Он не противоречит канонам ООП, если какой-то зануда будет перечи­
тывать ваш код, он не сможет придраться.
• Предоставляется доступ ко всем открытым (public) полям и методам
первой формы.
• Передача данных возможна в обе стороны.
Если наша сетка данных привязана к источнику данных (в нашем случае - к
БД), мы не можем просто добавить строчку методом Rows.Add():
m a i n . d a t a G r i d V i e w l . R o w s . A d d (1,

"Имя",

"111-22-33",

"em ail");

Мы получим исключение. Вместо этого нам нужно добавить данные непо­
средственно в источник данных, а затем обновить сетку данных. Сформиру­
ем добавляемую строку (запись):
D a t a R o w nRow = m a i n . m y c o n t a c t s D a t a S e t . T a b l e s [ 0 ] . N e w R o w( ) ;
i n t r c = m a i n . d a t a G r i d V i e w l . R o w C o u n t + 1;

nRow[0] = г с ;
n R o w [l] = t b N a m e .T e x t ;
nRow[2] = t b P h o n e . T e x t ;
nRow[3] = t b M a i l . T e x t ;
nRow[4] = t b P h o t o . T e x t ;
m a i n . m y c o n t a c t s D a t a S e t . T a b l e s [ 0 ] . R o w s.A d d (n R o w );

Сначала мы вызываем метод NewRow(). Здесь mycontactsDataSet - наш
набор данных, таблица contacts у нас первая (нумерация с 0), поэтому ее
идентификатор будет выглядеть как Tables[0].
Нумерация полей записи тоже начинается с 0, поэтому элементы массива
nRow[] нумеруются с 0. Мы вносим номер последней записи, увеличенный
на 1 (гс), имя, телефон, электронную почту и название файла с фото - по­
следние четыре значения получаются из полей ввода текста (TextBox).
После этого нам нужно вызвать метод Rows.Add() и передать ему сформи­
рованную нами запись. Осталось только обновить все и вся - адаптер та­
блицы, вызвать метод AcceptChanges() для самой таблицы и вызвать метод
Refresh() для сетки данных:
m a in . c o n t a c t s T a b l e A d a p t e r . U p d a te ( m a in .m y c o n t a c t s D a t a S e t . c o n t a c t s ) ;
m a i n . m y c o n t a c t s D a t a S e t . T a b l e s ( 0 ] .A c c e p tC h a n g e s ( ) ;
m a in . d a t a G r i d V ie w l . R e f r e s h ( ) ;

Работающая программа изображена на рис. 14.23. Видно, что мы только что
добавили строку в таблицу. В реальной программе вам придется очищать
поля ввода перед вставкой следующей записи. Делается это так:
t b N a m e .T e x t =

Рис. 14.23. Работа программы

Полный код обработчика кнопки Добавить приведен ниже:

p r i v a t e v o id A d d B tn _ C lic k (o b je c t s e n d e r , EventArgs e)

{
Forml main = th is.O w n er a s Forml;
i f (main != n u l l)

{

DataRow nRow = m a in .m y c o n ta c ts D a ta S e t. T a b l e s [ 0 ] .NewRowO;
i n t r c = m ain . d a ta G r id V ie w l. RowCount + 1;
nRow[0] = r c ;
nRow[l] = tbName.Text;
nRow[2] = tbPhone. T e x t ;
nRow[3] = t b M a il .T e x t ;
nRow[4] = tb P h o to . T e x t ;
m ain .m y c o n ta c ts D a ta S e t. T a b l e s [ 0 ] . Rows. Add(nRow);
m ain . c o n ta c ts T a b le A d a p t e r .U p d a te ( m a in .m y c o n t a c ts D a ta S e t. c o n t a c t s ) ;
m a i n .m y c o n t a c t s D a t a S e t .T a b l e s [ 0 ] .AcceptChanges ( ) ;
m ain . d a t a G r i d V ie w l .R e f r e s h ( ) ;
tbName.Text = « » ;
tbPh on e.Text = « » ;
t b M a il .T e x t = « » ;
tbPh oto. T ext = «»;'

}
)

14.6. Поиск данных
Рассмотрим, как можно найти данные в таблице. Есть два способа. Первый
- использовать SQL-операторы. Он подходит для очень больших таблиц, по­
скольку выполнение SQ L-операторов обычно осуществляется на сервере и
не будет нагружать компьютер клиента (пользователя). Второй - поиск по
dataGridView. Ведь данные уже записаны в ячейки сетки данных и мы мо­
жем в цикле пройтись по всем ячейкам и найти запись. Такой способ тоже
имеет право на существование и подойдет для не очень больших таблиц. Мы
рассмотрим второй способ для демонстрации использования сетки данных.
Создайте форму с именем SearchForm и расположите компоненты на ней,
так, как показано на рис. 14.24. Название текстового поля - tbStr. Обработ­
чик кнопки Закрыть будет таким же, как и в предыдущем случае:
C lo se ();

Прежде чем приступить к написанию обработчика кнопки Найти, нужно
вернуться к обработчику кнопки Найти на главной форме и написать сле­
дующий код:
S e a r c h F o r m s f = new S e a r c h F o r m ( ) ;
s f .O w n e r = t h i s ;
s f . S h o w( ) ;
t

Как и в предыдущем случае, данный код
отображает дочернюю форму. Можно
было бы сделать поле поиска, но форма
поиска может предоставить больший
функционал - вы можете добавить раз­
личные параметры поиска, которые бы
позволяли производить поиск только
по определенным столбцам или же про­
изводить поиск по целому слову. Мы
используем метод Contains(), благода­
ря чему можно производить поиск по Рис- 14.24. Создание формы поиска
части слова. Например, если в базе есть имя Антон и вы введете Ант, то
программа найдет строку, содержащую имя Антон.
Обработчик кнопки поиска приведен ниже:
p r i v a t e v o i d F i n d _ C l i c k ( o b j e c t s e n d e r , E v en tA rgs e)

{

Forml main = t h is .O w n e r a s Form l;
i f (main != n u l l )

{
fo r

( i n t i = 0; i < m ain .d ata G rid V iew l.R ow C ou n t; i+ + )

{

m ain .d ata G rid V ie w l.R o w s[i].S e le c te d = f a l s e ;
f o r (in t j = 0; j < main.dataGridViewl.ColumnCount; j++)
i f (m a in .d a t a G r id V ie w l.R o w s [i] .C e lls[j].V a lu e != n u ll)
i f (m ain .d ataG rid V ie w l.R o w s[i].C e lls[j].V a lu e .
T o S trin g O .C o n ta in s(tb S tr.T e x t))

{

)

m ain .d ata G rid V iew l.R o w s[i].S e le cted = tr u e ;
break;

}

}
}
Код, в принципе, очень простой. Мы проходимся по всем ячейкам, и если
было найдено совпадение, мы выделяем соответствующую строку и преры­
ваем цикл.
В завершение этой главы вам нужно модифицировать форму поиска и до­
бавить переключатели и списки, позволяющие уточнить критерии поиска,
а именно:
• Добавить возможность выбора поля (столбца), по которому
осуществляется поиск.
• Добавить возможность поиска не первой встреченной записи, а
продолжить поиск по принципу функции «Найти далее» в текстоI
вом редакторе.

*

на примерах
4-е издание

Группа подготовки издания:

Зав. редакцией компьютерной литературы: М. В. Финков
Редактор: Е. В. Финков
Корректор: А. В. Громова

ООО "Наука и Техника"
Лицензия №000350 от 23 декабря 1999 года.
192029, г. Санкт-Петербург, пр. Обуховской обороны, д. 107.
Подписано в печать 01.04.2019. Формат 70x100 1/16.
Бумага газетная. Печать офсетная. Объем 20 п. л.
Тираж 1400. Заказ 3944.

Отпечатано с готовых файлов заказчика
в АО "Первая Образцовая типография"
филиал "УЛЬЯНОВСКИЙ ДОМ ПЕЧАТИ"
432980, г. Ульяновск, ул. Гончарова, 14.