C# TryParse の正解 — int.TryParse / DateTime.TryParse / Enum.TryParse で業務SE が踏む3つのハマり

みなさんこんにちは!ヒロポンです!

よくさーーtry/catchまわりで地味に削られてるなーーって思うわけ。

「赤文字エラーが出てるのに返り値は正常、例外も飛ばずにcatch スルー」と頭を抱える人。

「ChatGPTに書かせたスクリプトのtry-catch、例外を拾えてなくて結局直した」と苦笑いする人。

「ErrorActionPreferenceと -ErrorAction と try-catch の組み合わせで挙動が変わる仕様、作ったやつは変態だろ」と毒づく人まで。

まじで色々いるよね。

ただ共通するのは、全部拾えるつもりで書いた try-catch が実運用ですり抜けてくること。

文字列変換みたいに「失敗が日常」な層では、例外で受ける設計そのものが地雷化しがちなんだなーと思っています。。

でここから本題。

C#でCSVやユーザー入力の文字列を数値や日付に変換する時、手が勝手にint.Parse + try/catchを書いてた、ってこと、ないですか??いや勝手に書くことはないけど、癖とかおまじないとか!

私もやりました。

流通系基幹で CSV インポートのバリデーション層を int.Parse + try/catch で組んだら、1万件中 200件くらいが空欄や不正値で来てて、例外ログが業務エラーで埋まって監視アラートが鳴りっぱなし。

性能もズタボロでした。

ぶっちゃけ業務系の文字列変換は TryParse一択
Parse + try/catch を書きたくなる手癖を「全部 TryParse」に書き換えるだけで、性能とログの両方で勝てます。

💡 例外処理そのものの使い分けは別記事 C# 例外処理の正解 — try-catch-finally / using / Exception フィルタ (when句) の使い分け でまとめてます。今回は 文字列変換のところで Parse を選ぶか TryParse を選ぶかの判断軸に絞った話です。

目次

忙しいあなたへのまとめ

  • 業務系の文字列変換 (CSV / TextBox / SQL 結果) は TryParse 一択。Parse は「パースが確実に通る文脈」だけ
  • 3パターンの主役: int.TryParse / DateTime.TryParseExact + InvariantCulture / Enum.TryParse + IsDefined 2段ガード
  • en-US ロケール本番で '2026/05/19' が NULL になる事故は TryParseExact で完封できる
  • 動作確認は .NET 9 SDK + LINQ-to-Objects 相当で実機検証 (.NET Framework 4.7.2 / VS2019 環境でも同じ API シグネチャで動作)
  • 流通系基幹で CSV 1万件 / 例外ログ汚染 / 業務側に頭下げ半日コストを TryParse + IsDefined だけで再発ゼロに詰めた実測ベース

動作確認メモ: 今回のサンプルコードは .NET 9 SDK で int.TryParse / DateTime.TryParseExact / Enum.TryParse の API シグネチャをそのまま実行確認しています。.NET Framework 4.7.2 / VS2019 / C# 7.3 の業務系現場でも out var 含めて同じコードがビルド+動作します (out var は C# 7.0 から)。

ロケール周りで1点注意。.NET 9 は ICU ベースのグローバリゼーションが既定で、DateTime.TryParse の fallback parser が .NET Framework 4.7.2 + Windows NLS 時代より寛容になってます。.NET 9 環境では en-US ロケールでも '2026/5/19' を拾える挙動を確認しました。後段で書く en-US ロケール事故談は .NET Framework 4.7.2 + Windows NLS の旧来挙動として歴史的に正しい話で、業務系の現場 (VS2019 + Win Server) では今でも再現します。TryParseExact + InvariantCulture を選ぶ判断軸そのものは新旧どちらの環境でも同じなので、業務系では問答無用で TryParseExact に倒すのが安全です。

結論: TryParse 3兄弟の使い分け軸

はいでは結論。

C# で文字列を変換する時、業務SE が押さえる軸は3つ。

  1. int / double / decimal などの数値int.TryParse / decimal.TryParse で bool 戻り値 + out var
  2. DateTime / DateTimeOffsetDateTime.TryParseExact + CultureInfo.InvariantCulture + 明示フォーマット
  3. Enum (区分コードを Enum で扱う設計の時)Enum.TryParse<T>ignoreCase: true + Enum.IsDefined で2段ガード

