.NETでは、カルチャ(≒ロケール)がスレッド毎に割り当てられます。 また、カルチャ(CultureInfo)には言語や国・地域に固有な書式や規則が定義されています。

.NETのクラスライブラリでは、カルチャで定義されている書式や規則がデフォルトとして使用されることがあり、これにより、現在のカルチャ・UIカルチャによって動作や結果が変わるものがあります。 例えば、特に書式等を指定せずにDateTimeを文字列化(DateTime.ToString)すると、ja-JP(日本語/日本)の環境とen-US(英語/米国)など他の環境とで得られる結果が異なります。

こういった動作は、カルチャに依存する(カルチャを意識するculture awareな)と表現されます。 カルチャに依存する動作は、例えば異なるカルチャ間でのデータ交換の際に用いると、データの解釈に不一致・齟齬が生じる、それにより例外の発生や意図しない動作を引き起こすことがあります。 (§.カルチャごとの動作の違いによって生じる齟齬とインバリアントカルチャの使用)

一方、こういったカルチャに依存する動作やカルチャ間での齟齬を避けるために、.NETではインバリアントカルチャ(CultureInfo.InvariantCulture)を使用することができます。

インバリアントカルチャでは、書式や規則として(英語・英語圏を基本とするものの)特定の言語や国・地域に依存しないものが定義されていて、どのカルチャにも依存しない(カルチャに対して不変なculture invariantな)動作や結果を期待する際の代替カルチャとして使用することができます。 インバリアントカルチャは、Cロケール・POSIXロケール(LANG=C)に相当するカルチャと見ることもできます。

インバリアントカルチャ以外にも、StringComparison.InvariantCultureRegexOptions.CultureInvariantなどのオプション、String.ToUpperInvariantメソッドといったメソッドなど、インバリアントカルチャの定義に従った動作、つまりカルチャに依存しない動作を行うものも用意されています。

ここでは、CultureInfoの動作とカルチャに依存した動作・結果となる例とインバリアントカルチャの使用について解説します。 カルチャの取得・変更やカルチャの種類、システムロケールとの対応、CultureInfoから取得できる情報などについてはカルチャの基本・種類・カルチャ情報の取得を参照してください。

以下、この文章における実行結果は、ランタイムや実行環境、特に設定されている言語によって結果が異なる箇所があります。 その場合は前提条件を明記するようにしていますが、特に明記しない場合は日本語/日本の環境での実行結果となります。

カルチャに依存した動作

.NETのクラスライブラリには、カルチャに依存した動作となるものがあります。 例として文字列化を挙げると、特に書式等を指定しない場合はカルチャに固有の表記で文字列化されます

文字列化におけるカルチャに依存した動作(デフォルトの書式での数値の文字列化) 
using System;

// 数値を表示する(数値を文字列化して標準出力に書き込む)
Console.WriteLine(3.14);
文字列化におけるカルチャに依存した動作(デフォルトの書式での数値の文字列化)
Imports System

Class Sample
  Shared Sub Main()
    ' 数値を表示する(数値を文字列化して標準出力に書き込む)
    Console.WriteLine(3.14)
  End Sub
End Class
ja-JP(日本語/日本)での出力結果
3.14
fr-FR(フランス語/フランス)での出力結果
3,14
ar-AE(アラビア語/UAE)での出力結果(.NET 5/Ubuntu 20.04)
3٫14

ファイルの読み込み・書き込みや、他のシステムとのデータ交換などの入出力においてカルチャに依存した動作を使用すると、表記の違いに起因する齟齬が生じる、具体的には例外FormatExceptionやArgumentExceptionなどがスローされることがあります。

.NETにおけるデフォルトのカルチャはシステムロケールに基づくため、こういった問題は「自環境・一部の環境では期待通りに動作するが、他の環境では正しく動作しない・例外が発生する」といった事象として発覚することになります。

特定のカルチャ、特にシステムロケールに依存しない動作とする書式の違いに起因する齟齬を避けるためには、ローカライズされない書式を使用する、文字列比較・ソートでは序数(コードポイント)による比較(StringComparison.Ordinal)を行うほかに、インバリアントカルチャを使用することが挙げられます。 以下、この文章ではインバリアントカルチャを使用する場合を中心に解説します。

カルチャごとの動作とランタイム・プラットフォームによる差異

カルチャ(CultureInfo)として定義される書式や規則は、プラットフォームのAPIや他のライブラリなどから取得されます。 このため、システムロケールだけでなく、ランタイムの種類(.NET/.NET Core/.NET Framework)、実行されるプラットフォームによる差異も、書式の違いによる齟齬や環境ごとに異なる動作の原因となる場合があります。

Windows上では、ICU(libicu)によって得られるカルチャ情報(ICUがインストールされている場合・.NETのみ)、もしくはWindows OSのNSL(National Language Support)によって得られるカルチャ情報(.NET/.NET Core/.NET Frameworkの場合)を使用します。 加えて、仮にICUが使用できる環境であっても、環境変数・プロジェクト設定によってNSLを使用するように変更することができます。 一方、Linux上では(常に)ICUによって得られるカルチャ情報を使用するとされています。 (詳細は章末)

このため、仮に同一のロケールであっても、またインバリアントカルチャも含め、ランタイムの種類・プラットフォーム・ICUのインストール使用可否等、構成・設定により異なる動作となる場合があります。

