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

LXF99:Mono

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
 
Строка 12: Строка 12:
  
 
Этот пример достаточно прост для затравки, но он ставит в тупик некоторых начинающих. Допустим, у вас есть объект '''List<string>''' с именем '''MyNames''', то есть он хранит массив строк. Поскольку это обобщенный тип данных, вам следует добавить '''using System.Collections.Generic'''; в  начало вашего файла проекта. И если вы хотите удалить из этого списка все имена, начинающиеся с '''"Mike"''', то первый вариант вашего кода может выглядеть так:
 
Этот пример достаточно прост для затравки, но он ставит в тупик некоторых начинающих. Допустим, у вас есть объект '''List<string>''' с именем '''MyNames''', то есть он хранит массив строк. Поскольку это обобщенный тип данных, вам следует добавить '''using System.Collections.Generic'''; в  начало вашего файла проекта. И если вы хотите удалить из этого списка все имена, начинающиеся с '''"Mike"''', то первый вариант вашего кода может выглядеть так:
<code>
+
<source lang=csharp>
 
  for (int i = 0; i < MyNames.Count; ++i) {
 
  for (int i = 0; i < MyNames.Count; ++i) {
 
   if (MyNames[i].StartsWith("Mike")) {
 
   if (MyNames[i].StartsWith("Mike")) {
Строка 18: Строка 18:
 
     }
 
     }
 
   }
 
   }
</code>
+
</source>
  
 
Или, если вы аккуратист, то так:
 
Или, если вы аккуратист, то так:
<code>
+
<source lang=csharp>
 
  foreach (string name in MyNames) {
 
  foreach (string name in MyNames) {
 
     if (name.StartsWith("Mike")) {
 
     if (name.StartsWith("Mike")) {
Строка 27: Строка 27:
 
     }
 
     }
 
   }
 
   }
</code>
+
</source>
  
 
Но здесь налицо серьезная проблема: '''.NET''' не разрешает изменять массив, пока вы перемещаетесь по нему, то есть первый '''Mike''' будет удален, но цикл продолжится, и появится ошибка, потому что на самом деле вы сдвинули все элементы на одну позицию, и какой же элемент должен быть следующим в цикле? Приведенное решение вполне очевидно, если немного подумать: при перемещении по массиву в обратную сторону сдвиг элементов не имеет значения, потому что вы его уже обработали. Вот оно:
 
Но здесь налицо серьезная проблема: '''.NET''' не разрешает изменять массив, пока вы перемещаетесь по нему, то есть первый '''Mike''' будет удален, но цикл продолжится, и появится ошибка, потому что на самом деле вы сдвинули все элементы на одну позицию, и какой же элемент должен быть следующим в цикле? Приведенное решение вполне очевидно, если немного подумать: при перемещении по массиву в обратную сторону сдвиг элементов не имеет значения, потому что вы его уже обработали. Вот оно:
<code>
+
<source lang=csharp>
 
   for (int i = MyNames.Count – 1; i >= 0; --i) {
 
   for (int i = MyNames.Count – 1; i >= 0; --i) {
 
     if (MyNames[i].StartsWith("Mike")) {
 
     if (MyNames[i].StartsWith("Mike")) {
Строка 36: Строка 36:
 
     }
 
     }
 
   }
 
   }
</code>
+
</source>
  
 
'''Проблема'''
 
'''Проблема'''
Строка 43: Строка 43:
  
 
По мере увеличения объема кода нарастает необходимость его чистки. Одной из наиболее раздражающих ловушек в коде на '''C#''' является '''Math.Round()''', потому что если вы захотите написать код  
 
По мере увеличения объема кода нарастает необходимость его чистки. Одной из наиболее раздражающих ловушек в коде на '''C#''' является '''Math.Round()''', потому что если вы захотите написать код  
<code>
+
<source lang=csharp>
 
   int foo = Math.Round(10.1f);
 
   int foo = Math.Round(10.1f);
</code>
+
</source>
 
он не сработает. О нет – вы получите сообщение об ошибке преобразования: '''Mono''' не умеет преобразовывать из '''double''' в '''int'''. Вы-то думали, что этот код преобразует число с плавающей точкой '''10.1''' в целое '''10''', но '''Math.Round()''' возвращает не целое – потому что если указать второй параметр, можно получить число, округленное до указанного знака после запятой.
 
