Как писать высококлассный код. Часть третья. Ошибки, на которые никто не обращает внимания
Это третья часть серии статей под общим названием «Как писать высококлассный код». Предыдущие части:
- Как писать высококлассный код. Часть первая
- Как писать высококлассный код. Часть вторая. Возможности Visual Studio 2010
Сегодня же мы поговорим об ошибках, которые легко допустить, но не так легко со временем выловить.
Форматирование строк и String.Format
Форматирование строк – пожалуй, одна из самых часто используемых операций при написании кода. Такие операции, как string.Format(), someObject.ToString() мы вызываем повсеместно. Но часто забываем о такой важной детали, как настройки глобализации.
Рассмотрим на примере:
using System; namespace BeautifulCode { class Program { static void Main(string[] args) { DateTime dt = DateTime.Now; Console.WriteLine(dt.ToString()); } } }
В первом случае программа нам выведет 31/01/2011 22:55:45 (так как у меня стоят американские настройки).
А теперь попробуем изменить настройки глобализации:
using System;
using System.Threading; using System.Globalization; namespace BeautifulCode { class Program { static void Main(string[] args) { Thread thread = Thread.CurrentThread; thread.CurrentCulture = new CultureInfo("ru-RU"); DateTime dt = DateTime.Now; Console.WriteLine(dt.ToString()); } } }
Результат выполнения этого кода: 31.01.2011 22:59:00.
Пока вы используете методы ToString(), Format(), Parse() только для отображения дат и строк, то, вероятно, вы не попадете в одну из тех ужасных ситуаций, когда после заливки работающего (локально) кода на сервер вы получите красивый желтый экран для ASP.NET приложений и падения Windows Forms приложения.
Но все может быть еще хуже, если ваша логика завязана на разбор дат, строк и т.д. И ох как часто этот разбор также происходит без указания настроек локализации! А с вами случались такие ситуации, когда простым string.Replace() вы меняли точки на запятые и наоборот перед вызовом double.Parse() или float.Parse()? Причина этому – все те же региональные настройки.
Ах, да! И не путайте CurrentCulture и CurrentUICulture. CurrentCulture отвечает за региональные настройки, в первую очередь, за отображение дат и формата чисел и валют. CurrentUICulture отвечает за язык, на котором будут отображаться текстовые строки в интерфейсе (такие строки как login, edit, delete, new и т.д.). Т.е. у вас может быть русский интерфейс, но американский формат даты или английский интерфейс и наш формат даты.
Как же быть в таких случаях:
- всегда используйте перегруженные методы ToString(), Format() и Parse(), где необходимо указать CultureInfo. Лучше всего писать CultureInfo.CurrentCulture и особо не заморачиваться;
- для всех приложений указывайте региональные настройки по умолчанию;
- для всех UI компонентов также устанавливайте региональные настройки
- при сравнении строк используйте метод Equals с указанием параметра StringComparer.InvariantCultureIgnorCase или StringComparer.CurrentCultureIgnorCase. Более детально об этом можно почитать в MSDN
- не использовать == и != для сравнения строк
Для скептиков и людей, которые считают это очевидным, приведу один пример, когда одно онлайн-казино недосчиталось около 90 тыс. евро при пересчете с одной валюты на другую, при этом забыв в функции конвертирования применить региональные настройки. Пришлось полдня всем немного помучиться, восстанавливая статус-кво.
Раз мы уже заговорили о финансовых операциях, то необходимо добавить, что при математических и финансовых расчетах всегда необходимо использовать тип decimal вместо double. А то получится также, как и с одним горе-разработчиком, который отправлял остатки округления себе на счет. Все равно ведь поймают : )
String vs. StringBuilder
Это вторая распространенная ошибка при использовании строк. В .NET есть типы данных mutable и immutable – что значит изменяемые и неизменяемые типы данных.
Изменяемые типы допускают изменение своих свойств при некоторых операциях, неизменяемые объекты всегда возвращают новый объект.
В .NET изменяемым типом является класс StringBuilder, неизменяемым – класс String.
Таким образом, если вы часто используете операции над строками – конкатенацию, удаление, поиск и т.д., то в памяти сохраняются все созданные вами строки. Поэтому для этих целей необходимо использовать класс StringBuilder. Ошибка такого рода грозит вам исключением OutOfMemoryException при больших операциях над строками.
Перехват и бросание исключений
Если у вас несколько уровней абстракции, что часто возникает задача «перебросить» исключение дальше – вниз по иерархии. В таких случаях часто используют выражение throw ex; В то время когда правильный вариант – просто throw;
Ошибка такого рода грозит вам тем, что информация о stack trace на более низком уровне придет не совсем корректной, что чревато последствиями при отладке и протоколировании работы системы.
Магические цифры
Магическими цифрами называют все константы, которые используются непосредственно в теле программы.
Посмотрим на такой пример:
if(mode == 3) { ... } else if(mode == 4) { ... }
Когда человек, не знакомый с кодами всех режимом попытается изменить или отладить ваш код, то, наверняка, он столкнется с некоторыми сложностями при трактовке ваших чисел.
Один из вариантов – использовать константные переменные и хранить их в каком-нибудь классе (важно помнить, что они должны быть константами, чтобы быть полностью уверенным, что никто нигде в программе их не сможет переопределить).
Второй вариант – использование перечислений:
if(mode == MyEnum.ShowAllUsers) { ... } else if(mode == MyEnum.ShowOnlyActiveUsers) { ... }
Теперь ваш код становится понятным даже постороннему человеку.
Использование общих сущностей вместо частных
Рассмотрим очень простой пример:
public static int Count(string[] array) { return array.Length; }
Что с этим кодом может быть не так?
Все очень просто – а вдруг мы захотим узнать размер списка объектов или коллекции типа ArrayList? Наша функция не справиться с этим.
Поэтому более правильный вариант – использовать более общий тип, например, IList, что позволит расширить использование этого метода на большее количество сценариев:
public static int Count(IList<string> array) { return array.Count; }
Пример использования:
int count = Count(new string[] {"1", "2", "3" }); int count2 = Count(new List<string> { "1", "2", "3" });
Честно признаюсь, я иногда тоже забываю использовать интерфейсы вместо конкретных реализаций.
Работа с I/O
Очень часто разработчики забывают указывать полные пути к файлам и директориям. Это не критично, если используются классы и методы, которые понимают относительные пути. Но здесь кроется другая проблема: локальные пути и пути на сервере могут не совпадать (а точнее почти всегда не совпадают).
Именно поэтому для перестраховки всегда необходимо преобразовывать локальные пути в полные. Это можно сделать несколькими способами:
Для ASP.NET – Server.MapPath(“~”) будет возвращать корень проекта, для элементов управления можно использовать ResolveUrl.
В Module или Handler получить полный путь можно с помощью такого кода:
string path = VirtualPathUtility.ToAbsolute("~/admin/paths.aspx");
В более общем случае можно использовать AppDomain.CurrentDomain.BaseDirectory.
Если это Windows Forms приложение, то можно воспользоваться таким кодом:
using System.IO; using System.Windows.Forms; string appPath = Path.GetDirectoryName(Application.ExecutablePath);
или
using System.IO; using System.Reflection; string path = Path.GetDirectoryName( Assembly.GetAssembly(typeof(MyClass)).CodeBase);
Предлагаю к ознакомлению замечательную статью на тему относительных и абсолютных путей.
Приведение типов
Еще одна часто встречающаяся ошибка, которая не вызовет ошибку компиляции, но с легкостью может упасть в рантайме – это ошибка небезопасного приведения типов.
Пример небезопасного приведения типов:
ArrayList list = new ArrayList(); list.Add(1); var item = (int)list[0];
Реальность может быть такой, что первый элемент может быть не типом int и тогда этот код в райнтайме упадет.
Для безопасного приведения типов используются операторы is и as. Is используется для проверки объекта на его тип, as используется для безопасного преобразования (если тип будет неверный, то исключение не будет брошено).
Еще один совет, связанный с приведением типов – использование ArrayList крайне нежелательное. В-первых, есть вероятность ошибки, описанной выше, а кроме того, еще и работает намного медленнее List
Динамические типы
Так как мы уже используем C# 4.0, то необходимо также сказать о динамических типах данных, которые объявляются с помощью dynamic. Компилятор ничего не будет знать о типе объекта до того момента, как его запросит другой элемент программы в рантайме и если что-то пойдет не так, то ошибки вам не избежать. Поэтому нужно очень осторожно использовать динамические переменные во избежание проблем с отладкой.
В третьей части серии статей мы рассмотрели типичные ошибки, которые совершают разработчики и которые легко локализировать, зная о них, и очень сложно, если процесс разработки зашел слишком далеко. В следующей части поговорим об инструменте для статистического анализа кода под названием FxCop.
Компании из статьи
Microsoft Украина | Украинское подразделение компании Microsoft. |