例えば、次のコードは、同じロケールar-AEであっても、ランタイムの種類によって異なる結果となります。

数値の文字列化におけるカルチャに依存した動作とプラットフォーム・ランタイムによる差異 
using System;
using System.Globalization;

// 現在のカルチャを表示する
Console.WriteLine(CultureInfo.CurrentCulture);

// 数値を表示する(数値を文字列化して標準出力に書き込む)
Console.WriteLine(3.14);
数値の文字列化におけるカルチャに依存した動作とプラットフォーム・ランタイムによる差異
Imports System
Imports System.Globalization

Class Sample
  Shared Sub Main()
    ' 現在のカルチャを表示する
    Console.WriteLine(CultureInfo.CurrentCulture)

    ' 数値を表示する(数値を文字列化して標準出力に書き込む)
    Console.WriteLine(3.14)
  End Sub
End Class
実行結果
ar-AE
3٫14
実行結果
ar-AE
3.14
実行結果
ar-AE
3٫14
実行結果
ar-AE
3.14

また、次のコードは、インバリアントカルチャを使用した場合でもランタイムの種類によって異なる結果となります。

文字列配列のソートにおけるインバリアントカルチャの動作とプラットフォーム・ランタイムによる差異 
using System;
using System.Globalization;
using System.Linq;

// 現在のカルチャをインバリアントカルチャにする
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// あるいはIComparer<string>としてStringComparer.InvariantCultureを指定しても同じ

// 文字列配列を並べ替えて表示する
Console.WriteLine(
  string.Join(", ", new[] {"あ", "ア", "亜", "ア", "A", "A"}.OrderBy(s => s/*, StringComparer.InvariantCulture*/))
);
Console.WriteLine(
  string.Join(", ", new[] {"0-0", "00-0", "00-00", "0-00", "0-1", "0-01", "0-10"}.OrderBy(s => s/*, StringComparer.InvariantCulture*/))
);

// 比較として、序数(コードポイント)による比較によって並べ替えた場合
Console.WriteLine();
Console.WriteLine("[Ordinal]");
Console.WriteLine(
  string.Join(", ", new[] {"あ", "ア", "亜", "ア", "A", "A"}.OrderBy(s => s, StringComparer.Ordinal))
);
Console.WriteLine(
  string.Join(", ", new[] {"0-0", "00-0", "00-00", "0-00", "0-1", "0-01", "0-10"}.OrderBy(s => s, StringComparer.Ordinal))
);
実行結果
A, A, あ, ア, ア, 亜
0-0, 0-00, 0-01, 0-1, 0-10, 00-0, 00-00

[Ordinal]
A, あ, ア, 亜, A, ア
0-0, 0-00, 0-01, 0-1, 0-10, 00-0, 00-00
実行結果
A, A, ア, ア, あ, 亜
0-0, 00-0, 0-00, 00-00, 0-01, 0-1, 0-10

[Ordinal]
A, あ, ア, 亜, A, ア
0-0, 0-00, 0-01, 0-1, 0-10, 00-0, 00-00
実行結果
A, A, あ, ア, ア, 亜
0-0, 0-00, 0-01, 0-1, 0-10, 00-0, 00-00

[Ordinal]
A, あ, ア, 亜, A, ア
0-0, 0-00, 0-01, 0-1, 0-10, 00-0, 00-00
実行結果
A, A, ア, ア, あ, 亜
0-0, 00-0, 0-00, 00-00, 0-01, 0-1, 0-10

[Ordinal]
A, あ, ア, 亜, A, ア
0-0, 0-00, 0-01, 0-1, 0-10, 00-0, 00-00

このほかにも、DateTimeにおける書式指定子Dなど、書式よっては.NET Frameworkと.NET Core/.NETで表記に差異があるものがあり、これにより齟齬の原因となる場合があります。


なお、カルチャ情報(CultureInfoとして定義される書式や規則の情報)は以下のように取得されるとされています。

CultureInfo と文化のデータ

.NET は、実装、プラットフォーム、およびバージョンに応じて、さまざまなソースからカルチャデータを派生させることができます。

  • .NET Framework 3.5 以前のバージョンでは、カルチャデータは、Windows オペレーティングシステムと .NET Framework の両方によって提供されます。
  • .NET Framework 4 以降のバージョンでは、カルチャデータは Windows オペレーティングシステムによって提供されます。
  • Windows で実行されている .NET Core のすべてのバージョンでは、Windows オペレーティングシステムによってカルチャデータが提供されます。
  • Unix プラットフォームで実行されている .NET Core のすべてのバージョンでは、International Components For Unicode (ICU) ライブラリによってカルチャデータが提供されます。 ICU ライブラリの特定のバージョンは、個々のオペレーティングシステムによって異なります。

このため、特定の .NET 実装、プラットフォーム、またはバージョンで使用できるカルチャは、別の .NET 実装、プラットフォーム、またはバージョンでは使用できない場合があります。

CultureInfo クラス (System.Globalization) | Microsoft Docs

Windows 上の ICU

Windows 10 の 2019 年 5 月更新プログラム以降では、OS の一部として icu.dll が含まれるようになり、.NET 5.0 以降のバージョンでは、既定で ICU が使用されるようになりました。 .NET 5.0 以降のバージョンを Windows で実行する場合、icu.dll の読み込みが試行され、存在する場合、グローバリゼーションの実装で使用されます。 Windows の古いバージョンを実行している場合などで、このライブラリを見つけたり、読み込むことができない場合、.NET 5.0 以降のバージョンは NLS ベースの実装に戻ります。