он не сработает. О нет – вы получите сообщение об ошибке преобразования: '''Mono''' не умеет преобразовывать из '''double''' в '''int'''. Вы-то думали, что этот код преобразует число с плавающей точкой '''10.1''' в целое '''10''', но '''Math.Round()''' возвращает не целое – потому что если указать второй параметр, можно получить число, округленное до указанного знака после запятой.
  
 
Конечно, это лишнее, если вам всего лишь надо преобразовать число с плавающей точкой в целое, поэтому я предлагаю создать такой небольшой метод:
 
Конечно, это лишнее, если вам всего лишь надо преобразовать число с плавающей точкой в целое, поэтому я предлагаю создать такой небольшой метод:
<code>
+
<source lang=csharp>
 
   public int Round(float num) {
 
   public int Round(float num) {
 
     return (int)Math.Round(num);
 
     return (int)Math.Round(num);
 
   }
 
   }
</code>
+
</source>
  
 
Вы можете использовать его так:
 
Вы можете использовать его так:
<code>
+
<source lang=csharp>
 
   int foo = Round(10.1f);
 
   int foo = Round(10.1f);
</code>
+
</source>
 
        
 
        
 
Вам, вероятно, кажется, что можно и без него обойтись, но пред ставьте такой код:
 
Вам, вероятно, кажется, что можно и без него обойтись, но пред ставьте такой код:
<code>
+
<source lang=csharp>
 
   DrawRectangle((int)Math.Round(obj.x), (int)Math.Round(obj.y), (int)Math. Round(obj.w), (int)Math.Round(obj.h))
 
   DrawRectangle((int)Math.Round(obj.x), (int)Math.Round(obj.y), (int)Math. Round(obj.w), (int)Math.Round(obj.h))
</code>
+
</source>
  
 
Ну не уродство? Странно: в '''Java''' есть прекрасный метод '''Math.round()''', получающий '''float''', а возвращающий '''int''', а вот в '''C#''' требуется собственный код. Не опасайтесь снижения производительности за счет добавочного вызова функции: такой простой метод, вероятно, будет встроенным '''(inline)'''.
 
Ну не уродство? Странно: в '''Java''' есть прекрасный метод '''Math.round()''', получающий '''float''', а возвращающий '''int''', а вот в '''C#''' требуется собственный код. Не опасайтесь снижения производительности за счет добавочного вызова функции: такой простой метод, вероятно, будет встроенным '''(inline)'''.
Строка 72: Строка 72:
  
 
Стандартный класс '''List''' имеет метод '''Sort()''', который выстраивает строки и числа в определенном порядке, но он бесполезен, если вы храните объекты и хотите отсортировать их по определенному свойству. Однако вы можете сообщить '''Sort()''' имя сравнивающей функции, способной выполнять более продвинутую сортировку, а затем использовать ее обычным способом. Например, пусть у вас есть класс
 
Стандартный класс '''List''' имеет метод '''Sort()''', который выстраивает строки и числа в определенном порядке, но он бесполезен, если вы храните объекты и хотите отсортировать их по определенному свойству. Однако вы можете сообщить '''Sort()''' имя сравнивающей функции, способной выполнять более продвинутую сортировку, а затем использовать ее обычным способом. Например, пусть у вас есть класс
<code>
+
<source lang=csharp>
 
  public class User {
 
  public class User {
 
   public int ID;
 
   public int ID;
 
   public string Name;
 
   public string Name;
 
  }
 
  }
</code>
+
</source>
 
и '''List''' [Список] этих пользователей, вроде такого:
 
и '''List''' [Список] этих пользователей, вроде такого:
<code>
+
<source lang=csharp>
 
  List<User> MyUsers = new List<User>();
 
  List<User> MyUsers = new List<User>();
 
  User user = new User();
 
  User user = new User();
Строка 97: Строка 97:
 
  user.Name = "Graham";
 
  user.Name = "Graham";
 
  MyUsers.Add(user);
 
  MyUsers.Add(user);
</code>
+
</source>
  
 
Сортируя его при помощи обычного старого '''Sort()''', вы получите ошибку, ибо '''.NET''' не умеет обращаться с объектами '''User'''. Но не так уж трудно написать собственный метод сравнения сложных объектов. А если вы предоставите его имя функции '''Sort()''', он будет вызываться для каждого сравнения двух объектов, чтобы решить, в каком порядке их расположить. Метод должен возвращать '''1''' (объект '''1''' должен следовать после объекта '''2'''), '''-1''' (объект '''2''' должен следовать за объектом '''1''') или '''0''' (объекты '''1''' и '''2''' равноправны). Он может выглядеть примерно так:
 
