Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции по ООП (язык C#).pdf
Скачиваний:
190
Добавлен:
16.05.2015
Размер:
1.54 Mб
Скачать

Чернов Э. А.

- 88 -

Лекции по языку C# v 2.3

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

 

 

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

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

Интерфейсы IComparer<T> и IComparable<T> являются точными аналогами интерфейсов IEqualityComparer<T> и IEquatable<T>, но эти интерфейсы применяются для проверки на равенство, а не для установки порядка следования элементов.

Коллекции

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

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

Контейнеры можно разделить на две группы:

Последовательные контейнеры.

Ассоциативные множества (словари).

Ниже указаны основные последовательные контейнеры:

Чернов Э. А.

- 89 -

Лекции по языку C# v 2.3

Список ArrayList необобщенный список. Объявляется без лексемы обобщения

(<T>).

Список List -обобщенный вариант класса ArrayList и

Связный список (класс Linkedlist).

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

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

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

Для всех коллекций вводится понятие перечисляемого типа данных, который поддерживается как в необобщенных интерфейсах IEnumerator и IEnumerable, так и в обобщенных интерфейсах IEnumerator<T> и IEnumerable<T>. Перечисляемый тип данных предоставляет возможность доступа к элементам коллекции. В каждой коллекции должен быть реализован обобщенный или необобщенный интерфейс IEnumerable, поэтому элементы любого класса коллекции должны быть доступны посредством методов, определенных в интерфейсе IEnumerator или IEnumerator<T>. Это означает, что, внеся минимальные изменения в код циклического обращения к коллекции одного типа, его можно использовать для аналогичного обращения к коллекции другого типа.

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

Ниже приведен пример программы для обработки обобщенного списка List. Элементом списка является класс List, содержащий 4 поля. В программе рассмотрено:

Создание списка.

Чернов Э. А.

- 90 -

Лекции по языку C# v 2.3

Просмотр списка (вывод на экран).

Сортировка списка (по умолчанию).

Сортировка списка по заданному полю.

Поиск элемента с заданным значением поля.

Подсчет количества элементов с заданным значением поля.

Фильтрация элементов списка.

Список создается из файла, в одной строке значения для одного элемента списка.

class Program

{

class Block: IComparable<Block> // Подключение интерфейса

{ // Описание класса

public string prof { get; set; } public string name { get; set; } public int skil;

public double wage;

public Block() { } // Пустой конструктор

public Block(string p, string s, int k, double w) // Конструктор с параметрами

{

prof = p; name = s; skil = k; wage = w;

}

public int CompareTo(Block obj)

{

Block blk;

blk = (Block) obj;

int n = this.prof.CompareTo(blk.prof); // Сравнение по полю prof return n;

}

public class sortOnSkill : IComparer<Block> // Для переключения поля

{ // Имя sortOnSkil произвольное, для каждого поля сравнения свое имя public int Compare(Block a, Block b)

{

if (a.skil > b.skil) return 1; // Сравнение по полю skil else if (a.skil < b.skil) return -1;

else return 0;

}

}

}

static void Main(string[] args)

{ // Файл поместить в папку Debug, либо прописывать полный путь

StreamReader file =

new StreamReader("C:\\Temp\\blocks.txt", Encoding.Default);

string s; int sk; double wg; string[ ] str;

Чернов Э. А.

- 91 -

Лекции по языку C# v 2.3

Block blk = new Block(); List<Block> lst = new List<Block>() ; // Формирование списка из файла while ((s = file.ReadLine()) != null)

{

str = s.Split(' '); // Расщепление строки по пробелам (str - массив) sk = int.Parse(str[2]); // Преобразование строки в число

wg = double.Parse(str[3]);

lst.Add(new Block(str[0],str[1], sk, wg)); // Добавить в блок

}

Console.WriteLine("Начальный список");

foreach (Block bl in lst) // Вывод начального списка Console.WriteLine("{0,-12}\t{1,-8}\t{2}\t{3}", bl.prof, bl.name, bl.skil, bl.wage); Console.WriteLine("\n Сортировка по профессии");

lst.Sort();

// Сортировка по умолчанию

foreach (Block bl in lst)

// Вывод сортированного списка

Console.WriteLine("{0,-12}\t{1,-8}\t{2}\t{3}",

bl.prof, bl.name, bl.skil,bl.wage); Console.WriteLine("\nСортировка по квалификации");

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

IComparer<Block> comp = new ArrayLst.Program.Block.sortOnSkill(); lst.Sort(comp);

foreach (Block bl in lst) // Вывод сортированного списка

Console.WriteLine("{0,-12}\t{1,-8}\t{2}\t{3}", bl.prof, bl.name, bl.skil, bl.wage); Console.WriteLine("\nПоиск");

blk = lst.Find(le => le.prof == "токарь" && le.name == "Иванов"); Console.WriteLine("{0,-12}\t{1,-8}\t{2}\t{3}",

blk.prof, blk.name, blk.skil, blk.wage); Console.WriteLine("\n Подсчет количества выбранных элементов"); int count = 0;

string fstr = blk.prof;// Блок blk был найден при поиске foreach (Block bl in lst)

{

if (bl.prof == fstr) count++;// Количество блоков с заданным элементом

}

Console.WriteLine("Количество элементов \"{0}\" = {1}",fstr, count); Console.WriteLine("\n Выборка с помощью фильтра");

// Список flst для размещения отфильтрованных блоков

List<Block> flst = new List<Block>();

double fwg = lst[1].wage;// Выбрана зарплата foreach (Block bl in lst)

{ // Фильтрация

if (bl.wage == fwg) flst.Add(new Block(bl.prof, bl.name, bl.skil, bl.wage));

}

foreach (Block bl in flst)

{ // Просмотр результатов фильтрации

Console.WriteLine("{0,-12}\t{1,-8}\t{2}\t{3}",

bl.prof, bl.name, bl.skil, bl.wage);

}

Console.ReadKey();

Чернов Э. А.

- 92 -

Лекции по языку C# v 2.3

}

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

Классы Queue и Stack также являются последовательными контейнерами, но изменяют доступ к элементам контейнера для получения специализированных списков (очередей и стеков).

Словари

Язык C# позволяет работать со словарями, содержащих в качестве элементов пары <Ключ> → <Значение>. Где «Ключ» жестко связан за следующим за ним значением. Примером может служить любой словарь иностранного языка.

Ассоциативные множества (словари) состоят из следующих типов:

Класс HashTable. (это словарь, для доступа к элементам которого выполняется преобразование ключа, и доступ к элементам выполняется с помощью этого преобразованного ключа). Обобщенный вариант класса HashTable называется

Dictionary. Имеется также класс SortedList и SortedDictionary.

Множества HashSet<T> и SortedSet<T>, содержат списки, обычно строк, например, списки для реализации IntelliSense (Подсказка для выбора компонентов при разработке программ).

Для реализации словарей существует универсальный класс Dictionary<TKey, TValue>. При реализации словаря ключ не может быть пустым. Нельзя добавить в

Чернов Э. А.

- 93 -

Лекции по языку C# v 2.3

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

Ключи в словаре должны быть уникальны. Однако, понятие уникальности требует уточнений, поскольку понятие равенства не однозначно. Так слова могут отличаться строчными и заглавными буквами. Поэтому имеются интерфейсы IEqualityComparer<T> и IEquatable<T>, которые позволяют уточнить условие равенства.

Если создан пустой объект класса Dictionary<TKey, TValue> для добавления элементов в словарь применяется метод Add. При добавлении часто требуется проверка отсутствия нового ключа. Это можно выполнить несколькими способами. Пусть объявлен пустой словарь:

Dictionary<string, string> Dic = new Dictionary<string, string>();

В этот словарь добавляются строки:

Dic.Add("кошка", "ловит мышей"); Dic.Add("собака", "сторожит дом");

Тогда проверку отсутствия нового ключа при добавлении элемента требуется:

Проверить добавление с помощью блока try-catch и обработать исключительную ситуацию ArgumentException. (в блок вставить соответствующий оператор)

Использовать индексатор и с помощью блока try-catch обработать исключи-

тельную ситуацию KeyNotFoundException. Пример добавления try

{

Dic.AddT [T"мышь"T] = T"вредитель".

}

catch (KeyNotFoundException) { действие}

Использовать метод TryGetValue для проверки существования ключа. Пример

if (Dic.TryGetValue("кошка", out value)) // Найден

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

if (!Dic.ContainsKey("корка"))

// Не найден

В примере ниже показано, как выполнить перечисление ключей и значений в словаре, используя свойства Keys и Values класса KeyValuePair. Ключами являются фамилии, причем в файле нет дублирования фамилий. (Если фамилия будет продублирована, будет выдана исключительная ситуация ArgumentException)

class Program

{

class Block

{

public string sl;

Чернов Э. А.

- 94 -

Лекции по языку C# v 2.3

public int skil; public double wage;

public Block(string s, int k, double w)

{

sl = s; skil = k; wage = w;

}

}

static void Main(string[] args)

{

StreamReader file = new StreamReader

("C:\\Temp\\blockd.txt", Encoding.Default);

string s; int sk; double wg; string[ ] str;

Dictionary<string, Block> proflst = new Dictionary<string, Block>(); while ((s = file.ReadLine()) != null)

{

str = s.Split(' ');

sk = int.Parse(str[2]);

wg = double.Parse(str[3]);

proflst.Add(str[0], new Block( str[1], sk, wg));

}

foreach (KeyValuePair<string, Block> kvp in proflst) Console.WriteLine("{0,-12}\t{1,-8}\t{2}\t{3}",

kvp.Key, kvp.Value.sl, kvp.Value.skil, kvp.Value.wage);

Console.ReadKey();

}

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

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

сок SortedList

SortedList<string, Block> proflst = new SortedList<string, Block>();

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

Если объявлена переменная типа KeyValuePair (в примере kvp), с ее помощью можно вывести на экран только ключи, либо только значения. В нашем примере «значением» является объект класса Block, поэтому для обращения к полям этого

Чернов Э. А.

- 95 -

Лекции по языку C# v 2.3

класса применяется форма kvp.Value.skil. (первая точка для обращения к свойству Value объекта kvp, вторая точка к полю класса Block).

Инициализаторы коллекций

При создании коллекций можно всегда вызывать метод Add (), например, как показано выше в примере реализации интерфейсов сравнения. Но при создании коллекции можно сократить запись, указав список инициализации, для которого компилятор сформирует автоматические вызовы метода Add (), подставляя в качестве параметров значения из этого списка. Синтаксис аналогичен инициализации массивов: после вызова пустого конструктора в фигурных скобках через запятую перечисляют значения для инициализации списка. Числа перечисляются без изменений, символы заключаются в апострофы, а строки в кавычки. Если коллекция содержит объекты класса, то для инициализации применяется список, каждый элемент которого состоит из операции new, имени_класса и в круглых скобках перечень параметров для конструктора класса. Ниже перечислены основные варианты инициализации списков (и словаря)

List <int> iLst = new List<int>() { 1, 2, 3 };

List <char> cLst = new List<char>() { 'a', 'd', 'h' };

List<string> sLst = new List<string>() {"Понедельник", "Вторник", "Среда" }; List<Product> pLst = new List<Product>() {new Product{Name="Пицца", Price=83},

new Product{Name="Багет",Price=65} };

Dictionary<string, string> d = new Dictionary<string, string>()

{

{"cat", "кошка"},{"dog", "собака"},

};

Итераторы

Для обращения к элементам списка можно реализовать интерфейсы IEnumerator и IEnumerable. В последних версиях Visual Studio эти интерфейсы, часто, реализуются автоматически компилятором. Но можно использовать итератор, который представляет собой метод, оператор или аксессор, возвращающий по очереди члены совокупности объектов от ее начала и до конца. Отличие итератора от индекса заключается в том, что итератор не связан с типом списка и позволяет обращаться к элементам связного списка так же, как и к элементам обычного массива. Более того, при создании коллекций с применением итераторов вся коллекция сразу не создается, формируется только текущий элемент.

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

Простой итератор

public static IEnumerable<int> Numbers(int start, int count)

{

for (int i = 0; i < count; ++i)

{

yield return start + i;

}

}

Чернов Э. А.

- 96 -

Лекции по языку C# v 2.3

static void Main(string[] args)

{

foreach (int i in Numbers(1, 5))

{

Console.WriteLine(i);

}

}

Итератор похож на обычный метод, но различен способ возврата значения. Итератор в примере имеет код возврата IEnumerable<int>, и не видно, чтобы он что-либо возвращал этого типа. Он не содержит обычного оператора возврата, и содержит только оператор yield return, и он возвращает одиночное число типа int, а не коллекцию. Итераторы формируют значения по одному за раз с помощью оператора yield return, и в отличие от обычного возврата метод продолжает выполняться, пока либо достигнет конца, либо будет остановлен преждевременно с помощью оператора yield break, либо возникнет исключительная операция. Каждый оператор формирует значение, которое вставляется в последовательность. В следующем примере это показано более четко, где формируются числа 1 ÷ 3.

Очень простой итератор

public static IEnumerable<int> ThreeNumbers()

{

yield return 1; yield return 2; yield return 3;

}

Хотя это простая концепция, способ реализации в чем-то отличен, поскольку итераторы не выполняются таким же способом, как и другой код. Напомним, что с помощью IEnumerable<T> вызывающая программа занята при извлечении следующего значения. Цикл foreach получает перечислитель и повторяет вызов метода MoveNext() до тех пор, пока не будет возвращено значение false и не будет обращения к свойству Current для получения текущего значения. Однако значения перечислителя не хранятся в списке типа List<T>

Ниже приведен простой пример итератора, в котором вместо явной реализации интерфейсов IEnumerator и IEnumerable применяется итератор.

// Простой пример применения итератора. class MyClass

{

string[ ] strs = { "Anchor", "Broadcast", "City", "Danger" };

// Этот итератор возвращает строки из массива strs в классе MyClass. public IEnumerator<string> GetEnumerator()

{

foreach (string st in strs) yield return st;

}

}

class Iterator

{