グローバリゼーションと ICU | Microsoft Docs

ICU の代わりに NLS を使用する

NLS の代わりに ICU を使用すると、一部のグローバリゼーション関連の操作で動作が違ってしまうことがあります。 開発者は、NLS を使用するように、ICU の実装を戻すことを選択することができます。 アプリケーションでは、次のいずれかの方法で NLS モードを有効にできます。

グローバリゼーションと ICU | Microsoft Docs

カルチャごとの動作の違いによって生じる齟齬とインバリアントカルチャの使用

カルチャに依存した動作による齟齬が生じる具体的な例として、それぞれ異なるカルチャ(ロケール)で動作するシステム間でデータをやりとりする状況を考えます。

ここでは、送信側はDouble型の数値を(バイナリ形式ではなく)文字列としてファイルに書き込み、そのファイルを何らかの方法で転送し、受信側で読み込む実装とします。 このとき、送信側のロケールはja-JPとし、受信側のロケールはja-JP(日本語/日本)とfr-FR(フランス語/フランス)の2種類が存在すると仮定します。

送信側
数値データを文字列形式で送受信する(送信側の実装) 
using System;
using System.Globalization;
using System.IO;

// 現在のカルチャを表示
Console.WriteLine(CultureInfo.CurrentCulture);

// 送信するデータ(数値)
double data = 1234.5678;

// 送信するデータをファイルに書き込む
using (var writer = new StreamWriter("data.txt")) {
  writer.WriteLine(data);
}

// このあと何らかの方法でファイルを転送したとする
//Network.SendFile("data.txt");
数値データを文字列形式で送受信する(送信側の実装)
Imports System
Imports System.Globalization
Imports System.IO

Class Sample
  Shared Sub Main()
    ' 現在のカルチャを表示
    Console.WriteLine(CultureInfo.CurrentCulture)

    ' 送信するデータ(数値)
    Dim data As Double = 1234.5678

    ' 送信するデータをファイルに書き込む
    Using writer As New StreamWriter("data.txt")
      writer.WriteLine(data)
    End Using

    ' このあと何らかの方法でファイルを転送したとする
    'Network.SendFile("data.txt")
  End Sub
End Class
実行結果
ja-JP
data.txtに出力される内容(受信側に転送される内容)
1234.5678
受信側
数値データを文字列形式で送受信する(受信側の実装) 
using System;
using System.Globalization;
using System.IO;

// 現在のカルチャを表示
Console.WriteLine(CultureInfo.CurrentCulture);

// 何らかの方法でファイルが転送されてきたとする
//Network.ReceiveFile("data.txt");

// ファイルから受信したデータを読み込む
using (var reader = new StreamReader("data.txt")) {
  var data = double.Parse(reader.ReadLine());

  // 読み込んだデータを表示する
  Console.WriteLine($"data = {data}");
}
数値データを文字列形式で送受信する(受信側の実装)
Imports System
Imports System.Globalization
Imports System.IO

Class Sample
  Shared Sub Main()
    ' 現在のカルチャを表示
    Console.WriteLine(CultureInfo.CurrentCulture)

    ' 何らかの方法でファイルが転送されてきたとする
    'Network.ReceiveFile("data.txt")

    ' ファイルから受信したデータを読み込む
    Using reader As New StreamReader("data.txt")
      Dim data As Double = Double.Parse(reader.ReadLine())

      ' 読み込んだデータを表示する
      Console.WriteLine($"data = {data}")
    End Using
  End Sub
End Class
ja-JPでの実行結果
ja-JP
data = 1234.5678
fr-FRでの実行結果
fr-FR
ハンドルされていない例外: System.FormatException: 入力文字列の形式が正しくありません。
   場所 System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
   場所 System.Number.ParseDouble(String value, NumberStyles options, NumberFormatInfo numfmt)
   場所 System.Double.Parse(String s, NumberStyles style, NumberFormatInfo info)
   場所 Sample.Main()

この実装を実際に運用すると、上記の実行結果のようにロケールがja-JPのシステムでは問題なく動作するものの、fr-FRのシステムでは「入力文字列の形式が正しくない」として例外FormatExceptionが発生します。

これは、fr-FRでは小数点区切りに.(ピリオド)ではなく,(カンマ)が用いられることに起因します。 fr-FRでは実数値に1234,5678のような表記が用いられる一方、文字列1234.5678は実数値(Double型)の表記としては不正とみなされ、これが原因で例外が発生します。

上記のコードでは、受信側でCultureInfo.CurrentCulture = new CultureInfo("fr-FR")のようにしてカルチャを変更することにより、システムロケールfr-FRの環境での動作を再現することができます。 コード中でのカルチャの変更についてはカルチャの基本・種類・カルチャ情報の取得 §.スレッドのカルチャの変更を参照してください。

このように、カルチャの違いによって書式とその解釈に齟齬が生じ、その結果として環境により動作しないという事象が生じることになります。 齟齬を生じさせずに動作させるためには、2者間で使用する書式を一致させる必要があります。

そこで、先の実装における数値→文字列・文字列→数値の変換に際してインバリアントカルチャを使用するように変更すると、例外を発生させることなく意図した通りに動作させることができます。