Сортируя его при помощи обычного старого '''Sort()''', вы получите ошибку, ибо '''.NET''' не умеет обращаться с объектами '''User'''. Но не так уж трудно написать собственный метод сравнения сложных объектов. А если вы предоставите его имя функции '''Sort()''', он будет вызываться для каждого сравнения двух объектов, чтобы решить, в каком порядке их расположить. Метод должен возвращать '''1''' (объект '''1''' должен следовать после объекта '''2'''), '''-1''' (объект '''2''' должен следовать за объектом '''1''') или '''0''' (объекты '''1''' и '''2''' равноправны). Он может выглядеть примерно так:
<code>
+
<source lang=csharp>
 
  private int CompareUserByID(User a, User b) {
 
  private int CompareUserByID(User a, User b) {
 
   if (a.ID > b.ID) {
 
   if (a.ID > b.ID) {
Строка 110: Строка 110:
 
   }
 
   }
 
  }
 
  }
</code>
+
</source>
  
 
Затем сортируйте ваш массив, используя
 
Затем сортируйте ваш массив, используя
<code>
+
<source lang=csharp>
 
  MyUsers.Sort(CompareUserByID);
 
  MyUsers.Sort(CompareUserByID);
</code>
+
</source>
  
 
Теперь все элементы будут переставлены. Приведенный способ показывает, как создать свою собственную систему сортировки для любого типа данных, но встроенные типы данных – '''int, string''' и т.п. – можно сравнивать еще проще. Все эти типы имеют специальный метод '''CompareTo()''', принимающий в качестве единственного параметра другой идентичный тип и возвращающий вам '''1''', '''-1''' или '''0'''. Поэтому, если хотите, можете написать метод '''CompareUserByName()''' вот так:
 
Теперь все элементы будут переставлены. Приведенный способ показывает, как создать свою собственную систему сортировки для любого типа данных, но встроенные типы данных – '''int, string''' и т.п. – можно сравнивать еще проще. Все эти типы имеют специальный метод '''CompareTo()''', принимающий в качестве единственного параметра другой идентичный тип и возвращающий вам '''1''', '''-1''' или '''0'''. Поэтому, если хотите, можете написать метод '''CompareUserByName()''' вот так:
<code>
+
<source lang=csharp>
 
  private int CompareUserByName(User a, User b) {
 
  private int CompareUserByName(User a, User b) {
 
   return a.Name.CompareTo(b.Name);
 
   return a.Name.CompareTo(b.Name);
 
  }
 
  }
</code>
+
</source>
  
 
'''Проблема'''
 
'''Проблема'''
Строка 131: Строка 131:
  
 
Простой путь перемешать массив таков:
 
Простой путь перемешать массив таков:
<code>
+
<source lang=csharp>
 
   public void ShuffleList(List<string> list) {
 
   public void ShuffleList(List<string> list) {
 
     Random rand = new Random();
 
     Random rand = new Random();
Строка 140: Строка 140:
 
     }
 
     }
 
   }
 
   }
</code>
+
</source>
 
В цикле перемещаемся по массиву, удаляя каждый элемент и вставляя его в случайную позицию. Заметьте: код генерирует случайное число, используя новый объект '''Random''' при каждом вызове метода; это лучше, чем создание одного объекта '''Random''' для всей программы.
 
В цикле перемещаемся по массиву, удаляя каждый элемент и вставляя его в случайную позицию. Заметьте: код генерирует случайное число, используя новый объект '''Random''' при каждом вызове метода; это лучше, чем создание одного объекта '''Random''' для всей программы.
  
 
Итак, вот схема перемешивания: взять '''List''', содержащий строки, и перемешать их случайным образом. А если вы захотите перемешать массив целых чисел? Или объектов '''User'''? Или чего угодно, но не строк? Можно, конечно, создать несколько методов '''ShuffleList()''', но это недальновидное решение: ваш код очень скоро раздуется. Намного лучше использовать стандарты, создав функцию, которая принимает список любого типа и перемешивает содержимое. Тут требуется некий специальный синтаксис '''C#''', потому что вам необходимо сообщить своему методу, что он будет принимать неизвестный тип и использовать этот тип для всех данных. Обычно на обобщенные типы ссылаются как на '''T''' или '''T1, T2''' и т.д., если их более одного. Итак, метод '''ShuffleList()''' можно переписать так:
 
