【問】 在C#和Visual Basic的轉換中,以下一些轉換的用法和區別是什麼呢? [C#] [VB.NET] 【錯誤回答】 沒有區別,因為運行了之後都可以正常轉化。 【正解】 光從運行結果來看當然是毫無區別,因為題目所給出的僅僅是一部分的例子,不是全部。許多初學者容易產生“以偏概全”的錯誤認識。
【問】
在C#和Visual Basic的轉換中,以下一些轉換的用法和區別是什麼呢?
[C#]
long num = 12;
int n = (int)num;
int m = Convert.ToInt32(num);
[VB.NET]
Dim num As Long = 12
Dim n As Integer = num
n = CInt(num)
n= CType(num,Integer)
n = Convert.ToInt32(num)
【錯誤回答】
沒有區別,因為運行了之後都可以正常轉化。
【正解】
光從運行結果來看當然是毫無區別,因為題目所給出的僅僅是一部分的例子,不是全部。許多初學者容易產生“以偏概全”的錯誤認識。讓我們先以C#作為例子進行比較:
[C#]
long num = long.MaxValue;
int n = (int)num; …………………………①
int m = Convert.ToInt32(num); ………②
註意①處的例子,因為我要把一個long類型賦值給int類型。因此語法上VS認為不能直接轉換,除非你使用強制類型轉換聲明。同時又因為long.MaxValue已經遠遠大於int所可以容納的範圍,因此實際存入n中的數值是溢出的(即不是原來的值)。
如果註釋掉①句,單獨運行②的話你會發現有一個錯誤——提示你說num大於int的上限或者下限了,而單獨運行①只是因為字元串被截斷,沒有錯誤提示。顯然地,Convert的轉換原則是直接檢查被轉換的這個變數(本題為num)實際存放的數值是否可以被另一個接收方(本題為m)接受——如果超出了m類型所容納的範圍(即int的MaxValue和MinValue),即被視為錯誤。
在VB.NET中同樣地我們來做一個實驗:
Dim num As Long = Long.MaxValue
Dim n As Integer = num…………………①
n = CInt(num) …………………………②
n= CType(num,Integer) ………………③
n = Convert.ToInt32(num)
結果你發現無論註釋掉其它表達式,單獨運行①、②或是③的任何一句,都會拋出“算術運算導致溢出”的錯誤。可見在VB.NET中,在語法層面檢測可以進行轉換之後,直接檢測被轉換的那個數值是否可以被接收方容納。如果不可容納,則拋出異常,視為錯誤。
附帶說一句——實際上,C#在強制轉換中也可以進行類似VB.NET一樣的“溢出”檢測,只不過預設C#是啟用unchecked機制。你完全可以使用checked機制來達到類似Convert轉換一樣的效果。比如:
[C#]
checked
{
long num = long.MaxValue;
int n = (int)num; //運行到此立即引發一個錯誤,表示結果溢出
}
【總結】
1) C#中普通強制類型轉換(類似C#例子中第一句),預設只進行語法檢測,對實際轉換的值是否可以被接受不做檢測,除非顯式地放入“checked”塊中。
2) C#或VB.NET的Convert語句,不僅對轉換的類型進行檢測(因為Convert.ToXXX的“XXX”就是需要轉換後的類型),而且還檢測了“被轉換”變數中那個數值是否可以完全被轉換後的變數容納。
3)C#語法嚴謹,因此從大類型=>小類型轉換時候必須“顯式聲明”;相對而言,VB.NET並不需要如此嚴謹的語法。對於VB.NET而言,直接賦值“A=B”(相當於A=CType(B,A))就可以了。對於值類型而言,B必須認為定義隱式或者是顯式轉換函數(下麵有)。但CType對於“類”而言在語法上相當於A a =(A)B,是要嚴格檢查B是否可以轉換成A(兩種情況:要麼B是A的父類,要麼是自定義隱式/顯式從B=>A的轉換,否則會引發語法錯誤;另外對於第一種而言,如果要保證運行不出錯,B里包含的實體類至少是A或者其子類)。
4)另外VB.NET還有一個DirectCast轉化,它接受的兩個對象都必須是類,不能是結構(即便結構自定義隱式/顯式轉換也不行)。
【拓展】
一、自定義顯式轉換和隱式轉換:
我們目前知道一個事實——那就是.NET框架體系中(C#和VB.NET)而言,“類型轉換”中是按照“里氏原則”的——即子類向父類轉換;也就是說,毫無關係的類是無法進行強制轉換的。那麼現在的問題在於:int,double等都是結構(結構是不允許被繼承的),它們是怎樣實現互相轉換的?實際上,C#或者VB.NET允許為兩個毫不相干的類型(這裡是指代“無繼承關係”的)人為指定隱式轉換和顯式轉換。其語法分別為:
[C#]
public static implicit/exclipt operator 轉換後類型(轉換前類型 變數名)
[VB.NET]
Public Shared Widening/Narrowing Operator CType(轉換前類型 變數名) As轉換後類型
因為這個方法是自定義賦值符號(=),不屬於任何類的實例,比較特殊,有些近似於“運算符重載”,因此是靜態方法,一旦運行時立即載入並且自動被調用。“implicit”隱式轉換,“exclipit”顯式轉換(定義了exclipit方法的時候,調用必須寫成:xxx = (轉換後類型)變數名,就像long=>int必須寫成(int)long變數的形式)。
任意一個類都從其先天的父類“object”直接繼承了ToString()方法,但是預設是不重寫這個方法的。因此返回的也都是“命名空間.類名”。我們不希望這樣做,最好的結果是我們如果直接把這個類賦值給一個String,然後通過String就輸出其主要內容。下麵就通過這個簡單的“複數類”說明問題:
[C#]
public class Complex
{
public double Real { get; set; }
public double Comnum { get; set; }
public static implicit operator string(Complex c)
{
// 準備轉化後輸出的字元串變數
string result = string.Empty;
// 如果實數部分不等於0
if (c.Real != 0)
{
//拼接實數部分
result += c.Real.ToString();
}
//虛數部分,如果不等於0
if (c.Comnum != 0)
{
//如果虛數部分小於0,連同負號一起輸出
if (c.Comnum < 0)
{
result += c.Comnum+"i";
}
//如果虛數部分大於0
else if(c.Comnum>0)
{
//如果實數等於0的話,直接輸出虛數部分
if (c.Real == 0.0)
{
result = c.Comnum.ToString()+"i";
}
//否則前面輸出帶有加號的
else
{
result += "+" + c.Comnum+"i";
}
}
}
return result;
}
}
[VB.NET]
Public Class Complex
Public Property Real() As Double
Get
Return m_Real
End Get
Set
m_Real = Value
End Set
End Property
Private m_Real As Double
Public Property Comnum() As Double
Get
Return m_Comnum
End Get
Set
m_Comnum = Value
End Set
End Property
Private m_Comnum As Double
Public Shared Widening Operator CType(c As Complex) As String
' 準備轉化後輸出的字元串變數
Dim result As String = String.Empty
' 如果實數部分不等於0
If c.Real <> 0 Then
'拼接實數部分
result += c.Real.ToString()
End If
'虛數部分,如果不等於0
If c.Comnum <> 0 Then
'如果虛數部分小於0,連同負號一起輸出
If c.Comnum < 0 Then
result += c.Comnum + "i"
'如果虛數部分大於0
ElseIf c.Comnum > 0 Then
'如果實數等於0的話,直接輸出虛數部分
If c.Real = 0.0 Then
result = c.Comnum.ToString() & "i"
Else
'否則前面輸出帶有加號的
result += "+" + c.Comnum & "i"
End If
End If
End If
Return result
End Operator
End Class
調用的時候(寫法VS2008以上語法,你完全可以先創建實體,對實體屬性逐個賦值):
[C#]
string s = new Complex { Real = 1.2, Comnum = 2.3 };
Console.WriteLine(s);
s = new Complex { Real = 0, Comnum = -2.5 };
Console.WriteLine(s);
s = new Complex { Real = -2.5, Comnum = 0 };
Console.WriteLine(s);
s = new Complex { Real = -2.5, Comnum = -2.5 };
Console.WriteLine(s);
[VB.NET]
Dim s As String = New Complex() With { _
Key .Real = 1.2, _
Key .Comnum = 2.3 _
}
Console.WriteLine(s)
s = New Complex() With { _
Key .Real = 0, _
Key .Comnum = -2.5 _
}
Console.WriteLine(s)
s = New Complex() With { _
Key .Real = -2.5, _
Key .Comnum = 0 _
}
Console.WriteLine(s)
s = New Complex() With { _
Key .Real = -2.5, _
Key .Comnum = -2.5 _
}
Console.WriteLine(s)
看到吧——一個自定義的Complex和string竟然可以轉化(註意:String是密封類,不可能存在任何其它子類的)。所以我們說——在未實現其它任何隱式或者顯式轉化情況下,預設兩個類之間的轉化必須遵照“里氏原則”(子類=>父類)進行。
如果把上面示例中的“implicit”轉化成“exclipit”,那麼調用的時候(僅舉一例)——
string s = (string)new Complex { Real = 1.2, Comnum = 2.3 };
到這裡,我們就可以回答“拓展”中的那個問題了:之所以“毫不相干,沒有繼承”關係的結構可以互相轉化,是因為微軟也應用了類似手法,為每一個結構(例如Int32等)定義了“隱式”和“顯式”轉換方法。使得這些結構之間可以互相進行“顯式”或者“隱式”轉換。比如微軟為long定義了從long到int的顯式轉換法則,又為long到double定義了隱式轉換法則(當然,你把long到int定義成隱式的,把long到double定義成顯式的也未嘗不可)。所以這裡引申出來的一個問題在於——到底何時定義implicit和exclipt?
一般地,主要是按照“異常原則”進行判斷(所謂“異常原則”,就是說某個類型轉換成某個類型的時候如果肯定不會出錯或者造成數據丟失等情況,應當建議使用implicit;否則的話建議使用explicit(像int=>long恆成立,因為int只占用4個位元組,long是8個位元組,恆成立;但是反過來自然有溢出的可能了,或者轉換不正常情況)。
二、C#中“異常原則”在“語法層面”上轉化的實際應用:
說到轉換,因為C#遠遠比VB.NET複雜(須經過“語法”、“實際數據”兩步)。我們來看一下C#在“語法層面”上的轉換規則——一般地,大家可以按照以下規則記憶(1、其中sybe,int,long是帶符號的類型,byte,uint/char,ulong 是無符號的類型。2、從左到右,數值可以表示的範圍越來越大。其中char所能夠表示的範圍等同於uint,只是輸出的是字元):
sbyte byte short ushort int uint/char long ulong float decimal double
1)帶符號之間的轉換法則:大=>小一律隱式,小=>大一律顯式。
2)無符號之間轉換法則:大=>小一律隱式,小=>大一律顯式。
3)float,double和decimal之間一律使用顯式轉換。
4)一切帶符號轉化到無符號(包括轉化到char)一律使用顯式轉換。
其實這些基本原則的總的一個母體都是從“異常原則”推導而來(例如:無符號因為沒有負數,把帶符號賦值給無符號可能引發數值異常等,所以需要強制轉換)。