Общий метод анализа без бокса

Я пытаюсь написать общий метод Parse, который преобразует и возвращает строго типизированное значение из NamedValueCollection. Я попробовал два метода, но оба этих метода проходят через бокс и unboxing, чтобы получить значение. Кто-нибудь знает, как избежать бокса? Если бы вы видели это в процессе производства, вам это не понравилось бы, насколько это плохо для производительности?

Usuage:

var id = Request.QueryString.Parse<int>("id");

Попытка # 1:

public static T Parse<T>(this NameValueCollection col, string key)
{
    string value = col[key];

    if (string.IsNullOrEmpty(value))
        return default(T);

    if (typeof(T) == typeof(int))
    {
        //return int.Parse(value); // cannot convert int to T
        //return (T)int.Parse(value); // cannot convert int to T
        return (T)(object)int.Parse(value); // works but boxes
    }
    if (typeof(T) == typeof(long))
    {
        return (T)(object)long.Parse(value); // works but boxes
    }
    ...

    return default(T);
}

Попытка № 2 (с использованием отражения):

public static T Parse<T>(this NameValueCollection col, string key)
{
    string value = col[key];

    if (string.IsNullOrEmpty(value))
        return default(T);

    try
    {
        var parseMethod = typeof(T).GetMethod("Parse", new Type[] { typeof(string) });

        if (parseMethod == null)
            return default(T);

        // still boxing because invoke returns an object
        var parsedVal = parseMethod.Invoke(null, new object[] { value });
        return (T)parsedVal;
    }
    // No Proper Parse Method found
    catch(AmbiguousMatchException) 
    {
    }

    return default(T);
}

Ответы

Ответ 1

Я думаю, вы уже оцениваете влияние бокса/распаковки. Метод parse будет иметь гораздо большие накладные расходы (синтаксический анализ строк), что приводит к снижению стоимости бокса. Кроме того, все утверждения if будут иметь большее влияние. Отражение имеет наибольшее влияние всех.

Мне бы не хотелось видеть этот код в производстве, так как есть более чистый способ сделать это. Основная проблема, с которой я столкнулся, - это большое количество операторов if, которые вам нужно будет охватить все случаи, и тот факт, что кто-то может передать ему какой-либо старый тип.

Что бы я сделал, так это написать функцию синтаксического анализа для каждого типа, который я хочу проанализировать (например, ParseInt()). Это яснее, и четко определено, что будет пытаться выполнить функция. Также с короткими статическими методами компилятор с большей вероятностью встраивает их, сохраняя вызов функции.

Я думаю, что это плохое применение дженериков, какая-то особая причина для этого?

Ответ 2

public static T Parse<T>(this NameValueCollection col, string key)
{
  return (T)Convert.ChangeType(col[key], typeof(T));
}

Я не совсем уверен в коробках ChangeType или нет (я думаю, что чтение документов скажет мне, но сейчас я настаиваю на времени), но, по крайней мере, он избавляется от всей этой проверки типов. Накладные расходы бокса не очень высоки, поэтому я не стал бы слишком беспокоиться об этом. Если вы беспокоитесь о согласованности типа времени выполнения, я бы написал функцию как:

public static T Parse<T>(this NameValueCollection col, string key)
{
  T value;

  try
  {
    value = (T)Convert.ChangeType(col[key], typeof(T));
  }
  catch
  {
    value = default(T);
  }

  return value;
}

Таким образом, функция не будет бомбить, если значение не может быть преобразовано по какой-либо причине. Это означает, конечно, что вам нужно будет проверить возвращаемое значение (которое вам все равно придется делать, поскольку пользователь может редактировать запрос).

Ответ 3

Я добавлю небольшой недокументированный способ:

public static T Convert<T>()
{
    if (typeof(T) == typeof(int))
    {
        int a = 5;
        T value = __refvalue(__makeref(a), T);
        return value;
    }
    else if (typeof(T) == typeof(long))
    {
        long a = 6;
        T value = __refvalue(__makeref(a), T);
        return value;
    }

    throw new NotImplementedException();
}

Существует мало документации о них, но они работают с С# 4.0. Читайте, например, здесь Скрытые возможности С#? Помните, что недокументированные средства не поддерживаются, бла-бла-бла не может работать в будущем, если вы используете их, дьявол придет за вы бла-бла-бла: -)

Ответ 4

Для лучшей читаемости вы можете использовать общий словарь с анонимной функцией следующим образом:

var parserFuncs = new Dictionary<Type, Func<string, object>>() {
    { typeof(int), p => (int) int.Parse(p) },
    { typeof(bool), p => (bool) bool.Parse(p) },
    { typeof(long), p => (long) long.Parse(p) },
    { typeof(short), p => (short) short.Parse(p) },
    { typeof(DateTime), p => (DateTime) DateTime.Parse(p) }
    /* ...same for all the other primitive types */
};

return (T) parserFuncs[typeof(T)](value);

Ответ 5

Другое предложение для реализации, используя метод TryParse или Parse с общим подходом. Я написал это первоначально, чтобы преобразовать строки, проанализированные из файла csv, в разные типы, int, decimal, list и т.д.

 public static bool TryParse<T>(this string value, out T newValue, T defaultValue = default(T))
        where T : struct, IConvertible
    {
        newValue = defaultValue;
        try
        {
            newValue = (T)Convert.ChangeType(value, typeof(T));
        }
        catch
        {
            return false;
        }
        return true;
    }

    public static T Parse<T>(this string value)
        where T : struct, IConvertible
    {
        return (T) Convert.ChangeType(value, typeof (T));
    }

Здесь метод parse try сначала устанавливает значение newValue в значение по умолчанию, затем пытается преобразовать значение в тип T и возвращает newValue как тип T. Если преобразование завершается неудачно, оно возвращает значение по умолчанию T.

Метод Parse просто пытается выполнить преобразование, однако, если он не прошел проверку безопасности и генерирует исключение, если преобразование завершается неудачно.

Ответ 6

int value = int.Parse(Request.QueryString["RecordID"]);

Ответ 7

Здесь предлагается реализация, следуя логике Роберта Вагнера, но используя общий подход к сокращению дублирования:

public static int ParseInt32(this NameValueCollection col, string key)
{
    return Parse(col, key, int.Parse);
}
public static double ParseDouble(this NameValueCollection col, string key)
{
    return Parse(col, key, double.Parse);
}
private static T Parse<T>(NameValueCollection col, string key, Func<string, T> parse)
{
    string value = col[key];

    if (string.IsNullOrEmpty(value))
        return default(T);

    return parse(value);
}

По правде говоря, возвращение нуля для пустой или пустой строки пугает меня; это может вызвать проблемы, если некоторые значения законно равны нулю. Вместо этого у меня бы методы возвращали значения nullables (int?, double? и т.д.), Что является чуть более компактным подходом, чем шаблон out-parameter, используемый для фреймворческих методов TryParse. Вы можете сделать это:

public static int? ParseInt32(this NameValueCollection col, string key)
{
    return Parse(col, key, int.Parse);
}
public static double? ParseDouble(this NameValueCollection col, string key)
{
    return Parse(col, key, double.Parse);
}
private static T? Parse<T>(NameValueCollection col, string key, Func<string, T> parse)
    where T : struct    
{
    string value = col[key];

    if (string.IsNullOrEmpty(value))
        return default(T?);

    return parse(value);
}

Но это все равно вызовет исключение для строк, отличных от нуля или пустых, которые не являются числовыми. Лучше использовать TryParse. Встроенные делегаты Func не поддерживают параметры ref или out, поэтому вам придется объявить свой собственный тип делегата, но это довольно тривиально.