以上!!!!!

ぶっちゃけ、業務系のバリデーション層でこの3つを反射で書けるかどうかで、本番事故の踏みやすさが大きく変わります。

逆にどれか1個でも Parse + try/catch のままだと、CSV 数万件食わせた時に性能とログの両方で刺される側に倒れます。。

3パターンを「外部入力の信頼度 / ロケール影響 / nullable 対応 / 例外コスト」の軸でまとめると、こんな感じになります。

C# TryParse 3パターン比較 — int.TryParse / DateTime.TryParseExact / Enum.TryParse を例外コスト・書きやすさ・ロケール影響・nullable 対応・非適用ケースで観点別整理

本命用途」行を見ると一目瞭然じゃないですか?

数値はそのまま int.TryParse、日付は問答無用で TryParseExact + InvariantCulture、Enum は IsDefined と組み合わせて2段ガードという棲み分け。

なぜ Parse + try/catch が業務系で刺さるのか

ここで int.Parse + try/catch の中身を1段だけ覗いてみましょう。

int.Parse("abc")FormatException を投げる API。

例外を投げる処理は、内部でスタックトレースの生成 + 例外オブジェクトのアロケーションが走るので、正常系の数十〜数百倍のコストがかかる。

1回や2回なら誤差ですが、CSV 数万件のループで「不正値が来たら catch」をやると、見事に性能が悪くなる。

ここに業務系の現実が乗ります。

// これが本番で刺さるやつ
foreach (var line in csvLines) {
    try {
        var amount = int.Parse(line[2]);   // 空欄 / 全角数字 / カンマ入りで FormatException
        // 業務処理
    } catch (FormatException ex) {
        log.Error($"行 {line[0]} の金額が不正: {ex}");   // ログ汚染
    }
}

CSV の中に空欄 + 全角数字 + "1,000" 表記が混じる現場って、業務系では普通にあります。

これが1万件中 200件混入すると、200回の例外スロー + 200本のスタックトレースが INFO/ERROR ログに残って、監視アラートが鳴り続けます。

いや?普通に空欄チェック先にやればよくない??って思うかもしれない。そう思ったあなたは優秀。ここで読むのやめて本業戻った方がいい。

でもさーー、業務SEの手癖って「Parse して落ちたら catch」のパターンが染み付いてますやん。そんなことない?

だから私はこう言いたい。そういった手ぐせはTryParse に書き換えるのがいちばん早い修正!。

// 正解
foreach (var line in csvLines) {
    if (int.TryParse(line[2], out var amount)) {
        // 業務処理
    } else {
        log.Warn($"行 {line[0]} の金額が不正 (空欄/全角/カンマ含む): '{line[2]}'");
    }
}

例外が一切飛ばない構造になるので、ログは Warn レベルで集約、性能は数十倍速くなる。

これだけでだいぶ業務系が静かになっていい感じです!!

3パターンのコード比較 (動作確認つき)

ここから3パターンの実装を順番に見ていきます。動作確認は .NET 9 SDK で実機実行しました。.NET Framework 4.7.2 / VS2019 でも同じシグネチャで動きます。

パターン1: int.TryParse — 数値変換は bool 戻り値 + out var

業務系で頻度がいちばん高いやつ。

string[] inputs = { "1234", "0", "-5", "", "abc", "123", "1,000", null! };

foreach (var s in inputs) {
    if (int.TryParse(s, out var value)) {
        Console.WriteLine($"  '{s}' → OK ({value})");
    } else {
        Console.WriteLine($"  '{s}' → NG (パース不可)");
    }
}

実行結果 (.NET 9 SDK):

Pattern 1 int.TryParse 動作確認 — 空欄 / 全角数字 / カンマ入り / null を例外なく NG 判定する証跡

ポイントは2つ。

  1. 例外を一切投げない — どんな不正文字列でも false を返すだけ。foreach の中で安心して使える
  2. out var の書き方int.TryParse(s, out var value) で1行で受けられる。C# 7.0+ なら .NET Framework 4.7.2 / VS2019 でも使える

業務系で int? ToIntOrNull(string s) のヘルパーを作る時はこれを1行で包めばOK。こんな感じです。

