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

LXF99:Mono

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

Mono: Рецепты

За прошедший год Пол Хадсон рассказал нам о файловых системах, доступе к базе данных, графическом интерфейсе, XML и сетях. Пришло время расправить крылья...

Дошло, что от вас требуется? Чтобы стать хорошим программистом, необходимо нечто большее, чем пристрастие к сандалиям и очки, держащиеся только на клейкой ленте. На самом деле, это острый ум, хорошая память, жадность до новых технологий и – бесспорно, самое важное – способность хранить часто используемые участки кода в голове, чтобы решать стандартные проблемы быстро. Многое тут вытекает из склада ума, но ум приходит с опытом: вы сталкиваетесь с проблемой, решаете ее и создаете личную библиотеку кода, позволяющую быстро сколачивать решения. Данный урок – последний в нашей серии учебников по Mono, поэтому мы сосредоточимся на кусках кода, способных помочь вам решить стандартные проблемы и извлечь преимущества из полезных технических приемов – пожалуйста, берите их и свободно используйте в своих проектах, под какой бы лицензией они не издавались.

Проблема

Удалить в цикле несколько элементов из массива

Этот пример достаточно прост для затравки, но он ставит в тупик некоторых начинающих. Допустим, у вас есть объект List<string> с именем MyNames, то есть он хранит массив строк. Поскольку это обобщенный тип данных, вам следует добавить using System.Collections.Generic; в начало вашего файла проекта. И если вы хотите удалить из этого списка все имена, начинающиеся с "Mike", то первый вариант вашего кода может выглядеть так:

for (int i = 0; i < MyNames.Count; ++i) {
  if (MyNames[i].StartsWith("Mike")) {
      MyNames.RemoveAt(i);
   }
 }

Или, если вы аккуратист, то так:

foreach (string name in MyNames) {
   if (name.StartsWith("Mike")) {
      MyNames.Remove(name);
   }
 }

Но здесь налицо серьезная проблема: .NET не разрешает изменять массив, пока вы перемещаетесь по нему, то есть первый Mike будет удален, но цикл продолжится, и появится ошибка, потому что на самом деле вы сдвинули все элементы на одну позицию, и какой же элемент должен быть следующим в цикле? Приведенное решение вполне очевидно, если немного подумать: при перемещении по массиву в обратную сторону сдвиг элементов не имеет значения, потому что вы его уже обработали. Вот оно:

 for (int i = MyNames.Count – 1; i >= 0; --i) {
   if (MyNames[i].StartsWith("Mike")) {
      MyNames.RemoveAt(i);
   }
 }

Проблема

Округление чисел портит ваш код

По мере увеличения объема кода нарастает необходимость его чистки. Одной из наиболее раздражающих ловушек в коде на C# является Math.Round(), потому что если вы захотите написать код

 int foo = Math.Round(10.1f);

он не сработает. О нет – вы получите сообщение об ошибке преобразования: Mono не умеет преобразовывать из double в int. Вы-то думали, что этот код преобразует число с плавающей точкой 10.1 в целое 10, но Math.Round() возвращает не целое – потому что если указать второй параметр, можно получить число, округленное до указанного знака после запятой.

Конечно, это лишнее, если вам всего лишь надо преобразовать число с плавающей точкой в целое, поэтому я предлагаю создать такой небольшой метод:

 public int Round(float num) {
   return (int)Math.Round(num);
 }

Вы можете использовать его так:

 int foo = Round(10.1f);

Вам, вероятно, кажется, что можно и без него обойтись, но пред ставьте такой код:

 DrawRectangle((int)Math.Round(obj.x), (int)Math.Round(obj.y), (int)Math. Round(obj.w), (int)Math.Round(obj.h))

Ну не уродство? Странно: в Java есть прекрасный метод Math.round(), получающий float, а возвращающий int, а вот в C# требуется собственный код. Не опасайтесь снижения производительности за счет добавочного вызова функции: такой простой метод, вероятно, будет встроенным (inline).

Проблема

Сортировка массива экзотических данных

Стандартный класс List имеет метод Sort(), который выстраивает строки и числа в определенном порядке, но он бесполезен, если вы храните объекты и хотите отсортировать их по определенному свойству. Однако вы можете сообщить Sort() имя сравнивающей функции, способной выполнять более продвинутую сортировку, а затем использовать ее обычным способом. Например, пусть у вас есть класс

