読者です 読者をやめる 読者になる 読者になる

match があれば TryParse いらないんじゃないか

C#

Scala の match を真似て C# で色々書いているわけですが、なんかもう TryParse は Obsolete でいいんじゃ、と思えてきました。

例えばですよ、

public static T MatchInt<T>(this string self, Func<int, T> ifMatch, Func<T> ifNotMatch)
{
    int result;
    return int.TryParse(self, out result) ? ifMatch(result)
                                          : ifNotMatch();
}

public static T MatchInt<T>(this string self, Func<int, T> ifMatch)
{
    return MatchInt<T>(self, ifMatch, () => default(T));
}

こんな拡張メソッドを用意しておけば、

int result;
if (!int.TryParse(str, out result))
{
    // 何か処理
    return hoge;
}
// 何か処理
return piyo;

なんて書かずに、

return str.MatchInt(
    i =>  { /* 何か処理 */ return piyo; },
    () => { /* 何か処理 */ return hoge; }
);

って式で書けるんですよ。出力引数もいりません。
例えば数値に変換できるなら変換して返し、できないなら -1 を返す、なんてのは

return str.MatchInt(i => i, () => -1);

ですよ。-1 じゃなくて 0 でいいなら、

return str.MatchInt(i => i);

です。


こんなのも用意したとします。

public static void MatchInt(this string self, Action<int> ifMatch, Action ifNotMatch)
{
    MatchInt<object>(self, i => { ifMatch(i); return null; }, () => { ifNotMatch(); return null; });
}

public static void MatchInt(this string self, Action<int> ifMatch)
{
    MatchInt(self, ifMatch, () => {});
}

で、int に変換できるなら標準出力に出力したいなんてのは、

str.MatchInt(Console.WriteLine);

こうです!


あとは、これを T4 Template

<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" language="C#v3.5" debug="true" hostSpecific="true" #>
<#@ output extension=".cs" encoding="UTF-8" #>
<#@ Assembly Name="System.dll" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #> 
<#
// TryParseを持っている型を好きなだけ並べるといい
var targets = new[] { "int", "double", "DateTime" };
#>
using System;

public static partial class StringExtension
{
<#
foreach (var target in targets) {
    var camelCase = target.ToUpper()[0] + target.Substring(1);
#>
    #region <#= target #>への変換
    
    public static T Match<#= camelCase #><T>(this string self, Func<<#= target #>, T> ifMatch, Func<T> ifNotMatch)
    {
        <#= target #> result;
        return <#= target #>.TryParse(self, out result) ? ifMatch(result)
                                              : ifNotMatch();
    }
    
    public static T Match<#= camelCase #><T>(this string self, Func<<#= target #>, T> ifMatch)
    {
        return Match<#= camelCase #><T>(self, ifMatch, () => default(T));
    }
    
    public static void Match<#= camelCase #>(this string self, Action<<#= target #>> ifMatch, Action ifNotMatch)
    {
        Match<#= camelCase #><object>(self, converted => { ifMatch(converted); return null; }, () => { ifNotMatch(); return null; });
    }

    public static void Match<#= camelCase #>(this string self, Action<<#= target #>> ifMatch)
    {
        Match<#= camelCase #>(self, ifMatch, () => {});
    }
    #endregion
<# } #>
}

自動生成!


bool みたいに取り得る値の範囲が狭いなら、

public static T TrueFalseOther<T>(this string self, Func<T> truePart, Func<T> falsePart, Func<T> otherPart)
{
    bool result;
    if (bool.TryParse(self, out result))
        return result ? truePart() : falsePart();
    return otherPart();
}

public static void TrueFalseOther(this string self, Action truePart, Action falsePart, Action otherPart)
{
    TrueFalseOther<object>(
        () => { truePart(); return null; },
        () => { falsePart(); return null; },
        () => { otherPart(); return null; });
}

とするのもありでしょう。

return str.TrueFalseOther(() => 1, () => 0, () => -1);

素敵!


・・・と、ここまで来て何ですが、この方法には重大な欠点があります。
それは、これ全部 string の拡張メソッドとして実現しているという点です。
「"".」まで打ったらメソッドがどばー。
それに、何でもかんでも string のメソッドって意味的にどうなの?ってのもあります。


そこに目を瞑れるなら、アリなんじゃないでしょうか?
少なくとも個人的には、式でわかりやすく記述できる範囲が増えるのでアリです。