Denis Gladkikh
Russian   |  English

Метод расширение для безопасного приведения типов

В добавление к записи Дмитрия Нестерука - Паттерны методов расширения хотел бы добавить еще один метод расширение “Приведение типов”, без которого мне уже сложно обходиться. Автор идеи этого подхода работает сейчас в фирме, где я работал раньше.

Нам часто приходится писать, примерно, такой код:

int intValue;
if (obj == null || !int.TryParse(obj.ToString(), out intValue))
    intValue = 0;

Это способ безопасного приведения к типу int. Напрашивается сразу же какой либо унифицированный метод для безопасного приведения типов.

Мне нравится подход вынесения преобразования в extension method и использовать его затем следующим образом:

int i;
i = "1".To<int>();
// i == 1
i = "1a".To<int>();
// i == 0 (default value of int)
i = "1a".To(10);
// i == 10 (set as default value 10)
i = "1".To(10);
// i == 1
// ********** Nullable sample **************
int? j;
j = "1".To<int?>();
// j == 1
j = "1a".To<int?>();
// j == null
j = "1a".To<int?>(10);
// j == 10
j = "1".To<int?>(10);
// j == 1

И соответственно реализация данного подхода:

public static class Parser
{
    /// <summary>
    /// Try cast <paramref name="obj"/> value to type <typeparamref name="T"/>,
    /// if can't will return default(<typeparamref name="T"/>)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <returns></returns>
    public static T To<T>(this object obj)
    {
        return To(obj, default(T));
    }
 
    /// <summary>
    /// Try cast <paramref name="obj"/> value to type <typeparamref name="T"/>,
    /// if can't will return <paramref name="defaultValue"/>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public static T To<T>(this object obj, T defaultValue)
    {
        if (obj == null)
            return defaultValue;
 
        if (obj is T)
            return (T) obj;
 
        Type type = typeof (T);
 
        // Place convert to reference types here
 
        if (type == typeof(string))
        {
            return (T)(object)obj.ToString();
        }
 
        Type underlyingType = Nullable.GetUnderlyingType(type);
        if (underlyingType != null)
        {
            return To(obj, defaultValue, underlyingType);
        }
 
        return To(obj, defaultValue, type);
    }
 
    private static T To<T>(object obj, T defaultValue, Type type)
    {
        // Place convert to sructures types here
 
        if (type == typeof(int))
        {
            int intValue;
            if (int.TryParse(obj.ToString(), out intValue))
                return (T)(object)intValue;
            return defaultValue;
        }
 
        if (type == typeof(long))
        {
            long intValue;
            if (long.TryParse(obj.ToString(), out intValue))
                return (T)(object)intValue;
            return defaultValue;
        }
 
        if (type == typeof(bool))
        {
            bool bValue;
            if (bool.TryParse(obj.ToString(), out bValue))
                return (T)(object)bValue;
            return defaultValue;
        }
 
        if (type.IsEnum)
        {
            if (Enum.IsDefined(type, obj))
                return (T)Enum.Parse(type, obj.ToString());
            return defaultValue;
        }
 
        throw new NotSupportedException(string.Format("Couldn't parse to Type {0}", typeof(T)));
    }
}

Данный метод не полон – это уже моя собственная реализация данного подхода, которую я использую внутри своего сайта. Он умеет работать с int, long, bool, string, enums (и с Nullable типами этих типов), но, думаю, по ходу работы вам не составит труда дополнять данный парсер необходимыми типами (главное не забывайте про культуры при необходимости).

Очевидно, что данный подход хорошо использовать только внутри знающих что делает данный метод разработчиков, так как не совсем очевидно, почему этот метод не может преобразовать любой тип A к типу B.

Progg it


Вас также может заинтересовать

rss twitter

Комментарии (42)

