C#学习笔记 -- 转换
1、什么是转换
声明两个不同类型的变量, 然后把一个源变量的值赋值给一个目标变量, 在赋值之前, 必须转换源变量的类型为目标变量类型
-
转换是接受一个类型的值并将它用作另一个类型的等价值的过程
-
转换后的值应该和源值引用, 但其类型为目标类型
short var1 = 5; sbyte var2 = 10; var2 = (sbyte) var1; //强制转换表达式,把var1的值转换为sbyte
2、隐式转换
有些类型的转换不会丢失数据或精度, 例如将8位的值转换为16位是非常容易的, 并且不会丢失数据
-
语言会自动做这些转换, 叫做隐式转换
(1)无符号转换
-
从位数更少的源类型转换为位数更多的目标类型时, 目标中多出来的位需要用0或1填充
-
当从更小的无符号类型转换为更大的无符号类型时, 目标类型多数来的最高位都用0进行填充, 这叫做0扩展
(2)有符号类型
-
额外的最高位用源表达式的符号位进行填充
-
维持了被转换的值的正确符号和大小
-
这叫做符号扩展
3、显式转换和强制转换
除了把短类型转换为长类型, 其他情况下, 目标类型也许无法在不损失数据的情况下容纳源值
强制转换
-
对于预定义的类型, 会自动将一个数据类型转换为另一个, 但只是针对哪些从源类型转换为目标类型时不会发生数据丢失的情况
-
对于源类型的任意值在被转换成目标类型时会丢失值的情况时, 不会为这俩类型提供自动转换, 如果希望对这样的类型进行转换, 就必须使用显式转换, 叫做强制转换表达式
sbyte v = (sbyte) var1;
-
如果使用强制转换表达式就意味着要承担执行操作可能引起的丢失数据的后果
//高转低 数据小 ushort sh = 10; byte sb = (byte) sh; //10 //高转低 数据大 sh = 1365; sb = (byte) sh; //85
4、转换的类型
-
除了标准转换还可以为用户自定义隐式类型转换和显式转换
-
还有一个预定义的转换类型, 叫做装箱, 可以将任何值类型转换为
-
object
-
System.ValueType
-
-
拆箱可以将一个装箱的值转换为原始类型
5、数字的转换
任何数字都可以转换为其他类型, 一些是隐式的, 另外一些是显示的
(1)隐式数字转换
-
如果沿着箭头存在一条能从源类型到目标类型的路径, 则存在从源类型到目标类型的隐式转换
-
沿着箭头不存在从源类型到目标类型的路径的数字转换一定是显式转换
(2)溢出监测上下文
-
对于整数类型, 使用
checked
运算符和checked
语句类实现转换检测结果溢出 -
代码片段是否被监测称作溢出监测上下文
-
如果指定一个表达式或一段代码为
checked
, 会在转换产生溢出时, 抛出一个OverflowException
异常 -
如果代码不是
checked
, 转换会继续而不管是否产生溢出
-
-
默认的溢出是不检查
a.checked
和unchecked
运算符
控制表达式的溢出监测上下文,表达式防止在一对圆括号内并且不能是一个方法
checked (表达式) unchecked (表达式)
static void Main(string[] args) { ushort sh = 2000; byte sb = unchecked((byte)sh); //丢失数据 Console.WriteLine($"unchecked sb = {sb}"); //208 sb = checked((byte)sh); Console.WriteLine($"checked sb = {sb}"); //OverflowException }
b.checked
和unchecked
语句
-
语句和运算符执行相同的功能, 但是控制的是一块代码中的所有转换, 不是单个表达式
-
可以被嵌套在任意层次
-
影响的是一段代码
checked { } unchecked { }
static void Main(string[] args) { ushort sh = 2000; byte sb; checked { unchecked { sb = (byte)sh; Console.WriteLine($"unchecked sb = {sb}"); //208 } sb = (byte)sh; Console.WriteLine($"checked sb = {sb}"); //OverflowException } }
(3)显式数字转换
a.整数到整数类型
-
在checked的情况下, 如果转换会丢失数据, 则会抛出异常
-
在unchecked的情况下, 丢失的位不会异常
b.浮点float\double
数到整数
把浮点数转换为整数时, 会舍弃小数, 得整数
static void Main(string[] args) { float f = 10.9f; double d = 11.9; int @if; int id; checked { @if = (int)f; id = (int)d; } Console.WriteLine($"{@if}"); //10 Console.WriteLine($"{id}"); //11 }
-
如果checked, 溢出抛出异常
-
如果是unchecked, C# 不定义他的值是什么
static void Main(string[] args) { float f = 101111.9f; double d = 102222.9; byte @if; byte id; unchecked { @if = (byte)f; id = (byte)d; } Console.WriteLine($"{@if}"); //243 Console.WriteLine($"{id}"); //78 }
c.decimal
到整数类型
如果结果值不在目标类型范围内,直接抛出溢出异常, 不管是否checked
decimal dc = new decimal(11111111.9); byte bdc; int idc; //不论这里是否checked, 都会异常 unchecked { bdc = (byte)dc; idc = (int)dc; } Console.WriteLine($"{bdc}"); Console.WriteLine($"{idc}");
d.double
到float
float
类型的值占32位, 而double
类型的值占64位, 当double
被舍入位float
时, double
类型的值会被舍入到最接近的float
类型的值
-
如果值太小而不能用
float
表示, 那么值会被设置为正0或者负0double d = 0.000000000000000000000000000000000000000000000001; float f; checked { f = (float)d; } Console.WriteLine($"{f}"); //0 -
如果值太大而不能用
float
表示, 那么值会被设置为正无穷大或者负无穷大double d = 9999999999999999999999999999999999999999999999999.99999999; float f; checked { f = (float)d; } Console.WriteLine($"{f}"); //无穷
e.float
或double
到decimal
-
如果值太小而不能用decimal类型表示, 那么值会设置为0
-
如果值太大, 那么会抛出异常
float fh = 99999999999999999999999999999999.9f; float fl = 0.00000000000000000000000000000001f; double dh = 99999999999999999999999999999999.9; double dl = 0.00000000000000000000000000000001; decimal dcfh = (decimal)fh; //太大 异常 不往下执行 decimal dcfl = (decimal)fl; //太小 0 decimal dcdh = (decimal)dh; //太大 异常 不往下执行 decimal dcdl = (decimal)dl; //太小 0 Console.WriteLine($"{dcfh} \n {dcfl} \n {dcdh} \n {dcdl}");
f.decimal
到float
或者double
-
这样强制转换总是会成功
-
但是会损失精度
decimal dcl = new decimal(0.00001); decimal dch = new decimal(99999.9); float fl = (float) dcl; float fh = (float) dch; double dl = (double)dcl; double dh = (double)dch; Console.WriteLine($"{fl} \n {fh} \n {dl} \n {dh}");
6、引用转换
我们已经知道引用类型对象由内存中的两部分组成: 引用和数据
-
由引用保存的那部分信息是指向它的数据类型
-
引用类型接受源引用并返回一个指向堆中同一位置的引用, 但是把引用标记为其他类型
class A { public int FieldA; }
class B : A { public int FieldB; }
static void Main(string[] args) { B b = new B(); A a = (A)b; Console.WriteLine($"{a.FieldA}"); //Console.WriteLine($"{a.FieldB}"); 编译错误 FieldB对a不可见 }
-
对于
b
来说, 他引用的对象是B类型的对象 -
对于
a
来说, 同样的对象看上去像是A的对象-
即使他实际指向B的对象, 他也看不到B扩展A的部分, 因此访问不到
FieldB
-
(1)隐式引用转换
-
所有引用类型可以被隐式转换为object类型
-
任何接口可以隐式转换为它继承的接口
-
类可以隐式转换为
-
继承链中任何类
-
实现的任何接口
-
-
委托可以隐式转为
.NET BCL
类和接口 -
ArrayS
数组(其中元素是Ts
类型)可以隐式转换为-
.NET BCL
类和接口 -
另一个数组
ArrayT,
其中类型是Tt
类型(如果满足以下所有条件)-
两个数组维度一样
-
元素类型
Ts
和Tt
都是引用类型, 不是值类型 -
在类型
Ts
和Tt
中存在隐式转换
-
-
(2)显式引用转换
-
从一个普通类型转换为更精确的类型的引用转换, 包括
-
从
object
到任何引用类型的转换 -
从基类到派生自它的类的转换
-
-
如果转换的类型不受限制, 很容易尝试引用在内存中实际并不存在的类成员
-
编译器允许这样的转换, 在运行时遇到他们, 会抛出异常
-
class A { public int FieldA; }
class B : A { public int FieldB; }
static void Main(string[] args) { A a = new A(); B b = (B)a; //不安全 在运行时会抛出异常 }
(3)有效显示引用转换
在运行时能成功进行的显示转换有3种情况
a.显示转换是没有必要的
也就是说进行了隐式转换, 比如父子类之间总是进行隐式转换
class A{} class B : A{}
B b = new B(); A a = (A)b;
b.源引用是null
即使基类转派生类的引用通常是不安全的, 但是由于源引用是null, 这种转换是允许的
A a = null; B b = (B)a;
c.有源引用指向的实际数据可以安全地进行隐式转换
-
隐式转换
myVar2
, 实际上myVar2
是B类型的数据 -
强制转换
myVar2
到myVar3
其实指向的对象实际就是B类型的数据项
B myVar1 = new B(); A myVar2 = myVar1; B myVar3 = (B)myVar2;
7、装箱转换
包括值类型在内的所有C#类型都派生自object类型, 然而, 值类型是高效轻量的类型, 因为默认情况下, 堆上不包括他们的对象组件, 然而, 如果需要对象组件, 我们可以使用装箱, 装箱是一种隐式转换, 他接受值类型的值, 根据这个值在堆上创建一个完整的引用类型对象, 并返回对象引用
int i = 12; object oi = null; oi = i;
-
变量i在堆上没有可赋值给oi的引用, 因此它必须
-
在内存上创建int类型对象
-
赋值i的值到这个对象
-
把int对象的引用返回给oi
-
(1)装箱是创建副本
-
装箱操作返回值是引用类型副本
-
在装箱之后, 值有两个副本, 每个都可以独立操作
-
原始值类型
-
引用类型副本
-
static void Main(string[] args) { int i = 10; object oi = i; Console.WriteLine($"i: {i}, oi: {oi}"); //10 10 i = 12; oi = 15; Console.WriteLine($"i: {i}, oi: {oi}"); //12 15 }
(2)装箱转换
任何值类型ValueTypeS
都可以被隐式转换为object、System.ValueType、InterfaceT
, 前提是ValueTypes实现了InterfaceT
8、拆箱转换
-
拆向转换是显示转换
-
值拆箱为ValueTypeT执行如下步骤
-
检测到拆箱的对象实际上ValueTypeT的装箱值
-
将对象的值复制到变量
-
int i = 10; object oi = i; int j = (int)oi; //拆箱 Console.WriteLine($"i: {i}, oi: {oi}, j: {j}"); //10 10 10
9、用户自定义转换
//隐式 public static 必需 public static implicit operator 目标类型 (源类型 源对象) { return 目标类型对象 }
//显式 public static 必需 public static explicit operator 目标类型 (源类型 源对象) { return 目标类型对象 }
(1)约束
-
只可以为类或者结构定义用户自定义转换
-
不能重定义标准隐式或显式转换
-
对于源类型
S
和目标类型T
,-
必须是不同类型
-
不能通过继承关联
-
都不能是接口或者
object
类型 -
转换运算符必须是
s
或T
的成员
-
-
对于相同的源类型
(2)例子
隐式
class Person190902 { public string Name; public int Age; public Person190902(string name, int age) { Name = name; Age = age; } //显式转换Person -> int public static explicit operator int(Person190902 p) { return p.Age; } //隐式转换int -> Person public static implicit operator Person190902(int i) { return new Person190902("N/A", i); } }
static void Main(string[] args) { Person190902 nemo = new Person190902("Nemo", 11); //显式 int age = (int)nemo; Console.WriteLine($"{nemo.Age}, {nemo.Name}"); //隐式 Person190902 bill = 22; Console.WriteLine($"{bill.Age}, {bill.Name}"); }
(3)评估用户自定义转换
以上为单步用户自定义转换
用户自定义转换在完整转换中最多有三个步骤
-
预备标准转换
-
用户自定义转换
-
后续标准转换
这个链中不可能有一个以上的用户自定义转换
(4)多步例子
class Employee170904 : Person170904 { }
class Person170904 { public string Name; public int Age; //隐式转换Person -> int public static implicit operator int(Person170904 p) { return p.Age; } }
static void Main(string[] args) { Employee170904 bill = new Employee170904(); bill.Name = "William"; bill.Age = 25; float fVar = bill; Console.WriteLine($"{bill.Age}, {bill.Name}"); }
10、is运算符
使用is
运算符来检查转换是否会成功完成, 从而避免盲目尝试转换
源 is 目标类型;
-
如果源可以通过以下方式成功转换为目标类型, 则返回为true
-
引用类型
-
装箱转换
-
拆箱转换
-
-
is运算符只可用于引用转换以及拆箱装箱转换, 不能用于用户自定义转换
class Person { public string Name = "Mark"; public int Age = 25; }
class Employee : Person { }
static void Main(strintg[] args) { Employee mark = new Employee(); if (mark is Person) { Person personMark = (Person) mark; Console.WriteLine($"{personMark.Age}, {personMark.Name}"); } }
11、as运算符
-
与强制转换运算符类似, 只是他不抛出异常, 如果转换失败, 返回null而不是排除异常, 目标类型必须是引用类型
源 as 目标类型; -
as运算符返回引用表达式, 所以可以用于赋值操作中的源
-
as运算符只可用于引用转换以及拆箱装箱转换, 不能用于用户自定义转换
static void Main(string[] agrs) { Employee mark = new Employee(); Person personMark; personMark = mark as Person; if (personMark != null) { Console.WriteLine($"{personMark.Age}, {personMark.Name}"); } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律