C#图解教程之转换

作者 Zhendong Ho 日期 2019-08-01
C#
C#图解教程之转换

什么是转换

把一个变量(源)的值赋值给另一个变量(目标)。在赋值之前,源的值必须转换成目标类型的值

16-1

例:两个不同类型的变量的赋值。

short var1 = 5;
sbyte var2 = 10;

var2 = (sbyte)var1; // 强制转换表达式,把var1值转换为sbyte类型

16-2

隐式转换

有些类型的转换不会丢失数据或精度。如短类型转化为长类型

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

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

16-3

对于有符号类型的转换而言,额外的高位用源表达式的符号位进行填充。这样维持了被转换的值的正确符号和大小。这叫做符号扩展

例:10和-10的转换。

16-4

显式转换和强制转换

长类型转化为短类型的时候,有可能会损失精度。

例:把1365的ushort类型转换为byte类型会导致数据丢失。

16-5

因为,ushort的范围是0~65535,而byte的范围是0~255。源值是1365,而目标的最大值只能是255,所以源值的最高位的数据会丢失。最终结果值为85。

强制转换

对于预定义的类型,C#会自动完成类型转换。但是只针对那些从源类型到目标类型不会发生数据丢失的情况

对于会发生数据丢失的情况,必须使用强制转换表达式,这就是显式转换

例:强制转换表达式将两个ushort类型的值转换为byte类型。第一种没有数据丢失,第二种最高位丢失了。

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

sh = 1365;
sb = (byte)sh;
Console.WriteLine("sb: {0} = 0x{0:X}", sb); // sb: 85 = 0x55

16-6

注意:sb = (byte)sh,即将长类型转换为短类型,必须使用强制转换,否则会编译错误。

转换的类型

有很多标准的、预定义的用于数字和引用类型的转换。

16-7

注意

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

数字的转换

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

16-8

隐式数字转换

源类型到目标类型按照箭头进行隐式转换。如果上方没有箭头,则需要显式转换占据较少位的数字类型可以隐式转换为占据较多位的数字类型

16-9

溢出检测上下文

对于整数类型,C#给我们提供了选择运行时是否应该在进行类型转换时检测结果溢出的能力。通过checked运算符和checked语句来实现。

  • 代码片段是否被检查称作溢出检测上下文

    • 如果我们指定一个表达式或一段代码为checked,CLR会在转换产生溢出时抛出一个OverflowException异常。

    • 如果代码不是checked,转换会继续而不管是否产生溢出。

  • 默认的溢出检测上下文是不检查

checked和unchecked运算符

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

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

例:在unchecked上下文中,会忽略溢出,结果值是208。在checked上下文中,抛出OverflowException异常。

ushort sh = 2000;
byte sb;

sb = unchecked((byte)sh); // 高位的数丢失了
Console.WriteLine("sb: {0}", sb); // 208

sb = checked((byte)sh); // 抛出OverflowException异常 Unhandled Exception: System.OverflowException: ...
Console.WriteLine("sb: {0}", sb);

checked语句和unchecked语句

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

例:checked语句和unchecked语句可以被嵌套在任意层次。

byte sb;
ushort sh = 2000;

unchecked // 设置unchecked
{
sb = (byte)sh;
Console.WriteLine("sb: {0}", sb); // 显式溢出结果

checked // 设置checked
{
sb = (byte)sh; // 抛出OverflowException异常
Console.WriteLine("sb: {0}", sb);
}
}

显式数字转换

显式转换则可能会丢失数据。因此,作为一个程序员,知道发生数据丢失时转换会如何处理很重要。

16-10

整数类型到整数类型

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

16-11

float或double转到整数类型

把浮点类型转换为整数类型时,值会舍掉小数截断为最接近的整数

如果截断后的值不在目标类型的范围内:

  • 如果溢出检测上下文是checked,则CLR会抛出OverflowException异常。
  • 如果上下文是unchecked,则C#将不定义它的值应该是什么。

16-12

decimal到整数类型

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

16-13

double到float

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

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

16-14

float或double到decimal

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

16-15

decimal到float或double

从decimal类型转换到float类型总是会成功的。然而,可能会损失精度

16-16

引用转换

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

例:引用转换示例。

class A
{
public int Field1;
}

class B : A
{
public int Field2;
}

class Program
{
static void Main()
{
B myVar1 = new B();
A myVar2 = (A)myVar1; // 作为A类的引用返回myVar1的引用
Console.WriteLine("{0}", myVar2.Field1); // 正确
Console.WriteLine("{0}", myVar2.Field2); // 编译错误,Field2对myVar2不可见
}
}

隐式引用转换

隐式引用转换,与语言为我们自动实现的隐式数字转换类似。

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

委托可以隐式转换成.NET BCL类和接口。如:System.Delegate、System.MulticastDelegate等。

ArrayS数组,其中的元素是Ts类型,可以隐式转换成:

  • .NET BCL类和接口,如:System.ICloneable、System.IList、System.IEnumerable等。
  • 另一个数组ArrayT,其中的元素是Tt类型(如果满足下面的所有条件)。
    1. 两个数组有一样的维度。
    2. 元素类型Ts和Tt都是引用类型。
    3. 在类型Ts和Tt中存在隐式转换。

显式引用转换

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

显式转换包括:

  • 从object到任何引用类型的转换。
  • 从基类到从它继承的类的转换。

