C#图解教程 第十六章 转换

转换

什么是转换


要理解什么是转换,让我们先从声明两个不同类型的变量,然后把一个变量(源)的值赋值给另一个变量(目标)的简单示例开始讲起。在赋值前,源的值必须转换成目标类型的值。


  • 转换(conversion)是接受一个类型的值并使用它作为另一个类型的等价值的过程
  • 转换后的值应和源值一样的,但其类型为目标类型

例:

  • var1是short类型的16位符号整数,初始值为5。var2是byte类型的8位有符号整数,初始值为10
  • 第三行代码把var1赋给var2。由于它们类型不同,在赋值前,var1的值必须先转换为与var2类型相同的值类型。这将通过强制转换表达式来实现
  • var1的类型和值都没有改变。
short var1=5;
sbyte var2=10;
...
var2=(sbyte)var1;

隐式转换


有些类型的转换不会丢失数据或精度。

  • 语言会自动做这些转换,这叫做隐式转换
  • 从位数更少的源转换为位数更多的目标类型时,目标中多出来的位需要用0或1填充
  • 当从更小的无符号类型转换为更大的无符号类型时,目标类型多出来的最高位都以0进行填充,这叫做零扩展(zero extension)

例:使用零扩展把8位的10转换为16位的10


对于有符号类型的转换而言,额外的高位用源表达式的符号位进行填充。

  • 这样就维持了被转换的值的正确符号和大小
  • 这叫做符号扩展(sign extension)

例:10和-10的转换


显式转换和强制转换


如果需要把长类型转换为短类型,目标类型也许无法在不损失数据的情况下提供源值。
例:把1365的ushort转换为byte,数据丢失,因为目标类型最大值只能是255.最终字节中的后8位保留,值为85。


强制转换

对于预定义类型,C#会自动完成类型转换,但只是针对哪些从源类型到目标类型不会发生数据丢失的情况。
对于会发生数据丢失的情况,必须使用显式转换–强制转换表达式。
强制类型转换格式:

目标类型
  ↓
(sbyte)var1;
         ↑
      源表达式

如果我们使用强制转换表达式,就要承担执行操作可能引起的丢失数据的结果。
例:两个ushort转byte

ushort sh=10;
byte sb=(byte)sh;
Console.WriteLine("sb: {0} =0x{0:X}",sb);
sh=1365;
sb=(byte)sh;
Console.WriteLine("sb: {0} =0x{0:X}",sb);

转换的类型


有很多标准的、预定义的用于数字和引用类型的转换。下图演示了这些不同的转换类型。


  • 除了标准转换,还可以为自定义类型定义隐式转换和显式转换
  • 还有一个预定义转换类型,叫做装箱,可以将任何值类型转换为:
    • object类型
    • System.ValueType类型
  • 拆箱可以将一个装箱的值转换为原始类型

数字的转换


任何数字都可以转换为其他数字类型。
一些转换是隐式的,而另外一些转换必须是显式的。


隐式数字转换
  • 如果有路径,从源类型到目标类型可以按照箭头进行隐式转换
  • 任何从源类型到目标类型的箭头方向上没有路径的数字转换都必须是显式转换

溢出检测上下文

我们已经知道了,显式转换可能会丢失数据并且不能在目标类型中同等地表示源值。对于整数类型,C#给我们提供了选择运行时是否应该在进行类型转换时检测结果溢出的能力。这将通过checked运算符和checked语句来实现。

  • 代码片段是否被检查称作溢出检测上下文
    • 如果我们指定一个表达式或一段代码为checked,CLR会在转换产生溢出时抛出一个OverflowException异常
    • 如果代码不是checked,转换会继续而不管是否产生溢出
  • 默认的溢出检测上下文是不检查
1.checked和unchecked运算符

checked和unchecked运算符控制表达式的溢出检测上下文。表达式放置在一对圆括号内并且不能是一个方法。

checked(表达式)
unchecked(表达式)

例:

  • 在unchecked上下文中,会忽略溢出,结果值是208
  • 在checked上下文中,抛出OverflowException异常
ushort sh=2000;
byte sb;
sb=unchecked((byte)sh);
Console.WriteLine("sb:{0}",sb);
sb=checked((byte)sh);
Console.WriteLine("sb:{0}",sb);

2.checked语句和unchecked语句

checked和unchecked运算符用于圆括号内的单个表达式。而checked和unchecked语句执行相同的功能,但控制的是一块代码中的所有转换,而不是单个表达式。

  • checked语句和unchecked语句可以被嵌套在任意层次

例:checked语句影响一段代码

ushort sh=2000;
byte sb;
unchecked
{
    sb=(byte)sh;
    Console.WriteLine("sb:{0}",sb);
    checked
    {
        sb=(byte)sh;
        Console.WriteLine("sb:{0}",sb);
    }
}
显式数字转换

显式数字转换,可能丢失数据。因此,作为一个程序员,知道发生数据丢失时转换会如何处理很重要。
在本节中,我们来看看各种显式数字转换。