送信側
数値データを文字列形式で送受信する(インバリアントカルチャを使用した送信側の実装) 
using System;
using System.Globalization;
using System.IO;

// 現在のカルチャを表示
Console.WriteLine(CultureInfo.CurrentCulture);

// 送信するデータ(数値)
double data = 1234.5678;

// 送信するデータをファイルに書き込む
using (var writer = new StreamWriter("data.txt")) {
  // インバリアントカルチャの書式で文字列化した上で書き込む
  writer.WriteLine(
    data.ToString(CultureInfo.InvariantCulture)
  );
}

// このあと何らかの方法でファイルを転送したとする
//Network.SendFile("data.txt");
数値データを文字列形式で送受信する(インバリアントカルチャを使用した送信側の実装)
Imports System
Imports System.Globalization
Imports System.IO

Class Sample
  Shared Sub Main()
    ' 現在のカルチャを表示
    Console.WriteLine(CultureInfo.CurrentCulture)

    ' 送信するデータ(数値)
    Dim data As Double = 1234.5678

    ' 送信するデータをファイルに書き込む
    Using writer As New StreamWriter("data.txt")
      ' インバリアントカルチャの書式で文字列化した上で書き込む
      writer.WriteLine(
        data.ToString(CultureInfo.InvariantCulture)
      )
    End Using

    ' このあと何らかの方法でファイルを転送したとする
    'Network.SendFile("data.txt")
  End Sub
End Class
実行結果
ja-JP
data.txtに出力される内容(受信側に転送される内容)
1234.5678
受信側
数値データを文字列形式で送受信する(インバリアントカルチャを使用した受信側の実装) 
using System;
using System.Globalization;
using System.IO;

// 現在のカルチャを表示
Console.WriteLine(CultureInfo.CurrentCulture);

// 何らかの方法でファイルが転送されてきたとする
//Network.ReceiveFile("data.txt");

// ファイルから受信したデータを読み込む
using (var reader = new StreamReader("data.txt")) {
  // インバリアントカルチャの書式で文字列を解析する
  var data = double.Parse(
    reader.ReadLine(),
    CultureInfo.InvariantCulture
  );

  // 読み込んだデータを表示する
  Console.WriteLine($"data = {data}");
}
数値データを文字列形式で送受信する(インバリアントカルチャを使用した受信側の実装)
Imports System
Imports System.Globalization
Imports System.IO

Class Sample
  Shared Sub Main()
    ' 現在のカルチャを表示
    Console.WriteLine(CultureInfo.CurrentCulture)

    ' 何らかの方法でファイルが転送されてきたとする
    'Network.ReceiveFile("data.txt")

    ' ファイルから受信したデータを読み込む
    Using reader As New StreamReader("data.txt")
      ' インバリアントカルチャの書式で文字列を解析する
      Dim data As Double = Double.Parse(
        reader.ReadLine(),
        CultureInfo.InvariantCulture
      )

      ' 読み込んだデータを表示する
      Console.WriteLine($"data = {data}")
    End Using
  End Sub
End Class
ja-JPでの実行結果
ja-JP
data = 1234.5678
fr-FRでの実行結果
fr-FR
data = 1234,5678

このように、2者間で共通のカルチャとしてインバリアントカルチャを使用し、文字列⇄数値の変換に際して使用される書式を一致させることにより、齟齬をなくすことができます。

ここでは2者間で用いられる書式に齟齬を生じさせなければよいため、必ずしもインバリアントカルチャでなくてもよく、例えば「2者間でやり取りするデータの書式は常にja-JPのカルチャを使用する」などのように仕様を定めることもできます。

ここではデータ交換を例として挙げましたが、ほかにも設定ファイルを読み込む場合なども同様で、設定ファイル内で特定カルチャに依存する書式が用いられている場合、カルチャによってはその読み込みに失敗する、といった状況が想定されます。


別の例として、日付と時刻に関しても同様の齟齬が想定されます。 日付の表記における年月日の順序は国や言語による違いとしてよく挙げられますが、.NETにおけるDateTime.Parseメソッドも、カルチャによって異なる順序で日付の年月日を解釈をします。

具体的には、文字列で表された日付12/3/4ja-JP(日本/日本語)では(20)12年3月4日として解釈される一方、en-US(英語/米国)では(200)4年12月3日en-GB(英語/英国)では(200)4年3月12日として解釈されます。

これをカルチャによらず同じ日付として解釈させたい場合は、先の例と同様に共通のカルチャを使用します。 文字列12/3/4をインバリアントカルチャの書式で解釈させれば、システムロケールによらず常に2004年12月3日として解釈させることができます。

実行結果(.NET 5)
ja-JP
12/3/4 -> 2012年3月4日日曜日
12/3/4 -> 2004年12月3日金曜日
実行結果(.NET Framework 4.8)
ja-JP
12/3/4 -> 2012年3月4日
12/3/4 -> 2004年12月3日
実行結果(.NET 5)
en-US
12/3/4 -> Friday, December 3, 2004
12/3/4 -> Friday, December 3, 2004
実行結果(.NET Framework 4.8)
en-US
12/3/4 -> Friday, December 3, 2004
12/3/4 -> Friday, December 3, 2004
実行結果(.NET 5)
en-GB
12/3/4 -> Friday, 12 March 2004
12/3/4 -> Friday, 3 December 2004
実行結果(.NET Framework 4.8)
en-GB
12/3/4 -> 12 March 2004
12/3/4 -> 03 December 2004
文字列で表現された日付を現在のカルチャ・インバリアントカルチャで解析する 
using System;
using System.Globalization;

