【问】
在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)一律使用显式转换。
其实这些基本原则的总的一个母体都是从“异常原则”推导而来(例如:无符号因为没有负数,把带符号赋值给无符号可能引发数值异常等,所以需要强制转换)。