软件开发中,数据类型的转换是一项非常重要的工作,同时也是非常容易产生错误的地方;本文,我们就C#开发中的各种数据类型转换做一些讨论,供大家参考。这些内容包括:

  • C#语言内置转换
  • as与is运算符
  • TryParse()方法
  • Convert类
  • 封装转换方法
  • 对象的传递、复制和序列化

C#语言内置转换

在C#编程语言层面,内置了一些数据类型转换的方法,如隐式转换、强制(显式)转换,以及与对象相关的as和is运算符。下面分别进行讨论。

隐式转换。是指在表达式中有不同类型的数据进行运算,此时,就会将其中的一些数据的类型进行转换,然后,表达式会使用相同类型的数据进行运算。如“float sum = 1.0f + 1;”表达式中,1.0f为float类型,而1则默认为int类型,此时,会自动将数据1转换成float类型后再进行计算,而最终的结果也是float类型。为什么是这样?稍后讨论。

强制转换。在表达式中,我们可以在操作数前使用一对小括号指定目标类型,比如有一个float类型的变量fNum,在表达式“int sum = 2 + (int)fNum;”中,就可以将其转换为int类型后进行计算。

隐式转换和强制转换操作一般用于数值类型的转换。我们知道,不同数据类型的取值范围是不同的;在进行隐式转换时,会遵循一个基本的原则,即在转换时,将取值范围窄的类型转换为取值范围宽的类型,如果默认不能完成这样的工作,编译器就会报错,如“int sum = 1.0f + 1;”语句就不能通过编译,因为操作数1会隐式转换为float,“1.0f + 1”的结果会是float,但是float不能隐式转换为int类型(那样就会丢掉数值的小数部分,而在数据的传递中,数据的丢失并不是一个好的选择)。

如果我们在代码中需要明确的目标数据类型,就应该使用强制转换,但在使用时必须要注意数据的丢失问题,特别是在浮点数或decimal类型转换成整数类型的过程中。

此外,关于数据丢失,还有一些比较重要的概念,如取值范围和溢出。表达式“int sum = int.MaxValue + 1;”中,sum的结果会是什么呢?此语句在编译时也会报错,因为产生了溢出,数据类型不能保存取值范围(参考MinValue和MaxValue字段)以外的数据。

当然,你也可以强制完成上面的计算,但必须使用unchecked关键字,如“int sum = unchecked(int.MaxValue + 1);”。但这样一来,运算的结果恐怕比没有结果更没有意义。

as与is运算符

这是关于对象类型判断和转换的运算符。

使用is运算符,可以判断一个对象是不是某个类的实例。比如,我们定义了一个arr对象为ArrayList类型,那么“arr is ArrayList”的运算结果就是true。请注意下面的代码:

ArrayList arr = new ArrayList();
object obj = arr;
bool result = obj is ArrayList;

代码中,result的值会是什么?答案是true,因为每个对象在传递时,其原始类型标识都会保留,所以,代码中依然可以判断出对象的原始类型。

在使用is运算符判断一个对象是否为一个类型后,我们可以使用as运算符将对象转换为这个类型,然后,对象就可以调用此类型中定义的成员了。如在窗体中添加下面的代码:

foreach (object obj in this.Controls)
{
    if (obj is TextBox)
    {
        TextBox txt = obj as TextBox;
        txt.Text = txt.Name;
        txt.ForeColor = Color.Red;
    }
}

代码中,我们在遍历窗体中的控件(使用obj对象),当这个控件是文本框控件(TextBox)时,使用as运算符转换为TextBox类型对象txt,然后在文本框中显示其名称,并将字体设置为红色;而直接使用object类型对象obj就无法完成这些工作。

TryParse()方法

在.NET Framework中定义的值类型中,都定义了TryParse()方法(.NET 2.0及以后版本)。此方法用于将字符串转换为相应的值类型,其语法为: bool TryParse(stringValue, out result);