// 現在のカルチャを表示
Console.WriteLine(CultureInfo.CurrentCulture);

// 文字列で表現された日付
var dateString = "12/3/4";

// 文字列をDateTimeとして解析し、書式'D'(長い形式の日付)で表示する
// (現在のカルチャの書式での解釈を行い、現在のカルチャの書式で文字列化する)
Console.WriteLine(
  "{0} -> {1}",
  dateString, DateTime.Parse(dateString).ToString("D")
);

// 文字列をDateTimeとして解析し、書式'D'(長い形式の日付)で表示する
// (インバリアントカルチャの書式での解釈を行い、現在のカルチャの書式で文字列化する)
Console.WriteLine(
  "{0} -> {1}",
  dateString, DateTime.Parse(dateString, CultureInfo.InvariantCulture).ToString("D")
);
文字列で表現された日付を現在のカルチャ・インバリアントカルチャで解析する
Imports System
Imports System.Globalization
Imports System.IO

Class Sample
  Shared Sub Main()
    ' 現在のカルチャを表示
    Console.WriteLine(CultureInfo.CurrentCulture)

    ' 文字列で表現された日付
    Dim dateString As String = "12/3/4"

    ' 文字列をDateTimeとして解析し、書式'D'(長い形式の日付)で表示する
    ' (現在のカルチャの書式での解釈を行い、現在のカルチャの書式で文字列化する)
    Console.WriteLine(
      "{0} -> {1}",
      dateString, DateTime.Parse(dateString).ToString("D")
    )

    ' 文字列をDateTimeとして解析し、書式'D'(長い形式の日付)で表示する
    ' (インバリアントカルチャの書式での解釈を行い、現在のカルチャの書式で文字列化する)
    Console.WriteLine(
      "{0} -> {1}",
      dateString, DateTime.Parse(dateString, CultureInfo.InvariantCulture).ToString("D")
    )
  End Sub
End Class

先の例とは異なり、この例でのDateTime.Parseメソッドはカルチャによって異なる結果を生じるものの一応は動作する例外とはならないため、意図しない動作の原因箇所としては見つけにくいものとなる可能性があります。


このほか、カルチャに依存する動作が起こる箇所と、カルチャに依存しない動作とするためのオプション等については§.CultureInfoの動作とカルチャに依存する動作で解説します。

CultureInfoの動作とカルチャに依存する動作

.NETではスレッドごとにカルチャ(CurrentCulture)・UIカルチャ(CurrentUICulture)が割り当てられ、それらで定義されている書式・規則がデフォルトとして使用される場合があります。 これにより、明示的に書式や規則を指定しない場合のデフォルトの動作がカルチャに依存する動作となるものがあります。

ここでは、カルチャ(CultureInfo)が参照される状況とその動作、また動作を変更する手段等について解説します。

書式プロバイダ(IFormatProvider)としてのCurrentCulture

数値型、DateTimeTimeSpanなど日時と時間間隔を表す型では、ToStringメソッドString.Formatメソッドによって値を任意の形式(書式)で文字列化することができます。 このとき、カルチャ(CurrentCulture)はデフォルトの書式プロバイダ(IFormatProvider)として使用されます。

文字列化(ToStringメソッド)の動作と、引数およびCurrentCultureの関わりを簡単に述べると次のようになります。

  • メソッドの引数として与えられた書式指定子(引数format)と書式プロバイダ(引数provider)を検証する
    • 書式指定子が省略されている場合は、デフォルトの書式(一般的な形式を表す書式指定子G)を使用する
    • 書式プロバイダが省略されている場合は、スレッドにおける現在のカルチャ(CurrentCulture)を使用する
  • 書式プロバイダから、書式指定子に対応する書式の定義を取得する(DateTimeであれば年月日の並び順、曜日名、区切りに用いられる記号など)
  • 得られた書式の定義にしたがって値を文字列に変換し、結果として返す

書式指定子には、ローカライズされる書式ローカライズされない書式が存在します。 DateTimeのデフォルトの書式指定子であるG(一般的な日付と時刻の形式)をはじめ、F(完全な日付と時刻の形式)やD(日付のみの形式)といった書式指定子はローカライズされる書式であり、その具体的な書式の定義は書式プロバイダから取得されます。

書式プロバイダは、特に指定されない場合はデフォルトとして現在のカルチャ(CurrentCulture)が使用されます。 そのため、書式プロバイダが明示的に指定されていなければ、ローカライズされる書式はカルチャによって異なる表記で文字列化されることになり、また何らかのカルチャが書式プロバイダとして指定されていれば、そのカルチャに応じた表記で文字列化されることになります。

DateTimeの文字列化における書式指定子・書式プロバイダの動作 
using System;
using System.Globalization;
using System.Threading;