public class User {
  public int ID;
  public string Name;
}

и List [Список] этих пользователей, вроде такого:

List<User> MyUsers = new List<User>();
User user = new User();
user.ID = 1;
user.Name = "Paul";
MyUsers.Add(user);
user = new User();
user.ID = 10;
user.Name = "Scott";
MyUsers.Add(user);
user = new User();
user.ID = 5;
user.Name = "Mike";
MyUsers.Add(user);
user = new User();
user.ID = 50;
user.Name = "Graham";
MyUsers.Add(user);

Сортируя его при помощи обычного старого Sort(), вы получите ошибку, ибо .NET не умеет обращаться с объектами User. Но не так уж трудно написать собственный метод сравнения сложных объектов. А если вы предоставите его имя функции Sort(), он будет вызываться для каждого сравнения двух объектов, чтобы решить, в каком порядке их расположить. Метод должен возвращать 1 (объект 1 должен следовать после объекта 2), -1 (объект 2 должен следовать за объектом 1) или 0 (объекты 1 и 2 равноправны). Он может выглядеть примерно так:

private int CompareUserByID(User a, User b) {
  if (a.ID > b.ID) {
     return 1;
  } else if (a.ID < b.ID) {
     return -1;
  } else {
     return 0;
  }
}

Затем сортируйте ваш массив, используя

MyUsers.Sort(CompareUserByID);

Теперь все элементы будут переставлены. Приведенный способ показывает, как создать свою собственную систему сортировки для любого типа данных, но встроенные типы данных – int, string и т.п. – можно сравнивать еще проще. Все эти типы имеют специальный метод CompareTo(), принимающий в качестве единственного параметра другой идентичный тип и возвращающий вам 1, -1 или 0. Поэтому, если хотите, можете написать метод CompareUserByName() вот так:

private int CompareUserByName(User a, User b) {
  return a.Name.CompareTo(b.Name);
}

Проблема

Перемешать элементы массива случайным образом

.NET имеет несколько способов манипуляции массивами, но ни один из них не столь сжат, как функция shuffle() в PHP: даете ей массив и получаете обратно перемешанный. Вы можете скопировать ее одним из двух способов – в зависимости от того, хотите ли вы потренироваться или выглядеть круче!

Простой путь перемешать массив таков:

 public void ShuffleList(List<string> list) {
   Random rand = new Random();
   for (int i = 0; i < list.Count; i++) {
       string tmp = list[i];
       list.RemoveAt(i);
       list.Insert(rand.Next(0, list.Count), tmp);
   }
 }

В цикле перемещаемся по массиву, удаляя каждый элемент и вставляя его в случайную позицию. Заметьте: код генерирует случайное число, используя новый объект Random при каждом вызове метода; это лучше, чем создание одного объекта Random для всей программы.

Итак, вот схема перемешивания: взять List, содержащий строки, и перемешать их случайным образом. А если вы захотите перемешать массив целых чисел? Или объектов User? Или чего угодно, но не строк? Можно, конечно, создать несколько методов ShuffleList(), но это недальновидное решение: ваш код очень скоро раздуется. Намного лучше использовать стандарты, создав функцию, которая принимает список любого типа и перемешивает содержимое. Тут требуется некий специальный синтаксис C#, потому что вам необходимо сообщить своему методу, что он будет принимать неизвестный тип и использовать этот тип для всех данных. Обычно на обобщенные типы ссылаются как на T или T1, T2 и т.д., если их более одного. Итак, метод ShuffleList() можно переписать так:

 public void ShuffleList<T>(List<T> list) {
     Random rand = new Random();
     for (int i = 0; i < list.Count; i++) {
         T tmp = list[i];
         list.RemoveAt(i);
         list.Insert(rand.Next(0, list.Count), tmp);
     }
 }

Когда вы используете ShuffleList(MyUsers), .NET по сути заменяет в этом методе «Т» на «User», т.е. ShuffleList() принимает List<User>, а переменная tmp получает тип User. Итак, вы можете вызвать ShuffleList() со списком [List] строк, целых чисел, дробных, логических, людей, рыбок или данных любого другого типа, какой сможете придумать.

Проблема

Узнать, когда один объект находится над другим

