程時所需的許多核心功能不是由 C# 語言提供的,而是由 .NET BCL 中的類型提供的。在本章中,我們將介紹有助于完成基本編程任務的類型,例如虛擬相等比較、順序比較和類型轉換。我們還介紹了基本的 .NET 類型,例如字符串、日期時間和枚舉。
本節中的類型駐留在 System 命名空間中,但以下:
C# 字符表示單個 Unicode 字符,并為 System.Char 結構設置別名。在第 中,我們描述了如何表達字符文字:
char c = 'A';
char newLine = '\n';
System.Char 定義了一系列用于處理字符的靜態方法,ToLower 和 IsWhiteSpace 。您可以通過 類型或其 char 別名調用這些:
Console.WriteLine (System.Char.ToUpper ('c')); // C
Console.WriteLine (char.IsWhiteSpace ('\t')); // True
ToUpper 和 ToLower 遵循最終用戶的區域設置,這可能會導致細微的錯誤。以下表達式在土耳其的計算結果為 false:
char.ToUpper ('i') == 'I'
原因是因為在土耳其,炭。ToUpper ('i') 是 '?'(注意上面的點!)。為了避免這個問題,System.Char(和System.String)還提供了ToUpper和ToLower的區域性不變版本,以單詞結尾。這些始終適用英語區域性規則:
Console.WriteLine (char.ToUpperInvariant ('i')); // I
這是以下各項的快捷方式:
Console.WriteLine (char.ToUpper ('i', CultureInfo.InvariantCulture))
有關區域設置和區域性的更多信息,請參閱
char 剩下的大多數靜態方法都與字符分類有關。 列出了這些內容。
用于對字符進行分類的靜態方法 | ||
靜態方法 | 包含的字符 | 包括 Unicode 類別 |
是信 | A–Z、a–z 和其他字母的字母 | 大寫字母 小寫字母標題大小寫字母修飾符字母其他字母 |
IsUpper | 大寫字母 | 大寫字母 |
伊斯洛爾 | 小寫字母 | 小寫字母 |
IsDigit | 0–9 加其他字母的數字 | 十進制數字數 |
IsLetterOrDigit | 字母加數字 | ( IsLetter , IsDigit ) |
編號 | 所有數字加上 Unicode 分數和羅馬數字符號 | 十進制數字數字字母數字其他數字 |
分隔符 | 空格加上所有 Unicode 分隔符 | 行分隔符段落分隔符 |
是空白空間 | 所有分隔符加上 \n 、\r 、\t 、\f 和 \v | 行分隔符段落分隔符 |
標點符號 | 西方和其他字母表中用于標點符號的符號 | 破折號標點符號連接器標點符號首字母報價標點符號最終報價標點符號 |
是符號 | 大多數其他可打印符號 | 數學符號修飾符符號其他符號 |
是控制 | 0x20下方不可打印的“控制”字符,如 \r 、\n 、\t 、>0x20下方不可打印的“控制”字符,如 \r 、\n 、\t 、\0 以及介于 0x7F 和 0x9A 之間的字符< 以及介于 0x7F 和 0x9A 之間的字符 | (無) |
對于更精細的分類,char 提供了一個名為 GetUnicodeCategory 的靜態方法;這將返回一個 UnicodeCategory 枚舉,其成員顯示在 的最右側列中。
通過從整數顯式轉換,可以在分配的 Unicode 集之外生成字符。要測試字符的有效性,請調用 char。GetUnicodeCategory:如果結果是 UnicodeCategory.OtherNotAssigned ,則該字符無效。
字符的寬度為 16 位,足以表示中的任何 Unicode 字符。除此之外,您必須使用代理項對:我們在中描述了執行此操作的方法。
C# 字符串 ( == System.String ) 是不可變(不可更改)的字符序列。在第 中,我們描述了如何表達字符串文字、執行相等比較以及連接兩個字符串。本節介紹用于處理字符串的其余函數,這些函數通過 System.String 類的靜態和實例成員公開。
構造字符串的最簡單方法是分配文本,如所示:
string s1 = "Hello";
string s2 = "First Line\r\nSecond Line";
string s3 = @"\\server\fileshare\helloworld.cs";
要創建重復的字符序列,可以使用字符串的構造函數:
Console.Write (new string ('*', 10)); // **********
您還可以從 char 數組構造字符串。ToCharArray 方法執行相反的操作:
char[] ca = "Hello".ToCharArray();
string s = new string (ca); // s = "Hello"
字符串的構造函數也被重載以接受各種(不安全的)指針類型,以便從 char* 等類型創建字符串。
空字符串的長度為零。若要創建空字符串,可以使用文本字符串或靜態字符串。空字段;若要測試空字符串,可以執行相等比較或測試其 Length 屬性:
string empty = "";
Console.WriteLine (empty == ""); // True
Console.WriteLine (empty == string.Empty); // True
Console.WriteLine (empty.Length == 0); // True
由于字符串是引用類型,因此它們也可以為 null :
string nullString = null;
Console.WriteLine (nullString == null); // True
Console.WriteLine (nullString == ""); // False
Console.WriteLine (nullString.Length == 0); // NullReferenceException
靜態字符串。方法是一個有用的快捷方式,用于測試給定字符串是空還是空。
字符串的索引器在給定索引處返回單個字符。與所有對字符串進行操作的函數一樣,這是零索引:
string str = "abcde";
char letter = str[1]; // letter == 'b'
字符串還實現了 IEnumerable<char> ,因此您可以對其進行 foreach :
foreach (char c in "123") Console.Write (c + ","); // 1,2,3,
在字符串中搜索的最簡單方法是 開始與 、結束與 和包含 。這些都返回真或假:
Console.WriteLine ("quick brown fox".EndsWith ("fox")); // True
Console.WriteLine ("quick brown fox".Contains ("brown")); // True
StartsWith 和 EndsWith 已重載,以便指定 StringComparison 枚舉或 CultureInfo 對象來控制區分大小寫和區域性(請參閱)。默認設置是使用適用于當前(本地化)區域性的規則執行區分大小寫的匹配。以下操作使用區域性的規則執行不區分大小寫的搜索:
"abcdef".StartsWith ("aBc", StringComparison.InvariantCultureIgnoreCase)
包含方法不提供此重載的便利性,盡管您可以使用 IndexOf 方法獲得相同的結果。
IndexOf 功能更強大:它返回給定字符或子字符串的第一個位置(如果未找到子字符串,則返回 ?1):
Console.WriteLine ("abcde".IndexOf ("cd")); // 2
IndexOf 也被重載以接受 startPosition(從中開始搜索的索引)以及 StringComparison 枚舉:
Console.WriteLine ("abcde abcde".IndexOf ("CD", 6,
StringComparison.CurrentCultureIgnoreCase)); // 8
LastIndexOf 類似于 IndexOf ,但它通過字符串向后工作。
IndexOfAny 返回一組字符中任何一個字符的第一個匹配位置:
Console.Write ("ab,cd ef".IndexOfAny (new char[] {' ', ','} )); // 2
Console.Write ("pas5w0rd".IndexOfAny ("0123456789".ToCharArray() )); // 3
LastIndexOfAny 在相反的方向上做同樣的事情。
因為 String 是不可變的,所以所有“操作”字符串的方法都會返回一個新字符串,而原始方法保持不變(重新分配字符串變量時也是如此)。
子字符串提取字符串的一部分:
string left3 = "12345".Substring (0, 3); // left3 = "123";
string mid3 = "12345".Substring (1, 3); // mid3 = "234";
如果省略長度,則會得到字符串的其余部分:
string end3 = "12345".Substring (2); // end3 = "345";
插入和刪除 在指定位置插入或刪除字符:
string s1 = "helloworld".Insert (5, ", "); // s1 = "hello, world"
string s2 = s1.Remove (5, 2); // s2 = "helloworld";
PadLeft 和 PadRight 使用指定的字符(如果未指定,則為空格)將字符串填充到給定長度:
Console.WriteLine ("12345".PadLeft (9, '*')); // ****12345
Console.WriteLine ("12345".PadLeft (9)); // 12345
如果輸入字符串的長度大于填充長度,則返回的原始字符串將保持不變。
修剪開始 和 修剪結束 從字符串的開頭或結尾刪除指定的字符;修剪兩者兼而有之。默認情況下,這些函數刪除空格字符(包括空格、制表符、換行符和這些字符的 Unicode 變體):
Console.WriteLine (" abc \t\r\n ".Trim().Length); // 3
替換替換特定字符或的所有(非重疊)匹配項:
Console.WriteLine ("to be done".Replace (" ", " | ") ); // to | be | done
Console.WriteLine ("to be done".Replace (" ", "") ); // tobedone
ToUpper 和 ToLower 返回輸入字符串的大寫和小寫版本。默認情況下,它們遵循用戶的當前語言設置;ToUpperInvariant 和 ToLowerInvariant 始終應用英文字母規則。
拆分將字符串分成幾部分:
string[] words = "The quick brown fox".Split();
foreach (string word in words)
Console.Write (word + "|"); // The|quick|brown|fox|
默認情況下,拆分使用空格字符作為分隔符;它也被重載以接受字符或字符串分隔符的參數數組。 Split 還可以選擇接受 StringSplitOptions 枚舉,該枚舉具有刪除空條目的選項:當單詞由一行中的多個分隔符分隔時,這很有用。
靜態連接方法執行與拆分相反的操作。它需要一個分隔符和字符串數組:
string[] words = "The quick brown fox".Split();
string together = string.Join (" ", words); // The quick brown fox
靜態 Concat 方法類似于 Join,但只接受參數字符串數組,并且不應用分隔符。Concat 完全等價于 + 運算符(編譯器實際上將 + 轉換為 Concat):
string sentence = string.Concat ("The", " quick", " brown", " fox");
string sameSentence = "The" + " quick" + " brown" + " fox";
靜態 Format 方法提供了一種生成嵌入變量的字符串的便捷方法。嵌入的變量(或值)可以是任何類型的;格式只是在它們上調用 ToString。
包含嵌入變量的主字符串稱為。調用字符串時。格式 ,您需要提供一個復合格式字符串,后跟每個嵌入變量:
string composite = "It's {0} degrees in {1} on this {2} morning";
string s = string.Format (composite, 35, "Perth", DateTime.Now.DayOfWeek);
// s == "It's 35 degrees in Perth on this Friday morning"
(那是攝氏!
我們可以使用內插字符串文字來實現相同的效果(請參閱)。只需在字符串前面加上 $ 符號,并將表達式放在大括號中:
string s = $"It's hot this {DateTime.Now.DayOfWeek} morning";
大括號中的每個數字稱為。該數字對應于參數位置,可以選擇后跟:
最小寬度對于對齊列很有用。如果值為負數,則數據左對齊;否則,它是右對齊的:
string composite = "Name={0,-20} Credit Limit={1,15:C}";
Console.WriteLine (string.Format (composite, "Mary", 500));
Console.WriteLine (string.Format (composite, "Elizabeth", 20000));
結果如下:
Name=Mary Credit Limit= $500.00
Name=Elizabeth Credit Limit= $20,000.00
這是不使用字符串的等效項。格式:
string s = "Name=" + "Mary".PadRight (20) +
" Credit Limit=" + 500.ToString ("C").PadLeft (15);
信用額度通過“C”格式字符串格式化為貨幣。我們在中詳細描述了格式字符串。
在比較兩個值時,.NET 區分比較和的概念。相等比較測試兩個實例在語義上是否相同;順序比較測試在按升序或降序排列兩個(如果有)實例時,哪個實例排在第一位。
相等比較不是順序比較的;這兩個系統有不同的目的。例如,在同一排序位置有兩個不相等的值是合法的。我們在中繼續這個話題。
對于字符串相等性比較,可以使用 == 運算符或字符串的 Equals 方法之一。后者更通用,因為它們允許您指定不區分大小寫等選項。
另一個區別是,如果變量被強制轉換為對象類型,== 不能可靠地處理字符串。我們在中解釋了為什么會這樣。
對于字符串順序比較,可以使用 CompareTo 實例方法或靜態 Compare 和 CompareOrdinal 方法。它們返回一個正數或負數,或者零,具體取決于第一個值是在第二個值之后、之前還是旁邊。
在詳細介紹每個細節之前,我們需要檢查 .NET 的底層字符串比較算法。
字符串比較有兩種基本算法:和。序號比較將字符簡單地解釋為數字(根據其數字 Unicode 值);區分區域性的比較根據特定字母表解釋字符。有兩種特殊的區域性:“當前區域性”,它基于從計算機的控制面板中選取的設置,以及“固定區域性”,它在每臺計算機上都是相同的(并且與美國區域性非常匹配)。
對于相等比較,序數和特定于區域性的算法都很有用。但是,對于排序,特定于區域性的比較幾乎總是更可取的:要按字母順序對字符串進行排序,您需要一個字母表。序號依賴于數字 Unicode 點值,這些值恰好將英語字符按字母順序排列,但即便如此,也不完全符合您的預期。例如,假設區分大小寫,請考慮字符串 “Atom” , “atom” 和 “Zamia” 。固定區域性將它們按以下順序排列:
"atom", "Atom", "Zamia"
序號按如下方式排列它們:
"Atom", "Zamia", "atom"
這是因為固定區域性封裝了一個字母表,該字母表將大寫字符與其小寫對應字符相鄰(aAbBcCdD...)。但是,序號算法首先放置所有大寫字符,然后放置所有小寫字符(A...Z,一個...這本質上是對 1960 年代發明的 ASCII 字符集的回歸。
盡管序號有限制,但字符串的 == 運算符始終執行的比較。字符串的實例版本也是如此。在沒有參數的情況下調用時等于;這定義了字符串類型的“默認”相等比較行為。
序數算法被選為字符串的 == 和 Equals 函數,因為它既高效又。字符串相等比較被認為是基本的,并且比順序比較更頻繁地執行。
相等的“嚴格”概念也與 == 運算符的一般用法一致。
以下方法允許區分區域性或不區分大小寫的比較:
public bool Equals(string value, StringComparison comparisonType);
public static bool Equals (string a, string b,
StringComparison comparisonType);
靜態版本的優點在于,如果一個或兩個字符串為 null,它仍然有效。 StringComparison 是一個枚舉,定義如下:
public enum StringComparison
{
CurrentCulture, // Case-sensitive
CurrentCultureIgnoreCase,
InvariantCulture, // Case-sensitive
InvariantCultureIgnoreCase,
Ordinal, // Case-sensitive
OrdinalIgnoreCase
}
例如:
Console.WriteLine (string.Equals ("foo", "FOO",
StringComparison.OrdinalIgnoreCase)); // True
Console.WriteLine ("?" == "ǖ"); // False
Console.WriteLine (string.Equals ("?", "ǖ",
StringComparison.CurrentCulture)); // ?
(第三個示例的結果由計算機的當前語言設置確定。
字符串的 CompareTo 實例方法執行、順序比較。與 == 運算符不同,CompareTo 不使用序號比較:對于排序,區分區域性的算法要有用得多。下面是該方法的定義:
public int CompareTo (string strB);
CompareTo 實例方法實現了通用 IComparable 接口,這是跨 .NET 庫使用的標準比較協議。這意味著字符串的 CompareTo 定義了字符串在諸如排序集合之類的應用程序中的默認排序行為。有關 IComparable 的更多信息,請參閱
對于其他類型的比較,可以調用靜態 Compare 和 CompareOrdinal 方法:
public static int Compare (string strA, string strB,
StringComparison comparisonType);
public static int Compare (string strA, string strB, bool ignoreCase,
CultureInfo culture);
public static int Compare (string strA, string strB, bool ignoreCase);
public static int CompareOrdinal (string strA, string strB);
最后兩個方法只是調用前兩個方法的快捷方式。
所有順序比較方法都返回正數、負數或零,具體取決于第一個值是在第二個值之后、之前還是旁邊:
Console.WriteLine ("Boston".CompareTo ("Austin")); // 1
Console.WriteLine ("Boston".CompareTo ("Boston")); // 0
Console.WriteLine ("Boston".CompareTo ("Chicago")); // -1
Console.WriteLine ("?".CompareTo ("ǖ")); // 0
Console.WriteLine ("foo".CompareTo ("FOO")); // -1
下面使用當前區域性執行不區分大小寫的比較:
Console.WriteLine (string.Compare ("foo", "FOO", true)); // 0
通過提供 CultureInfo 對象,您可以插入任何字母表:
// CultureInfo is defined in the System.Globalization namespace
CultureInfo german = CultureInfo.GetCultureInfo ("de-DE");
int i = string.Compare ("Müller", "Muller", false, german);
類(System.Text 命名空間)表示可變(可編輯)字符串。使用字符串生成器,您可以追加、插入、刪除和替換子字符串,而無需替換整個字符串生成器。
StringBuilder 的構造函數可以選擇接受初始字符串值以及其內部容量的起始大小(默認為 16 個字符)。如果超出此范圍,StringBuilder 會自動調整其內部結構的大小,以容納(以輕微的性能成本)達到其最大容量(默認值為 int。最大值)。
StringBuilder 的一個流行用途是通過重復調用 Append 來構建一個長字符串。此方法比重復連接普通字符串類型要高效得多:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 50; i++) sb.Append(i).Append(",");
要獲得最終結果,請調用 ToString():
Console.WriteLine (sb.ToString());
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,
27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,
追加行執行添加新行序列的追加(在 Windows 中為“\r\n”)。AppendFormat 接受復合格式字符串,就像 String.Format 一樣。
除了 Insert 、Remove 和 replace 方法(Replace 的工作方式與字符串的 Replace 類似)之外,StringBuilder 還定義了一個 Length 屬性和一個可寫索引器,用于獲取/設置單個字符。
要清除字符串生成器的內容,請實例化一個新字符串或將其長度設置為零。
將 StringBuilder 的長度設置為零不會縮小其容量。因此,如果 StringBuilder 之前包含 100 萬個字符,則在將其長度歸零后,它將繼續占用大約 2 兆字節的內存。如果要釋放內存,則必須創建一個新的 StringBuilder 并允許舊的 StringBuilder 退出范圍(并被垃圾回收)。
字符集是字符的分配,每個字符都有一個數字代碼或碼位。有兩種常用的字符集:Unicode和ASCII。Unicode具有大約一百萬個字符的地址空間,其中大約100,000個當前已分配。Unicode涵蓋了世界上大多數的語言以及一些歷史語言和特殊符號。ASCII集只是Unicode集的前128個字符,它涵蓋了您在美式鍵盤上看到的大部分內容。ASCII比Unicode早了30年,有時仍因其簡單和高效而被使用:每個字符由一個字節表示。
.NET 類型系統旨在使用 Unicode 字符集。但是,ASCII 是隱式支持的,因為它是 Unicode 的一個子集。
將字符從其數字代碼點映射到二進制表示形式。在 .NET 中,文本編碼主要在處理文本文件或流時發揮作用。將文本文件讀入字符串時,會將文件數據從二進制轉換為字符和字符串類型所需的內部 Unicode 表示形式。文本編碼可以限制可以表示的字符以及影響存儲效率。
.NET 中有兩種類型的文本編碼:
第一類包含遺留編碼,例如 IBM 的 EBCDIC 和 8 位字符集,這些字符集在 Unicode 之前很流行(由代碼頁標識),這些字符集在 Unicode 之前很流行。ASCII 編碼也屬于這一類:它對前 128 個字符進行編碼并刪除其他所有字符。此類別還包含GB128,這是自 18030 年以來在中國編寫或銷往中國的應用程序的強制性標準。
第二類是UTF-8,UTF-16和UTF-32(以及過時的UTF-7)。每一個都有不同的空間效率。UTF-8對于大多數類型的文本來說是最節省空間的:它使用1到4個字節來表示每個字符。前128個字符只需要一個字節,使其與ASCII兼容。UTF-8是文本文件和流(特別是在互聯網上)最流行的編碼,它是.NET中流輸入/輸出(I/O)的默認值(事實上,它幾乎是所有隱式使用編碼的默認值)。
UTF-16 使用一個或兩個 16 位字來表示每個字符。這就是 .NET 在內部用來表示字符和字符串的內容。某些程序還以 UTF-16 寫入文件。
UTF-32 最節省空間:它將每個代碼點直接映射到 32 位,因此每個字符消耗四個字節。出于這個原因,很少使用 UTF-32。但是,它確實使隨機訪問變得非常容易,因為每個字符占用的字節數相等。
System.Text 中的編碼類是封裝文本編碼的類的通用基類型。有幾個子類 — 它們的目的是封裝具有相似特征的編碼族。最常見的編碼可以通過編碼上的專用靜態屬性獲得:
編碼名稱 | 編碼上的靜態屬性 |
UTF-8 | Encoding.UTF8 |
UTF-16 | Encoding.Unicode(UTF16) |
UTF-32 | Encoding.UTF32 |
ASCII | Encoding.ASCII |
您可以通過使用標準互聯網號碼分配機構 (IANA) 字符集名稱調用 Encoding.GetEncoding 來獲取其他編碼:
// In .NET 5+ and .NET Core, you must first call RegisterProvider:
Encoding.RegisterProvider (CodePagesEncodingProvider.Instance);
Encoding chinese = Encoding.GetEncoding ("GB18030");
靜態 GetEncodings 方法返回所有受支持編碼的列表及其標準 IANA 名稱:
foreach (EncodingInfo info in Encoding.GetEncodings())
Console.WriteLine (info.Name);
獲取編碼的另一種方法是直接實例化編碼類。這樣做允許您通過構造函數參數設置各種選項,包括:
編碼對象最常見的應用程序是控制文本的讀取和寫入文件或流的方式。例如,下面寫“正在測試...”到名為 的 UTF-16 編碼文件:
System.IO.File.WriteAllText ("data.txt", "Testing...", Encoding.Unicode);
如果省略最后一個參數,WriteAllText 將應用無處不在的 UTF-8 。
UTF-8 是所有文件和流 I/O 的默認文本編碼。
我們將在第 中繼續討論此主題。
還可以使用編碼對象來回字節數組。GetBytes 方法使用給定的編碼從字符串轉換為 byte[]; GetString 從 byte[] 轉換為字符串:
byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes ("0123456789");
byte[] utf16Bytes = System.Text.Encoding.Unicode.GetBytes ("0123456789");
byte[] utf32Bytes = System.Text.Encoding.UTF32.GetBytes ("0123456789");
Console.WriteLine (utf8Bytes.Length); // 10
Console.WriteLine (utf16Bytes.Length); // 20
Console.WriteLine (utf32Bytes.Length); // 40
string original1 = System.Text.Encoding.UTF8.GetString (utf8Bytes);
string original2 = System.Text.Encoding.Unicode.GetString (utf16Bytes);
string original3 = System.Text.Encoding.UTF32.GetString (utf32Bytes);
Console.WriteLine (original1); // 0123456789
Console.WriteLine (original2); // 0123456789
Console.WriteLine (original3); // 0123456789
回想一下,.NET 以 UTF-16 格式存儲字符和字符串。由于 UTF-16 要求每個字符一個或兩個 16 位字,而一個字符的長度僅為 16 位,因此某些 Unicode 字符需要兩個字符來表示。這有幾個:
大多數應用程序都忽略了這一點,因為幾乎所有常用字符都適合稱為 (BMP) 的 Unicode 部分,該部分只需要一個 UTF-16 的 16 位字。BMP涵蓋了幾十種世界語言,包括30,000多個漢字。不包括一些古代語言的字符,樂譜符號和一些不太常見的漢字。
如果需要支持兩個單詞字符,char 中的以下靜態方法會將 32 位代碼點轉換為包含兩個字符的字符串,然后再轉換回來:
string ConvertFromUtf32 (int utf32)
int ConvertToUtf32 (char highSurrogate, char lowSurrogate)
兩個單詞的字符稱為項。它們很容易發現,因為每個單詞都在0xD800到0xDFFF的范圍內。您可以在 char 中使用以下靜態方法來提供幫助:
bool IsSurrogate (char c)
bool IsHighSurrogate (char c)
bool IsLowSurrogate (char c)
bool IsSurrogatePair (char highSurrogate, char lowSurrogate)
命名空間中的 StringInfo 類還提供了一系列用于處理雙字字符的方法和屬性。
BMP 之外的字符通常需要特殊字體,并且操作系統支持有限。
System 命名空間中的以下不可變結構執行表示日期和時間的工作:DateTime 、DateTimeOffset 、TimeSpan 、DataOnly 和 TimeOnly 。C# 沒有定義映射到這些類型的任何特殊關鍵字。
時間跨度表示時間間隔或一天中的某個時間。在后一個角色中,它只是“時鐘”時間(沒有日期),相當于午夜以來的時間,假設沒有夏令時轉換。TimeSpan的分辨率為100 ns,最大值約為10萬天,可以是正數或數。
有三種方法可以構造時間跨度:
以下是構造函數:
public TimeSpan (int hours, int minutes, int seconds);
public TimeSpan (int days, int hours, int minutes, int seconds);
public TimeSpan (int days, int hours, int minutes, int seconds,
int milliseconds);
public TimeSpan (long ticks); // Each tick = 100ns
靜態從...當您只想以單個單位(如分鐘、小時等)指定間隔時,方法更方便:
public static TimeSpan FromDays (double value);
public static TimeSpan FromHours (double value);
public static TimeSpan FromMinutes (double value);
public static TimeSpan FromSeconds (double value);
public static TimeSpan FromMilliseconds (double value);
例如:
Console.WriteLine (new TimeSpan (2, 30, 0)); // 02:30:00
Console.WriteLine (TimeSpan.FromHours (2.5)); // 02:30:00
Console.WriteLine (TimeSpan.FromHours (-2.5)); // -02:30:00
TimeSpan 重載<和>運算符以及 + 和 - 運算符。以下表達式的計算結果為 2.5 小時的時間跨度:
TimeSpan.FromHours(2) + TimeSpan.FromMinutes(30);
下一個表達式的計算結果是少 10 天一秒:
TimeSpan.FromDays(10) - TimeSpan.FromSeconds(1); // 9.23:59:59
使用這個表達式,我們可以說明整數屬性天、小時、分鐘、秒和毫秒:
TimeSpan nearlyTenDays = TimeSpan.FromDays(10) - TimeSpan.FromSeconds(1);
Console.WriteLine (nearlyTenDays.Days); // 9
Console.WriteLine (nearlyTenDays.Hours); // 23
Console.WriteLine (nearlyTenDays.Minutes); // 59
Console.WriteLine (nearlyTenDays.Seconds); // 59
Console.WriteLine (nearlyTenDays.Milliseconds); // 0
相比之下,總...屬性返回描述整個時間跨度的雙精度類型的值:
Console.WriteLine (nearlyTenDays.TotalDays); // 9.99998842592593
Console.WriteLine (nearlyTenDays.TotalHours); // 239.999722222222
Console.WriteLine (nearlyTenDays.TotalMinutes); // 14399.9833333333
Console.WriteLine (nearlyTenDays.TotalSeconds); // 863999
Console.WriteLine (nearlyTenDays.TotalMilliseconds); // 863999000
靜態 Parse 方法與 ToString 相反,將字符串轉換為 TimeSpan。 TryParse 執行相同的操作,但如果轉換失敗,則返回 false 而不是引發異常。類還提供了遵循標準 XML 格式協議的 TimeSpan /string 轉換方法。
TimeSpan 的默認值是 TimeSpan.Zero 。
TimeSpan也可以用來表示一天中的時間(自午夜以來經過的時間)。若要獲取一天中的當前時間,請調用 DateTime.Now.TimeOfDay 。
DateTime 和 DateTimeOffset 是不可變的結構,用于表示日期和時間(可選)。它們的分辨率為 100 ns,范圍涵蓋 0001 至 9999 年。
DateTimeOffset 在功能上類似于 DateTime。它的顯著特點是它還存儲協調世界時 (UTC) 偏移量;這可以在比較不同時區的值時獲得更有意義的結果。
關于引入DateTimeOffset背后的基本原理的優秀文章可以,標題為“DateTime的簡史”,作者是Anthony Moore。
日期時間和日期時間偏移量在處理時區的方式上有所不同。DateTime 包含一個三態標志,指示 DateTime 是否相對于:
DateTimeOffset 更具體 — 它將 UTC 的偏移量存儲為 TimeSpan:
July 01 2019 03:00:00 -06:00
這會影響相等比較,這是在日期時間和日期時間偏移之間進行選擇的主要因素。具體說來:
夏令時可以使這種區別變得重要,即使應用程序不需要處理多個地理時區也是如此。
因此,DateTime 認為以下兩個值不同,而 DateTimeOffset 認為它們相等:
July 01 2019 09:00:00 +00:00 (GMT)
July 01 2019 03:00:00 -06:00 (local time, Central America)
在大多數情況下,DateTimeOffset 的相等邏輯更可取。例如,在計算兩個國際事件中哪一個是最近的事件時,DateTimeOffset 隱式給出正確答案。同樣,策劃分布式拒絕服務攻擊的黑客會達到日期時間偏移量!若要對 DateTime 執行相同的操作,需要在整個應用程序中對單個時區(通常為 UTC)進行標準化。這是有問題的,原因有兩個:
但是,DateTime 最好在運行時指定相對于本地計算機的值,例如,如果要將每個國際辦事處的存檔安排在下周日當地時間凌晨 3 點(此時活動量最少)。在這里,DateTime 會更合適,因為它會尊重每個站點的本地時間。
在內部,DateTimeOffset 使用短整數來存儲 UTC 偏移量(以分鐘為單位)。它不存儲任何區域信息,因此沒有任何內容可以指示 +08:00 的偏移量是指新加坡時間還是珀斯時間。
我們在中更深入地重新審視了時區和相等比較。
SQL Server 2008 通過同名的新數據類型引入了對 DateTimeOffset 的直接支持。
DateTime 定義接受年、月和日的整數(以及可選的小時、分鐘、秒和毫秒)的構造函數:
public DateTime (int year, int month, int day);
public DateTime (int year, int month, int day,
int hour, int minute, int second, int millisecond);
如果僅指定日期,則時間將隱式設置為午夜 (0:00)。
DateTime 構造函數還允許您指定 DateTimeKind — 具有以下值的枚舉:
Unspecified, Local, Utc
這對應于上一節中描述的三態標志。未指定是默認值,這意味著 DateTime 與時區無關。本地表示相對于當前計算機上的本地時區。本地 DateTime 不包含有關它所引用的信息,也不包含與 UTC 的數字偏移量,與 DateTimeOffset 不同。
DateTime 的 Kind 屬性返回其 DateTimeKind 。
DateTime 的構造函數也被重載以接受 Calendar 對象。這允許您使用 System.Globalization 中定義的任何日歷子類指定日期:
DateTime d = new DateTime (5767, 1, 1,
new System.Globalization.HebrewCalendar());
Console.WriteLine (d); // 12/12/2006 12:00:00 AM
(此示例中日期的格式取決于計算機的控制面板設置。DateTime 始終使用默認公歷 — 此示例在構造期間進行一次性轉換。若要使用另一個日歷執行計算,必須使用 Calendar 子類本身上的方法。
您還可以使用長 的單個刻度值構造一個日期時間,其中是從午夜 100/01/01 開始的 0001 ns 間隔數。
為了便于操作,DateTime 提供了靜態的 FromFileTime 和 FromFileTimeUtc 方法,用于從 Windows 文件時間(指定為長)進行轉換,以及用于從 OLE 自動化日期/時間(指定為)轉換的 FromOADate 方法。
若要從字符串構造 DateTime,請調用靜態 Parse 或 ParseExact 方法。這兩種方法都接受可選標志和格式提供程序;ParseExact 也接受格式字符串。我們將在中更詳細地討論解析。
DateTimeOffset 有一組類似的構造函數。不同之處在于,您還將 UTC 偏移量指定為 TimeSpan:
public DateTimeOffset (int year, int month, int day,
int hour, int minute, int second,
TimeSpan offset);
public DateTimeOffset (int year, int month, int day,
int hour, int minute, int second, int millisecond,
TimeSpan offset);
時間跨度必須為整數分鐘數;否則將引發異常。
DateTimeOffset 還具有接受 Calendar 對象、長值以及接受字符串的靜態 Parse 和 ParseExact 方法的構造函數。
可以使用這些構造函數從現有日期時間構造日期時間偏移量
public DateTimeOffset (DateTime dateTime);
public DateTimeOffset (DateTime dateTime, TimeSpan offset);
或隱式強制轉換:
DateTimeOffset dt = new DateTime (2000, 2, 3);
從 DateTime 到 DateTimeOffset 的隱式強制轉換非常方便,因為大多數 .NET BCL 都支持 DateTime — 而不是 DateTimeOffset 。
如果未指定偏移量,則使用以下規則從 DateTime 值推斷出偏移量:
為了在另一個方向上轉換,DateTimeOffset 提供了三個返回 DateTime 類型的值的屬性:
DateTime 和 DateTimeOffset 都有一個靜態 Now 屬性,該屬性返回當前日期和時間:
Console.WriteLine (DateTime.Now); // 11/11/2019 1:23:45 PM
Console.WriteLine (DateTimeOffset.Now); // 11/11/2019 1:23:45 PM -06:00
DateTime 還提供了一個 Today 屬性,該屬性僅返回日期部分:
Console.WriteLine (DateTime.Today); // 11/11/2019 12:00:00 AM
靜態 UtcNow 屬性以 UTC 格式返回當前日期和時間:
Console.WriteLine (DateTime.UtcNow); // 11/11/2019 7:23:45 AM
Console.WriteLine (DateTimeOffset.UtcNow); // 11/11/2019 7:23:45 AM +00:00
所有這些方法的精度取決于操作系統,通常在 10 到 20 毫秒的范圍內。
DateTime 和 DateTimeOffset 提供了一組類似的實例屬性,這些屬性返回各種日期/時間元素:
DateTime dt = new DateTime (2000, 2, 3,
10, 20, 30);
Console.WriteLine (dt.Year); // 2000
Console.WriteLine (dt.Month); // 2
Console.WriteLine (dt.Day); // 3
Console.WriteLine (dt.DayOfWeek); // Thursday
Console.WriteLine (dt.DayOfYear); // 34
Console.WriteLine (dt.Hour); // 10
Console.WriteLine (dt.Minute); // 20
Console.WriteLine (dt.Second); // 30
Console.WriteLine (dt.Millisecond); // 0
Console.WriteLine (dt.Ticks); // 630851700300000000
Console.WriteLine (dt.TimeOfDay); // 10:20:30 (returns a TimeSpan)
DateTimeOffset 還具有類型為 TimeSpan 的 Offset 屬性。
這兩種類型都提供以下實例方法來執行計算(大多數接受 double 或 int 類型的參數):
AddYears AddMonths AddDays
AddHours AddMinutes AddSeconds AddMilliseconds AddTicks
這些都返回一個新的日期時間或日期時間偏移量,并且它們考慮了諸如閏年之類的東西。您可以傳入負值進行減法。
Add 方法將 TimeSpan 添加到 DateTime 或 DateTimeOffset 。+ 運算符重載以執行相同的工作:
TimeSpan ts = TimeSpan.FromMinutes (90);
Console.WriteLine (dt.Add (ts));
Console.WriteLine (dt + ts); // same as above
您還可以從日期時間/日期時間偏移量中減去時間跨度,并從另一個日期時間/日期時間偏移量中減去另一個日期時間/日期時間偏移量。后者給你一個時間跨度:
DateTime thisYear = new DateTime (2015, 1, 1);
DateTime nextYear = thisYear.AddYears (1);
TimeSpan oneYear = nextYear - thisYear;
在日期時間上調用 ToString 會將結果格式設置為(所有數字)后跟(包括秒)。例如:
11/11/2019 11:50:30 AM
默認情況下,操作系統的控制面板確定日、月或年是先排嗎;使用前導零;以及是使用 12 小時還是 24 小時時間。
在 DateTimeOffset 上調用 ToString 是相同的,只是還返回了偏移量:
11/11/2019 11:50:30 AM -06:00
ToShortDateString和ToLongDateString方法只返回日期部分。長日期格式也由控制面板決定;一個例子是“2015年11月11日,星期三”。ToShortTimeString和ToLongTimeString只返回時間部分,例如17:10:10(前者不包括秒)。
這四種剛剛描述的方法實際上是四種不同的快捷方式。ToString 重載以接受格式字符串和提供程序,允許您指定各種選項并控制區域設置的應用方式。我們在中對此進行了描述。
如果區域性設置與設置格式設置時有效的區域性設置不同,則可能會錯誤分析日期時間和日期時間偏移量 s。通過將 ToString 與忽略區域性設置(如“o”)的格式字符串結合使用,可以避免此問題:
DateTime dt1 = DateTime.Now;
string cannotBeMisparsed = dt1.ToString ("o");
DateTime dt2 = DateTime.Parse (cannotBeMisparsed);
靜態 Parse / TryParse 和 ParseExact / TryParseExact 方法與 ToString 相反,將字符串轉換為 DateTime 或 DateTimeOffset 。這些方法也會重載以接受格式提供程序。Try * 方法返回 false 而不是拋出 FormatException 。
由于 DateTime 和 DateTimeOffset 是結構體,因此它們本質上不可為空。當您需要可為空性時,有兩種方法可以解決此問題:
可為 null 的類型通常是最佳方法,因為編譯器有助于防止錯誤。DateTime.MinValue 對于向后兼容在 C# 2.0(引入可為 null 的值類型時)之前編寫的代碼非常有用。
在 DateTime.MinValue 上調用 ToUniversalTime 或 ToLocalTime 可能會導致它不再是 DateTime.MinValue(取決于您在 GMT 的哪一邊)。如果你在格林威治標準時間(英格蘭,夏令時之外)是正確的,那么問題根本不會出現,因為本地時間和UTC時間是相同的。這是你對英國冬天的補償!
DateOnly 和 TimeOnly 結構(從 .NET 6 開始)日期或時間的情況。
DateOnly 與 DateTime 類似,但沒有時間部分。DateOnly 也缺少 DateTimeKind ;實際上,它始終是未指定的,并且沒有本地或UTC的概念。DateOnly 的歷史替代方案是使用零時間(午夜)的 DateTime。這種方法的困難在于,當非零時間進入代碼時,相等比較會失敗。
TimeOnly 與 日期時間 ,但沒有日期組件。TimeOnly 用于捕獲一天中的時間,適用于記錄鬧鐘時間或營業時間等應用。
日期時間在處理時區方面非常簡單。在內部,它使用兩條信息存儲日期時間:
比較兩個日期時間實例時,僅比較它們的值;他們的日期時間種類被忽略:
DateTime dt1 = new DateTime (2000, 1, 1, 10, 20, 30, DateTimeKind.Local);
DateTime dt2 = new DateTime (2000, 1, 1, 10, 20, 30, DateTimeKind.Utc);
Console.WriteLine (dt1 == dt2); // True
DateTime local = DateTime.Now;
DateTime utc = local.ToUniversalTime();
Console.WriteLine (local == utc); // False
實例方法將轉換為通用時間/本地時間。這些設置應用計算機的當前時區設置,并返回新的日期時間,其日期時間類型為 UTC 或本地。如果在已經是 UTC 的日期時間上調用 ToUniversalTime,或者在已經是 Local 的日期時間上調用 ToLocalTime,則不會發生轉換。但是,如果您在未指定的日期時間上調用 ToUniversalTime 或 ToLocalTime,您將獲得轉換。
您可以使用靜態 DateTime.SpecifyKind 方法構造一個僅在 Kind 上不同于另一個的 DateTime:
DateTime d = new DateTime (2015, 12, 12); // Unspecified
DateTime utc = DateTime.SpecifyKind (d, DateTimeKind.Utc);
Console.WriteLine (utc); // 12/12/2015 12:00:00 AM
在內部,DateTimeOffset 包含一個 DateTime 字段(其值始終采用 UTC),以及一個 16 位整數字段,用于表示 UTC 偏移量(以分鐘為單位)。比較只看 (UTC) 日期時間 ;偏移量主要用于格式化。
ToUniversalTime / ToLocalTime 方法返回一個 DateTimeOffset 表示相同的時間點,但具有 UTC 或本地偏移量。與 DateTime 不同,這些方法不會影響基礎日期/時間值,只影響偏移量:
DateTimeOffset local = DateTimeOffset.Now;
DateTimeOffset utc = local.ToUniversalTime();
Console.WriteLine (local.Offset); // -06:00:00 (in Central America)
Console.WriteLine (utc.Offset); // 00:00:00
Console.WriteLine (local == utc); // True
若要在比較中包含偏移量,必須使用 EqualsExact 方法:
Console.WriteLine (local.EqualsExact (utc)); // False
類提供有關時區名稱、UTC 偏移量和夏令時規則的信息。
靜態 TimeZone.CurrentTimeZone 方法返回一個時區:
TimeZone zone = TimeZone.CurrentTimeZone;
Console.WriteLine (zone.StandardName); // Pacific Standard Time
Console.WriteLine (zone.DaylightName); // Pacific Daylight Time
方法返回給定年份的特定夏令時信息:
DaylightTime day = zone.GetDaylightChanges (2019);
Console.WriteLine (day.Start.ToString ("M")); // 10 March
Console.WriteLine (day.End.ToString ("M")); // 03 November
Console.WriteLine (day.Delta); // 01:00:00
靜態 TimeZoneInfo.Local 方法基于當前本地設置返回 TimeZoneInfo 對象。下面演示了在加利福尼亞州運行的結果:
TimeZoneInfo zone = TimeZoneInfo.Local;
Console.WriteLine (zone.StandardName); // Pacific Standard Time
Console.WriteLine (zone.DaylightName); // Pacific Daylight Time
IsDaylightSavingTime 和 GetUtcOffset 方法的工作方式如下:
DateTime dt1 = new DateTime (2019, 1, 1); // DateTimeOffset works, too
DateTime dt2 = new DateTime (2019, 6, 1);
Console.WriteLine (zone.IsDaylightSavingTime (dt1)); // True
Console.WriteLine (zone.IsDaylightSavingTime (dt2)); // False
Console.WriteLine (zone.GetUtcOffset (dt1)); // -08:00:00
Console.WriteLine (zone.GetUtcOffset (dt2)); // -07:00:00
您可以通過使用區域 ID 調用 FindSystemTimeZoneById 來獲取世界上任何時區的 TimeZoneInfo。我們將轉向西澳大利亞州,原因很快就會變得清晰:
TimeZoneInfo wa = TimeZoneInfo.FindSystemTimeZoneById
("W. Australia Standard Time");
Console.WriteLine (wa.Id); // W. Australia Standard Time
Console.WriteLine (wa.DisplayName); // (GMT+08:00) Perth
Console.WriteLine (wa.BaseUtcOffset); // 08:00:00
Console.WriteLine (wa.SupportsDaylightSavingTime); // True
屬性對應于傳遞給 FindSystemTimeZoneById 的值。靜態 GetSystemTimeZones 方法返回所有世界時區;因此,您可以列出所有有效的區域 ID 字符串,如下所示:
foreach (TimeZoneInfo z in TimeZoneInfo.GetSystemTimeZones())
Console.WriteLine (z.Id);
您還可以通過調用 TimeZoneInfo.CreateCustomTimeZone 創建自定義時區。由于 TimeZoneInfo 是不可變的,因此必須將所有相關數據作為方法參數傳入。
您可以通過調用 ToSerializedString 將預定義或自定義時區序列化為(半)人類可讀字符串,并通過調用 TimeZoneInfo.FromSerializedString 對其進行反序列化。
靜態 ConvertTime 方法將日期時間或日期時間偏移量從一個時區轉換為另一個時區。您可以只包含目標時區信息,也可以同時包含源和目標時區信息對象。您還可以使用方法直接從 UTC 轉換或轉換為 UTC 轉換時間到UTC 。
為了使用夏令時,時區信息提供了以下附加方法:
您無法從指示夏令時開始和結束的時區信息獲取簡單日期。相反,您必須調用 GetAdjustRules ,這將返回適用于所有年份的所有夏令時規則的聲明性摘要。每個規則都有一個日期開始和日期結束,指示規則有效的日期范圍:
foreach (TimeZoneInfo.AdjustmentRule rule in wa.GetAdjustmentRules())
Console.WriteLine ("Rule: applies from " + rule.DateStart +
" to " + rule.DateEnd);
西澳大利亞州在2006年季首次引入夏令時(然后在2009年取消了它)。這需要第一年的特殊規則;因此,有兩個規則:
Rule: applies from 1/01/2006 12:00:00 AM to 31/12/2006 12:00:00 AM
Rule: applies from 1/01/2007 12:00:00 AM to 31/12/2009 12:00:00 AM
每個調整規則都有一個 TimeSpan 類型的 DaylightDelta 屬性(幾乎每種情況下都是一小時)和名為 DaylightTransitionStart 和 DaylightTransitionEnd 的屬性。后兩者的類型為 時區信息.過渡時間 ,它具有以下屬性:
public bool IsFixedDateRule { get; }
public DayOfWeek DayOfWeek { get; }
public int Week { get; }
public int Day { get; }
public int Month { get; }
public DateTime TimeOfDay { get; }
轉換時間有些復雜,因為它需要表示固定日期和浮動日期。浮動日期的一個例子是“三月的最后一個星期日”。以下是解釋過渡時間的規則:
在最后一種情況下,周是指每月的一周,“5”表示上周。我們可以通過枚舉 wa 時區的調整規則來證明這一點:
foreach (TimeZoneInfo.AdjustmentRule rule in wa.GetAdjustmentRules())
{
Console.WriteLine ("Rule: applies from " + rule.DateStart +
" to " + rule.DateEnd);
Console.WriteLine (" Delta: " + rule.DaylightDelta);
Console.WriteLine (" Start: " + FormatTransitionTime
(rule.DaylightTransitionStart, false));
Console.WriteLine (" End: " + FormatTransitionTime
(rule.DaylightTransitionEnd, true));
Console.WriteLine();
}
在 格式過渡時間 ,我們尊重剛才描述的規則:
static string FormatTransitionTime (TimeZoneInfo.TransitionTime tt,
bool endTime)
{
if (endTime && tt.IsFixedDateRule
&& tt.Day == 1 && tt.Month == 1
&& tt.TimeOfDay == DateTime.MinValue)
return "-";
string s;
if (tt.IsFixedDateRule)
s = tt.Day.ToString();
else
s = "The " +
"first second third fourth last".Split() [tt.Week - 1] +
" " + tt.DayOfWeek + " in";
return s + " " + DateTimeFormatInfo.CurrentInfo.MonthNames [tt.Month-1]
+ " at " + tt.TimeOfDay.TimeOfDay;
}
如果使用 DateTimeOffset 或 UTC DateTime,則相等比較不受夏令時影響的阻礙。但是對于本地日期時間,夏令時可能會出現問題。
我們可以將規則總結如下:
IsDaylightSavingTime 告訴您給定的本地 DateTime 是否受夏令時約束。UTC 時間總是返回 false :
Console.Write (DateTime.Now.IsDaylightSavingTime()); // True or False
Console.Write (DateTime.UtcNow.IsDaylightSavingTime()); // Always False
假設 dto 是一個 DateTimeOffset ,下面的表達式也做同樣的事情:
dto.LocalDateTime.IsDaylightSavingTime
夏令時的結束給使用本地時間的算法帶來了特別的復雜性,因為當時鐘倒流時,同一小時(或更準確地說,Delta )會重復。
您可以通過首先調用 ToUniversalTime 來可靠地比較任意兩個日期時間。當(且僅當)其中一個具有 DateTimeKind 未指定 時,此策略將失敗。是支持量的另一個原因。
格式化;解析意味著從字符串在編程中,在各種情況下,經常需要格式化或解析。因此,.NET 提供了多種機制:
ToString 和 Parse
這些方法為許多類型提供默認功能。
格式提供程序
這些清單作為接受格式和/或的附加 ToString(和 Parse)方法。格式提供商具有高度的靈活性和區域性意識。.NET 包括數字類型和日期時間/日期時間偏移量的格式提供程序。
XmlConvert
這是一個靜態類,其方法在遵循 XML 標準的同時進行格式化和分析。當您需要區域性獨立性或想要搶占錯誤分析時,XmlConvert 對于常規用途轉換也很有用。XmlConvert 支持數值類型、布爾值、日期時間、日期時間偏移量、時間跨度和 Guid。
類型轉換器
這些目標設計器和 XAML 分析程序。
在本節中,我們將討論前兩種機制,特別關注格式提供程序。然后我們描述 XmlConvert、類型轉換器和其他轉換機制。
最簡單的格式設置機制是 ToString 方法。它對所有簡單值類型(布爾值、日期時間、日期時間偏移量、時間跨度、Guid 和所有數值類型)給出有意義的輸出。對于反向操作,以下每種類型都定義一個靜態 Parse 方法:
string s = true.ToString(); // s = "True"
bool b = bool.Parse (s); // b = true
如果分析失敗,則會引發格式異常。許多類型還定義了 TryParse 方法,如果轉換失敗,該方法返回 false,而不是引發異常:
bool failure = int.TryParse ("qwerty", out int i1);
bool success = int.TryParse ("123", out int i2);
如果您不關心輸出并且只想測試解析是否成功,則可以使用丟棄:
bool success = int.TryParse ("123", out int _);
如果預計會出現錯誤,則調用 TryParse 比在異常處理塊中調用 Parse 更快、更優雅。
日期時間(偏移量)上的解析和嘗試解析方法以及數值類型遵循本地區域性設置;可以通過指定 CultureInfo 對象來更改此設置。指定固定區域性通常是一個好主意。例如,將 “1.234” 解析為雙精度數,在德國得到 1234:
Console.WriteLine (double.Parse ("1.234")); // 1234 (In Germany)
這是因為在德國,句點表示千位分隔符而不是小數點。指定可解決此問題:
double x = double.Parse ("1.234", CultureInfo.InvariantCulture);
這同樣適用于調用 ToString() 時:
string x = 1.234.ToString (CultureInfo.InvariantCulture);
有時,您需要更好地控制格式化和分析的發生方式。有幾十種方法可以格式化日期時間(偏移量) ,例如。格式提供程序允許對格式設置和分析進行廣泛控制,并且支持數字類型和日期/時間。用戶界面控件還使用格式提供程序進行格式設置和分析。
使用格式提供程序的網關是 IFormatable 。所有數值類型和日期時間(偏移量)都實現此接口:
public interface IFormattable
{
string ToString (string format, IFormatProvider formatProvider);
}
第一個參數是;第二個是。格式字符串提供說明;格式提供程序確定如何翻譯說明。例如:
NumberFormatInfo f = new NumberFormatInfo();
f.CurrencySymbol = "$$";
Console.WriteLine (3.ToString ("C", f)); // $$ 3.00
此處,“C”是指示的格式字符串,而 NumberFormatInfo 對象是確定如何呈現貨幣和其他數字表示形式的格式提供程序。這一機制允許全球化。
數字和日期的所有格式字符串都列在中。
如果指定空格式字符串或提供程序,則應用默認值。默認格式提供程序是 CultureInfo.CurrentCulture ,除非重新分配,否則它反映計算機的運行時控制面板設置。例如,在此計算機上:
Console.WriteLine (10.3.ToString ("C", null)); // .30
為方便起見,大多數類型重載 ToString,以便您可以省略空:
Console.WriteLine (10.3.ToString ("C")); // $10.30
Console.WriteLine (10.3.ToString ("F4")); // 10.3000 (Fix to 4 D.P.)
在 DateTime ( 偏移量 ) 或不帶參數的數值類型上調用 ToString 等效于使用具有空格式字符串的默認格式提供程序。
.NET 定義了三個格式提供程序(它們都實現 IFormatProvider):
NumberFormatInfo
DateTimeFormatInfo
CultureInfo
所有枚舉類型也是可格式化的,盡管沒有特殊的 IFormatProvider 類。
在格式提供程序的上下文中,CultureInfo 充當其他兩個格式提供程序的間接機制,返回適用于區域性區域設置的 NumberFormatInfo 或 DateTimeFormatInfo 對象。
在下面的示例中,我們請求特定的區域性(reat ritain中的英語語言):
CultureInfo uk = CultureInfo.GetCultureInfo ("en-GB");
Console.WriteLine (3.ToString ("C", uk)); // £3.00
這將使用適用于 en-GB 區域性的默認 NumberFormatInfo 對象執行。
下一個示例使用固定區域性設置日期時間的格式。無論計算機的設置如何,固定區域性始終相同:
DateTime dt = new DateTime (2000, 1, 2);
CultureInfo iv = CultureInfo.InvariantCulture;
Console.WriteLine (dt.ToString (iv)); // 01/02/2000 00:00:00
Console.WriteLine (dt.ToString ("d", iv)); // 01/02/2000
固定區域性基于美國文化,具有以下區別:
在下一個示例中,我們實例化一個 NumberFormatInfo 并將組分隔符從逗號更改為空格。然后,我們使用它來將數字格式化為小數點后三位:
NumberFormatInfo f = new NumberFormatInfo ();
f.NumberGroupSeparator = " ";
Console.WriteLine (12345.6789.ToString ("N3", f)); // 12 345.679
NumberFormatInfo 或 DateTimeFormatInfo 的初始設置基于固定區域性。但是,有時選擇不同的起點更有用。為此,您可以克隆現有的格式提供程序:
NumberFormatInfo f = (NumberFormatInfo)
CultureInfo.CurrentCulture.NumberFormat.Clone();
克隆的格式提供程序始終是可寫的,即使原始格式提供程序是只讀的。
復合格式字符串允許您將變量替換與格式字符串組合在一起。靜態字符串。格式方法接受復合格式字符串(我們在中對此進行了說明):
string composite = "Credit={0:C}";
Console.WriteLine (string.Format (composite, 500)); // Credit=$500.00
控制臺類本身重載其 Write 和 WriteLine 方法以接受復合格式字符串,從而允許我們稍微縮短此示例:
Console.WriteLine ("Credit={0:C}", 500); // Credit=0.00
您還可以將復合格式字符串附加到 StringBuilder(通過 AppendFormat)和用于 I/O 的文本編寫器(參見)。
字符串。格式接受可選的格式提供程序。一個簡單的應用程序是在傳入格式提供程序時對任意對象調用 ToString:
string s = string.Format (CultureInfo.InvariantCulture, "{0}", someObject);
這等效于以下內容:
string s;
if (someObject is IFormattable)
s = ((IFormattable)someObject).ToString (null,
CultureInfo.InvariantCulture);
else if (someObject == null)
s = "";
else
s = someObject.ToString();
沒有用于通過格式提供程序進行分析的標準接口。相反,每個參與類型都會重載其靜態 Parse(和 TryParse)方法,以接受格式提供程序和(可選)NumberStyles 或 DateTimeStyles 枚舉。
NumberStyles 和 DateTimeStyles 控制解析的工作方式:它們允許您指定括號或貨幣符號是否可以出現在輸入字符串中。(默認情況下,這兩個問題的答案都是。例如:
int error = int.Parse ("(2)"); // Exception thrown
int minusTwo = int.Parse ("(2)", NumberStyles.Integer |
NumberStyles.AllowParentheses); // OK
decimal fivePointTwo = decimal.Parse ("£5.20", NumberStyles.Currency,
CultureInfo.GetCultureInfo ("en-GB"));
下一節列出了所有 NumberStyles 和 DateTimeStyles 成員以及每種類型的默認分析規則。
所有格式提供程序都實現 IFormatProvider:
public interface IFormatProvider { object GetFormat (Type formatType); }
此方法的目的是提供間接性 - 這是允許 CultureInfo 遵從適當的 NumberFormatInfo 或 DateTimeInfo 對象來完成工作的原因。
通過實現 IFormatProvider 以及 ICustomFormatter,您還可以編寫自己的格式提供程序,與現有類型結合使用。 ICustomFormatter 定義了單個方法,如下所示:
string Format (string format, object arg, IFormatProvider formatProvider);
以下自定義格式提供程序將數字寫為單詞:
public class WordyFormatProvider : IFormatProvider, ICustomFormatter
{
static readonly string[] _numberWords =
"zero one two three four five six seven eight nine minus point".Split();
IFormatProvider _parent; // Allows consumers to chain format providers
public WordyFormatProvider () : this (CultureInfo.CurrentCulture) { }
public WordyFormatProvider (IFormatProvider parent) => _parent = parent;
public object GetFormat (Type formatType)
{
if (formatType == typeof (ICustomFormatter)) return this;
return null;
}
public string Format (string format, object arg, IFormatProvider prov)
{
// If it's not our format string, defer to the parent provider:
if (arg == null || format != "W")
return string.Format (_parent, "{0:" + format + "}", arg);
StringBuilder result = new StringBuilder();
string digitList = string.Format (CultureInfo.InvariantCulture,
"{0}", arg);
foreach (char digit in digitList)
{
int i = "0123456789-.".IndexOf (digit),
StringComparison.InvariantCulture);
if (i == -1) continue;
if (result.Length > 0) result.Append (' ');
result.Append (_numberWords[i]);
}
return result.ToString();
}
}
請注意,在 Format 方法中,我們使用了字符串。格式 - 使用固定文化 - 將輸入數字轉換為字符串。在arg上調用ToString()會更簡單,但是后來會使用CurrentCulture。需要固定區域性的原因在后面幾行中很明顯:
int i = "0123456789-.".IndexOf (digit);
在這里,數字字符串僅包含字符 0123456789- 至關重要。而不是這些的任何國際化版本。
以下是使用 WordyFormatProvider 的示例:
double n = -123.45;
IFormatProvider fp = new WordyFormatProvider();
Console.WriteLine (string.Format (fp, "{0:C} in words is {0:W}", n));
// -$123.45 in words is minus one two three point four five
只能在復合格式字符串中使用自定義格式提供程序。
標準格式字符串控制如何將數值類型或日期時間/日期時間偏移量轉換為字符串。有兩種格式字符串:
標準格式字符串
有了這些,您可以提供一般指導。標準格式字符串由單個字母組成,后跟一個數字(其含義取決于字母)。一個例子是 “C” 或 “F2” .
自定義格式字符串
有了這些,您可以使用模板對每個字符進行微觀管理。一個例子是 “0:#.000E+00” 。
自定義格式字符串與自定義格式提供程序無關。
列出了所有標準數字格式字符串。
標準數字格式字符串 | ||||
信 | 意義 | 示例輸入 | 結果 | 筆記 |
G 或 g | “一般” | 1.2345, “G” 0.00001, “G” 0.00001, “g” 1.2345, “G3” 12345, “G3” | 1.2345 1E-05 1E-05 1.23 1.23E04 | 對于小數字或大數字,切換到指數表示法。 G3 將精度限制為(點之前 + 點后)。 |
F | 定點 | 2345.678, “F2” 2345.6, “F2” | 2345.68 2345.60 | F2 舍入到小數點后兩位。 |
N | 帶的固定點(“數字”) | 2345.678, “N2” 2345.6, “N2” | 2,345.68 2,345.60 | 如上所述,使用組(1,000s)分隔符(格式提供程序的詳細信息)。 |
D | 帶前導零的焊盤 | 123, “D5” 123, “D1” | 00123 123 | 僅適用于整型。 D5 墊子左至五位數;不會截斷。 |
E 或 E | 強制指數表示法 | 56789, “E” 56789, “E” 56789, “E2” | 5.678900E+004 5.678900e+004 5.68E+004 | 六位數默認精度。 |
C | 貨幣 | 1.2, “C” 1.2, “C4” | .20 .2000 | 不帶數字的 C 使用格式提供程序的默認 D.P. 編號。 |
P | 百分之 | .503, “P” .503, “P0” | 50.30% 50% | 使用格式提供程序中的符號和布局。可以選擇覆蓋小數位。 |
X 或 x | 十六進制 | 47, “X” 47, “x” 47, “X4” | 2樓 2樓 002樓 | X 表示大寫十六進制數字;x 表示小寫十六進制數字。僅限積分。 |
R 或 G9/G17 | 往返 | 1樓 / 3樓,“R” | 0.333333343 | 使用 R 表示 BigInteger,G17 表示雙精度,或 G9 表示浮點數。 |
不提供數字格式字符串(或空或空白字符串)等效于使用“G”標準格式字符串后跟無數字。這將表現出以下行為:
剛才描述的自動舍入通常是有益的,不會被注意到。但是,如果您需要往返號碼,則可能會造成麻煩;換句話說,將其轉換為字符串并再次(可能重復)回來,同時保持值相等。因此,存在 R、G17 和 G9 格式字符串來規避這種隱式舍入。
列出了自定義數字格式字符串。
自定義數字格式字符串 | ||||
規范 | 意義 | 示例輸入 | 結果 | 筆記 |
# | 數字占位符 | 12.345, “.##” 12.345, “.####” | 12.35 12.345 | 限制 DP 之后的數字 |
0 | 零占位符 | 12.345, “.00” 12.345, “.0000” 99, “000.00” | 12.35 12.3450 099.00 | 如上所述,但在 D.P. 之前和之后也用零填充。 |
. | 小數點 | 表示 DP 實際符號來自 數字格式信息 。 | ||
, | 組分隔符 | 1234, “#,###,###” 1234, “0,000,000” | 1,234 0,001,234 | 符號來自 數字格式信息 。 |
, (同上) | 乘數 | 1000000, “#,” 1000000, “#,, | 1000 1 | 如果逗號位于 D.P. 的末尾或之前,則它充當乘數 - 將結果除以 1,000、1,000,000 等。 |
% | 百分比表示法 | 0.6, "00%" | 60% | 首先乘以 100,然后替換從 獲得的百分比符號。 |
E0, e0, E+0, e+0 E-0, e-0 | 指數表示法 | 1234, “0E0” 1234, “0E+0” 1234, “0.00E00” 1234, “0.00e00” | 1E3 1E+3 1.23E03 1.23E03 | |
\ | 字面人物引用 | 50, @"\#0" | #50 | 與字符串上的 @ 前綴結合使用,或使用 \。 |
“xx”“xx” | 文字字符串引用 | 50, "0 '...'" | 50 ... | |
; | 節分隔符 | 15, “#;(#);零” | 15 | (如果為陽性。 |
-5, “#;(#);零” | (5) | (如果為負數。 | ||
0, “#;(#);零” | 零 | (如果為零。 | ||
任何其他字符 | 字面 | 35.2, “>35.2, “$0 .00c”< .00c” | .20c |
每個數值類型定義一個接受 NumberStyles 參數的靜態 Parse 方法。NumberStyles 是一個標志枚舉,可用于確定在字符串轉換為數值類型時如何讀取字符串。它具有以下可組合成員:
AllowLeadingWhite AllowTrailingWhite
AllowLeadingSign AllowTrailingSign
AllowParentheses AllowDecimalPoint
AllowThousands AllowExponent
AllowCurrencySymbol AllowHexSpecifier
NumberStyles 還定義了以下復合成員:
None Integer Float Number HexNumber Currency Any
除了 None 之外,所有復合值都包括 AllowLeadingWhite 和 AllowTrailingWhite 。顯示了他們剩余的妝容,其中最有用的三個強調了。
在未指定任何標志的情況下調用 Parse 時,將應用 中所示的默認值。
如果不需要 中所示的默認值,則必須顯式指定 :
int thousand = int.Parse ("3E8", NumberStyles.HexNumber);
int minusTwo = int.Parse ("(2)", NumberStyles.Integer |
NumberStyles.AllowParentheses);
double aMillion = double.Parse ("1,000,000", NumberStyles.Any);
decimal threeMillion = decimal.Parse ("3e6", NumberStyles.Any);
decimal fivePointTwo = decimal.Parse ("$5.20", NumberStyles.Currency);
由于我們未指定格式提供程序,因此此示例使用本地貨幣符號、組分隔符、小數點等。下一個示例經過硬編碼,可與歐元符號和貨幣的空白組分隔符一起使用:
NumberFormatInfo ni = new NumberFormatInfo();
ni.CurrencySymbol = "€";
ni.CurrencyGroupSeparator = " ";
double million = double.Parse ("€1 000 000", NumberStyles.Currency, ni);
日期時間/日期時間偏移量的格式字符串可以根據它們是否支持區域性和格式提供程序設置分為兩組。 列出了這樣做的那些; 列出了那些沒有的。示例輸出來自以下 DateTime 的格式設置(在 中為使用):
new DateTime (2000, 1, 2, 17, 18, 19);
區分區域性的日期/時間格式字符串 | ||
格式字符串 | 意義 | 示例輸出 |
d | 短日期 | 01/02/2000 |
D | 長日期 | Sunday, 02 January 2000 |
t | 時間短 | 17:18 |
T | 乆 | 17:18:19 |
f | 長日期+短時間 | Sunday, 02 January 2000 17:18 |
F | 長日期+長時間 | Sunday, 02 January 2000 17:18:19 |
g | 短日期+短時間 | 01/02/2000 17:18 |
G(默認) | 短日期 + 長時間 | 01/02/2000 17:18:19 |
米,米 | 月和日 | 02 一月 |
y, y | 年和月 | January 2000 |
不區分區域性的日期/時間格式字符串 | |||
格式字符串 | 意義 | 示例輸出 | 筆記 |
o | 可往返跳閘 | 2000-01-02T17:18:19.0000000 | 將附加時區信息,除非 日期時間種類 未。 |
r , R | RFC 1123 標準 | 周日, 02 一月 2000 17:18:19 GMT | 您必須使用 顯式轉換為 UTC。 |
s | 可排序;ISO 8601 認證 | 2000-01-02T17:18:19 | 與基于文本的排序兼容。 |
u | “通用”可排序 | 2000-01-02 17:18:19Z | 與上面類似;必須顯式轉換為 UTC。 |
U | 世界協調時 | Sunday, 02 January 2000 17:18:19 | 長日期 + 短時間,轉換為 UTC。 |
格式字符串 “r” 、 “R” 和 “u” 發出一個暗示 UTC 的后綴;但是,它們不會自動將本地轉換為 UTC 日期時間(因此您必須自己進行轉換)。具有諷刺意味的是,“U”會自動轉換為UTC,但不寫時區后綴!事實上,“o”是組中唯一可以在沒有干預的情況下編寫明確日期時間的格式說明符。
DateTimeFormatInfo 還支持自定義格式字符串:這些類似于數字自定義格式字符串。該列表內容廣泛,可在Microsoft的文檔中在線獲得。下面是自定義格式字符串的示例:
yyyy-MM-dd HH:mm:ss
將月份或日期放在首位的字符串不明確,很容易被錯誤解析,尤其是在您有全球客戶的情況下。這在用戶界面控件中不是問題,因為解析時和格式化時相同的設置有效。但是,例如,在寫入文件時,日/月錯誤解析可能是一個真正的問題。有兩種解決方案:
第二種方法更可靠,特別是如果您選擇將四位數年份放在首位的格式:此類字符串更難被另一方錯誤解析。此外,使用年份優先格式(例如“o”)格式化的字符串可以與本地格式的字符串一起正確解析 - 更像“通用捐贈者”。(用“s”或“u”格式設置的日期還有可排序的進一步好處。
為了說明這一點,假設我們生成一個不區分區域性的 DateTime 字符串 :
string s = DateTime.Now.ToString ("o");
“o”格式字符串在輸出中包含毫秒。以下自定義格式字符串給出與“o”相同的結果,但沒有毫秒:
yyyy-MM-ddTHH:mm:ss K
我們可以通過兩種方式重新解析它。ParseExact 要求嚴格遵守指定的格式字符串:
DateTime dt1 = DateTime.ParseExact (s, "o", null);
(您可以使用 XmlConvert 的 ToString 和 ToDateTime 方法獲得類似的結果。
然而,Parse隱式地接受“o”格式和CurrentCulture:
DateTime dt2 = DateTime.Parse (s);
這適用于 日期時間和日期時間偏移量 。
如果您知道要解析的字符串的格式,則通常最好使用 ParseExact 。這意味著,如果字符串格式不正確,則會引發異常,這通常比冒著錯誤解析日期的風險要好。
DateTimeStyles 是一個標志枚舉,在對 DateTime ( 偏移量) 上調用 Parse 時提供附加指令。以下是其成員:
None,
AllowLeadingWhite, AllowTrailingWhite, AllowInnerWhite,
AssumeLocal, AssumeUniversal, AdjustToUniversal,
NoCurrentDateDefault, RoundTripKind
還有一個復合成員,AllowWhiteSpaces:
AllowWhiteSpaces = AllowLeadingWhite | AllowTrailingWhite | AllowInnerWhite
默認值為 無 。這意味著通常禁止使用額外的空格(作為標準 DateTime 模式一部分的空格除外)。
假設本地和假設通用如果字符串沒有時區后綴(如 Z 或 +9:00)則適用。AdjustToUniversal 仍支持時區后綴,但隨后使用當前區域設置轉換為 UTC。
If you parse a string comprising a time but no date, today’s date is applied by default. If you apply the NoCurrentDateDefault flag, however, it instead uses 1st January 0001.
在第 的中,我們描述了枚舉值的格式化和解析。 列出了每個格式字符串以及將其應用于以下表達式的結果:
Console.WriteLine (System.ConsoleColor.Red.ToString (formatString));
枚舉格式字符串 | |||
格式字符串 | 意義 | 示例輸出 | 筆記 |
G 或 g | “一般” | 紅 | 違約 |
F 或 f | 視為存在標志屬性 | 紅 | 適用于組合成員,即使枚舉沒有標志屬性 |
D 或 d | 十進制值 | 12 | 檢索基礎積分值 |
X 或 x | 十六進制值 | 0000000C | 檢索基礎積分值 |
在前兩節中,我們介紹了格式提供程序 - 。NET 用于格式化和分析的主要機制。其他重要的轉換機制分散在各種類型和命名空間中。有些與字符串進行轉換,有些則進行其他類型的轉換。在本節中,我們將討論以下主題:
.NET 調用以下類型:
靜態 Convert 類定義將每個基類型轉換為每個其他基類型的方法。不幸的是,這些方法中的大多數都是無用的:它們要么拋出異常,要么與隱式強制轉換一起冗余。但是,在混亂中,有一些有用的方法,在以下各節中列出。
所有基類型(顯式地)都實現了 IConvertible,它定義了轉換為所有其他基類型的方法。在大多數情況下,這些方法中的每一個的實現只是在 轉換 .在極少數情況下,編寫接受 IConvertible 類型的參數的方法可能很有用。
在第 中,我們了解了隱式和顯式強制轉換如何允許您在數值類型之間進行轉換。總結:
鑄件針對效率進行了優化;因此,它們不適合的數據。從實數轉換為整數時,這可能是一個問題,因為通常您希望而不是截斷。Convert 的數值轉換方法正好解決了這個問題——它們總是:
double d = 3.9;
int i = Convert.ToInt32 (d); // i == 4
轉換使用,將中點值捕捉為偶數(這可以防止正偏差或負偏差)。如果銀行家的四舍五入是一個問題,首先調用實數的 Math.Round:這接受一個額外的參數,允許您控制中點舍入。
隱藏在方法中的是解析另一個基數的重載:To(integral-type)
int thirty = Convert.ToInt32 ("1E", 16); // Parse in hexadecimal
uint five = Convert.ToUInt32 ("101", 2); // Parse in binary
第二個參數指定基數。它可以是你喜歡的任何基數——只要是 2、8、10 或 16!
有時,您需要從一種類型轉換為另一種類型,但直到運行時您才知道這些類型是什么。為此,Convert 類提供了一個 ChangeType 方法:
public static object ChangeType (object value, Type conversionType);
源和目標類型必須是“基”類型之一。ChangeType 還接受可選的 IFormatProvider 參數。下面是一個示例:
Type targetType = typeof (int);
object source = "42";
object result = Convert.ChangeType (source, targetType);
Console.WriteLine (result); // 42
Console.WriteLine (result.GetType()); // System.Int32
這可能有用的一個示例是編寫可以使用多種類型的反序列化程序。它還可以將任何枚舉轉換為其整數類型(參見中的)。
ChangeType 的一個限制是不能指定格式字符串或分析標志。
有時,您需要在文本文檔(如 XML 文件或電子郵件)中包含二進制數據(如位圖)。Base 64 是一種普遍使用二進制數據編碼為可讀字符的方法,使用 ASCII 集中的 64 個字符。
Convert 的 ToBase64String 方法從字節數組轉換為基數 64;FromBase64String 執行相反的操作。
如果要處理源自或發往 XML 文件的數據,XmlConvert(在 System.Xml 命名空間中)提供了最合適的格式設置和分析方法。XmlConvert 中的方法處理 XML 格式的細微差別,而無需特殊的格式字符串。例如,XML 中的 true 是“true”而不是“True”。.NET BCL 在內部廣泛使用 XmlConvert。XmlConvert 也適用于獨立于區域性的通用序列化。
XmlConvert 中的格式化方法都作為重載的 ToString 方法提供;解析方法稱為 ToBoolean 、ToDateTime 等:
string s = XmlConvert.ToString (true); // s = "true"
bool isTrue = XmlConvert.ToBoolean (s);
與 DateTime 相互轉換的方法接受 XmlDateTimeSerializationMode 參數。這是一個具有以下值的枚舉:
Unspecified, Local, Utc, RoundtripKind
本地和 UTC 會導致在格式化時發生轉換(如果日期時間尚未在該時區中)。然后將時區追加到字符串中:
2010-02-22T14:08:30.9375 // Unspecified
2010-02-22T14:07:30.9375+09:00 // Local
2010-02-22T05:08:30.9375Z // Utc
未指定在格式化之前剝離嵌入在日期時間(即日期時間種類)中的任何時區信息。RoundtripKind 遵循 DateTime 的 DateTimeKind — 因此當它被重新分析時,生成的 DateTime 結構將完全符合它原來的樣子。
類型轉換器設計用于在設計時環境中格式化和分析。它們還分析可擴展應用程序標記語言 (XAML) 文檔中的值,如 Windows Presentation Foundation (WPF) 中使用的值。
在 .NET 中,有 100 多個類型轉換器,涵蓋顏色、圖像和 URI 等內容。相比之下,格式提供程序只為少數簡單值類型實現。
類型轉換器通常以多種方式分析字符串,無需提示。例如,在 Visual Studio 的 WPF 應用程序中,如果通過在相應的屬性窗口中鍵入“Miige”為控件分配背景色,則 Color 的類型轉換器會確定您引用的是顏色名稱,而不是 RGB 字符串或系統顏色。這種靈活性有時可以使類型轉換器在設計器和 XAML 文檔之外的上下文中有用。
所有類型轉換器子類類型轉換器在系統組件模型中。要獲取 TypeConverter,請調用 TypeDescriptor.GetConverter 。下面獲取顏色類型的類型轉換器(在 System.Drawing 命名空間中):
TypeConverter cc = TypeDescriptor.GetConverter (typeof (Color));
在許多其他方法中,TypeConverter 定義了 ConvertToString 和 ConvertFromString 的方法。我們可以按如下方式調用這些:
Color beige = (Color) cc.ConvertFromString ("Beige");
Color purple = (Color) cc.ConvertFromString ("#800080");
Color window = (Color) cc.ConvertFromString ("Window");
按照約定,類型轉換器的名稱以 結尾,并且通常與它們要轉換的類型位于同一命名空間中。一個類型通過TypeConverterAttribute鏈接到它的轉換器,允許設計師自動拾取轉換器。
類型轉換器還可以提供設計時服務,例如生成標準值列表以填充設計器中的下拉列表或協助代碼。
大多數基類型都可以通過調用 BitConverter 轉換為字節數組。獲取字節 :
foreach (byte b in BitConverter.GetBytes (3.5))
Console.Write (b + " "); // 0 0 0 0 0 0 12 64
BitConverter還提供了方法,例如ToDouble,用于在另一個方向上進行轉換。
十進制和日期時間(偏移量)類型不受BitConverter的支持。但是,您可以通過調用十進制將小數轉換為 int 數組。獲取比特 .反過來說,decimal 提供了一個接受 int 數組的構造函數。
在 DateTime 的情況下,您可以在實例上調用 ToBinary,這將返回一個 long(然后您可以使用 BitConverter)。靜態 DateTime.FromBinary 方法執行相反的操作。
應用程序有兩個方面:和。
涉及三項任務(按重要性降序排列):
意味著通過為特定區域性編寫附屬程序集來完成最后一項任務。您可以在編寫程序執行此操作(我們將在第 的中介紹詳細信息)。
.NET 通過默認應用特定于區域性的規則來幫助你完成第二個任務。我們已經了解了在日期時間或數字上調用 ToString 如何遵循本地格式規則。遺憾的是,這很容易使第一個任務失敗并導致程序中斷,因為您希望根據假定的區域性設置日期或數字的格式。正如我們所看到的,解決方案是在格式化和分析時指定區域性(例如固定區域性),或者使用與區域性無關的方法(例如 XmlConvert 中的方法)。
我們已經在本章中介紹了要點。以下是所需基本工作的摘要:
您可以通過重新分配 Thread 的 CurrentCulture 屬性(在 System.Threading 中)來針對不同的區域性進行測試。以下內容將當前區域性更改為:
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo ("tr-TR");
土耳其是一個特別好的測試案例,因為:
您還可以通過在 Windows 控制面板中更改數字和日期格式設置來進行試驗:這些設置反映在默認區域性 ( CultureInfo.CurrentCulture ) 中。
CultureInfo.GetCulture() 返回所有可用區域性的數組。
Thread 和 CultureInfo 還支持 CurrentUICulture 屬性。這更關注本地化,我們將在第中介紹。
我們在前面的章節和章節中介紹了數字轉換; 總結了所有選項。
數值轉換摘要 | ||
任務 | 功能 | 例子 |
e | Parse TryParse | 國際 i;布爾確定 = 整數。TryParse (“3”, out i); |
從基數 2、8 或 16 解析 | Convert.toIntegral | int i = Convert.ToInt32 (“1E”, 16); |
格式化為十六進制 | ToString (“X”) | 字符串十六進制 = 45.ToString (“X”); |
無損數字轉換 | 隱式演員表 | 整數 i = 23;雙 d = i; |
數值轉換 | 顯式強制轉換 | 雙倍 d = 23.5; int i = (int) d; |
數字轉換(實數到整數) | Convert.toIntegral | 雙倍 d = 23.5; int i = Convert.ToInt32 (d); |
列出了靜態 Math 類的關鍵成員。三角函數接受雙精度類型的參數;其他方法(如 Max)被重載以對所有數值類型進行操作。數學類還定義了數學常數 E() 和 PI。
靜態數學類中的方法 | |
類別 | 方法 |
舍入 | 圓形 , 截斷 , 地板 , 天花板 |
最大值/最小值 | 最大 , 最小值 |
絕對值和符號 | 腹肌 , 標志 |
平方根 | Sqrt |
提升到權力 | 戰俘 , 經驗 |
對數 | 日志 , 日志10 |
三角 | 罪 , 科斯 , 譚 , 辛 , 科什 , 坦 , 阿辛 , 阿科斯 , 阿坦 |
Round 方法允許您指定舍入的小數位數以及如何處理中點(遠離零或使用銀行家舍入)。樓層和天花板舍入到最接近的整數:地板始終向下舍入,天花板始終向上舍入 - 即使使用負數也是如此。
Max 和 Min 只接受兩個論點。如果您有數字數組或序列,請使用 System.Linq.Enumerable 中的 Max 和 Min 擴展方法。
BigInteger 結構是一種專用的數值類型。它駐留在 System.Numerics 命名空間中,允許您表示任意大的整數,而不會損失任何精度。
C#不提供對BigInteger的原生支持,所以沒有辦法表示BigInteger文字。但是,您可以從任何其他整數類型隱式轉換為 BigInteger:
BigInteger twentyFive = 25; // implicit conversion from integer
表示更大的數字,例如一個 googol (10100),您可以使用 BigInteger 的靜態方法之一,例如 Pow(提高到冪):
BigInteger googol = BigInteger.Pow (10, 100);
或者,您可以解析字符串:
BigInteger googol = BigInteger.Parse ("1".PadRight (101, '0'));
在此調用 ToString() 會打印每個數字:
Console.WriteLine (googol.ToString()); // 10000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000
可以使用顯式強制轉換運算符在 BigInteger 和標準數值類型之間執行潛在的有損轉換:
double g2 = (double) googol; // Explicit cast
BigInteger g3 = (BigInteger) g2; // Explicit cast
Console.WriteLine (g3);
由此產生的輸出演示了精度的損失:
9999999999999999673361688041166912...
BigInteger重載所有算術運算符,包括余數(%)以及比較和相等運算符。
您還可以從字節數組構造 BigInteger。以下代碼生成一個適合加密的 32 字節隨機數,然后將其分配給 BigInteger:
// This uses the System.Security.Cryptography namespace:
RandomNumberGenerator rand = RandomNumberGenerator.Create();
byte[] bytes = new byte [32];
rand.GetBytes (bytes);
var bigRandomNumber = new BigInteger (bytes); // Convert to BigInteger
將這樣的數字存儲在 BigInteger 中而不是字節數組的優點是可以獲得值類型語義。調用 ToByteArray 會將 BigInteger 轉換回字節數組。
Half 結構是 16 位浮點類型,隨 .NET 5 一起引入。Half 主要用于與顯卡處理器互操作,在大多數 CPU 中沒有本機支持。
您可以通過顯式強制轉換在 Half 和 float 或 double 之間進行轉換:
Half h = (Half) 123.456;
Console.WriteLine (h); // 123.44 (note loss of precision)
沒有為此類型定義算術運算,因此必須轉換為另一種類型(如浮點型或雙精度型)才能執行計算。
一半的范圍為 -65500 到 65500:
Console.WriteLine (Half.MinValue); // -65500
Console.WriteLine (Half.MaxValue); // 65500
請注意最大范圍內的精度損失:
Console.WriteLine ((Half)65500); // 65500
Console.WriteLine ((Half)65490); // 65500
Console.WriteLine ((Half)65480); // 65470
復數結構是另一種專門的數值類型,表示具有雙精度類型的實部和虛部的復數。Complex 駐留在命名空間中(與 BigInteger 一起)。
要使用 Complex ,實例化結構,指定實值和虛值:
var c1 = new Complex (2, 3.5);
var c2 = new Complex (3, 0);
還有來自標準數值類型的隱式轉換。
復雜結構公開實值和虛值以及相位和量級的屬性:
Console.WriteLine (c1.Real); // 2
Console.WriteLine (c1.Imaginary); // 3.5
Console.WriteLine (c1.Phase); // 1.05165021254837
Console.WriteLine (c1.Magnitude); // 4.03112887414927
您還可以通過指定幅度和相位來構造復數:
Complex c3 = Complex.FromPolarCoordinates (1.3, 5);
標準算術運算符被重載以處理復數:
Console.WriteLine (c1 + c2); // (5, 3.5)
Console.WriteLine (c1 * c2); // (6, 10.5)
Complex 結構公開了更高級函數的靜態方法,包括:
Random 類生成隨機字節 s、整數 s 或雙精度 s 的偽隨機序列。
要使用 隨機 ,您首先實例化它,可以選擇提供一個種子來啟動隨機數序列。使用相同的種子可以保證相同的數字系列(如果在同一 CLR 版本下運行),這在需要時有時很有用:
Random r1 = new Random (1);
Random r2 = new Random (1);
Console.WriteLine (r1.Next (100) + ", " + r1.Next (100)); // 24, 11
Console.WriteLine (r2.Next (100) + ", " + r2.Next (100)); // 24, 11
如果你不想要可重復性,你可以在沒有種子的情況下構建隨機;在這種情況下,它使用當前系統時間來彌補一個。
由于系統時鐘的粒度有限,因此兩個相鄰創建的隨機實例(通常在 10 毫秒內)將產生相同的值序列。一個常見的陷阱是每次需要隨機數時實例化一個新的 Random 對象,而不是重用對象。
一個好的模式是聲明單個靜態隨機實例。但是,在多線程方案中,這可能會導致麻煩,因為隨機對象不是線程安全的。我們在中描述了一種解決方法。
調用將生成一個介于 0 和 之間的隨機整數。NextDouble 生成一個介于 0 和 1 之間的隨機雙精度值。NextBytes 用隨機值填充字節數組。Next(n)n?1
對于高安全性應用程序(如加密)而言,隨機性被認為不夠隨機。為此,.NET 在 System.Security.Cryptography 命名空間中提供了一個隨機數生成器。以下是使用它的方法:
var rand = System.Security.Cryptography.RandomNumberGenerator.Create();
byte[] bytes = new byte [32];
rand.GetBytes (bytes); // Fill the byte array with random numbers.
缺點是它不太靈活:填充字節數組是獲取隨機數的唯一方法。要獲取整數,必須使用 BitConverter :
byte[] bytes = new byte [4];
rand.GetBytes (bytes);
int i = BitConverter.ToInt32 (bytes, 0);
System.Numerics.BitOperations 類(來自 .NET 6)公開了以下方法來幫助進行 base-2 操作:
IsPow2
如果數字是 2 的冪,則返回 true
LeadingZeroCount / TrailingZeroCount
返回格式為 base-2 32 位或 64 位無符號整數時前導零的數目
日志2
返回無符號整數的整數 base-2 日志
流行計數
返回無符號整數中設置為 1 的位數
向左旋轉/向右旋轉
執行按位左/右旋轉
RoundUpToPowerOf2
將無符號整數向上舍入到最接近的 2 的冪
在第 中,我們描述了 C# 的枚舉類型,并展示了如何組合成員、測試相等性、使用邏輯運算符和執行轉換。.NET 通過 System.Enum 類型擴展了 C# 對枚舉的支持。此類型有兩個角色:
意味著您可以將任何枚舉成員隱式強制轉換為 System.Enum 實例:
Display (Nut.Macadamia); // Nut.Macadamia
Display (Size.Large); // Size.Large
void Display (Enum value)
{
Console.WriteLine (value.GetType().Name + "." + value.ToString());
}
enum Nut { Walnut, Hazelnut, Macadamia }
enum Size { Small, Medium, Large }
System.Enum 上的靜態實用程序方法主要與執行轉換和獲取成員列表有關。
有三種方法可以表示枚舉值:
在本節中,我們將介紹如何在兩者之間進行轉換。
回想一下,顯式強制轉換在枚舉成員及其整數值之間進行轉換。如果您在編譯時知道枚舉類型,則顯式強制轉換是正確的方法:
[Flags]
public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
...
int i = (int) BorderSides.Top; // i == 4
BorderSides side = (BorderSides) i; // side == BorderSides.Top
您可以以相同的方式將 System.Enum 實例強制轉換為其整型。訣竅是先強制轉換為對象,然后轉換整型:
static int GetIntegralValue (Enum anyEnum)
{
return (int) (object) anyEnum;
}
這取決于您知道整型類型:我們剛剛編寫的方法如果傳遞一個整型很長的枚舉就會崩潰。若要編寫適用于任何整型枚舉的方法,可以采用以下三種方法之一。第一個是調用 Convert.ToDecimal :
static decimal GetAnyIntegralValue (Enum anyEnum)
{
return Convert.ToDecimal (anyEnum);
}
這是有效的,因為每個整數類型(包括 ulong )都可以轉換為十進制而不會丟失信息。第二種方法是調用 Enum.GetUnderlyingType 以獲取枚舉的整型,然后調用 Convert.ChangeType:
static object GetBoxedIntegralValue (Enum anyEnum)
{
Type integralType = Enum.GetUnderlyingType (anyEnum.GetType());
return Convert.ChangeType (anyEnum, integralType);
}
這將保留原始整型,如以下示例所示:
object result = GetBoxedIntegralValue (BorderSides.Top);
Console.WriteLine (result); // 4
Console.WriteLine (result.GetType()); // System.Int32
我們的 GetBoxedIntegralType 方法實際上不執行任何值轉換;相反,它會在另一種類型中相同的值。它將型服裝中的積分值轉換為服裝中的積分值。我們將在中進一步描述這一點。
第三種方法是調用 Format 或 ToString,指定“d”或“D”格式字符串。這為您提供了 enum 的整數值作為字符串,并且在編寫自定義序列化格式化程序時很有用:
static string GetIntegralValueAsString (Enum anyEnum)
{
return anyEnum.ToString ("D"); // returns something like "4"
}
Enum.ToObject 將整數值轉換為給定類型的枚舉實例:
object bs = Enum.ToObject (typeof (BorderSides), 3);
Console.WriteLine (bs); // Left, Right
這是以下內容的動態等效項:
BorderSides bs = (BorderSides) 3;
ToObject 被重載以接受所有整型以及對象。(后者適用于任何盒裝積分類型。
若要將枚舉轉換為字符串,可以調用靜態 Enum.Format 方法,也可以在實例上調用 ToString。每個方法都接受一個格式字符串,該字符串可以是“G”表示默認格式設置行為,“D”表示以字符串形式發出基礎整數值,“X”表示十六進制格式相同的字符串,或者“F”表示格式化沒有 Flags 屬性的枚舉的組合成員。我們在中列出了這些示例。
Enum.Parse 將字符串轉換為枚舉。它接受枚舉類型和可以包含多個成員的字符串:
BorderSides leftRight = (BorderSides) Enum.Parse (typeof (BorderSides),
"Left, Right");
可選的第三個參數允許您執行不區分大小寫的分析。如果未找到該成員,則會引發 ArgumentException。
Enum.GetValues 返回一個包含特定枚舉類型的所有成員的數組:
foreach (Enum value in Enum.GetValues (typeof (BorderSides)))
Console.WriteLine (value);
復合成員,例如 左右 = 左 |權利也包括在內。
Enum.GetNames 執行相同的函數,但返回一個數組。
在內部,CLR 通過反映枚舉類型中的字段來實現 GetValues 和 GetNames。緩存結果以提高效率。
枚舉的語義主要由編譯器強制執行。在 CLR 中,枚舉實例(未裝箱時)與其基礎整數值之間沒有運行時差異。此外,CLR 中的枚舉定義只是 System.Enum 的一個子類型,每個成員都有靜態積分類型字段。這使得枚舉的常規使用非常高效,運行時成本與整數的成本相匹配。
這種策略的缺點是枚舉可以提供但不的類型安全性。我們在中看到了一個例子:
[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
...
BorderSides b = BorderSides.Left;
b += 1234; // No error!
當編譯器無法執行驗證時(如本例所示),運行時沒有備份來引發異常。
我們所說的枚舉實例與其整數值之間沒有運行時差異的內容似乎與以下內容不一致:
[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
...
Console.WriteLine (BorderSides.Right.ToString()); // Right
Console.WriteLine (BorderSides.Right.GetType().Name); // BorderSides
鑒于運行時枚舉實例的性質,您會期望它打印 2 和 Int32 !其行為的原因歸結為一些更多的編譯時技巧。C# 在調用枚舉實例的虛擬方法(如 ToString 或 GetType)之前顯式枚舉實例。當枚舉實例被裝箱時,它會獲得引用其枚舉類型的運行時包裝。
Guid 結構表示全局唯一標識符:一個 16 字節的值,在生成時,幾乎可以肯定在世界上是唯一的。Guid 通常用于應用程序和數據庫中的各種鍵。有 2 個128或 3.4 × 1038獨特的吉德 .
靜態 Guid.NewGuid 方法生成一個唯一的 Guid:
Guid g = Guid.NewGuid ();
Console.WriteLine (g.ToString()); // 0d57629c-7d6e-4847-97cb-9e2fc25083fe
若要實例化現有值,請使用其中一個構造函數。兩個最有用的構造函數是:
public Guid (byte[] b); // Accepts a 16-byte array
public Guid (string g); // Accepts a formatted string
表示為字符串時,Guid 的格式為 32 位十六進制數字,第 8、12、16 和 20 位數字后帶有可選的連字符。整個字符串也可以選擇用括號或大括號括起來:
Guid g1 = new Guid ("{0d57629c-7d6e-4847-97cb-9e2fc25083fe}");
Guid g2 = new Guid ("0d57629c7d6e484797cb9e2fc25083fe");
Console.WriteLine (g1 == g2); // True
作為結構體,Guid 尊重值類型語義;因此,相等運算符在前面的示例中起作用。
ToByteArray 方法將 Guid 轉換為字節數組。
靜態 Guid.Empty 屬性返回一個空 Guid(全部為零)。這通常用于代替 null 。
到目前為止,我們假設 == 和 != 運算符是相等比較的全部內容。然而,平等問題更加復雜和微妙,有時需要使用額外的方法和接口。本節探討了相等的標準 C# 和 .NET 協議,特別關注兩個:
但在探索平等協議的細節以及如何定制它們之前,我們首先必須看看價值與參考平等的初步概念。
有兩種平等:
價值平等
從某種意義上說,兩個值是的。
參照相等
兩個引用引用完全相同。
除非被覆蓋:
實際上,值類型使用值相等(除非裝箱)。值相等的簡單演示是比較兩個數字:
int x = 5, y = 5;
Console.WriteLine (x == y); // True (by virtue of value equality)
一個更詳細的演示是比較兩個 DateTimeOffset 結構。以下打印 True,因為兩個 DateTimeOffset 引用,因此被視為等效:
var dt1 = new DateTimeOffset (2010, 1, 1, 1, 1, 1, TimeSpan.FromHours(8));
var dt2 = new DateTimeOffset (2010, 1, 1, 2, 1, 1, TimeSpan.FromHours(9));
Console.WriteLine (dt1 == dt2); // True
DateTimeOffset 是一個結構,其相等語義已得到調整。默認情況下,結構表現出一種稱為結構相等的特殊值相等,其中兩個值被視為相等,如果它們的所有成員都。(您可以通過創建一個結構并調用其 Equals 方法來查看這一點;稍后會詳細介紹。
默認情況下,引用類型顯示引用相等性。在下面的示例中,f1 和 f2 不相等,盡管它們的對象具有相同的內容:
class Foo { public int X; }
...
Foo f1 = new Foo { X = 5 };
Foo f2 = new Foo { X = 5 };
Console.WriteLine (f1 == f2); // False
相反,f3 和 f1 相等,因為它們引用同一個對象:
Foo f3 = f1;
Console.WriteLine (f1 == f3); // True
在本節的后面部分,我們將介紹如何引用類型以顯示值相等性。這方面的一個例子是系統命名空間中的 Uri 類:
Uri uri1 = new Uri ("http://www.linqpad.net");
Uri uri2 = new Uri ("http://www.linqpad.net");
Console.WriteLine (uri1 == uri2); // True
字符串類表現出類似的行為:
var s1 = "http://www.linqpad.net";
var s2 = "http://" + "www.linqpad.net";
Console.WriteLine (s1 == s2); // True
類型可以實現三種標準協議以進行相等:
此外,還有協議和 IStructuralEequalable 接口,我們將在第 中描述。
我們已經在許多示例中看到了標準 == 和 != 運算符如何執行相等/不等式比較。== 和 != 的微妙之處之所以出現,是因為它們是;因此,它們是靜態解析的(實際上,它們是作為靜態函數實現的)。因此,當您使用 == 或 != 時,C# 會根據決定哪種類型將執行比較,并且不會發生任何虛擬行為。這通常是可取的。在下面的示例中,編譯器將 == 硬連線到 int 類型,因為 x 和 y 都是 int :
int x = 5;
int y = 5;
Console.WriteLine (x == y); // True
但在下一個示例中,編譯器將 == 運算符連接到對象類型:
object x = 5;
object y = 5;
Console.WriteLine (x == y); // False
因為 object 是一個類(因此是一個引用類型),對象的 == 運算符使用來比較 x 和 y。結果為 false,因為 x 和 y 分別引用堆上的不同盒裝對象。
為了在前面的示例中正確等同 x 和 y,我們可以使用虛擬 Equals 方法。等于在 System.Object 中定義,因此適用于所有類型:
object x = 5;
object y = 5;
Console.WriteLine (x.Equals (y)); // True
等于在運行時根據對象的實際類型進行解析。在這種情況下,它調用 Int32 的 Equals 方法,該方法將應用于操作數,返回 true 。對于引用類型,Equals 默認執行引用相等比較;對于結構,Equals 通過對其每個字段調用 Equals 來執行結構比較。
您可能想知道為什么 C# 的設計者沒有通過使 == 虛擬從而在功能上與 Equals 相同來避免這個問題。原因有三:
從本質上講,設計的復雜性反映了情況的復雜性:平等的概念涵蓋了多種場景。
因此,Equals 適合以與類型無關的方式等同兩個對象。以下方法等同于任何類型的兩個對象:
public static bool AreEqual (object obj1, object obj2)
=> obj1.Equals (obj2);
但是,在一種情況下,此操作會失敗。如果第一個參數為 null ,則得到一個 NullReferenceException 。這是修復:
public static bool AreEqual (object obj1, object obj2)
{
if (obj1 == null) return obj2 == null;
return obj1.Equals (obj2);
}
或者,更簡潔地說:
public static bool AreEqual (object obj1, object obj2)
=> obj1 == null ? obj2 == null : obj1.Equals (obj2);
對象類提供了一個靜態幫助程序方法,該方法執行前面示例中 AreEqual 的工作。它的名稱是 Equals(就像虛擬方法一樣),但沒有沖突,因為它接受參數:
public static bool Equals (object objA, object objB)
這為編譯時類型未知的情況提供了一種 null 安全的相等比較算法:
object x = 3, y = 3;
Console.WriteLine (object.Equals (x, y)); // True
x = null;
Console.WriteLine (object.Equals (x, y)); // False
y = null;
Console.WriteLine (object.Equals (x, y)); // True
一個有用的應用程序是在編寫泛型類型時。如果對象,則以下代碼將無法編譯。等于替換為 == 或 != 運算符:
class Test <T>
{
T _value;
public void SetValue (T newValue)
{
if (!object.Equals (newValue, _value))
{
_value = newValue;
OnValueChanged();
}
}
protected virtual void OnValueChanged() { ... }
}
此處禁止使用運算符,因為編譯器無法綁定到未知類型的靜態方法。
實現此比較的更精細方法是使用 EqualityComparer<T> 類。這樣做的好處是可以防止拳擊:
if (!EqualityComparer<T>.Default.Equals (newValue, _value))
我們將在第更詳細地討論EqualityComparer<T>(參見)。
有時,您需要強制引用相等比較。靜態對象。ReferenceEquals方法就是這樣做的:
Widget w1 = new Widget();
Widget w2 = new Widget();
Console.WriteLine (object.ReferenceEquals (w1, w2)); // False
class Widget { ... }
您可能希望這樣做,因為 Widget 可以覆蓋虛擬 Equals 方法,例如 w1。等于 (w2) 將返回 true 。此外,Widget 可能會重載 == 運算符,以便 w1==w2 也會返回 true。在這種情況下,調用對象。引用等于保證正常的引用相等語義。
強制引用相等比較的另一種方法是將值強制轉換為對象,然后應用 == 運算符。
調用對象的結果。等于是它強制對值類型進行裝箱。這在對性能高度敏感的方案中是不可取的,因為與實際比較相比,裝箱相對昂貴。在 C# 2.0 中引入了一個解決方案,其中包含 IEquatable<T> 接口:
public interface IEquatable<T>
{
bool Equals (T other);
}
這個想法是IEquatable<T>在實現時,給出的結果與調用對象的虛擬Equals方法相同,但更快。大多數基本的.NET類型實現IEquatable<T>。您可以使用 IEquatable<T> 作為泛型類型的約束:
class Test<T> where T : IEquatable<T>
{
public bool IsEqual (T a, T b)
{
return a.Equals (b); // No boxing with generic T
}
}
如果我們刪除泛型約束,類仍將編譯,但 a.Equals(b) 將綁定到較慢的對象。等于(假設 T 是值類型,則速度較慢)。
我們之前說過,有時 == 和 Equals 應用不同的相等定義很有用。例如:
double x = double.NaN;
Console.WriteLine (x == x); // False
Console.WriteLine (x.Equals (x)); // True
雙精度類型的 == 運算符強制要求一個 NaN 永遠不能等于其他任何東西——即使是另一個 NaN。從數學角度來看,這是最自然的,它反映了底層 CPU 行為。然而,平等方法必須應用平等;換句話說:
x.等于 (x) 必須返回 true。
集合和字典依賴于 Equals 以這種方式行事;否則,他們找不到以前存儲的項目。
對于值類型來說,讓 Equals 和 == 應用不同的相等定義實際上非常罕見。更常見的方案是引用類型;當作者自定義 Equals 以便它執行值相等,同時讓 == 執行(默認)參照相等時,就會發生這種情況。StringBuilder 類正是這樣做的:
var sb1 = new StringBuilder ("foo");
var sb2 = new StringBuilder ("foo");
Console.WriteLine (sb1 == sb2); // False (referential equality)
Console.WriteLine (sb1.Equals (sb2)); // True (value equality)
現在讓我們看看如何自定義相等性。
回想一下默認的相等比較行為:
進一步:
有時,在編寫類型時重寫此行為是有意義的。這樣做有兩種情況:
當 == 和 Equals 的默認行為對您的類型不自然且時,更改相等的含義是有意義的。一個例子是 DateTimeOffset ,一個具有兩個私有字段的結構:UTC DateTime 和一個數字整數偏移量。如果要編寫此類型,則可能需要確保相等比較僅考慮 UTC 日期時間字段,而不考慮偏移量字段。另一個示例是支持 NaN 值的數字類型,例如浮點數和雙精度數。如果您自己實現此類類型,則需要確保在相等比較中支持 NaN 比較邏輯。
對于類,有時更自然地將相等作為默認值,而不是。對于保存簡單數據段的小型類,例如System.Uri(或System.String),通常就是這種情況。
對于記錄,編譯器會自動實現結構相等(通過比較每個字段)。但是,有時這將包括您不想比較的字段,或需要特殊比較邏輯的對象,例如集合。使用記錄覆蓋相等的過程略有不同,因為記錄遵循一種特殊的模式,該模式旨在很好地配合其繼承規則。
結構的默認比較算法相對較慢。通過覆蓋 Equals 來接管此過程可以將性能提高五倍。重載 == 運算符并實現 IEquatable<T> 允許無框相等比較,這可以再次將事情加快五倍。
重寫引用類型的相等語義不會影響性能。引用相等比較的默認算法已經非常快,因為它只是比較兩個 32 位或 64 位引用。
自定義相等還有另一種相當奇特的情況,那就是改進結構的哈希算法以獲得更好的哈希表中的性能。這是因為相等比較和哈希在臀部連接在一起。我們稍后會檢查哈希。
要覆蓋類或結構的相等性,請執行以下步驟:
該過程與記錄不同(且更簡單),因為編譯器已經根據自己的特殊模式覆蓋了相等方法和運算符。如果要進行干預,則必須符合此模式,這意味著使用如下所示的簽名編寫 Equals 方法:
record Test (int X, int Y)
{
public virtual bool Equals (Test t) => t != null && t.X == X && t.Y == Y;
}
請注意,Equals 是虛擬的(不是覆蓋的),并接受實際的記錄類型(在本例中為 test,而不是對象)。編譯器將識別您的方法具有“正確”的簽名,并將其修補。
您還必須覆蓋 GetHashCode() ,就像使用類或結構一樣。你不需要(也不應該)重載!=和==,或者實現IEquatable<T>,因為這已經為你完成了。
System.Object - 其成員占用空間很小 - 定義了一個具有專門和狹隘目的的方法,這似乎很奇怪。GetHashCode 是 Object 中的一個虛擬方法,符合此描述;它的存在主要是為了滿足以下兩種類型:
System.Collections.Hashtable
System.Collections.Generic.Dictionary<TKey,TValue>
這些是 - 每個元素都有一個用于存儲和檢索的鍵的集合。哈希表應用非常具體的策略,根據元素的鍵有效地分配元素。這要求每個密鑰都有一個 Int32 編號或。每個鍵的哈希代碼不必是唯一的,但應盡可能多樣,以獲得良好的哈希表性能。哈希表被認為足夠重要,因此 GetHashCode 在 System.Object 中定義,因此每種類型都可以發出哈希代碼。
我們將在第中詳細描述哈希表。
引用和值類型都有 GetHashCode 的默認實現,這意味著您不需要覆蓋此方法—— Equals。(如果你覆蓋GetHashCode,你幾乎肯定會希望也覆蓋Equals。
以下是覆蓋對象的其他規則。獲取哈希碼 :
為了在哈希表中獲得最佳性能,您應該編寫 GetHashCode,以便最大程度地減少兩個不同值返回相同哈希代碼的可能性。這就產生了在結構上覆蓋 Equals 和 GetHashCode 的第三個原因,即提供比默認值更有效的哈希算法。結構的默認實現由運行時自行決定,并且可以基于結構中的每個字段。
相比之下,的默認 GetHashCode 實現基于對象令牌,該令牌對于 CLR 當前中的每個實例都是唯一的。
如果對象的哈希代碼在作為鍵添加到字典后發生更改,則該對象將無法再在字典中訪問。您可以通過基于不可變字段進行哈希代碼計算來搶占這種情況。
我們提供了一個完整的示例,說明如何很快覆蓋GetHashCode。
對象的公理。等于如下:
除了覆蓋 Equals 之外,您還可以選擇重載相等和不相等運算符。這幾乎總是通過結構完成的,因為不這樣做的后果是 == 和 != 運算符根本不適用于您的類型。
對于類,有兩種方法可以繼續:
第一種方法在自定義類型(尤其是類型)中最常見。它確保您的類型遵循 == 和 != 應與引用類型表現出引用相等的期望,這可以防止混淆使用者。我們之前看到過一個例子:
var sb1 = new StringBuilder ("foo");
var sb2 = new StringBuilder ("foo");
Console.WriteLine (sb1 == sb2); // False (referential equality)
Console.WriteLine (sb1.Equals (sb2)); // True (value equality)
第二種方法對于使用者永遠不需要引用相等的類型是有意義的。這些通常是不可變的(例如字符串和 System.Uri 類),有時是結構體的良好候選項。
雖然有可能重載 != 這樣它的意思不是!(==) ,這在實踐中幾乎從未做過,除了比較浮點數等情況。南。
為了完整起見,在覆蓋等于時實現 IEquatable<T> 也很好。其結果應始終與重寫對象的 Equals 方法的結果匹配。如果按照稍后的示例構建 Equals 方法實現,則實現 IEquatable<T> 無需編程成本。
想象一下,我們需要一個結構來表示寬度和高度可互換的區域。換句話說,5 × 10 等于 10 × 5。(這種類型適用于排列矩形形狀的算法。
下面是完整的代碼:
public struct Area : IEquatable <Area>
{
public readonly int Measure1;
public readonly int Measure2;
public Area (int m1, int m2)
{
Measure1 = Math.Min (m1, m2);
Measure2 = Math.Max (m1, m2);
}
public override bool Equals (object other)
=> other is Area a && Equals (a); // Calls method below
public bool Equals (Area other) // Implements IEquatable<Area>
=> Measure1 == other.Measure1 && Measure2 == other.Measure2;
public override int GetHashCode()
=> HashCode.Combine (Measure1, Measure2);
public static bool operator == (Area a1, Area a2) => a1.Equals (a2);
public static bool operator != (Area a1, Area a2) => !a1.Equals (a2);
}
從 C# 10 開始,可以使用記錄縮短該過程。通過將其聲明為 記錄結構 ,您可以刪除構造函數后面的所有代碼。
在實現 GetHashCode 時,我們使用了 。NET的HashCode.Combation函數來生成復合哈希碼。(在該函數存在之前,一種流行的方法是將每個值乘以某個素數,然后將它們相加。
下面是 Area 結構的演示:
Area a1 = new Area (5, 10);
Area a2 = new Area (10, 5);
Console.WriteLine (a1.Equals (a2)); // True
Console.WriteLine (a1 == a2); // True
如果希望類型僅針對特定方案采用不同的相等語義,則可以使用可插入的 IEqualityComparer 。這與標準集合類結合使用時特別有用,我們將在下一章中對其進行描述。
除了定義相等的標準協議外,C# 和 .NET 還定義了兩個標準協議,用于確定一個對象相對于另一個對象的順序:
IComparable 接口由通用排序算法使用。在下面的示例中,靜態 Array.Sort 方法之所以有效,是因為 System.String 實現了 IComparable 接口:
string[] colors = { "Green", "Red", "Blue" };
Array.Sort (colors);
foreach (string c in colors) Console.Write (c + " "); // Blue Green Red
<運算符和>運算符更專用,它們主要用于數值類型。因為它們是靜態解析的,所以它們可以轉換為高效的字節碼,適用于計算密集型算法。
.NET 還通過 IComparer 接口提供可插入的排序協議。我們將在第的最后一節中描述這些內容。
可比較接口定義如下:
public interface IComparable { int CompareTo (object other); }
public interface IComparable<in T> { int CompareTo (T other); }
這兩個接口表示相同的功能。對于值類型,泛型類型安全接口比非泛型接口更快。在這兩種情況下,CompareTo 方法的工作方式如下:
例如:
Console.WriteLine ("Beck".CompareTo ("Anne")); // 1
Console.WriteLine ("Beck".CompareTo ("Beck")); // 0
Console.WriteLine ("Beck".CompareTo ("Chris")); // -1
大多數基本類型都實現了這兩個可比較接口。這些接口有時也會在編寫自定義類型時實現。我們稍后提供一個例子。
考慮一種既覆蓋等于又實現 IComparable 接口的類型。你會期望當 Equals 返回 true 時,CompareTo 應該返回 0 。你是對的。但這里有一個問題:
當 Equals 返回 false 時,CompareTo 可以返回它喜歡的內容(只要它內部一致)!
換句話說,相等可以比比較“挑剔”,但反之則不然(違反這一點,排序算法就會中斷)。因此,CompareTo可以說,“所有對象都是平等的”,而Equals則說,“但有些對象比其他對象更平等!
一個很好的例子是 System.String 。字符串的 Equals 方法和 == 運算符使用號比較,它比較每個字符的 Unicode 點值。然而,它的CompareTo方法使用了一種不太挑剔的文化性比較。例如,在大多數計算機上,字符串“?”和“ǖ”根據等于不同,但根據比較相同。
在第 中,我們討論了可插拔排序協議 IComparer,它允許您在排序或實例化排序集合時指定替代排序算法。自定義 IComparer 可以進一步擴展 CompareTo 和 Equals 之間的差距 — 例如,不區分大小寫的字符串比較器在比較“A”和“a”時將返回 0。然而,反向規則仍然適用:CompareTo 永遠不能比 Equals 更挑剔。
在自定義類型中實現 IComparable 接口時,可以通過編寫 CompareTo 的第一行來避免與此規則沖突,如下所示:
if (Equals (other)) return 0;
之后,它可以返回它喜歡的東西,只要它是一致的!
某些類型定義<運算符和>運算符。例如:
bool after2010 = DateTime.Now > new DateTime (2010, 1, 1);
您可以期望<運算符和>運算符在實現時與 IComparable 接口在功能上保持一致。這是跨 .NET 的標準做法。
在<和>過載時實現 IComparable 接口也是標準做法,但反之則不然。事實上,大多數實現 IComparable 的 .NET 類型重載 < 和 > 。這與相等的情況不同,在覆蓋等于時重載 == 是正常的。
通常,只有在以下情況下,>和<才會重載:
System.String 不滿足最后一點:字符串比較的結果可能因語言而異。因此,字符串不支持>和<運算符:
bool error = "Beck" > "Anne"; // Compile-time error
在以下表示音符的結構中,我們實現了 IComparable 接口以及重載<和>運算符。為了完整起見,我們還覆蓋了 Equals / GetHashCode 并重載 == 和 != :
public struct Note : IComparable<Note>, IEquatable<Note>, IComparable
{
int _semitonesFromA;
public int SemitonesFromA { get { return _semitonesFromA; } }
public Note (int semitonesFromA)
{
_semitonesFromA = semitonesFromA;
}
public int CompareTo (Note other) // Generic IComparable<T>
{
if (Equals (other)) return 0; // Fail-safe check
return _semitonesFromA.CompareTo (other._semitonesFromA);
}
int IComparable.CompareTo (object other) // Nongeneric IComparable
{
if (!(other is Note))
throw new InvalidOperationException ("CompareTo: Not a note");
return CompareTo ((Note) other);
}
public static bool operator < (Note n1, Note n2)
=> n1.CompareTo (n2) < 0;
public static bool operator > (Note n1, Note n2)
=> n1.CompareTo (n2) > 0;
public bool Equals (Note other) // for IEquatable<Note>
=> _semitonesFromA == other._semitonesFromA;
public override bool Equals (object other)
{
if (!(other is Note)) return false;
return Equals ((Note) other);
}
public override int GetHashCode() => _semitonesFromA.GetHashCode();
public static bool operator == (Note n1, Note n2) => n1.Equals (n2);
public static bool operator != (Note n1, Note n2) => !(n1 == n2);
}
靜態控制臺類處理基于控制臺的應用程序的標準輸入/輸出。在命令行(控制臺)應用程序中,輸入通過 讀取 、 讀取鍵 和 讀取線 來自鍵盤,輸出通過寫入和寫入線進入文本窗口。您可以使用屬性控制窗口的位置和尺寸 窗口左 、 窗口頂部 、 窗口高度 和 窗口寬度 。還可以更改“背景色”和“前景色”屬性,并使用“光標左”、“光標頂部”和“光標大小”屬性操作光標:
Console.WindowWidth = Console.LargestWindowWidth;
Console.ForegroundColor = ConsoleColor.Green;
Console.Write ("test... 50%");
Console.CursorLeft -= 3;
Console.Write ("90%"); // test... 90%
Write 和 WriteLine 方法被重載以接受復合格式字符串(請參閱中的 String.Format)。但是,這兩種方法都不接受格式提供程序,因此您只能使用CultureInfo.CurrentCulture。(當然,解決方法是顯式調用字符串。格式 .)
屬性返回一個 TextWriter 。將 Console.Out 傳遞給需要 TextWriter 的方法是一種有用的方法,可以讓該方法寫入控制臺以進行診斷。
您還可以通過 SetIn 和 SetOut 方法重定向控制臺的輸入和輸出流:
// First save existing output writer:
System.IO.TextWriter oldOut = Console.Out;
// Redirect the console's output to a file:
using (System.IO.TextWriter w = System.IO.File.CreateText
("e:\\output.txt"))
{
Console.SetOut (w);
Console.WriteLine ("Hello world");
}
// Restore standard console output
Console.SetOut (oldOut);
在第 中,我們描述了流和文本編寫器的工作原理。
在Visual Studio下運行WPF或Windows Forms應用程序時,控制臺的輸出會自動重定向到Visual Studio的輸出窗口(在調試模式下)。這可以使 Console.Write 用于診斷目的;盡管在大多數情況下,System.Diagnostics 命名空間中的 Debug 和 Trace 類更合適(請參閱)。
靜態 System.Environment 類提供了一系列有用的屬性:
文件和文件夾
當前目錄 , 系統目錄 , 命令行
計算機和操作系統
機器名稱 , 處理器計數 , OSVersion , 換行符
用戶登錄
用戶名 , 用戶交互 , 用戶域名
診斷
TickCount , StackTrace , WorkingSet , Version
您可以通過調用 獲取文件夾路徑 ;我們將在第 的中對此進行描述。
您可以使用以下三種方法訪問操作系統環境變量(在命令提示符下鍵入“set”時看到的內容):GetEnvironmentVariable 、GetEnvironmentVariables 和 SetEnvironmentVariable 。
ExitCode 屬性允許您設置返回代碼(當從命令或批處理文件調用程序時),并且 FailFast 方法立即終止程序,而不執行清理。
可用于 Windows 應用商店應用的環境類僅提供有限數量的成員(處理器計數、換行符和快速故障)。
System.Diagnostics中的進程類允許您啟動新進程。(在第中,我們描述了如何使用它與計算機上運行的其他進程進行交互)。
出于安全原因,Process 類不適用于 Windows 應用商店應用,并且不能啟動任意進程。相反,您必須使用 Windows.System.Launcher 類來“啟動”您有權訪問的 URI 或文件。例如:
Launcher.LaunchUriAsync (new Uri ("http://albahari.com"));
var file = await KnownFolders.DocumentsLibrary
.GetFileAsync ("foo.txt");
Launcher.LaunchFileAsync (file);
這將使用與 URI 方案或文件擴展名關聯的任何程序打開 URI 或文件。您的程序必須位于前臺才能正常工作。
靜態 Process.Start 方法有幾個重載;最簡單的接受帶有可選參數的簡單文件名:
Process.Start ("notepad.exe");
Process.Start ("notepad.exe", "e:\\file.txt");
最靈活的重載接受 ProcessStartInfo 實例。有了這個,您可以捕獲和重定向啟動進程的輸入、輸出和錯誤輸出(如果您將 UseShellExecute 保留為 false)。下面捕獲調用 ipconfig 的輸出:
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c ipconfig /all",
RedirectStandardOutput = true,
UseShellExecute = false
};
Process p = Process.Start (psi);
string result = p.StandardOutput.ReadToEnd();
Console.WriteLine (result);
如果不重定向輸出,Process.Start 將并行執行程序到調用方。如果要等待新進程完成,可以在進程對象上調用 WaitForExit,并提供可選的超時。
使用 UseShellExecute false(.NET 中的默認值),可以捕獲標準輸入、輸出和錯誤流,然后通過 StandardInput 、StandardOutput 和 StandardError 屬性寫入/讀取這些流。
當您需要重定向標準輸出和標準錯誤流時,會出現一個困難,因為您通常無法知道從每個錯誤流中讀取數據的順序(因為您事先不知道數據將如何交錯)。解決方案是一次從兩個流中讀取,您可以通過異步讀取(至少)其中一個流具體操作方法如下:
以下方法在捕獲輸出流和錯誤流的同時運行可執行文件:
(string output, string errors) Run (string exePath, string args = "")
{
using var p = Process.Start (new ProcessStartInfo (exePath, args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
});
var errors = new StringBuilder ();
// Read from the error stream asynchronously...
p.ErrorDataReceived += (sender, errorArgs) =>
{
if (errorArgs.Data != null) errors.AppendLine (errorArgs.Data);
};
p.BeginErrorReadLine ();
// ...while we read from the output stream synchronously:
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
return (output, errors.ToString());
}
在.NET 5+(和.NET Core)中,UseShellExecute的默認值為false,而在.NET Framework中,為true。由于這是一項重大更改,因此在從 .NET Framework 移植代碼時,值得檢查對 Process.Start 的所有調用。
UseShellExecute 標志更改 CLR 啟動進程的方式。使用 UseShellExecute true,您可以執行以下操作:
缺點是無法重定向輸入或輸出流。如果需要這樣做(在啟動文件或文檔時),解決方法是將 UseShellExecute 設置為 false 并使用“/c”開關調用命令行進程 (cmd.exe),就像我們之前調用 時所做的那樣。
在 Windows 下,UseShellExecute 指示 CLR 使用 Windows 函數而不是 函數。在 Linux 下,UseShellExecute 指示 CLR 調用 、 或 。
靜態 System.AppContext 類公開了兩個有用的屬性:
此外,AppContext 類還管理布爾值的全局字符串鍵字典,旨在為庫編寫器提供一種標準機制,允許使用者打開或關閉新功能。這種非類型化方法對于您希望對大多數用戶保持未記錄的實驗性功能是有意義的。
庫的使用者請求啟用功能,如下所示:
AppContext.SetSwitch ("MyLibrary.SomeBreakingChange", true);
然后,該庫中的代碼可以檢查該開關,如下所示:
bool isDefined, switchValue;
isDefined = AppContext.TryGetSwitch ("MyLibrary.SomeBreakingChange",
out switchValue);
如果開關未定義,則 TryGetSwitch 返回 false;這使您可以區分未定義的開關和值設置為 false 的開關(如有必要)。
具有諷刺意味的是,TryGetSwitch 的設計說明了如何不編寫 API。out 參數是不必要的,該方法應返回一個可為空的布爾值,其值為 true、false 或 null 表示未定義。然后,這將啟用以下用途:
bool switchValue = AppContext.GetSwitch ("...") ?? false;
機器之心報道
編輯:小舟、馬梓文
平替不止模型,RLHF也有平替了。
2 月底,Meta 開源了一個大模型系列 LLaMA(直譯為羊駝),參數量從 70 億到 650 億不等,被稱為 Meta 版 ChatGPT 的雛形。之后斯坦福大學、加州大學伯克利分校等機構紛紛在 LLaMA 的基礎上進行「二創」,陸續推出了 Alpaca、Vicuna 等多個開源大模型,一時間「羊駝」成為 AI 圈頂流。開源社區構建的這些類 ChatGPT 模型迭代速度非常快,并且可定制性很強,被稱為 ChatGPT 的開源平替。
然而,ChatGPT 之所以能在文本理解、生成、推理等方面展現出強大的能力,是因為 OpenAI 為 ChatGPT 等大模型使用了新的訓練范式 ——RLHF (Reinforcement Learning from Human Feedback) ,即以強化學習的方式依據人類反饋優化語言模型。使用 RLHF 方法,大型語言模型可與人類偏好保持對齊,遵循人類意圖,最小化無益、失真或偏見的輸出。但 RLHF 方法依賴于大量的人工標注和評估,通常需要數周時間、花費數千美元收集人類反饋,成本高昂。
現在,推出開源模型 Alpaca 的斯坦福大學又提出了一種模擬器 ——AlpacaFarm(直譯為羊駝農場)。AlpacaFarm 能在 24 小時內僅用約 200 美元復制 RLHF 過程,讓開源模型迅速改善人類評估結果,堪稱 RLHF 的平替。
AlpacaFarm 試圖快速、低成本地開發從人類反饋中學習的方法。為了做到這一點,斯坦福的研究團隊首先確定了研究 RLHF 方法的三個主要困難:人類偏好數據的高成本、缺乏可信賴的評估、缺乏參考實現。
為了解決這三個問題,AlpacaFarm 構建了模擬注釋器、自動評估和 SOTA 方法的具體實現。目前,AlpacaFarm 項目代碼已開源。
GitHub 地址:https://github.com/tatsu-lab/alpaca_farm
論文地址:https://tatsu-lab.github.io/alpaca_farm_paper.pdf
如下圖所示,研究人員可以使用 AlpacaFarm 模擬器快速開發從人類反饋數據中學習的新方法,也能將已有 SOTA 方法遷移到實際的人類偏好數據上。
模擬注釋器
AlpacaFarm 基于 Alpaca 數據集的 52k 指令構建,其中 10k 指令用于微調基本的指令遵循模型,剩余的 42k 指令用于學習人類偏好和評估,并且大部分用于從模擬注釋器中學習。該研究針對 RLHF 方法的注釋成本、評估和驗證實現三大挑戰,逐一提出解決方法。
首先,為了減少注釋成本,該研究為可訪問 API 的 LLM(如 GPT-4、ChatGPT)創建了 prompt,使得 AlpacaFarm 能夠模擬人類反饋,成本僅為 RLHF 方法收集數據的 1/45。該研究設計了一種隨機的、有噪聲的注釋方案,使用 13 種不同的 prompt,從多個 LLM 提取出不同的人類偏好。這種注釋方案旨在捕獲人類反饋的不同方面,如質量判斷、注釋器之間的變化性和風格偏好。
該研究通過實驗表明 AlpacaFarm 的模擬是準確的。當研究團隊使用 AlpacaFarm 訓練和開發方法時,這些方法與使用實際人類反饋訓練和開發的相同方法排名非常一致。下圖顯示了由 AlpacaFarm 模擬工作流和人類反饋工作流產生的方法在排名上的高度相關性。這一特性至關重要,因為它說明從模擬中得出的實驗結論在實際情況下也有可能成立。
除了方法層面的相關性,AlpacaFarm 模擬器還可以復制獎勵模型過度優化等定性現象,但以此針對代理獎勵(surrogate reward)的持續 RLHF 訓練可能會損害模型性能。下圖是在人類反饋 (左) 和 AlpacaFarm (右) 兩種情況下的該現象,我們可以發現 AlpacaFarm 最初捕獲了模型性能提升的正確定性行為,然后隨著 RLHF 訓練的持續,模型性能下降。
評估
在評估方面,研究團隊使用與 Alpaca 7B 的實時用戶交互作為指導,并通過結合幾個現有公共數據集來模擬指令分布,包括 self-instruct 數據集、anthropic helpfulness 數據集和 Open Assistant、Koala 和 Vicuna 的評估集。使用這些評估指令,該研究比較了 RLHF 模型與 Davinci003 模型的響應(response)情況,并使用一個分值度量 RLHF 模型響應更優的次數,并將這個分值稱為勝率(win-rate)。如下圖所示,在該研究的評估數據上進行的系統排名量化評估表明:系統排名和實時用戶指令是高度相關的。這一結果說明,聚合現有的公開數據能實現與簡單真實指令相近的性能。
參考方法
對于第三個挑戰 —— 缺少參考實現,研究團隊實現并測試了幾種流行的學習算法 (如 PPO、專家迭代、best-of-n 采樣)。研究團隊發現在其他領域有效的更簡單方法并不比該研究最初的 SFT 模型更好,這表明在真實的指令遵循環境中測試這些算法是非常重要的。
根據人工評估,PPO 算法被證明是最有效的,它將模型與 Davinci003 相比的勝率從 44% 提高到 55%,甚至超過了 ChatGPT。
這些結果表明,PPO 算法在為模型優化勝率方面是非常有效的。需要注意的是,這些結果是特定于該研究的評估數據和注釋器得出的。雖然該研究的評估指令代表了實時用戶指令,但它們可能無法涵蓋更具有挑戰性的問題,并且并不能確定有多少勝率的改進來源于利用風格偏好,而不是事實性或正確性。例如,該研究發現 PPO 模型產生的輸出要長得多,并且通常為答案提供更詳細的解釋,如下圖所示:
總的來說,使用 AlpacaFarm 在模擬偏好上訓練模型能夠大幅改善模型的人類評估結果,而不需要讓模型在人類偏好上重新訓練。雖然這種遷移過程比較脆弱,并且在效果上仍略遜于在人類偏好數據上重新訓練模型。但能在 24 小時內,僅用 200 美元就復制出 RLHF 的 pipeline,讓模型迅速提升人類評估性能,AlpacaFarm 這個模擬器還是太香了,是開源社區為復刻 ChatGPT 等模型的強大功能做出的又一努力。
參考鏈接:https://crfm.stanford.edu/2023/05/22/alpaca-farm.html