class Sample {
  static void Main()
  {
    var dt = new DateTime(2000, 1, 23, 4, 5, 6); // DateTime型で表された日時

    // 現在のカルチャ(ja-JP)の(デフォルトの)書式でDateTimeを文字列化
    Console.WriteLine($"{CultureInfo.CurrentCulture}: {dt}");

    // カルチャをen-US(英語/アメリカ合衆国)に変更してDateTimeを文字列化
    Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");

    Console.WriteLine($"{CultureInfo.CurrentCulture}: {dt}");

    // de-DE(ドイツ語/ドイツ)のカルチャを書式プロバイダに指定してDateTimeを文字列化
    var de_de = new CultureInfo("de-DE");

    Console.WriteLine(string.Format(provider: de_de, "{0}: {1}", de_de, dt));
    Console.WriteLine();

    // カルチャに応じてローカライズされる書式指定子'F'(完全な日付と時刻)と、
    // 各カルチャを書式プロバイダに指定してDateTimeを文字列化
    Console.WriteLine("F(ja-JP): {0}", dt.ToString(format: "F", provider: new CultureInfo("ja-JP"))); // 日本語/日本
    Console.WriteLine("F(en-US): {0}", dt.ToString("F", new CultureInfo("en-US"))); // 英語/米国
    Console.WriteLine("F(en-GB): {0}", dt.ToString("F", new CultureInfo("en-GB"))); // 英語/英国
    Console.WriteLine("F(de-DE): {0}", dt.ToString("F", new CultureInfo("de-DE"))); // ドイツ語/ドイツ
  }
}
DateTimeの文字列化における書式指定子・書式プロバイダの動作 
Imports System
Imports System.Globalization
Imports System.Threading

Class Sample
  Shared Sub Main()
    Dim dt As New DateTime(2000, 1, 23, 4, 5, 6) ' DateTime型で表された日時

    ' 現在のカルチャ(ja-JP)の(デフォルトの)書式でDateTimeを文字列化
    Console.WriteLine($"{CultureInfo.CurrentCulture}: {dt}")

    ' カルチャをen-US(英語/アメリカ合衆国)に変更してDateTimeを文字列化
    Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")

    Console.WriteLine($"{CultureInfo.CurrentCulture}: {dt}")

    ' de-DE(ドイツ語/ドイツ)のカルチャを書式プロバイダに指定してDateTimeを文字列化
    Dim de_de As New CultureInfo("de-DE")

    Console.WriteLine(String.Format(provider := de_de, "{0}: {1}", de_de, dt))
    Console.WriteLine()

    ' カルチャに応じてローカライズされる書式指定子'F'(完全な日付と時刻)と、
    ' 各カルチャを書式プロバイダに指定してDateTimeを文字列化
    Console.WriteLine("F(ja-JP): {0}", dt.ToString(format := "F", provider := new CultureInfo("ja-JP"))) ' 日本語/日本
    Console.WriteLine("F(en-US): {0}", dt.ToString("F", new CultureInfo("en-US"))) ' 英語/米国
    Console.WriteLine("F(en-GB): {0}", dt.ToString("F", new CultureInfo("en-GB"))) ' 英語/英国
    Console.WriteLine("F(de-DE): {0}", dt.ToString("F", new CultureInfo("de-DE"))) ' ドイツ語/ドイツ
  End Sub
End Class
実行結果
ja-JP: 2000/01/23 4:05:06
en-US: 1/23/2000 4:05:06 AM
de-DE: 23.01.2000 04:05:06

F(ja-JP): 2000年1月23日日曜日 4:05:06
F(en-US): Sunday, January 23, 2000 4:05:06 AM
F(en-GB): Sunday, 23 January 2000 04:05:06
F(de-DE): Sonntag, 23. Januar 2000 04:05:06
実行結果
ja-JP: 2000/01/23 4:05:06
en-US: 1/23/2000 4:05:06 AM
de-DE: 23.01.2000 04:05:06

F(ja-JP): 2000年1月23日 4:05:06
F(en-US): Sunday, January 23, 2000 4:05:06 AM
F(en-GB): 23 January 2000 04:05:06
F(de-DE): Sonntag, 23. Januar 2000 04:05:06
実行結果
ja-JP: 2000/01/23 4:05:06
en-US: 1/23/2000 4:05:06 AM
de-DE: 23.01.2000 04:05:06

F(ja-JP): 2000年1月23日 4:05:06
F(en-US): Sunday, January 23, 2000 4:05:06 AM
F(en-GB): 23 January 2000 04:05:06
F(de-DE): Sonntag, 23. Januar 2000 04:05:06

文字列化に際してカルチャ(CurrentCulture)によって異なる表記となることが問題となる場合は、ローカライズされない書式を用いるか、インバリアントカルチャなど固定のカルチャを書式プロバイダとして用いるようにします。

日付と時刻に関して言えば、日付と時刻の書式としてISO 8601形式(書式指定子o)やRFC1123形式(書式指定子r)などのカルチャによってローカライズされない書式が定義されているため、インバリアントカルチャだけでなくこれらの書式を使用することもできます。

文字列比較の規則・ソート順定義(CompareInfo)としてのCurrentCulture

カルチャ(CurrentCulture)は文字列比較におけるデフォルトの規則としても動作します。 .NETの文字列クラスであるStringでは、文字列比較の際、デフォルトでは現在のカルチャよって定義される規則(CompareInfo)に従った動作を行います。

この規則は、文字列内の探索・判定(IndexOf, Containsなど)のほか、文字列同士の大小関係の比較(Compare)、等価性の比較(Equals)などに適用されます。 これに従い、Dictionaryにおける文字列キーの一致・不一致の判定や、正規表現におけるパターンマッチ、文字列配列のソート時のデフォルトの順序などにも影響することになります。