Павел ( ) #
avatar
Спасибо. Так удобней и самое главное код читается не хуже, а даже лучше.
А в первом примере int.TryParse в случае ошибки и так вернет 0. Зачем еще раз приравнивать к 0 или я ошибаюсь?
Denis Gladkikh ( ) #
avatar
Павел, компилятор не даст работать если не установить intValut = 0. Скажет "Use of unassigned local variable 'intValue'"
zandroid ( ) #
avatar
А почему не используется стандартный класс Convert? У него есть метода как приведения к стандартным типам, так и к произвольному типу, и доступен он был ещё до появления расширяющих методов.
Denis Gladkikh ( ) #
avatar
zandroid, Convert безусловно хороший класс, но он же небезопасно делает каст, то есть такой вызов Convert.ToInt32("a1") выдаст exception. В этом же случае можно либо "a1".To(0) - вернется 0, либо "a1".To() - вернется null
zandroid ( ) #
avatar
Я имел в виду не использование ВМЕСТО вашего подхода, а ВНУТРИ. :) Я тоже использую расширяющие методы для приведения, но внутри реализации опираюсь на класс Convert (оборачивая его в try-catch), вместо того, чтобы делать перебор возможных типов выданных данных. (А до появления расширяющих был просто набор статических методов, в которых параметром шло значение по умолчанию)
Denis Gladkikh ( ) #
avatar
zandroid, понял.

Да, наверное, вы правы, было бы проще использовать так же Convert.ChangeType, чем перебирать типы.
Denis Gladkikh ( ) #
avatar
В итоге можно вместо последнего метода использовать такой:

Last method

private static T To<T>(object obj, T defaultValue, Type type)
{
    if (type.IsEnum)
    {
        if (Enum.IsDefined(type, obj)) return (T) Enum.Parse(type, obj.ToString());
        return defaultValue;
    }
 
    try
    {
        return (T) Convert.ChangeType(obj, type);
    }
    catch (Exception e)
    {
        // Log exception
        return defaultValue;
    }
}
Дмитрий Пялов ( ) #
avatar
Мое ИМХО - не стоит делать Generic методов для базовых типов a la int, string, double - только потеря производительности, усложнение кода, а практической пользы никакой - разницы в написании между To и ToInt особой нет, ToInt даже выглядит красивее.

Кстати, посмотрите на тот же Convert - там присутствует весь ассортимент методов для базовых типов
Дмитрий Пялов ( ) #
avatar
В прошлом комментарии имелось в виду "To<int> и ToInt " - чертов HTML :)
Аноним ( ) #
avatar
-- режим зануды вкл. --
Метод скрывает факт применения значения по умолчанию. Пользователь метода об этом никак не может узнать. Мне кажется это опасным.
-- режим зануды выкл. --

Ну а вообще конечно удобно, если точно знаешь, что на ошибки можно забить.
Denis Gladkikh ( ) #
avatar
Аноним, именно, потому я и говорю, что "данный подход хорошо использовать только внутри знающих", а не выставлять наружу в своем движке. По поводу метода по умолчанию, тот много опять применений, все зависит от задачи, к пример если web и обрабатываем Request, то можно Request["id"].To<int?>() и смотреть если null то ругаться, если нет то обрабатывать. Так же, например, ConfigurationManager.AppSettings["MessageSendIntervalMin"].To<int>(10), ну и т.п. В общем много задач где его хорошо можно применить.

Дмитрий, согласен Convert.ToInt и т.п. более понятны, но к такому extension method очень быстро привыкаешь :)
зануда ( ) #
avatar
Да, согласен насчёт To.
зануда ( ) #
avatar
Тьфу ты. Согласен насчёт To угловая скобочка открывается int вопросик угловая скобочка закрывается.
Дмитрий Пялов ( ) #
avatar
Denis Gladkikh,

Я не имею ничего против Extension-метода, я лишь хочу сказать, что для базовых типов использовать Generic - не очень хорошее решение, лучше сделать отдельный Extension-метод - руководствуюсь своей практикой.
Denis Gladkikh ( ) #
avatar
Дмитрий, понял, но в случае большого количества extension методов ToInt, ToString, ToDecimal будет тяжело отыскивать обычные методы. Что не очень удобно бывает.
Andrew Bogoslovskiy ( ) #
avatar
Задачи, в которых применяется конвертация, в 99% случаев _нельзя_ использовать такие методы. Лучше пусть код выбросит исключение, чем вместо нужного значения даст его default значение, что просто лавинообразно даст вместо правильных данных непонятно что. Обычно это задачи расчетов для ученых, возможно импорт данных из Excel.
Статья очень интересная и каждый для себя нашел что-то полезное. Например, я, сделал, чтобы мне возвращалось Nullable, причем, T : struct. Таким образом, если парс удачный, то имею свойство HasValue = true и собственно - Value; если парс неудачен - получаю null.
Спасибо за статью!
xoposhiy ( ) #
avatar
Я бы параноидально вставил бы NumberFormatInfo.Invariant во все TryParse.

