企业网站网页,静态网站更新,宁波网络营销推广外包公司,怎么看一个网站是哪个公司做的C#是静态类型的语言#xff0c;变量一旦声明就无法重新声明或者存储其他类型的数据#xff0c;除非进行类型转换。本章的主要任务就是学习类型转换的知识。类型转换有显式的#xff0c;也有隐式的。所谓显式#xff0c;就是我们必须明确地告知编译器#xff0c;我们要把变…C#是静态类型的语言变量一旦声明就无法重新声明或者存储其他类型的数据除非进行类型转换。本章的主要任务就是学习类型转换的知识。类型转换有显式的也有隐式的。所谓显式就是我们必须明确地告知编译器我们要把变量从源类型转换成什么类型而隐式的则不需要编译器会自动帮我们进行转换。知道装箱和拆箱吗我们将在本文中学习装箱和拆箱的知识。 1 隐式类型转换
什么是隐式转换呢 如果编译器认为从类型1下称T1到类型2下称T2的转换不会产生不良后果那么T1到T2的转换就是由编译器自动完成的这就是隐式转换。我们举个例子如代码清单5-1所示。
代码清单5-1 隐式类型转换
namespace ProgrammingCSharp4
{class TypeConvert{private void DoSomething(){int intValue 10;long longValue intValue;}}
}
第8行执行的是int型到long型的转换long型对应的是System.Int64int型对应的是System.Int32显然long型的取值范围要比int型大因此这种转换是安全的编译器允许了此次转换。为了了解类型转换的实质我们可以通过查看上述代码编译后生成的CIL代码如代码清单5-2所示。
代码清单5-2 CIL代码
.method private hidebysig instance void DoSomething() cil managed
{// Code size 8 (0x8).maxstack 1.locals init([0] int32 intValue, [1] int64 longValue)IL_0000: nopIL_0001: ldc.i4.s 10IL_0003: stloc.0IL_0004: ldloc.0IL_0005: conv.i8IL_0006: stloc.1IL_0007: ret
} // end of method TypeConvert:DoSomething
为了突出重点我们先忽略其他不相关内容只关注与类型转换相关的CIL指令。 第7行类型为i4即int32的数据10入栈 第8行出栈赋予变量[0]即intValue 第9行变量0数据入栈 第10行将栈顶中的数据转换为i8类型即int64也就是long类型 第11行出栈赋予变量[1]即longValue
其中最重要的是第11行编译器生成了类型转换的CIL指令
conv.to type
to type就是要转换到的目标类型。
可见查看CIL代码有助于我们了解编译器所做的实际操作有助于我们更加深刻地理解C#这门语言以及.NET CLR的一些工作机制。在本书的其他章节我们还会通过CIL代码来进行学习。由于CIL的知识超出了本文的范围需要进一步了解CIL的读者可以自行查阅其他资料。
大家现在应该对隐式类型转换有了初步的了解接下来将进一步学习数值类型的隐式转换以及引用类型中的隐式转换。 1.1 数值类型
C#语言支持的数值类型的隐式转换如下所示 sbyte到short、int、long、float、double或decimal byte到short、ushort、int、uint、long、ulong、float、double或decimal short到int、long、float、double或decimal ushort到int、uint、long、ulong、float、double或decimal int到long、float、double或decimal uint到long、ulongfloat、double或decimal long到float、double或decimal ulong到float、double或decimal char到ushort、int、uint、long、ulong、float、double或decimal float到double。
上述的隐式转换是安全的不会造成任何精度或者数量级的损失。需要说明的是C#不支持任何其他类型到char类型的隐式转换。
有两种特殊的隐式转换需要说明之所以说它们特殊是因为它们会带来精度损失但没有数量级损失它们是 从int、uint、long或者ulong到float的转换 long或者ulong到double的转换。
我们还是以一段代码为例演示从int到float的类型转换以此演示精度损失的情况如代码清单5-3所示。
代码清单5-3 精度损失示例
using System;namespace ProgrammingCSharp4
{class TypeConvert{static void Main(string[] args){TypeConvert typeConvert new TypeConvert();typeConvert.DoSomething();}public void DoSomething(){int max int.MaxValue;float floatValue max;Console.WriteLine(max);Console.WriteLine(floatValue);}}
}
上述代码打印了int类型支持的最大有效值MaxValue然后将它赋予了一个float类型的变量floatValue编译器执行了隐式转换。运行结果是
2147483647
2.147484E09
这里2.147484E09表示科学计数法相当于2.147484 × 10^9也就是2,147,484,000。
可以看出转换后的数值比原值的有效位减少了因为原值是2,147,483,647有效位10转换后的值是2,147,484,000有效位7很显然类型转换造成了精度损失但数量级并没有损失。至于另外一种情况——从long或ulong到double的转换请大家自行验证。
我们在学习程序设计时一定要重视实验注意不是“试验”而是实地验证将书本或者课题上讲的内容、知识点进行实际验证。这可以加深我们对知识的理解同时也能积累解决问题的方式和方法。
下一节我们将讲述引用类型的隐式转换。 1.2 引用类型
符合以下情况之一者编译器可以自动实施隐式类型转换并且不需要运行时类型检查 任意引用类型到object类型的转换 派生类型到基类型的转换 派生类型到其实现的接口类型的转换 派生接口类型到基接口类型的转换 数组类型到System.Array类型的转换 委托类型到System.Delegate类型的转换 null类型到所有引用类型的转换。
对于引用类型来说无论是隐式还是显式的类型转换改变的仅仅是引用的类型至于该引用指向的对象的类型以及对象的值都是保持不变的。如图5-1所示它实际改变的是变量1的类型而引用的对象“对象1”则保持类型和值不变。 1.3 装箱
之所以再次讨论装箱是因为装箱也属于类型转换的知识范畴。我们先来看一段示例代码如代码清单5-4所示。
代码清单5-4 装箱
namespace ProgrammingCSharp4
{class Boxing{public void DoSomething(){int x 10;object obj x;}}
}
第7行声明了一个int型变量x并初始化为10。接着第8行声明了一个object类型obj并使用x为其初始化这里既是装箱也是本文讲的类型转换其本质还是类型转换即将int型“装箱”为object类型这个装箱的过程即是隐式的类型转换。
我们仍然通过查看上述代码编译生成的CIL代码来观察装箱的具体过程CIL代码如代码清单5-5所示。
代码清单5-5 DoSomething()函数的CIL代码
.method public hidebysig instance void DoSomething() cil managed
{// Code size 12 (0xc).maxstack 1.locals init ([0] int32 x,[1] object obj)IL_0000: nopIL_0001: ldc.i4.s 10IL_0003: stloc.0IL_0004: ldloc.0IL_0005: box [mscorlib]System.Int32IL_000a: stloc.1IL_000b: ret
} // end of method Boxing:DoSomething
这里只关注与装箱相关的代码对CIL有兴趣的读者可以自行查找相关资料进行学习。
代码清单5-5的第13行是重点box指令指示把栈中的int型值类型变量装箱为引用类型object。经过装箱这一过程后原来的值类型的变量就不存在了取而代之的就是装箱后的引用类型的变量。
另外枚举类型经过装箱以后成为System.Enum类型因为System.Enum类是枚举类型的基类。而结构类型和枚举类型装箱后则为System.ValueType类型原因一样因为System.ValueType类型是所有结构类型和枚举类型的基类。
在本节的最后我们对装箱的类型转换做个总结如下 值类型可隐式转换到object类型或System.ValueType类型 非Nullable值类型可隐式转换到它实现的接口 枚举类型可隐式转换到System.Enum类型。 2. 显式类型转换
显式类型转换又叫做显式强制类型转换、强制类型转换因为不能自动进行转换和隐式类型转换相比而言因而需要显式地告知编译器需要类型转换。隐式类型转换往往是由窄向宽的转换而显式类型转换恰恰相反是由宽向窄的类型转换。以数值类型为例从一个取值范围更大的类型向较小的类型转换时由于可能导致精度损失或引发异常因此编译器不会自动进行隐式转换除非明确告知。因此显式转换也称为收缩转换。
那么该如何告诉编译器我们确定要做这种显式的转换呢很简单只需要在变量前使用一对小括号()运算符小括号中是目标类型。如果未定义相应的()运算符则强制转换会失败。以后我们还将学到还可以使用as运算符进行类型转换如代码清单5-6所示。
代码清单5-6 long类型到int类型的转换
using System;namespace ProgrammingCSharp4
{class TypeConvert{public void DoSomething(){long longValue 10;int intValue (int)longValue;}}
}
编译上述代码编译器会产生如下编译错误
无法将类型“long”隐式转换为“int”。存在一个显式转换是否缺少强制转换?
分析一下为什么会产生这样的错误在代码的第10行我们试图将取值范围更大的long类型隐式地转换为int类型。前面讲过这可能会造成信息丢失因此编译器将之作为一个错误并拒绝进行转换。如果确实要进行转换就需要显式类型转换了即使用()运算符或者as运算符。知道了错误的原因那么只需对第10行做如下修改即可解决问题
int intValue (int)longValue;
这里的()运算符(int)明确告知编译器需要将long转换为int。至此问题解决。
其实所有的隐式类型转换都可以显式地进行类型转换。因此可以说隐式类型转换都是隐藏了()运算符的显式类型转换。例如
int intValue 10;
long longValue (long)intValue; // 等价于 long longValue intValue;
接下来将分别研究数值类型、引用类型的显式类型转换以及拆箱转换和显式类型转换的关系。 2.1 数值类型
在下列情况下由于不存在自动的隐式转换因此必须明确地进行显式类型转换 sbyte到byte、ushort、uint、ulong、char byte到sbyte、char short到sbyte、byte、ushort、uint、ulong、char ushort到sbyte、byte、short、char int到sbyte、byte、short、ushort、uint、ulong、char uint到sbyte、byte、short、ushort、int、char long到sbyte、byte、short、ushort、int、uint、ulong、char ulong到sbyte、byte、short、ushort、int、uint、long、char char到sbyte、byte、short float到sbyte、byte、short、ushort、int、uint、long、ulong、char、decimal double到sbyte、byte、short、ushort、int、uint、long、ulong、char、float、decimal decimal到sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double
我们知道隐式类型转换可以看作省略了()运算符的显式类型转换因此对于数值类型间的转换来说总是使用()运算符也没问题。
但是显式的数值类型转换有可能造成信息丢失或者导致系统抛出异常这也是系统为什么对于这种类型转换要求人工干预并且特别确认的原因。 2.2 溢出检查
当一种整型转换到另一种整型这个过程取决于溢出检查上下文。checked关键字用于对整型算术运算和转换显式启用溢出检查而unchecked关键字则用于取消整型算术运算和转换的溢出检查。
启用溢出检查
操作数的值在目标类型的取值范围内则转换成功否则将抛出一个System.OverflowException异常如代码清单5-7所示。
代码清单5-7 使用checked上下文
using System;namespace ProgrammingCSharp4
{class TypeConvert{static void Main(string[] args){TypeConvert typeConvert new TypeConvert();typeConvert.DoSomething();}public void DoSomething(){// MyInt的值为2147483647.try{int MyInt int.MaxValue;byte MyByte checked((byte)MyInt);}catch (OverflowException){throw;}}}
}
在上述代码中第18行中的int型变量MyInt的值为2,147,483,647在第19行将MyInt强制转换为byte类型后由于byte型的取值范围为0255因为这里使用了checked关键字启用了溢出检查因此这里因为byte型无法容纳远大于其容量的数值而抛出System.OverflowException异常。
取消溢出检查
由于在转换过程将不检查数据是否超过目标类型的取值范围意味着类型转换永远都会成功。如果源类型的取值范围大于目标类型那么超过的部分将被截掉如果源类型的取值范围小于目标类型那么转换后将使用符号或者零填充至与目标类型的大小相等如果等于则直接转换至目标类型如代码清单5-8所示。
代码清单5-8 使用unchecked上下文
namespace ProgrammingCSharp4
{class TypeConvert{static void Main(string[] args){TypeConvert typeConvert new TypeConvert();typeConvert.DoSomething();}public void DoSomething(){// MyInt的值为2147483647.try{int MyInt int.MaxValue;byte MyByte (byte)MyInt;}catch (OverflowException){throw;}}}
}
第17行并没有启用溢出检查因此并没有抛出System.OverflowException异常但转换的值也是有问题的。限于byte类型的取值范围这里赋值后MyByte的值将为255与原始值可以说大相径庭。第19行还可以使用unchecked关键字改写
byte MyByte unchecked((byte)MyInt); 2.3 引用类型
引用类型不同于值类型它由两部分组成栈中的变量和堆中的对象。对于引用类型的显式类型转换来说转换的是栈中变量的类型而该变量指向的位于堆中的对象则类型和数据都不受影响。一般来说从基类向派生类的转换需要显式转换因为基类“宽”而派生类“窄”故而必须进行显式类型转换。
符合下列情况之一的需要进行显式类型转换 object类型到任何引用类型的转换任何引用类型都是object类型的子类 基类到派生类的转换 类到其实现的接口的转换 非密封类到其没有实现接口的转换 接口到另一个不是其基接口的转换 System.Array类型到数组类型的转换 System.Delegate类型到委托类型的转换。
显式类型转换的结果是否成功只有在运行时才能知道转换失败则会抛出System.InvalidCastException异常。 2.4 拆箱
与装箱相反从引用类型到值类型的转换称为拆箱。符合以下条件之一的进行拆箱操作 从object类型或System.ValueType到值类型的转换 从接口类型到值类型实现了该接口的转换 从System.Enum类型到枚举类型的转换。
在执行拆箱操作前编译器会首先检查引用类型是否是某个值类型或枚举类型的“装箱”版本如果是就将其值拷贝出来还原为值类型的变量。 3. as和is运算符
我们知道隐式转换是安全的而显式转换往往是不安全的有可能造成精度损失甚至会抛出异常。但类型转换又是不可避免的例如对于某些集合类型常常会用到System.Object类型的变量使用泛型可以避免这种情况对于非泛型集合在将数据放入集合时将发生“向上转型”即当前类型信息丢失数据的类型成了object类型而当需要把数据从集合取出时因为需要恢复数据的本来类型因此也就需要执行“向下转型”到它本来的类型。因此如何更安全地进行类型转换就是一个值得探讨的问题了。幸好C#已经为我们提供了解决方案我们有两种选择 使用as运算符进行类型转换 先使用is运算符判断类型是否可以转换再使用()运算符进行显式类型转换。
那么我们先来介绍一下as和is运算符
as运算符用于在两个引用类型之间进行转换如果转换失败则返回null并不抛出异常因此转换是否成功可以通过结果是否为null进行判断并且只能在运行时才能判断。
代码示例
using System;namespace ProgrammingCSharp4
{class Class1 { }class Class2 { }class TypeConvert{static void Main(string[] args){object[] objArray new object[6];objArray[0] new Class1();objArray[1] new Class2();objArray[2] hello;objArray[3] 123;objArray[4] 123.4;objArray[5] null;for (int i 0; i objArray.Length; i){string s objArray[i] as string;Console.Write({0}, i);if (s ! null){Console.WriteLine(是string类型其值为 s );}else{Console.WriteLine(不是string类型);}}}}
}
这段代码用到了前面讲过的知识数组、Console对象、命名空间、类也有如for循环、if判断等。
编译运行上述代码输出结果为
0不是string类型
1不是string类型
2是string类型其值为hello
3不是string类型
4不是string类型
5不是string类型
特别要注意的是as运算符有一定的适用范围它只适用于引用类型或可以为null的类型而无法执行其他转换如值类型的转换以及用户自定义的类型转换这类转换应使用强制转换表达式来执行。
is运算符用于检查对象是否与给定类型兼容并不执行真正的转换。如果判断的对象引用为null则返回false。由于仅仅判断是否兼容因此它并不会抛出异常。用法如下
if (obj is MyObject)
{// 其他操作...
}
上述代码可以确定obj变量是否是MyObject类型的实例或者是MyObject类的派生类。
同样也要注意is的适用范围它只适用于引用类型转换、装箱转换和拆箱转换。而不支持其他的类型转换如值类型的转换。
现在我们已经了解了as和is运算符在实际工作中建议尽量使用as运算符而少使用()运算符显式转换。理由如下 无论是as还是is运算符都比直接使用()运算符强制转换更安全 不会抛出异常免除了使用try...catch进行异常捕获的必要和系统开销只需要判断是否为null 使用as比使用is性能上更好这一点可以通过代码清单5-9来说明。
代码清单5-9 as和is运算符的性能对比
using System;
using System.Diagnostics;namespace ProgrammingCSharp4
{class Class1 { }class AsIsSample{private Class1 c1 new Class1();public static void Main(){AsIsSample aiSample new AsIsSample();Stopwatch timer new Stopwatch();timer.Start();for (int i 0; i 10000; i){aiSample.DoSomething1();}timer.Stop();decimal micro timer.Elapsed.Ticks / 10m;Console.WriteLine(执行DoSomething1() 10000次的时间{0:F1} 微秒., micro);timer new Stopwatch();timer.Start();for (int i 0; i 10000; i){aiSample.DoSomething2();}timer.Stop();micro timer.Elapsed.Ticks / 10m;Console.WriteLine(执行DoSomething2() 10000次的时间{0:F1} 微秒., micro);}public void DoSomething1(){object c2 c1;if (c2 is Class1){Class1 c (Class1)c2;}}public void DoSomething2(){object c2 c1;Class1 c c2 as Class1;if (c ! null){// 其他操作...}}}
}
输出为
执行DoSomething1() 10000次的时间288.9 微秒.
执行DoSomething2() 10000次的时间258.6 微秒.
从第37行开始声明并定义了两个方法DoSomething1和DoSomething2其中分别使用is和as运算符进行类型转换。在第18行和第28行对每个方法分别连续调用10,000次通过使用BCL中的Stopwatch对象对两者的调用时间进行统计。从结果可以看出DoSomething2()的性能比DoSomething1()要好。至于原因可以通过查看DoSomething1和DoSomething2两个方法的CIL代码来一探究竟。方法DoSomething1的CIL代码如代码清单5-10所示。
代码清单5-10 方法DoSomething1的CIL代码
.method public hidebysig instance void DoSomething1() cil managed
{// Code size 34 (0x22).maxstack 2.locals init ([0] object c2, [1] class ProgrammingCSharp4.Class1 c, [2] bool CS$4$0000)IL_0000: nopIL_0001: ldarg.0IL_0002: ldfld class ProgrammingCSharp4.Class1 ProgrammingCSharp4.AsIsSample::c1IL_0007: stloc.0IL_0008: ldloc.0IL_0009: isinst ProgrammingCSharp4.Class1IL_000e: ldnullIL_000f: cgt.unIL_0011: ldc.i4.0IL_0012: ceqIL_0014: stloc.2IL_0015: ldloc.2IL_0016: brtrue.s IL_0021IL_0018: nopIL_0019: ldloc.0IL_001a: castclass ProgrammingCSharp4.Class1IL_001f: stloc.1IL_0020: nopIL_0021: ret
} // end of method ProgrammingCSharp4.AsIsSample::DoSomething1
代码清单5-10的第13行首先测试了是否能转换到Class1类型如果可以则进行转换第23行再次测试能否转换到Class1类型如果测试成功则进行转换。
方法DoSomething2的CIL代码如代码清单5-11所示。
代码清单5-11 方法DoSomething2的CIL代码
.method public hidebysig instance void DoSomething2() cil managed
{// Code size 26 (0x1a).maxstack 2.locals init ([0] object c2, [1] class ProgrammingCSharp4.Class1 c, [2] bool CS$4$0000)nopldarg.0ldfld class ProgrammingCSharp4.Class1 ProgrammingCSharp4.AsIsSample::c1stloc.0ldloc.0isinst ProgrammingCSharp4.Class1stloc.1ldloc.1ldnullceqstloc.2ldloc.2brtrue.s IL_0019nopnopret
} // end of method ProgrammingCSharp4.AsIsSample::DoSomething2
代码清单5-11的第11行同样测试了能否转换到Class1类型如果可以则进行转换。
由此可见前者进行了两次测试和检查而后者只进行了一次测试这是造成两者之间性能差异的原因。
现在我们总结下什么场合该使用is什么场合该使用as如果测试对象的目的是确定它是否属于所需类型并且如果测试结果为真就要立即进行转换这种情况下使用as操作符的效率更高但有时仅仅只是测试并不想立即转换也可能根本就不会转换只是在对象实现了接口时要将它加到一个列表中这时is操作符就是一种更好的选择。