控制流程和轉換類型
本章的內容主要包括編寫代碼、對變量執行簡單的操作、做出決策、重復執行語句塊、將變量或表達式值從一種類型轉換為另一種類型、處理異常以及在數值變量中檢查溢出。
本章涵蓋以下主題:
- 操作變量
- 理解選擇語句
- 理解迭代語句
- 類型轉換
- 處理異常
- 檢查溢出
3.1操作變量
運算符可將簡單的操作(如加法和乘法)應用於操作數(如變量和字面值)。它們通常返回一個新值,作為分配給變量的操作的結果。
大多數運算符是二元的,這意味着它們可以處理兩個操作數,如下所示:
var resultOfOperation= onlyOperand operator;
var resultOfOperation2=operator onlyOperand;
一元運算符可用於遞增操作以及檢索類型或大小(以字節為單位)。如下所示:
int x=5;
int incrementedByOne=x++;
int incrementedByOneAgain=++x;
Type theTypeOfAnInteger=typeof(int);
int howManyBytesInAnInteger=sizeof(int);
三元運算符則作用於三個操作數,如下所示:
var resultOfOperation=firstOperand firstOperator
secondOperand secondOperator=thirdOperand;
3.1.1一元運算符
有兩個常用的一元運算符,它們可用於遞增(++)或遞減(--)數字。
(1)如果完成了前面的章節,那么user文件夾中應該已經由了Code文件夾。如果沒有,就創建Code文件夾。
(2)在Code文件夾中創建一個名為Chapter03的文件夾。
(3)啟動Visual Studio Code,關閉任何打開的工作區或文件夾。
(4)將當前工作區保存在Chapter03文件夾中,名為Chapter03.code-workspace。
(5)創建一個Operators的新文件夾,並添加到Chapter03工作區。
(6)導航到Terminal|NEW Terminal。
(7)在終端窗口中輸入命令,從而在Operators文件夾中創建新的控制台應用程序。
(8)打開Program.cs
(9)靜態導入System.Console名稱空間。
(10)在Main方法中,聲明兩個名為a和b的整型變量,將a設置為3,在將結果賦值給b的同時增加a,然后輸出它們的值,如下所示:
int a=3;
int b=a++;
Console.WriteLine($"a is {a},b is {b}");
(11)在運行控制台程序應用程序之前,問自己一個問題:當輸出時,b的值是多少?考慮到這一點后,運行控制台應用程序,並將預測結果與實際結果進行比較,如下所示:
a is 4, b is 3
變量b的值為3,因為++運算符在賦值之后執行;這稱為后綴運算符。如果需要在賦值之前遞增,那么可以使用前綴運算符。
(12)復制並粘貼語句,然后修改它們以重命名變量,並使用前綴運算符,如下所示:
int c=3;
int d=++c;
Console.WriteLine($"c is {c}, d is {d}");
(13)重新運行控制台應用程序並觀察結果,輸出如下所示:
a is 4,b is 3
c is 4, d is 4
由於遞增、遞減運算符與賦值運算符在前綴和后綴方面容易讓人混淆。Swift編程語言的設計者決定在Swift3 中取消對遞增、遞減運算符的支持。建議在C#中不要將++和--運算符與賦值運算符=結合使用。可將操作作為單獨的部件執行。
3.1.2 二元算術運算符
遞增和遞減運算符都是一元算術運算符。其他算術運算符通常是二元的,允許對兩個數字執行算術運算。
(1)將如下語句添加到Main方法的底部,對兩個著型變量e和f進行聲明並賦值,然后對這兩個變量執行5種常見的二元算術運算:
int e=11;
int f=3;
Console.WriteLine($"e is {e}, f is {f}");
Console.WriteLine($"e+f={e+f}");
Console.WriteLine($"e-f={e-f}");
Console.WriteLine($"e*f={e*f}");
Console.WriteLine($"e/f={e/f}");
Console.WriteLine($"e%f={e%f}");
(2)重新運行控制台應用程序並觀察結果,輸出如下所示:
e is 11, f is 3
e+f=14
e-f=8
e*f=33
e/f=3
e%f=2
為了理解將除法/和取模%運算符應用到整數時的i情況,需要回想一下小學課程。假設有11顆糖果和3名小朋友。怎么把這些糖果分給這些小朋友呢?可以給每個小朋友分3顆糖果,還剩下兩顆。剩下的這兩顆糖果是模數,也稱為余數。如果有12顆糖果,那么每個向朋友正好可以分得4顆。所以余數是0。
(3)添加如下語句,聲明名為g的double變量並賦值,以顯示整數和實數之差:
double g=11.0;
Console.WriteLine($"g is {g:N1}, f is {f}");
Console.WriteLine($"g/f={g/f}");
(4)運行控制台程序並觀察結果,輸出如下所示:
g is 11.0, f is 3
g/f=3.6666666666666665
如果第一個操作是浮點數,比如變量g,值為11.0,那么除法運算符也將返回一個浮點數(比如3.6666666666666665)而不是整數。
3.1.3賦值運算符
前面使用了最常用的賦值運算符=。
為了使代碼更加簡潔,可以把賦值運算符和算術運算符等其他運算符結合起來,如下所示:
int p=6;
Console.WriteLine(p+=3); //equivalent to p=p+3;
Console.WriteLine(p-=3); //equivalent to p=p-3;
Console.WriteLine(p*=3); //equivalent to p=p*3;
Console.WriteLine(p/=3); //equivalent to p=p/3;
3.1.4 邏輯運算符
邏輯運算符對布爾值進行操作,因此它們返回true或false。下面研究一下用於操作兩個布爾值的二元邏輯操作符。
(1)創建一個新的文件夾名為BooleanOperators的控制台應用程序,並將它們添加到Chapter03工作區。記得使用Command Palette選擇BooleanOperators作為當前項目。
(2)在Program.cs的Main方法中添加語句以聲明兩個布爾變量,它們的值分別為true和false,然后輸出真值表,顯示應用AND、OR和XOR(exclusive OR)邏輯運算符之后的結果,如下所示:
bool a=true;
bool b=false;
Console.WriteLine($"AND | a | b ");
Console.WriteLine($"a |{a&a,-5}|{a&b,-5}");
Console.WriteLine($"b |{b&a,-5}|{b&b,-5}");
Console.WriteLine();
Console.WriteLine($"OR | a | b ");
Console.WriteLine($"a |{a|a,-5}|{a|b,-5}");
Console.WriteLine($"b |{b|a,-5}|{b|b,-5}");
Console.WriteLine();
Console.WriteLine($"XOR | a | b ");
Console.WriteLine($"a |{a^a,-5}|{a^b,-5}");
Console.WriteLine($"b |{b^a,-5}|{b^b,-5}");
對於AND邏輯運算符&,如果結果為true,那么兩個操作數都必須為true。對於OR邏輯運算符|,如果結果為true,那么操作數可以為true。對於XOR邏輯運算符^,如果結果為true,那么任何一個操作數都可以為true(但不能2個都是true)。
3.1.5條件邏輯運算符
條件邏輯運算符類似於邏輯運算符,但需要使用兩個符號而不是一個符號。例如,需要使用&&而不是&,以及使用||而不是|。
第四章詳細介紹函數,但是現在需要簡單介紹一下函數以解釋條件邏輯運算符(也稱為短路布爾運算符)。
函數執行語句,然后返回一個值。這個值可以是布爾值,如true,從而在布爾操作中使用。
(1)在Main方法之后聲明一個函數,用於向控制台寫入消息並返回true,如下所示:
private static bool DoStuff()
{
Console.WriteLine("I am doing some stuff.");
return true;
}
(2)在Main方法的底部,對變量a和變量b以及函數的調用結果執行AND操作,如下所示:
Console.WriteLine($"a & DoStuff()={a&DoStuff()}");
Console.WriteLine($"b & DoStuff()={b&DoStuff()}");
(3)運行控制台應用程序,查看結果,注意函數被調用了兩次,一次是為變量a,一次是為變量b,輸出如下所示:
I am doing some stuff.
a & DoStuff()=True
I am doing some stuff.
b & DoStuff()=False
(4)將代碼中的&運算符修改為&&運算符,如下所示。
Console.WriteLine($"a && DoStuff()={a&&DoStuff()}");
Console.WriteLine($"b && DoStuff()={b&&DoStuff()}");
(5)運行控制台應用程序,查看結果,注意函數在與變量a合並時會運算,但函數在與變量b合並時不會運行。因為變量b為false,結果為false,所以不需要執行函數。輸出如下:
I am doing some stuff.
a && DoStuff()=True
b && DoStuff()=False。
3.1.6 按位和二元移位運算符
按位運算符影響的是數字中的位。二元移位運算符相比傳統運算符能夠更快地執行一些常見的算術運算。
下面研究按位和二元移位運算符。
(1)創建一個名為BitwiseAndShiftOperators的新文件夾和一個控制台應用程序項目,並將這個項目添加到工作區。
(2)想Main方法添加如下語句,聲明兩個整型變量,值分別為10和6,然后輸出應用AND、OR和XOR(exclusive OR) 按位運算符后的結果:
int a=10;//0000 1010
int b=6;//0000 0110
Console.WriteLine($"a ={a}");
Console.WriteLine($"b ={b}");
Console.WriteLine($"a &b={a&b}");
Console.WriteLine($"a |b={a|b}");
Console.WriteLine($"a &b={a^b}");
(3)運行結果如下:
a =10
b =6
a &b=2
a |b=14
a &b=12
&按位運算,如果2個數的同一位都為1,則為1,否則為0.
|按位運算,如果2個數的同一位有一個滿足為1,就為1。否則為0.
(4)向Main方法添加如下語句,應用左移運算符並輸出結果:
//0101 0000 left-shift a by three bit columns
Console.WriteLine($"a<<3={a<<3}");
//multiply a by 8
Console.WriteLine($"a *8={a*8}");
//0000 0011 right-shift b by one bit column
Console.WriteLine($"b>>1={b>>1}");
(5)運行並觀察結果如下:
a<<3=80
a *8=80
b>>1=3
將變量a左移3位相當於乘以8,變量右移一位相當於除以2。
3.1.7 其他運算符
處理類型時,nameof和sizeof是十分常用的運算符。nameof運算符以字符串值的形式返回變量、類型或成員的短名稱(沒有名稱空間),這在輸出異常消息時非常有用。sizeof運算符返回簡單類型的字節大小,這對於確定數據存儲的效率很有用。
還有很多其他運算符,例如,變量與其成員之間的點稱為成員訪問運算符,函數或方法名末尾的圓括號稱為調用運算符。
3.2理解選擇語句
每個應用程序都需要能夠從選項中進行選擇,並沿着不同的代碼路徑進行分支。C#中的兩個選擇語句是if和switch。可以對所有代碼使用if語句,但是switch語句可以在一些常見的場景中簡化代碼。例如當一個變量有多個值,而每個值都需要進行不同的處理時。
3.2.1使用if語句進行分支
if語句通過計算布爾表達式來確定要執行哪個分支。如果布爾表達式為true,就執行if語句塊否則執行else語句塊。if語句可以嵌套。
if語句也可以與其他if語句以及else if分支結合使用。
每個if語句的布爾表達式都獨立於其他語句,而不像switch語句那樣需要引用單個值。
下面創建一個控制台應用程序來研究if語句。
(1)創建一個文件夾和一個名為SelectionStatements的控制台應用程序項目,並將這個項目添加到工作區。
(2)在Main方法中添加如下語句,檢查是否有參數傳遞給這個控制台應用程序。
if(args.Length==0)
{
WriteLine("There are no arguments");
}
else
{
WriteLine("There is at least one argument.")
}
3.2.2 if語句為什么應總是使用花括號
由於每個語句塊中只有一條語句,因為前面的代碼可以不使用花括號來編譯,但是要避免使用這種不帶花括號的if語句,因為可能引入嚴重的缺陷。例如,蘋果的IOS操作系統中就存在臭名昭著的¥gotofail缺陷。IOS6的安全套接字層(SSL) 加密代碼存在缺陷,這意味着任何用戶在運行IOS6設備上的網絡瀏覽器Safari時,如果試圖連接到安全的網站,比如銀行,將得不到適當的安全保護,因為不小心跳過了一項重要檢查。
你可以通過以下鏈接進一步了解這個臭名昭著的缺陷https://gotofail.com/
你不能僅僅因為可以省去花括號就真的這樣做。沒有了它們,代碼不會“更有效率”;相反,代碼的可維護性會更差,而且可能更危險。
3.2.3模式匹配與if語句
模式匹配是C#7.0及后續版本引入的一個特性。if語句可以將is關鍵字與局部變量聲明結合起來使用,從而使代碼更加安全。
(1)將如下語句添加到Main方法的末尾。這樣,如果存儲在變量o中的值是int類型,就將值分配給局部變量i,然后可以在if語句中使用局部變量i。這比使用變量o安全,因為可以確定i是int變量。
object o="3";
int j=4;
if(o is int i)
{
WriteLine($"{i} x {j}={i*j}");
}
else
{
WriteLine("o is not an int so it cannot multiply!");
}
(2)運行控制台應用程序並查看結果,輸出如下所示:
o is not an int so it cannot multiply!
(3)刪除3兩邊的雙引號,從而使變量o中存儲的值改變為int類型。
(4)重新運行控制台應用程序並查看結果,輸出如下:
3 x 4=12
3.2.4使用switch語句進行分支
switch語句與if語句不同,因為前者會將單個表達式與多個可能的case語句進行比較。每個case語句都與單個表達式相關。每個case部分必須以如下內容結尾:
- break關鍵字(比如下面代碼中的case 1).
- 或者goto case 關鍵字,(比如下面代碼中的case 2).
- 或者沒有語句(比如下面代碼中的case 3).
下面編寫一些代碼來研究switch語句。
(1)在前面編寫的if語句之后,為switch語句輸入一些代碼。注意,第一行是一個可以跳轉到的標簽,第二行將生產一個隨機數。switch語句將根據這個隨機數的值進行分支,如下所示:
A_label:
var number=(new Random()).Next(1,7);
WriteLine($"My random number is {number}");
switch(number)
{
case 1:
WriteLine("One");
break;
case 2:
WriteLine("Two");
break;
case 3:
case 4:
WriteLine("Three or four");
goto case 1;
case 5:
System.Threading.Thread.Sleep(500);
goto A_label;
default:
WriteLine("Default");
break;
}
(2)多次運行控制台應用程序,以查看對於不同的隨機數會發生什么,輸出如下:
PS D:\Code\Chapter03\SelectionStatements> dotnet run
o is not an int so it cannot multiply!
My random number is 1
PS D:\Code\Chapter03\SelectionStatements> dotnet run
o is not an int so it cannot multiply!
My random number is 6
Default
PS D:\Code\Chapter03\SelectionStatements> dotnet run
o is not an int so it cannot multiply!
My random number is 3
Three or four
One
PS D:\Code\Chapter03\SelectionStatements> dotnet run
o is not an int so it cannot multiply!
My random number is 4
Three or four
One
PS D:\Code\Chapter03\SelectionStatements> dotnet run
o is not an int so it cannot multiply!
My random number is 3
Three or four
One
3.2.5模式匹配與switch語句
與if語句一樣,switch語句在C#7.0及更高版本中支持模式匹配。case值不再必須是字面值,而可以是模式。
下面看一個使用文件夾路徑與switch語句匹配的模式示例。
(1)將以下語句添加到文件的頂部,以導入用於處理輸入輸出的類型:
using System.IO;
(2)在Main方法的末尾添加如下語句以聲明文件的字符串路徑,將其作為流打開,然后根據流的類型和功能顯示消息:
string path=@"d:\Code\Chapter03";
Stream s=File.Open(Path.Combine(path,"file.txt"),FileMode.OpenOrCreate);
string message=string.Empty;
switch(s)
{
case FileStream writeableFile when s.CanWrite:
message="The stream is a file stream that I can write to.";
break;
case FileStream readOnlyFile:
message="The stream is a read-only file.";
break;
case MemoryStream ms:
message="The stream is a memory address.";
break;
default:
message ="The stream is some other type.";
break;
case null:
message="The stream is null.";
break;
}
WriteLine(message);
(3)運行控制台應用程序比注意,名為s的變量被聲明為Stream類型,因而可以是流的任何子類型,比如內存流或文件流。在上面這段代碼中,流是使用File.Open方法創建的文件流。由於使用了FileMode,文件流是可以寫的,因此得到一條描述的消息,如下所示:
The stream is a file stream that I can write to.
在.NET中,有多種類型的流,包括FileStream和MemoryStream。在C#7.0及后續版本中,代碼可以基於流的字類型更簡潔地進行分支,你可以聲明並分配本地變量以安全地使用流。第9章將詳細介紹System.IO名稱空間和Stream類型。
此外,case語句可以包含when關鍵字以執行更具體地模式匹配。在前面步驟(2)中地第一個case 語句中,只有當流是FileStream且CanWrite屬性為true時,s變量才是匹配的。
3.2.6 使用switch表達式簡化switch語句
在C#8.0或更高版本中,可以使用switch表示簡化switch語句。
大多數switch語句非常簡單,但是它們需要大量的輸入。switch表達式的涉及目的時簡化需要輸入的代碼,同時仍然表達相同的意圖。
下面實現前面使用switch語句的代碼,這樣就可以比較這兩種風格了。
(1)在Main方法的末尾添加如下語句,根據流的類型和功能,使用switch表達式設置消息:
message=s switch
{
FileStream writeableFile when s.CanWrite=>"The stream is a file stream that I can write to.",
FileStream readOnnlyFile=>"The stream is memory address.",
MemoryStream memoryStream=>"The stream is a memory address",
null=> "The stream is null",
_=>"The stream is some other type."
};
WriteLine(message);
區別主要是去掉了case和break關鍵字。下划線字符用於表示默認的返回值。
(2)運行控制台應用程序,注意結果與前面相同。
3.3理解迭代語句
當條件為真時,迭代語句會重復執行語句塊,或為集合中的每一項重復執行語句塊。具體使用哪種循環語句則取決於解決邏輯問題的易理解性和個人偏好。
3.3.1 while循環語句
while循環語句會對布爾表達式求值,並在布爾表達式為true時繼續循環。
(1)創建一個新的文件夾和一個名為IterationStatements的控制台應用程序,並將這個項目添加到工作區。
(2)在Main方法中輸入以下代碼:
while(x<10)
{
Console.WriteLine(x);
x++;
}
(3)運行控制台應用程序並查看結果,結果應該時數字0~9,如下所示:
0
1
2
3
4
5
6
7
8
9
3.3.2do循環語句
do循環語句與while循環語句類似,知識布爾表達式是在語句塊的底部而不是頂部進行檢查的,這意味着語句塊總是至少執行一次。
(1)在Main方法的后面輸入以下代碼:
string password=string.Empty;
do{
Console.WriteLine("Enter your password:");
password=Console.ReadLine();
}
while(password!="pa$$w0rd");
Console.WriteLine("Correct!");
(2)運行控制台應用程序,程序將重復提示你輸入密碼,直到輸入的密碼正確為止,如下所示:
Enter your password:
passwpord
Enter your password:
12345678
Enter your password:
ninja
Enter your password:
correct horse battery staple
Enter your password:
pa$$w0rd
Correct!
(3)做額外的挑戰,可添加語句,使用戶在顯示錯誤消息之前只能嘗試輸入密碼10次。
3.3.3for循環語句
for循環語句與while循環語句類似,只是更簡潔。for循環語句結合了如下表達式:
-
初始化表達式,它在循環開始時執行一次。
-
條件表達式,它在循環開始后的每次迭代中執行,以檢查循環是否應該繼續。
-
迭代器表達式,它在每個循環的底部語句中執行。
for(int y=1;y<=10;y++) { Console.WriteLine(y); }
(2)運行控制台應用程序以查看結果,結果應該是數字1~10。
3.3.4foreach循環語句
foreach 循環語句與前面的三種循環語句稍有不同。foreach循環語句用於對序列(例如數組或集合)中的每一項執行語句塊。序列中的每一項通常是只讀的,如果在循環期間修改序列結構,例如添加或刪除項,就將拋出異常。
(1)使用類型語句創建一個字符串變量數組,然后輸出每個字符串變量的長度,如下所示:
string[] names={"Adam","Barry","Charlie"};
foreach(string name in names)
{
Console.WriteLine($"{name} has {name.Length} characters.");
}
(2)運行控制台應用程序並查看結果,輸出如下所示:
Adam has 4 characters.
Barry has 5 characters.
Charlie has 7 characters.
理解foreach循環語句如何工作的
從技術上講,foreach循環語句適用於符合以下規則的任何類型:
- 類型必須有一個名為GetEnumerator的方法。該方法會返回一個對象。
- 返回的這個對象必須有一個名為Current的屬性和一個名為MoveNext的方法。
- 如果有更多的項要枚舉,那么MoveNext方法必須返回true,否則返回false。
有2個名為IEnumerable和IEnumerable
編譯器會將前一個例子中的foreach語句轉換成下面的偽代碼:
IEnumerator e=names.GetEnumerator();
while(e.MoveNext())
{
string name=(String)e.Current;
Console.WriteLine($"{name} has {name.Length} characters.");
}
由於使用了迭代器,因此foreach循環語句中聲明了變量不能用於修改當前項的值。
3.4類型轉換
我們常常需要在不同類型之間轉換變量的值。例如,數據通常在控制台中以文本形式輸入,因此它們最初儲存字符串類型的變量中,但隨后需要將它們轉換為日期/時間、數字或其他數據類型,這取決於它們的存儲和處理方式。
有時需要在數字類型之間進行轉換,比如在整數和浮點數之間進行轉換,然后才執行計算。轉換也稱為強制類型轉換,分為隱式和顯式的兩種。隱式的強制類型轉換是自動進行的,並且是安全的,這意味着不會丟失任何信息。
顯式的強制類型轉換必須手動執行,因為可能會丟失一些信息,例如數字的精度。通過進行顯式的強制類型轉換,可以告訴C#編譯器,我們理解並接受這種風險。
3.4.1隱式和顯式的轉換數字
將int變量隱式轉換為double 變量是安全的,因為不會丟失任何信息。
(1)創建一個新的文件夾和一個名為CastingConverting的控制台應用程序項目,並將這個項目添加到工作區。
(2)在Main方法中輸入如下語句以聲明並賦值一個int變量和一個double變量,然后在給double 變量b賦值時,隱式地轉換int變量a的值:
int a=10;
double b=a;
Console.WriteLine(b);
double c=9.8;
int d=c;
Console.WriteLine(d);
(3)查看終端窗口,運行dotnet run命令,觀察錯誤信息:
Program.cs(13,19): error CS0266: 無法將類型“double”隱式轉換為“int”。存在一個顯式轉換(是否缺少強制轉換?) [D:\Code\Chapter03\CastingConverting\CastingConverting.csproj
不能隱式的將double變量轉換為int變量,因為這可能是不安全的,可能會丟失數據。
必須在要轉換的double類型的兩邊使用一對圓括號,才能顯式的將double變量轉換為int變量,這對圓括號是強制類型轉換運算符。即使這樣,也必須注意小數點后的部分將自動刪除,因為我們選擇了執行顯式的強制類型轉換。
(6)修改變量d的賦值語句,如下所示:
int d=(int)c;
Console.WriteLine(d);
(7)運行控制台應用程序查看結果,輸出如下所示:
10
9
在大整數和小整數之間轉換時,必須執行類似的操作,再次提醒,可能會丟失信息,因為任何太大的值都將以意想不到的方式復制並解釋二進制位。
(8)輸入如下語句以聲明一個64位的long變量並將它們賦值給一個32位的int變量,它們兩者都使用一個可以工作的小值和一個不能工作的大值:
long e=10;
int f=(int)e;
Console.WriteLine($"e is {e:N0} and f is {f:N0}");
e=long.MaxValue;
f=(int)e;
Console.WriteLine($"e is {e:N0} and f is {f:N0}");
(9)運行控制台,查看輸出結果如下:
e is 10 and f is 10
e is 9,223,372,036,854,775,807 and f is -1
(10)將變量e的值修改為50億,如下所示:
e=5_000_000_000;
f=(int)e;
Console.WriteLine($"e is {e:N0} and f is {f:N0}");
(11)運行結果如下:
e is 9,223,372,036,854,775,807 and f is -1
e is 5,000,000,000 and f is 705,032,704
3.4.2使用System.Convert類型進行轉換
使用強制類型轉換運算符的另一種方法時使用System.Convert類型。System.Convert類型可以轉換為所有C#數字類型,也可以轉換為布爾值、字符串、日期和時間值。
(1)在Program.cs文件的頂部靜態導入System.Convert類型,如下所示:
using static System.Convert;
(2)在Main方法的底部添加如下語句以聲明double變量g並為之賦值,將變量g的值轉換為整數,然后將這兩個值都寫入控制台;
double g=9.8;
int h=ToInt32(g);
WriteLine($"g is {g} and h is {h}");
(3)運行控制台應用程序並查看結果,輸出如下所示:
g is 9.8 and h is 10
可以看出,double值9.8被轉換為整數位10,而不是去掉小數點后的部分。
3.4.3圓整數字
如前所述,強制類型轉換運算符會對實數的小數部分進行處理,而使用System.Convert類型的話,則會向上或向下圓整。然而,圓整規則是什么?
1、理解默認的圓整規則
如果小數部分是0.5或更大,則向上圓整;如果小數部分比0.5小,則向下圓整。
下面探索C#是否遵循相同的規則。
(1)在Main方法的底部添加如下語句以聲明一個double數組並賦值,將其中的每個double值轉換為整數,然后將結果寫入控制台:
double[] doubles=new double[]{9.49,9.5,9.51,10.49,10.5,10.51};
foreach(var n in doubles)
{
Console.WriteLine($"ToInt({n}) is {ToInt32(n)}");
}
(2)運行控制台並查看結果如下:
ToInt(9.49) is 9
ToInt(9.5) is 10
ToInt(9.51) is 10
ToInt(10.49) is 10
ToInt(10.5) is 10
ToInt(10.51) is 11
C#中的圓整規則略有不同:
- 如果小數部分小於0.5,則向下圓整。
- 如果小數部分大於0.5,則向上圓整。
- 如果小數部分是0.5,那么在非小數部分是奇數的情況下向上圓整,在非小數部分是偶數的情況下向下圓整。
以上規則又稱為“銀行家的圓整法",以上規則之所以受青睞,是因為可通過上下圓整的交替來減少偏差。遺憾的是,其他編程語言使用的是默認的圓整規則。
2控制圓整規則
可以使用math類的Round方法來控制圓整規則。
(1)在Main方法的底部添加如下語句,使用”遠離0“的圓整規則(也稱為向上圓整)來圓整每個double值,然后將結果寫入控制台:
foreach(var num in doubles)
{
Console.WriteLine(format:"Math.Round({0},0,MidpointRounding.AwayFromZero) is {1}",arg0:nn,arg1:Math.Round(value:num,digits:0,mode:MidpointRounding.AwayFromZero));
}
(2)運行控制台並查看結果:
Math.Round(9.49,0,MidpointRounding.AwayFromZero) is 9
Math.Round(9.5,0,MidpointRounding.AwayFromZero) is 10
Math.Round(9.51,0,MidpointRounding.AwayFromZero) is 10
Math.Round(10.49,0,MidpointRounding.AwayFromZero) is 10
Math.Round(10.5,0,MidpointRounding.AwayFromZero) is 11
Math.Round(10.51,0,MidpointRounding.AwayFromZero) is 11
3.4.4從任何類型轉換為字符串
最常見的轉換時從任何類型轉換為字符串變量,以便輸出人類可讀的文本,因此所有類型都提供了從System.Object類繼承的ToString方法。
ToString方法可將任何變量的當前值轉換為文本教師性時。有些類型不能合理地表示為文本,因此它們返回名稱空間和類型名稱。
(1)在Main方法的底部輸入如下語句以聲明一些變量,將它們轉換為字符串表示形式,並將它們寫入控制台:
int number=12;
Console.WriteLine(number.ToString());
bool boolean=true;
Console.WriteLine(boolean.ToString());
DateTime now=DateTime.Now;
Console. WriteLine(now.ToString());
object me=new object();
Console.WriteLine(me.ToString());
(2)運行控制台並查看結果,如下所示:
12
True
2021/5/12 22:46:56
System.Object
3.4.5從二進制對象轉換為字符串
對於將要存儲或傳輸的二進制對象,例如圖像或視頻,又是不想發送原始位,因為不知道如何解釋那些位,例如通過網絡協議傳輸或由另一個操作系統讀取及存儲的二進制對象。
最安全的做法是將二進制對象轉換成安全字符串,程序員稱之為Base64編碼。
Convert類型提供了兩個方法-ToBase64String和FromBase64String,用於執行這種轉換。
(1)將如下語句添加到Main方法的末尾,創建一個字節數組,在其中隨機填充字節值,將格式良好的每個字節寫入控制台,然后將相同 的字節轉換成Base64編碼並寫入控制台:
byte[] binaryObject =new byte[128];
(new Random()).NextBytes(binaryObject);
Console.WriteLine("Binary Object as bytes:");
for(int index=0;index<binaryObject.Length;index++)
{
Console.Write($"{binaryObject[index]:X} ");
}
Console.WriteLine();
string encoded=Convert.ToBase64String(binaryObject);
Console.WriteLine($"Binary Object as Base64:{encoded}");
(2)默認情況下,如果采用十進制計數法,就會輸出一個int值。可以使用:X這樣的格式,通過十六進制計數法對值進行格式化。
Binary Object as bytes:
54 B3 69 CD 11 79 8E A2 1F 2A 76 61 B CC 4C AC 95 B8 82 CF 57 E2 8C C4 39 9C B9 E D8 DE D 7E BB 5E 6D 40 67 4A 5 88 7E DA A3 16 69 2D 38 15 6 F8 8D 43 1D A7 A9 72 26 A4 94 75 BF 80 C9 40 1D F7 A5 AD 5F B 45 4B B3 FA 51 7C 3E 76 5A C 63 A4 7 DF 50 CA 61 14 B4 FA 19 C2 B6 8F C8 FE 10 62 F9 71 87 8A B6 FA B3 42 8F 22 56 2D 33 B0 4A 87 12 1B 78 EA F0 52 96 91 F9 64 F2 82 95 23
Binary Object as Base64:VLNpzRF5jqIfKnZhC8xMrJW4gs9X4ozEOZy5DtjeDX67Xm1AZ0oFiH7aoxZpLTgVBviNQx2nqXImpJR1v4DJQB33pa1fC0VLs/pRfD52WgxjpAffUMphFLT6GcK2j8j+EGL5cYeKtvqzQo8iVi0zsEqHEht46vBSlpH5ZPKClSM=
3.4.6將字符串轉換為數字或日期和時間
還有一種十分常見的轉換是將字符串轉換為數字或日期和時間。
(1)在Main方法中添加如下語句,從字符串中解析出整數以及日期和時間,然后將結果寫入控制台:
int age=int.Parse("27");
DateTime birthday=DateTime.Parse("4 July 1980");
Console.WriteLine($"I was born {age} years ago.");
Console.WriteLine($"My birthday is {birthday}.");
Console.WriteLine($"My birthday is {birthday:D}");
(2)運行控制台應用程序並查看結果,輸出如下所示:
I was born 27 years ago.
My birthday is 1980/7/4 0:00:00.
My birthday is 1980年7月4日
默認情況下,日期和時間輸出為短日期格式。可以使用諸如D的格式代碼,僅輸出使用了長日期格式的日期部分。
Parse方法存在的問題是:如果字符串不能轉換,該方法就會報錯。只有少數類型具有Parse方法,包括所有的數字類型和DateTime。
(3)在Main方法的底部添加如下語句,嘗試將一個包含字母的字符串解析為整型變量:
nt count =int.Parse("abc");
(4)運行控制台應用程序並查看結果,輸出如下:
未能找到類型或命名空間名“nt”(是否缺少 using 指令或程序集引用?) [D:\Code\Chapter03\CastingConverting\CastingConverting.csproj]
與前面的異常消息一樣,你會看到堆棧跟蹤。這個系列的筆記不包含堆棧跟蹤,因為它們會占用太多的篇幅。
使用TryParse方法避免異常
為了避免錯誤,可以使用TryParse方法。TryParse方法將嘗試轉換輸入字符串,如果可以轉換,則返回true,否則返回false。
out關鍵字是必須的,從而允許TryParse方法在轉換時設置Count變量。
(1)將int count聲明替換為使用TryParse方法的語句,並要求用戶輸入雞蛋的數量,如下所示:
Console.WriteLine("How many eggs are there?");
int count;
string input=Console.ReadLine();
if(int.TryParse(input,out count))
{
Console.WriteLine($"There are {count} eggs.");
}
else
{
Console.WriteLine($"I Could not parse the input.");
}
(2)運行控制台程序。
(3)輸入12並查看結果,輸出如下所示:
How many eggs are there?
12
There are 12 eggs.
(4)再次運行控制台應用程序。
(5)輸入twelve並查看結果。
How many eggs are there?
twelve
I Could not parse the input.
你還可以使用System.Convert類型將字符串轉換為其他類型,但是,與Parse方法一樣,如果不能進行轉換,這里也會報錯。
3.4.7在轉換類型時處理異常
前面介紹了在轉換類型時發生錯誤的幾個常見。當發生這種情況時,就會拋出運行時異常。
可以看到,控制台應用程序的默認行為時編寫關於異常的消息,包括輸出中的堆棧跟蹤,然后停止運行應用程序。
一定要避免編寫可能會拋出異常的代碼,這可以通過執行if語句檢查來實現,但有時也可能做不到。在這些場景中,可以捕獲異常,並以比默認行為更好的處理它們。
1.將容易出錯的代碼封裝到try塊中。
當知道某個語句可能導致錯誤時,就應該將其封裝到try塊中,例如,從文本到數字的解析可能會導致錯誤。只有當try塊中的語句拋出異常時,才會執行catch塊中的任何語句。我們不需要在catch塊中做任何事。
(1)創建Main方法中添加如下語句,提示用戶輸入年齡,然后將年齡寫入控制台,因為和前面代碼定義變量名沖突,我們在變量后加a:
Console.Write("What is your age?");
string inputa=Console.ReadLine();
try{
int agea=int.Parse(inputa);
Console.WriteLine($"You are {agea} years old.");
}
catch
{}
Console.WriteLine("After parsing");
上面這段代碼包含兩條信息,分別在解析之前和解析之后顯式,以幫助你清楚的理解代碼中的流程。當示例代碼變得更復雜時,這將特別有用。
(3)運行控制台程序。
(4)輸入有效的年齡,例如47隨,然后查看結果,輸出如下所示:
Before parsing
What is your age?47
You are 47 years old.
After parsing
(5)再次運行控制台應用程序。
(6)輸入無效的年齡,例如kermit,然后查看結果,輸出如下所示:
Before parsing
What is your age?kermit
After parsing
當執行代碼時,異常被捕獲,不會輸出默認消息和堆棧跟蹤,控制台應用程序繼續運行。這筆默認行為更好,但是查看發生的錯誤類型可能更有用。
2.捕獲所有異常。
要獲取可能發生的任何類型的異常信息,可以為catch塊聲明類型為System.Exception的變量。
(1)向catch塊添加如下異常變量聲明,並將有關異常的信息寫入控制台:
catch(Exception ex)
{
Console.WriteLine($"{ex.GetType()} says {ex.Message}");
}
(2)運行控制台應用程序。
(3)輸入無效的年齡,例如kermit,然后查看結果,輸出如下所示:
Before parsing
What is your age?kermit
System.FormatException says Input string was not in a correct format.
After parsing
3.捕獲特定異常
現在,在知道發生了哪種特定類型的異常后,就可以捕獲這種類型的異常,並制定想要顯式給用戶的消息以改進代碼。
(1)退出現有的catch塊,在上方為格式一次類型添加另一個新的catch塊,FormatException這個catch,如下所示:
try{
int agea=int.Parse(inputa);
Console.WriteLine($"You are {agea} years old.");
}
catch(FormatException)
{
Console.WriteLine($"The age you entered is not a valid number format.");
}
catch(Exception ex)
{
Console.WriteLine($"{ex.GetType()} says {ex.Message}");
}
Console.WriteLine("After parsing");
(2)運行控制台應用程序。
(3)輸入無效的年齡,例如Kermit,然后查看結果,輸出如下:
Before parsing
What is your age?kermit
The age you entered is not a valid number format.
After parsing
之前所保留的前面的那個catch塊,時因為可能會發生其他類型的異常。
(4)運行控制台程序
(5)輸入一個對於整數來說過大的數字,例如9876543210,查看結果:
Before parsing
What is your age?9876543210
System.OverflowException says Value was either too large or too small for an Int32.
After parsing
你可以為這種類型的異常添加另一個catch塊。
(6)退出現有的catch塊,為溢出異常類型添加新的catch塊,如下面顯式的代碼所示:
catch(OverflowException)
{
Console.WriteLine("Your age is a valid number format but it is either too big or small.");
}
(7)運行控制台程序
(8)輸入一個對於整數來說過大的數字,然后查看結果:
Before parsing
What is your age?9876543210
Your age is a valid number format but it is either too big or small.
After parsing
異常捕獲的順序很重要,正確的順序與異常類型的繼承層次結構有關。第五章將介紹繼承。但時,你不用太擔心-如果以錯誤的順序得到異常,編譯器會報錯。
3.4.8檢查溢出
如前所述,在數字類之間進行強制類型轉換時,可能會丟失信息,例如在將long變量強制轉換為int變量時。如果類型中存儲的值太大,就會溢出。
1.使用checked語句拋出溢出異常
checked語句告訴.NET,要在發生溢出時拋出異常,而不是沉默不語。
下面我們把int變量x的初值設置為int類型所能存儲的最大值減1。然后,將變量x遞增幾次,每次遞增是都輸出值。一旦超出最大值,就會溢出到最小值,並從那里繼續遞增。
(1)創建一個文件夾和一個名為CheckingForOverflow的控制台應用程序項目,並將這個項目添加到工作區。
(2)在M愛你方法中輸入如下語句,聲明int變量x並賦值為int類型所能存儲的最大值減1,然后將x遞增三次,並且每次遞增時都把值寫入控制台:
int x=int.MaxValue-1;
Console.WriteLine($"Initial value: {x}");
x++;
Console.WriteLine($"After incrementing:{x}");
x++;
Console.WriteLine($"After incrementing:{x}");
x++;
Console.WriteLine($"After incrementing:{x}");
(3)運行控制台應用程序並檢查結果,輸出如下所示:
Initial value: 2147483646
After incrementing:2147483647
After incrementing:-2147483648
After incrementing:-2147483647
(4)現在,使用checked塊封裝語句,編譯器會警告出現了溢出。
checked
{
int x=int.MaxValue-1;
Console.WriteLine($"Initial value: {x}");
x++;
Console.WriteLine($"After incrementing:{x}");
x++;
Console.WriteLine($"After incrementing:{x}");
x++;
Console.WriteLine($"After incrementing:{x}");
}
(5)運行控制台應用程序並查看結果,輸出如下:
Initial value: 2147483646
After incrementing:2147483647
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
at CheckingForOverflow.Program.Main(String[] args) in D:\Code\Chapter03\CheckingForOverflow\Program.cs:line 15
(6)與任何其他異常一樣,應該將這些語句封裝在try塊中,並未用戶顯式更友好的錯誤信息,如下所示:
try{
checked
{
int x=int.MaxValue-1;
Console.WriteLine($"Initial value: {x}");
x++;
Console.WriteLine($"After incrementing:{x}");
x++;
Console.WriteLine($"After incrementing:{x}");
x++;
Console.WriteLine($"After incrementing:{x}");
}
}
catch(OverflowException)
{
Console.WriteLine("The code overflowed but I caught the exception.");
}
(7)運行控制台程序,輸出如下:
Initial value: 2147483646
After incrementing:2147483647
The code overflowed but I caught the exception.
2.使用unchecked語句禁用編譯時檢查
unchecked語句能夠關閉由編譯器在一段代碼內執行的溢出檢查。
(1)在前面語句的末尾輸入下面的語句。編譯器不會編譯這條語句,因為編譯器知道會發生溢出:
int y=int.MaxValue+1;
(2)使用控制台運行,並觀察輸出:
Program.cs(26,19): error CS0220: 在 checked 模式下,運算在編譯時溢出 [D:\Code\Chapter03\CheckingForOverflow\CheckingForOverflow.csproj]
(3)要禁用編譯時檢查,請將語句封裝在unchecked塊中,將y的值寫入控制台,遞減y,然后重復,如下所示:
unchecked{
int y=int.MaxValue+1;
Console.WriteLine("Initial value:{y}");
y--;
Console.WriteLine($"After decrementing:{y}");
y--;
Console.WriteLine($"After decrementing: {y}");
}
(4)運行控制台應用程序並查看結果,輸出如下:
Initial value:-2147483648
After decrementing:2147483647
After decrementing: 2147483646
當然,我們很少希望像這樣顯式的 關閉編譯時檢查,從而允許發生溢出。但是,也許在某些場景中,我們需要顯式的關閉溢出檢查。