Это весьма общая формулировка, а вот конкретный пример: вы хотите знать, когда мышь находится над нарисованным вами объектом. Проблема решается очень просто: все, что надо сделать – это проверить, что координаты мыши больше, чем позиция X и Y вашего объекта, и меньше чем X, Y + ширина и высота объекта. Вчерне можно записать подобный метод так:

public bool PointOverRect(int x1, int y1, int x2, int y2, int width, int height) {
  if (x1 >= x2 && x1 <= x2 + width) {
     if (y1 >= y2 && y1 <= y2 + height) {
        return true;
     }
  }
  return false;
}

Для использования этого метода передайте координаты X и Y мыши в качестве первых двух параметров, затем X, Y, ширину и высоту вашего объекта в качестве вторых параметров. Конечно, реально это работает только для прямоугольных объектов, но создавайте прямоугольные рамки вокруг объектов другой формы, и все будет хорошо.[для фигур произвольной формы часто в качестве второго входного параметра используется массив координат узлов контура, ограничивающего объект или, если контур является геометрической фигурой, то ее атрибуты, например, центр и радиус окружности, – прим. пер.]

В принципе, можно взять этот простой метод и подстроить под свои личные цели. Например, вы могли бы захотеть добавить поддержку вращения ограничивающей рамки. Если все, что вам надо, это стандартная функция проверки нахождения точки в прямоугольнике, используйте метод Contains() вашего прямоугольника и передайте ему точку.

Проблема

Нужно узнать, перекрываются ли два объекта

Еще одна общая проблема, так что снова поясню на примере: вы хотите реализовать проверку столкновений в игре. Это очень похоже на проверку, принадлежит ли точка прямоугольнику, особенно если использовать метод Contains(). Однако, хотя Contains() и может принимать объект Rectangle в качестве параметра, он возвращает true, если один прямоугольник полностью лежит внутри другого, а не просто пересекает его, а для обнаружения столкновений вам необходимо последнее.

К счастью, для прямоугольников есть другой небольшой полезный метод, под названием Intersect(), который накладывает один прямоугольник на другой и возвращает новый прямоугольник-пересечение, и вы можете проверить его ширину и высоту, чтобы понять, имеет ли место пересечение. Простой и легкий способ проверки столкновений – вот такой метод:

 public bool RectOverRect(int x1, int y1, int width1, int height1, int x2, int y2, int width2, int height2) {
   Rectangle rectthis = new Rectangle(x1, y1, width1, height1);
   Rectangle rectthat = new Rectangle(x2, y2, width2, height2);
   rectthis.Intersect(rectthat);
   if (rectthis.Width == 0 && rectthis.Height == 0) {
      return false;
   } else {
      return true;
   }
 }

Проблема

Обработка ошибок при их возникновении

Я не затрагивал старый добрый блок try/catch в нашей серии, но теперь настало время это сделать! Система try/catch позволяет выполнять команды и предпринимать заданные действия, если возникла ошибка. Например, вы можете написать:

 try {
          SomeDangerousMethodCall();
 } catch (Exception e) {
          Console.WriteLine("Ой!");
 }

Обычно ошибка в SomeDangerousMethod() приводит к краху программы, но использование try/catch означает, что такая ошибка в SomeDangerousMethod() вернет управление в вызывающий код, с последующей передачей блоку catch, а тот выведет «Ой!», элегантно обработав вашу ошибку. Это не повод становиться программистом-неряхой, потому что код обработки исключений вроде этого здорово тормозит – уж лучше заранее выполнять проверки в коде!

Вы можете перехватывать несколько типов исключительных ситуаций, добавив новые блоки catch; выполнится лишь один, соответствующий конкретному исключению; а если возможны непредвиденные ситуации, следует, вероятно, добавить общий обработчик Exception – это базовый класс всех исключительных ситуаций, соответствующий всем исключениям вообще.

try {
   DangerousMethod();
 } catch (DllNotFoundException e) {
   Console.WriteLine("Ой - отсутствует необходимая DLL!");
 } catch (FileNotFoundException e) {
   Console.WriteLine("Ой - отсутствует необходимый файл!");
 } catch (Exception e) {
   Console.WriteLine("Ой - произошла ошибка!");
 }

Преимущество соответствия конкретному исключению в том, что вы получаете дополнительные данные для обработки. Например, FileNotFoundException имеет свойство FileName, которое подскажет, какой файл отсутствует.