一例としてOrderByメソッドを用いた文字列配列のソートでは、(文字列比較のオプションを指定しない限り)次のようにカルチャによってソート順序が変わります。

文字列配列のソートにおけるCompareInfoの動作
using System;
using System.Globalization;
using System.Linq;

class Sample {
  // 文字列配列を並べ替えて表示する
  static void DisplaySortedStrings()
  {
    Console.WriteLine(
      "{0,-12}: {1} | {2} | {3}",
      CultureInfo.CurrentCulture,
      // OrderByメソッドを使い、オプションを指定せず(現在のカルチャの規則で)ソート
      string.Join(", ", new[] {"亜", "井", "宇"}.OrderBy(s => s)),
      string.Join(", ", new[] {"一", "二", "三", "四", "五"}.OrderBy(s => s)),
      string.Join(", ", new[] {"あ", "ア", "亜", "ア", "A"}.OrderBy(s => s))
    );
  }

  static void Main()
  {
    // 現在のカルチャをja-JPに変更して実行
    CultureInfo.CurrentCulture = new CultureInfo("ja-JP");
    DisplaySortedStrings();

    // 現在のカルチャをzh-Hant-TW(中国語/繁体字/台湾)に変更して実行
    CultureInfo.CurrentCulture = new CultureInfo("zh-Hant-TW");
    DisplaySortedStrings();

    // 現在のカルチャをen-USに変更して実行
    CultureInfo.CurrentCulture = new CultureInfo("en-US");
    DisplaySortedStrings();

    // 現在のカルチャをインバリアントカルチャに変更して実行
    CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
    DisplaySortedStrings();
  }
}
文字列配列のソートにおけるCompareInfoの動作
Imports System
Imports System.Globalization
Imports System.Threading

Class Sample
  ' 文字列配列を並べ替えて表示する
  Shared Sub DisplaySortedStrings()
    Console.WriteLine(
      "{0,-12}: {1} | {2} | {3}",
      CultureInfo.CurrentCulture,
      String.Join(", ", ({"亜", "井", "宇"}).OrderBy(Function (s) s)), ' OrderByメソッドを使い、オプションを指定せず(現在のカルチャの規則で)ソート
      String.Join(", ", ({"一", "二", "三", "四", "五"}).OrderBy(Function (s) s)),
      String.Join(", ", ({"あ", "ア", "亜", "ア", "A"}).OrderBy(Function (s) s))
    )
  End Sub

  Shared Sub Main()
    ' 現在のカルチャをja-JPに変更して実行
    CultureInfo.CurrentCulture = New CultureInfo("ja-JP")
    DisplaySortedStrings()

    ' 現在のカルチャをzh-Hant-TW(中国語/繁体字/台湾)に変更して実行
    CultureInfo.CurrentCulture = New CultureInfo("zh-Hant-TW")
    DisplaySortedStrings()

    ' 現在のカルチャをen-USに変更して実行
    CultureInfo.CurrentCulture = New CultureInfo("en-US")
    DisplaySortedStrings()

    ' 現在のカルチャをインバリアントカルチャに変更して実行
    CultureInfo.CurrentCulture = CultureInfo.InvariantCulture
    DisplaySortedStrings()
  End Sub
End Class
実行結果
ja-JP       : 亜, 井, 宇 | 一, 五, 三, 四, 二 | A, あ, ア, ア, 亜
zh-Hant-TW  : 井, 宇, 亜 | 一, 二, 三, 五, 四 | 亜, A, あ, ア, ア
en-US       : 井, 亜, 宇 | 一, 三, 二, 五, 四 | A, あ, ア, ア, 亜
            : 井, 亜, 宇 | 一, 三, 二, 五, 四 | A, あ, ア, ア, 亜
実行結果
ja-JP       : 亜, 井, 宇 | 一, 五, 三, 四, 二 | A, ア, ア, あ, 亜
zh-TW       : 井, 宇, 亜 | 一, 二, 三, 五, 四 | A, ア, ア, あ, 亜
en-US       : 井, 亜, 宇 | 一, 三, 二, 五, 四 | A, ア, ア, あ, 亜
            : 井, 亜, 宇 | 一, 三, 二, 五, 四 | A, ア, ア, あ, 亜
実行結果
ja-JP       : 亜, 井, 宇 | 一, 五, 三, 四, 二 | A, ア, ア, あ, 亜
zh-TW       : 井, 亜, 宇 | 一, 三, 二, 五, 四 | A, ア, ア, あ, 亜
en-US       : 井, 亜, 宇 | 一, 三, 二, 五, 四 | A, ア, ア, あ, 亜
            : 井, 亜, 宇 | 一, 三, 二, 五, 四 | A, ア, ア, あ, 亜

zh-Hans-*/zh-Hant-*はサポートされていない

このように、漢字同士のソートでは、ja-JP(日本語/日本)では漢字の読みに準じた並び「(あ), (い), (う)」となり、一方zh-Hant-TW(中国語/繁体字/台湾)では詳細は不明ながら画数と部首に準じた並び「(4画), (6画), (7画)」となるようです。 このほかen-US(英語/米国)やインバリアントカルチャでは、文字のコードポイントに基づいた並び「(U+4E95)・(U+4E9C)・(U+5B87)」となります。

