C#.NET 中的类型转换

C# 出来也有些日子了,最近由于编程的需要,对 C# 的类型转换做了一些研究,其内容涉及 C# 的装箱/拆箱/别名、数值类型间相互转换、字符的 ASCII 码和 Unicode 码、数值字符串和数值之间的转换、字符串和字符数组/字节数组之间的转换、各种数值类型和字节数组之间的转换、十六进制数输出以及日期型数据的一些转换处理,在这里与大家分享——

1. 装箱、拆箱还是别名

许多 C#.NET 的书上都有介绍 int -> Int32 是一个装箱的过程,反之则是拆箱的过程。许多其它变量类型也是如此,如:short <-> Int16long <-> Int64 等。对于一般的程序员来说,大可不必去了解这一过程,因为这些装箱和拆箱的动作都是可以自动完成的,不需要写代码进行干预。但是我们需要记住这些类型之间的关系,所以,我们使用“别名”来记忆它们之间的关系。

C# 是全面向对象的语言,比 Java 的面向对象都还彻底——它把简单数据类型通过默认的装箱动作封装成了类。Int32Int16Int64 等就是相应的类名,而那些我们熟悉的、简单易记的名称,如 int、short、long 等,我们就可以把它称作是 Int32、Int16、Int64 等类型的别名。

那么除了这三种类型之外,还有哪些类有“别名”呢?常用的有如下一些:

  • bool -> System.Boolean (布尔型,其值为 true 或者 false)
  • char -> System.Char (字符型,占有两个字节,表示 1 个 Unicode 字符)
  • byte -> System.Byte (字节型,占 1 字节,表示 8 位正整数,范围 0 ~ 255)
  • sbyte -> System.SByte (带符号字节型,占 1 字节,表示 8 位整数,范围 -128 ~ 127)
  • ushort -> System.UInt16 (无符号短整型,占 2 字节,表示 16 位正整数,范围 0 ~ 65,535)
  • uint -> System.UInt32 (无符号整型,占 4 字节,表示 32 位正整数,范围 0 ~ 4,294,967,295)
  • ulong -> System.UInt64 (无符号长整型,占 8 字节,表示 64 位正整数,范围 0 ~ 大约 10 的 20 次方)
  • short -> System.Int16 (短整型,占 2 字节,表示 16 位整数,范围 -32,768 ~ 32,767)
  • int -> System.Int32 (整型,占 4 字节,表示 32 位整数,范围 -2,147,483,648 到 2,147,483,647)
  • long -> System.Int64 (长整型,占 8 字节,表示 64 位整数,范围大约 -(10 的 19) 次方 到 10 的 19 次方)
  • float -> System.Single (单精度浮点型,占 4 个字节)
  • double -> System.Double (双精度浮点型,占 8 个字节)

我们可以用下列代码做一个实验:

private void TestAlias() {
    // this.textBox1 是一个文本框,类型为 System.Windows.Forms.TextBox
    // 设计中已经将其 Multiline 属性设置为 true
    byte a = 1; char b = 'a'; short c = 1;
    int d = 2; long e = 3; uint f = 4; bool g = true;
    this.textBox1.Text = "";
    this.textBox1.AppendText("byte -> " + a.GetType().FullName + "\n");
    this.textBox1.AppendText("char -> " + b.GetType().FullName + "\n");
    this.textBox1.AppendText("short -> " + c.GetType().FullName + "\n");
    this.textBox1.AppendText("int -> " + d.GetType().FullName + "\n");
    this.textBox1.AppendText("long -> " + e.GetType().FullName + "\n");
    this.textBox1.AppendText("uint -> " + f.GetType().FullName + "\n");
    this.textBox1.AppendText("bool -> " + g.GetType().FullName + "\n");
}

在窗体中新建一个按钮,并在它的单击事件中调用该 TestAlias() 函数,我们将看到运行结果如下:

byte -> System.Byte
char -> System.Char
short -> System.Int16
int -> System.Int32
long -> System.Int64
uint -> System.UInt32
bool -> System.Boolean

这足以说明各别名对应的类!

2. 数值类型之间的相互转换

这里所说的数值类型包括 byte, short, int, long, fload, double 等,根据这个排列顺序,各种类型的值依次可以向后自动进行转换。举个例来说,把一个 short 型的数据赋值给一个 int 型的变量,short 值会自动行转换成 int 型值,再赋给 int 型变量。如下例:

private void TestBasic() {
    byte a = 1; short b = a; int c = b;
    long d = c; float e = d; double f = e;
    this.textBox1.Text = "";
    this.textBox1.AppendText("byte a = " + a.ToString() + "\n");
    this.textBox1.AppendText("short b = " + b.ToString() + "\n");
    this.textBox1.AppendText("int c = " + c.ToString() + "\n");
    this.textBox1.AppendText("long d = " + d.ToString() + "\n");
    this.textBox1.AppendText("float e = " + e.ToString() + "\n");
    this.textBox1.AppendText("double f = " + f.ToString() + "\n");
}

编译顺利通过,运行结果是各变量的值均为 1;当然,它们的类型分别还是 System.Byte 型……System.Double 型。现在我们来试试,如果把赋值的顺序反过来会怎么样呢?在 TestBasic() 函数中追加如下语句:

int g = 1;
short h = g;
this.textBox1.AppendText("h = " + h.ToString() + "\n");

结果编译报错:

G:\Projects\Visual C#\Convert\Form1.cs(118): 无法将类型“int”隐式转换为“short”

其中,Form1.cs 的 118 行即 short h = g 所在行。

这个时候,如果我们坚持要进行转换,就应该使用强制类型转换,这在 C 语言中常有提及,就是使用“(类型名) 变量名”形式的语句来对数据进行强制转换。如上例修改如下:

short g = 1;
byte h = (byte) g; // 将 short 型的 g 的值强制转换成 short 型后再赋给变量 h
this.textBox1.AppendText("h = " + h.ToString() + "\n");

编译通过,运行结果输出了 h = 1,转换成功。

但是,如果我们使用强制转换,就不得不再考虑一个问题:short 型的范围是 -32768 ~ 23767,而 byte 型的范围是 0 ~ 255,那么,如果变量 g 的大小超过了 byte 型的范围又会出现什么样的情况呢?我们不妨再一次改写代码,将值改为 265,比 255 大 10

short g = 265; //265 = 255 + 10
byte h = (byte) g;
this.textBox1.AppendText("h = " + h.ToString() + "\n");

编译没有出错,运行结果却不是 h = 265,而是 h = 9。

因此,我们在进行转换的时候,应当注意被转换的数据不能超出目标类型的范围。这不仅体现在多字节数据类型(相对,如上例的 short) 转换为少字节类型(相对,如上例的 byte) 时,也体现在字节数相同的有符号类型和无符号类型之间,如将 byte 的 129 转换为 sbyte 就会溢出。这方面的例子大同小异,就不详细说明了。

3. 字符的 ASCII 码和 Unicode 码

很多时候我们需要得到一个英文字符的 ASCII 码,或者一个汉字字符的 Unicode 码,或者从相关的编码查询它是哪一个字符的编码。很多人,尤其是从 VB 程序序转过来学 C# 的人,会报怨 C# 里为什么没有提供现成的函数来做这个事情——因为在 VB 中有 Asc() 函数和 Chr() 函数用于这类转换。

但是如果你学过 C,你就会清楚,我们只需要将英文字符型数据强制转换成合适的数值型数据,就可以得到相应的 ASCII 码;反之,如果将一个合适的数值型数据强制转换成字符型数据,就可以得到相应的字符。

C# 中字符的范围扩大了,不仅包含了单字节字符,也可以包含双字节字符,如中文字符等。而在字符和编码之间的转换,则仍延用了 C 语言的做法——强制转换。不妨看看下面的例子

