Как писать высококлассный код. Часть третья. Ошибки, на которые никто не обращает внимания

понедельник, 31 января 2011, Александр Краковецкий

Это третья часть серии статей под общим названием «Как писать высококлассный код». Предыдущие части:

Сегодня же мы поговорим об ошибках, которые легко допустить, но не так легко со временем выловить.

Форматирование строк и 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 Украина


Сайт:
http://www.microsoft.com/ukr/ua/

Microsoft Украина Украинское подразделение компании Microsoft.

Ищите нас в интернетах!

Комментарии

Свежие вакансии