我们在思想、言词、行为方面的精确性程度是对我们是否忠于真理的初步检验。
——巴兰德
摘要
许多公司在面试的时候都喜欢问一些非常基础的问题。例如面试官会问:“.net里有哪些内建类型?”,应聘者一边心里嘀咕着:“就不能整点4岁滴?”,一边回答说:“有int、long、double、decimal....喔,对了,还有String,常用的差不多就是这些了。”这个答案并没有错,但是它很难让你脱颖而出。在这个什么都讲优化的年代,我们也有必要做一点“面试优化”了^_^
内建类型
出彩
1. float、double、decimal之间有什么异同?
float和double是符合IEEE标准的二进制浮点数,它们的取值范围比decimal大得多,但是有效数字位数比decimal少,有舍入误差,只有符合“k/(2^n) 其中k和n均为整数”的数值能够被精确表示,其它形式的数值均有舍入误差。
例如下列数值不会产生舍入误差
而下列数值则会产生舍入误差
decimal 是专为金融和财务设计的十进制浮点数,它的取值范围没有float和Decimal那么大(但是对于财务计算足够了),但是有效数字比double多,没有舍入误差,所以对于金额等对精度要求高的数据应该使用decimal。
需要注意的是Decimal是一个非常特殊的类型。虽然C#和VB都把Decimal看作是一个primitive type,但CLR却不是这样。这意味着:
①操作Decimal值得执行效率将比其它primitive type低。请看下面的示例:
比较float和decimal这两种不同的类型所对应的IL代码,可以发现两个double类型的数字相减是使用的sub指令,而两个decimal类型的数字相减需要调用Decimal的operator-()函数。
②checked和unchecked操作符,以及相关的编译器命令行开关对Decimal没有任何影响。因为checked操作符指示编译器使用add.ovf、conv.ovf等进行溢出检查的指令;unchecked操作符指示编译器使用add、conv等不进行溢出检查的指令。由于这些指令只对primitive type有效,所以不管是否使用了unchecked操作符,只要发生溢出Decimal都会抛出异常。
2. 什么是装箱和拆箱?如何避免?
1) 装箱。装箱的核心是把值类型转换为对象类型,或者转换为由值类型执行的接口类型。常见的装箱转换有4种:
① 从任何值类型转换为对象类型。
举例:
int i = 28;
object age = i; // 装箱
问题是真会有人写这么BT的代码么?有的。因为在.net里值类型是不能被赋值为null的。例如
int i = null; // 编译错误:无法将 NULL 转换成“int”,因为它是一种值类型
那么该如何表示数据库中的null值呢?方法之一就是像上面那样用一个object来表示数据库中的一个字段。只是这种方法不仅会造成装箱,而且可读性也不好,因为我们只能通过注释和文档才能知道age里面保存的什么类型的数据。
解决方法1(适用于.net 1.x):
可以约定一个“哨兵值”,例如约定int.MaxValue表示null:
public static bool IsNull(int arg)
{
return arg == int.MaxValue;
}
解决方法2(适用于.net 2.0):
在.net 2.0中新增了可空值类型 Nullable<T> ,可以很好地解决这个问题。
Nullable<int> age = null; // 这是个语法糖,相当于 Nullable<int> age = new Nullable<int>();
// 也可以使用C#提供的简便写法: int? age = null;
bool b = age.HashValue; // b = false
age = 9; // 这是个语法糖,相当于 age = new Nullable<int>(9);
int i = age ?? 28; // 相当于 int i = age.HasValue ? age.GetValueOrDefault() : 28;
可以看出通过C#提供的一些语法糖,使代码看上去像是把null赋值给了age,其实不过是使用默认值初始化了age而已,而age的bool型的属性HashValue默认会被初始化为false,这样就正好可以表示age是“null”这一语义了。整个过程都没有发生装箱。
再举一个装箱的例子:
int age = 17;
Console.WriteLine(age); // 没有发生装箱操作,因为Console有一个WriteLine(Int32)的函数定义。
Console.WriteLine("{0}", age); // 装箱。因为符合这个调用的函数只有Console.WriteLine(String, Object);
// 所以相当于调用 Console.WriteLine("{0}", (Object)age)
// 这是一个很隐蔽“将值类型转换为对象类型”的操作
解决方法是:
Console.WriteLine("{0}", age.ToString()); // 没有发生装箱操作
之所以没有发生装箱是因为String本身就是一个引用类型(后面会讲到),自然不会发生装箱了。不过在Console.WriteLine() 函数内部还是会再调用一次 arg.ToString(),所以Console.WriteLine("{0}", age.ToString())和Console.WriteLine("{0}", age)哪个更快取决于“把arg装箱”更快还是调用“arg.ToString() ”更快。我实测的结果是Console.WriteLine("{0}", age)稍微快一点,不信你也可以测试一下。
② 从任何值类型转换为System.ValueType类型。
举例:
int a = 9;
ValueType b = a; // 装箱
③ 从任何值类型转换为值类型实现的任何接口类型。
举例:
decimal a = 9;
IConvertible b = a; // 装箱
int c = b.ToInt32(null);
decimal等数值类型实现了IFormattable, IComparable, IConvertible等等很有用的接口,可是TNND对这些接口全部采用了显示接口实现,结果就是这些有用的函数不能直接调用,必须先转换成对应的接口才行。
④ 从枚举类型转换为System.Enum类型。
enum Tricolor { Red, Green, Blue }
Tricolor a = Tricolor.Blue;
Enum b = a; // 装箱
2) 拆箱。拆箱相对于装箱是一个相反的过程,其核心是将引用类型显示转换为值类型,或者是将一个接口类型转换为一个实现该接口的值类型。
① 从对象类型转换为任何值类型。
举例:
int a = 9;
object b = a; // 装箱
int c = (int)b; // 拆箱
② 从System.ValueType类型转换为任何值类型。
③ 从任何接口类型转换为实现该接口类型的任何值类型。
④ 从System.Enum类型转换为任何枚举类型。
enum Tricolor { Red, Green, Blue }
Tricolor a = Tricolor.Blue;
Enum b = a; // 装箱
Tricolor c = (Tricolor)b; // 拆箱
Tricolor red = (Tricolor)Enum.Parse(typeof(Tricolor), "Red"); // 拆箱, 因为Enum.Parse()返回值是Object类型
3. String到底是引用类型还是值类型?
String是引用类型,但是它(被故意设计成)具有值类型的行为。换句话说,人们希望String具有值类型的行为,但是出于性能的考虑需要它是引用类型。
例如
string a = "对1-2-3的赞美之词(此处省略2000字...)"; // a 将占用约4k内存
string b = a; // 如果 String 是值类型,就必须进行由 a 到 b 的逐位拷贝,
// 那么a和b加起来将占用约8K内存,且效率不佳。
// 如果String是引用类型,那么b就是一个指向a的引用,b只需要4个字节的内存。
string b = b.Insert(1, "傻瓜"); // 我们想把 "傻瓜" 插入到b中,可是因为b是a的引用,修改b就相当
// 于修改了a,而我们不喜欢这样(也就是说我们希望String具有值类
// 型的行为)。于是Insert()函数被设计成并不直接修改b,而是先把
// b克隆一份,然后修改这个克隆,然后返回这个克隆。这就是
// ValueObject 模式
Console.WriteLine(a); // 输出 "对1-2-3的赞美之词(此处省略2000字...)";
Console.WriteLine(b); // 输出 "对傻瓜1-2-3的赞美之词(此处省略2000字...)";
——巴兰德
摘要
许多公司在面试的时候都喜欢问一些非常基础的问题。例如面试官会问:“.net里有哪些内建类型?”,应聘者一边心里嘀咕着:“就不能整点4岁滴?”,一边回答说:“有int、long、double、decimal....喔,对了,还有String,常用的差不多就是这些了。”这个答案并没有错,但是它很难让你脱颖而出。在这个什么都讲优化的年代,我们也有必要做一点“面试优化”了^_^
内建类型
.Net Framework 类型 | 位数 | 范围 | C# | VB | ||
---|---|---|---|---|---|---|
类型 | 示例 | 类型 | 示例 | |||
类型 | 示例 | 类型 | 示例 | |||
Boolean | 1 | / | bool | bool a; // 编译错误:a 未必初始化就不能使用(对于所有基本类型都有这个约束,后面就不一一列举了) bool b = true; |
Boolean | Dim a As Boolean ' OK. a = False (对于所有基本类型都会像这样被初始化为默认值,后面就不一一列举了) Dim b As Boolean = True |
SByte | 1 | -2^7 ~ 2^7-1 | sbyte | sbyte a = -3; a = 128; // 编译错误:常量值“128”无法转换为“sbyte” |
SByte | Dim a As SByte = -3 a = 128 ' 编译错误:常量表达式无法在类型“SByte”中表示。 |
Byte | 1 | 0 ~ 2^8-1 | byte | byte a = 128; a = -3; // 编译错误:常量值“-3”无法转换为“byte” |
Byte | Dim a As Byte = 128 a = -3 ' 编译错误:常量表达式无法在类型“Byte”中表示。 |
Int16 | 2 | -2^15 ~ 2^15-1 | short | short a = -3; a = 32768; // 编译错误:常量值“32768”无法转换为“short” |
Short | Dim a As Short = -3 a = 32768 ' 编译错误:常量表达式无法在类型“Short”中表示。 |
UInt16 | 2 | 0 ~ 2^16-1 | ushort | ushort a = -3; // 编译错误:常量值“-3”无法转换为“ushort” a = 32768; |
UShort | Dim a As UShort = -3 ' 编译错误:常量表达式无法在类型“UShort”中表示。 a = 32768 |
Int32 | 4 | -2^31 ~ 2^31-1 | int | int a = -3; | Integer | Dim a As Integer = -3 |
UInt32 | 4 | 0 ~ 2^32-1 | uint | uint a = 0; | UInteger | Dim a As UInteger = 0 |
Int64 | 8 | -2^63 ~ 2^63-1 | long | long a = 3; long b = 3L; |
Long | Dim a as Long = 3 Dim a as Long = 3L |
UInt64 | 8 | 0 ~ 2^64-1 | ulong | ulong a = 3; ulong b = 3L; |
ULong | Dim a as ULong = 3 Dim a as ULong = 3L |
Single | 4 | ±1.5*10^-45 ~ ±3.4*10^38 有效数字7位 |
float | float a = 21; // OK. a = 21 float b = 21.5; // 编译错误:不带后缀的小数被视为double类型。 float c = 21.5F; // c = 21.5 float t = 12345678L; // OK, t = 12345680.0。int 和 long 可被隐式转换为float, 但精度会有损失 |
Single | Dim a As Single = 21 ' OK Din b As Single = 21.5 'OK. VB的编译器能根据b的类型自行确定21.5的类型 Dim c As Single = 21.5F 'OK Dim t As Single = 12345678L; ' OK, t = 12345680.0。int 和 long 可被隐式转换为float, 但精度会有损失 |
Double | 8 | ±5.0*10^−324 ~ ±1.7*10^308 有效数字15~16位 |
double | double a = 21.2; // a = 21.2 double b = 21.2F; // b = 21.2000007629395 double c = 34.6 - 34.0; // c = 0.600000000000001 double d = 21; // d = 21 |
Double | Dim a As Double = 21.2 ' a = 21.2 Dim b As Double = 21.2F ' b = 21.2000007629395 Dim c As Double = 34.6 - 34.0 ' c = 0.600000000000001 Dim d As Double = 21 ' d = 21 |
Decimal | 16 | ±1.0×10^−28 ~ ±7.9*10^28 有效数字28~29位 |
deicmal | decimal a = 21L; // a = 21 decimal b = 21; // b = 21 decimal c = 1.5; // 编译错误:不能隐式地将 Double 类型转换为“decimal”类型 decimal d = 1.5M; // d = 1.5 |
Decimal | Dim a As Decimal = 21L ' a = 21 Dim b As Decimal = 21 ' b = 21 Dim c As Decimal = 1.5 ' OK VB的编译器能根据c的类型自行确定1.5的类型 Dim d As Decimal = 1.5D ' 注意在VB里“D”表示“Decimal”;而在C# 里“D”表示“Double” |
Char | 2 | 0 ~ 2^16-1 | char | char c1 = 'a'; char c2 = '天'; int a = (int)c1; // a = 97 int b = (int)c2; // b = 22825 (0x5929) char c3 = '\u5929'; // c3 = '天' char c4 = '\x5929'; // c4 = '天' |
Char | Dim c1 As Char = "a" Dim c2 As Char = "天" Dim a As Integer = AscW(c1) ' a = 97 Dim b As Integer = AscW(c2) ' b = 22825 (0x5929) Dim c3 As Char = ChrW(22825) ' c3 = "天" Dim c4 As Char = ChrW(&H5929) ' c4 = "天" |
String | / | / | string | string a = "abc"; string b = "123"; string c = a + b; // c = "abc123" string t = null; |
String | Dim a As String = "abc" Dim b As String = "123" Dim c As String = a + b ' c = "abc123" Dim d As String = a & b ' d = "abc123" Din t as String = Nothing |
Object | / | / | object | Object a = 9; // 装箱 int b = (int)a; // 拆箱 |
Dim a As Object = 9 ' 装箱 Dim b As Integer = a ' 拆箱 |
出彩
1. float、double、decimal之间有什么异同?
float和double是符合IEEE标准的二进制浮点数,它们的取值范围比decimal大得多,但是有效数字位数比decimal少,有舍入误差,只有符合“k/(2^n) 其中k和n均为整数”的数值能够被精确表示,其它形式的数值均有舍入误差。
例如下列数值不会产生舍入误差
double a = 0.5 // a = (1/2^1) 被实际存储为 0.5
double b = 0.25; // a = (1/2^2) 被实际存储为 0.25
double c = 0.1875; // a = (3/2^4) 被实际存储为 0.1875
double d = 13.28125; // a = (425/2^5) 被实际存储为 13.28125
double b = 0.25; // a = (1/2^2) 被实际存储为 0.25
double c = 0.1875; // a = (3/2^4) 被实际存储为 0.1875
double d = 13.28125; // a = (425/2^5) 被实际存储为 13.28125
而下列数值则会产生舍入误差
double e = 0.2; // a = (1/5) 被实际存储为 0.20000000000000001
double f = 0.3; // a = (3/10) 被实际存储为 0.29999999999999999
double g = 0.6; // a = (3/5) 被实际存储为 0.59999999999999998
double h = 34.6; // a = (173/5) 被实际存储为 34.600000000000001
double f = 0.3; // a = (3/10) 被实际存储为 0.29999999999999999
double g = 0.6; // a = (3/5) 被实际存储为 0.59999999999999998
double h = 34.6; // a = (173/5) 被实际存储为 34.600000000000001
decimal 是专为金融和财务设计的十进制浮点数,它的取值范围没有float和Decimal那么大(但是对于财务计算足够了),但是有效数字比double多,没有舍入误差,所以对于金额等对精度要求高的数据应该使用decimal。
需要注意的是Decimal是一个非常特殊的类型。虽然C#和VB都把Decimal看作是一个primitive type,但CLR却不是这样。这意味着:
①操作Decimal值得执行效率将比其它primitive type低。请看下面的示例:
比较float和decimal这两种不同的类型所对应的IL代码,可以发现两个double类型的数字相减是使用的sub指令,而两个decimal类型的数字相减需要调用Decimal的operator-()函数。
②checked和unchecked操作符,以及相关的编译器命令行开关对Decimal没有任何影响。因为checked操作符指示编译器使用add.ovf、conv.ovf等进行溢出检查的指令;unchecked操作符指示编译器使用add、conv等不进行溢出检查的指令。由于这些指令只对primitive type有效,所以不管是否使用了unchecked操作符,只要发生溢出Decimal都会抛出异常。
2. 什么是装箱和拆箱?如何避免?
1) 装箱。装箱的核心是把值类型转换为对象类型,或者转换为由值类型执行的接口类型。常见的装箱转换有4种:
① 从任何值类型转换为对象类型。
举例:
int i = 28;
object age = i; // 装箱
问题是真会有人写这么BT的代码么?有的。因为在.net里值类型是不能被赋值为null的。例如
int i = null; // 编译错误:无法将 NULL 转换成“int”,因为它是一种值类型
那么该如何表示数据库中的null值呢?方法之一就是像上面那样用一个object来表示数据库中的一个字段。只是这种方法不仅会造成装箱,而且可读性也不好,因为我们只能通过注释和文档才能知道age里面保存的什么类型的数据。
解决方法1(适用于.net 1.x):
可以约定一个“哨兵值”,例如约定int.MaxValue表示null:
public static bool IsNull(int arg)
{
return arg == int.MaxValue;
}
解决方法2(适用于.net 2.0):
在.net 2.0中新增了可空值类型 Nullable<T> ,可以很好地解决这个问题。
Nullable<int> age = null; // 这是个语法糖,相当于 Nullable<int> age = new Nullable<int>();
// 也可以使用C#提供的简便写法: int? age = null;
bool b = age.HashValue; // b = false
age = 9; // 这是个语法糖,相当于 age = new Nullable<int>(9);
int i = age ?? 28; // 相当于 int i = age.HasValue ? age.GetValueOrDefault() : 28;
可以看出通过C#提供的一些语法糖,使代码看上去像是把null赋值给了age,其实不过是使用默认值初始化了age而已,而age的bool型的属性HashValue默认会被初始化为false,这样就正好可以表示age是“null”这一语义了。整个过程都没有发生装箱。
再举一个装箱的例子:
int age = 17;
Console.WriteLine(age); // 没有发生装箱操作,因为Console有一个WriteLine(Int32)的函数定义。
Console.WriteLine("{0}", age); // 装箱。因为符合这个调用的函数只有Console.WriteLine(String, Object);
// 所以相当于调用 Console.WriteLine("{0}", (Object)age)
// 这是一个很隐蔽“将值类型转换为对象类型”的操作
解决方法是:
Console.WriteLine("{0}", age.ToString()); // 没有发生装箱操作
之所以没有发生装箱是因为String本身就是一个引用类型(后面会讲到),自然不会发生装箱了。不过在Console.WriteLine() 函数内部还是会再调用一次 arg.ToString(),所以Console.WriteLine("{0}", age.ToString())和Console.WriteLine("{0}", age)哪个更快取决于“把arg装箱”更快还是调用“arg.ToString() ”更快。我实测的结果是Console.WriteLine("{0}", age)稍微快一点,不信你也可以测试一下。
② 从任何值类型转换为System.ValueType类型。
举例:
int a = 9;
ValueType b = a; // 装箱
③ 从任何值类型转换为值类型实现的任何接口类型。
举例:
decimal a = 9;
IConvertible b = a; // 装箱
int c = b.ToInt32(null);
decimal等数值类型实现了IFormattable, IComparable, IConvertible等等很有用的接口,可是TNND对这些接口全部采用了显示接口实现,结果就是这些有用的函数不能直接调用,必须先转换成对应的接口才行。
④ 从枚举类型转换为System.Enum类型。
enum Tricolor { Red, Green, Blue }
Tricolor a = Tricolor.Blue;
Enum b = a; // 装箱
2) 拆箱。拆箱相对于装箱是一个相反的过程,其核心是将引用类型显示转换为值类型,或者是将一个接口类型转换为一个实现该接口的值类型。
① 从对象类型转换为任何值类型。
举例:
int a = 9;
object b = a; // 装箱
int c = (int)b; // 拆箱
② 从System.ValueType类型转换为任何值类型。
③ 从任何接口类型转换为实现该接口类型的任何值类型。
④ 从System.Enum类型转换为任何枚举类型。
enum Tricolor { Red, Green, Blue }
Tricolor a = Tricolor.Blue;
Enum b = a; // 装箱
Tricolor c = (Tricolor)b; // 拆箱
Tricolor red = (Tricolor)Enum.Parse(typeof(Tricolor), "Red"); // 拆箱, 因为Enum.Parse()返回值是Object类型
3. String到底是引用类型还是值类型?
String是引用类型,但是它(被故意设计成)具有值类型的行为。换句话说,人们希望String具有值类型的行为,但是出于性能的考虑需要它是引用类型。
例如
string a = "对1-2-3的赞美之词(此处省略2000字...)"; // a 将占用约4k内存
string b = a; // 如果 String 是值类型,就必须进行由 a 到 b 的逐位拷贝,
// 那么a和b加起来将占用约8K内存,且效率不佳。
// 如果String是引用类型,那么b就是一个指向a的引用,b只需要4个字节的内存。
string b = b.Insert(1, "傻瓜"); // 我们想把 "傻瓜" 插入到b中,可是因为b是a的引用,修改b就相当
// 于修改了a,而我们不喜欢这样(也就是说我们希望String具有值类
// 型的行为)。于是Insert()函数被设计成并不直接修改b,而是先把
// b克隆一份,然后修改这个克隆,然后返回这个克隆。这就是
// ValueObject 模式
Console.WriteLine(a); // 输出 "对1-2-3的赞美之词(此处省略2000字...)";
Console.WriteLine(b); // 输出 "对傻瓜1-2-3的赞美之词(此处省略2000字...)";