Массивы
Массивы массивов
Еще одним видом массивов C# являются массивы массивов, называемые также изрезанными массивами (jagged arrays). Такой массив массивов можно рассматривать как одномерный массив, его элементы являются массивами, элементы которых, в свою очередь снова могут быть массивами, и так может продолжаться до некоторого уровня вложенности.
В каких ситуациях может возникать необходимость в таких структурах данных? Эти массивы могут применяться для представления деревьев, у которых узлы могут иметь произвольное число потомков. Таковым может быть, например, генеалогическое дерево. Вершины первого уровня - Fathers, представляющие отцов, могут задаваться одномерным массивом, так что Fathers[i] - это i-й отец. Вершины второго уровня представляются массивом массивов - Children, так что Children[i] - это массив детей i-го отца, а Children[i][j] - это j-й ребенок i-го отца. Для представления внуков понадобится третий уровень, так что GrandChildren [i][j][k] будет представлять к-го внука j-го ребенка i-го отца.
Есть некоторые особенности в объявлении и инициализации таких массивов. Если при объявлении типа многомерных массивов для указания размерности использовались запятые, то для изрезанных массивов применяется более ясная символика - совокупности пар квадратных скобок; например, int[][] задает массив, элементы которого - одномерные массивы элементов типа int.
Сложнее с созданием самих массивов и их инициализацией. Здесь нельзя вызвать конструктор new int[3][5], поскольку он не задает изрезанный массив. Фактически нужно вызывать конструктор для каждого массива на самом нижнем уровне. В этом и состоит сложность объявления таких массивов. Начну с формального примера:
//массив массивов - формальный пример //объявление и инициализация int[][] jagger = new int[3][] { new int[] {5,7,9,11}, new int[] {2,8}, new int[] {6,12,4} };
Массив jagger имеет всего два уровня. Можно считать, что у него три элемента, каждый из которых является массивом. Для каждого такого массива необходимо вызвать конструктор new, чтобы создать внутренний массив. В данном примере элементы внутренних массивов получают значение, будучи явно инициализированы константными массивами. Конечно, допустимо и такое объявление:
int[][] jagger1 = new int[3][] { new int[4], new int[2], new int[3] };
В этом случае элементы массива получат при инициализации нулевые значения. Реальную инициализацию нужно будет выполнять программным путем. Стоит заметить, что в конструкторе верхнего уровня константу 3 можно опустить и писать просто new int[][]. Самое забавное, что вызов этого конструктора можно вообще опустить, он будет подразумеваться:
int[][] jagger2 = { new int[4], new int[2], new int[3] };
Но вот конструкторы нижнего уровня необходимы. Еще одно важное замечание - динамические массивы возможны и здесь. В общем случае, границы на любом уровне могут быть выражениями, зависящими от переменных. Более того, допустимо, чтобы массивы на нижнем уровне были многомерными. Но это уже "от лукавого", вряд ли стоит пользоваться такими сложными структурами данных, ведь с ними предстоит еще и работать.
Приведу теперь чуть более реальный пример, описывающий простое генеалогическое дерево, которое условно назову "отцы и дети":
/// <summary> /// массив массивов -"Отцы и дети" /// </summary> public void GenTree() { int Fcount = 3; string[] Fathers = new string[Fcount]; Fathers[0] = "Николай"; Fathers[1] = "Сергей"; Fathers[2] = "Петр"; string[][] Children = new string[Fcount][]; Children[0] = new string[] {"Ольга", "Федор"}; Children[1] = new string[] {"Сергей", "Валентина", "Ира", "Дмитрий"}; Children[2] = new string[] {"Мария", "Ирина", "Надежда"}; Arrs.PrintAr3(Fathers, Children); }
Здесь отцов описывает обычный динамический одномерный массив Fathers. Для описания детей этих отцов необходим уже массив массивов, который также является динамическим на верхнем уровне, поскольку число его элементов совпадает с числом элементов массива Fathers. Здесь показан еще один способ создания таких массивов. Вначале конструируется массив верхнего уровня, содержащий ссылки со значением void. А затем на нижнем уровне конструктор создает настоящие массивы в динамической памяти, с которыми и связываются ссылки.
Я не буду демонстрировать работу с генеалогическим деревом, ограничусь лишь печатью этого массива. Здесь есть несколько поучительных моментов. В классе Arrs для печати массива создан специальный метод PrintAr3, которому в качестве аргументов передаются массивы Fathers и Children. Вот текст данной процедуры:
/// <summary> /// Печать дерева "Отцы и дети", /// заданного массивами Fathers и Children /// </summary> /// <param name="Fathers">массив отцов</param> /// <param name="Children"> массив массивов детей</param> public static void PrintAr3(string[] Fathers, string[][] Children) { for (int i = 0; i < Fathers.Length; i++) { Console.WriteLine("Отец : {0}; Его дети:", Fathers[i]); for (int j = 0; j < Children[i].Length; j++) Console.Write(Children[i][j] + " "); Console.WriteLine(); } }//PrintAr3
Приведу некоторые комментарии к этой процедуре.
- Внешний цикл по i организован по числу элементов массива Fathers. Заметьте, здесь используется свойство Length, в отличие от ранее применяемого метода GetLength.
- В этом цикле с тем же успехом можно было бы использовать и имя массива Children. Свойство Length для него возвращает число элементов верхнего уровня, совпадающее, как уже говорилось, с числом элементов массива Fathers.
- Во внутреннем цикле свойство Length вызывается для каждого элемента Children[i], который является массивом.
- Остальные детали, надеюсь, понятны.
Приведу вывод, полученный в результате работы процедуры PrintAr3.
Процедуры и массивы
В наших примерах массивы неоднократно передавались процедурам в качестве входных аргументов и возвращались в качестве результатов. Остается подчеркнуть только некоторые детали.
- В процедуру достаточно передавать только сам объект - массив. Все его характеристики (размерность, границы) можно определить, используя свойства и методы этого объекта.
- Когда массив является выходным аргументом процедуры, как аргумент C в процедуре MultMatr, выходной аргумент совсем не обязательно снабжать ключевым словом ref или out (хотя и допустимо). Передача аргумента по значению в таких ситуациях так же хороша, как и передача по ссылке. В результате вычислений меняется сам массив в динамической памяти, а ссылка на него остается постоянной. Процедура и ее вызов без ключевых слов выглядит проще, поэтому обычно они опускаются. Заметьте, в процедуре GetSizes, где определялись границы массива, ключевое слово out, сопровождающее аргументы, совершенно необходимо.
- Функция может возвращать массив в качестве результата.
Алгоритмы и задачи
Алгоритмы и задачи, рассматриваемые в этой главе, являются частью фундамента, на котором строится образование программиста. Нет ни одной проблемной области, в задачах которой не требовались бы массивы. Поэтому задачи, требующие использования массивов, появлялись уже в предыдущих главах, появятся они и в последующих. Но здесь мы будем заниматься ими целенаправленно.
Последовательность элементов - - одна из любимых структур в математике. Последовательность можно рассматривать как функцию , которая по заданному значению индекса элемента возвращает его значение. Эта функция задает отображение , где - это тип элементов последовательности. В программировании последовательности это одномерные массивы, но от этого они не перестают быть менее любимыми.
Определение. Массив - это упорядоченная последовательность элементов одного типа. Порядок элементов задается с помощью индексов.
В отличие от математики, где последовательность может быть бесконечной, массивы всегда имеют конечное число элементов. Для программистов важно то, как массивы хранятся в памяти. Массивы занимают непрерывную область памяти, поэтому, зная адрес начального элемента массива, зная, сколько байтов памяти требуется для хранения одного элемента, и зная индекс (индексы) некоторого элемента, нетрудно вычислить его адрес, а значит, и хранимое по этому адресу значение элемента. На этом основана адресная арифметика в языках C и C++, где адрес элемента a(i) задается адресным выражением a+i, в котором имя массива a воспринимается как адрес первого элемента. При вычислении адреса i-го элемента индекс i умножается на длину слова, требуемого для хранения элементов типа T. Адресная арифметика использует 0-базируемость элементов массива, полагая индекс первого элемента равным нулю, поскольку первому элементу соответствует адресное выражение а+0.
Язык C# сохранил 0-базируемость массивов. Индексы элементов массива в языке C# изменяются в плотном интервале значений от нижней границы, всегда равной 0, до верхней границы, которая задана динамически вычисляемым выражением, возможно, зависящим от переменных. Массивы C# являются 0-базируемыми динамическими массивами. Это важно понимать с самого начала.
Не менее важно понимать и то, что массивы C# относятся к ссылочным типам.
Ввод-вывод массивов
Как у массивов появляются значения, как они изменяются? Возможны три основных способа:
- вычисление значений в программе;
- значения вводит пользователь;
- связывание с источником данных.
В задачах этого раздела ограничимся пока рассмотрением первых двух способов. Первый способ более или менее понятен. Простые примеры его применения приводились неоднократно. Стоит только отметить, что в классе, работающем с массивами, всегда полезно иметь метод FillArray, позволяющий заполнять массив случайными числами. В примерах использование возможностей класса Random для моделирования элементов массива встречалось неоднократно.
Приведу некоторые рекомендации по вводу и выводу массивов, ориентированные на работу с конечным пользователем.
Для консольных приложений ввод массива обычно проходит несколько этапов:
- ввод размеров массива;
- создание массива;
- организация цикла по числу элементов массива, в теле которого выполняется:
- приглашение к вводу очередного элемента;
- ввод элемента;
- проверка корректности введенного значения.
Вначале у пользователя запрашиваются размеры массива, затем создается массив заданного размера. В цикле по числу элементов организуется ввод значений. Вводу каждого значения предшествует приглашение к вводу с указанием типа вводимого значения, а при необходимости - и диапазона, в котором должно находиться требуемое значение. Поскольку ввод значений - это ответственная операция, а на пользователя никогда нельзя положиться, после ввода часто организуется проверка корректности введенного значения. При некорректном задании значения элемента ввод повторяется, пока не будет достигнут желаемый результат.
При выводе массива на консоль обычно вначале выводится имя массива, а затем его элементы в виде пары: <имя> = <значение> (например, f[5] = 77,7). Задача осложняется для многомерных массивов, когда пользователю важно видеть не только значения, но и структуру массива, располагая строку массива в строке экрана.
Как организовать контроль ввода? Наиболее разумно использовать для этих целей конструкцию охраняемых блоков - try - catch блоков. Это общий подход, когда все опасные действия, связанные с работой пользователя, внешних устройств, внешних источников данных, размещаются в охраняемых блоках.
Как правило, для ввода-вывода массивов пишутся специальные процедуры, вызываемые в нужный момент.
Ввод-вывод массивов в Windows-приложениях
Приложения Windows позволяют построить дружелюбный интерфейс пользователя, облегчающий работу по вводу и выводу массивов. И здесь, когда данные задаются пользователем, заполнение массива проходит через те же этапы, что рассматривались для консольных приложений. Но выглядит все это более красиво, наглядно и понятно. Пример подобного интерфейса, обеспечивающего работу по вводу и выводу одномерного массива, показан на рис. 6.4.
Пользователь вводит в текстовое окно число элементов массива и нажимает командную кнопку "Создать массив", обработчик которой создает массив заданной размерности, если корректно задан размер массива, в противном случае выдает сообщение об ошибке и ждет корректного ввода.
В случае успешного создания массива пользователь может переходить к следующему этапу - вводу элементов массива. Очередной элемент массива вводится в текстовое окно, а обработчик командной кнопки "Ввести элемент" обеспечивает передачу значения в массив. Корректность ввода контролируется и на этом этапе, проверяя значение введенного элемента и выводя в специальное окно сообщение в случае его некорректности, добиваясь, в конечном итоге, получения от пользователя корректного ввода.
Для облегчения работы пользователя выводится подсказка, какой именно элемент должен вводить пользователь. После того, как все элементы массива введены, окно ввода становится недоступным для ввода элементов. Интерфейс формы позволяет многократно создавать новый массив, повторяя весь процесс.
На рис. 6.4 форма разделена на две части - для ввода и вывода массива. Крайне важно уметь организовать ввод массива, принимая данные от пользователя. Не менее важно уметь отображать существующий массив в форме, удобной для восприятия пользователя. На рисунке показаны три различных элемента управления, пригодные для этих целей, - ListBox, CheckedListBox и ComboBox. Как только вводится очередной элемент, он немедленно отображается во всех трех списках.
В реальности отображать массив в трех списках, конечно, не нужно, это сделано только в целях демонстрации возможностей различных элементов управления. Для целей вывода подходит любой из них, выбор зависит от контекста и предпочтений пользователя. Элемент ComboBox имеет дополнительное текстовое окно, в которое пользователь может вводить значение. Элемент CheckedListBox обладает дополнительными свойствами в сравнении с элементом ListBox, позволяя отмечать некоторые элементы списка (массива). Отмеченные пользователем элементы составляют специальную коллекцию. Эта коллекция доступна, с ней можно работать, что иногда весьма полезно. Чаще всего для вывода массива используется элемент ListBox.
Посмотрим, как это все организовано программно. Начну с полей формы OneDimArrayForm, показанной на рис. 6.4:
//fields int n = 0; double[] mas; int currentindex = 0; double ditem = 0; const string SIZE = "Корректно задайте размер массива!"; const string INVITE = "Введите число в формате m[,n]"; const string EMPTY = "Массив пуст!"; const string ITEMB = "mas["; const string ITEME = "] = "; const string FULL = "Ввод недоступен!"; const string OK = "Корректный ввод!"; const string ERR = "Ошибка ввода числа! Повторите ввод!";
Полями этого класса является одномерный массив, его размер, текущий индекс и константы, используемые в процессе диалога с пользователем. Обработчик события Click командной кнопки, отвечающей за создание массива, имеет вид:
private void buttonCreateArray_Click(object sender, EventArgs e) { try { n = Convert.ToInt32(textBoxN.Text); mas = new double[n]; labelInvite.Text = INVITE; labelItem.Text = ITEMB + "0" + ITEME; labelResult.Text = EMPTY; textBoxItem.ReadOnly = false; listBox1.Items.Clear(); comboBox1.Items.Clear(); checkedListBox1.Items.Clear(); comboBox1.Items.Clear(); currentindex = 0; } catch (Exception) { labelResult.Text = SIZE; } }
Первым делом принимается размер массива, введенный пользователем. Преобразование к типу int введенного значения помещено в охраняемый блок, поэтому ошибки некорректного ввода будут перехвачены с выдачей соответствующего сообщения. Если же массив успешно создан, то инициализируются начальными значениями все элементы интерфейса, участвующие в вводе элементов массива. Рассмотрим, как устроен ввод элементов.
private void buttonAddItem_Click(object sender, EventArgs e) { //Заполнение массива элементами if (GetItem()) { mas[currentindex] = ditem; listBox1.Items.Add(mas[currentindex]); checkedListBox1.Items.Add(mas[currentindex]); comboBox1.Items.Add(mas[currentindex]); currentindex++; labelItem.Text = ITEMB + currentindex + ITEME; textBoxItem.Text = ""; labelResult.Text = OK; if (currentindex == n) { labelInvite.Text = ""; labelItem.Text = ""; labelResult.Text = FULL; textBoxItem.Text = ""; textBoxItem.ReadOnly = true; } } }
Функция GetItem вводит значение очередного элемента. Если пользователь корректно задал его значение, то элемент добавляется в массив, а заодно и в списки, отображающие текущее состояние массива. Создается подсказка для ввода следующего элемента массива, а если массив полностью определен, то форма переходит в состояние окончания ввода.
/// <summary> /// Ввод с контролем текущего элемента массива /// </summary> /// <returns>true в случае корректного ввода значения</returns> bool GetItem() { string item = textBoxItem.Text; bool res = false; if (item == "") labelResult.Text = INVITE; else { try { ditem = Convert.ToDouble(item); res = true; } catch(Exception) { labelResult.Text = ERR; } } return res; }
Форму OneDimArrayForm можно рассматривать как некоторый шаблон, полезный при организации ввода и вывода одномерных массивов.