Предыдущая лекция | &nbsp; | Следующая лекция
:----------------:|:----------:|:----------------:
[Строки.](./4_prog_string.md) | [Содержание](../readme.md#тема-4-программирование-на-языке-c-основы) | [Общие сведения о подпрограммах.](./t5_function.md)

# Перечисления. Множества. Работа с датами. Кортежи.

* [Перечисления](#перечисления-enum)
* [Кортежи](#кортежи-tuple)
* [Множества](#множества)
* [Работа с датами и временем](#работа-с-датами-и-временем)

## Перечисления (enum)

Кроме примитивных типов данных в **C#** есть такой тип как **enum** или **перечисление**. Перечисления представляют набор логически связанных констант. Объявление перечисления происходит с помощью оператора **enum**. Далее идет название перечисления, после которого указывается тип перечисления - он обязательно должен представлять целочисленный тип (**byte**, **int**, **short**, **long**). Если тип явным образом не указан, то **по умолчанию используется тип int**. Затем идет список элементов перечисления через запятую:

```cs
enum Days
{
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}
```

```cs
enum Time : byte
{
    Morning,
    Afternoon,
    Evening,
    Night
}
```

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

```cs
enum Operation
{ 
    Add = 1,   // каждый следующий элемент по умолчанию увеличивается на единицу
    Subtract, // этот элемент равен 2
    Multiply, // равен 3
    Divide    // равен 4
}
```

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

```cs
enum Operation
{ 
    Add = 2,
    Subtract = 4,
    Multiply = 8,
    Divide = 16
}
```

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

```cs
enum Color
{
    White = 1,
    Black = 2,
    Green = 2,
    Blue = White // Blue = 1
}
```

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

```cs
enum Operation
{
    Add = 1,
    Subtract,
    Multiply,
    Divide
}

Operation op;
op = Operation.Add;
Console.WriteLine(op); // Add
    
Console.ReadLine();
```

В программе мы можем присвоить значение этой переменной. При этом в качестве её значения должна выступать одна из констант, определенных в данном перечислении. То есть несмотря на то, что каждая константа сопоставляется с определенным числом, мы не можем присвоить ей числовое значение, например, `Operation op = 1;`. И также если мы будем выводить на консоль значение этой переменной, то мы получим имя константы, а не числовое значение. Если же необходимо получить числовое значение, то следует выполнить приведение к числовому типу:

```cs
Operation op;
op = Operation.Multiply;
Console.WriteLine((int) op); // 3
                  ^^^^^ 
```

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

```cs
enum Operation
{
    Add = 1,
    Subtract,
    Multiply,
    Divide
}

// Тип операции задаем с помощью константы Operation.Add
MathOp(10, 5, Operation.Add);

// Тип операции задаем с помощью константы Operation.Multiply
MathOp(11, 5, Operation.Multiply);
        
Console.ReadLine();

static void MathOp(double x, double y, Operation op)
{
    double result = 0.0;

    switch (op)
    {
        case Operation.Add:
            result = x + y;
            break;
        case Operation.Subtract:
            result = x - y;
            break;
        case Operation.Multiply:
            result = x * y;
            break;
        case Operation.Divide:
            result = x / y;
            break;
    }

    Console.WriteLine("Результат операции равен {0}", result);
}
```

---

## Кортежи (Tuple)

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

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

```cs
var tuple = (5, 10);
```

В данном случае определен кортеж *tuple*, который имеет два значения: `5` и `10`. В дальнейшем мы можем обращаться к каждому из этих значений через поля с названиями `Item+<порядковый_номер_поля_в_кортеже>`. Например:

```cs
var tuple = (5, 10);
Console.WriteLine(tuple.Item1); // 5
Console.WriteLine(tuple.Item2); // 10
tuple.Item1 += 26;
Console.WriteLine(tuple.Item1); // 31
Console.Read();
```

В данном случае тип определяется неявно. Но мы также можем явным образом указать для переменной кортежа тип:

```cs
(int, int) tuple = (5, 10);
```

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

```cs
(string, int, double) person = ("Tom", 25, 81.23);
```

Первый элемент кортежа в данном случае представляет строку, второй элемент - тип **int**, а третий - тип **double**.

Мы также можем дать названия полям кортежа:

```cs
var tuple = (count:5, sum:10);
             ^^^^^    ^^^
Console.WriteLine(tuple.count); // 5
Console.WriteLine(tuple.sum); // 10
```

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

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

```cs
var (name, age) = ("Tom", 23);
Console.WriteLine(name);    // Tom
Console.WriteLine(age);     // 23
Console.Read();
```

### Использование кортежей

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

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

```cs
var tuple = GetValues();
Console.WriteLine(tuple.Item1); // 1
Console.WriteLine(tuple.Item2); // 3
    
Console.Read();

private static (int, int) GetValues()
{
    var result = (1, 3);
    return result;
}
```

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

Другой пример:

```cs
var tuple = GetNamedValues(new int[]{ 1,2,3,4,5,6,7});
Console.WriteLine(tuple.count);
Console.WriteLine(tuple.sum);
    
Console.Read();

private static (int sum, int count) GetNamedValues(int[] numbers)
{
    var result = (sum:0, count: 0);
    for (int i=0; i < numbers.Length; i++)
    {
        result.sum += numbers[i];
        result.count++;
    }
    return result;
}
```

И также кортеж может передаваться в качестве параметра в метод:

```cs
var (name, age) = GetTuple(("Tom", 23), 12);
Console.WriteLine(name);    // Tom
Console.WriteLine(age);     // 35
Console.Read();
         
private static (string name, int age) GetTuple((string n, int a) tuple, int x)
{
    var result = (name: tuple.n, age: tuple.a + x);
    return result;
}
```

## Множества

Коллекция, содержащая только уникальные элементы, называется множеством (**set**). В составе **.NET** имеются два множества — **HashSet`<T>`** и **SortedSet`<T>`**. Оба они реализуют интерфейс **ISet`<T>`**. Класс **HashSet`<T>`** содержит неупорядоченный список различающихся элементов, а в **SortedSet`<T>`** элементы упорядочены.

Интерфейс **ISet`<T>`** предоставляет методы для **создания**, **объединения** нескольких множеств, **пересечения** множеств и определения, является ли одно множество надмножеством или подмножеством другого.

Ниже перечислены наиболее употребительные конструкторы, определенные в классе **HashSet`<T>`**:

* `public HashSet ()`

    создается пустое множество

* `public HashSet(IEnumerable<T> collection)`

    создается множество, состоящее из элементов указываемой коллекции _collection_

* `public HashSet(IEqualityCompare comparer)`

    допускается указывать способ сравнения с помощью параметра comparer

* `public HashSet(IEnumerable<T> collection, IEqualityCompare comparer)`

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

В этом классе предоставляется также метод _RemoveWhere_, удаляющий из множества элементы, удовлетворяющие заданному условию, или предикату. Помимо свойств, определенных в интерфейсах, которые реализуются в классе **HashSet`<T>`**, в него введено дополнительное свойство _Comparer_, приведенное ниже:

```cs
public IEqualityComparer<T> Comparer { get; }
```

Оно позволяет получать метод сравнения для вызывающего хеш-множества.

Ниже перечислены четыре наиболее часто используемых конструкторов, определенных в классе SortedSet<T>:

* `public SortedSet()`

    создается пустое множество

* `public SortedSet(IEnumerable<T> collection)`

    создается множество, состоящее из элементов указываемой коллекции collection

* `public SortedSet(IComparer comparer)`

    допускается указывать способ сравнения с помощью параметра comparer

* `public SortedSet(IEnumerable<T> collection, IComparer comparer)`

    создается множество, состоящее из элементов указываемой коллекции collection, и используется заданный способ сравнения comparer

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

В этом классе предоставляется также метод _GetViewBetween_, возвращающий часть множества в форме объекта типа **SortedSet`<T>`**, метод _RemoveWhere_, удаляющий из множества элементы, не удовлетворяющие заданному условию, или предикату, а также метод _Reverse_, возвращающий объект типа **IEnumerable`<T>`**, который циклически проходит множество в обратном порядке.

Помимо свойств, определенных в интерфейсах, которые реализуются в классе **SortedSet`<T>`**, в него введены дополнительные свойства, приведенные ниже:

* `public IComparer<T> Comparer { get; }`
* `public T Max { get; }`
* `public T Min { get; }`

Свойство _Comparer_ получает способ сравнения для вызывающего множества. Свойство _Мах_ получает наибольшее значение во множестве, а свойство _Min_ — наименьшее значение во множестве. Давайте рассмотрим пример использования множеств:

```cs
// Создадим два множества
SortedSet<char> ss = new SortedSet<char>();
SortedSet<char> ss1 = new SortedSet<char>();

ss.Add('A');
ss.Add('B');
ss.Add('C');
ss.Add('Z');
ShowColl(ss, "Первая коллекция: ");

ss1.Add('X');
ss1.Add('Y');
ss1.Add('Z');
ShowColl(ss1, "Вторая коллекция");

ss.SymmetricExceptWith(ss1);
ShowColl(ss,"Исключили разноименность (одинаковые элементы) двух множеств: ");

ss.UnionWith(ss1);
ShowColl(ss, "Объединение множеств: ");

ss.ExceptWith(ss1);
ShowColl(ss, "Вычитание множеств");

Console.ReadLine();

static void ShowColl(SortedSet<char> ss, string s)
{
    Console.WriteLine(s);
    foreach (char ch in ss)
        Console.Write(ch + " ");
    Console.WriteLine("\n");
}
```

Самописанный пример, объединяющий работу перечислений и множеств:

```cs
enum Days
{
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

HashSet<Days> DaysHashSet = new HashSet<Days>();
DaysHashSet.Add(Days.Friday);
DaysHashSet.Add(Days.Friday);
DaysHashSet.Add(Days.Monday);

foreach (Days DayItem in DaysHashSet)
    Console.WriteLine(DayItem);

Console.Write("Press ENTER to continue...");
Console.ReadLine();
```

Программа выведет только `Friday` и `Monday`. Т.е. дубли в множество не добавляются и при этом никаких ошибок не возникает. Такое поведение может быть полезно при отслеживании состояния какого-то устройства. Периодически опрашивая устройство мы можем устанавливать состояния не проверя установлено уже это состояние или нет.

Для коллекций вообще и для множеств в частности есть методы объединения, пересечения и разности.

### Разность последовательностей

С помощью метода **Except** можно получить разность двух последовательностей:

```cs
var DaysHashSet1 = new HashSet<Days>() { Days.Friday, Days.Monday };
var DaysHashSet2 = new HashSet<Days>() { Days.Friday, Days.Sunday };

var ResHashSet = DaysHashSet1.Except(DaysHashSet2);

foreach (Days DayItem in ResHashSet)
    Console.WriteLine(DayItem);

Console.Write("Press ENTER to continue...");
Console.ReadLine();
```

В данном случае из множества *DaysHashSet1* убираются все элементы, которые есть в массиве *DaysHashSet2*. Результатом операции будет один элемент:

```
Monday
```

### Пересечение последовательностей

Для получения пересечения последовательностей, то есть общих для обоих наборов элементов, применяется метод _Intersect_:

```cs
string[] soft = { "Microsoft", "Google", "Apple"};
string[] hard = { "Apple", "IBM", "Samsung"};
 
// пересечение последовательностей
var result = soft.Intersect(hard);
 
foreach (string s in result)
    Console.WriteLine(s);
```

Так как оба набора имеют только один общий элемент, то соответственно только он и попадет в результирующую выборку:

```
Apple
```

### Объединение последовательностей

Для объединения двух последовательностей используется метод **Union**. Его результатом является новый набор, в котором имеются элементы, как из первой, так и из второй последовательности. Повторяющиеся элементы добавляются в результат только один раз:

```cs
string[] soft = { "Microsoft", "Google", "Apple"};
string[] hard = { "Apple", "IBM", "Samsung"};
 
// объединение последовательностей
var result = soft.Union(hard);
 
foreach (string s in result)
    Console.WriteLine(s);
```

Результатом операции будет следующий набор:

```
Microsoft
Google
Apple
IBM
Samsung
```

Если же нам нужно простое объединение двух наборов, то мы можем использовать метод **Concat**:

```cs
var result = soft.Concat(hard);
```

Те элементы, которые встречаются в обоих наборах, дублируются.

### Удаление дубликатов

Для удаления дублей в наборе используется метод **Distinct**:

```cs
var result = soft.Concat(hard).Distinct();
```

Последовательное применение методов *Concat* и *Distinct* будет подобно действию метода *Union*.

--- 

## Работа с датами и временем

Для работы с датами и временем в **.NET** предназначена структура **DateTime**. Она представляет дату и время от `00:00:00 1 января 0001 года` до `23:59:59 31 декабря 9999` года.

Для создания нового объекта **DateTime** также можно использовать конструктор. Пустой конструктор создает начальную дату:

```cs
DateTime date1 = new DateTime();
Console.WriteLine(date1); // 01.01.0001 0:00:00
```

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

```cs
Console.WriteLine(DateTime.MinValue);
```

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

```cs
DateTime date1 = new DateTime(2015, 7, 20); // год - месяц - день
Console.WriteLine(date1); // 20.07.2015 0:00:00
```

Установка времени:

```cs
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // год - месяц - день - час - минута - секунда
Console.WriteLine(date1); // 20.07.2015 18:30:25
```

Если необходимо получить текущую время и дату, то можно использовать ряд свойств **DateTime**:

```cs
Console.WriteLine(DateTime.Now);
Console.WriteLine(DateTime.UtcNow);
Console.WriteLine(DateTime.Today);
```

Консольный вывод:

```
20.07.2015 11:43:33
20.07.2015 8:43:33
20.07.2015 0:00:00
```

Свойство *DateTime.Now* возвращает текущие дату и время компьютера (локальные), *DateTime.UtcNow* - дату и время относительно времени по Гринвичу (GMT) и *DateTime.Today* - только текущую дату (время сброшено в ноль).

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

```cs
DateTime someDate = new DateTime(1582, 10, 5);
Console.WriteLine(someDate.DayOfWeek);
```

Консоль выстветит значение `Tuesday`, то есть вторник. Однако, как может быть известно из истории, впервые переход с юлианского календаря на григорианский состоялся в октябре 1582 года. Тогда после даты 4 октября (четверг) (еще по юлианскому календарю) сразу перешли к 15 октября (пятница)(уже по григорианскому календарю). Таким образом, фактически выкинули 10 дней. То есть после 4 октября шло 15 октября.

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

### Операции с DateTime

Основные операции со структурой **DateTime** связаны со сложением или вычитанием дат. Например, надо к некоторой дате прибавить или, наоборот, отнять несколько дней.

Для добавления дат используется ряд методов:

* `Add(TimeSpan value)`: добавляет к дате значение типа **TimeSpan**

* `AddDays(double value)`: добавляет к текущей дате несколько дней

* `AddHours(double value)`: добавляет к текущей дате несколько часов

* `AddMinutes(double value)`: добавляет к текущей дате несколько минут

* `AddMonths(int value)`: добавляет к текущей дате несколько месяцев

* `AddYears(int value)`: добавляет к текущей дате несколько лет

Например, добавим к некоторой дате 3 часа:

```cs
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25
Console.WriteLine(date1.AddHours(3)); // 20.07.2015 21:30:25
```

Для вычитания дат используется метод `Substract(DateTime date)`:

```cs
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25
DateTime date2 = new DateTime(2015, 7, 20, 15, 30, 25); // 20.07.2015 15:30:25
Console.WriteLine(date1.Subtract(date2)); // 03:00:00
```

Здесь даты различаются на три часа, поэтому результатом будет **TimeSpan** "03:00:00".

Метод **Substract** не имеет возможностей для отдельного вычитания дней, часов и так далее. Но это и не надо, так как мы можем передавать в метод _AddDays_ и другие методы добавления отрицательные значения:

```cs
// вычтем три часа
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25);  // 20.07.2015 18:30:25
Console.WriteLine(date1.AddHours(-3)); // 20.07.2015 15:30:25
```

### Форматирование даты

Кроме операций сложения и вычитания еще есть ряд методов форматирования дат:


```cs
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25);
Console.WriteLine(date1.ToLocalTime()); // 20.07.2015 21:30:25
Console.WriteLine(date1.ToUniversalTime()); // 20.07.2015 15:30:25
Console.WriteLine(date1.ToLongDateString()); // 20 июля 2015 г.
Console.WriteLine(date1.ToShortDateString()); // 20.07.2015
Console.WriteLine(date1.ToLongTimeString()); // 18:30:25
Console.WriteLine(date1.ToShortTimeString()); // 18:30
```

Метод _ToLocalTime_ преобразует время **UTC** в локальное время, добавляя смещение относительно времени по Гринвичу. Метод _ToUniversalTime_, наоборот, преобразует локальное время во время **UTC**, то есть вычитает смещение относительно времени по Гринвичу. Остальные методы преобразуют дату к определенному формату.

Метод _ToString_ тоже можно использовать для форматирования. По умолчанию для русской культурной среды он возвращает дату в формате `ДД.ММ.ГГГГ Ч:ММ:СС`. Но можно задать нужный формат в параметрах: 
`дата.ToString("dd.MM.yyyy HH:mm:ss");`

Описатель | Описание
:---:|----
d | Представляет день месяца от 1 до 31. Одноразрядные числа используются без нуля в начале
dd | Представляет день месяца от 1 до 31. К одноразрядным числам в начале добавляется ноль
ddd | Сокращенное название дня недели (пн,вт...)
dddd | Полное название дня недели (понедельник, вторник...)
f / fffffff | Представляет миллисекунды. Количество символов f указывает на число разрядов в миллисекундах
g | Представляет период или эру (например, "н. э.")
h | Часы в виде от 1 до 12. Часы с одной цифрой не дополняются нулем
hh | Часы в виде от 01 до 12. Часы с одной цифрой дополняются нулем
H | Часы в виде от 0 до 23. Часы с одной цифрой не дополняются нулем
HH | Часы в виде от 0 до 23. Часы с одной цифрой дополняются нулем
K | Часовой пояс
m | Минуты от 0 до 59. Минуты с одной цифрой не дополняются начальным нулем
mm | Минуты от 0 до 59. Минуты с одной цифрой дополняются начальным нулем
M | Месяц в виде от 1 до 12
MM | Месяц в виде от 1 до 12. Месяц с одной цифрой дополняется начальным нулем
MMM | Сокращенное название месяца
MMMM | Полное название месяца
s | Секунды в виде числа от 0 до 59. Секунды с одной цифрой не дополняются начальным нулем
ss | Секунды в виде числа от 0 до 59. Секунды с одной цифрой дополняются начальным нулем
t | Первые символы в обозначениях AM и PM
tt | AM или PM
y | Представляет год как число из одной или двух цифр. Если год имеет более двух цифр, то в результате отображаются только две младшие цифры
yy | Представляет год как число из одной или двух цифр. Если год имеет более двух цифр, то в результате отображаются только две младшие цифры. Если год имеет одну цифру, то он дополняется начальным нулем
yyy | Год из трех цифр
yyyy | Год из четырех цифр
yyyyy | Год из пяти цифр. Если в году меньше пяти цифр, то он дополняется начальными нулями
z | Представляет смещение в часах относительно времени UTC
zz | Представляет смещение в часах относительно времени UTC. Если смещение представляет одну цифру, то она дополняется начальным нулем.

---

## Контрольные вопросы

1. [Перечисления](#перечисления-enum)
1. [Кортежи](#кортежи-tuple)
1. [Множества](#множества)
1. [Дата и время](#работа-с-датами-и-временем)

Предыдущая лекция | &nbsp; | Следующая лекция
:----------------:|:----------:|:----------------:
[Строки.](./4_prog_string.md) | [Содержание](../readme.md#тема-4-программирование-на-языке-c-основы) | [Общие сведения о подпрограммах.](./t5_function.md)