В истинно устойчивой как скала программе следует использовать блоки try/catch почаще – при желании их даже можно вкладывать друг в друга, чтобы предусмотреть самые причудливые ошибки. Предусмотрены исключения для всех сортов типичных проблем: OutOfMemoryExceptions, AccessViolationException и немаловажное NullReferenceException. Не скупитесь на проверки!

И, наконец, finally...

Было бы неверно описать try/catch, не сказав об его кузене try/finally. Он используется намного реже, чем try/catch, вследствие общего заблуждения, что в .NET-коде незачем беспокоиться об управлении памятью. Что ж, вот вам изящный поворот: всякий раз, когда вы беретесь за собственный код, будьте с памятью поосторожнее. Объекты, классы и ресурсы .NET все под контролем, то есть автоматически освобождаются, когда больше не нужны, но собственные ресурсы – например, 3D-текстуры, загруженные вами в OpenGL – не управляемы, и о них необходимо позаботиться вам. Блок try/finally разработан, чтобы обезопасить управление памятью, путем насильственного выполнения заданного блока кода, невзирая ни на что. Это полезно даже помимо управления памятью, потому что вы будете уверены, что определенный метод вызовется перед тем, как объект будет освобожден.

Перейдем от теории к практике на примере: когда клиент подключается к вашему серверу, вы хотите отослать ему текст приветствия, прочитать какой-то текст, отправить текст-прощание, затем закрыть сокет. Посмотрите этот код:

 function ClientConnect(MySocket sock) {
        try {
                sock.SendHello();
                sock.ReadMessage();
                sock.SendGoodbye();
        } catch (Exception e) {
                Console.WriteLine("При подключении клиента возникла ошибка.");
        }
 }

Вам этот код может показаться вполне пригодным, но вдруг в процессе чтения возникнет ошибка – предположим, клиент отправит неправильно оформленное сообщение? Вот что произойдет:

  • 1 Вызов SendHello()
  • 2 Вызов ReadMessage()
  • 3 Возникло исключение
  • 4 Вызов Console.WriteLine()
  • 5 Метод завершился

Как видите, в этой последовательности событий нет SendGoodbye(), то есть сообщение-прощание никогда не будет отослано. Если клиент ожидает его, или сервер использует этот метод для выполнения некой очистки собственных ресурсов, тогда у вас проблема. Тут-то и выходит на сцену try/finally, потому что он в общем гарантирует, что определенный блок кода вызовется при любом раскладе. Перепишем предыдущий пример:

 void ClientConnect(MySocket sock) {
        try {
                 try {
                         sock.SendHello();
                         sock.ReadMessage();
                   } catch (Exception e) {
                            Console.WriteLine ("При подключении клиента возникла ошибка.");
                   }
        } finally {
                   sock.SendGoodbye();
        }
 }

Даже если в SendHello() или в ReadMessage() возникнет исключение, SendGoodbye все равно будет вызван. На самом деле, работает даже нечто вроде этого:

 void ClientConnect(MySocket sock) {
        try {
                   sock.SendHello();
                   sock.ReadMessage();
                   return;
        } finally {
                   sock.SendGoodbye();
        }
 }

Вызов return должен бы привести к немедленному выходу из метода, да и приводит – но .NET все-таки сначала выполняет все блоки finally. Даже старый метод Environment.Exit() находит время для вызова блоков finally перед завершением программы – а если вы не хотите, чтобы ваш блок finally выполнился (поэтому я и сказал, что блоки finally «в общем гарантируют», а не «абсолютно гарантируют» выполнение блока кода), используйте метод Environment.FailFast(). LXF

Перехватываемые исключения

AccessViolationException Возникает, когда вы пытаетесь записать в область памяти только для чтения.
ArgumentNullException Возникает, когда метод требует аргументы, а вы случайно передаете ему null.
DivideByZeroException Деление любых чисел на ноль – табу в любом языке программирования; перехватывается здесь!
DllNotFoundException Когда .NET создает ссылки на несуществующие родные библиотеки, возникает это исключение.
Exception Дедушка всех исключений; хорош для перехвата, когда вы не представляете, что может произойти.
IndexOutOfRangeException Возникает при выходе за границы и попытке чтения несуществующего элемента массива.
NullReferenceException Вы получаете это при попытке читать из несозданного объекта.
OutOfMemoryException Системе не хватает памяти, и, вероятно, ваша программа будет закрыта.
Персональные инструменты
купить
подписаться
Яндекс.Метрика