Итак, вот схема перемешивания: взять '''List''', содержащий строки, и перемешать их случайным образом. А если вы захотите перемешать массив целых чисел? Или объектов '''User'''? Или чего угодно, но не строк? Можно, конечно, создать несколько методов '''ShuffleList()''', но это недальновидное решение: ваш код очень скоро раздуется. Намного лучше использовать стандарты, создав функцию, которая принимает список любого типа и перемешивает содержимое. Тут требуется некий специальный синтаксис '''C#''', потому что вам необходимо сообщить своему методу, что он будет принимать неизвестный тип и использовать этот тип для всех данных. Обычно на обобщенные типы ссылаются как на '''T''' или '''T1, T2''' и т.д., если их более одного. Итак, метод '''ShuffleList()''' можно переписать так:
<code>
+
<source lang=csharp>
 
   public void ShuffleList<T>(List<T> list) {
 
   public void ShuffleList<T>(List<T> list) {
 
       Random rand = new Random();
 
       Random rand = new Random();
Строка 153: Строка 153:
 
       }
 
       }
 
   }
 
   }
</code>
+
</source>
  
 
Когда вы используете '''ShuffleList(MyUsers)''', '''.NET''' по сути заменяет в этом методе «'''Т'''» на «'''User'''», т.е. '''ShuffleList()''' принимает '''List<User>''', а переменная '''tmp''' получает тип '''User'''. Итак, вы можете вызвать '''ShuffleList()''' со списком '''[List]''' строк, целых чисел, дробных, логических, людей, рыбок или данных любого другого типа, какой сможете придумать.
 
Когда вы используете '''ShuffleList(MyUsers)''', '''.NET''' по сути заменяет в этом методе «'''Т'''» на «'''User'''», т.е. '''ShuffleList()''' принимает '''List<User>''', а переменная '''tmp''' получает тип '''User'''. Итак, вы можете вызвать '''ShuffleList()''' со списком '''[List]''' строк, целых чисел, дробных, логических, людей, рыбок или данных любого другого типа, какой сможете придумать.
Строка 162: Строка 162:
  
 
Это весьма общая формулировка, а вот конкретный пример: вы хотите знать, когда мышь находится над нарисованным вами объектом. Проблема решается очень просто: все, что надо сделать – это проверить, что координаты мыши больше, чем позиция '''X''' и '''Y''' вашего объекта, и меньше чем '''X''', '''Y + ширина и высота объекта'''. Вчерне можно записать подобный метод так:
 
Это весьма общая формулировка, а вот конкретный пример: вы хотите знать, когда мышь находится над нарисованным вами объектом. Проблема решается очень просто: все, что надо сделать – это проверить, что координаты мыши больше, чем позиция '''X''' и '''Y''' вашего объекта, и меньше чем '''X''', '''Y + ширина и высота объекта'''. Вчерне можно записать подобный метод так:
<code>
+
<source lang=csharp>
 
  public bool PointOverRect(int x1, int y1, int x2, int y2, int width, int height) {
 
  public bool PointOverRect(int x1, int y1, int x2, int y2, int width, int height) {
 
   if (x1 >= x2 && x1 <= x2 + width) {
 
   if (x1 >= x2 && x1 <= x2 + width) {
Строка 171: Строка 171:
 
   return false;
 
   return false;
 
  }
 
  }
</code>
+
</source>
  
 
Для использования этого метода передайте координаты '''X''' и '''Y''' мыши в качестве первых двух параметров, затем '''X''', '''Y''', ширину и высоту вашего объекта в качестве вторых параметров. Конечно, реально это работает только для прямоугольных объектов, но создавайте прямоугольные рамки вокруг объектов другой формы, и все будет хорошо.'''[для фигур произвольной формы часто в качестве второго входного параметра используется массив координат узлов контура, ограничивающего объект или, если контур является геометрической фигурой, то ее атрибуты, например, центр и радиус окружности, – прим. пер.]'''
 
Для использования этого метода передайте координаты '''X''' и '''Y''' мыши в качестве первых двух параметров, затем '''X''', '''Y''', ширину и высоту вашего объекта в качестве вторых параметров. Конечно, реально это работает только для прямоугольных объектов, но создавайте прямоугольные рамки вокруг объектов другой формы, и все будет хорошо.'''[для фигур произвольной формы часто в качестве второго входного параметра используется массив координат узлов контура, ограничивающего объект или, если контур является геометрической фигурой, то ее атрибуты, например, центр и радиус окружности, – прим. пер.]'''
Строка 184: Строка 184:
  
 
К счастью, для прямоугольников есть другой небольшой полезный метод, под названием '''Intersect()''', который накладывает один прямоугольник на другой и возвращает новый прямоугольник-пересечение, и вы можете проверить его ширину и высоту, чтобы понять, имеет ли место пересечение. Простой и легкий способ проверки столкновений – вот такой метод:
 
К счастью, для прямоугольников есть другой небольшой полезный метод, под названием '''Intersect()''', который накладывает один прямоугольник на другой и возвращает новый прямоугольник-пересечение, и вы можете проверить его ширину и высоту, чтобы понять, имеет ли место пересечение. Простой и легкий способ проверки столкновений – вот такой метод:
<code>
+
<source lang=csharp>
 
   public bool RectOverRect(int x1, int y1, int width1, int height1, int x2, int y2, int width2, int height2) {
 
   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 rectthis = new Rectangle(x1, y1, width1, height1);
Строка 195: Строка 195:
 
     }
 
     }
 
   }
 
   }