また、同種の文字同士の順序だけでなく、文字の種類(A)の順序も、カルチャによって異なります。 いずれにせよ、文字列配列をソートしたときの結果はカルチャによってそれぞれ異なったものとなります。

注釈

データの比較と並べ替えの規則は、カルチャによって異なります。 たとえば、並べ替え順序は、ふりがなまたは文字の視覚的表現に基づいている場合があります。 東アジア圏の言語では、文字が表意文字の画数と部首によって並べ替えられます。 また、並べ替えは、言語やカルチャで使用されているアルファベットの順序によっても異なります。 たとえば、デンマーク語の文字 "Æ" は、アルファベットでは "Z" の後に位置します。

CompareInfo クラス (System.Globalization) | Microsoft Docs

並べ替えキーの使用

.NET Framework では、並べ替えキーを使用して、カルチャに依存した並べ替え操作をサポートします。 文字列内の各文字には、アルファベット順、大文字と小文字の区別、発音の区別など、さまざまなカテゴリの並べ替えウェイトが指定されます。 並べ替えキーは、特定の文字列に対するこれらのウェイトを格納するリポジトリとして使用されます。 たとえば、並べ替えキーにはアルファベット順ウェイトの文字列、大文字小文字のウェイトの文字列などが特定の順序で格納されています。 並べ替えキーの詳細については、「Unicode Technical Standard #10: Unicode Collation Algorithm (Unicode 技術標準 #10: Unicode 照合順序アルゴリズム)」の Unicode 標準を参照してください。

固有カルチャのデータの比較と並べ替え | Microsoft Docs

文字列比較に際してカルチャによって異なる動作となることが問題となる場合は、StringComparison列挙体やStringComparerクラスを文字列比較動作のオプションとして明示的に指定します。 このほか、String.ToUpperInvariantメソッドRegexOptions.CultureInvariantなどのように、インバリアントカルチャに基づいた処理を行うように指定できるメソッド・オプションが用意されているものもあるため、これを使用することもできます。

ほかにも、文字列を自然言語としての意味解釈を廃した単なる数値列として比較するような目的には、インバリアントカルチャよりもコードポイントによる比較を選択したほうがより適切です。 具体的には、ファイルパスやID、識別子、仕様や標準で定義される名称・予約語・キーワードなどに対する比較がこれにあたります。 コードポイントによる比較は、StringComparison.OrdinalなどのオプションやString.CompareOrdinalなどによって行うことができます。

例外メッセージの言語選択としてのCurrentUICulture

UIカルチャは、ダイアログやメッセージなど、主にユーザーが直接触れるユーザーインターフェイス要素のローカライズに用いられます。

例として、例外クラスではUIカルチャに基づいてローカライズされたメッセージが設定されます。 このため、同じ型の例外でもUIカルチャによって表示されるメッセージが変わります。

スローされる例外のメッセージにおけるCurrentUICultureの動作
using System;
using System.Globalization;
using System.Threading;

class Sample {
  static void ThrowException()
  {
    try {
      throw new InvalidOperationException(); // 例外をスローする
    }
    catch (Exception ex) {
      // 現在のUIカルチャと、スローされた例外のメッセージを表示
      Console.WriteLine($"{CultureInfo.CurrentUICulture}: {ex.Message}");
    }
  }

  static void Main()
  {
    ThrowException();

    // UIカルチャをen-US(英語/アメリカ合衆国)に変更
    Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");

    ThrowException();
  }
}
スローされる例外のメッセージにおけるCurrentUICultureの動作
Imports System
Imports System.Globalization
Imports System.Threading

Class Sample
  Shared Sub ThrowException()
    Try
      Throw New InvalidOperationException() ' 例外をスローする
    Catch ex As Exception
      ' 現在のUIカルチャと、スローされた例外のメッセージを表示
      Console.WriteLine($"{CultureInfo.CurrentUICulture}: {ex.Message}")
    End Try
  End Sub

  Shared Sub Main()
    ThrowException()

    ' UIカルチャをen-US(英語/アメリカ合衆国)に変更
    Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US")

    ThrowException()
  End Sub
End Class
実行結果
ja-JP: オブジェクトの現在の状態に問題があるため、操作は有効ではありません。
en-US: Operation is not valid due to the current state of the object.
実行結果
ja-JP: Operation is not valid due to the current state of the object.
en-US: Operation is not valid due to the current state of the object.
実行結果
ja-JP: Operation is not valid due to the current state of the object.
en-US: Operation is not valid due to the current state of the object.

このほかの例として、ResourceManagerクラスでは、UIカルチャにしたがってそのカルチャ向けのリソースを取得することができます。 ResourceManagerでは、特定のカルチャを指定してそのカルチャ向けにローカライズされたリソースを取得することもできます。

例外クラスにおいても、内部的にはResourceManagerによってローカライズされた例外メッセージを取得しています。

.NET Coreおよび少なくとも.NET 5の時点では、例外メッセージはローカライズされずに表示されます。 ただし、これはあくまでリソース内における例外メッセージがローカライズされていないだけで、ResourceManagerによってUIカルチャに基づく例外メッセージを取得する動作そのものは.NET Frameworkと同じとなっているようです。 そのため、今後のバージョンではローカライズされた例外メッセージが表示されるようになる可能性があります。