суббота, 11 января 2014 г.

Методы расширения для конвертации строк в простые типы с помощью t4 кодогенерации (C#)

В жизни каждого .NET программиста рано или поздно наступает момент, когда ему приходится парсить строку в int, или DateTime, или какой-нибудь decimal. Ничего сложного, скажете вы, int.Parse(source) - и дело в шляпе. А если в source пусто, или вообще не число лежит, то как в результате получить null (конечно тогда ожидаемым типом будет int? )?
Опять пустяки, скажете вы! Только надо заключить логику конвертирования в метод расширения:

Code:
public static int? TryConvertToInt(this string source)
{
     int result;
     if(int.TryParse(source, out result))
          return result;
     return null;
}
На стороне клиента вызов метода конвертации будет смотреться очень логично и прозрачно:
int a = "12345".TryConvertToInt();
Теперь было бы полезно иметь метод, выкидывающий свой эксепшен при неудаче конвертации, а так же метод, проверяющий возможность конвертации:
public static class ConverterExtensions
{
 public static bool TryConvertToInt(
 this string input,
 out int value)
 {
  return int.TryParse(input, NumberStyles.Number, CultureInfo.InvariantCulture, out value);
 }
 public static int ConvertToIntOrThrow(this string input, string)
 {
  int value;
  if (TryConvertToInt(input, out value))
   return value;
  throw new ContractViolationException("Value '{0}' can not be converted to int.", input);
 }
 public static bool IsInt(this string input)
 {
  int value;
  return TryConvertToInt(input, out value);
 }
 public static bool IsNotInt(this string input)
 {
  return !IsInt(input);
 }
}
Обобщенный метод конвертации
Прикинув сколько подобного кода придется писать для конвертации всех простых типов, любой адекватный программист поматерится про себя и будет думать, как бы все это обобщить.
Первое что приходит в голову - использовать генерики, а покопавшись в гугле можно отыскать подобные универсальные варианты реализации:
public static bool TryConvertTo<T>(this string input, out T value, Type type = null)
{
 if (type == null)
  type = typeof(T);
 var converter = TypeDescriptor.GetConverter(type);
 try
 {
  value = (T)converter.ConvertFromString(null, CultureInfo.InvariantCulture, input);
  return true;
 }
 catch (Exception)
 {
  value = default(T);
  return false;
 }
}
В данном примере TypeDescriptor.GetConverter( type ) во время выполнения по типу генерика получает нужный конвертер (наследник от TypeConverter), с помощью которого осуществляется конвертация.
И вроде можно радоваться: написав 5 методов расширения с генериками значительно упрощается вызов конвертации на стороне клиента для всех простых типов( например "123.45".ConvertTo<decimal>(); ), но опытный программист заметит 2 неприятных момента, ухудшающих производительность по сравнению с первоначальным вариантом:
  • боксинг/анбоксинг сконвертированного значения
    value = ( T ) converter.ConvertFromString( null, CultureInfo.InvariantCulture, input );
  • если строка не может быть преобразована в требуемый тип, выбрасывается исключение NotSupportedException, которое необходимо отловить при реализации TryConvertTo, а в методе ConvertOrThrow< T > придется повторно выбрасывать исключение уже своего типа (например IncorrectFormatException )Методы парсинга строки для простых типов ( на подобии xxx.TryParse() ) реализованы так, что не генерируют исключений.
Заранее скажу что в боевых условиях переход с TypeConverter на xxx.TryParse() уменьшил использование CPU на 15% (при довольно частых конвертациях, половина из которых завершались фэйлом).
T4 кодогенерация
Чтобы с одной стороны сохранить возможность парсинга любого простого типа из строки, а с другой - писать как можно меньше кода, можно воспользоваться замечательным инструментом - t4 кодогенерацией. Тут можно скачать файл с tt шаблоном, который генерирует статический класс с методами расширения. Для каждого из конвертируемых типов сгенеруется по 5 методов:
  • int? ConvertToInt(this string input) - если невозможно сконвертировать, то вернется null.
  • bool TryConvertToInt(this string input, out int value )
  • int ConvertToIntOrThrow(this string input, string parameterName = "") - если невозможно сконвертировать, то выбросится исключение (выбрасываемое исключение нужно настроить в методе GetFormatException). В моем случае parameterName кастомизирует сообщение об ошибке:
    •  Если он не пуст - то выбрасывается "Parameter '{parameterName}' has value '{value}' that is can't converted to type {type}."
    • Иначе - "Value '{value}' can not be converted to type {type}."
  • bool IsInt( this string input ) - проверка строки на возможность конвертации. Фактически тот же TryConvertToInt, только без out-параметра.
  • bool IsNotInt( this string input ) - То же самое что и IsInt, только с отрицанием.
Чтобы он заработал, надо поправить неймспейс и положить в проект рядом с шаблоном файл конфигурации - TypesForConvertation.txt -  с перечислением всех типов, для которых будут сгенерированы методы конвертации. Формат каждой строки файла такой: 
{тип}|{дополнительные параметры для парсинга}

Пример файла:
int| NumberStyles.Number, CultureInfo.InvariantCulture,
long| NumberStyles.Number, CultureInfo.InvariantCulture,
TimeSpan| CultureInfo.InvariantCulture,
decimal| NumberStyles.Number, CultureInfo.InvariantCulture,
DateTime| CultureInfo.InvariantCulture, DateTimeStyles.None,
bool|
Guid|

{дополнительные параметры для парсинга} вставляются в метод TryParse() между входной строкой и out-параметром с результатом парсинга. Их понадобилось вводить для того, чтобы выставить CultureInfo.InvariantCulture (так необходимо было в проекте ).
Конечно, некоторые возможности парсинга теряются при таком подходе, например, установка IFormatProvider или стиля для каждого метода в отдельности, но в подавляющем большинстве случаев в проекте этого попросту не требовалось, а значит усложнять незачем.
Из интересного в сгенерированном коде разве что пара атрибутов:
  • GeneratedCode указывает инструментам по анализу кода (например подсчету метрик кода в VS), что данный класс сгенерирован и его не надо учитывать.
  • Pure над методами нужен для CodeContracts. Он говорит о том, что метод "чистый", т.е. не изменяет состояние объекта. 
Заключение
Конвертация строк в простые типы - часто встречаемая операция на любых проектах. Ее надо выносить в методы расширения для улучшения читаемости кода, а эти методы выносить в nuget-пакет со всеми остальными часто применяемыми экстеншенами. В плане производительности конвертацию лучше всего  осуществлять методами xxx.TryParse(). Ну и конечно не забывать про кодогенерацию t4 - она способна сэкономить кучу времени и нервов.

Комментариев нет:

Отправить комментарий