</code>
+
</source>
  
 
'''Проблема'''
 
'''Проблема'''
Строка 202: Строка 202:
  
 
Я не затрагивал старый добрый блок '''try/catch''' в нашей серии, но теперь настало время это сделать! Система '''try/catch''' позволяет выполнять команды и предпринимать заданные действия, если возникла ошибка. Например, вы можете написать:
 
Я не затрагивал старый добрый блок '''try/catch''' в нашей серии, но теперь настало время это сделать! Система '''try/catch''' позволяет выполнять команды и предпринимать заданные действия, если возникла ошибка. Например, вы можете написать:
<code>
+
<source lang=csharp>
 
   try {
 
   try {
 
           SomeDangerousMethodCall();
 
           SomeDangerousMethodCall();
Строка 208: Строка 208:
 
           Console.WriteLine("Ой!");
 
           Console.WriteLine("Ой!");
 
   }
 
   }
</code>
+
</source>
  
 
Обычно ошибка в '''SomeDangerousMethod()''' приводит к краху программы, но использование '''try/catch''' означает, что такая ошибка в '''SomeDangerousMethod()''' вернет управление в вызывающий код, с последующей передачей блоку '''catch''', а тот выведет «Ой!», элегантно обработав вашу ошибку. Это не повод становиться программистом-неряхой, потому что код обработки исключений вроде этого здорово тормозит – уж лучше заранее выполнять проверки в коде!
 
Обычно ошибка в '''SomeDangerousMethod()''' приводит к краху программы, но использование '''try/catch''' означает, что такая ошибка в '''SomeDangerousMethod()''' вернет управление в вызывающий код, с последующей передачей блоку '''catch''', а тот выведет «Ой!», элегантно обработав вашу ошибку. Это не повод становиться программистом-неряхой, потому что код обработки исключений вроде этого здорово тормозит – уж лучше заранее выполнять проверки в коде!
  
 
Вы можете перехватывать несколько типов исключительных ситуаций, добавив новые блоки '''catch'''; выполнится лишь один, соответствующий конкретному исключению; а если возможны непредвиденные ситуации, следует, вероятно, добавить общий обработчик '''Exception''' – это базовый класс всех исключительных ситуаций, соответствующий всем исключениям вообще.
 
Вы можете перехватывать несколько типов исключительных ситуаций, добавив новые блоки '''catch'''; выполнится лишь один, соответствующий конкретному исключению; а если возможны непредвиденные ситуации, следует, вероятно, добавить общий обработчик '''Exception''' – это базовый класс всех исключительных ситуаций, соответствующий всем исключениям вообще.
<code>
+
<source lang=csharp>
 
  try {
 
  try {
 
     DangerousMethod();
 
     DangerousMethod();
Строка 223: Строка 223:
 
     Console.WriteLine("Ой - произошла ошибка!");
 
     Console.WriteLine("Ой - произошла ошибка!");
 
   }
 
   }
</code>
+
</source>
  
 
Преимущество соответствия конкретному исключению в том, что вы получаете дополнительные данные для обработки. Например, '''FileNotFoundException''' имеет свойство '''FileName''', которое подскажет, какой файл отсутствует.
 
