LXF92:Mono-Мания

Материал из Linuxformat.

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

Текущая версия

  • Купить электронную версию
  • Подписаться на бумажную версию