А то знаем мы их...
http://xoposhiy.livejournal.com/52457.html
Denis Gladkikh ( ) #
avatar
xoposhiy, ну тут все зависит от того что за систему пишите: интернациональную или локально. Вообще, конечно же тут явная проблема клиента, то что у него стояло "7" либо кривые руки, либо вирус. Удивительно как он не замечал никаких проблем с офисом и т.п.
xoposhiy ( ) #
avatar
Немного не так. Зависит от того, откуда пришел obj.
Вообще, чем больше я думаю про региональные языковые настройки, тем меньше мне нравится этот велосипед.

При необходимости аккуратно работать с obj как в инвариантной культуре, так и в локальной, граблей с этими ToString() -> TryParse не оберешься.
Denis Gladkikh ( ) #
avatar
xoposhiy, ну без этого тоже никак. Можно конечно себя обезопасить так, что внутри приложения держать всегда одни и те же региональные настройки, для хранения, чтения и т.п. А учитывать пользовательские только при отображении информации, но тут нужно будет часто задумываться когда какие использовать..

Насчет импорта данных, то тут конечно же тоже проблем может быть масса. Помню как то делал интеграцию посредством веб-сервисов, передавали данные в xml, так ребята на той стороне чуть ли не каждую неделю меняли форматы, то точки, то запятые, то даты меняли. Но было это конечно же поначалу, пока они там сами все настраивали, но приятного было мало, когда вроде все работало, готовы были показывать заказчику, а тут бац, на той стороне формат поменяли и все... В общем обезопасить себя от этого никак, конечно же, нельзя.
build_your_web ( ) #
avatar
API хорошо бы сделать таким:

"a1".To<Int>();
- вернет Exception

"a1".To<Int>().OrDefault(-1);
- вернет -1
xoposhiy ( ) #
avatar
build_your_web: Это же невозможно! :)
build_your_web ( ) #
avatar
я знаю, но было бы прикольно =).
в этом плане функциональные языки рулят.
Denis Gladkikh ( ) #
avatar
build_your_web, выглядит, конечно, очень читабельно. Можно конечно будет подумать над такой конструкцией, только извращаться придется сильно, нужно чтобы To<Int>() возвращал определенный класс, который как раз и будет парсить, либо приведением типа(который нужно написать), либо вызовом метода Default..
Вообще, конечно, в такой вариант можно множество всяческих вкусностей запихнуть, вроде
To<Int>().OrDefault() - вернет 0, а вот OrDefault(-1) - вернут -1, а еще можно сделать, OrExecute(method) - для передачи execption... В общем идея интересная, спасибо, только пока оставлю как есть у себя. Про будущее подумаю. :)
build_your_web ( ) #
avatar
Немного трансформировал:
http://snipt.org/Ignp

и теперь он проходит эти тесты:
[Test]
public void ConvertToInt()
{
Assert.AreEqual("321".To(), 321);
}

[Test]
public void OrDefault_Return0()
{
Assert.AreEqual("a1".To().OrDefault(), 0);
}

[Test]
public void OrDefault5_Return5()
{
Assert.AreEqual("a1".To().OrDefault(5), 5);
}

... http://snipt.org/Igog

Но есть проблема, всегда нужно явно или неявно приводить к нужныи типам (в тестах показан момент, когда Exception не возникает).
Denis Gladkikh ( ) #
avatar
build_your_web, ага как раз я о том и говорю, что нужно будет писать приведения. Как вариант можно, конечно, использовать такое "a1".To().OrDefault().Value, читается так же приятно, но код увеличивается. Прощается только в случае, если там будет, действительно (!), мощный фреймворк.
Andrew Bogoslovskiy ( ) #
avatar
Короче, все пришли к выводу, что ребята из Microsoft не такие уж и дураки и за время "доделки" .NET не просто так к методу T.Parse(...) добавили T.TryParse(...) и дальше не двинулись =). Ибо самое прозрачное и не конфликтующее с логикой решение. А с Extensions methods каждый по-своему начинает извращаться =)
Denis Gladkikh ( ) #
avatar
Andrew, отличное подитоживание. Главное, чтобы как приводила пример stumanova.livejournal.com в том же Microsoft, чтобы внутри команды не было по два реализованных парсеров и т.п. :)
Shakirov Ruslan ( ) #
avatar
Новая редакция
http://snipt.org/Ikq

Тесты:
"321".ToInt32(new { Default = 1 })
.Equals(321);

