- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF92:Mono
Материал из Linuxformat.
- Mono-Мания Программирование на современной платформе для новичков
Содержание |
Mono: Объекты и обобщенные типы
- Объектно-ориентированное программирование кое-кого пугает больше, чем школьников прививки, но Пол Хадсон собирается обойтись без боли.
В теории, теория и практика совпадают, но на практике так бывает редко. Вот почему в наших уроках обучение строится на процессе выполнения: если вы читали этот учебник с самого начала, то уже сделали пять полноценных приложений, решающих реальные задачи. На данном уроке я хочу отойти от принятой формулы и обсудить довольно сложную теорию программирования: объектно-ориентированное программирование (ООП) и обобщенные типы.
Это не штуки, из которых можно делать свои приложения, а просто методы, которые пригодятся вам при программировании на Mono. Коль скоро вы это поняли, я покажу вам, как реализовать карточную игру с помощью названных двух методов.
Классификация объектов
ООП позволяет определять предметы в вашем программном коде и даже придавать им желаемое поведение. Вы уже использовали ООП, только не догадывались об этом. Создайте новый проект с именем Geno (еще один редкий персонаж Nintendo – уж простите!), и вы уви- дите, что MonoDevelop напишет код по умолчанию:
class MainClass { public static void Main(string[] args) { Console.WriteLine(“Hello World!”); } }
Здесь применяется ООП, и программа отлично работает, даже если вы не понимаете, что это значит (да и знать не хотите). Но теперь знайте: класс – это определение предмета, а объект – это экземпляр предмета. Ясно как ночь? Так вот: на вопросы «Какого цвета машина?», «Какая длина у машины?» или «Сколько у машины передач?» ответ будет «это зависит от»: что такое машина, представляют все, но каждая машина индивидуальна.
В терминах ООП, «машина» является классом. Но «машину вообще» увидеть нельзя: это абстрактное понятие. На самом деле мы видим «Форды», «Хонды» и так далее, то есть физические реализации класса «машина». Итак: машина, находящаяся на шоссе, это объект класса «машина». У нее есть цвет, длина, и вы знаете, сколько у нее передач, но это просто переменные свойства класса «машина». Другие машины, даже той марки, что и ваша, могут сильно отличаться; но все они машины.
Если для вас это все еще пустой звук, подождите: вы все поймете из кода! А пока MonoDevelop определил для нас класс MainClass, содержащий метод public static void Main(), который мы все время используем. Два странных слова – public и static – относятся к ООП.
Объект Geno
Измените MainClass на Geno, имя нашего проекта. Теперь замените строку Console.WriteLine() на следующую:
Geno game = new Geno();
Этот код создает объект класса Geno и присваивает его переменной game. Что представляет собой класс Geno? В данный момент он содержит только Main() и больше ничего – это просто пустая переменная. Но она рождает интересный вопрос: строка находится внутри метода Main(), который находится внутри класса Geno. Как может Geno создать сам себя? Или – основной вопрос философии: что появилось раньше, класс Geno или метод Main()?
Тут на помощь приходит слово ‘static’ – статический. Метод Main(), если вы помните, помечен как public static void, и на то есть причина: статические методы могут вызываться без экземпляра класса. Фактически они привязывают метод к классу, просто в организационных целях. Например, если в нашем классе был метод СменитьПередачу(), его применение не имело бы смысла без конкретного экземпляра машины, так как в противном случае, какую передачу надо менять? А как насчет вычисления тормозного пути машины, мчащейся со скоростью 100 км/ч? К машинам это имеет отношение, ноясно, что для этого не требуется реальный объект машины.
Таким образом, статический метод Main() может вызываться без существования класса Geno, и мы используем его для создания объекта Geno так, чтобы можно было играть в карточную игру. Объект Geno будет контролировать все аспекты игры, поэтому других объектов нам не понадобится. Но, ради интереса, мы добавим еще два класса: один будет отвечать за игроков, другой за карты. Логика отдается на откуп объекту Geno, так что классы игрока и карт предназначены просто для хранения данных.
Есть еще кое-что, что вам надо знать, прежде чем писать код. Иногда нужно, чтобы переменная принимала значение только из определенного набора. Например, набор данных для дней недели – воскресенье, понедельник, вторник и т.д. Для карточной игры требуется, чтобы каждая карта была определенной масти: червы, бубны, трефы или пики. C# позволяет определить масти карт как перечисление:
public enum Suits { Hearts, Diamonds, Clubs, Spades };
Описываем игру
Запрограммируем детскую игру: она, возможно, знакома вам как «Дама червей». Из карточной колоды извлекается одна дама (бубен), остается 51 карта. Карты сдаются всем игрокам, втемную. Игроки смотрят на свои карты и сбрасывают пары карт одинакового достоинства: например, если у игрока есть две десятки, то он кладет эти две десятки на стол. Дама червей не может быть использована как парная карта; игрок, которому она досталась, должен ее сохранить.
После того, как все игроки выкинули свои пары, первый игрок поворачивается к игроку справа и втемную забирает у него произвольную карту. Если в результате у игрока образовались парные карты, то он может их сбросить. Игра продолжается, и второй игрок поворачивается к игроку справа и вынимает карту – и так далее. В конечном счете, каждая карта должна найти себе пару, за исключением дамы червей, а игрок, у которого она на руках, проигрывает. [порусски эта игра называется «Акулина» или «Акулька», но непарная дама – пиковая, – прим.ред.]
Нам надо предусмотреть следующие функции:
- Play() Эта функция начинает игру, после проведения необходимых настроек.
- ShuffleCards() Перетасовать колоду (рандомизировать порядок карт).
- RemovePlayerPairs() Поиск и удаление подходящей пары карты у игрока.
- PrintResult() Печать результатов игры (у кого осталась червонная дама).
При работе с MonoDevelop проверьте, что он использует среду .NET 2.0, в противном случае столкнетесь с проблемами. Вам надо изменить настройки для каждого проекта – выберите Project > Options, затем выберите категорию Runtime Options и установите среду выполнения на 2.0, а не 1.1..
Также потребуется определить классы Player и Card, которые будут содержать информацию. Вот скелет будущего кода [для большей ясности: Suit – масть, Card – карта, Hearts – черви, Diamonds – бубны, Clubs – трефы, они же крести, Spades – пики, Player – игрок, англ.]:
using System; namespace Geno { enum Suits { Hearts, Diamonds, Clubs, Spades }; class Card { public int Val; public Suits Suit; } class Player { public int Score; } class Geno { static void Main(string[] args) { Geno game = new Geno(); game.Play(); } void Play() { } void ShuffleCards() { } void RemovePlayerPairs(int player) { } void PrintResults() { } } }
Ну да, знаю, здесь куча пустых методов, но они проясняют структуру программы. Заметили, что мне пришлось объявить все переменные внутри классов Player и Card как public? Это потому, что переменные внутри объекта доступны только внутри самого объекта, чтобы внешние части кода их не затрагивали. Но так как у нас довольно простая программа, класс Geno будет делать большую часть работы и использовать классы Player и Card просто для хранения значений. То, что эти переменные стали public, значит, что класс Geno может их читать и записывать [в серьезных программах так делать не рекомендуется – вместо этого следует определить методы, обеспечивающие доступ извне к закрытым переменным, – прим.ред.].
Введение в обобщенные типы
Если вы не хотите, чтобы переменная была недоступна вне класса, удалите слово public. Или, если хотите быть точным, напишите вместо него слово private.
У нас есть класс Card, то есть мы можем определить значение карты (от 1 до 13) и ее масть. Но мы еще не создали сам объект «карты» и не знаем, где их хранить. Здесь приходят на помощь массивы: они позволяют хранить множество объектов в одной переменной. У Mono есть огромное количество типов массивов, но долгое время наиболее используемым был ArrayList. Он позволяет хранить в массиве любой тип объекта и обращаться к нему просто по индексу. Но здесь есть проблема: у каждой переменной в C# есть тип, будь то integer, string, Suit или любой другой. Так как ArrayList может содержать переменную любого типа, то вам всегда придется сообщать Mono, какой тип используется. Например:
int i = 1; ArrayList numbers = new ArrayList(); numbers.Add(i); int j = numbers[0];
Здесь будет ошибка – Mono не сможет преобразовать numbers[0] в integer, даже если мы знаем, что оно уже типа integer. Вместо этого надо написать:
int j = (int)numbers[0];
Префикс (int) значит «обращаться с numbers[0] как с целым числом», и наш код будет компилироваться правильно. Новые версии C#, включая поставляемую с Fedora Core 6, поддерживает новый способ программирования, известный как обобщенные типы (generics). И если вы когда-либо раньше использовали С++, то знакомы с термином «шаблон», а это почти тоже самое – только на вид гораздо легче!
Обобщенные типы – это произвольные массивы, которые принимают только один тип переменных. Вам уже не надо приписывать (int), чтобы вытаскивать целые числа из обобщенного списка – туда так и так можно помещать только целые числа. В Geno обобщенные типы будут использоваться для двух вещей: хранения карт и хранения игроков. У каждого игрока будет свой собственный список карт, так как карты из колоды раздаются именно игрокам.
Добавьте такие две строчки сразу под строкой class Geno:
List<Card> Cards = new List<Card>(); List<Player> Players = new List<Player>();
Этот синтаксис может затуманить мозги, но по-простому он гласит «Хочу, чтобы один список содержал переменные типа Card, а второй список содержал переменные типа Players». Вы также должны добавить строку до переменной Score в классе Player:
public List<Card> Cards = new List<Card>();
Устанавливаем игру
Оба наших списка Players и Cards пусты, поэтому первым заданием будет поместить 51 карту в колоду (помните, что мы убрали бубновую даму) и разместить несколько игроков. Самым простым способом вста- вить карты будет перебрать в цикле все масти ('Suits), и для каждой масти посчитать от одного до 13 так, чтобы получить все от тузов до королей [туз считается единицей]. Добавив все карты и всех игроков, завершаем установку, вызывая метод ShuffleCards() для перетасовки карт.
foreach (Suits suit in Enum.GetValues(typeof(Suits))) { for (int i = 1; i < 14; ++i) { Card card = new Card(); card.Val = i; card.Suit = suit; if (card.Val == 12 && card.Suit == Suits.Diamonds) continue; Cards.Add(card); } } for (int i = 0; i < 4; ++i) { Player player = new Player(); Players.Add(player); } ShuffleCards();
Вы видите, что надо вызвать просто Cards.Add(card), чтобы вставить карту в список из Cards; все очень просто. Метод ShuffleCards() пока ничего не делает, потому что он пустой. Нам необходимо пройтись по всему массиву Cards, вытащить отдельные карты и поместить их обратно в произвольную позицию. Тут не обойтись без генератора случайных чисел – вставьте эту строку перед static void Main:
Random Rand = new Random();
Теперь полная реализация ShuffleCards():
void ShuffleCards() { for (int i = 0; i < Cards.Count; ++i) { Card tmp = Cards[i]; Cards.RemoveAt(i); Cards.Insert(Rand.Next(0, Cards.Count), tmp); } }
Здесь показано несколько новых возможностей списков: у них есть свойство Count, которое возвращает число содержащихся в них элементов; они содержат метод RemoveAt(), который удаляет элемент в указанной позиции; и у них есть метод Insert(), который вставляет элемент в указанную позицию. Номер позиции определяется переменной Rand, которая может генерировать число между 0 и Crads.Count (количество карт в колоде).
Раздаем карты
Наша колода заполнена и стасована. Осталось сдать ее игрокам. Чтобы это сделать, будем давать им карты, пока колода не кончится. Это зна чит, что нам надо начать с игрока 0 (в C# списки начинаются с 0), сдать карту, перейти к игроку 1, сдать карту и так далее, пока не закончатся игроки; потом перейти снова к игроку 0. В коде этот алгоритм будет выглядеть вот так:
int playernum = 0; while (Cards.Count > 0) { Players[playernum].Cards.Add(Cards[0]); Cards.RemoveAt(0); ++playernum; if (playernum == Players.Count) playernum = 0; } // удалять начальные пары for (int i = 0; i < Players.Count; ++i) { RemovePlayerPairs(i); }
В последней части (от комментариев и ниже) уже начинается логи ка игры: каждый игрок удаляет пары карт, которые оказались у него в начале игры.
Пишем логику
Метод RemovePlayerPairs() принимает на вход номер игрока и удаляет у него парные карты. Чтобы облегчить понимание, я разделил функцию на две, вот так:
void RemovePlayerPairs(int player) { while (TryPairRemove(player)) { ++Players[player].Score; } }
Итак, номер игрока поступает в функцию и передается методу TryPairRemove(). Если этот метод вернул true, значит, была найдена пара; затем он вызывается снова. В конце концов будут найдены все пары, и цикл завершит свою работу.
Метод TryPairRemove() немного сложноват, так как ему надо у каждого игрока взять карту, перебрать остальные его карты на предмет совпадения, и если в паре ни одна из карт не является дамой червей удалить пару и вернуть true.
bool TryPairRemove(int player) { Card card1; Card card2; Player thisplayer = Players[player]; for (int i = 0; i < thisplayer.Cards.Count; ++i) { card1 = thisplayer.Cards[i]; if (card1.Suit == Suits.Hearts && card1.Val == 12) continue; for (int j = i + 1; j < thisplayer.Cards.Count; ++j) { card2 = thisplayer.Cards[j]; if (card2.Suit == Suits.Hearts && card2.Val == 12) continue; if (card1.Val == card2.Val) { thisplayer.Cards.RemoveAt(j); thisplayer.Cards.RemoveAt(i); Console.WriteLine(“Player “ + (player + 1) + “ plays “ + CardName(card1) + “ and “ + CardName(card2)); return true; } } } return false; }
Некоторые комментарии по коду:
- 1 j начинается с i+1: среди проверенных карт совпадений нет.
- 2 Проверка совпадения с дамой червей делается и для card1, и для card2.
- 3 Если найдено совпадение, сначала удаляется j, затем i. Удаление элемента из списка заставляет сдвигаться все элементы массива, чтобы закрыть пробел, поэтому первым надо удалять элемент с более высоким индексом.
- 4 Метод CardName() будет описан далее.
Да, это солидный кусок кода, ведь мы должны дважды перебрать карты игрока. Метод CardName() очень прост и выдает названия карт, я не буду приводить его здесь – можете обратиться к исходному коду на диске.
Списки – это несортированные массивы, обращение к которым осуществляется по номеру позиции, но можно также использовать словари, с обращением через определенный вами тип. Например, Dictionary<string,string> хранит строки как ключи массива и как его значения. Если ключ foo имеет значение bar, вы можете найти его так: MyDictionary[“foo”]. Можете использовать любой тип данных – даже объекты или другие обобщенные типы, если хотите.
Последний рывок
Оставшийся код обрабатывает основной игровой цикл: игроки выбирают карты и пытаются найти пары. Самой сложной частью является выбор игрока, у которого надо вытащить карту: код должен начать искать игрока «справа» от нас (то есть его номер должен быть больше нашего), но если он никого не находит, то начинает смотреть с начала списка. Если он вернулся обратно, значит, игра закончилась. Если найден подходящий игрок, у него выбирается карта, возможно, сбрасываются новые пары, и игра продолжается.
Этот код должен следовать до конца метода Play(). Он длинный, но не такой сложный, если разбить его на части вот так:
Есть соблазн использовать целые числа, а не перечисления для списков. Например, воскресенье может быть 0, понедельник – 1, и так далее. Хотя для небольших списков это работает, в случае больших списков легко запутаться. Перечисления работают ничуть не медленнеe целых, так как автоматически преобразуются в целые числа.
bool finished = false; while (!finished) { int playersleft = Players.Count; for (int i = 0; i < Players.Count; ++i) { Player player = Players[i]; if (player.Cards.Count == 0) { --playersleft; continue; }
Здесь отслеживается число игроков, оставшихся в игре. Если это значение равно 1, то надо выходить; займемся этим позже. Сейчас цикл просто обрабатывает каждого игрока.
int playertouse = -1; for (int j = i + 1; j < Players.Count; ++j) { if (Players[j].Cards.Count > 0) { playertouse = j; break; } } if (playertouse == -1) { for (int j = 0; j < Players.Count; ++j) { if (Players[j].Cards.Count > 0) { playertouse = j; break; } } }
Этот блок ищет игрока, у которого нужно вытянуть карту: сперва над текущей позицией, потом с начала. Игроки, у которых нет карт, автоматически пропускаются.
if (playertouse == i) { finished = true; break; } else { int cardtochoose = Rand.Next(0, Players[playertouse].Cards. Count); Players[i].Cards.Add(Players[playertouse]. Cards[cardtochoose]); Players[playertouse].Cards.RemoveAt(cardtochoose); RemovePlayerPairs(i); } }
Теперь начинается самое интересное: если мы добрались сами до себя, значит, нет игроков, у которых можно взять карту. В противном случае, берем произвольную карту и вызываем метод RemovePlayerPairs(), для сброса подходящей пары.
if (playersleft == 1) finished = true; } PrintResults();
Наконец, если игроков не осталось, завершаем цикл. Игра закончена, и PrintResult() выдает сообщение:
void PrintResults() { Console.WriteLine(“”); for (int i = 0; i < Players.Count; ++i) { if (Players[i].Cards.Count > 0) { Console.WriteLine(“У игрока “ + (i + 1) + “ осталась дама червей!”); break; } } }
Если вы запустите программу, то увидите четырех компьютерных игроков, играющих около секунды. Теперь у вас есть понимание о классах и объектах, и вы можете извлечь преимущества обобщенных типов для хранения ваших объектов. Вы также узнали, как сделать несложную карточную игру – полпути к разработке покера или любой другой игры. LXF
Категории: Учебники | Mono | Пол Хадсон