任何编程语言都有数据类型的概念,这些数据类型大体可分为字符串、文本、数字、日期等等。下图就是C#中数据类型的继承关系图。
在这个结构图中所有的以“System”开头的都属于基础数据类型,其他的都是自定义数据类型。
System.Object类型
该类型表示C#数据类型体系中最为基础的类型,在C#中使用关键字“object”表示该类型。
C#的数据类型体系和其他编程语言的最大的不同就是实现了各种数据类型的统一,并提供了一个所有数据类型的基础类型,那就是在命名空间System下面的Object类型。
在其他编程语言,比如VB,其整数、浮点数等基础类型都是原子类型,不是从其他类型派生的,但C#是彻彻底底的实现了面向对象编程,即使这些技术类型都是从Object类型派生过来的,而Object类型就是最基础的类,再往上就没有类型了。
Object类型提供一些成员方法,最常见的有
Equals |
带一个参数,用于对两个对象数据进行比较,若相等则返回True,否则返回False。 |
Finalize |
在自动回收对象之前执行清理操作,该方法一般由.NET框架自动调用。 |
GetHashCode |
生成一个与对象的值相对应的数字以支持哈希表的使用。 |
ToString |
生成描述对象数据的字符串。一般供人阅读。 |
在Object类型上C#使用以下基础数据类型,而且有很多基础数据类型在C#有对应的关键字表示。
类型 |
对应的 |
说明 |
System.Boolean |
bool |
布尔类型,其值只能为true或false,该数据类型占用1个字节内存。 |
System.Byte |
byte |
表示一个从0到255的整数,该数据类型占用1个字节内存。 |
System.SByte |
sbyte |
表示一个从-127到127的整数,占用1个字节, |
System.Char |
char |
表示一个字符数据,占用2个字节。和C语言类似,Char类型可以强制转换为整数。这个字符数据是采用Unicode编码格式。 |
System.Int16 |
short |
表示一个从 -32768 到 +32767 的整数,占用2个字节。 |
System.UInt16 |
ushort |
表示一个从0 和 65535的整数,占用2个字节。 |
System.Int32 |
int |
表示一个从-2,147,483,648(约负21亿) 到 +2,147,483,647(约21亿)的整数,占用4个字节。 |
System.UInt32 |
uint |
表示一个从0 到 4,294,967,295 之间的整数,占用4个字节。 |
System.Int64 |
long |
表示一个从-9,223,372,036,854,775,808 到 +9,223,372,036,854,775,807的整数,占有8个字节。 |
System.UInt64 |
ulong |
表示一个从0 到 18,446,744,073,709,551,615的整数,占用8个字节。 |
System.Single |
float |
表示一个从-3.402823e38 和 +3.402823e38 之间的单精度浮点数字,有7位有效数字。占用4个字节。 |
System.Double |
double |
表示一个-1.79769313486232e308 和 +1.79769313486232e308 之间的浮点数,有15位有效数字,占用8个字节。 |
System.Decimal |
decimal |
表示一个从+79,228,162,514,264,337,593,543,950,335 到 -79,228,162,514,264,337,593,543,950,335之间的数字。而且计算时尽量不进行舍入操作,这样能维护运算精度,比较适合财务运算。 |
System.DateTime |
无 |
表示一个从公元(基督纪元)0001 年 1 月 1 日午夜 12:00:00 到公元 (C.E.) 9999 年 12 月 31 日晚上 11:59:59 之间的时间日期数据,精确到100纳秒。 在初始化日期数据的时候,可以传递年数、月份数和日数,也可以继续添加24小时制的小时数、分钟数和秒数。 以下代码就定义了一个DateTime类型。 DateTime dtm = new DateTime( 1980 , 2 , 14 ); 也可以为 DateTime dtm2 = new DateTime( 1980 , 2 , 14 , 16, 23 , 39 ); |
System.String |
string |
表示一段文本,采用UTF-16编码,可以包含字符“\0”。 |
System.Enum |
enum |
所有枚举类型的基础类型。 |
System.Deleate |
delegate |
所有委托类型的基础类型。 |
Syatem.Array |
无 |
所有数组类型的基础类型。 |
C#支持数组,它包含着若干干相同类型的变量。在C#代码中,使用“type[] arrayName”的方式来定义数组,比如以下代码定义了几个数组
int[ ] ids = null; // 定义了一个整数数组 string[ ] names = new string[ 100 ]; // 定义了一个字符串数组,并初始化为包含100个数据。 |
在初始化数组时,数组里的元素值也初始化了,对于bool类型的数组,其元素值初始化为false,数值型的初始化为0,字符串的初始化为null。
在C#中数组的下标是从0开始的,可以使用数组对象的Length属性获得数组的长度,比如以下代码就是遍历数组所有的元素。
string[ ] names = new string[100]; for (int iCount = 0; iCount < names.Length; iCount++) { string name = names[iCount]; } |
也可以使用foreach语句来遍历数组中所有的元素。如下所示
string[ ] names = new string[100]; foreach( string name in names ) { // 此处可以使用变量“name”的值 } |
注意在for循环中可以通过修改“names[ iCount ]”的值来修改数组中存储的数据;但在foreach循环中,name不能修改,是只读的。
在初始化结构体类型的数组时,数组是有效的,而且数组中的元素也是有效的;而在初始化类类型的数组时,虽然数组是有效的,但其数组的元素全部为空引用。
例如以下代码定义了结构体类型MyPeopleStruct。
public struct MyPeopleStruct { public string Code; public string Name; } |
针对该类型执行以下代码是不会出事的
MyPeopleStruct[] peoples = new MyPeopleStruct[100]; peoples[33].Name = "张三"; |
因为结构体数组初始化后,系统会自动创建数组元素对象。
若以下代码定义了类类型MyPeopleClass。
public class MyPeopleClass { public string Code = null; public string Name = null; } |
若有以下代码
MyPeopleClass[] peoples = new MyPeopleClass[100]; peoples[3].Name = "张三"; |
这段代码可以编译通过,但运行时会在代码“peoples[3].Name = "张三"”处报空引用的程序错误。这是因为初始化了类类型数组,只是为数组分配了内存空间,但没有相应的创建对象类型,该数组的元素全部设置为空引用,对于空引用调用其成员就会报空引用错误。
空引用错误是C#程序中最常见的错误。
在程序开发中个,仅仅使用这些基础数据类型是不够的,还需要开发人员根据需要自定义数据类型。在C#中,自定义的数据类型有类、结构体、枚举和委托。
在C#中使用关键字“class”定义的类型为类类型,以下代码就定义了一个类类型。
public class PeopleClass { public PeopleClass() { }
private string _Name = null; public string Name { get { return _Name; } set { _Name = value; } } public override string ToString() { return _Name; } } |
在代码“public class PeopleClass”中,关键字“public”说明该类型是公开的;“class”说明这是一个类类型;“PeopleClass”指定了类型的名称。
类类型是引用类型,比如执行了以下代码
PeopleClass p1 = new PeopleClass(); p1.Name = "张三"; People p2 = p1 ; p2.Name = "李四"; |
则代码执行后,变量p1和p2指向的是同一个对象,也可以理解为指向同一个内存地址,很显然,通过变量p2修改值和通过p1修改其效果是一样的,这样p1.Name和p2.Name的值都等于“李四”。
C#支持结构体类型,以下代码就定义了一个结构体类型。
public struct PeopleStruct { public string Code; public string Name; public bool Sex; } |
这段代码定义了一个名为People的结构体,它有“Code”、“Name”、“Sex”三个公开字段。我们可以使用一下的代码来使用这个结构体类型。
PeopleStruct myPeople = new PeopleStruct( ); myPeople.Code = “1000”; myPeople.Name = “张三”; |
结构体类型中可以定义字段、属性、方法和事件,但和类类型有着以下区别
●结构体中字段不能初始化,比如在结构体中需要写成“public string Code;”,而在类类型中可以写成“public string Code = null;”。
●结构体中不能定义默认的构造函数(无参数的构造函数)。
●结构体可以定义带参数的构造函数,但在函数体中在执行任何代码前必须对所有的字段进行赋值初始化。
●结构体对象在赋值时会重新创建一个对象并复制所有的字段值。对新结构体的数据的修改不会影响原始对象的内容。比如对于结构体类型People执行了以下代码
People p1 = new People(); p1.Name = "张三"; People p2 = p1 ; p1.Name = "李四"; |
则p2.Name的值还是为“张三”。
●结构体不能继承自其他结构体类型,也不能派生新的结构类型。所有的结构体类型直接继承自类型System.ValueType。
●结构体可以实现接口。
C#支持枚举类型,以下代码就定义了一个枚举类型。
public enum BarcodeStyle { Code128A, Code128B, Code128C } |
在C#中,枚举变量是可以转换为整数的,第一个枚举成员默认等于0,然后依次增加,不过也可以明确的设置枚举成员数值。例如上述代码等价于
public enum BarcodeStyle { Code128A = 0 , Code128B = 1, Code128C = 2 } |
这个设置数值是比较自由的,可以任意指定,比如可以使用以下代码定义枚举类型。
public enum FlagStyle { Flag1 = 1 , Flag2 = 4, Flag3 = 64 } |
所有的枚举类型都是从基础类型System.Enum派生的。System.Enum类型提供了一些能用于所有枚举类型的方法,常用的有
GetName |
获得等于指定数据的枚举项目的名称。该函数是静态的,具有两个参数,第一个是枚举类型,第二个是某个常数。 例如对于上面的BarcodeStyle枚举类型,执行代码“Enum.GetName( typeof( BarcodeStyle ) , 0 ) ”,就返回字符串“Code128A”;执行“Enum.GetName( typeof( BarcodeStyle” , 1 )”就返回字符串“Code128B”。 |
GetNames |
获得枚举类型的所有枚举项目的名称组成的字符串数组。该函数时静态的,参数就是枚举类型变量。 例如对于BarcodeStyle枚举类型,执行代码“Enum.GetNames(typeof( BarcodeStyle ))”就返回一个字符串数组,数组元素是“Code128A”、“Code128B”、“Code128C”。 |
GetValues |
获得枚举类型的所有枚举项目组成的数组。该函数是静态的,参数就是枚举类型变量。 例如对于BarcodeStyle类型,执行代码“Enum.GetValues( typeof( BarocdeStyle ))”就返回一个数组,数组元素就是“BarcodeStyle.Code128A”、“BarcodeStyle.Code128B”、“BarcodeStyle.Code128C”。 |
Parse |
解析字符串并转化为枚举类型,若转化失败则会抛出异常。该函数时静态的,参数是指定的枚举类型和要解析的字符串,此外还有第三个布尔类型的可选参数,用于指明是否区分大小写。 例如对于BarcodeStyle类型,执行代码“Enum.Parse( typeof( BarcodeStyle ) , “Code128A” )”就返回“BarcodeStyle.Code128A”。 执行代码“Enum.Parse( typeof( BarcodeStyle ) , “code128a”, true)”也返回“BarcodeStyle.Code128A”。 注意,当解析失败时,该函数会抛出异常。 |
TryParse |
解析字符串并试图将其转化为枚举类型,如转化失败则不抛出异常,该函数返回转化是否成功的布尔值。该函数是静态的,第一个参数就是要转化的字符串,第二个可选参数就是转化时是否区分大小写,第三个参数就是保存转化结果的枚举变量。 例如对于“BarcodeStyle”,调用代码“Enum.TryParse( “Code128A” , out value )”,则函数返回true而且value值被设置成“BarcodeStyle.Code128A”;调用代码“Enum.TryParse(“abc” , out value )”,则函数返回false,表明转化失败。 |
ToString |
返回表示枚举值的字符串,一般为其所表示的枚举项目的名称。 |
在C#中使用关键字“interface”定义的数据类型为接口,以下代码就定义了一个接口类型。
public interface IMyInterface { string Value { get; set; } int Sum( int a , int b ); } |
在这段代码中,“public”说明该类型是公开的;“interface”说明该类型是接口类型;“IMyInterface”指定了类型名称,一般的约定成俗,接口类型的名称是以大写字母“I”开头的;“string Value { get ; set ;}”定义了一个属性;“int Sum( int a , int b )”定义了一个方法。
接口用于定义一组属性、方法和事件,但不能包含字段,接口的成员无需修饰,全部是公开的。
接口不能实例化,仅仅是一个空洞的模板让人敬仰,但毫无作用;它必须深入群众,灵魂附在其他类或者结构体中才能发挥作用。这个灵魂附体的过程就是实现接口。
实现接口的方式不复杂,就是在定义类型的时候声明是从接口类型派生的,并将接口中定义的所有的属性、方法一个不拉的实现,从这个的角度上看接口类型有点像类类型。
例如对于上面的IMyInterface类型,使用以下代码就定义了一个实现该接口的类。
public class MyClass2 : IMyInterface { private string _Value = null ; public string Value { get { return _Value ; } set { _Value = value ; } } public int Sum(int a, int b) { return a + b ; } } |
当接口成员比较多难于记忆时,VS.NET的C#代码编辑器会提供一些帮助功能来编写实现接口的代码,如下图所示,文本光标移动到方框所在的“IMyInterface”,此时其左下角出现一个智能标签,鼠标点击这个智能标签会显示一个菜单。
点击“实现接口”IMyInterface””菜单项目则自动的往“MyClass2”的代码块中插入能实现IMyInterface的代码,此时自动生成的代码如下
public class MyClass2 : IMyInterface {
#region IMyInterface 成员
public string Value { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } }
public int Sum(int a, int b) { throw new NotImplementedException(); }
#endregion }
|
而点击“显式实现接口”IMyInterface””菜单项目则自动生成以下代码
public class MyClass2 : IMyInterface { #region IMyInterface 成员
string IMyInterface.Value { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } }
int IMyInterface.Sum(int a, int b) { throw new NotImplementedException(); }
#endregion }
|
实现接口和显式实现接口是有区别的。对于实现接口,可以通过以下代码的方式访问它的成员。
MyClass2 instance = new MyClass2(); instance.Sum(3, 4); |
也可以通过以下方式访问成员。
IMyInterface instance = new MyClass2(); instance.Sum(3, 4); 或者 MyClass2 instance = new MyClass2(); IMyInterface instance2 = (IMyInterface )instance; instance2.Sum(3, 4); |
而对于显式实现接口只能使用第二种方式访问。
在实际开发中,这两种实现接口的方式还可以混用,不过显式实现接口用得比较少。
在C#中,类只能继承另外一个类,但可以实现多个接口。以此不同的是C++可以继承自多个类和多个接口。
委托就是一个指向成员方法的对象,C语言中有一个函数指针的概念,委托就可以看做面向对象的函数指针。其定义语法为“delgate 返回值 委托名( 参数类别 )”,例如以下代码就定义了一个委托类型
public delegate int Int2Handler( int a , int b ); |
在这段代码中,“public”指明的该类型是公开的,“delegate”说明正在定义一个委托类型,“int”指明委托的返回类型,“Int2Handler”是委托类型的名称,“int a , int b”就是该委托使用的参数列表。
委托代表一个类型成员方法,而且该方法的返回值和参数列表必须和委托声明的一模一样。例如在某个类中定义了以下三个成员方法。
private int Sum(int a, int b) { return a + b; }
private int Mul(int a, int b) { return a * b; }
private double Sum2(double a, double b) { return a + b; } |
则方法Sum和Mul的签名和委托Int2Handler的签名完全吻合,因此Int2Handler委托可以指向这两个方法;而方法Sum2的签名不对,因此Int2Handler委托不能指向该方法。
以下代码演示了使用委托
Int2Handler handler = null; handler = new Int2Handler(Sum); int result = handler(3, 4); // 返回7 handler = new Int2Handler(Mul); result = handler(3, 4); // 返回12 |
在这段代码中,创建委托对象实例传入的参数就是方法名称,因此同样执行代码“handler( 3 , 4 )”,由于第一次委托指向方法Sum,计算结果为7;而第二次委托执行方法Mul,计算结果为12。这样委托就能实现非常好的灵活性。
C#中可以实现匿名委托,以下代码就演示了匿名委托。
Int2Handler handler = delegate(int a, int b) { return a + b; }; int result = handler(3, 4);
handler = delegate(int a, int b) { return a * b; };
result = handler(3, 4); |
可以看出可以使用“委托类型 委托变量名 = delegate(参数列表) { 方法功能代码 };”的语句创建一个匿名委托,而且无需编写独立的方法来放置功能代码。因此使用匿名委托能写出比较精巧的代码,但注意匿名委托也有缺点,那就是不利于调试修改。
VS.NET提供断点调试时修改程序代码的功能,VB也有类似的功能。但当修改了包含匿名委托的方法中的代码时,VS.NET的这个功能就无效,需要重新编译重新运行程序时才能应用代码的改变。
在程序开发中经常需要进行数据类型的转换,C#提供强制数据类型转换和使用关键字as的数据类型转换,还提供关键字is来进行数据类型的判断。
强制类型转换就是将一个类型的数据无条件的强制转换为其他数据类型。具有安全的强制类型转换和不安全的强制类型转换。
对于数值类型的基础数据类型,存在以下安全的强制数据类型转换路径“bool→sbyte→char→short→int→long→float→double”,从这里可以看出,这条安全的路径越靠后,取值范围就越大。比如以下代码就是一个安全的强制类型转换。
int v1 = 98; double v2 = (double)v1; |
对于安全的强制类型转换,可以将类型转换运算符省掉,比如上述第二行代码可以写成“double v2 = v1;”
反过来,在这个强制类型转换路径上逆道行驶,则就是不安全的强制类型转换,比如以下代码就是不安全的强制类型转换,
double v3 = 98.34; int v4 = (int )v3; |
这个转换后,数值“98.34”被转换为“98”,两者不等,因此是不安全的,可能导致程序错误,而且不安全的强制转换中转换符号不能省略,比如上面的第二行代码就不能写成“int v4 = v3 ;”。
类类型之间也能进行强制类型转换,语法为“类型名称 变量名=(类型名称)变量名2”。而且要保证要转换的对象引用是转换后的类型或者派生类。
比如有类型Class1派生自Object类型,类型Class2派生自Class1,则存在类型转换路径“object→Class1→Class2”,若顺着这个路径就是安全的类型转换,若在这个路径上逆道行驶,则就是不安全的强制类型转换,肯定会失败。
比如有以下代码
Class1 instance = new Class1(); Class2 instance2 = ( Class2 ) instance; |
这就是一个不安全的类型转换,程序编译时有可能不报错,但在运行时报错。
object instance = new Class2(); Class1 instance2 = ( Class1 ) instance; |
这就是一个安全的类型转换过程,转换是会成功的。
由于强制类型转换可能会发生错误,C#提供一个使用关键字as的类型转换。其语法为“要转换的类型名称 变量= 要转换的变量 as 要转换的类型名称 ”,若转换成功则会设置变量值,若转换失败则设置变量值为空引用。
比如有类型Class1派生自Object类型,类型Class2派生自Class1,则存在类型转换路径“object→Class1→Class2”。此时可以执行以下代码
Class1 instance = new Class1(); Class2 instance2 = instance as Class2 ; |
由于这是不安全的转换,转换失败,此时程序不会报错,但会设置变量instance2为空引用。
object instance = new Class2(); Class1 instance2 = instance as Class1; |
这是安全的转换,转换成功,会设置变量instance2的变量指向某个对象实例。
使用as类型转换比强制类型转换的好处就是转换失败时不会发生错误,不过程序需要检查转换结果值来判断转换是否成功。
C#提供关键字is来进行类型判断,其用法为“变量名 is 类型名称”,这是一个表达式,返回的是布尔值,用于判断指定的对象实例可否转换为指定的类型。例如对于上面的类型Class1和Class2执行以下代码
object instance = new Class2( ); bool result = instance is Class1 ; |
由于此处instance指向的是一个Class2类型,而Class2是Class1的派生类,可以转换为Class1类型,因此instance是类型Class1,因此result的值为true。
使用关键字is的类型判断能判断对象是否是指定的类型或者派生类型,也可判断是否实现了指定的接口,而且这个过程不会报错。