"321a".ToInt32(new { Default = 1 })
.Equals(1);
Andrew Bogoslovskiy ( ) #
avatar
Shakirov Ruslan, раз уж на то пошло, то почему бы не юзать вот такое:

public static class Ex
{
public static TDefault To(this object obj, TDefault defaultValue)
{
Type t = typeof(TDefault);

if (t.FullName.Equals("System.Int32"))
{
int res;
if (Int32.TryParse(obj.ToString(), out res))
return (TDefault)(object)res;
else
return defaultValue;
}

return defaultValue;
}
}

Вызов соответственно такой: int result = "12".To(1);

Причем компилятор сам поймет, что вы передаете в качестве default значения и уже нельзя написать такое: double result = "12".To(1);
Но конечно есть и минусы. Например, надо четко указывать в скобках тип. Допустим хотим Decimal, тогда надо писать 1.0m с суффиксом, чтобы компилятор знал, какой тип мы ему отдаем. Не правда ли очень просто и более прозрачно?
Denis Gladkikh ( ) #
avatar
Andrew, а у меня разве не тоже самое? :)

Ruslan, не понял в чем прелесть анонимных типов? Без них вроде было бы лучше.
Andrew Bogoslovskiy ( ) #
avatar
Причем "прикручивать" новые преобразования к double, decimal и прочим очень легко, просто добавляем копи-пастом (не люблю копи-паст, но все же) if-else конструкцию с проверкой по имени типа.
Andrew Bogoslovskiy ( ) #
avatar
Денис, я вообще-то твой код и скопипастил =)
Просто немного изменил, т.к. операция typeof() довольно ресурсоемкая. А пост мой к тому, что если уж и использовать default значение, то именно в твоей реализации.
У Руслана творческий процесс и это хорошо!
Denis Gladkikh ( ) #
avatar
Andrew, понял )

Андрей а откуда информация что Equals лучше чем сравнение с typeof?

Берем вот этот класс для тестирования.

Пишем легкий тест:
static void Main(string[] args)
{
  PerformanceTester tester = new PerformanceTester(() => { bool r = Action1(typeof(string)); });
  tester.MeasureExecTimeWithMetrics(1000000);
  Console.WriteLine(tester.TotalTime);
 
  PerformanceTester tester2 = new PerformanceTester(() => { bool r = Action2(typeof(string)); });
  tester2.MeasureExecTimeWithMetrics(1000000);
  Console.WriteLine(tester2.TotalTime);
 
  Console.ReadKey();
}
 
public static bool Action1(Type type)
{
  bool ret = false;
  if (type == typeof(int))
  {
    ret = true;
  }
  if (type == typeof(double))
  {
    ret = true;
  }
  if (type == typeof(string))
  {
    ret = true;
  }
  return ret;
}
 
public static bool Action2(Type type)
{
  bool ret = false;
  if (type.FullName.Equals("System.Int32"))
  {
    ret = true;
  }
  if (type.FullName.Equals("System.Double"))
  {
    ret = true;
  }
  if (type.FullName.Equals("System.String"))
  {
    ret = true;
  }
  return ret;
}


Результат (проигрыш у второго метода, причем в таких масштабах не значительный, вообще):
00:00:01.3145763
00:00:01.7558681

Или я что-то не то понял?
Andrew Bogoslovskiy ( ) #
avatar
Денис, если я не ошибаюсь, то type1 == type2 производит сравнивание не текстового представления, а возможно внутреннего уникального идентификатора RuntimeTypeHandle (возможно ошибаюсь).
Вам не стоит при каждом сравнивании вызывать операцию typeof(). Дело в том, что все зависит от области применения. Например, если это научные расчеты, то следует сделать так:

public static readonly Type TInt32 = typeof(int);
.....
и так далее

Т.е. наш статический класс при инициализации будет иметь уже нужные нам типы, с ними и надо сравнивать, т.е. if (type == TInt32) return ...;

В таком случае Вы не вызываете операцию typeof(). А если Вам надо преобразовать миллион переменных?

Если же надо сэкономить память и преобразование нужно не более сотни/тысячи раз, то да, конечно стоит использовать операцию typeof().

Я более чем уверен, что с type == TInt32 Ваш тест даст еще более лучший результат, чем String.Equals(String str).
Denis Gladkikh ( ) #
avatar
Андрей, согласен, спасибо.

Правда я теперь использую вариант Convert.ChangeType(obj, type), который я предложил чуть выше, и тогда мне ссылки на типы не нужны.

