Как сделать общий парсер чисел в C#?
чтобы разобрать строку на int, вызывается Int32.Parse(string)
, для двойной, Double.Parse(string)
, для длинных, Int64.Parse(string)
и так далее..
можно ли создать метод, который делает его универсальным, например, ParseString<T>(string)
? где T
может быть Int32
, Double
, etc. Я замечаю, что количество типов не реализует какой-либо общий интерфейс, и Parse
методы не имеют общего родителя.
есть ли способ достичь этого или чего-то подобного этому?
5 ответов
вам в основном придется использовать отражение, чтобы найти соответствующий статический Parse
метод, вызовите его и верните возвращаемое значение в T
. Кроме того, вы можете использовать Convert.ChangeType
или сделать соответствующий TypeDescriptor
и связанного с TypeConverter
.
более ограниченным, но эффективным (и простым, в некотором смысле) подходом было бы сохранить словарь от типа до разбора делегата - приведите делегат к Func<string, T>
и вызывать его. Это было бы позволяет использовать разные методы для разных типов, но вам нужно знать типы, которые необходимо преобразовать в Up-front.
что бы вы ни делали, вы не сможете указать общее ограничение, которое сделало бы его безопасным во время компиляции. Действительно, вам нужно что-то вроде моей идеи статические интерфейсы для такого рода вещи. EDIT: как уже упоминалось, есть IConvertible
интерфейс, но это не обязательно означает, что вы сможете конвертировать от string
. Другой тип мог бы реализовать IConvertible
без какого-либо способа преобразования в этот тип из строки.
на самом деле, стандартные типы номером do реализовать общий интерфейс: в разделе iconvertible. Это тот, который Convert.ChangeType
использовать.
к сожалению, нет TryParse
аналог, он будет бросать исключение, если строка не может быть проанализирована.
в качестве примечания, кажется, что вся эта область" преобразования " была полностью забыта командой BCL. Там нет ничего нового с .NET Framework 1 (кроме методов TryParse).
Это очень хакерский, но он работает с использованием 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
Я написал код, который использует отражение, чтобы найти 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
но я не тестировал их слишком много, поэтому у них могут быть некоторые ошибки/не работать правильно с каждым типом. Обработка ошибок также немного отсутствует.
и у них нет перегрузок для инвариантной культуры разбор. Так что вам, вероятно, нужно добавить это.
да, типы, которые могут быть проанализированы из строки, скорее всего, будут иметь static 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);
}