static int? ToIntOrNull(string s)
    => int.TryParse(s, out var v) ? v : (int?)null;

decimal.TryParse / double.TryParse / long.TryParse も全部同じシグネチャなので、一気に置き換えできます。

パターン2: DateTime.TryParseExact + InvariantCulture — ロケール事故をゼロに

ここが業務SE がいちばん刺されるやつ。

using System.Globalization;

string[] inputs = { "2026/05/19", "2026-05-19", "2026/5/19", "05/19/2026" };

// NG パターン: DateTime.TryParse — ロケール依存
var jp = new CultureInfo("ja-JP");
var us = new CultureInfo("en-US");

Console.WriteLine("--- DateTime.TryParse (ロケール依存) ---");
foreach (var s in inputs) {
    var okJp = DateTime.TryParse(s, jp, DateTimeStyles.None, out var dtJp);
    var okUs = DateTime.TryParse(s, us, DateTimeStyles.None, out var dtUs);
    Console.WriteLine($"  '{s}' → ja-JP={okJp,-5} ({(okJp ? dtJp.ToString("yyyy-MM-dd") : "NG")})  en-US={okUs,-5} ({(okUs ? dtUs.ToString("yyyy-MM-dd") : "NG")})");
}

// OK パターン: TryParseExact + InvariantCulture
Console.WriteLine("--- DateTime.TryParseExact + InvariantCulture (固定) ---");
string[] formats = { "yyyy/MM/dd", "yyyy/M/d", "yyyy-MM-dd" };
foreach (var s in inputs) {
    var ok = DateTime.TryParseExact(s, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt);
    Console.WriteLine($"  '{s}' → {(ok ? $"OK ({dt:yyyy-MM-dd})" : "NG (フォーマット不一致)")}");
}

実行結果 (.NET 9 SDK):

Pattern 2 DateTime ロケール依存事故と TryParseExact の比較 — en-US で 2026/05/19 が NG になる証跡

画像では .NET 9 ICU の fallback parser が強化されてるおかげで ja-JP / en-US 両方で 2026/5/19 まで拾えてます。

ところが .NET Framework 4.7.2 + Windows NLS の業務系本番ではこの fallback が効かず、en-US ロケールだと 2026/05/19 は通る (en-US は M/d/yyyy 形式想定だが yyyy 4桁が先頭なら通ることがある) ものの、2026/5/19 のようにゼロ埋めなしの月日が来た時に本番だけ false が返ってNULLに化ける、という事故が起きる。

つまり「.NET 9 で動いた = 本番 .NET Framework でも動く」は成立しない。ここがいちばん怖い。

DateTime.TryParseExact + CultureInfo.InvariantCulture + 明示フォーマット配列で固定すれば、開発機と本番でロケールが違っても同じ結果。

これ、私がやらかしたパターンです。

開発機 ja-JP で書いたバリデーション層を、en-US 設定の本番にデプロイした瞬間、CSV インポートの日付列だけ全部 NULL という結果に。

詳細は後段の現場メモで。

タイムゾーンの扱いまで含めると話が広がるので、そっちは C# DateTime と DateTimeOffset の違い・タイムゾーン処理の正解(業務SE本番事故編) で書いた DateTimeOffset への乗り換え判断と合わせて読むのをおすすめします。というかここまで読んだなら別タブで開いて後で呼んでください。

パターン3: Enum.TryParse + IsDefined — 2段ガードで数字文字列の罠も封じる

業務系で区分コードを Enum で扱ってる現場向け。

public enum Status { Active = 1, Inactive = 2, Pending = 3 }

string[] inputs = { "Active", "active", "Pending", "Unknown", "0", "99", "" };

// NG パターン: Enum.TryParse 単独 (数字文字列が定義外でも true になる)
Console.WriteLine("--- Enum.TryParse 単独 (危険) ---");
foreach (var s in inputs) {
    var ok = Enum.TryParse<Status>(s, ignoreCase: true, out var v);
    Console.WriteLine($"  '{s}' → ok={ok,-5} value={v} (定義値か: {Enum.IsDefined(typeof(Status), v)})");
}

