6.5.2 多重数据类型转换
在定义数据类型转换时必须考虑的一个问题是,如果在进行要求的数据类型转换时,C#编译器没有可用的直接转换方式,C#编译器就会寻找一种方式,把几种转换合并起来。例如,在Currency结构中,假定编译器遇到下面的代码:
Currency balance = new Currency(10,50); long amount = (long)balance; double amountD = balance; |
首先初始化一个Currency实例,再把它转换为一个long。问题是不能定义这样的转换。但是,这段代码仍可以编译成功。因为编译器知道我们要定义一个从Currency到float的隐式转换,而且它知道如何显式地从float 转换为long。所以它会把这行代码编译为中间语言代码,首先把balance转换为float,再把结果转换为long。上述代码的最后一行也是这样,把balance转换为double型时,因为从Currency到 float的转换和从float 到double的转换都是隐式的,就可以在代码中把这个转换当作一种隐式转换。如果要显式地指定转换过程,可以编写如下代码:
Currency balance = new Currency(10,50); long amount = (long)(float)balance; double amountD = (double)(float)balance; |
但是,在大多数情况下,这会使代码变得比较复杂,因此是不必要的。下面的代码会产生一个编译错误:
Currency balance = new Currency(10,50); long amount = balance; |
原因是编译器可以找到的最佳匹配的转换仍是首先转换为flost,再转换为long,但从float到long的转换需要显式指定。
所有这些都不会带来太多的麻烦。转换的规则是非常直观的,主要是为了防止在开发人员不知情的情况下丢失数据。但是,在定义数据类型转换时如果不小心,编译器就有可能指定一条导致不期望的结果的路径。例如,假定编写Currency结构的其他小组成员要把一个uint转换为Currency,而该uint中包含了美分的总数(美分不是美元,因为我们不希望丢掉美元的小数部分),为此应编写如下代码:
public static implicit operator Currency (uint value)
{
return new Currency(value/100u, (ushort)(value%100));
} // Don't do this!
|
注意,在这段代码中,第一个100后面的u可以确保把value/100u解释为uint。如果写成value/100,编译器就会把它解释为一个int型的值,而不是uint型的值。
在这段代码中清楚地注释了“不要这么做”。下面说明其原因。看看下面的代码段,它把包含350的uint转换为一个Currency,再转换回uint。那么在执行完这段代码后,bal2中又将包含什么?
uint bal = 350; Currency balance = bal; uint bal2 = (uint)balance; |
答案不是350,而是3!这是符合逻辑的。我们把350隐式地转换为Currency,得到balance.Dollars=3,balance.Cents=50。然后编译器进行通常的操作,为转换回uint指定最佳路径。balance最终会被隐式地转换为float型(其值为3.5),然后显式地转换为uint型,其值为3。
当然,转换为另一个数据类型后,再转换回来有时会丢失数据。例如,把包含6.8的float转换为int,再转换回float,会丢失数字中的小数部分,得到5,但丢失数字中的小数部分和一个整数被100整除的情况略有区别。Currency现在成了一种相当危险的类,它会对整数进行一些奇怪的操作。
问题是,在转换过程中如何解释整数是有矛盾的。从Currency到float的转换会把整数1解释为1美元,但从uint到Currency的转换会把这个整数解释为1美分,这是很糟糕的。如果希望类易于使用,就应确保所有的转换都按一种互相兼容的方式执行,即这些转换应得到相同的结果。在本例中,显然要重新编写从uint到Currency的转换,把整数值1解释为1美元:
public static implicit operator Currency (uint value)
{
return new Currency(value, 0);
}
|
偶尔也会觉得这种新的转换方式可能根本不需要。但实际上这种转换方式是非常有用的。没有它,编译器在执行从uint到Currency的转换时,就只能通过float来进行。此时直接转换的效率要高得多,所以进行这种额外转换会提高性能,但需要确保它的结果与通过float进行转换得到的结果相同。在其他情况下,也可以为不同的预定义数据类型分别定义转换,让更多的转换隐式执行,而不是显式地执行,但本例不是这样。
测试这种转换是否成功,应确定无论使用什么转换路径,它都能得到相同的结果(而不是像在从float到int的转换过程中丢失数据那样)。Currency类就是一个很好的示例。下面的代码:
Currency balance = new Currency(50, 35); ulong bal = (ulong) balance; |
目前,编译器只能采用一种方式来执行这个转换:把Currency隐式地转换为float,再显式地转换为ulong。从float到ulong的转换需要显式指定,本例就显式指定了这个转换,所以编译是成功的。
但假定要添加另一个转换,从Currency隐式地转换为uint,就需要修改Currency结构,添加从uint到Currency的转换和从Currency到uint的转换,这段代码可以下载,作为SimpleCurrency2示例:
public static implicit operator Currency (uint value)
{
return new Currency(value, 0);
}
public static implicit operator uint (Currency value)
{
return value.Dollars;
}
|
现在,编译器从Currency转换到 ulong可以使用另一条路径:先从Currency隐式地转换为uint,再隐式地转换为ulong。该采用哪条路径? C#有一些规则(本书不详细讨论这些规则,读者可参阅MSDN文档说明),告诉编译器如何确定哪条是最佳路径。但最好自己设计转换,让所有的转换都得到相同的结果(但没有精确度的损失),此时编译器选择哪条路径就不重要了(在本例中,编译器会选择Currency→uint→ulong路径,而不是Currency→float→ulong路径)。
为了测试SimpleCurrency2示例,给SimpleCurrency的测试程序添加如下代码:
try
{
Currency balance = new Currency(50,35);
Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " + balance.ToString());
uint balance3 = (uint) balance;
Console.WriteLine("Converting to uint gives " + balance3);
|
运行这个示例,得到如下所示的结果:
SimpleCurrency2 50 balance is $50.35 balance is (using ToString()) $50.35 Converting to uint gives 50 After converting to float, = 50.35 After converting back to Currency, = $50.34 Now attempt to convert out of range value of–$100.00 to a Currency: Exception occurred: Arithmetic operation resulted in an overflow. |
这个结果显示了到uint的转换是成功的,但丢失了Currency的美分部分(小数部分),把负的float 转换为 Currency也产生了预料中的溢出异常,因为float到Currency的转换本身定义了一个checked环境。
但是,这个输出结果也说明了进行转换时最后一个要注意的潜在问题:结果的第一行没有正确显示结余,显示了50,而不是$50.35。在下面的代码中:
Console.WriteLine(balance);
Console.WriteLine("balance is " + balance);
Console.WriteLine("balance is (using ToString()) " + balance.ToString());
|
只有最后两行把Currency正确显示为一个字符串。这是为什么?问题是在把转换和方法重载合并起来时,会出现另一个不希望的错误源。下面用倒序的方式解释这段代码。
第三行的Console.WriteLine()语句显式调用Currency.ToString()方法,以确保Currency显示为一个字符串。第二行代码没有这么做。字符串"balance is "传送给Console.WriteLine(),告诉编译器这个参数应解释为字符串,因此要隐式地调用Currency.ToString()方法。
但第一行的Console.WriteLine()方法只是把原来的Currency结构传送给Console.Write Line()。目前Console.WriteLine()有许多重载,但它们的参数都不是Currency结构。所以编译器会到处搜索,看看它能把Currency转换为什么,以与Console.WriteLine()的一个重载方法匹配。如上所示,Console.WriteLine()的一个重载方法可以快速而高效地显示uint,且其参数是一个uint。因此应把Currency隐式地转换为uint。
实际上,Console.WriteLine()有另一个重载方法,它的参数是一个double,结果是显示该double的值。如果仔细看看第一个SimpleCurrency示例的结果,就会发现该结果的第一行就是使用这个重载方法把Currency显示为一个double。在这个示例中,没有直接把Currency转换为uint,所以编译器选择Currency→float→double作为可用于Console.WriteLine()重载方法的首选转换方式。但在SimpleCurrency2中可以直接转换为uint,所以编译器会选择后者。
如果方法调用带有多个重载方法,并要给该方法传送参数,而该参数的数据类型不匹配任何重载方法,就可以迫使编译器确定使用哪些转换方式进行数据转换,决定使用哪个重载方法(并进行相应的数据转换)。当然,编译器总是按逻辑和严格的规则来工作,但结果可能并不是我们所期望的。如果可能会出问题,最好显式指定转换路径。
| 回书目 上一节 下一节 |
|
· 第六章 你能帮我吗?.. · Linux笔试面试题选摘测.. · 08年5月软考网管上午真.. · 性能测试从零开始 目录 · 08年5月软考网工上午真.. · 上周拒绝服务攻击(DDo.. |
· 08年5月各大网上书店及.. · 2008年5月24日软考试题.. · 软件设计师专家临考模.. · 上周网络管理员专家自.. · 网络工程师自测获奖名.. · 08年4月各大网上书店及.. |
|
||||
| · NAC安全访问控制 · 网络布线测试仪器 · Windows Server 2008专.. · Windows远程桌面应用 · 网络故障排除宝典 · 运营商封堵ADSL共享 中.. · 解析35岁技术人的价值.. · 世纪枭雄比尔盖茨的王.. |
· 主流品牌防火墙配置 · ASP.NET开发教程 · 超级计算机TOP500专题 · Vista SP1对决XP SP3 · SQL Server 2008/2005.. · 程序员如何成长? · C#技术开发指南 · 虚拟化技术还有点“虚” |
|||
|
||||
| · SOA 面向服务架构 · SQL Server 2008/2005.. · Apache技术专题 · 三层交换技术专题 · SQL Server入门到精通 · Windows远程桌面应用 · C#技术开发指南 · Apache技术专题 |
· Windows集群服务应用 · C#技术开发指南 · 国际文档格式标准开战 · 路由器设置与口令恢复 · Linux 集群技术专题 · PHP开发应用手册 · SOA 面向服务架构 · 企业数据恢复指南 |
|||
|
||||
| · SQL Server入门到精通 · SQL Server 2008/2005.. · SOA 面向服务架构 · Apache技术专题 · C#技术开发指南 · 三层交换技术专题 · Apache技术专题 · C#技术开发指南 |
· Windows远程桌面应用 · 企业数据恢复指南 · Windows集群服务应用 · 路由器设置与口令恢复 · Linux 集群技术专题 · SOA 面向服务架构 · 了解统一威胁管理(UTM).. · 反垃圾邮件技术应用 |
|||