Как сделать общий анализатор чисел в С#?
Чтобы разобрать строку в int, вызывается Int32.Parse(string)
, для double, Double.Parse(string)
, long, Int64.Parse(string)
и т.д.
Можно ли создать метод, который делает его универсальным, например, ParseString<T>(string)
? где T
может быть Int32
, Double
и т.д. Я заметил, что у ряда типов нет общего интерфейса, а у методов Parse
нет общего родителя.
Есть ли способ достичь этого или что-то похожее на это?
Ответы
Ответ 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);
}