// OK パターン: TryParse + IsDefined の2段ガード
Console.WriteLine("--- Enum.TryParse + IsDefined (2段ガード) ---");
static bool TryParseStrict<T>(string s, out T value) where T : struct, Enum
    => Enum.TryParse<T>(s, ignoreCase: true, out value) && Enum.IsDefined(typeof(T), value);

foreach (var s in inputs) {
    if (TryParseStrict<Status>(s, out var v)) {
        Console.WriteLine($"  '{s}' → OK ({v})");
    } else {
        Console.WriteLine($"  '{s}' → NG (定義外 or パース不可)");
    }
}

実行結果 (.NET 9 SDK):

Pattern 3 Enum.TryParse + IsDefined 2段ガード — 数字文字列 99 や 0 が定義外なのに true が返る罠を IsDefined で封じる証跡

Enum.TryParse<Status>("99", out var v)true を返します。

v には (Status)99 が入る。定義されていない値なのに、パースが通ってしまう。

これが業務系の区分コードでよくあるハマり。「DB の status カラムに NULL とか未定義値が入ってた時、Enum パースを通したらバグの種が画面まで運ばれてしまう」構造。

Enum.IsDefined を後ろに付けて2段ガードすれば、定義外の値は false になります。

ignoreCase: true も忘れずに付けるのが業務SE 流儀。Active / active / ACTIVE を区別する意味がないからです。

ハマりポイント: TryParse でも踏む3つの落とし穴

3パターンの基本は押さえました。

が、ここからは業務系で TryParseに書き換えた後でも踏む落とし穴を3つ共有しておきます。

落とし穴 症状 対処
int.TryParse で全角数字 '123' が NG になる NumberStyles 指定なしだと半角数字のみ int.TryParse(s, NumberStyles.AllowThousands | NumberStyles.AllowLeadingSign, ja, out v) で許容、もしくは事前に半角変換
DateTime.TryParseExact で深夜0時の文字列が 2026/05/19 00:00:00 形式で来た時 NG フォーマット配列に "yyyy/MM/dd HH:mm:ss" が無い フォーマット配列に想定パターン全部を列挙。"yyyy/MM/dd" "yyyy/MM/dd HH:mm:ss" "yyyy-MM-dd" の3つは最低入れる
Enum.TryParse<T>(null, ...) が C# 8+ の nullable 文脈で warning を出す API が null 入力に対して false を返す挙動 呼び出し側で string.IsNullOrEmpty を先にチェックする・もしくは null 許容型でラップ

特に2番目のフォーマット配列の網羅漏れは、開発時には拾えない事故。

ビジネスサイドからCSVのサンプルを最低5パターン (時刻あり / 時刻なし / スラッシュ / ハイフン / 年4桁/2桁) もらって、全部 TryParseExactで通ることを確認してからデプロイする、が自分の身を守る最短ルート。

ちなみに、これは結構大事でビジネスサイドに「サンプルください」と頼みに行く時間そのものが信頼を作ります。マジで。。。

「おぉこの人現場理解してくれようとしている。」って思われます。
そんな信頼関係を作れると、後工程の説明コストが半分になります。

私の流通系基幹での en-US ロケール事故

ここからは現場メモ。

数年前、流通系基幹の CSV インポートバリデーション層を任されました。受注データの CSV を夜間バッチで取り込む、件数は1日あたり1万件。

開発機は普通に .NET Framework 4.7.2 / VS2019 / Windows 10 ja-JP でテスト、全件 OK。

「これで完了!よっしゃ通ったわ」ってデプロイしました。

翌朝ユーザーから「CSV インポートで日付がNULLの行が大量に出てる」と電話。。

「え??開発機では通ったのに??」って血の気が引きました。またこのパターン。

何が起きてたかというと:

  • 本番サーバーが en-US ロケールで構築されてた (国際物流対応で AWS リージョン US-East に置く運用)
  • ランタイムは .NET Framework 4.7.2 + Windows Server NLS (現代の .NET 9 ICU より fallback parser が貧弱な世代)
  • バリデーション層は DateTime.TryParse(s, out var dt) で書いてた (カルチャ未指定 → サーバーロケール依存)
  • '2026/5/19' (月日ゼロ埋めなし) が en-US ロケールで認識されず NG に倒れた
  • 開発機 ja-JP では '2026/5/19' も通ってたので、テストではすり抜けてた