P.S. Кстати, подумал что надо бы сделать ссылки на комменты, а то как то не удобно.
Andrew Bogoslovskiy ( ) #
avatar
Я провел тест с использованием метода Equals() и сравнивания type == TInt32, для 1 миллиона итераций преобразования. Первый вариант дал 1.317 секунд, второй - 0.790, что почти в 2 раза.

А можно увидеть тело метода Convert.ChangeType(obj, type)?
Andrew Bogoslovskiy ( ) #
avatar
Опа, туплю =) Сейчас Reflector запущу =)
Andrew Bogoslovskiy ( ) #
avatar
Ну что и требовалось доказать.

internal static readonly Type[] ConvertTypes;

public static object ChangeType(object value, Type conversionType, IFormatProvider provider)
{
...
if (conversionType == ConvertTypes[9])
{
return convertible.ToInt32(provider);
}
....
if (conversionType == ConvertTypes[14])
{
return convertible.ToDouble(provider);
}
....
}
build_your_web ( ) #
avatar
Denis Gladkikh,

прелесть анонимных делегатов, что теперь становится интуитивно понятным написание этой функции.

Вариант "123".To<int>(1) плох тем, что неоднозначно понимание функции единицы. Один подумает, что это значения по умолчанию, другой что это ключ поведения (типа enum) так передали или еще что.

Или вот для сравнения
"123ф".To<int>(e => Logger.Write(e));
Что тут подразумевается запись эксепшена или запись входного значения, а может запись экспоненты числа?

"123ф".ToInt32(new {OnException = e => Logger.Write(e)});
Тут мы требуем от всех соблюдение ясного синтаксиса.

Вариант "123".To<int>().OrDefaut(1)
плох тем, что нужно явно приводить возвращаемое значение, иначе вместо сравнения с числом получим сравнение с классом, инкапсулирующий преобразования.

Если вас пугает копипастинг (хотя он тут минимальный, и взамен мы получаем возможность индивидуально рулить процессом преобразования разных типов), можете отрефакторить синтаксис до To<int>(). Это дело 10 минут и анонимные делегаты тут абсолютно непричем. Просто мне удобнее, чтобы IntelliSense дописывал полностью команду преобразования в число одним нажатием кнопки.
Denis Gladkikh ( ) #
avatar
Andrew, великолепно. То есть с использованием Convert как раз и уменьшаем использование оператора typeof.

build_your_web, тогда уж лучше передавать именно какой либо тип класса, тот же ConversionParameters, так как могут потом вылазить ошибки из-за такого кода:
o.ToInt32(new { Defalt = 1 })
Одну букву пропустили, визуально такое вообще может никто не заметить, год может проект работать, потом что-то свыше поменяли и ошибки поползли...

Более того будет интуитивно понятно кому больше? Тот кто будет читать код или писать? Пришел я к вам в команду, увидел метод ToInt(int default) или ToInt() и понял, что можно передавать дефолтное значение в метод если не распарсится (детали могу узнать из комментариев к методу). А вот в вашем случае будет ToInt(object default), что за object default? что туда передавать, int или еще чего, а ну придется детально разбирать комментарий к методу и понимать что нужно указывать анонимный тип со свойством Default...

В общем мне мил вариант без анонимных типов, не тот это случай, где их нужно применять, мое имхо. Все параметры скрывают свое предназначение в методах именами или комментариями.
Andrew Bogoslovskiy ( ) #
avatar
build_your_web по своему прав. Однако xml-документацию к коду никто не отменял, можно написать что функция возвращает и что принимает, и даже пример кода там написать, так что кому-что удобнее.
Ваш вариант читабелен, но в нем много букв, но самое главное много рефлексии, что ой-ей-ей как сказывается на производительности.
И вообще, устроили тут холивар :Р
Программисты всегда так, защищают свой код, ибо он самый лучший =)
Лично я всех выслушаю, что даст возможность с разных сторон взглянуть на такую проблему и выработать решение, которое будет работать =)
Добавить комментарий

Если вы хотите получать уведомления о новых комментариях к данному топику, укажите, пожалуйста, email и отметьте соответствующий пункт в форме. Если вы хотите добавить код в тексте комментария, то заключите его внутри тега [code]...[/code], более того можно уточнить язык, на котором написан данный код при помощи [code cs]...[/code], где вместо cs могут быть cs, html, xml, java, js, php, sql, cpp, css.

 

busy