1.整数类型到整数类型

在checked的情况下,如果转换会丢失数据,操作会抛出一个OverflowException异常。
在unchecked的情况下,丢失的位不会发出警告。


2.float或double转到整数类型

当把浮点类型转换为整数类型时,值会舍掉小数截断为最接近的整数。
如果截断后的值不在目标类型的范围内:

  • checked下,CLR会抛出OverflowException异常
  • unchecked下,则C#不定义它的值应该是什么


3.decimal到整数类型

从decimal转换到整数类型时,如果结果值不在目标类型的范围内,CLR会抛出OverflowException异常。


4.double到float

float类型的值占32位,而double类型的值占64位。double类型的值被舍入到最接近的float类型的值。

  • 如果值太小而不能用float表示,那么值会被设置为正或负0
  • 如果值太大而不能用float表示,那么值会被设置为正无穷大或负无穷大

5.float或double转decimal
  • 如果值太小而不能用decimal类型表示,那么值会被设置为0
  • 如果值太大,那么CLR会抛出OverflowException异常

6.decimal到float或double

decimal转float类型总会成功。然而可能会损失精度。


引用转换


我们已经知道引用类型对象由内存中的两部分组成:引用和数据。

  • 由引用保存的那部分信息是它指向的数据类型
  • 引用转换接受源引用并返回一个指向堆中同一位置的引用,但是把引用“标记”为其他类型

例:引用转换示例

  • myVar1,它引用的对象看上去是B类型–其实就是
  • myVar2,同样的对象看上去像类型A的对象
    • 即使它实际指向B类型的对象,它也不能看到B扩展到A的部分,因此不能看到Field2
    • 第二个WriteLine语句因此产生编译错误
class A {public int Field1;}
class B:A{public int Field2;}
class Program
{
    static void Main()
    {
        B myVar1=new B();
        A myVar2=(A)myVar1;
        Console.WriteLine("{0}",myVar2.Field1);
        Console.WriteLine("{0}",myVar2.Field2);//编译错误
    }
}

隐式引用转换

与自动隐式数字转换类似,还有隐式引用转换

  • 所有引用类型可以被隐式转换为object类型
  • 任何类型可以隐式转换到它继承的接口
  • 类可以隐式转换到:
    • 它继承链中的任何类
    • 它实现的任何接口

委托可以隐式转换成.NET BCL类和接口。ArrayS数组,其中的元素是是Ts类型,可以隐式转换成:

  • .NET BCL类和接口
  • 另一个数组ArrayT,其中的元素是Tt类型(如果满足下面的所有条件)
    • 两个数组维度相同
    • Ts、Tt都是引用类型
    • 在Ts、Tt中存在隐式转换

显式引用转换

显式引用转换是从一个普通类型到一个更精确类型的引用转换。

  • 显式转换包括:
    • 从object到任何引用类型的转换
    • 从父类到子类的转换
  • 倒转图16-18、16-19的箭头方向

如果转换的类型不受限制,很可能会导致我们很容易地尝试引用在内存中实际不存在的类成员。然而,编译器确实允许这样的转换。只不过,系统运行时遇到它们会抛出异常。

例:错误的显式引用转换

  • 如果myVar2尝试访问Field2,它会尝试访问对象中“B部分”的字段(它不在内存中),这会导致内存错误
  • 运行时会捕获这种错误的强制转换并抛出InvalidCastException异常(它不会导致编译错误)
class A {public int Field1;}
class B:A{public int Field2;}
class Program
{
    static void Main()
    {
        B myVar1=new B();
        A myVar2=(A)myVar1;
    }
}

有效显式引用转换

在运行时能成功进行(不抛出InvalidCastException异常)的显示转换有3种情况。

第一种情况:显式转换是没必要的。即语言已经为我们进行了隐式转换。例如,从衍生类到基类的转换总是隐式转换。

class A{}
class B:A{}
...
B myVar1=new B();
A myVar2=(A)myVar1;

第二种情况:源引用是null。

class A{}
class B:A{}
...
A myVar1=null;
B myVar2=(B)myVar1;

第三种情况:由源引用指向的实际数据可以被安全地进行隐式转换。

B myVar1=new B();
A myVar2=myVar1;    //将myVar1隐式转换为A类型
B myVar3=(B)myVar2;

装箱转换


包括值类型在内的所有C#类型都派生自object类型。然而,值类型是高效轻量的类型,因为默认情况下在堆上不包括它们的对象组件。然而,如果需要对象组件,我们可以使用装箱(boxing)。装箱是一种隐式转换,它接受值类型的值,根据这个值在堆上创建一个完整的引用类型对象并返回对象引用。

例:装箱示例

int i=12;
object oi=null;
oi=i;

装箱是创建副本

在装箱后,该值有两份副本–原始值类型和引用类型副本,每个都可以独立操作。

int i=10;
object oi=i;
Console.WriteLine("i:{0},io:{1}",i,oi);
i=12;
oi=15;
Console.WriteLine("i:{0},io:{1}",i,oi);