倒转图16-18、16-19的箭头方向,即为显式引用转换

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

例:错误的显式引用转换。

class A
{
public int Field1;
}

class B : A
{
public int Field2;
}

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

有效显式引用转换

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

第一种情况

显式转换是没有必要的。也就是说,语言已经为我们进行了隐式转换。衍生类到基类的转换总是隐式转换的

B myVar1 = new B();
A myVar2 = (A)myVar1; // 不必转换,因为A是B的基类

第二种情况

源引用是null。转换基类的引用到衍生类的引用是不安全的,但是由于源引用是null,这种转换还是允许的。

A myVar1 = null;
B myVar2 = (B)myVar1; // 允许转换,因为myVar1为空

第三种情况

由源引用指向的实际数据可以被安全地进行隐式转换

B myVar1 = new B();
A myVar2 = myVar1; // 将myVar1隐式转换为A类型
B myVar3 = (B)myVar2; // 该转换是被允许的,因为数据是B类型的

装箱转换

包括值类型在内的所有C#类型都派生自object类。然而,值类型是高效轻量的类型,因为默认情况下在堆上不包括它们的对象组件。然而,如果需要对象组件,可以使用装箱(boxing)

装箱是一种隐式转换,它接受值类型的值,根据这个值在堆上创建一个完整的引用类型对象并返回对象引用。

例:装箱示例。

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

系统将i的值装箱过程如下

  • 在堆上创建了int类型的对象。
  • 将i的值复制到int对象。
  • 返回int对象的引用,让oi作为引用保存。

装箱是创建副本

装箱返回的是值的引用类型副本。在装箱产生之后,该值有两份副本——原始值类型和引用类型副本,每一个都可以独立操作。

例:装箱创建了一份可以被独立操作的副本。

int i = 10;	// 创建并初始化值类型
object oi = i; // 对i装箱并把引用赋值给oi
Console.WriteLine("i: {0}, io: {1}", i, oi); // i: 10, io: 10

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

装箱转换

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

拆箱转换

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

系统在拆箱时,执行了如下操作:

  • 检测到要拆箱的对象实际是ValueTypeT的装箱值(实际上必须是一个值类型)。
  • 把对象的值复制到变量。

例:拆箱示例。

int i = 10;
object oi = i; // 对i装箱并把引用赋值给oi
int j = (int)oi; // 对oi拆箱并把值赋值给j
Console.WriteLine("i: {0}, oi: {1}, j: {2}", i, oi, j); // i: 10, oi: 10, j: 10

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

下图显示了拆箱转换。

用户自定义转换

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

用户自定义转换语法:

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

例:Person类型的对象转换为int示例。

class Person
{
public int Age { get; set; }

public static implicit operator int(Person p)
{
return p.Age;
}
}

class Program
{
static void Main()
{
Person p = new Person();
p.Age = 24;
int age = p;
Console.WriteLine(age); // 24
Console.ReadKey();
}
}

用户自定义转换的约束

用户自定义转换有一些重要约束

  • 只可以为类或结构定义用户自定义转换。
  • 不能重定义标准隐式转换或显式转换。
  • 对于源类型S和目标类型T,如下命题为真。
    1. S和T必须是不同类型
    2. S和T不能通过继承关联
    3. S和T都不能是接口类型或object类型
  • 对于相同的源和目标类型,我们不能声明隐式转换和显式转换。

用户自定义转换的示例

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) // 将Person转换为int
{
return p.Age;
}

public static implicit operator Person(int i) // 将int转换为Person
{
return new Person("Nemo", i);
}
}

class Program
{
static void Main()
{
Person bill = new Person("bill", 25);

int age = bill; // 把Person对象转换成int
Console.WriteLine("Person Info: {0}, {1}", bill.Name, age);

Person anon = 35;
Console.WriteLine("Person Info: {0}, {1}", anon.Name, anon.Age);
}
}

输出结果:

如果使用explicit运算符而不是implicit来定义相同的转换。需要使用强制转换表达式来进行转换。

...
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;

// 将Person对象转换为int
public static implicit operator int(Person p)
{
return p.Age;
}
}

class Program
{
static void Main()
{
Employee bill = new Employee();
bill.Name = "William";
bill.Age = 25;
float fVar = bill; // 把Employee转换成float
Console.WriteLine("Person Info: {0}, {1}", bill.Name, fVar); // Person Info: William, 25
}
}

is运算符

可以使用is运算符来检查转换是否成功完成,从而避免盲目尝试转换。

例:is运算符示例。

class Employee : Person { }

class Person
{
public string Name = "Anonymous";
public int Age = 25;
}

class Program
{
static void Main()
{
Employee bill = new Employee();
Person p;

// 检测变量bill是否能转换为Person类型
if (bill is Person)
{
p = bill;
Console.WriteLine("Person Info: {0}, {1}", p.Name, p.Age); // Person Info: Anonymous, 25
}
}
}

注意:is运算符只可以用于引用转换以及装箱、拆箱转换,不能用于用户自定义转换

as运算符

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

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

例:as运算符示例。

class Employee : Person { }

class Person
{
public string Name = "Anonymous";
public int Age = 25;
}

class Program
{
static void Main()
{
Employee bill = new Employee();
Person p;

p = bill as Person;
if (p != null)
{
Console.WriteLine("Person Info: {0}, {1}", p.Name, p.Age);
}
}
}

注意:as运算符不能用于用户自定义转换到值类型的转换。