全球化涉及设计和开发一个适应全球的应用程序,该应用程序支持为多个文化背景的用户提供本地化界面和区域特定数据。 在开始设计阶段之前,你应确定你的应用将支持哪些文化。 尽管应用默认面向单一文化或地区,你可以设计和编写它,以便能够轻松扩展至其他文化或地区的用户。
作为开发人员,我们对由文化形成的用户界面和数据都有假设。 例如,对于美国的英语开发人员,将日期和时间数据序列化为格式 MM/dd/yyyy hh:mm:ss
的字符串似乎完全合理。 但是,在不同文化背景的系统上反序列化该字符串可能会引发 FormatException 异常或生成不准确的数据。 全球化使我们能够识别此类区域性特定的假设,并确保它们不会影响应用的设计或代码。
本文讨论了应考虑的一些主要问题,以及处理全球化应用中的字符串、日期和时间值以及数值时可以遵循的最佳做法。
字符串
字符和字符串的处理是全球化的核心焦点,因为每个文化或地区可能使用不同的字符和字符集,并以不同方式排序。 本部分提供有关在全球化应用中使用字符串的建议。
在内部使用 Unicode
默认情况下,.NET 使用 Unicode 字符串。 Unicode 字符串由零个、一个或多个 Char 对象组成,每个对象表示 UTF-16 代码单元。 对于每个字符集中的几乎每个字符来说,都有一个在全球范围内使用的 Unicode 表达式。
许多应用程序和作系统(包括 Windows作系统)还可以使用代码页来表示字符集。 代码页通常包含从0x00到0x7F的标准 ASCII 值,并将其他字符映射到从0x80到0xFF的剩余值。 从0x80到0xFF的值的解释取决于特定的代码页。 因此,应尽可能避免在全球化应用中使用代码页。
下面的示例演示了当系统上的默认代码页不同于保存数据的代码页时解释代码页数据的危险。 (为了模拟此方案,该示例显式指定不同的代码页。首先,该示例定义一个由希腊字母表的大写字符组成的数组。 它使用代码页 737(也称为 MS-DOS 希腊文)将它们编码为字节数组,并将字节数组保存到文件中。 如果检索文件并使用代码页 737 解码其字节数组,则会还原原始字符。 但是,如果使用代码页 1252(或表示拉丁字母中的字符的 Windows-1252)对文件进行检索并解码其字节数组,原始字符将丢失。
using System;
using System.IO;
using System.Text;
public class Example
{
public static void CodePages()
{
// Represent Greek uppercase characters in code page 737.
char[] greekChars =
{
'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ',
'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π',
'Ρ', 'Σ', 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω'
};
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding cp737 = Encoding.GetEncoding(737);
int nBytes = cp737.GetByteCount(greekChars);
byte[] bytes737 = new byte[nBytes];
bytes737 = cp737.GetBytes(greekChars);
// Write the bytes to a file.
FileStream fs = new FileStream(@".\\CodePageBytes.dat", FileMode.Create);
fs.Write(bytes737, 0, bytes737.Length);
fs.Close();
// Retrieve the byte data from the file.
fs = new FileStream(@".\\CodePageBytes.dat", FileMode.Open);
byte[] bytes1 = new byte[fs.Length];
fs.Read(bytes1, 0, (int)fs.Length);
fs.Close();
// Restore the data on a system whose code page is 737.
string data = cp737.GetString(bytes1);
Console.WriteLine(data);
Console.WriteLine();
// Restore the data on a system whose code page is 1252.
Encoding cp1252 = Encoding.GetEncoding(1252);
data = cp1252.GetString(bytes1);
Console.WriteLine(data);
}
}
// The example displays the following output:
// ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
// €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—
Imports System.IO
Imports System.Text
Module Example
Public Sub CodePages()
' Represent Greek uppercase characters in code page 737.
Dim greekChars() As Char = {"Α"c, "Β"c, "Γ"c, "Δ"c, "Ε"c, "Ζ"c, "Η"c, "Θ"c,
"Ι"c, "Κ"c, "Λ"c, "Μ"c, "Ν"c, "Ξ"c, "Ο"c, "Π"c,
"Ρ"c, "Σ"c, "Τ"c, "Υ"c, "Φ"c, "Χ"c, "Ψ"c, "Ω"c}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
Dim cp737 As Encoding = Encoding.GetEncoding(737)
Dim nBytes As Integer = CInt(cp737.GetByteCount(greekChars))
Dim bytes737(nBytes - 1) As Byte
bytes737 = cp737.GetBytes(greekChars)
' Write the bytes to a file.
Dim fs As New FileStream(".\CodePageBytes.dat", FileMode.Create)
fs.Write(bytes737, 0, bytes737.Length)
fs.Close()
' Retrieve the byte data from the file.
fs = New FileStream(".\CodePageBytes.dat", FileMode.Open)
Dim bytes1(CInt(fs.Length - 1)) As Byte
fs.Read(bytes1, 0, CInt(fs.Length))
fs.Close()
' Restore the data on a system whose code page is 737.
Dim data As String = cp737.GetString(bytes1)
Console.WriteLine(data)
Console.WriteLine()
' Restore the data on a system whose code page is 1252.
Dim cp1252 As Encoding = Encoding.GetEncoding(1252)
data = cp1252.GetString(bytes1)
Console.WriteLine(data)
End Sub
End Module
' The example displays the following output:
' ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
' €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—
Unicode 的使用可确保相同的代码单元始终映射到相同的字符,并且相同的字符始终映射到相同的字节数组。
使用资源文件
即使你正在开发面向单一文化或区域的应用,也应使用资源文件来存储用户界面中显示的字符串和其他资源。 不应将它们直接添加到代码中。 使用资源文件具有许多优点:
- 所有字符串都位于单个位置。 您不必在整个源代码中搜索来识别需要修改以适应特定语言或文化的字符串。
- 无需重复字符串。 不使用资源文件的开发人员通常会在多个源代码文件中定义相同的字符串。 此重复会增加修改字符串时将忽略一个或多个实例的概率。
- 可以将非字符串资源(如图像或二进制数据)包含在资源文件中,而不是将它们存储在单独的独立文件中,以便可以轻松检索它们。
如果要创建本地化的应用,则使用资源文件具有特殊优势。 在附属程序集中部署资源时,公共语言运行时会基于由 CultureInfo.CurrentUICulture 属性定义的用户当前 UI 区域性来自动选择适合区域性的资源。 只要提供了相应的区域性特定资源并正确示例化了 ResourceManager 对象或使用了强类型的资源类,运行时就会负责检索适合的资源。
有关创建资源文件的详细信息,请参阅 “创建资源文件”。 有关创建和部署附属程序集的信息,请参阅创建附属程序集和打包和部署资源。
搜索和比较字符串
应尽可能将字符串作为整个字符串进行处理,而不是将它们作为一系列单个字符进行处理。 当你对子字符串进行排序或搜索时,这一点尤其重要,以防止与分析组合字符相关的问题。
小窍门
可以使用 StringInfo 该类来处理文本元素,而不是字符串中的单个字符。
在字符串搜索和比较中,常见的错误是将字符串视为字符集合,每个字符都由对象 Char 表示。 事实上,单个字符可能由一个、两个或多个 Char 对象构成。 此类字符在一些字符串中出现得最频繁,这些字符串位于其字母表是由 Unicode 基本拉丁字符范围(从 U+0021 到 U+007E)以外的字符所组成的区域性中。 以下示例尝试在字符串中找到拉丁文大写字母 A WITH GRAVE 字符(U+00C0)的索引。 但是,可以通过两种不同的方式表示此字符:单个代码单元(U+00C0)或复合字符(两个代码单元:U+0041 和 U+0300)。 在这种情况下,字符在字符串实例中由两个 Char 对象(U+0041 和 U+0300)表示。 示例代码调用 String.IndexOf(Char) 并 String.IndexOf(String) 重载以查找此字符在字符串实例中的位置,但这些结果返回不同的结果。 第一个方法调用具有参数 Char ;它执行序号比较,因此找不到匹配项。 第二次调用包含String参数;它执行区分文化差异的比较,因此找到匹配项。
using System;
using System.Globalization;
using System.Threading;
public class Example17
{
public static void Main17()
{
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("pl-PL");
string composite = "\u0041\u0300";
Console.WriteLine($"Comparing using Char: {composite.IndexOf('\u00C0')}");
Console.WriteLine($"Comparing using String: {composite.IndexOf("\u00C0")}");
}
}
// The example displays the following output:
// Comparing using Char: -1
// Comparing using String: 0
Imports System.Globalization
Imports System.Threading
Module Example17
Public Sub Main17()
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("pl-PL")
Dim composite As String = ChrW(&H41) + ChrW(&H300)
Console.WriteLine("Comparing using Char: {0}", composite.IndexOf(ChrW(&HC0)))
Console.WriteLine("Comparing using String: {0}", composite.IndexOf(ChrW(&HC0).ToString()))
End Sub
End Module
' The example displays the following output:
' Comparing using Char: -1
' Comparing using String: 0
可以通过调用包含 StringComparison 参数(如 String.IndexOf(String, StringComparison) 或 String.LastIndexOf(String, StringComparison) 方法)的重载来避免此示例的某些歧义(对返回不同结果的方法的两个类似重载的调用)。
然而,搜索并不总是具有文化敏感性。 如果搜索的目的是做出安全决策或允许或禁止访问某些资源,则应按序号进行比较,如下一部分所述。
测试字符串的相等性
如果要测试两个字符串是否相等,而不是确定它们在排序顺序中的比较方式,请使用 String.Equals 该方法而不是字符串比较方法,例如 String.Compare 或 CompareInfo.Compare。
通常对相等性进行比较以有条件地访问某些资源。 例如,您可以执行相等比较来验证密码或确认文件是否存在。 此类非语言比较应始终是顺序的,而不是文化敏感的。 通常,应调用实例 String.Equals(String, StringComparison) 方法或静态 String.Equals(String, String, StringComparison) 方法,其中包含字符串的值 StringComparison.Ordinal (如密码),以及文件名或 URI 等字符串的值 StringComparison.OrdinalIgnoreCase 。
相等性的比较有时会涉及到搜索或子字符串比较,而不是对 String.Equals 方法的调用。 在某些情况下,可以使用子字符串搜索来确定该子字符串是否等于另一个字符串。 如果比较的目的是非语义的,那么搜索也应该为序号搜索,而不区分区域性。
以下示例说明了对非语言数据进行区域性敏感搜索的危险。 此方法 AccessesFileSystem
旨在禁止以子字符串“FILE”开头的 URI 的文件系统访问。 为了实现这一点,它会对 URI 的开头部分与字符串“FILE”进行不区分大小写并考虑文化差异的比较。 由于访问文件系统的 URI 可以以“FILE:”或“file:”开头,因此隐式假设是“i”(U+0069)始终是“I”(U+0049)的小写等效项。 然而,在土耳其语和阿塞拜疆语中,“i”的大写版本是“İ”(U+0130)。 由于这种差异,基于文化敏感性的比较允许访问本应被禁止的文件系统。
using System;
using System.Globalization;
using System.Threading;
public class Example10
{
public static void Main10()
{
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR");
string uri = @"file:\\c:\users\username\Documents\bio.txt";
if (!AccessesFileSystem(uri))
// Permit access to resource specified by URI
Console.WriteLine("Access is allowed.");
else
// Prohibit access.
Console.WriteLine("Access is not allowed.");
}
private static bool AccessesFileSystem(string uri)
{
return uri.StartsWith("FILE", true, CultureInfo.CurrentCulture);
}
}
// The example displays the following output:
// Access is allowed.
Imports System.Globalization
Imports System.Threading
Module Example10
Public Sub Main10()
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR")
Dim uri As String = "file:\\c:\users\username\Documents\bio.txt"
If Not AccessesFileSystem(uri) Then
' Permit access to resource specified by URI
Console.WriteLine("Access is allowed.")
Else
' Prohibit access.
Console.WriteLine("Access is not allowed.")
End If
End Sub
Private Function AccessesFileSystem(uri As String) As Boolean
Return uri.StartsWith("FILE", True, CultureInfo.CurrentCulture)
End Function
End Module
' The example displays the following output:
' Access is allowed.
因此,可执行忽视大小写的序号比较来避免出现此问题,如下例所示。
using System;
using System.Globalization;
using System.Threading;
public class Example11
{
public static void Main11()
{
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR");
string uri = @"file:\\c:\users\username\Documents\bio.txt";
if (!AccessesFileSystem(uri))
// Permit access to resource specified by URI
Console.WriteLine("Access is allowed.");
else
// Prohibit access.
Console.WriteLine("Access is not allowed.");
}
private static bool AccessesFileSystem(string uri)
{
return uri.StartsWith("FILE", StringComparison.OrdinalIgnoreCase);
}
}
// The example displays the following output:
// Access is not allowed.
Imports System.Globalization
Imports System.Threading
Module Example11
Public Sub Main11()
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR")
Dim uri As String = "file:\\c:\users\username\Documents\bio.txt"
If Not AccessesFileSystem(uri) Then
' Permit access to resource specified by URI
Console.WriteLine("Access is allowed.")
Else
' Prohibit access.
Console.WriteLine("Access is not allowed.")
End If
End Sub
Private Function AccessesFileSystem(uri As String) As Boolean
Return uri.StartsWith("FILE", StringComparison.OrdinalIgnoreCase)
End Function
End Module
' The example displays the following output:
' Access is not allowed.
排列和排序字符串
通常,要在用户界面中显示的有序字符串应根据区域性进行排序。 在大多数情况下,调用排序字符串的方法(例如 Array.Sort 或 List<T>.Sort)时,此类字符串比较由 .NET 隐式处理。 默认情况下,使用当前区域性的排序约定对字符串进行排序。 下面的示例演示了使用英语(美国)和瑞典(瑞典)的语言和文化习惯对字符串数组进行排序时的差异。
using System;
using System.Globalization;
using System.Threading;
public class Example18
{
public static void Main18()
{
string[] values = { "able", "ångström", "apple", "Æble",
"Windows", "Visual Studio" };
// Change thread to en-US.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
// Sort the array and copy it to a new array to preserve the order.
Array.Sort(values);
string[] enValues = (String[])values.Clone();
// Change culture to Swedish (Sweden).
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("sv-SE");
Array.Sort(values);
string[] svValues = (String[])values.Clone();
// Compare the sorted arrays.
Console.WriteLine("{0,-8} {1,-15} {2,-15}\n", "Position", "en-US", "sv-SE");
for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues[ctr], svValues[ctr]);
}
}
// The example displays the following output:
// Position en-US sv-SE
//
// 0 able able
// 1 Æble Æble
// 2 ångström apple
// 3 apple Windows
// 4 Visual Studio Visual Studio
// 5 Windows ångström
Imports System.Globalization
Imports System.Threading
Module Example18
Public Sub Main18()
Dim values() As String = {"able", "ångström", "apple",
"Æble", "Windows", "Visual Studio"}
' Change thread to en-US.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
' Sort the array and copy it to a new array to preserve the order.
Array.Sort(values)
Dim enValues() As String = CType(values.Clone(), String())
' Change culture to Swedish (Sweden).
Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
Array.Sort(values)
Dim svValues() As String = CType(values.Clone(), String())
' Compare the sorted arrays.
Console.WriteLine("{0,-8} {1,-15} {2,-15}", "Position", "en-US", "sv-SE")
Console.WriteLine()
For ctr As Integer = 0 To values.GetUpperBound(0)
Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues(ctr), svValues(ctr))
Next
End Sub
End Module
' The example displays the following output:
' Position en-US sv-SE
'
' 0 able able
' 1 Æble Æble
' 2 ångström apple
' 3 apple Windows
' 4 Visual Studio Visual Studio
' 5 Windows ångström
区分区域性的字符串比较是由 CompareInfo 对象定义的,该对象由每个区域性的 CultureInfo.CompareInfo 属性返回。 使用 String.Compare 方法重载的区分区域性的字符串比较也使用 CompareInfo 对象。
.NET 使用表对字符串数据执行具有文化敏感性的排序。 这些表的内容包含排序权重和字符串规范化的数据,由特定版本的 .NET 实现的 Unicode 标准版本确定。 下表列出了由指定版本的 .NET 实现的 Unicode 版本。 此支持的 Unicode 版本列表仅适用于字符比较和排序;它不适用于按类别分类 Unicode 字符。 有关详细信息,请参阅本文中的 String “字符串和 Unicode 标准”部分。
.NET Framework 版本 | 操作系统 | Unicode 版本 |
---|---|---|
.NET Framework 2.0 | 所有操作系统 | Unicode 4.1 |
.NET Framework 3.0 | 所有操作系统 | Unicode 4.1 |
.NET Framework 3.5 | 所有操作系统 | Unicode 4.1 |
.NET Framework 4 | 所有操作系统 | Unicode 5.0 |
.NET Framework 4.5 及更高版本 | Windows 7 | Unicode 5.0 |
.NET Framework 4.5 及更高版本 | Windows 8 及更高版本的作系统 | Unicode 6.3.0 |
.NET Core 和 .NET 5+ | 取决于基础 OS 支持的 Unicode 标准版本。 |
从 .NET Framework 4.5 开始,在 .NET Core 和 .NET 5+ 的所有版本中,字符串比较和排序取决于作系统。 在 Windows 7 上运行的 .NET Framework 4.5 及更高版本从实现 Unicode 5.0 的自己的表中检索数据。 在 Windows 8 及更高版本上运行的 .NET Framework 4.5 及更高版本从实现 Unicode 6.3 的作系统表中检索数据。 在 .NET Core 和 .NET 5+ 上,支持的 Unicode 版本取决于基础作系统。 如果对区分区域性的已排序数据进行序列化,可使用 SortVersion 类来确定何时需要对序列化数据进行排序,使其与 .NET 和操作系统的排序顺序保持一致。 有关示例,请参阅 SortVersion 类主题。
如果您的应用程序执行大量特定于区域性字符串数据的排序操作,则可以使用 SortKey 类来比较字符串。 排序键反映了特定文化的排序权重,包括特定字符串的字母、大小写和附加符号的权重。 由于使用排序键的比较是二进制的,因此它们比隐式或显式使用 CompareInfo 对象的比较更快。 可通过将字符串传递给 CompareInfo.GetSortKey 方法为特定字符串创建区分区域性的排序关键字。
以下示例类似于前面的示例。 但是,它并没有调用Array.Sort(Array)方法(该方法会隐式调用CompareInfo.Compare方法),而是定义了一个System.Collections.Generic.IComparer<T>实现来比较排序键,并实例化该实现以传递给Array.Sort<T>(T[], IComparer<T>)方法。
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
public class SortKeyComparer : IComparer<String>
{
public int Compare(string? str1, string? str2)
{
return (str1, str2) switch
{
(null, null) => 0,
(null, _) => -1,
(_, null) => 1,
(var s1, var s2) => SortKey.Compare(
CultureInfo.CurrentCulture.CompareInfo.GetSortKey(s1),
CultureInfo.CurrentCulture.CompareInfo.GetSortKey(s1))
};
}
}
public class Example19
{
public static void Main19()
{
string[] values = { "able", "ångström", "apple", "Æble",
"Windows", "Visual Studio" };
SortKeyComparer comparer = new SortKeyComparer();
// Change thread to en-US.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
// Sort the array and copy it to a new array to preserve the order.
Array.Sort(values, comparer);
string[] enValues = (String[])values.Clone();
// Change culture to Swedish (Sweden).
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("sv-SE");
Array.Sort(values, comparer);
string[] svValues = (String[])values.Clone();
// Compare the sorted arrays.
Console.WriteLine("{0,-8} {1,-15} {2,-15}\n", "Position", "en-US", "sv-SE");
for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues[ctr], svValues[ctr]);
}
}
// The example displays the following output:
// Position en-US sv-SE
//
// 0 able able
// 1 Æble Æble
// 2 ångström apple
// 3 apple Windows
// 4 Visual Studio Visual Studio
// 5 Windows ångström
Imports System.Collections.Generic
Imports System.Globalization
Imports System.Threading
Public Class SortKeyComparer : Implements IComparer(Of String)
Public Function Compare(str1 As String, str2 As String) As Integer _
Implements IComparer(Of String).Compare
Dim sk1, sk2 As SortKey
sk1 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str1)
sk2 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str2)
Return SortKey.Compare(sk1, sk2)
End Function
End Class
Module Example19
Public Sub Main19()
Dim values() As String = {"able", "ångström", "apple",
"Æble", "Windows", "Visual Studio"}
Dim comparer As New SortKeyComparer()
' Change thread to en-US.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
' Sort the array and copy it to a new array to preserve the order.
Array.Sort(values, comparer)
Dim enValues() As String = CType(values.Clone(), String())
' Change culture to Swedish (Sweden).
Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
Array.Sort(values, comparer)
Dim svValues() As String = CType(values.Clone(), String())
' Compare the sorted arrays.
Console.WriteLine("{0,-8} {1,-15} {2,-15}", "Position", "en-US", "sv-SE")
Console.WriteLine()
For ctr As Integer = 0 To values.GetUpperBound(0)
Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues(ctr), svValues(ctr))
Next
End Sub
End Module
' The example displays the following output:
' Position en-US sv-SE
'
' 0 able able
' 1 Æble Æble
' 2 ångström apple
' 3 apple Windows
' 4 Visual Studio Visual Studio
' 5 Windows ångström
避免字符串串联
如果可能,请避免使用从串联短语在运行时生成的复合字符串。 复合字符串难以本地化,因为它们通常假定应用的原始语言中的语法顺序不适用于其他本地化语言。
处理日期和时间
如何处理日期和时间值取决于它们是显示在用户界面中还是持久化。 本部分将检查这两种用法。 它还讨论了在处理日期和时间时,如何应对时区差异和进行算术运算。
显示日期和时间
通常,如果日期和时间要显示在用户界面中,应使用用户区域性的格式约定,此约定是由 CultureInfo.CurrentCulture 属性以及 CultureInfo.CurrentCulture.DateTimeFormat
属性返回的 DateTimeFormatInfo 对象定义的。 在使用以下 3 种方法之一设置日期的格式时会自动使用当前区域性的格式约定:
- 无 DateTime.ToString() 参数方法。
- 该方法 DateTime.ToString(String) 包括格式字符串。
- 无 DateTimeOffset.ToString() 参数方法。
- DateTimeOffset.ToString(String),其中包含一个格式字符串。
- 复合格式设置 功能在与日期一起使用时。
以下示例显示 2012 年 10 月 11 日的日出和日落数据两次。 它首先将当前区域性设置为克罗地亚语(克罗地亚),然后是英语(英国)。 在每种情况下,日期和时间都以适合该文化的格式显示。
using System;
using System.Globalization;
using System.Threading;
public class Example3
{
static DateTime[] dates = { new DateTime(2012, 10, 11, 7, 06, 0),
new DateTime(2012, 10, 11, 18, 19, 0) };
public static void Main3()
{
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("hr-HR");
ShowDayInfo();
Console.WriteLine();
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
ShowDayInfo();
}
private static void ShowDayInfo()
{
Console.WriteLine($"Date: {dates[0]:D}");
Console.WriteLine($" Sunrise: {dates[0]:T}");
Console.WriteLine($" Sunset: {dates[1]:T}");
}
}
// The example displays the following output:
// Date: 11. listopada 2012.
// Sunrise: 7:06:00
// Sunset: 18:19:00
//
// Date: 11 October 2012
// Sunrise: 07:06:00
// Sunset: 18:19:00
Imports System.Globalization
Imports System.Threading
Module Example3
Dim dates() As Date = {New Date(2012, 10, 11, 7, 6, 0),
New Date(2012, 10, 11, 18, 19, 0)}
Public Sub Main3()
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("hr-HR")
ShowDayInfo()
Console.WriteLine()
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
ShowDayInfo()
End Sub
Private Sub ShowDayInfo()
Console.WriteLine("Date: {0:D}", dates(0))
Console.WriteLine(" Sunrise: {0:T}", dates(0))
Console.WriteLine(" Sunset: {0:T}", dates(1))
End Sub
End Module
' The example displays the following output:
' Date: 11. listopada 2012.
' Sunrise: 7:06:00
' Sunset: 18:19:00
'
' Date: 11 October 2012
' Sunrise: 07:06:00
' Sunset: 18:19:00
存留日期和时间
永远不要以可以因文化变化而不同的格式存储日期和时间数据。 这是一个常见的编程错误,导致数据损坏或运行时异常。 以下示例使用英语(美国)区域性的格式约定将两个日期(2013 年 1 月 9 日和 2013 年 8 月 18 日)序列化为字符串。 使用美国英语文化习惯检索并分析数据时,数据能够被成功恢复。 但是,当使用英语(英国)文化的约定检索和分析时,第一个日期被错误地解释为9月1日,第二个日期无法解析,因为公历没有第十八个月。
using System;
using System.IO;
using System.Globalization;
using System.Threading;
public class Example4
{
public static void Main4()
{
// Persist two dates as strings.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
DateTime[] dates = { new DateTime(2013, 1, 9),
new DateTime(2013, 8, 18) };
StreamWriter sw = new StreamWriter("dateData.dat");
sw.Write("{0:d}|{1:d}", dates[0], dates[1]);
sw.Close();
// Read the persisted data.
StreamReader sr = new StreamReader("dateData.dat");
string dateData = sr.ReadToEnd();
sr.Close();
string[] dateStrings = dateData.Split('|');
// Restore and display the data using the conventions of the en-US culture.
Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
foreach (var dateStr in dateStrings)
{
DateTime restoredDate;
if (DateTime.TryParse(dateStr, out restoredDate))
Console.WriteLine($"The date is {restoredDate:D}");
else
Console.WriteLine($"ERROR: Unable to parse {dateStr}");
}
Console.WriteLine();
// Restore and display the data using the conventions of the en-GB culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
foreach (var dateStr in dateStrings)
{
DateTime restoredDate;
if (DateTime.TryParse(dateStr, out restoredDate))
Console.WriteLine($"The date is {restoredDate:D}");
else
Console.WriteLine($"ERROR: Unable to parse {dateStr}");
}
}
}
// The example displays the following output:
// Current Culture: English (United States)
// The date is Wednesday, January 09, 2013
// The date is Sunday, August 18, 2013
//
// Current Culture: English (United Kingdom)
// The date is 01 September 2013
// ERROR: Unable to parse 8/18/2013
Imports System.Globalization
Imports System.IO
Imports System.Threading
Module Example4
Public Sub Main4()
' Persist two dates as strings.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
Dim dates() As DateTime = {New DateTime(2013, 1, 9),
New DateTime(2013, 8, 18)}
Dim sw As New StreamWriter("dateData.dat")
sw.Write("{0:d}|{1:d}", dates(0), dates(1))
sw.Close()
' Read the persisted data.
Dim sr As New StreamReader("dateData.dat")
Dim dateData As String = sr.ReadToEnd()
sr.Close()
Dim dateStrings() As String = dateData.Split("|"c)
' Restore and display the data using the conventions of the en-US culture.
Console.WriteLine("Current Culture: {0}",
Thread.CurrentThread.CurrentCulture.DisplayName)
For Each dateStr In dateStrings
Dim restoredDate As Date
If Date.TryParse(dateStr, restoredDate) Then
Console.WriteLine("The date is {0:D}", restoredDate)
Else
Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
End If
Next
Console.WriteLine()
' Restore and display the data using the conventions of the en-GB culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
Console.WriteLine("Current Culture: {0}",
Thread.CurrentThread.CurrentCulture.DisplayName)
For Each dateStr In dateStrings
Dim restoredDate As Date
If Date.TryParse(dateStr, restoredDate) Then
Console.WriteLine("The date is {0:D}", restoredDate)
Else
Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
End If
Next
End Sub
End Module
' The example displays the following output:
' Current Culture: English (United States)
' The date is Wednesday, January 09, 2013
' The date is Sunday, August 18, 2013
'
' Current Culture: English (United Kingdom)
' The date is 01 September 2013
' ERROR: Unable to parse 8/18/2013
可通过以下三种方式之一避免此问题:
- 以二进制格式序列化日期和时间,而不是字符串。
- 不考虑用户的区域性,使用同一自定义格式字符串保存和分析日期和时间的字符串表示形式。
- 使用固定区域性的格式约定保存字符串。
下面的示例演示了最后一种方法。 它使用静态 CultureInfo.InvariantCulture 属性返回的固定区域性的格式约定。
using System;
using System.IO;
using System.Globalization;
using System.Threading;
public class Example5
{
public static void Main5()
{
// Persist two dates as strings.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
DateTime[] dates = { new DateTime(2013, 1, 9),
new DateTime(2013, 8, 18) };
StreamWriter sw = new StreamWriter("dateData.dat");
sw.Write(String.Format(CultureInfo.InvariantCulture,
"{0:d}|{1:d}", dates[0], dates[1]));
sw.Close();
// Read the persisted data.
StreamReader sr = new StreamReader("dateData.dat");
string dateData = sr.ReadToEnd();
sr.Close();
string[] dateStrings = dateData.Split('|');
// Restore and display the data using the conventions of the en-US culture.
Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
foreach (var dateStr in dateStrings)
{
DateTime restoredDate;
if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
DateTimeStyles.None, out restoredDate))
Console.WriteLine($"The date is {restoredDate:D}");
else
Console.WriteLine($"ERROR: Unable to parse {dateStr}");
}
Console.WriteLine();
// Restore and display the data using the conventions of the en-GB culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
foreach (var dateStr in dateStrings)
{
DateTime restoredDate;
if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
DateTimeStyles.None, out restoredDate))
Console.WriteLine($"The date is {restoredDate:D}");
else
Console.WriteLine($"ERROR: Unable to parse {dateStr}");
}
}
}
// The example displays the following output:
// Current Culture: English (United States)
// The date is Wednesday, January 09, 2013
// The date is Sunday, August 18, 2013
//
// Current Culture: English (United Kingdom)
// The date is 09 January 2013
// The date is 18 August 2013
Imports System.Globalization
Imports System.IO
Imports System.Threading
Module Example5
Public Sub Main5()
' Persist two dates as strings.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
Dim dates() As DateTime = {New DateTime(2013, 1, 9),
New DateTime(2013, 8, 18)}
Dim sw As New StreamWriter("dateData.dat")
sw.Write(String.Format(CultureInfo.InvariantCulture,
"{0:d}|{1:d}", dates(0), dates(1)))
sw.Close()
' Read the persisted data.
Dim sr As New StreamReader("dateData.dat")
Dim dateData As String = sr.ReadToEnd()
sr.Close()
Dim dateStrings() As String = dateData.Split("|"c)
' Restore and display the data using the conventions of the en-US culture.
Console.WriteLine("Current Culture: {0}",
Thread.CurrentThread.CurrentCulture.DisplayName)
For Each dateStr In dateStrings
Dim restoredDate As Date
If Date.TryParse(dateStr, CultureInfo.InvariantCulture,
DateTimeStyles.None, restoredDate) Then
Console.WriteLine("The date is {0:D}", restoredDate)
Else
Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
End If
Next
Console.WriteLine()
' Restore and display the data using the conventions of the en-GB culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
Console.WriteLine("Current Culture: {0}",
Thread.CurrentThread.CurrentCulture.DisplayName)
For Each dateStr In dateStrings
Dim restoredDate As Date
If Date.TryParse(dateStr, CultureInfo.InvariantCulture,
DateTimeStyles.None, restoredDate) Then
Console.WriteLine("The date is {0:D}", restoredDate)
Else
Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
End If
Next
End Sub
End Module
' The example displays the following output:
' Current Culture: English (United States)
' The date is Wednesday, January 09, 2013
' The date is Sunday, August 18, 2013
'
' Current Culture: English (United Kingdom)
' The date is 09 January 2013
' The date is 18 August 2013
序列化和时区感知
日期和时间值可以有多个解释,从常规时间(“商店于 2013 年 1 月 2 日打开,上午 9:00”)到特定时间(“出生日期:2013 年 1 月 2 日 6:32:00 A.M.”)。 当时间值表示特定时间点并从序列化值中还原时,应确保它表示相同的时间,而不考虑用户的地理位置或时区。
下面的示例演示了此问题。 它以三 种标准格式将单个本地日期和时间值保存为字符串:
- “G”表示常规日期(长时间)。
- “s”用于可排序的日期/时间。
- “o”表示往返日期/时间。
using System;
using System.IO;
public class Example6
{
public static void Main6()
{
DateTime dateOriginal = new DateTime(2023, 3, 30, 18, 0, 0);
dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local);
// Serialize a date.
if (!File.Exists("DateInfo.dat"))
{
StreamWriter sw = new StreamWriter("DateInfo.dat");
sw.Write("{0:G}|{0:s}|{0:o}", dateOriginal);
sw.Close();
Console.WriteLine("Serialized dates to DateInfo.dat");
}
Console.WriteLine();
// Restore the date from string values.
StreamReader sr = new StreamReader("DateInfo.dat");
string datesToSplit = sr.ReadToEnd();
string[] dateStrings = datesToSplit.Split('|');
foreach (var dateStr in dateStrings)
{
DateTime newDate = DateTime.Parse(dateStr);
Console.WriteLine($"'{dateStr}' --> {newDate} {newDate.Kind}");
}
}
}
Imports System.IO
Module Example6
Public Sub Main6()
' Serialize a date.
Dim dateOriginal As Date = #03/30/2023 6:00PM#
dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local)
' Serialize the date in string form.
If Not File.Exists("DateInfo.dat") Then
Dim sw As New StreamWriter("DateInfo.dat")
sw.Write("{0:G}|{0:s}|{0:o}", dateOriginal)
sw.Close()
End If
' Restore the date from string values.
Dim sr As New StreamReader("DateInfo.dat")
Dim datesToSplit As String = sr.ReadToEnd()
Dim dateStrings() As String = datesToSplit.Split("|"c)
For Each dateStr In dateStrings
Dim newDate As DateTime = DateTime.Parse(dateStr)
Console.WriteLine("'{0}' --> {1} {2}",
dateStr, newDate, newDate.Kind)
Next
End Sub
End Module
当数据还原到与序列化系统位于同一时区的系统上时,反序列化的日期和时间值会准确反映原始值,如输出所示:
'3/30/2013 6:00:00 PM' --> 3/30/2013 6:00:00 PM Unspecified
'2013-03-30T18:00:00' --> 3/30/2013 6:00:00 PM Unspecified
'2013-03-30T18:00:00.0000000-07:00' --> 3/30/2013 6:00:00 PM Local
但是,如果在不同的时区中还原系统上的数据,则只有使用“o”(往返)标准格式字符串格式化的日期和时间值会保留时区信息,因此表示相同的即时时间。 以下是在罗马尼亚标准时间时区的系统上恢复日期和时间数据的输出:
'3/30/2023 6:00:00 PM' --> 3/30/2023 6:00:00 PM Unspecified
'2023-03-30T18:00:00' --> 3/30/2023 6:00:00 PM Unspecified
'2023-03-30T18:00:00.0000000-07:00' --> 3/31/2023 3:00:00 AM Local
若要准确反映某一时刻的日期和时间值,而不考虑数据反序列化系统的时区,可以进行以下任一种处理:
- 使用“o”(往返)标准格式字符串将值另存为字符串。 然后在目标系统上进行反序列化。
- 将其转换为 UTC,并使用“r”(RFC1123)标准格式字符串将其另存为字符串。 然后在目标系统上反序列化它,并将其转换为本地时间。
- 将其转换为 UTC,并使用“u”(通用可排序)标准格式字符串将其另存为字符串。 然后在目标系统上反序列化它,并将其转换为本地时间。
下面的示例演示了每种技术。
using System;
using System.IO;
public class Example9
{
public static void Main9()
{
// Serialize a date.
DateTime dateOriginal = new DateTime(2023, 3, 30, 18, 0, 0);
dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local);
// Serialize the date in string form.
if (!File.Exists("DateInfo2.dat"))
{
StreamWriter sw = new StreamWriter("DateInfo2.dat");
sw.Write("{0:o}|{1:r}|{1:u}", dateOriginal,
dateOriginal.ToUniversalTime());
sw.Close();
}
// Restore the date from string values.
StreamReader sr = new StreamReader("DateInfo2.dat");
string datesToSplit = sr.ReadToEnd();
string[] dateStrings = datesToSplit.Split('|');
for (int ctr = 0; ctr < dateStrings.Length; ctr++)
{
DateTime newDate = DateTime.Parse(dateStrings[ctr]);
if (ctr == 1)
{
Console.WriteLine($"'{dateStrings[ctr]}' --> {newDate} {newDate.Kind}");
}
else
{
DateTime newLocalDate = newDate.ToLocalTime();
Console.WriteLine($"'{dateStrings[ctr]}' --> {newLocalDate} {newLocalDate.Kind}");
}
}
}
}
Imports System.IO
Module Example9
Public Sub Main9()
' Serialize a date.
Dim dateOriginal As Date = #03/30/2023 6:00PM#
dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local)
' Serialize the date in string form.
If Not File.Exists("DateInfo2.dat") Then
Dim sw As New StreamWriter("DateInfo2.dat")
sw.Write("{0:o}|{1:r}|{1:u}", dateOriginal,
dateOriginal.ToUniversalTime())
sw.Close()
End If
' Restore the date from string values.
Dim sr As New StreamReader("DateInfo2.dat")
Dim datesToSplit As String = sr.ReadToEnd()
Dim dateStrings() As String = datesToSplit.Split("|"c)
For ctr As Integer = 0 To dateStrings.Length - 1
Dim newDate As DateTime = DateTime.Parse(dateStrings(ctr))
If ctr = 1 Then
Console.WriteLine("'{0}' --> {1} {2}",
dateStrings(ctr), newDate, newDate.Kind)
Else
Dim newLocalDate As DateTime = newDate.ToLocalTime()
Console.WriteLine("'{0}' --> {1} {2}",
dateStrings(ctr), newLocalDate, newLocalDate.Kind)
End If
Next
End Sub
End Module
当数据序列化在太平洋标准时区的系统上,并在浪漫标准时区的系统上反序列化时,该示例将显示以下输出:
'2023-03-30T18:00:00.0000000-07:00' --> 3/31/2023 3:00:00 AM Local
'Sun, 31 Mar 2023 01:00:00 GMT' --> 3/31/2023 3:00:00 AM Local
'2023-03-31 01:00:00Z' --> 3/31/2023 3:00:00 AM Local
有关详细信息,请参阅 在时区之间转换时间。
执行日期和时间算法
和DateTimeDateTimeOffset类型都支持算术运算。 可以计算两个日期值之间的差异,或者在一个日期值上加或者减去特定的时间间隔。 但是,日期和时间值的算术运算不考虑时区和时区调整规则。 因此,表示时间时刻的值的日期和时间算术可能会返回不准确的结果。
例如,从太平洋标准时间过渡到太平洋夏令时,发生在 2013 年 3 月的第二个星期天,即 2013 年 3 月 10 日。 如以下示例所示,如果在太平洋标准时区的系统上计算 2013 年 3 月 9 日上午 10:30 之后 48 小时的日期和时间,则结果 2013 年 3 月 11 日上午 10:30 没有考虑到中间的时间调整。
using System;
public class Example7
{
public static void Main7()
{
DateTime date1 = DateTime.SpecifyKind(new DateTime(2013, 3, 9, 10, 30, 0),
DateTimeKind.Local);
TimeSpan interval = new TimeSpan(48, 0, 0);
DateTime date2 = date1 + interval;
Console.WriteLine($"{date1:g} + {interval.TotalHours:N1} hours = {date2:g}");
}
}
// The example displays the following output:
// 3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 10:30 AM
Module Example7
Public Sub Main7()
Dim date1 As Date = DateTime.SpecifyKind(#3/9/2013 10:30AM#,
DateTimeKind.Local)
Dim interval As New TimeSpan(48, 0, 0)
Dim date2 As Date = date1 + interval
Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
date1, interval.TotalHours, date2)
End Sub
End Module
' The example displays the following output:
' 3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 10:30 AM
若要确保日期和时间值的算术运算产生准确的结果,请执行以下步骤:
- 将源时区中的时间转换为 UTC。
- 执行算术运算。
- 如果结果为日期和时间值,请将其从 UTC 转换为源时区中的时间。
以下示例与上一示例类似,只是遵循这三个步骤将 48 小时正确添加到 2013 年 3 月 9 日上午 10:30。
using System;
public class Example8
{
public static void Main8()
{
TimeZoneInfo pst = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
DateTime date1 = DateTime.SpecifyKind(new DateTime(2013, 3, 9, 10, 30, 0),
DateTimeKind.Local);
DateTime utc1 = date1.ToUniversalTime();
TimeSpan interval = new TimeSpan(48, 0, 0);
DateTime utc2 = utc1 + interval;
DateTime date2 = TimeZoneInfo.ConvertTimeFromUtc(utc2, pst);
Console.WriteLine($"{date1:g} + {interval.TotalHours:N1} hours = {date2:g}");
}
}
// The example displays the following output:
// 3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 11:30 AM
Module Example8
Public Sub Main8()
Dim pst As TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")
Dim date1 As Date = DateTime.SpecifyKind(#3/9/2013 10:30AM#,
DateTimeKind.Local)
Dim utc1 As Date = date1.ToUniversalTime()
Dim interval As New TimeSpan(48, 0, 0)
Dim utc2 As Date = utc1 + interval
Dim date2 As Date = TimeZoneInfo.ConvertTimeFromUtc(utc2, pst)
Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
date1, interval.TotalHours, date2)
End Sub
End Module
' The example displays the following output:
' 3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 11:30 AM
有关详细信息,请参阅 使用日期和时间执行算术运算。
使用文化敏感的名称来表示日期元素
你的应用可能需要显示月份或星期几的名称。 为此,类似以下的代码是常见的。
using System;
public class Example12
{
public static void Main12()
{
DateTime midYear = new DateTime(2013, 7, 1);
Console.WriteLine($"{midYear:d} is a {GetDayName(midYear)}.");
}
private static string GetDayName(DateTime date)
{
return date.DayOfWeek.ToString("G");
}
}
// The example displays the following output:
// 7/1/2013 is a Monday.
Module Example12
Public Sub Main12()
Dim midYear As Date = #07/01/2013#
Console.WriteLine("{0:d} is a {1}.", midYear, GetDayName(midYear))
End Sub
Private Function GetDayName(dat As Date) As String
Return dat.DayOfWeek.ToString("G")
End Function
End Module
' The example displays the following output:
' 7/1/2013 is a Monday.
但是,此代码始终以英语返回星期几的名称。 提取月份名称的代码通常更不灵活。 它经常假定一个具有特定语言月份名称的十二个月日历。
通过使用 自定义日期和时间格式字符串 或对象的属性 DateTimeFormatInfo ,可以轻松提取反映用户区域性中星期或月份的名称的字符串,如以下示例所示。 它将当前区域性更改为法语(法国),并为 2013 年 7 月 1 日显示一周中某天的名称和月份的名称。
using System;
using System.Globalization;
public class Example13
{
public static void Main13()
{
// Set the current culture to French (France).
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");
DateTime midYear = new DateTime(2013, 7, 1);
Console.WriteLine($"{midYear:d} is a {DateUtilities.GetDayName(midYear)}.");
Console.WriteLine($"{midYear:d} is a {DateUtilities.GetDayName((int)midYear.DayOfWeek)}.");
Console.WriteLine($"{midYear:d} is in {DateUtilities.GetMonthName(midYear)}.");
Console.WriteLine($"{midYear:d} is in {DateUtilities.GetMonthName(midYear.Month)}.");
}
}
public static class DateUtilities
{
public static string GetDayName(int dayOfWeek)
{
if (dayOfWeek < 0 | dayOfWeek > DateTimeFormatInfo.CurrentInfo.DayNames.Length)
return String.Empty;
else
return DateTimeFormatInfo.CurrentInfo.DayNames[dayOfWeek];
}
public static string GetDayName(DateTime date)
{
return date.ToString("dddd");
}
public static string GetMonthName(int month)
{
if (month < 1 | month > DateTimeFormatInfo.CurrentInfo.MonthNames.Length - 1)
return String.Empty;
else
return DateTimeFormatInfo.CurrentInfo.MonthNames[month - 1];
}
public static string GetMonthName(DateTime date)
{
return date.ToString("MMMM");
}
}
// The example displays the following output:
// 01/07/2013 is a lundi.
// 01/07/2013 is a lundi.
// 01/07/2013 is in juillet.
// 01/07/2013 is in juillet.
Imports System.Globalization
Imports System.Threading
Module Example13
Public Sub Main13()
' Set the current culture to French (France).
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")
Dim midYear As Date = #07/01/2013#
Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear))
Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear.DayOfWeek))
Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear))
Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear.Month))
End Sub
End Module
Public Class DateUtilities
Public Shared Function GetDayName(dayOfWeek As Integer) As String
If dayOfWeek < 0 Or dayOfWeek > DateTimeFormatInfo.CurrentInfo.DayNames.Length Then
Return String.Empty
Else
Return DateTimeFormatInfo.CurrentInfo.DayNames(dayOfWeek)
End If
End Function
Public Shared Function GetDayName(dat As Date) As String
Return dat.ToString("dddd")
End Function
Public Shared Function GetMonthName(month As Integer) As String
If month < 1 Or month > DateTimeFormatInfo.CurrentInfo.MonthNames.Length - 1 Then
Return String.Empty
Else
Return DateTimeFormatInfo.CurrentInfo.MonthNames(month - 1)
End If
End Function
Public Shared Function GetMonthName(dat As Date) As String
Return dat.ToString("MMMM")
End Function
End Class
' The example displays the following output:
' 01/07/2013 is a lundi.
' 01/07/2013 is a lundi.
' 01/07/2013 is in juillet.
' 01/07/2013 is in juillet.
数值
数字的处理取决于它们是显示在用户界面中还是持久化。 本部分将检查这两种用法。
注释
在分析和格式设置作中,.NET 仅将基本拉丁字符 0 到 9(U+0030 到 U+0039)识别为数字数字。
显示数值
通常,当数字显示在用户界面中时,应使用用户文化的格式约定,这一约定是由 CultureInfo.CurrentCulture 属性以及由 NumberFormatInfo 属性返回的 CultureInfo.CurrentCulture.NumberFormat
对象定义的。 使用以下方式设置日期格式时,将自动应用当前文化的格式约定:
- 使用任何数值类型的无参数
ToString
方法。 - 使用任何数值类型的方法
ToString(String)
,该方法以格式字符串作为参数。 - 使用数值进行复合格式化。
以下示例显示法国巴黎每月的平均温度。 它首先将当前文化设置为法语(法国),然后显示数据,然后将其设置为英语(美国)。 在每种情况下,月份名称和温度都以适合该文化的格式显示。 请注意,这两种文化在温度值中使用不同的小数分隔符。 另请注意,该示例使用“MMMM”自定义日期和时间格式字符串来显示全月名称,并通过确定数组中最长月份名称的长度,为结果字符串中的 DateTimeFormatInfo.MonthNames 月份名称分配适当的空间量。
using System;
using System.Globalization;
using System.Threading;
public class Example14
{
public static void Main14()
{
DateTime dateForMonth = new DateTime(2013, 1, 1);
double[] temperatures = { 3.4, 3.5, 7.6, 10.4, 14.5, 17.2,
19.9, 18.2, 15.9, 11.3, 6.9, 5.3 };
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");
Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
// Build the format string dynamically so we allocate enough space for the month name.
string fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM} {1,4}";
for (int ctr = 0; ctr < temperatures.Length; ctr++)
Console.WriteLine(fmtString,
dateForMonth.AddMonths(ctr),
temperatures[ctr]);
Console.WriteLine();
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM} {1,4}";
for (int ctr = 0; ctr < temperatures.Length; ctr++)
Console.WriteLine(fmtString,
dateForMonth.AddMonths(ctr),
temperatures[ctr]);
}
private static int GetLongestMonthNameLength()
{
int length = 0;
foreach (var nameOfMonth in DateTimeFormatInfo.CurrentInfo.MonthNames)
if (nameOfMonth.Length > length) length = nameOfMonth.Length;
return length;
}
}
// The example displays the following output:
// Current Culture: French (France)
// janvier 3,4
// février 3,5
// mars 7,6
// avril 10,4
// mai 14,5
// juin 17,2
// juillet 19,9
// août 18,2
// septembre 15,9
// octobre 11,3
// novembre 6,9
// décembre 5,3
//
// Current Culture: English (United States)
// January 3.4
// February 3.5
// March 7.6
// April 10.4
// May 14.5
// June 17.2
// July 19.9
// August 18.2
// September 15.9
// October 11.3
// November 6.9
// December 5.3
Imports System.Globalization
Imports System.Threading
Module Example14
Public Sub Main14()
Dim dateForMonth As Date = #1/1/2013#
Dim temperatures() As Double = {3.4, 3.5, 7.6, 10.4, 14.5, 17.2,
19.9, 18.2, 15.9, 11.3, 6.9, 5.3}
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")
Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
Dim fmtString As String = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM} {1,4}"
For ctr = 0 To temperatures.Length - 1
Console.WriteLine(fmtString,
dateForMonth.AddMonths(ctr),
temperatures(ctr))
Next
Console.WriteLine()
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
' Build the format string dynamically so we allocate enough space for the month name.
fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM} {1,4}"
For ctr = 0 To temperatures.Length - 1
Console.WriteLine(fmtString,
dateForMonth.AddMonths(ctr),
temperatures(ctr))
Next
End Sub
Private Function GetLongestMonthNameLength() As Integer
Dim length As Integer
For Each nameOfMonth In DateTimeFormatInfo.CurrentInfo.MonthNames
If nameOfMonth.Length > length Then length = nameOfMonth.Length
Next
Return length
End Function
End Module
' The example displays the following output:
' Current Culture: French (France)
' janvier 3,4
' février 3,5
' mars 7,6
' avril 10,4
' mai 14,5
' juin 17,2
' juillet 19,9
' août 18,2
' septembre 15,9
' octobre 11,3
' novembre 6,9
' décembre 5,3
'
' Current Culture: English (United States)
' January 3.4
' February 3.5
' March 7.6
' April 10.4
' May 14.5
' June 17.2
' July 19.9
' August 18.2
' September 15.9
' October 11.3
' November 6.9
' December 5.3
存留数字值
不应以特定于文化的格式存储数值数据。 这是一个常见的编程错误,导致数据损坏或运行时异常。 下面的示例生成 10 个随机浮点数,然后使用英语(美国)区域性的格式约定将它们序列化为字符串。 使用英语(美国)文化的约定来检索和解析数据时,数据能够成功还原。 但是,当使用法语(法国)区域性的约定进行检索和分析时,无法分析任何数字,因为区域性使用了不同的小数分隔符。
using System;
using System.Globalization;
using System.IO;
using System.Threading;
public class Example15
{
public static void Main15()
{
// Create ten random doubles.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
double[] numbers = GetRandomNumbers(10);
DisplayRandomNumbers(numbers);
// Persist the numbers as strings.
StreamWriter sw = new StreamWriter("randoms.dat");
for (int ctr = 0; ctr < numbers.Length; ctr++)
sw.Write("{0:R}{1}", numbers[ctr], ctr < numbers.Length - 1 ? "|" : "");
sw.Close();
// Read the persisted data.
StreamReader sr = new StreamReader("randoms.dat");
string numericData = sr.ReadToEnd();
sr.Close();
string[] numberStrings = numericData.Split('|');
// Restore and display the data using the conventions of the en-US culture.
Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
foreach (var numberStr in numberStrings)
{
double restoredNumber;
if (Double.TryParse(numberStr, out restoredNumber))
Console.WriteLine(restoredNumber.ToString("R"));
else
Console.WriteLine($"ERROR: Unable to parse '{numberStr}'");
}
Console.WriteLine();
// Restore and display the data using the conventions of the fr-FR culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");
Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
foreach (var numberStr in numberStrings)
{
double restoredNumber;
if (Double.TryParse(numberStr, out restoredNumber))
Console.WriteLine(restoredNumber.ToString("R"));
else
Console.WriteLine($"ERROR: Unable to parse '{numberStr}'");
}
}
private static double[] GetRandomNumbers(int n)
{
Random rnd = new Random();
double[] numbers = new double[n];
for (int ctr = 0; ctr < n; ctr++)
numbers[ctr] = rnd.NextDouble() * 1000;
return numbers;
}
private static void DisplayRandomNumbers(double[] numbers)
{
for (int ctr = 0; ctr < numbers.Length; ctr++)
Console.WriteLine(numbers[ctr].ToString("R"));
Console.WriteLine();
}
}
// The example displays output like the following:
// 487.0313743534644
// 674.12000879371533
// 498.72077885024288
// 42.3034229512808
// 970.57311049223563
// 531.33717716268131
// 587.82905693530529
// 562.25210175023039
// 600.7711019370571
// 299.46113717717174
//
// Current Culture: English (United States)
// 487.0313743534644
// 674.12000879371533
// 498.72077885024288
// 42.3034229512808
// 970.57311049223563
// 531.33717716268131
// 587.82905693530529
// 562.25210175023039
// 600.7711019370571
// 299.46113717717174
//
// Current Culture: French (France)
// ERROR: Unable to parse '487.0313743534644'
// ERROR: Unable to parse '674.12000879371533'
// ERROR: Unable to parse '498.72077885024288'
// ERROR: Unable to parse '42.3034229512808'
// ERROR: Unable to parse '970.57311049223563'
// ERROR: Unable to parse '531.33717716268131'
// ERROR: Unable to parse '587.82905693530529'
// ERROR: Unable to parse '562.25210175023039'
// ERROR: Unable to parse '600.7711019370571'
// ERROR: Unable to parse '299.46113717717174'
Imports System.Globalization
Imports System.IO
Imports System.Threading
Module Example15
Public Sub Main15()
' Create ten random doubles.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
Dim numbers() As Double = GetRandomNumbers(10)
DisplayRandomNumbers(numbers)
' Persist the numbers as strings.
Dim sw As New StreamWriter("randoms.dat")
For ctr As Integer = 0 To numbers.Length - 1
sw.Write("{0:R}{1}", numbers(ctr), If(ctr < numbers.Length - 1, "|", ""))
Next
sw.Close()
' Read the persisted data.
Dim sr As New StreamReader("randoms.dat")
Dim numericData As String = sr.ReadToEnd()
sr.Close()
Dim numberStrings() As String = numericData.Split("|"c)
' Restore and display the data using the conventions of the en-US culture.
Console.WriteLine("Current Culture: {0}",
Thread.CurrentThread.CurrentCulture.DisplayName)
For Each numberStr In numberStrings
Dim restoredNumber As Double
If Double.TryParse(numberStr, restoredNumber) Then
Console.WriteLine(restoredNumber.ToString("R"))
Else
Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr)
End If
Next
Console.WriteLine()
' Restore and display the data using the conventions of the fr-FR culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")
Console.WriteLine("Current Culture: {0}",
Thread.CurrentThread.CurrentCulture.DisplayName)
For Each numberStr In numberStrings
Dim restoredNumber As Double
If Double.TryParse(numberStr, restoredNumber) Then
Console.WriteLine(restoredNumber.ToString("R"))
Else
Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr)
End If
Next
End Sub
Private Function GetRandomNumbers(n As Integer) As Double()
Dim rnd As New Random()
Dim numbers(n - 1) As Double
For ctr As Integer = 0 To n - 1
numbers(ctr) = rnd.NextDouble * 1000
Next
Return numbers
End Function
Private Sub DisplayRandomNumbers(numbers As Double())
For ctr As Integer = 0 To numbers.Length - 1
Console.WriteLine(numbers(ctr).ToString("R"))
Next
Console.WriteLine()
End Sub
End Module
' The example displays output like the following:
' 487.0313743534644
' 674.12000879371533
' 498.72077885024288
' 42.3034229512808
' 970.57311049223563
' 531.33717716268131
' 587.82905693530529
' 562.25210175023039
' 600.7711019370571
' 299.46113717717174
'
' Current Culture: English (United States)
' 487.0313743534644
' 674.12000879371533
' 498.72077885024288
' 42.3034229512808
' 970.57311049223563
' 531.33717716268131
' 587.82905693530529
' 562.25210175023039
' 600.7711019370571
' 299.46113717717174
'
' Current Culture: French (France)
' ERROR: Unable to parse '487.0313743534644'
' ERROR: Unable to parse '674.12000879371533'
' ERROR: Unable to parse '498.72077885024288'
' ERROR: Unable to parse '42.3034229512808'
' ERROR: Unable to parse '970.57311049223563'
' ERROR: Unable to parse '531.33717716268131'
' ERROR: Unable to parse '587.82905693530529'
' ERROR: Unable to parse '562.25210175023039'
' ERROR: Unable to parse '600.7711019370571'
' ERROR: Unable to parse '299.46113717717174'
若要避免此问题,可以使用以下方法之一:
- 不考虑用户的区域性,使用同一自定义格式字符串保存和分析数字的字符串表示形式。
- 通过使用不变文化的格式约定(由CultureInfo.InvariantCulture属性返回),将数字保存为字符串。
序列化货币值是一种特殊情况。 由于货币值取决于其表示的货币单位,因此将其视为独立的数值没有什么意义。 但是,如果将货币值保存为包含货币符号的格式化字符串,则无法在默认区域性使用不同的货币符号的系统上反序列化货币值,如以下示例所示。
using System;
using System.Globalization;
using System.IO;
using System.Threading;
public class Example1
{
public static void Main1()
{
// Display the currency value.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
Decimal value = 16039.47m;
Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
Console.WriteLine($"Currency Value: {value:C2}");
// Persist the currency value as a string.
StreamWriter sw = new StreamWriter("currency.dat");
sw.Write(value.ToString("C2"));
sw.Close();
// Read the persisted data using the current culture.
StreamReader sr = new StreamReader("currency.dat");
string currencyData = sr.ReadToEnd();
sr.Close();
// Restore and display the data using the conventions of the current culture.
Decimal restoredValue;
if (Decimal.TryParse(currencyData, out restoredValue))
Console.WriteLine(restoredValue.ToString("C2"));
else
Console.WriteLine($"ERROR: Unable to parse '{currencyData}'");
Console.WriteLine();
// Restore and display the data using the conventions of the en-GB culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
if (Decimal.TryParse(currencyData, NumberStyles.Currency, null, out restoredValue))
Console.WriteLine(restoredValue.ToString("C2"));
else
Console.WriteLine($"ERROR: Unable to parse '{currencyData}'");
Console.WriteLine();
}
}
// The example displays output like the following:
// Current Culture: English (United States)
// Currency Value: $16,039.47
// ERROR: Unable to parse '$16,039.47'
//
// Current Culture: English (United Kingdom)
// ERROR: Unable to parse '$16,039.47'
Imports System.Globalization
Imports System.IO
Imports System.Threading
Module Example1
Public Sub Main1()
' Display the currency value.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
Dim value As Decimal = 16039.47D
Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
Console.WriteLine("Currency Value: {0:C2}", value)
' Persist the currency value as a string.
Dim sw As New StreamWriter("currency.dat")
sw.Write(value.ToString("C2"))
sw.Close()
' Read the persisted data using the current culture.
Dim sr As New StreamReader("currency.dat")
Dim currencyData As String = sr.ReadToEnd()
sr.Close()
' Restore and display the data using the conventions of the current culture.
Dim restoredValue As Decimal
If Decimal.TryParse(currencyData, restoredValue) Then
Console.WriteLine(restoredValue.ToString("C2"))
Else
Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData)
End If
Console.WriteLine()
' Restore and display the data using the conventions of the en-GB culture.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
Console.WriteLine("Current Culture: {0}",
Thread.CurrentThread.CurrentCulture.DisplayName)
If Decimal.TryParse(currencyData, NumberStyles.Currency, Nothing, restoredValue) Then
Console.WriteLine(restoredValue.ToString("C2"))
Else
Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData)
End If
Console.WriteLine()
End Sub
End Module
' The example displays output like the following:
' Current Culture: English (United States)
' Currency Value: $16,039.47
' ERROR: Unable to parse '$16,039.47'
'
' Current Culture: English (United Kingdom)
' ERROR: Unable to parse '$16,039.47'
相反,你应该将数值与一些文化信息(例如文化名称)一起序列化,以便该值及其货币符号能够独立于当前文化进行反序列化。 以下示例通过定义一个包含两个成员的 CurrencyValue
结构: Decimal 值和该值所属文化的名称。
using System;
using System.Globalization;
using System.Text.Json;
using System.Threading;
public class Example2
{
public static void Main2()
{
// Display the currency value.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
Decimal value = 16039.47m;
Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
Console.WriteLine($"Currency Value: {value:C2}");
// Serialize the currency data.
CurrencyValue data = new()
{
Amount = value,
CultureName = CultureInfo.CurrentCulture.Name
};
string serialized = JsonSerializer.Serialize(data);
Console.WriteLine();
// Change the current culture.
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
// Deserialize the data.
CurrencyValue restoredData = JsonSerializer.Deserialize<CurrencyValue>(serialized);
// Display the round-tripped value.
CultureInfo culture = CultureInfo.CreateSpecificCulture(restoredData.CultureName);
Console.WriteLine($"Currency Value: {restoredData.Amount.ToString("C2", culture)}");
}
}
internal struct CurrencyValue
{
public decimal Amount { get; set; }
public string CultureName { get; set; }
}
// The example displays the following output:
// Current Culture: English (United States)
// Currency Value: $16,039.47
//
// Current Culture: English (United Kingdom)
// Currency Value: $16,039.47
Imports System.Globalization
Imports System.Text.Json
Imports System.Threading
Friend Structure CurrencyValue
Public Property Amount As Decimal
Public Property CultureName As String
End Structure
Module Example2
Public Sub Main2()
' Display the currency value.
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
Dim value As Decimal = 16039.47D
Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
Console.WriteLine("Currency Value: {0:C2}", value)
' Serialize the currency data.
Dim data As New CurrencyValue With {
.Amount = value,
.CultureName = CultureInfo.CurrentCulture.Name
}
Dim serialized As String = JsonSerializer.Serialize(data)
Console.WriteLine()
' Change the current culture.
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
' Deserialize the data.
Dim restoredData As CurrencyValue = JsonSerializer.Deserialize(Of CurrencyValue)(serialized)
' Display the round-tripped value.
Dim culture As CultureInfo = CultureInfo.CreateSpecificCulture(restoredData.CultureName)
Console.WriteLine("Currency Value: {0}", restoredData.Amount.ToString("C2", culture))
End Sub
End Module
' The example displays the following output:
' Current Culture: English (United States)
' Currency Value: $16,039.47
'
' Current Culture: English (United Kingdom)
' Currency Value: $16,039.47
使用特定于区域性的设置
在 .NET 中,类 CultureInfo 表示一个特定的文化或区域。 它的一些属性返回提供有关文化某些方面的特定信息的对象:
CultureInfo.CompareInfo 属性返回 CompareInfo 对象,该对象包含有关如何比较区域性和排列字符串的信息。
该 CultureInfo.DateTimeFormat 属性返回一个 DateTimeFormatInfo 对象,该对象提供用于设置日期和时间数据格式的区域性特定信息。
该 CultureInfo.NumberFormat 属性返回一个 NumberFormatInfo 对象,该对象提供用于设置数值数据格式的区域性特定信息。
该 CultureInfo.TextInfo 属性返回一个 TextInfo 对象,该对象提供有关文化的书写系统的信息。
一般情况下,不要对特定 CultureInfo 属性的值及其相关对象进行任何假设。 相反,您应该将文化特定数据视为可能会更改,原因如下:
个别财产价值可能会随着时间的推移而发生变化和修订,原因是数据被更正、更好的数据变得可用,或文化特定的惯例发生变化。
各个属性值可能因 .NET 版本或作系统版本而异。
.NET 支持替代文化。 这样就可以定义一个新的自定义区域性,该区域性是对现有标准区域性进行补充或完全替换现有标准区域性。
在 Windows 系统上,用户可以在控制面板中使用 区域和语言 应用自定义区域性特定的设置。 实例化 CultureInfo 对象时,可以通过调用 CultureInfo(String, Boolean) 构造函数来确定它是否反映这些用户自定义项。 通常,对于最终用户应用,应尊重用户首选项,以便用户以预期格式显示数据。