装箱转换

下图演示了装箱转换。任何值类型ValueTypeS都可以被隐式转换为object类型、System.ValueTpye或InterfaceT(如果ValueTypeS实现了InterfaceT)。


拆箱转换


拆箱(unboxing)是把装箱后的对象转换回值类型的过程。

  • 拆箱是显式转换
  • 系统在拆箱时执行如下步骤:
    • 它检测到要拆箱的对象实际是ValueTypeT的装箱值
    • 它把对象的值复制到变量
class Program
{
    static void Main()
    {
        int i=10;
        object oi=i;
        int j=(int)oi;
        Console.WriteLine("i:{0},oi:{1},j:{2}",i,oi,j);
    }
}

尝试将一个值拆箱为非原始类型会抛出一个InvalidCastException异常。

用户自定义转换


除了标准转换,我们还可以为类和结构定义隐式和显式转换。

用户自定义转换语法:

  • 除了implict和explicit关键字外,隐式和显式转换的声明语法一样
  • 需要public和static修饰符
    必须的              运算符     关键字          源
      ↓                   ↓         ↓            ↓
public static implicit operator TargetType(SourceType Identifier)
{
    ...
    return ObjectOfTargetType;
}

例:自定义转换示例

public static implicit operator int (Person p)
{
    return p.Age;
}
用户自定义转换的约束

自定义转换有些很重要的约束:

  • 只可以为类和结构定义用户自定义转换
  • 不能重定义标准隐式转换或显式转换
  • 对于源类型S和目标类型T,如下命题为真
    • S和T必须是不同类型
    • S和T不能通过继承关联
    • S和T都不能是接口类型或object类型
    • 转换运算符必须是S或T的成员
用户自定义转换示例
class Person
{
    public string Name;
    public int Age;
    public Person(string name,int age)
    {
        Name=name;
        Age=age;
    }
    public static implicit operator int(Person p)
    {
        return p.Age;
    }
    public static implicit operator Person(int i)
    {
        return new Person("Nemo",i);//"Nemo" is Latin for "No one".
    }
}
class Program
{
    static void Main()
    {
        Person bill=new Person("bill",25);
        int age=bill;
        Console.WriteLine("Person Info:{0},{1}",bill.Name,age);
        Person anon=35;
        Console.WriteLine("Person Info:{0},{1}",anon.Name,anon.Age);
    }
}

如果使用explicit运算符来定义转换,需要使用强制转换表达式来进行转换。

...
public static explicit operator int(Person p)
{
    return p.Age;
}
...
static void Main()
{
    ...
    int age=(int)bill;//必须强制转换表达式
    ...
}
评估用户自定义转换

到目前为止讨论的用户自定义转换都是在一步完成。

但是,用户自定义转换在完成转换中最多可以有3个步骤。

  • 预备标准转换
  • 用户自定义转换
  • 后续标准转换

在这个链中不能有一个以上的用户自定义转换。


多步用户自定义转换示例
class Employee:Person{}
class Person
{
    public string Name;
    public int Age;
    public static implicit operator int(Person p)
    {
        return p.Age;
    }
}
class Program
{
    static void Main()
    {
        var bill=new Employee();
        bill.Name="William";
        bill.Age=25;
        float fVar=bill;
        Console.WriteLine("Person Info:{0},{1}",bill.Name,fVar);
    }
}

is运算符


之前说过,有些转换会失败,并会在运行时抛出InvalidCastException异常。
我们可以使用is运算符来检查转换是否会成功完成,从而避免盲目尝试转换。

is运算符语法如下,Expr是源表达式:

Expr is TargetType//返回bool值

例:is运算符示例

class Employee:Person{}
class Person
{
    public string Name="Anonymous";
    public int Age=25;
}
class Program
{
    static void Main()
    {
        var bill=new Employee();
        Person p;
        if(bill is Person)
        {
            p=bill;
            Console.WriteLine("Person Info:{0},{1}",p.Name,p.Age);
        }
    }
}

is运算符只可用于引用转换、装箱、拆箱,不能用于用户自定义转换。

as运算符


as运算符和强制转换运算符类似,只是它不抛出异常。如果转换失败,它返回null而不是抛出异常。

as运算符语法如下:

  • Expr是源表达式
  • TargetType是目标类型,它必须是引用类型
Expr as TargetType//返回引用

由于as运算符返回引用表达式,它可以用作赋值操作中的源。

例:as运算符示例

class Employee:Person{}
class Person
{
    public string Name="Anonymous";
    public int Age=25;
}
class Program
{
    static void Main()
    {
        var bill=new Employee();
        Person p;
        p=bill as Person;
        if(p!=null)
        {
            Console.WriteLine("Person Info:{0},{1}",p.Name,p.Age);
        }
    }
}

和is运算符类似,as运算符只能用于引用转换和装箱转换。它不能用于用户自定义转换或到值类型的转换。

posted @ 2017-02-06 08:43  只追昭熙  阅读(1863)  评论(0编辑  收藏  举报