紅蓮のネタ帳

技術系の話を中心とした雑多なブログ

C#の.ToString()でハマった話

最近はお仕事でSQL Serverを弄りつつDelphiやったりC#やったりキーエンス独自言語やったりと色んな案件の仕事を振られている紅蓮です。
皆様いかがお過ごしでしょうか。

今回はC#のお話です。
C#初心者なので、.ToString()の説明もふわっと書いています。

C#の.ToString()は便利なメソッド

C#のだいたいの型には.ToString()メソッドが組み込まれており、これを呼んでしまえばオブジェクト型だろうがstring型に変換できるという優れもの。*1
型にもよるが、.ToString()が事実上の値取り出しのメソッドになっている場合もあり、stringに変換したあとの処理は実装次第で何とでもできるので何かと便利である。

引数指定でフォーマットを指定して変換できる

この.ToString()には引数を指定でき、引数でフォーマットの文字列を指定すれば変換の際に指定フォーマットで整形してくれる。
わざわざString.Format()を呼ばなくて済むので、ホントに至れり尽くせりのメソッドである。

書式を指定して数値を文字列に変換する - .NET Tips (VB.NET,C#...)

フォーマット指定に関してはVBC#開発者にはおなじみのdobon.netさんが参考になる
基本的に書式指定の方法はString.Format()と同じと考えて大丈夫だろう。

C#の.ToString()には、引数を受け付けてくれない型も存在する

今回の本題はここから。

プロジェクトで使用している.Net Frameworkのバージョンにもよると思われるが、.ToString()メソッドを使用する際に引数でフォーマットを指定するとコンパイルエラーが出る型が存在する。
このような型では、引数なしで呼び出す分には問題はないものの、フォーマットの文字列を指定して呼び出すと下記のようなエラーが出る。

引数を 1 個指定できる、メソッド 'ToString' のオーバーロードはありません

紅蓮はここでハマって2時間費やした。

問題が起きたのはDataTable型

お仕事の話に戻るが、先週C#の改修案件で回ってきたのがこんなものだった。

  • SQL Serverのストアドで取得したデータを帳票に出力するプログラムの改修(というかバグフィックス
  • ストアド側の設計がよろしくなく、小数点以下の数値が出る項目の小数点以下の桁数が統一されていない
  • ストアド側のSQLが複雑なので、ストアド側のアレな設計はとりあえず放置し、C#のクライアント側で表示桁数を調整する(先輩社員からそうするよう指示あり)

ストアド設計が悪いのが諸悪の根源であるのは言うまでもないが、C#のクライアント側もテストをちゃんとせずにリリースしちゃった感はある。
当時の担当者(退職済)はホントに何をしていたのやら。

実処理としてはDBから取得したデータをDataTable型に格納し、帳票出力用のフレームワークに文字列でセットする方式になっている。

DataTable型(正確に言うと、子オブジェクトのRows)には.ToString()が組み込まれており、元々の設計でも文字列への変換はこのメソッドでやっていた。
なぜか元々の処理は引数なしで.ToString()を呼び出していたので、前述のdobon.netさんなどの記事を参考に引数なしを入れてみることにしたが、入れたところコンパイルエラーが発生するようになった。

NGなコードサンプル

実コードをコピーしてここに貼るとコンプライアンス的によろしくないので、コンソールに出力するサンプルの処理を書いてみた。
DataTable.Rowsで引数を指定して.ToString()しようとすると、コンパイラに怒られる。

private void decimalFormatSample(DataTable sampleData) {

    for (int i = 0; i < sampleData.Rows.Count; i++) {
        
        //dataTableの.ToString()メソッドに引数を指定するとコンパイルエラー
        //引数を1個指定できる、メソッド'ToString'のオーバーロードはありません。
        Console.WriteLine(sampleData.Rows[i]["データ1"].ToString("0.00"));
        
    }
    
}

ちなみに、現環境の.Net Frameworkのバージョンは2.0。
互換性云々や大人の事情でこうなっているらしいが、化石のようなバージョンで動かしてるからこの手のエラーが出るのかもしれない。

.Net 4.x系じゃないと引数つきの.ToString()が動かない型もあるらしい

以下の記事はDataTable型ではないが、上記と同様のコンパイルエラー発生時の質問投稿に「.ToString()の引数指定に対応したのは.Net Framework 4.0から」といった回答がついている。

TimeSpanでのフォーマット指定方法: DOBON.NETプログラミング掲示板過去ログ

要するに、.ToString()の引数指定は昔からすべての型が対応していたわけではなく、.Net Frameworkのバージョンが新しくなるごとに拡張されていったということになるようだ。

.Netのバージョンを弄らずに対処する方法を考えてみた

どのみち、コンパイラのバージョンを勝手に弄るのは色々と問題があるので、それ以外の対処法を考えることになった。

冒頭でも言ったが、引数なしの.ToString()で文字列に変換してしまえば、あとは文字列をいじくり回すだけでなんとかなる。
その発想で処理を考えたところ、こうなった。

  • 一旦DataTableの中身を引数なしの.ToString()でstringに変換する(ここは改修前の処理と同じで、後続処理をしないと桁数が合わない状態)
  • stringからdecimalに変換して数値化する
  • decimalに対して.ToString()を引数付きで呼び出し、目的の桁数にフォーマットする

無駄が多いというか、エレガントさの欠片もない処理になってしまった。
decimal型は.Net 2.0でも引数ありの.ToString()を受け付けてくれるので、想定通りに変換が可能。

private void decimalFormatSample(DataTable sampleData) {

    for (int i = 0; i < sampleData.Rows.Count; i++) {
        
        //dataTableの.ToString()メソッドに引数を指定するとコンパイルエラー
        //引数を1個指定できる、メソッド'ToString'のオーバーロードはありません。
        //Console.WriteLine(sampleData.Rows[i]["データ1"].ToString("0.00"));
        
        //一旦decimal型に変換する
        decimal buf;
        buf = decimal.Parse(sampleData.Rows[i]["データ1"].ToString()); //引数指定なしの.ToString()はOK
        
        //decimal型から.ToString()する
        Console.WriteLine(buf.ToString("0.00"));
        
    }
    
}

今回のサンプルではFor文の中に変換処理を直接書いているが、実際に仕事で改修したプログラムでは、

  • For文の中で複数カラムの値を一気に取得する
  • ストアドを3つ回し、それぞれFor文でループ処理してデータを取り出す(取り出したデータはフレームワーク側で統合)

…といった元々の設計があったため、追加した変換処理の部分はフォーマット用の汎用関数として外部関数化する形をとった。

今回分かったこと・今後検証したいこと

分かったこと

  • .ToString()メソッドの引数指定ができない型がある
  • .Net Frameworkのバージョンが新しくなると、引数指定NGの型でも引数が指定できるようになる場合がある

検証したい事

  • .Net 4.xでDataTable型の.ToSring()の引数指定はできるのか
  • もうちょっとエレガントなフォーマット調整の方法はないか(C#の知識不足はあると思うので勉強したい)

今回色々調べてみて、C#もバージョンアップで機能追加されてるんだな、となんとなく分かったものの、お仕事の現場では常に最新バージョンを触れるとは限らないので、そのギャップとどう向き合っていくかは考えないとならない気はしている。

*1:中身がnullだと変換できないので、nullが想定される箇所ではきちんとnullのチェックをするお約束はある。