C# 温故知新 基础篇(3) 数据类型
类型 | BCL 类型 | 说明 | 示例 |
object | System.Object | 所有其他类型的最终基类型 |
object o = null;
|
string
|
System.String
|
字符串类型;字符串是 Unicode 字符序列 |
string s = "hello";
|
sbyte
|
System.SByte
|
8 位有符号整型 |
sbyte val = 12;
|
short
|
System.Int16
|
16 位有符号整型 |
short val = 12;
|
int
|
System.Int32
|
32 位有符号整型 |
int val = 12;
|
long
|
System.Int64
|
64 位有符号整型 |
long val1 = 12;
long val2 = 34L;
|
byte
|
System.Byte
|
8 位无符号整型 |
byte val1 = 12;
|
ushort
|
System.UInt16
|
16 位无符号整型 |
ushort val1 = 12;
|
uint
|
System.UInt32
|
32 位无符号整型 |
uint val1 = 12;
uint val2 = 34U;
|
ulong
|
System.UInt64
|
64 位无符号整型 |
ulong val3 = 56L;
ulong val4 = 78UL;
|
float
|
System.Single
|
单精度浮点型 |
float val = 1.23F;
|
double
|
System.Double
|
双精度浮点型 |
double val1 = 1.23;
double val2 = 4.56D;
|
bool
|
System.Boolean
|
布尔型;bool 值或为真或为假 |
bool val1 = true;
bool val2 = false;
|
char
|
System.Char
|
字符类型;char 值是一个 Unicode 字符(16位) |
char val = 'h';
|
decimal
|
System.Decimal
|
精确的小数类型,具有 28 个有效数字 |
decimal val = 1.23M;
|
在使用类型的过程中,我们应该选择一种最恰当的类型来存放我们的数据,避免不必要的系统资源浪费。
1.1 浮点类型和decimal类型的区别
decimal类型和浮点类型的本质区别在于:decimal类型的基数是十进制的,而浮点类型的基数是基于二进制的。所以一般使用浮点类型进行科学计算,而使用decimal类型用于金融领域的计算。但是使用这两种类型进行重要数据的运算时,需要注意二者能够表示数值的范围。
1.2 十六进制计数法
一般情况下我们定义的数值类型使用的都是十进计数法,有时候我们也会需要使用十六进制计数法(或者说每个数字可以使用16个符号来表示:0-9,A-F),例如,十进制的16我们可以这样表示:
1 short num = 0x10;
每个十六进制数字都是4个bit,所有一个字节可以表示两位十六进制数字。但是在这里我们需要注意:
十六进制和十进制的相互转换并不会改变数字本身所代表的值,改变的只是它们的表现形式而已。
十六进制格式数字和字符串的转换
十六进制字符串转换为对应的数值:
1 short result = Convert.ToInt16("10", 16);
数值转换为对应的十六进制字符串:
1 short number = 0x10; 2 string result= number.ToString("X2");//X2表示保留2位
1.3 字符串
string类型由字符构成,表示一个字符序列(零个或更多 Unicode 字符)。并且string属于不可变类型,即:自string对象被创建后,尽管从语法上看您似乎可以更改其内容,但实际上对于string的任何修改返回的都是一个新的对象。所以,出于性能上的考虑,如果出现需要对字符串进行频繁操作的情境时,应该使用StringBuilder类型而不是string类型,如下:
1 StringBuilder sb = new StringBuilder(); 2 sb.Append("One "); 3 sb.Append("two "); 4 sb.Append("three"); 5 string str = sb.ToString();
逐字前缀字符——@
字符串是由字符构成的,所有字符串可以包含转义字符串。我们也可是通过使用@ 符号来告知字符串构造函数忽略转义符。下面的两个字符串是完全等价的:
1 string p1 = "\\\\My Documents\\My Files\\"; 2 string p2 = @"\\My Documents\My Files\";
string类型的==和!=操作符
虽然string类型时引用类型,但是它重写了equal()方法和==、!=操作符,比较的是string对象的值(而不是引用),这使得字符串相等性测试更为直观。如下所示:
1 string str1 = "aa"; 2 string str2 = "aa"; 3 string str3 = "bb"; 4 Console.WriteLine(str1.Equals(str2));//true 5 Console.WriteLine(str1.Equals(str3));//false 6 Console.WriteLine(str1 == str2);//true 7 Console.WriteLine(str1==str3);//false 8 Console.WriteLine(str1 != str3);//true 9 Console.WriteLine(str1 != str2);//false
2.值类型引用类型
在C#中可以把所有的类型划分为两大类:值类型和引用类型。它们的区别来源于它们的复制方式:值类型的数据总是值被复制;而引用类型的数据总是引用被复制。
2.1 值类型
在C#的预定义类型中,除了string类型和object类型其他的都属于值类型。值类型直接包含值本身,对于值变量来说,变量引用的位置就是值在内存中实际存在的位置。如图所示:
将一个变量的值赋给另一个变量将会在新变量的位置创建原始变量的值的一个内存副本;同样的,如果将一个值类型传给一个方法,也会生成一个内存副本。所 以在方法内部对参数值进行任何修改都不会影响调用函数中的原始值。由于值类型的这些特性,我们不要让它们消耗太多的内存(通常应该小于16个字节)。
2.2 引用类型
引用类型不直接存储值,它们存储的是对一个内存地址的引用。为了访问引用类型的数据,CLR需要从变量中读取内存位置,然后在跳转到包含数据的内存位置(堆 heap)。如图所示:
引用类型和值类型不同,同样的数据可以直接复制引用而不是拷贝一个内存副本,所有:从内存的使用效率来说引用类型表现更好,但从运行效率来看,值类型又优于引用类型。由于引用类型只复制数据的地址,所有两个不同的变量可以引用相同的数据。同时,利用其中一个变量更改数据也会改变另一个变量引用的数据。例如:如果在一个方法的内部修改了引用类型的数据,在调用者那里也会发生同样的改动。如下:
1 StringBuilder sb = new StringBuilder("CSharp"); 2 StringBuilder sb2 = sb; 3 sb.Append(" Java"); 4 Console.WriteLine(sb2.ToString());//CSharp Java
有鉴于此,具体使用值类型还是引用类型的决定性因素就是:对象在逻辑上是不是像一个固定大小不可变的值,如果是,那么就定义成值类型。更多关于值类型和引用类型的选择请参考:《Effective C# 读书笔记——区分值类型和引用类型》。
3.数据类型转换
.NET Framework BCL 定义了大量的类型,而且我们也能够定义自己的类型,所以转型(casting)是程序中最常见的操作之一。 类型转换分为两种:显式转换(explicit cast)和隐式转换(implicit cast)。
二者区别在于:可能会造成丢失数量级或者引发异常(转换失败)的任何转换都需要显示转换;相反,不会丢失数量级,而且不会引发异常(无论操作数的类型是什么)的任何转换都属于隐式转换。
3.1 显示转换
在C#中可以使用转型运算符来进行显式转换,下面的代码可以将一个long类型转换为一个int类型:
1 long longNumber = 1234567891011; 2 int intNumber = (int)longNumber; 3 Console.WriteLine(intNumber);//print 1912277059
我们可以看到精度和数据发生了丢失,所有进行显示转换的是我们需要考虑这样做是否合理。所以,程序员需要确保数据成功转换,且符合逻辑的要求;或者在转换失败的时候提供相应的代码来处理那些错误。
3.2 隐式转换
因为隐式转换不会丢失数据和精度,且不会发生异常,所有代码只需要指定赋值运算符,转换将隐式地发生。例如下面的代码:
1 int intNumber = 31415; 2 long longNumber = intNumber;
3.3 使用方法进行数据转换
对于从字符串到数值类型的转换,我们可以使用Parse()这样的方法将字符串转换成对应的数值类型。还可以利用Convert类,将一个类转换为另一种类型,不过Convert类只支持预定义类型,且不可扩展,它允许任何基本类型转换到其他基本类型。
3.4 理解装箱和拆箱
装箱是将值类型转换为引用类型或者是值类型(如:结构)实现任一接口类型的过程。当 CLR 对值类型进行装箱时,会将该值包装到 System.Object 内部,再将后者存储在托管堆上。拆箱就是将从对象中提取值类型或者接口类型到实现该接口的值类型的显式转换。装箱时隐性的,拆箱是显式的。
相对于简单的赋值而言,装箱和取消装箱过程需要进行大量的计算。 对值类型进行装箱时,必须分配并构造一个新对象。反之,拆箱所需的强制转换也需要进行大量的计算。所以在程序中应该尽量减少装箱和拆箱的操作。
更多关于装箱和拆箱请参考《理解装箱和拆箱》。