private void TestChar() {
    char ch = 'a'; short ii = 65;
    this.textBox1.Text = "";
    this.textBox1.AppendText("The ASCII code of \'" + ch + "\' is: " + (short) ch + "\n");
    this.textBox1.AppendText("ASCII is " + ii.ToString() + ", the char is: " + (char) ii + "\n");
    char cn = '中'; short uc = 22478;
    this.textBox1.AppendText("The Unicode of \'" + cn + "\' is: " + (short) cn + "\n");
    this.textBox1.AppendText("Unicode is " + uc.ToString() + ", the char is: " + (char) uc + "\n");
}

它的运行结果是

The ASCII code of 'a' is: 97
ASCII is 65, the char is: A
The Unicode of '中' is: 20013
Unicode is 22478, the char is: 城

从这个例子中,我们便能非常清楚的了解——通过强制转换,可以得以字符的编码,或者得到编码表示的字符。如果你需要的不是 short 型的编码,请参考第 1 条进行转换,即可得到 int 等类型的编码值。

4. 数值字符串和数值之间的转换

首先,我们得搞明白,什么是数值字符串。我们知道,在 C# 中,字符串是用一对双引号包含的若干字符来表示的,如 "123"。而 "123" 又相对特殊,因为组成该字符串的字符都是数字,这样的字符串,就是数值字符串。在我们的眼中,这即是一串字符,也是一个数,但计算机却只认为它是一个字符串,不是数。因此,我们在某些时候,比如输入数值的时候,把字符串转换成数值;而在另一些时候,我们需要相反的转换。

将数值转换成字符串非常简单,因为每一个类都有一个 void ToString() 方法。所有数值型的 void ToString() 方法都能将数据转换为数值字符串。如 123.ToSting() 就将得到字符串 "123"。

那么反过来,将数值型字符串转换成数值又该怎么办呢?我们仔细查找一下,会发现 short, int, float 等数值类型均有一个 static Parse() 函数。这个函数就是用来将字符串转换为相应数值的。我们以一个 float 类型的转换为例: float f = float.Parse("543.21"); 其结果 f 的值为 543.21F。当然,其它的数值类型也可以使用同样的方法进行转换,下面的例子可以更明确的说明转换的方法:

private void TestStringValue() {
    float f = 54.321F;
    string str = "123";
    this.textBox1.Text = "";
    this.textBox1.AppendText("f = " + f.ToString() + "\n");
    if (int.Parse(str) == 123) {
        this.textBox1.AppendText("str convert to int successfully.");
    } else {
        this.textBox1.AppendText("str convert to int failed.");
    }
}

运行结果:

f = 54.321
str convert to int successfully.

5. 字符串和字符数组之间的转换

字符串类 System.String 提供了一个 void ToCharArray() 方法,该方法可以实现字符串到字符数组的转换。如下例:

private void TestStringChars() {
    string str = "mytest";
    char[] chars = str.ToCharArray();
    this.textBox1.Text = "";
    this.textBox1.AppendText("Length of \"mytest\" is " + str.Length + "\n");
    this.textBox1.AppendText("Length of char array is " + chars.Length + "\n");
    this.textBox1.AppendText("char[2] = " + chars[2] + "\n");
}

例中以对转换转换到的字符数组长度和它的一个元素进行了测试,结果如下:

Length of "mytest" is 6
Length of char array is 6
char[2] = t

可以看出,结果完全正确,这说明转换成功。那么反过来,要把字符数组转换成字符串又该如何呢?

我们可以使用 System.String 类的构造函数来解决这个问题。System.String 类有两个构造函数是通过字符数组来构造的,即 String(char[])String[char[], int, int)。后者之所以多两个参数,是因为可以指定用字符数组中的哪一部分来构造字符串。而前者则是用字符数组的全部元素来构造字符串。我们以前者为例,在 TestStringChars() 函数中输入如下语句:

char[] tcs = {'t', 'e', 's', 't', ' ', 'm', 'e'};
string tstr = new String(tcs);
this.textBox1.AppendText("tstr = \"" + tstr + "\"\n");

运行结果输入 tstr = "test me",测试说明转换成功。

实际上,我们在很多时候需要把字符串转换成字符数组只是为了得到该字符串中的某个字符。如果只是为了这个目的,那大可不必兴师动众的去进行转换,我们只需要使用 System.String[] 运算符就可以达到目的。请看下例,再在 TestStringChars() 函数中加入如如下语名:

char ch = tstr[3];
this.textBox1.AppendText("\"" + tstr + "\"[3] = " + ch.ToString());

正确的输出是 "test me"[3] = t,经测试,输出正确。

6. 字符串和字节数组之间的转换

如果还想从 System.String 类中找到方法进行字符串和字节数组之间的转换,恐怕你会失望了。为了进行这样的转换,我们不得不借助另一个类:System.Text.Encoding。该类提供了 bye[] GetBytes(string) 方法将字符串转换成字节数组,还提供了 string GetString(byte[]) 方法将字节数组转换成字符串。

System.Text.Encoding 类似乎没有可用的构造函数,但我们可以找到几个默认的 Encoding,即 Encoding.Default(获取系统的当前 ANSI 代码页的编码)、Encoding.ASCII(获取 7 位 ASCII 字符集的编码)、Encoding.Unicode(获取采用 Little-Endian 字节顺序的 Unicode 格式的编码)、Encoding.UTF7(获取 UTF-7 格式的编码)、Encoding.UTF8(获取 UTF-8 格式的编码) 等。这里主要说说 Encoding.DefaultEncoding.Unicode 用于转换的区别。

在字符串转换到字节数组的过程中,Encoding.Default 会将每个单字节字符,如半角英文,转换成 1 个字节,而把每个双字节字符,如汉字,转换成 2 个字节。而 Encoding.Unicode 则会将它们都转换成两个字节。我们可以通过下列简单的了解一下转换的方法,以及使用 Encoding.DefaultEncodeing.Unicode 的区别:

private void TestStringBytes() {
    string s = "C#语言";
    byte[] b1 = System.Text.Encoding.Default.GetBytes(s);
    byte[] b2 = System.Text.Encoding.Unicode.GetBytes(s);
    string t1 = "", t2 = "";
    foreach (byte b in b1) {
        t1 += b.ToString("") + " ";
    }
    foreach (byte b in b2) {
        t2 += b.ToString("") + " ";
    }
    this.textBox1.Text = "";
    this.textBox1.AppendText("b1.Length = " + b1.Length + "\n");
    this.textBox1.AppendText(t1 + "\n");
    this.textBox1.AppendText("b2.Length = " + b2.Length + "\n");
    this.textBox1.AppendText(t2 + "\n");
}

运行结果如下,不说详述,相信大家已经明白了。

b1.Length = 6
67 35 211 239 209 212
b2.Length = 8
67 0 35 0 237 139 0 138

将字节数组转换成字符串,使用 Encoding 类的 string GetString(byte[])string GetString(byte[], int, int) 方法,具体使用何种 Encoding 还是由编码决定。在 TestStringBytes() 函数中添加如下语句作为实例:

byte[] bs = {97, 98, 99, 100, 101, 102};
string ss = System.Text.Encoding.ASCII.GetString(bs);
this.textBox1.AppendText("The string is: " + ss + "\n");

运行结果为:The string is: abcdef

7. 各种数值类型和字节数组之间的转换

在第 1 条中我们可以查到各种数值型需要使用多少字节的空间来保存数据。将某种数值类型的数据转换成字节数组的时候,得到的一定是相应大小的字节数组;同样,需要把字节数组转换成数值类型,也需要这个字节数组大于相应数值类型的字节数。