慌てて DateTime.TryParseExact + CultureInfo.InvariantCulture + フォーマット配列 (時刻あり/なし/ゼロ埋めあり/なし 4種類) に書き換えてデプロイしたら、翌日の取り込みは 0件 NULL に落ち着きました。

ギリギリ耐えた。(耐えてない)

ただ、技術的な復旧は半日で済んだんですが、業務側に頭下げに行く時間がついて回りました。

「テスト環境で通ったのに本番で NULL になった」という説明をする時、ロケール差で再現しなかった話を技術知らない人に通すのが地味にしんどい。朝礼で「ロケール固定漏れでした」と言う2分のために、技術復旧と同じくらいの時間を準備に使った印象。

ん?普通に最初から InvariantCulture 使っとけって話やん??って今は思う。

当時は「DateTime.TryParse で動いてるから OK」という思い込みが完全に頭にあった。

ロケール依存 API は開発機と本番でロケールが違うと再現しないのがいちばん厄介な性質なんですよね。

教訓は3個。DateTime は TryParseExact + InvariantCulture + 明示フォーマット。そして業務側に CSV サンプルを5パターン以上もらってから本番デプロイ。さらに本番サーバーのロケール設定を最初に確認する。

まとめ

C# TryParse 3兄弟の話でした。

業務系で文字列変換が絡む箇所は、反射で TryParse を選べるかどうかで本番事故の踏みやすさが大きく変わります。

軸はシンプル。

  • 数値int.TryParse / decimal.TryParse + out var (Parse + try/catch は今すぐ書き換え)
  • 日付DateTime.TryParseExact + CultureInfo.InvariantCulture + フォーマット配列 (ロケール依存にしない)
  • EnumEnum.TryParse<T>ignoreCase: true + Enum.IsDefined で2段ガード

困ったら冒頭の比較表に戻ってきて、4軸 (例外コスト / 書きやすさ / ロケール / nullable 対応) のうちどこで刺されそうか確認するのがいちばん速い。

例外処理そのものの判断軸は C# 例外処理の正解 — try-catch-finally / using / Exception フィルタ (when句) の使い分け で書いた try-catch-finally / using / when 句の使い分けと併読おすすめします。

よくある質問

Q1. int.Parse と int.TryParse、どっちを使うべきですか?

ユーザー入力 / CSV / SQL 結果のような外部由来の文字列には TryParse 一択。例外を投げる Parse は性能コストと例外ログ汚染の2重ダメージです。Parse は「パースが通ることが確定している文脈」(定数文字列や検証済み入力) でのみ使います。

Q2. なぜ Dapper / EF6 の自動型変換に任せず TryParse を書くんですか?

ORM の自動型変換は DB→C# の変換にしか効きません。CSV / TextBox / ファイル / Web API JSON など、ORM を経由しない外部入力 が業務系の主戦場。そこでは TryParse が要ります。逆に DB 一本道なら ORM の型マッピングで十分です。

Q3. DateTime.TryParse が本番サーバーで突然 NULL を返す原因は?

本番サーバーのロケールが ja-JP 以外 (en-US 等) になっていて、'2026/05/19''2026/5/19' 形式の日付文字列が認識されないケースです。DateTime.TryParseExact + CultureInfo.InvariantCulture + 明示フォーマットで固定するのが正解。

Q4. Enum.TryParse で数字文字列が通ってしまうのは仕様ですか?

仕様です。Enum.TryParse<T>("99", out var v) は定義されていない 99 でも true を返します。Enum.IsDefined(typeof(T), v) で2段ガードするのが鉄則。TryParseStrict<T> のような1行ヘルパーを業務系の共通基盤に置いておくと、呼び出し側がスッキリします。

Q5. out var の書き方は C# 7.3 (VS2019) でも使えますか?

使えます。out var は C# 7.0 で導入された機能で、.NET Framework 4.7.2 / VS2019 / LangVersion 7.3 の業務系コードでも問題なく動きます。古い VS2017 (C# 7.0 未満) の現場では int v; int.TryParse(s, out v) の従来構文で OK。

関連記事

以上!

同じ罠でハマってる人いたら、どんどんシェア待ってるぜ!!


この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次