Прочитав эту главу, вы научитесь:
• объявлять переменные массивов;
• заполнять массив набором элементов данных;
• обращаться к элементам данных, хранящимся в массиве;
• осуществлять последовательный обход элементов данных в массиве.
Вы уже видели, как создаются и используются переменные множества различных типов. Но все встречавшиеся до сих пор примеры переменных имели одно общее свойство — они хранили информацию об одном элементе (типа int, float, Circle, Date и т.д.). А что делать, если нужно работать с набором элементов? Одним из решений будет создание переменной для каждого элемента набора, но это вызовет ряд новых вопросов: сколько переменных понадобится? Как их следует назвать? Если нужно с каждым элементом набора проделать одну и ту же операцию (например, увеличить значение каждой целочисленной переменной набора на единицу), то как тогда избежать применения повторяющегося кода? Использование переменной для отдельных элементов предполагает, что при написании программы вы знаете, сколько именно элементов потребуется. Но часто ли такое бывает? Например, если создается приложение, считывающее и обрабатывающее записи из базы данных, то сколько записей в этой базе и насколько высока вероятность изменения этого количества?
Механизм, помогающий решить эти проблемы, предоставляют массивы.
Массив представляет собой неупорядоченную последовательность элементов. В отличие от полей в структуре или классе, у которых могут быть разные типы, у всех элементов массива один и тот же тип. Элементы массива находятся в сплошном блоке памяти, и в отличие от полей в структуре или классе, доступ к которым осуществляется по именам, доступ к элементам массива получают с помощью индекса.
Переменная массива объявляется путем указания названия типа элемента, за которым стоит пара квадратных скобок, а затем имя переменной. Квадратные скобки обозначают, что переменная является массивом. Например, для объявления массива int-переменных по имени pins (для набора личных идентификаторов) можно набрать следующий код:
int[] pins; // Личные идентификаторы
ПРИМЕЧАНИЕ Если вам приходилось программировать на Microsoft Visual Basic, обратите внимание на то, что в объявлении используются не круглые, а квадратные скобки. Если вы знакомы с языками C и C++, также заметьте, что размер массива не является частью объявления. Кроме того, квадратные скобки должны стоять перед именем переменной.
В качестве элементов массива можно выбирать не только простые типы. Допускается также создание массивов структур, перечислений и классов. Например, можно создать массив Date-структур:
Date[] dates;
СОВЕТ Зачастую массивам больше подходят имена во множественном числе, такие как places (где элемент относится к типу Place), people (где элемент относится к типу Person) или times (где каждый элемент относится к типу Time).
Независимо от типов своих элементов массивы являются ссылочными типами. Это означает, что переменная массива ссылается на сплошной блок памяти, содержащий элементы массива в динамической памяти (куче). (Описание значений и ссылок, а также отличий стека от кучи можно найти в главе 8 «Основные сведения о значениях и ссылках».) Это правило действует независимо от типа данных, к которому относятся элементы массива. Даже если массив содержит значение такого типа, как int, память все равно будет выделяться в динамической области — этот как раз один из тех случаев, когда типам значений память в стеке не выделяется.
Вспомним, что при объявлении переменной класса память объекту не выделяется до тех пор, пока с помощью ключевого слова new не будет создан экземпляр этого класса. Массивы действуют по такой же схеме: когда объявляется переменная массива, вы не объявляете ее размер и ей не выделяется память, лишь в стек помещается ссылка. Память массиву выделяется только при создании экземпляра, и именно тогда указывается размер массива.
Для создания экземпляра массива используется ключевое слово new, за которым стоит тип элемента, затем указывается размер создаваемого массива, заключенный в квадратные скобки. При создании массива происходит также инициализация его элементов с использованием уже известных исходных значений (0, null или false в зависимости от того, к какому типу относятся его элементы — числовому, ссылочному или булеву соответственно). Например, чтобы создать и проинициализировать новый массив из четырех целых чисел для объявленной ранее переменной pins, нужно набрать следующий код:
pins = new int[4];
На рис. 10.1 показано, что произойдет при объявлении массива, а затем при создании его экземпляра.
Рис. 10.1
Поскольку память для экземпляра массива выделяется динамически, размер массива не обязательно должен быть постоянным — он, как показано в следующем примере, может вычисляться в ходе выполнения программы:
int size = int.Parse(Console.ReadLine());
int[] pins = new int[size];
Можно также создать массив размером 0. Как ни странно, но такой массив пригодится в тех случаях, когда размер массива определяется динамически и может быть даже равен нулю. Массив нулевого размера не является массивом пустых элементов — это массив, содержащий нуль элементов.
При создании экземпляра массива все элементы этого массива инициализируются исходным значением, зависящим от их типа. Например, для всех числовых значений устанавливается исходное значение 0, объекты и строки инициализируются в null, а для значений типа DateTime устанавливаются дата и время "01/01/0001 00:00:00". Можно изменить этот стиль поведения и инициализировать элементы массива конкретными нужными вам значениями. Это обеспечивается предоставлением списка перечисленных через запятую значений, заключенных в фигурные скобки. Например, чтобы инициализировать переменную pins массивом из четырех int-переменных со значениями 9, 3, 7 и 2, нужно набрать следующий код:
int[] pins = new int[4]{ 9, 3, 7, 2 };
Значения, указываемые в фигурных скобках, не обязательно должны быть константами, они, как показано в следующем примере, могут быть значениями, вычисляемыми в ходе выполнения программы. Код этого примера заполняет массив pins четырьмя случайными числами:
Random r = new Random();
int[] pins = new int[4]{ r.Next() % 10, r.Next() % 10,
r.Next() % 10, r.Next() % 10 };
ПРИМЕЧАНИЕ Класс System.Random представляет собой генератор псевдослучайных чисел. Метод Next по умолчанию возвращает положительное случайное целое число в диапазоне от 0 до Int32.MaxValue. Метод Next является перегружаемым, и его другие версии позволяют указывать минимальное и максимальное значения диапазона. Пассивный конструктор класса Random в качестве инициирующего значения, используемого генератором случайного числа, применяет значение, зависящее от текущего времени, что снижает возможности класса по дублированию последовательности случайных чисел. Используя перегружаемую версию конструктора, можно предоставить собственное инициирующее значение. Таким образом, в целях тестирования приложения можно будет генерировать повторяющуюся последовательность случайных чисел.
Количество значений в фигурных скобках должно в точности соответствовать размеру создаваемого экземпляра массива:
int[] pins = new int[3]{ 9, 3, 7, 2 }; // ошибка в ходе компиляции
int[] pins = new int[4]{ 9, 3, 7 }; // ошибка в ходе компиляции
int[] pins = new int[4]{ 9, 3, 7, 2 }; // компиляция завершается успешно
Когда переменная массива инициализируется именно таким образом, new-выражение и размер массива можно опустить. В таком случае компилятор вычислит размер из количества инициализирующих значений и сгенерирует код для создания массива:
int[] pins = { 9, 3, 7, 2 };
Если создается массив структур или объектов, каждую структуру в массиве можно инициализировать путем вызова конструктора структуры или класса:
Time[] schedule = { new Time(12,30), new Time(5,30) };
Тип элемента при объявлении массива должен соответствовать типу тех элементов, которые вы пытаетесь поместить в массив. Например, если, как показано в предыдущих примерах, объявляется, что pins будет массивом int-элементов, вы не можете помещать в этот массив элементы типов double, string, struct или любых других, отличных от int. Если при объявлении массива указывается список значений для инициализации, можно позволить компилятору C# вывести фактический тип элементов массива за вас:
var names = new[]{"John", "Diana", "James", "Francesca"};
В этом примере компилятор C# определяет, что переменная names является строковым массивом. В этом объявлении стоит обратить внимание на две синтаксические особенности. Во-первых, из указания типа убраны квадратные скобки — переменная names в данном случае объявлена просто как var, а не как var[]. Во-вторых, перед списком значений инициализации потребовалось указать оператор new и поставить квадратные скобки.
Если используется этот синтаксис, нужно обеспечить принадлежность всех инициализирующих значений одному и тому же типу. Следующий пример вызовет ошибку во время компиляции «No best type found for implicitly-typed array», которая означает, что при объявлении массива без явного указания типа компилятор не смог определить доминирующий тип данных:
var bad = new[]{"John", "Diana", 99, 100};
Но в некоторых случаях компилятор приводит элементы к другому типу, если в этом есть какой-то смысл. В следующем примере кода числовой массив является массивом double-элементов, поскольку константы 3.5 и 99.999 относятся к данным типа double и компилятор C# может привести целочисленные значения 1 и 2 к double-значениям:
var numbers = new[]{1, 2, 3.5, 99.999};
Все же лучше избегать смешанных типов, и не стоит тешить себя надеждой, что компилятор выполнит за вас их приведение к нужному типу.
Объявление массивов с неявным заданием типа лучше всего подходит для работы с безымянными типами, рассмотренными в главе 7 «Создание классов и объектов и управление ими». Следующий код предназначен для создания массива безымянных объектов, каждый из которых содержит два поля, определяющих имя и возраст членов моей семьи:
var names = new[] { new { Name = "John", Age = 50 },
new { Name = "Diana", Age = 50 },
new { Name = "James", Age = 23 },
new { Name = "Francesca", Age = 21 } };
Поля в безымянных типах должны быть одинаковыми для каждого элемента массива.
Для получения доступа к отдельному элементу массива следует задать индекс, показывающий, какой именно элемент вам нужен. Индексация элементов массива начинается с нуля, поэтому начальный элемент массива имеет индекс 0, а не 1. Значение индекса 1 позволяет обратиться ко второму элементу. Например, считать содержимое элемента 2 (третьего элемента) массива pins в int-переменную можно с помощью следующего кода:
int myPin;
myPin = pins[2];
Аналогично этому, назначая значение проиндексированному элементу, можно изменить содержимое массива:
myPin = 1645;
pins[2] = myPin;
Все обращения к элементам массива проходят проверку на выход за пределы его границ. Если указать индекс меньше нуля или больше длины массива или равный ей, компилятор выдаст исключение, связанное с выходом индекса массива за пределы допустимого диапазона значений, — IndexOutOfRangeException:
try
{
int[] pins = { 9, 3, 7, 2 };
Console.WriteLine(pins[4]); // ошибка, 4-й, он же последний, элемент
// имеет индекс 3
}
catch (IndexOutOfRangeException ex)
{
...
}
Массивы в Microsoft .NET Framework фактически являются экземплярами класса System.Array, и в этом классе определен ряд полезных свойств и методов. Например, чтобы узнать количество содержащихся в массиве элементов, можно запросить значение свойства Length, а для последовательного обхода всех элементов массива — воспользоваться инструкцией for. В следующем простом примере кода значения элементов массива pins записываются в консоль:
int[] pins = { 9, 3, 7, 2 };
for (int index = 0; index < pins.Length; index++)
{
int pin = pins[index];
Console.WriteLine(pin);
}
ПРИМЕЧАНИЕ Length является свойством, а не методом, поэтому при его вызове круглые скобки не используются. Свойства будут рассматриваться в главе 15 «Реализация свойств для доступа к полям».
Начинающие программисты склонны забывать, что массивы начинаются с элемента 0, а номер последнего элемента вычисляется с помощью выражения Length – 1. В C# предоставляется инструкция foreach, с помощью которой можно выполнять последовательный обход всех элементов массива, не задумываясь об этих обстоятельствах. Вот как, к примеру, выглядит предыдущая инструкция for, переписанная в виде ее эквивалента, использующего инструкцию foreach:
int[] pins = { 9, 3, 7, 2 };
foreach (int pin in pins)
{
Console.WriteLine(pin);
}
Инструкция foreach объявляет переменную итерации (в данном примере это int pin), которая автоматически получает значение каждого элемента массива. Тип этой переменной должен соответствовать типу элементов массива. Использование инструкции foreach является предпочтительным способом последовательного обхода всех элементов массива: с ее помощью предназначение кода выражается напрямую, что позволяет отказаться от всего оснащения, присущего циклу for. Но в некоторых случаях потребность возвратиться к использованию инструкции for все же будет возникать.
• Инструкция foreach всегда выполняет итерацию в отношении всего массива. Если нужно обойти только известную часть массива (например, первую половину) или пропустить конкретные элементы (например, каждый третий), проще воспользоваться инструкцией for.
• Инструкция foreach всегда выполняет итерацию от индекса 0 до индекса Length – 1. Если нужно обойти элементы массива в обратном порядке или в какой-то иной последовательности, проще воспользоваться инструкцией for.
• Если телу цикла необходимо знать значение индекса элемента, а не просто его значение, следует воспользоваться инструкцией for.
• Если нужно изменить элементы массива, придется воспользоваться инструкцией for. Дело в том, что переменная итерации инструкции foreach является копией каждого элемента массива, предназначенной только для чтения.
СОВЕТ Пытаться выполнять итерацию в отношении массива нулевой длины с помощью инструкции foreach абсолютно безопасно.
Можно объявить переменную итерации с помощью ключевого слова var и позволить компилятору C# самому определить тип переменной на основе типа элементов массива. Особую пользу от этого можно извлечь, если вам неизвестен тип элементов массива, например, в том случае, когда массив содержит безымянные объекты. В следующем примере показано, как можно выполнить последовательный обход элементов массива членов семьи, который был показан ранее:
var names = new[] { new { Name = "John", Age = 50 },
new { Name = "Diana", Age = 50 },
new { Name = "James", Age = 23 },
new { Name = "Francesca", Age = 21 } };
foreach (var familyMember in names)
{
Console.WriteLine($"Name: {familyMember.Name}, Age: {familyMember.Age}");
}
В C# вполне допустимо определять методы, принимающие массивы в качестве параметров или передающие их обратно в качестве возвращаемых значений.
Синтаксис для передачи массива в качестве параметра во многом похож на тот, который используется для объявления массива. Например, в следующем фрагменте кода определяется метод по имени ProcessData, получающий в качестве параметра массив целочисленных значений. Тело метода последовательно обходит элементы массива и выполняет некую неуказанную обработку каждого элемента:
public void ProcessData(int[] data)
{
foreach (int i in data)
{
...
}
}
Важно помнить, что массивы являются ссылками на объекты, поэтому при изменении внутри такого метода, как ProcessData, содержимого массива, переданного ему в качестве параметра, изменение можно проследить через все ссылки на массив, включая исходный аргумент, принимаемый методом в качестве параметра.
Для возвращения массива из метода нужно в качестве типа возвращаемого значения указать тип массива. Массив создается и заполняется в методе. В следующем примере пользователю предлагается ввести размер массива, а затем данные для каждого элемента. Массив, созданный методом, передается назад в качестве возвращаемого значения:
public int[] ReadData()
{
Console.WriteLine("How many elements?");
string reply = Console.ReadLine();
int numElements = int.Parse(reply);
int[] data = new int[numElements];
for (int i = 0; i < numElements; i++)
{
Console.WriteLine($"Enter data for element {i}");
reply = Console.ReadLine();
int elementData = int.Parse(reply);
data[i] = elementData;
}
return data;
}
Метод ReadData можно вызвать с помощью следующего кода:
int[] data = ReadData();
Параметры в виде массива и метод Main
Вероятно, вы заметили, что имеющийся в приложении метод Main получает в качестве параметра массив строк:
static void Main(string[] args)
{
...
}
Вспомним, что метод Main вызывается в самом начале выполнения программы и является точкой входа вашего приложения. Если запускать приложение из окна командной строки, ему можно указать дополнительные аргументы командной строки. Операционная система Windows передает эти аргументы общеязыковой среде выполнения (common language runtime (CLR)), которая в свою очередь передает их в качестве аргументов методу Main. Этот механизм дает вам простой способ, позволяющий пользователю предоставлять информацию при запуске приложения, вместо создания интерактивного приглашения для пользователя на ввод данных. Польза от применения этого подхода проявляется при создании утилит, которые могут запускаться из автоматизированных сценариев.
Следующий пример взят из служебного приложения по имени MyFileUtil, занимающегося обработкой файлов. Оно ожидает получения в командной строке набора имен файлов и вызывает метод ProcessFile (который здесь не показан), обрабатывающий каждый указанный файл:
static void Main(string[] args)
{
foreach (string filename in args)
{
ProcessFile(filename);
}
}
Пользователь может запустить приложение MyFileUtil из командной строки следующим образом:
MyFileUtil C:\Temp\TestData.dat C:\Users\John\Documents\MyDoc.txt
Каждый аргумент командной строки отделен от другого аргумента пробелом. Проверка на допустимость этих аргументов возлагается на приложение MyFileUtil.
Массивы относятся к ссылочным типам (вспомним, что массив является экземпляром класса System.Array). Переменная массива содержит ссылку на экземпляр массива. Это означает, что при копировании переменной массива вы, как показано в следующем примере, получаете две ссылки на один и тот же экземпляр массива:
int[] pins = { 9, 3, 7, 2 };
int[] alias = pins; // alias и pins ссылаются на один и тот же экземпляр массива
В данном примере изменения, вносимые в значение pins[1], будут видны также при чтении alias[1].
Если нужно создать копию экземпляра массива (данных в динамической памяти), на который ссылается переменная массива, следует выполнить два действия. Во-первых, нужно создать новый экземпляр массива того же типа, что и у копируемого массива, имеющий такую же длину, как и у него. Во-вторых, нужно выполнить поэлементное копирование данных из исходного массива в новый массив:
int[] pins = { 9, 3, 7, 2 };
int[] copy = new int[pins.Length];
for (int i = 0; i < pins.Length; i++)
{
copy[i] = pins[i];
}
Заметьте, что для задания размера нового массива в этом коде используется свойство Length исходного массива.
Копирование массива является весьма частым требованием многих приложений, настолько частым, что класс System.Array предоставляет ряд полезных методов, которыми можно воспользоваться при проведении этой операции. Например, метод CopyTo копирует содержимое одного массива в другой массив с заданного стартового индекса. В следующем примере все элементы из массива pins, начиная с нулевого элемента, копируются в массив copy:
int[] pins = { 9, 3, 7, 2 };
int[] copy = new int[pins.Length];
pins.CopyTo(copy, 0);
Еще один способ копирования значений предполагает использование имеющегося в System.Array статического метода по имени Copy. Как и в случае использования CopyTo, перед вызовом Copy вам следует проинициализировать целевой массив:
int[] pins = { 9, 3, 7, 2 };
int[] copy = new int[pins.Length];
Array.Copy(pins, copy, copy.Length);
ПРИМЕЧАНИЕ Методу Aray.Copy обязательно нужно указать правильное значение параметра length. Если указать отрицательное значение, метод выдаст исключение ArgumentOutOfRangeException. Если указать значение, превышающее количество элементов в исходном массиве, метод выдаст исключение ArgumentException.
Еще одной альтернативой является использование имеющегося в System.Array метода экземпляра по имени Clone. Этот метод можно вызвать для создания всего массива и копирования в него данных одним действием:
int[] pins = { 9, 3, 7, 2 };
int[] copy = (int[])pins.Clone();
ПРИМЕЧАНИЕ Описание методов Clone давалось в главе 8. Метод Clone класса Array возвращает тип object, а не тип Array, поэтому при использовании этого метода вам следует выполнить приведение типа возвращаемого им значения к типу соответствующего массива. Кроме того, методы Clone, CopyTo и Copy создают поверхностную копию массива (поверхностное и углубленное копирование также описываются в главе 8). Если копируемые элементы массива содержат ссылки, метод Clone просто копирует ссылки, а не объекты, на которые они указывают. После копирования оба массива ссылаются на один и тот же набор объектов. Если нужно создать углубленную копию такого массива, следует воспользоваться соответствующим кодом в цикле for.
Массивы, показанные до сих пор, были одномерными, и их можно было бы представить в виде простого списка значений. Но допускается создание и многомерных массивов. Например, чтобы создать двумерный массив, указывается массив, требующий двух целочисленных индексов. Следующий код позволяет создать двумерный массив по имени items, состоящий из 24 целых чисел. Если это вам поможет, можете представлять себе этот массив в виде таблицы, в которой первое значение указывает номера строк, а второе — номера столбцов:
int[,] items = new int[4, 6];
Чтобы обратиться к элементу массива, предоставляются два индексных значения, указывающих на ячейку (пересечение строки и столбца), содержащую элемент. В следующем фрагменте кода показан ряд примеров использования массива items:
items[2, 3] = 99; // установка для элемента в ячейке (2, 3) значения 99
items[2, 4] = items [2,3]; // копирование элемента из ячейки (2, 3) в ячейку (2, 4)
items[2, 4]++; // увеличение целочисленного значения в ячейке (2, 4) на единицу
Для массива можно указать любое количество размерностей без каких-либо ограничений. В следующем примере кода создается и используется трехмерный массив по имени cube. Обратите внимание на то, что для обращения к каждому элементу этого массива следует указывать три индекса:
int[, ,] cube = new int[5, 5, 5];
cube[1, 2, 1] = 101;
cube[1, 2, 2] = cube[1, 2, 1] * 3;
Здесь вполне уместно предостеречь вас от создания массивов более чем с тремя размерностями. Массивы могут занимать довольно большой объем памяти. Массив cube содержит 125 элементов (5 · 5 · 5). Четырехмерный массив, в котором каждая размерность состоит из пяти индексов, содержит 625 элементов. Если вы начнете создавать массивы с тремя и более размерностями, то вскоре можете исчерпать память. Поэтому при использовании многомерных массивов вы всегда должны быть готовы к перехвату и обработке исключений, свидетельствующих об отсутствии нужного объема памяти, — OutOfMemoryException.
В C# обычные многомерные массивы иногда называют массивами в прямоугольных координатах. У каждой размерности имеется постоянная форма. Например, в следующем табличном образовании, двумерном массиве items, в каждой строке содержится 40 элементов, то есть всего 160 элементов:
int[,] items = new int[4, 40];
Как говорилось в предыдущем разделе, многомерные массивы могут расходовать слишком много памяти. Если приложение использует только некоторые данные в каждом столбце, выделение памяти под неиспользуемые элементы приводит к ее неоправданному расходованию. В данном сценарии можно воспользоваться ступенчатым массивом, в котором все столбцы имеют разную длину:
int[][] items = new int[4][];
int[] columnForRow0 = new int[3];
int[] columnForRow1 = new int[10];
int[] columnForRow2 = new int[40];
int[] columnForRow3 = new int[25];
items[0] = columnForRow0;
items[1] = columnForRow1;
items[2] = columnForRow2;
items[3] = columnForRow3;
...
В этом примере приложению требуются только 3 элемента в первом столбце, 10 элементов во втором, 40 элементов в третьем и 25 элементов в четвертом. В данном коде показан массив массивов по имени items, который, вместо того чтобы быть двумерным массивом, имеет только одну размерность, но элементы в этой размерности сами по себе являются массивами. Более того, общий размер массива items составляет 78, а не 160 элементов, и пространство под не используемые приложением элементы не выделяется.
В этом примере стоит выделить ряд синтаксических особенностей. Следующее объявление указывает, что items является массивом массивов, состоящих из int-элементов:
int[][] items;
Следующая инструкция инициализирует items на содержание четырех элементов, каждый из которых является массивом неопределенной длины:
items = new int[4][];
Все массивы с columnForRow0 до columnForRow3 являются одномерными int-массивами, инициализированными на содержание объема данных, требующегося для каждого столбца. И наконец, каждый массив столбцов присвоен соответствующим элементам в массиве items, например:
items[0] = columnForRow0;
Вспомним, что массивы являются ссылочными объектами, поэтому данная инструкция просто добавляет ссылку на columnForRow0 к первому элементу массива items, не копируя при этом никаких данных. Заполнить этот столбец данными можно, либо присваивая значение индексированному элементу в columnForRow0, либо ссылаясь на него посредством массива items. Следующие инструкции выполняют одну и ту же задачу:
columnForRow0[1] = 99;
items[0][1] = 99;
Этот замысел можно развивать и дальше, если вам нужно будет создать массив массивов из массивов, а не прямоугольный трехмерный массив.
ПРИМЕЧАНИЕ Если вам уже приходилось создавать код на языке программирования Java, то вам должно быть знакомо это понятие. В Java нет многомерных массивов, вместо них можно создавать массивы массивов точно так же, как только что было описано.
В следующем упражнении массивы будут использоваться для создания приложения, сдающего игральные карты и являющегося частью карточной игры. Приложение выводит форму с четырьмя карточными раздачами, произведенными случайным образом из обычной колоды игральных карт (в 52 карты). Вам предстоит завершить создание кода, раздающего карты четырем игрокам.
Откройте в Microsoft Visual Studio 2015 проект Cards, который находится в папке \Microsoft Press\VCSBS\Chapter 10\Cards вашей папки документов.
Щелкните в меню Отладка на пункте Начать отладку, чтобы выполнить сборку и запуск приложения. Появится форма с надписью Card Game и четырьмя текстовыми полями с надписями North, South, East и West. В нижней части будет находиться панель команд, обозначенная многоточием (…). Щелкните на нем, чтобы раскрыть командную панель. Появится кнопка с надписью Deal (Раздать) (рис. 10.2).
ПРИМЕЧАНИЕ Используемая здесь технология считается предпочтительным механизмом размещения командных кнопок в приложениях универсальной платформы Windows (Universal Windows Platform (UWP)), и с этого момента все UWP-приложения, представленные в этой книге, будут выполнены в таком стиле.
Щелкните на кнопке Deal. Ничего не произойдет. Код, раздающий карты, пока не создан, и в данном упражнении вы как раз и займетесь его написанием.
Вернитесь в среду Visual Studio 2015 и в меню Отладка щелкните на пункте Остановить отладку.
Найдите в окне обозревателя решений файл Value.cs и откройте его в окне редактора. В этом файле содержится перечисление по имени Value, представляющее по нарастающей карты различного достоинства:
enum Value { Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack,
Queen, King, Ace }
Рис. 10.2
Откройте в окне редактора файл Suit.cs. В нем содержится перечисление, представляющее карточные масти в обычной колоде:
enum Suit { Clubs, Diamonds, Hearts, Spades }
Выведите в окно редактора файл PlayingCard.cs. В нем содержится класс PlayingCard, моделирующий отдельно взятую игральную карту.
class PlayingCard
{
private readonly Suit suit;
private readonly Value value;
public PlayingCard(Suit s, Value v)
{
this.suit = s;
this.value = v;
}
public override string ToString()
{
string result = $"{this.value} of {this.suit}";
return result;
}
public Suit CardSuit()
{
return this.suit;
}
public Value CardValue()
{
return this.value;
}
}
У этого класса имеются два поля только для чтения, представляющие достоинство и масть карты. Инициализацией этих полей занимается конструктор.
ПРИМЕЧАНИЕ Поля только для чтения хорошо подходят для моделирования данных, которые не должны изменяться после своей инициализации. Присвоить значение полю только для чтения можно с помощью инициализатора при его объявлении или в конструкторе, но после этого значение изменить уже невозможно.
Класс содержит два метода с именами CardValue и CardSuit, которые возвращают эту информацию, в нем также переопределяется метод ToString для возвращения текстового представления карты.
ПРИМЕЧАНИЕ Вообще-то методы CardValue и CardSuit лучше реализовать в виде свойств, чему вы научитесь, изучая главу 15.
Откройте в окне редактора файл Pack.cs. В нем содержится класс Pack, моделирующий колоду игральных карт. В начале определения класса Pack находятся два открытых поля с именами NumSuits и CardsPerSuit, содержащие целочисленные константы. В этих двух полях указывается количество мастей в карточной колоде и количество карт каждой масти. Закрытая переменная cardPack является двумерным массивом из объектов PlayingCard. Первая размерность будет использоваться для указания масти, а вторая — для указания достоинства карты в масти. Переменная randomCardSelector является случайным числом, сгенерированным на основе использования класса Random. Переменная randomCardSelector поможет перетасовать карты перед раздачей игрокам:
class Pack
{
public const int NumSuits = 4;
public const int CardsPerSuit = 13;
private PlayingCard[,] cardPack;
private Random randomCardSelector = new Random();
...
}
Найдите пассивный конструктор для класса Pack. В данный момент этот конструктор не содержит ничего, кроме комментария // TODO:. Удалите этот комментарий и добавьте следующую инструкцию, выделенную жирным шрифтом, чтобы создать экземпляр массива cardPack с соответствующими значениями для каждой размерности:
public Pack()
{
this.cardPack = new PlayingCard[NumSuits, CardsPerSuit];
}
Добавьте к конструктору Pack следующий код, выделенный жирным шрифтом. Имеющиеся в нем инструкции заполнят массив cardPack отсортированной колодой карт:
public Pack()
{
this.cardPack = new PlayingCard[NumSuits, CardsPerSuit];
for (Suit suit = Suit.Clubs; suit <= Suit.Spades; suit++)
{
for (Value value = Value.Two; value <= Value.Ace; value++)
{
this.cardPack[(int)suit, (int)value] = new PlayingCard(suit, value);
}
}
}
Внешний цикл for осуществляет последовательный обход списка значений перечисления Suit, а внутренний цикл обходит все значения, которые каждая карта может иметь в каждой масти. Внутренний цикл создает новый объект PlayingCard указанных масти и значения и добавляет его к соответствующему элементу в массиве cardPack.
ПРИМЕЧАНИЕ В качестве индексов внутри массива следует использовать один из целочисленных типов. Переменные suit и value являются переменными перечислений. Но перечисления основаны на целочисленных типах, поэтому их, как показано в коде, вполне свободно можно привести к int-типу.
Найдите в классе Pack метод DealCardFromPack. Он предназначен для выбора случайной карты из колоды, удаления этой карты из колоды, чтобы она не могла быть выбрана повторно, и последующей передачи ее назад в качестве возвращаемого из метода значения.
Первой задачей метода является случайный выбор масти. Удалите из метода комментарий и инструкцию, выдающую исключение NotImplementedException, заменив их следующей инструкцией, выделенной жирным шрифтом:
public PlayingCard DealCardFromPack()
{
Suit suit = (Suit)randomCardSelector.Next(NumSuits);
}
Эта инструкция использует метод Next, имеющийся в объекте генератора случайных чисел randomCardSelector и предназначенный для возвращения случайного числа, соответствующего масти. Параметр для метода Next указывает используемую исключительную верхнюю границу диапазона, выбираемое значение находится между нулем и этим значением за вычетом единицы. Учтите, что возвращаемое значение относится к типу int, поэтому, прежде чем присваивать его Suit-переменной, следует привести его к соответствующему типу.
Всегда есть вероятность того, что карт выбранной масти уже не останется. Вам нужно справиться с этой ситуацией и при необходимости выбрать другую масть.
После кода для выбора случайной масти добавьте цикл while, выделенный далее жирным шрифтом. Этот цикл вызывает метод IsSuitEmpty, чтобы определить, остались ли еще в колоде карты указанной масти (вскоре вам предстоит реализовать логику работы этого метода). Если таких карт не осталось, происходит случайный выбор другой масти (может быть опять выбрана та же самая масть) и выполняется новая проверка. Цикл повторяет процесс, пока не будет выбрана масть, которой принадлежит хотя бы одна оставшаяся в колоде карта:
public PlayingCard DealCardFromPack()
{
Suit suit = (Suit)randomCardSelector.Next(NumSuits);
while (this.IsSuitEmpty(suit))
{
suit = (Suit)randomCardSelector.Next(NumSuits);
}
}
Теперь вами уже выбрана случайная масть, представленная в колоде хотя бы одной картой. Следующая задача будет заключаться в выборе случайной карты этой масти. Чтобы выбрать значение карты, можно воспользоваться генератором случайных чисел, но, как и прежде, нет никакой гарантии, что такая карта уже не была сдана. Но можно воспользоваться тем же приемом, что и прежде: вызвать метод IsCardAlreadyDealt (который будет изучен и дополнен чуть позже), чтобы определить, не была ли карта уже сдана, а если была, то выбрать другую случайную карту и снова проверить, не была ли она сдана, и повторять процесс до тех пор, пока не будет найдена свободная карта. Чтобы выполнить эти действия, добавьте к методу DealCardFromPack после уже имеющегося в нем кода следующие инструкции, выделенные жирным шрифтом:
public PlayingCard DealCardFromPack()
{
...
Value value = (Value)randomCardSelector.Next(CardsPerSuit);
while (this.IsCardAlreadyDealt(suit, value))
{
value = (Value)randomCardSelector.Next(CardsPerSuit);
}
}
Теперь вы выбрали случайную, не покинувшую колоду ранее игральную карту. Добавьте в конец метода DealCardFromPack следующий код для возвращения этой карты и присвоения соответствующему элементу массива cardPack значения null:
public PlayingCard DealCardFromPack()
{
...
PlayingCard card = this.cardPack[(int)suit, (int)value];
this.cardPack[(int)suit, (int)value] = null;
return card;
}
Найдите метод IsSuitEmpty. Не забудьте, что целью этого метода является получение параметра Suit и возвращение булева значения, показывающего, остались ли еще карты данной масти в колоде. Удалите из этого метода комментарий и инструкцию, выдающую исключение NotImplementedException, а затем добавьте к нему следующий код, выделенный жирным шрифтом:
private bool IsSuitEmpty(Suit suit)
{
bool result = true;
for (Value value = Value.Two; value <= Value.Ace; value++)
{
if (!IsCardAlreadyDealt(suit, value))
{
result = false;
break;
}
}
return result;
}
Этот код обеспечивает последовательный доступ к возможным значениям карт и использует метод IsCardAlreadyDealt (создание кода которого будет завершено на следующем этапе) для определения того, осталась ли еще в массиве cardPack карта с указанными мастью и достоинством. Если при прохождении цикла карта будет найдена, значение переменной result будет установлено в false и инструкция break прервет выполнение цикла. Если цикл завершится, а карта не будет найдена, у переменной result так и останется исходное значение true. Значение переменной result будет передано обратно в качестве возвращаемого методом значения.
Найдите метод IsCardAlreadyDealt. Целью этого метода является определение того, сдана ли карта с указанными мастью и достоинством и удалена ли она из колоды. Чуть позже вы увидите, что в процессе сдачи карты метод DealCardFromPack удаляет карту из массива cardPack и устанавливает значение соответствующего элемента в null. Замените тело этого метода следующим кодом, выделенным жирным шрифтом:
private bool IsCardAlreadyDealt(Suit suit, Value value)
=> (this.cardPack[(int)suit, (int)value] == null);
Этот метод возвращает true, если элемент в массиве cardPack соответствующих масти и достоинства имеет значение null, и возвращает false в противном случае.
Следующий этап будет заключаться в добавлении выбранной игральной карты к картам игрока. Откройте в окне редактора файл Hand.cs. В нем содержится класс Hand, реализующий карты, находящиеся на руках у игрока (то есть все карты, сданные одному игроку).
В этом файле содержится открытое поле с неизменным значением int-типа по имени HandSize, имеющее значение размера того набора карт, который один игрок получает на руки (13). В нем также содержится массив объектов PlayingCard, инициализированный с использованием константы HandSize. Поле playingCardCount будет использоваться вашим кодом, чтобы отслеживать количество карт, имеющихся в данный момент у игрока в результате сдачи карт:
class Hand
{
public const int HandSize = 13;
private PlayingCard[] cards = new PlayingCard[HandSize];
private int playingCardCount = 0;
...
}
Метод ToString создает строку, представляющую карты, находящиеся на руках у игрока. В нем используется цикл foreach для последовательного обращения к элементам массива cards, и в отношении каждого найденного им объекта PlayingCard вызывается метод ToString. Для получения нужного формата эти строки объединяются с установкой между ними символа новой строки (\n):
public override string ToString()
{
string result = "";
foreach (PlayingCard card in this.cards)
{
result += $"{card.ToString()}\n";
}
return result;
}
Найдите в классе Hand метод AddCardToHand. Целью этого метода является выдача игральной карты, указанной в качестве параметра, на руки игроку. Добавьте к этому методу следующий код, выделенный жирным шрифтом:
public void AddCardToHand(PlayingCard cardDealt)
{
if (this.playingCardCount >= HandSize)
{
throw new ArgumentException("Too many cards");
}
this.cards[this.playingCardCount] = cardDealt;
this.playingCardCount++;
}
Сначала этот код проверяет, не сданы ли на руки все полагающиеся игроку карты. Если набор полон, выдается исключение ArgumentException (это не должно случиться, но все же лучше подстраховаться). В противном случае карта добавляется к массиву cards с индексом, указанным переменной playingCardCount, и значение этой переменной увеличивается на единицу.
Раскройте в обозревателе решений узел MainPage.xaml и откройте в окне редактора файл MainPage.xaml.cs.
Код этого файла предназначен для окна Card Game. Найдите метод dealClick. Этот метод запускается, когда пользователь щелкает на кнопке Deal. На данный момент в нем содержатся пустой try-блок и обработчик исключения, выводящий на экран сообщение о том, что произошло исключение.
Добавьте к try-блоку следующую инструкцию, выделенную жирным шрифтом:
private void dealClick(object sender, RoutedEventArgs e)
{
try
{
pack = new Pack();
}
catch (Exception ex)
{
...
}
}
Эта инструкция просто создает новую колоду карт. Ранее уже было показано, что соответствующий класс содержит двумерный массив из находящихся в колоде карт и конструктор заполняет этот массив подробными данными о каждой карте. Из этой колоды следует создать четыре набора карт, сдаваемых на руки каждому игроку.
Добавьте к try-блоку следующую инструкцию, выделенную жирным шрифтом:
try
{
pack = new Pack();
for (int handNum = 0; handNum < NumHands; handNum++)
{
hands[handNum] = new Hand();
}
}
catch (Exception ex)
{
...
}
Этот цикл for создает из колоды четыре набора карт, сдаваемых на руки каждому из игроков, и сохраняет их в массиве по имени hands. Каждый набор изначально пуст, поэтому вам нужно сдать карты из колоды каждому игроку.
Добавьте к циклу for следующий код, выделенный жирным шрифтом:
try
{
...
for (int handNum = 0; handNum < NumHands; handNum++)
{
hands[handNum] = new Hand();
for (int numCards = 0; numCards < Hand.HandSize; numCards++)
{
PlayingCard cardDealt = pack.DealCardFromPack();
hands[handNum].AddCardToHand(cardDealt);
}
}
}
catch (Exception ex)
{
...
}
Внутренний цикл for заполняет каждый набор сдаваемых игрокам карт с помощью метода DealCardFromPack, позволяющего извлечь случайную карту из колоды, и метода AddCardToHand, позволяющего добавить эту карту к набору карт, сдаваемых игроку.
Добавьте после внешнего цикла for следующий код, выделенный жирным шрифтом:
try
{
...
for (int handNum = 0; handNum < NumHands; handNum++)
{
...
}
north.Text = hands[0].ToString();
south.Text = hands[1].ToString();
east.Text = hands[2].ToString();
west.Text = hands[3].ToString();
}
catch (Exception ex)
{
...
}
Когда будут сданы все карты, этот код покажет в текстовых полях формы, что именно находится на руках у каждого игрока. Эти текстовые поля называются north, south, east и west. Для форматирования вывода карт, находящихся на руках у каждого игрока, в коде используется метод ToString.
Если на каком-либо из этапов работы будет выдано исключение, обработчик исключения выведет окно сообщения со сведениями об ошибке, характерной для данного исключения.
Щелкните в меню Отладка на пункте Начать отладку. Как только появится окно Card Game, раскройте панель команд и щелкните на кнопке Deal. Карты из колоды должны быть сданы на руки в случайном порядке и показаны в форме для каждого игрока, что создаст на экране примерно следующую картину (рис. 10.3).
Рис. 10.3
Щелкните на кнопке Deal еще раз. Убедитесь в том, что будет роздан новый набор карт и на этот раз карты у каждого игрока будут другими.
Вернитесь в среду Visual Studio и остановите отладку.
В этой главе вы научились создавать и использовать массивы для работы с наборами данных. Вы увидели, как объявляются и инициализируются массивы, как осуществляется доступ к данным, содержащимся в массивах, как массивы передаются в качестве параметров методам и как массивы возвращаются из методов. Вы также научились создавать многомерные массивы и узнали, как используются массивы массивов.
Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 11.
Если хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Если увидите диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.
Чтобы | Сделайте следующее |
Объявить переменную массива | Наберите название типа элемента, затем поставьте квадратные скобки, а за ними укажите имя переменной и поставьте точку с запятой, например: bool[] flags; |
Создать экземпляр массива | Наберите ключевое слово new, затем укажите название типа элемента, после чего укажите в квадратных скобках размер массива, например: bool[] flags = new bool[10]; |
Инициализировать элементы массива конкретными значениями | Запишите для массива через запятую список значений, заключенный в фигурные скобки, например: bool[] flags = { true, false, true, false }; |
Определить количество элементов в массиве | Используйте свойство Length, например: bool[] flags = ...; ... int noOfElements = flags.Length; |
Обратиться к отдельно взятому элементу массива | Наберите имя переменной массива, после чего поставьте целочисленный индекс элемента, заключенный в квадратные скобки. Не забудьте, что индексация массива начинается с нуля, а не с единицы, например: bool initialElement = flags[0]; |
Обратиться последовательно ко всем элементам массива | Воспользуйтесь инструкцией for или foreach, например: bool[] flags = { true, false, true, false }; for (int i = 0; i < flags.Length; i++) { Console.WriteLine(flags[i]); } foreach (bool flag in flags) { Console.WriteLine(flag); } |
Объявить переменную многомерного массива | Наберите название типа элемента, за ним поставьте набор, состоящий из квадратных скобок и разделителя в виде запятой, указывающий на количество размерностей, после него укажите имя переменной, а за ним поставьте точку с запятой. Например, для создания двумерного массива по имени table и его инициализации для хранения четырех строк по шесть столбцов воспользуйтесь следующими инструкциями: int[,] table; table = new int[4,6]; |
Объявить переменную ступенчатого массива | Объявите переменную в виде массива дочерних массивов. Каждый дочерний массив можно инициализировать на различную длину. Например, чтобы создать ступенчатый массив по имени items и инициализировать каждый дочерний массив, воспользуйтесь следующими инструкциями: int[][] items; items = new int[4][]; items[0] = new int[3]; items[1] = new int[10]; items[2] = new int[40]; items[3] = new int[25]; |