其中,stringValue是需要转换的字符串内容,而result定义为输出参数,其类型为相应的值类型,如int的TryParse()方法中的result参数定义为int类型。方法的返回值为bool类型,转换成功返回true,否则返回false。如下面的代码:

int result;
if (int.TryParse("123", out result))
{
    Console.WriteLine("字符串转换结果为 {0}", result);
}
else
{
    Console.WriteLine("字符串不能转换为int类型");
}

Convert类

Convert类为静态类,定义在System命名空间。

正如其名,Convert类的功能就是提供了一系列的各种标准数据类型之间的转换方法,这些方法名中都使用了.NET Framework类型中定义的数据类型名称,如C#中的int就是Int32,将其它类型转换为int类型的方法就是ToInt32()。如“int result = Convert.ToInt32("123");”。

使用Convert类时应注意,如果转换不能正确完成,则会抛出异常。

封装转换方法

前面,我们已经看到了在C#和.NET Framework类库中提供的常用的数据类型转换方法,我认为它有两个问题:

  • 除了TryParse()方法,其它的转换操作都有可能产生异常,这就需要在代码中很小心地进行处理。
  • TryParse()方法虽然不产生异常,但是,它只能对字符串进行转换,功能显然不够强大。

为了避免上述的两个问题,我将标准值类型的转换进行了封装,每一种目标类型都有两个方法,其中一个返回普通的值类型,另一个方法则返回相应的可空类型。以下是转换为int类型的两个方法:

public static int? ToIntNullable(object obj)
{
    if (obj == null) return null;
    int result;
    if (int.TryParse(obj.ToString(), out result))
        return result;
    else
        return null;
}

public static int ToInt(object obj)
{
    if (obj == null) return 0;
    int result;
    if (int.TryParse(obj.ToString(), out result))
        return result;
    else
        return 0;
}

先看一下ToIntNullable(object obj)方法,它的功能是将参数obj转换成可空的int类型;当obj可以转换成int类型时就返回转换后结果,否则返回null值。为什么对于int类型还要支持null值呢?这是为了与数据库中的数据兼容,我们知道,在数据库中的数据字段都有可能是null值的。通过可空类型,我们就可以完全兼容的方式在C#代码和数据库之间传递数据了。

而ToInt(object obj)方法,当obj可以转换成int类型时返回转换结果,否则返回0值。在C#代码中,如果只需要一个可以使用的int类型,而忽略空值或0的区别时,就可以使用这个方法,无论什么情况下,都有一个int类型的结果可供使用。

代码中,还可以根据需要封装其它类型的转换方法。

在C#代码中使用这样的转换方法,可以有效提高编码的效率。一方面可以有效避免可能的异常产生;另一方面,可以对空值和各种类型数据更方便地进行处理。在我的代码库中,这些转换方法会定义在一个名为CC的静态类中。

对象的传递、复制和序列化

我们先来看一下对象传递的特点。如下面的代码:

ArrayList arr1 = new ArrayList();
arr1.Add("abc");
ArrayList arr2 = arr1;
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);
arr2[0] = "123";
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);

代码中,我们定义了两个ArrayList对象arr1和arr2,它们实际上是指向了同一个“对象体”,这样,我们修改其中的一个,实际上arr1和arr2指向的内容就同时改变了,此代码的显示结果就是:

arr1[0]=abc, arr2[0]=abc
arr1[0]=123, arr2[0]=123

现在,我们看一个特殊的情况,那就是string类型的赋值。我们知道,string是引用类型,但是它的赋值却和其它的引用类型不太一样,这是因为,string保存的是不可变字符串,也就是说一个字符串对象(真正的字符串内容,而不是string对象变量)一旦创建,其内容是不能改变的,对于字符串的任何修改,都会产生一个新的字符串对象,如字符串的连接、重新赋值等操作。如下面的代码:

string str1 = "abc";
string str2 = str1;
Console.WriteLine("str1={0}, str2={1}", str1, str2);
str2 = "123";
Console.WriteLine("str1={0}, str2={1}", str1, str2);