Преимущество соответствия конкретному исключению в том, что вы получаете дополнительные данные для обработки. Например, '''FileNotFoundException''' имеет свойство '''FileName''', которое подскажет, какой файл отсутствует.
Строка 239: Строка 239:
  
 
Перейдем от теории к практике на примере: когда клиент подключается к вашему серверу, вы хотите отослать ему текст приветствия, прочитать какой-то текст, отправить текст-прощание, затем закрыть сокет. Посмотрите этот код:
 
Перейдем от теории к практике на примере: когда клиент подключается к вашему серверу, вы хотите отослать ему текст приветствия, прочитать какой-то текст, отправить текст-прощание, затем закрыть сокет. Посмотрите этот код:
<code>
+
<source lang=csharp>
 
   function ClientConnect(MySocket sock) {
 
   function ClientConnect(MySocket sock) {
 
         try {
 
         try {
Строка 249: Строка 249:
 
         }
 
         }
 
   }
 
   }
</code>
+
</source>
  
 
Вам этот код может показаться вполне пригодным, но вдруг в процессе чтения возникнет ошибка – предположим, клиент отправит неправильно оформленное сообщение? Вот что произойдет:
 
Вам этот код может показаться вполне пригодным, но вдруг в процессе чтения возникнет ошибка – предположим, клиент отправит неправильно оформленное сообщение? Вот что произойдет:
Строка 259: Строка 259:
  
 
Как видите, в этой последовательности событий нет '''SendGoodbye()''', то есть сообщение-прощание никогда не будет отослано. Если клиент ожидает его, или сервер использует этот метод для выполнения некой очистки собственных ресурсов, тогда у вас проблема. Тут-то и выходит на сцену '''try/finally''', потому что он в общем гарантирует, что определенный блок кода вызовется при любом раскладе. Перепишем предыдущий пример:
 
Как видите, в этой последовательности событий нет '''SendGoodbye()''', то есть сообщение-прощание никогда не будет отослано. Если клиент ожидает его, или сервер использует этот метод для выполнения некой очистки собственных ресурсов, тогда у вас проблема. Тут-то и выходит на сцену '''try/finally''', потому что он в общем гарантирует, что определенный блок кода вызовется при любом раскладе. Перепишем предыдущий пример:
<code>
+
<source lang=csharp>
 
   void ClientConnect(MySocket sock) {
 
   void ClientConnect(MySocket sock) {
 
         try {
 
         try {
Строка 272: Строка 272:
 
         }
 
         }
 
   }
 
   }
</code>
+
</source>
  
 
Даже если в '''SendHello()''' или в '''ReadMessage()''' возникнет исключение, '''SendGoodbye''' все равно будет вызван. На самом деле, работает даже нечто вроде этого:
 
Даже если в '''SendHello()''' или в '''ReadMessage()''' возникнет исключение, '''SendGoodbye''' все равно будет вызван. На самом деле, работает даже нечто вроде этого:
<code>
+
<source lang=csharp>
 
   void ClientConnect(MySocket sock) {
 
   void ClientConnect(MySocket sock) {
 
         try {
 
         try {
Строка 285: Строка 285:
 
         }
 
         }
 
   }
 
   }
</code>
+
</source>
  
 
Вызов '''return''' должен бы привести к немедленному выходу из метода, да и приводит – но '''.NET''' все-таки сначала выполняет все блоки '''finally'''. Даже старый метод '''Environment.Exit()''' находит время для вызова блоков '''finally''' перед завершением программы – а если вы не хотите, чтобы ваш блок '''finally''' выполнился (поэтому я и сказал, что блоки '''finally''' «в общем гарантируют», а не «абсолютно гарантируют» выполнение блока кода), используйте метод '''Environment.FailFast()'''. '''LXF'''
 
Вызов '''return''' должен бы привести к немедленному выходу из метода, да и приводит – но '''.NET''' все-таки сначала выполняет все блоки '''finally'''. Даже старый метод '''Environment.Exit()''' находит время для вызова блоков '''finally''' перед завершением программы – а если вы не хотите, чтобы ваш блок '''finally''' выполнился (поэтому я и сказал, что блоки '''finally''' «в общем гарантируют», а не «абсолютно гарантируют» выполнение блока кода), используйте метод '''Environment.FailFast()'''. '''LXF'''

Текущая версия на 10:06, 27 ноября 2008

Содержание

[править] 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.Count1; 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 Системе не хватает памяти, и, вероятно, ваша программа будет закрыта.
Персональные инструменты
купить
подписаться
Яндекс.Метрика