В жизни каждого .NET программиста рано или поздно наступает момент, когда ему приходится парсить строку в int, или DateTime, или какой-нибудь decimal. Ничего сложного, скажете вы, int.Parse(source) - и дело в шляпе. А если в source пусто, или вообще не число лежит, то как в результате получить null (конечно тогда ожидаемым типом будет int? )?
Опять пустяки, скажете вы! Только надо заключить логику конвертирования в метод расширения:
Code:
На стороне клиента вызов метода конвертации будет смотреться очень логично и прозрачно:
Теперь было бы полезно иметь метод, выкидывающий свой эксепшен при неудаче конвертации, а так же метод, проверяющий возможность конвертации:
Обобщенный метод конвертации
Прикинув сколько подобного кода придется писать для конвертации всех простых типов, любой адекватный программист поматерится про себя и будет думать, как бы все это обобщить.
Первое что приходит в голову - использовать генерики, а покопавшись в гугле можно отыскать подобные универсальные варианты реализации:
Первое что приходит в голову - использовать генерики, а покопавшись в гугле можно отыскать подобные универсальные варианты реализации:
В данном примере 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, только с отрицанием.
{тип}|{дополнительные параметры для парсинга}
Пример файла:
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|
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 - она способна сэкономить кучу времени и нервов.
Комментариев нет:
Отправить комментарий