此代码的运行结果为:

str1=abc, str2=abc
str1=abc, str2=123

所以,当我们看到string对象与其它引用类型的对象在传递中会有不一样的行为时,不应感到太多的惊讶。

回到我们的主题,在C#中,如果我们对对象进行赋值操作,就必须非常小心,如果修改了其中一个对象的内容,另一个对象的内容也会改变,这也是C#中默认的复制方式,我们称为“浅复制”。浅复制在复制引用类型时,只会复制对象的引用,而不是对象内容真正的复制,如果我们需要完整地复制对象体,则需要使用“深复制”,其中的一个方法让类支持ICloneable接口,实现其中的Clone()方法来完成对象的复制操作,实际操作上,这个方法实现起来还是比较复杂的。

此时,我们可以使用一个更加直观的方法来完整复制对象,这个方法就是使用序列化。

一个类型如果要支持序列化,必须在类定义时使用SerializableAttribute特性,如:

[Serializable]
public class Class1
{
    // 类定义
}

在.NET Framework类库中的很多类型都支持序列化操作,特别是数据容器或数据集合类型,ArrayList就是其中之一。

接下来,我们就要看看如何真正地完成序列化操作。序列化操作可以有两个方向相反的操作,即:

  • 对象转换为字节数组(序列化)。
  • 字节数组还原成对象(反序列化)。

为了方便使用,我们将这两个操作分别封装为两个方法,如:

using System;
using System.Collections;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace chyx
{
    // 静态类,封装常用代码
    public static class CC
    {
        // 将对象序列化成字节数组
        public static byte[] ToBytes(object obj)
        {
            if (obj == null) return null;
            using (MemoryStream s = new MemoryStream())
            {
                IFormatter f = new BinaryFormatter();
                f.Serialize(s, obj);
                return s.GetBuffer();
            }
        }

        // 将字节数组反序列化成对象
        public static object ToObject(byte[] Bytes)
        {
            using (MemoryStream s = new MemoryStream(Bytes))
            {
                IFormatter f = new BinaryFormatter();
                return f.Deserialize(s);
            }
        }

        // 更多成员...
    }
}

我们如何利用序列化来复制对象呢?这时,我们可以在CC类定义一个Clone()方法来做这项工作,如:

public static object Clone(object obj)
{
    if (obj == null) return null;
    byte[] arrByte = ToBytes(obj);
    return ToObject(arrByte);
}

是不是很简单,下面的代码,我们将对这个方法进行测试。

ArrayList arr1 = new ArrayList();
arr1.Add("abc");
ArrayList arr2 = CC.Clone(arr1) as ArrayList;
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);
arr2[0] = "123";
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);

由于我们使用Clone()完全复制了arr1,所以,arr2将是一个新的ArrayList对象,而不和arr2指向同一引用。代码的输出结果如下:

arr1[0]=abc, arr2[0]=abc
arr1[0]=abc, arr2[0]=123

使用序列化和反序列化可以简化复杂的对象的深复制(完全复制)操作,同时,我们还可以利用这一点对对象进行持久化操作,我们可以将一个对象序列化后保存在一个文件中(或其它形式),然后,可以读取它并还原成对象。如下面的代码:

ArrayList arr1 = new ArrayList();
arr1.Add("abc");
byte[] bytes = CC.ToBytes(arr1);
string fileName=@"d:\arr1.object";
File.WriteAllBytes(fileName, bytes);
byte[] readBytes = File.ReadAllBytes(fileName);
ArrayList arr2 = CC.ToObject(readBytes) as ArrayList;
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);

此代码的运行结果如下:

arr1[0]=abc, arr2[0]=abc

在界面、代码库和数据库三者之间,或者它们内部传递数据时,保证数据的完整性和正确性,以及保证传递的效率都是非常有挑战性的工作;本文中所讨论的内容是笔者在实际开发工作中一些关于数据传递和转换方面的总结。有不当之处,还请各位批评指正,不胜感激!