Подтвердить что ты не робот

Как сделать общий анализатор чисел в С#?

Чтобы разобрать строку в int, вызывается Int32.Parse(string), для double, Double.Parse(string), long, Int64.Parse(string) и т.д.

Можно ли создать метод, который делает его универсальным, например, ParseString<T>(string)? где T может быть Int32, Double и т.д. Я заметил, что у ряда типов нет общего интерфейса, а у методов Parse нет общего родителя.

Есть ли способ достичь этого или что-то похожее на это?

4b9b3361

Ответ 1

В основном вам пришлось бы использовать отражение, чтобы найти соответствующий статический метод Parse, вызвать его и привести возвращаемое значение обратно к T. В качестве альтернативы вы можете использовать Convert.ChangeType или получить соответствующий TypeDescriptor и связанный с ним TypeConverter.

Более ограниченный, но эффективный (и в некотором смысле простой) подход заключается в том, чтобы сохранить словарь от типа к делегату синтаксического анализа - привести делегат к Func<string, T> и вызвать его. Это позволило бы вам использовать разные методы для разных типов, но вам нужно было бы знать типы, которые необходимо преобразовать в предварительный.

Что бы вы ни делали, вы не сможете указать общее ограничение, которое сделало бы его безопасным во время компиляции. На самом деле вам нужно что-то вроде моего представления о статических интерфейсах для такого рода вещей. ОБНОВЛЕНИЕ: Как уже упоминалось, есть интерфейс IConvertible, но это не обязательно означает, что вы сможете конвертировать из string. Другой тип может реализовывать IConvertible без какого-либо способа преобразования в этот тип из строки.

Ответ 2

На самом деле стандартные типы номеров действительно реализуют общий интерфейс: IConvertible. Это тот, который Convert.ChangeType использует.

К сожалению, нет эквивалента TryParse, он будет выдавать исключения, если строка не может быть проанализирована.

Кстати, кажется, что вся эта область "конверсии" была полностью забыта командой BCL. Там нет ничего нового, начиная с .NET Framework 1 (кроме методов TryParse).

Ответ 3

Это очень смешно, но работает с использованием Newtonsoft.Json(Json.NET):

 JsonConvert.DeserializeObject<double>("24.11");
 // Type == System.Double - Value: 24.11

 JsonConvert.DeserializeObject<int>("29.4");
 // Type == System.Int32 - Value: 29

Ответ 4

Я написал код, который использует отражение, чтобы найти методы Parse/TryParse для типа и получить доступ к ним из общих функций:

private static class ParseDelegateStore<T>
{
    public static ParseDelegate<T> Parse;
    public static TryParseDelegate<T> TryParse;
}

private delegate T ParseDelegate<T>(string s);
private delegate bool TryParseDelegate<T>(string s, out T result);


public static T Parse<T>(string s)
{
    ParseDelegate<T> parse = ParseDelegateStore<T>.Parse;
    if (parse == null)
    {
        parse = (ParseDelegate<T>)Delegate.CreateDelegate(typeof(ParseDelegate<T>), typeof(T), "Parse", true);
        ParseDelegateStore<T>.Parse = parse;
    }
    return parse(s);
}

public static bool TryParse<T>(string s, out T result)
{
    TryParseDelegate<T> tryParse = ParseDelegateStore<T>.TryParse;
    if (tryParse == null)
    {
        tryParse = (TryParseDelegate<T>)Delegate.CreateDelegate(typeof(TryParseDelegate<T>), typeof(T), "TryParse", true);
            ParseDelegateStore<T>.TryParse = tryParse;
    }
    return tryParse(s, out result);
}

https://github.com/CodesInChaos/ChaosUtil/blob/master/Chaos.Util/Conversion.cs

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

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

Ответ 5

Да, типы, которые могут быть проанализированы из строки, скорее всего, будут иметь статические перегрузки Parse и TryParse, которые вы можете найти через отражение, как предположил Джон.

private static Func<string, T> GetParser<T>()
{
    // The method we are searching for accepts a single string.
    // You can add other types, like IFormatProvider to target specific overloads.
    var signature = new[] { typeof(string) };

    // Get the method with the specified name and parameters.
    var method = typeof(T).GetMethod("Parse", signature);

    // Initialize the parser delegate.
    return s => (T)method.Invoke(null, new[] { s });
}

Для производительности вы также можете создавать лямбда-выражения, вызывающие их, поскольку метод Invoke принимает параметры метода как object[], который является ненужным распределением, и если ваши параметры включают типы значений, вызывает бокс. Он также возвращает результат как object, который также вызывает бокс, когда ваш тип является типом значения.

private static Func<string, T> GetParser<T>()
{
    // Get the method like we did before.
    var signature = new[] { typeof(string) };
    var method = typeof(T).GetMethod("Parse", signature);

    // Build and compile a lambda expression.
    var param = Expression.Parameter(typeof(string));
    var call = Expression.Call(method, param);
    var lambda = Expression.Lambda<Func<string, T>>(call, param);
    return lambda.Compile();
}

Вызов скомпилированного лямбда-выражения по существу так же быстро, как вызов самого оригинального метода синтаксического анализа, но создание и компиляция его в первую очередь - нет. Вот почему, опять же, как предложил Джон, мы должны кэшировать полученный делегат.

Я использую статический универсальный класс для кэширования парсеров в ValueString.

private static class Parser<T>
{
    public static readonly Func<string, T> Parse = InitParser();

    private static Func<string, T> InitParser()
    {
        // Our initialization logic above.
    }
}

После этого ваш метод синтаксического анализа может быть записан следующим образом:

public static T Parse<T>(string s)
{
    return Parser<T>.Parse(s);
}