现在介绍此类转换的主角:System.BitConverter。该类提供了 byte[] GetBytes(...) 方法将各种数值类型转换成字节数组,也提供了 ToInt32ToInt16ToInt64ToUInt32ToSignleToBoolean 等方法将字节数组转换成相应的数值类型。

由于这类转换通常只是在需要进行较细微的编码/解码操作时才会用到,所以这里就不详细叙述了,仅把 System.BitConverter 类介绍给大家。

8. 转换成十六进制

任何数据在计算机内部都是以二进制保存的,所以进制与数据的存储无关,只与输入输出有关。所以,对于进制转换,我们只关心字符串中的结果。

在上面的第 4 条中提到了 ToString() 方法可以将数值转换成字符串,不过在字符串中,结果是以十进制显示的。现在我们带给它加一些参数,就可以将其转换成十六进制——使用 ToString(string) 方法。

这里需要一个 string 类型的参数,这就是格式说明符。十六进制的格式说明符是 "x" 或者 "X",使用这两种格式说明符的区别主要在于 A-F 六个数字:"x" 代表 a-f 使用小写字母表示,而 "X" 而表示 A-F 使用大字字母表示。如下例:

private void TestHex() {
    int a = 188;
    this.textBox1.Text = "";
    this.textBox1.AppendText("a(10) = " + a.ToString() + "\n");
    this.textBox1.AppendText("a(16) = " + a.ToString("x") + "\n");
    this.textBox1.AppendText("a(16) = " + a.ToString("X") + "\n");
}

运行结果如下:

a(10) = 188
a(16) = bc
a(16) = BC

这时候,我们可能有另一种需求,即为了显示结果的整齐,我们需要控制十六进制表示的长度,如果长度不够,用前导的 0 填补。解决这个问题,我们只需要在格式说明符“x”或者“X”后写上表示长度的数字就行了。比如,要限制在 4 个字符的长度,可以写成“X4”。在上例中追加一句:

this.textBox1.AppendText("a(16) = " + a.ToString("X4") + "\n");

其结果将输出 a(16) = 00BC

现在,我们还要说一说如何将一个表示十六进制数的字符串转换成整型。这一转换,同样需要借助于 Parse() 方法。这里,我需要 Parse(string, System.Globalization.NumberStyles) 方法。第一个参数是表示十六进制数的字符串,如“AB”、“20”(表示十进制的 32) 等。第二个参数 System.Globalization.NumberStyles 是一个枚举类型,用来表示十六进制的枚举值是 HexNumber。因此,如果我们要将“AB”转换成整型,就应该这样写:int b = int.Parse("AB", System.Globalization.NumberStyles.HexNumber),最后得到的 b 的值是 171。

9. 日期型数据和长整型数据之间的转换

为什么要将日期型数据转换为长整型数据呢?原因很多,但就我个人来说,经常将它用于数据库的日期存储。由于各种数据库对日期型的定义和处理是不一样的,各种语言对日期型数据的定义的处理也各不相同,因为,我宁愿将日期型数据转换成长整型再保存到数据库中。虽然也可以使用字符串来保存,但使用字符串也会涉及到许多问题,如区域等问题,而且,它需要比保存长整型数据更多的空间。

日期型数据,在 C# 中的参与运算的时候,应该也是转换为长整型数据来运算的。它的长整型值是自 0001 年 1 月 1 日午夜 12:00 以来所经过时间以 100 毫微秒为间隔表示时的数字。这个数在 C# 的 DateTime 中被称为 Ticks(刻度)。DateTime 类型有一个名为 Ticks 的长整型只读属性,就保存着这个值。如此,要从一个 DataTime 型数据得到 long 型值就非常简单了,只需要读出 DataTime 对象的 Ticks 值即可,如:

long longDate = DateTime.Now.Ticks;

DateTime 的构造函数中也提供了相应的,从长整型数据构造 DateTime 型数据的函数:DateTime(long)。如:

