はじめに
int.TryParseやdouble.TryParseは便利ですね。
これをさらに便利に使えるようにして,かつエラーの可能性を下げたい,というのがこの記事の目的です。
やりたいこと
やりたいことは2つあります:
文字列のメソッドとして使いたい
こんな風にできると便利です。
string str = 10;
if (!str.TryParse<int>(out int val)) return;
Console.WriteLine(val);
ジェネリックな拡張メソッドを作ればいいのですが,TryParse<T>に渡す型パラメータごとに呼び出すメソッドの実態が異なるので,工夫が必要です。
エラーに強くしたい
上の例で,条件を間違えたとします。
string str = 10;
if (str.TryParse<int>(out int val)) return; // 条件を逆にしてしまった
Console.WriteLine(val);
上記コードは普通にコンパイルが通ってしまいますが,できればエラーにしたいところです。
実装
(2/6追記)
@chocolamint さんに IParsable<TSelf>を教えていただきました。
これを使うと,拡張メソッドを以下のように書けて,色々ごにょごにょしなくて大丈夫です。
static class StringUtil
{
public static bool TryParse<T>(this string str, out T value)
where T : struct, IParsable<T>
{
value = default;
return T.TryParse(str, null, out value);
}
public static T? ParseTo<T>(this string str)
where T : struct, IParsable<T>
{
T value = default;
if (!T.TryParse(str, null, out value)) return null;
return value;
}
}
以下の内容は,IParsable<TSelf>のようなインターフェースが無いときにどうするか,という内容になっています。いつか同じような工夫が必要になったときのために残しておきます。
(追記終わり)
ジェネリックなTryParse
static class TryParseWithExpressionTree
{
delegate bool TryParseDelegate<T>(string input, out T value);
public static bool TryParse<T>(string str, out T value)
where T : struct
{
return Cache<T>.TryParse(str, out value);
}
static class Cache<T>
where T : struct
{
public static readonly TryParseDelegate<T> TryParse;
static Cache()
{
var tryparse_method = typeof(T).GetMethod("TryParse",
new[] { typeof(string), typeof(T).MakeByRefType() });
if (tryparse_method is null)
{
TryParse = (string str, out T b) => {
T defaultValue = default;
b = defaultValue;
return false;
};
return;
}
var tryparse_arg_0 = Expression.Parameter(typeof(string));
var tryparse_arg_1 = Expression.Parameter(typeof(T).MakeByRefType());
var tryparse_args = new[] { tryparse_arg_0, tryparse_arg_1 };
var callexp = Expression.Call(tryparse_method, tryparse_args);
var lambdaexp = Expression.Lambda<TryParseDelegate<T>>(callexp, tryparse_args);
TryParse = lambdaexp.Compile();
}
}
}
最初に,各値型が持つ TryParse と同じシグネチャのデリゲート型を宣言します(TryParseDelegate)。
次に,Cache<T>.TryParse 変数に,各値型のTryParseを呼び出すデリゲートを保持します。ここでは,式木を使って実装してみました。
型によっては TryParse がないものもあるので,その場合は常に失敗するラムダ式を保持します。
C# のジェネリクスは型パラメータが異なると別の型になるので,Cache<int>.TryParse は int.TryParseを参照し,Cache<double>.TryParse は double.TryParse を参照します。
拡張メソッド化とエラー対策
static class StringUtil
{
public static bool TryParse<T>(this string str, out T value)
where T : struct
{
return TryParseWithExpressionTree.TryParse<T>(str, out value);
}
public static T? ParseTo<T>(this string str)
where T : struct
{
T value = default;
if (!TryParseWithExpressionTree.TryParse<T>(str, out value)) return null;
return value;
}
}
ここまでできれば,TryParse<T> を呼び出す拡張メソッドは簡単です(パラメータにthisつけるだけ)。
エラー対策として,null許容値型を返す ParseTo<T> も作ってみました。
こんな感じで使うことができます。
string str = "10";
if (str.ParseTo<int>() is not int x) return;
Console.WriteLine(x);
これだけだと TryParse と代り映えしないですが,パターンマッチを使っているのでエラー対策になります。
int xはパターンマッチ成功のときのみ初期化されるので,コンパイラがエラーを出してくれます。
string str = "10";
if (str.ParseTo<int>() is int x) return; // not が抜けた,条件を反転させた...
// 未割り当てのローカル変数 x エラーになる
// Console.WriteLine(x);
その他
ジェネリックなTryParseは以下のようにも書けます。
力技でガリガリ書く必要がある代わりに,typeof(T)の比較がJIT最適化で消え,高速になるはずです。
数値型の数は限られているので,こちらの方がよかったかも...
Unsafe.As<TFrom,TTo>は物騒な名前ですが,ここでは安全です。
typeof(T)が数値型と等しいことを確認してから呼んでいるので,必ずTFrom==TToになるためです。
static class TryParseWithTypeofBranching
{
public static bool TryParse<T>(string str, out T value)
where T : struct
{
value = default;
// if (typeof(T) == typeof(int))
// {
// ref var v = ref Unsafe.As<T, int>(ref value);
// return int.TryParse(str, out v);
// }
if (typeof(T) == typeof(int)) return int.TryParse(str, out Unsafe.As<T, int>(ref value));
if (typeof(T) == typeof(double)) return double.TryParse(str, out Unsafe.As<T, double>(ref value));
return false;
}
}