Журнал LinuxFormat - перейти на главную

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() Печать результатов игры (у кого осталась червонная дама).


Также потребуется определить классы 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 может их читать и записывать [в серьезных программах так делать не рекомендуется – вместо этого следует определить методы, обеспечивающие доступ извне к закрытым переменным, – прим.ред.].

Введение в обобщенные типы

У нас есть класс 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() очень прост и выдает названия карт, я не буду приводить его здесь – можете обратиться к исходному коду на диске.


Последний рывок

Оставшийся код обрабатывает основной игровой цикл: игроки выбирают карты и пытаются найти пары. Самой сложной частью является выбор игрока, у которого надо вытащить карту: код должен начать искать игрока «справа» от нас (то есть его номер должен быть больше нашего), но если он никого не находит, то начинает смотреть с начала списка. Если он вернулся обратно, значит, игра закончилась. Если найден подходящий игрок, у него выбирается карта, возможно, сбрасываются новые пары, и игра продолжается.

Этот код должен следовать до конца метода Play(). Он длинный, но не такой сложный, если разбить его на части вот так:


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;
        }
    }
}

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

</code>

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);
   }
 }

</code>

Теперь начинается самое интересное: если мы добрались сами до себя, значит, нет игроков, у которых можно взять карту. В противном случае, берем произвольную карту и вызываем метод 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

Персональные инструменты
купить
подписаться
Яндекс.Метрика