DateTime theDate = new DateTime(longDate);

但这样对于很多 VB6 程序员来说,是给他们出了一道难题,因为 VB6 中的日期型数据内部是以 Double 型表示的,将其转换为长整型后得到的仅仅是日期,而没有时间。如何协调这两种日期类型呢?

System.DateTime 提供了 double ToOADate()static DateTime FromOADate(double) 两个函数来解决这个问题。前者将当前对象按原来的 double 值输出,后者则从一个 double 值获得一个 System.DateTime 对象。举例如下:

private void TestDateTimeLong() {
    double doubleDate = DateTime.Now.ToOADate();
    DateTime theDate = DateTime.FromOADate(doubleDate);
    this.textBox1.Text = "";
    this.textBox1.AppendText("Double value of now: " + doubleDate.ToString() + "\n");
    this.textBox1.AppendText("DateTime from double value: " + theDate.ToString() + "\n");
}

运行结果:

Double value of now: 37494.661541713
DateTime from double value: 2002-8-26 15:52:37

10. 格式化日期型数据

编程的过程中,通常需要将日期型数据按照一定的格式输出,当然,输出结果肯定是字符串。为此,我们需要使用 System.DateTime 类的 ToString() 方法,并为其指定格式字符串。

MSDN 中,System.Globalization.DateTimeFormatInfo 类的概述里对模式字符串有非常详细的说明,因此,这里我只对常用的一些格式进行说明,首先请看下表:

d月中的某一天一位数的日期没有前导零
dd月中的某一天一位数的日期有一个前导零
ddd周中某天的缩写名称在 AbbreviatedDayNames 中定义
dddd周中某天的完整名称在 DayNames 中定义
M月份数字一位数的月份没有前导零
MM月份数字一位数的月份有一个前导零
MMM月份的缩写名称在 AbbreviatedMonthNames 中定义
MMMM月份的完整名称在 MonthNames 中定义
y不包含纪元的年份如果不包含纪元的年份小于 10,则显示不具有前导零的年份
yy不包含纪元的年份如果不包含纪元的年份小于 10,则显示具有前导零的年份
yyyy包括纪元的四位数的年份
h12 小时制的小时一位数的小时数没有前导零
hh12 小时制的小时一位数的小时数有前导零
H24 小时制的小时一位数的小时数没有前导零
HH24 小时制的小时一位数的小时数有前导零
m分钟一位数的分钟数没有前导零
mm分钟一位数的分钟数有一个前导零
s一位数的秒数没有前导零
ss一位数的秒数有一个前导零

为了便于大家的理解,不妨试试下面的程序:

private void TestDateTimeToString() {
    DateTime now = DateTime.Now;
    string format;
    this.textBox1.Text = "";
    format = "yyyy-MM-dd HH:mm:ss";
    this.textBox1.AppendText(format + ": " + now.ToString(format) + "\n");
    format = "yy年M日d日";
    this.textBox1.AppendText(format + ": " + now.ToString(format) + "\n");
}

这段程序将输出结果:

yyyy-MM-dd HH:mm:ss: 2002-08-26 17:03:04
yy年M日d日: 02年8日26日

这时候,又出现一个问题,如果要输出的文本信息中包含格式字符怎么办?如

format = "year: yyyy, month: MM, day: dd";
this.textBox1.AppendText(now.ToString(format) + "\n");

将输出:

2ear: 2002, 4on下5: 08, 26a2: 26

这并不是我想要的结果,怎么办呢?有办法——

format = "\"year\": yyyy, \'month\': MM, \'day\': dd";
this.textBox1.AppendText(now.ToString(format) + "\n");

看,这次运行结果对了:

year: 2002, month: 08, day: 26

可以看出,只需要使用单引号或者双引号将文本信息括起来就好。 如果文本信息中包含双引号或者单引号又怎么办呢?这个问题,请读者们动动脑筋吧!

Contributors: FHL