第一章 程序设计
“要想成为真正的程序员,我们需要进行一场洗礼。”
“程序 = 数据结构 + 算法。”这样的公式很精辟,它越过了表层直接描述了程序的本质。不仅如此,这样几个简单的单词也让我们明白“我们应该学习什么内容?”。人们做任何事都有一定的程序,只是没有意识到。有些人将日常行动列成表格,这就是“编写程序”。
本章的学习重点:
◆ 基本数据类型
◆ 值类型与引用类型
◆ var类型
1.2数据类型
在本文中,我们首先从介绍基本数据类型开始,然后迅速进入关于引用类型和值类型的讨论。对所有的开发人员来说,熟练掌握引用类型和数值类型的应用差别尤其重要。在编写代码的过程中,如果对这两种类型使用不当会导致程序Bug并引起性能问题。
1.2.1基本数据类型
到目前为止,只用过少量基本数据类型,而且书中只对它们一笔带过,没有详细解释。C#中存在着大量类型,而且可以合并不同的类型来创建新类型。然而,C#中有几种类型非常简单,它们被视为其他所有类型的基础。这些类型称为预定义类型(predefined type)或者基本类型(primitive type)。C#语言的基本类型包括8种整数类型、2种浮点类型、1种高精度类型、1种布尔类型、1种字符类型以及1种字符串类型。下面我们将探讨这些基本数据类型。
1、 整数类型
C#语言共有8种整数类型,我们可以选择最恰当的一种数据类型来存放数据,避免浪费资源。
类型 大小 范围 BCL名称
sbyte 8位 -128~127 System.SByte
byte 8位 0~255 System.Byte
short 16位 -32768~32767 System.Int16
ushort 16位 0~65535 System.UInt16
int 32位 -2147483648~2147483 647 System.Int32
uint 32位 0~4294967295 System.Int32
long 64位 -9223372036854775808~9223372036854775807 System.Int64
ulong 64位 0~18 446 744 073 709 551 615 System.Int64
C#的所有基本类型都有一个短名称和一个完整名称。完整名称对应于BCL中命名的类型。这个名称在所有语言中都是相同的,而且它对程序集中的类型进行了唯一性的标识。因为基本数据类型是其他类型的基础,所以C#为基本数据类型的完整名称提供了短名称或者缩写。其实从编译器的角度看,两种名称是完全一样的,最终都将生成同样的代码。事实上,假如检查一下最终生成的CIL代码,会发现其中没有任何迹象显示源代码中使用的是哪一种名称。下面我们来看一个范例:
1 using System;
2 namespace Microsoft.Example
3 {
4 public class TestInt
5 {
6 static void Main(string[] args)
7 {
8 byte byteValue = 2;
9 short shortValue = -4;
10 int intValue = -8;
11 long longValue = -10;
12 Console.WriteLine("byte类型的变量值为:" + byteValue);
13 Console.WriteLine("short类型的变量值为:" + shortValue);
14 Console.WriteLine("int类型的变量值为:" + intValue);
15 Console.WriteLine("long类型的变量值为:" + longValue);
16 }
17 }
18 }
上述代码中,我们定义了一系列的整数类型,它们之间的差异主要是:不同类型的容量不同,还有就是数值的正负符合不同。第7行我们定义了一个byte类型,这个类型只能保存正整数。很多时候因为int既可以保存正数,也可以保存负数,uint才只能保存正整数。所有,我们按着这个思维定势,很容易搞混淆,以为byte可以保存正整数,也可以保存负整数。其实不是这样的,根本就没有ubyte这个类型,只有sbyte,这个大家一定要记住了。
最后的输出结果是:
byte类型的变量值为:2
short类型的变量值为:-4
int类型的变量值为:-8
long类型的变量值为:-10
在真实的项目中,我们定义变量类型的时候,通常不采用intValue方式来命名变量,这种方式叫做匈牙利命名法,就是把变量类型跟放在变量的前面,这种方法的好处是对变量的类型一目了然,我们这里这么写,也是因为这个原因,让读者比较快的分别不同的变量。在项目中,因为开发工具(IDE)的类型提示功能已经很强大了,根本不需要这么做。
2、 浮点类型
浮点数的精度是可变的。如果读取本来是0.1的一个浮点数,那么可能很容易读取成0.099 999 999 999 999 999或者0.100 000 000 000 000 000 1或者其他非常接近0.1的一个数。因为原始数字实在是太大了。根据定义,一个浮点数的精度与它包含的数字个数成正比。准确地说,精度取决于有效数字的个数,而不是一个固定值,比如±0.01。
类型 大小 范围 BCL名称 有效数字
Float 32位 ±1.5×1045~±3.4×1038 System.Single 7
Double 64位 ±5.0×10324~±1.7×10308 System.Double 15~16
3、 decimal类型
C#有一个数值类型具有128位精度。它适合大而精确的计算,尤其是金融计算。
类型 大小 范围 BCL名称 有效数字
Decimal 128位 ±1.0×1028~±7.9×1028 System.Decimal 28~29
与浮点数不同,decimal类型保证范围内的所有十进制数都是精确的。所以,对于decimal类型来说,0.1就是0.1,而不是一个近似值。不过,虽然decimal类型具有比浮点类型更高的精度,但它的范围较小。所以,从浮点类型转换为decimal类型可能发生溢出错误。此外,decimal的计算速度要稍微慢一些。
除非超过范围,否则decimal数字表示的十进制数都是完全准确的。与此相反,用浮点数来表示十进制数,则可能造成舍入错误。decimal类型和C#的浮点类型之所以存在这个区别,是因为decimal类型的指数是十进制数,而浮点类型的指数是二进制的。
默认情况下,如果输入一个带小数点的字面值,编译器会自动把它解释成double类型。相反,一个整数值(没有小数点)通常默认为int,前提是该值不是太大,以至于无法用int来存储。如果值太大,编译器会把它解释成long。此外,C#编译器允许向一个非int的数值类型赋值,前提是字面值对于目标数据类型来说是合法的。例如,short s = 42和byte b = 77都是允许的。下面我们来看一个范例:
1 namespace Microsoft.Example
2 {
3 public class TestDoubleAndDecimal
4 {
5 static void Main(string[] args)
6 {
7 double doubleValue = 1.618033988749895; //指定一个double字面量
8 decimal decimalValue = 1.618033988749895m; //指定一个decimal字面量
9 Console.WriteLine("double类型的变量值为" + doubleValue); //输出doubleValue
10 Console.WriteLine("decimal类型的变量值为" + decimalValue); //输出结decimalValue
11 }
12 }
13 }
上述代码中,我们在第7行定义了一个double类型的数据doubleValue,它的值是1.618033988749895,输出结果是1.61803398874989。前面讲过,C#有许多不同的数值类型。一个字面值被直接放到C#代码中。由于带小数点的数默认为double数据类型,输出是1.61803398874989(最后一个数字5丢失了),这符合我们预期的double值的精度。
第8行,我们定义了一个decimal类型的数据,要查看具有完整精度的数字,必须将字面值显式地声明为decimal类型,这是通过追加一个m(或者M)来实现的。输出结果是1.618033988749895。
现在,代码的输出与预期的结果相同:1.618033988749895。注意,d表示double,之所以用m表示decimal,是因为这种数据类型经常用在货币(monetary)计算中。还可以使用f和d作为后缀,将一个字面量显式地声明为float或者double。
最后的输出结果是:
double类型的变量值为1.61803398874989
decimal类型的变量值为1.618033988749895.
4、 布尔类型
另一个C#基本类型是布尔(Boolean)类型bool、也可以称为条件类型。在条件语句和表达式中,它用于表示真或假。允许的值包括关键字true和false。bool的BCL名称是System.Boolean。一个布尔类型的字面值使用关键字true和false。例如,为了在不区分大小写的前提下比较两个字符串,可以调用 string.Compare()方法,并传递bool字面量true。
例如:以不区分大小写的方式比较两个字符串
string option = “/help”;
int comparison = string.Compare(option, "/Help", true);
在这个例子中,我们以不区分大小写的方式比较变量option的内容和字面量/Help,并将结果赋给comparison。虽然从理论上说,一个比特就足以容纳一个布尔类型的值,但bool数据类型的实际大小是一个字节。下面我们来看一个范例:
1 namespace Microsoft.Example
2 {
3 public class TestBool
4 {
5 static void Main(string[] args)
6 {
7 bool boolFlag = false; //定义一个bool类型的数据
8 Console.WriteLine("bool类型的变量值为" + boolFlag.ToString()); //输出结果
9 }
10 }
11 }
上述代码中,第7行我们定义了一个bool类型的数据,并赋予false值。bool类型只有两个值,一个是true值,一个是false值。然后,我们在第8行代码中,把结果输出来。
最后的输出结果是:
bool类型的变量值为False
5、 字符类型
字符类型char用于表示16位字符,其取值范围对应于Unicode字符集。从技术上说,一个char的大小等同于一个16位无符号整数(ushort)的大小,后者的取值范围是0~65 535。然而,char是C#中的一个独一无二的类型,不应该像这样来对待。char的BCL名称是System.Char。
Unicode是用于对人类大多数语言中的字符进行表示的一个国际性标准。它便于计算机系统构建本地化的应用程序,为不同的语言文化显示具有本地特色的字符。令人遗憾的是,并不是所有Unicode字符都可以用一个16位char来表示。刚开始提出Unicode的概念的时候,它的设计者以为16位已经足够。但随着支持的语言越来越多,才发现当初的假定是错误的。结果是,一些Unicode字符要由一对称为“代理项”的char构成,总共32位。
为了输入一个字符类型的字面量,需要将字符放到一对单引号中,比如'A'。所有键盘字符都可以像这样输入,包括字母、数字以及特殊符号。有的字符不能直接插入源代码,需要进行特殊处理。这些字符有一个反斜杠(/)前缀,并跟随一个特殊字符代码。我们将反斜杠和特殊字符代码统称为转义序列(escape sequence)。例如,'/n'代表换行符,而'/t'代表制表符。由于反斜杠标志着一个转义序列的开始,所以不能再用来直接表示一个反斜杠字符,而要使用'//'来表示一个反斜杠字符。
下面我们来看一个范例:
1 namespace Microsoft.Example
2 {
3 public class TestChar
4 {
5 static void Main(string[] args)
6 {
7 char charValue = '/''; //定义了一个char类型的变量
8 System.Console.WriteLine("输出结果是:" + charValue); //输出结果
9 }
10 }
11 }
上述代码中,第7行我们定义了一个char类型的变量charValue,然后对它进行赋值’/’’。字符类型是使用单引号’来容纳字符的。然后我们使用WriteLine向控制台输出结果。
输出结果是:’
6、 字符串类型
C#的基本字符串类型是string,它的BCL名称是System.String。对于已经熟悉了其他语言的开发者,string的一些特点或许是他们意想不到的。比如字符串逐字前缀字符@,以及string属于不可变类型的事实。
String可以将字面量字符串输入代码,具体做法是将文本放入双引号(")内,就像HelloWorld程序中那样。
在C#中,可以在一个字符串前面使用@符号,指明转义序列不被处理。这样生成结果是一个逐字字符串字面量(verbatim string literal),它不仅将反斜杠当作普通字符来处理,而且还会逐字解释所有空白字符。例如:使用逐字字符串字面量来显示一个三角形
1 using System;
2 class TestString
3 {
4 static void Main()
5 {
6 System.Console.Write( @ "begin //注意,这里使用了@符号
7 /n //注意,这里使用了换行符“/n”,但并没有转义
8 end");
9 }
10 }
上述代码中,第6行我们使用了@符号来指明忽略转义序列,按字面上显示来逐字输出。在实际项目中,这个功能很好使,我们经常会用它来处理路径字符串,这样既简单,又清晰。第7行代码中我们先进行了回车换行,然后才输入”/n”的。这里需要特别注意,输出结果中的回车换行并不是“/n”产生了效果,而是我们在编写代码的时候进行了回车换行,所有使用了@符号后,也会进行回车换行,“/n”是完全被忽略的。
最后的输出结果是:
begin
/n
End
1.2.2值类型与引用类型
首先,变量是存储信息的基本单元,而对于计算机内部来说,变量就相当于一块内存空间。C#语言中的变量可以划分为值类型和引用类型两种:
值类型:基本数据类型、结构类型、枚举类型等
引用类型:类、数组、接口等。
(一)值类型和引用类型内存分配
值类型是在栈中操作,而引用类型则在堆中分配存储单元。栈在编译的时候就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存的大小。因此,值类型总是在内存中占用一个预定义的字节数(比如,int占用4个字节,即32位)。当声明一个值类型变量时,运行时会在堆栈中分配内存空间,并存储这个变量所包含的值。.NET会自动维护一个栈指针,它包含栈中下一个可用内存空间的地址。栈是先入后出的,栈中最上面的变量总是比下面的变量先离开作用域。当一个变量离开作用域时,栈指针向下移动被释放变量所占用的字节数,仍指向下一个可用地址。注意,值类型的变量在使用时必须初始化。
而引用类型的变量则在堆中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。.NET也自动维护一个堆指针,它包含堆中下一个可用内存空间的地址,但堆不是先入后出的,而是在对象不使用的时候才释放内存,.NET将定期执行垃圾收集。垃圾收集器递归地检查应用程序中所有的对象引用,当发现引用不再有效的对象使用的内存无法从程序中访问时,该内存就可以回收(除了fixed关键字固定在内存中的对象外)。
但值类型在栈上分配内存,而引用类型在托管堆上分配内存,却只是一种笼统的说法。更详细准确地描述是:
1、对于值类型的实例,如果做为方法中的局部变量,则被创建在线程栈上;如果该实例做为类型的成员,则作为类型成员的一部分,连同其他类型字段存放在托管堆上,
2、引用类型的实例创建在托管堆上,如果其字节小于85000byte,则直接创建在托管堆上,否则创建在LOH大对象堆上。
例如:
public class Test
{
private int i; //作为Test实例的一部分,与Test的实例一起被创建在托管堆上
public Test()
{
int j = 0; //作为局部实量,j的实例被创建在执行这段代码的线程栈上
}
}
(二)嵌套结构的内存分配
所谓嵌套结构,就是引用类型中嵌套有值类型,或值类型中嵌套有引用类型。
引用类型嵌套值类型是最常见的,上面的例子就是典型的例子,此时值类型是内联在引用类型中
值类型嵌套引用类型时,该引用类型作为值类型成员的变量,将在堆栈上保留该引用类型的引用,但引用类型还是要在堆中分配内存的。
(三)关于数组内存的分配
考虑当数组成员是值类型和引用类型时的情形:
成员是值类型:比如int[] arr = new int[5]。arr将保存一个指向托管堆中4*5byte(int占用4字节)的地址的引用,同时将所有元素赋值为0;
引用类型:myClass[] arr = new myClass[5]。arr在线程的堆栈中创建一个指向托管堆的引用。所有元素被置为null。
(四)值类型和引用类型在传递参数时的影响
由于值类型直接将它们的数据存放在栈中,当一个值类型的参数传递给一个方法时,该值的一个新的拷贝被创建并被传递,对参数所做的任何修改都不会导致传递给方法的变量被修改。而引用类型它只是包含引用,不包含实际的值,所以当方法体内参数所做的任何修改都将影响传递给方法调用的引用类型的变量。下面我们来看一个示例:
1 using System;
2 namespace Microsoft.Example
3 {
4 public class TestPassParam
5 {
6 static void Main()
7 {
8 int i = 0; //定义一个值类型(整数)的变量
9 int[] intArr = { 0 }; //定义一个引用类型(数组)的变量
10 SetValues(i, intArr); //进行参数传递
11 Console.WriteLine("i={0},intArr[0]={1}", i, intArr[0]); //输出结果
12 }
13 public static void SetValues(int i, int[] intArr)
14 {
15 i = 10; //改变值类型的值
16 intArr[0] = 10; //改变引用类型的值
17 }
18 }
19 }
上述代码中,第8行我们定义了一个值类型的变量i,它是整数类型的数据。第9行我们定义了一个引用类型的变量intArr,它是一个数组,数组的相关知识我们会在后面详细讲到,这里我们只需要知道它是一个引用类型就可以了。然后我们调用SetValues方法把值类型和引用类型的参数传递过去进行改变。
最后,我们在第11行输出结果发现,值类型的i在SetValues方法里面对其值的改变是无效的,依然为0,但是引用类型intArr在SetValues方法里面对其值的改变却依然保留下来了,值为10。
最后的输出结果是:
i=0,intArr[0]=10
(五)装箱和拆箱
装箱是将一个值类型转换为一个对象类型(object),而拆箱则是将一个对象类型显式转换为一个值类型。对于装箱而言,它是将被装箱的值类型复制一个副本来转换,而对于拆箱而言,需要注意类型的兼容性,比如,不能将一个值为“a”的object类型转换为int的类型。
下面我们来看一个示例:
1 using System;
2 namespace Microsoft.Example
3 {
4 public class TestBox
5 {
6 static void Main()
7 {
8 int i = 10; //定义一个整数变量
9 object o = i; //进行装箱操作
10 if (o is int) //判断是否装箱
11 {
12 Console.WriteLine("i已经被装箱");
13 }
14 int j = (int)o; //进行拆箱操作
15 Console.WriteLine("j已经被拆箱");
16 Console.WriteLine("i的值为" + i);
17 Console.WriteLine("j的值为" + j);
18 }
19 }
20 }
上述代码中,第8行中我们定义了一个整形变量i,然后我们在第9行中进行了装箱操作,如果装箱成功,那么o的引用就是int类型。我们可以通过o.GetType()可以得知o是int类型。第14行,我们通过强制类型转换,对o变量进行拆箱,得到一个整数类型的数据,我们把这个数据赋值给j整型变量进行保存。
最后的输出结果是:
i已经被装箱
j已经被拆箱
i的值为10
j的值为10
(六)关于string
string是引用类型,但却与其他引用类型有着一点差别。可以从以下两个方面来看:
1、 String类继承自object类。而不是System.ValueType。
2、string本质上是一个char[],而Array是引用类型,同样是在托管的堆中分配内存。但String作为参数传递时,却有值类型的特点,当传一个string类型的变量进入方法内部进行处理后,当离开方法后此变量的值并不会改变。原因是每次修改string的值时,都会创建一个新的对象。
1.2.3 var类型
在C#3.0中增加了一个var关键字,这个关键字和JavaScript的var类似,我们都可以用var来声明任意类型的局部变量。但又有不同,在C#语言中它仅仅是一个关键字,不代表一种新的类型,它仅是负责告诉编译器,该变量需要根据初始化表达式来推断变量的类型,而且只能是局部变量。var 声明变量之后,变量类型就确定下来了,不会再变,这和JavaScript有本质区别。
var关键字可以这样声明任意类型的局部变量:
var name = "csharp";
var names = new string[] { “C#”, ”VB”, ”C++” };
等价于下面语句:
string name = "csharp";
string[] names = new string[] { “C#”, ”VB”, ”C++” };
注意,在声明时必须同时给var类型赋值,因为声明的具体类型依赖于赋值号右边的表达式类型。表达式的类型也不可以是空(null)类型,编译器无法根据null来推断出局部变量的类型。
下面向大家介绍两个与var关键字密切相关的概念:隐式类型的局部变量、匿名类型。
1、隐式类型的局部变量
我们在上面介绍var关键字的概念的时候就知道,可以把值赋予局部变量var类型,它是通过右边的表达式的类型推断出来的隐式类型,而不是显式类型。隐式类型可以是内置类型、用户定义类型或 .NET Framework 类库中定义的类型。
下面的示例演示了使用var关键字声明隐式类型的局部变量:
var name = "csharp";
var names = new string[] { “C#”, ”VB”, ”C++” };
我们开始介绍概念的时候用的就是隐式类型的局部变量,这里我们用相同是示例,这样大家就不会感到混乱。需要了解的一点是,var关键字并不意味着“变体”,也不表示该变量是松散类型化变量或后期绑定变量。它只是表示由编译器确定和分配最适当的类型。
var 关键字可在下面的上下文中使用:
在 for 初始化语句中:for(var x = 1; x < 10; x++)
在 foreach 初始化语句中:foreach(var item in list){...}
在 using 语句中:using (var file = new StreamReader("C://myfile.txt")) {...}
有关for,foreach,using更多信息,请参见“基本结构”章节。在很多情况下,var是可选的,它只是提供了语法上的便利。
2、匿名类型
匿名类型提供了一种方便的方法,可以将一组只读属性封装到一个对象中,而无需首先显式定义这个对象的类型。类型名由编译器生成,并且不能在源代码级使用。这些属性的类型由编译器推断。下面的示例演示两个分别名为Amount和Message的属性来初始化的匿名类型。
var v = new { Amount = 100, Message = "Hello" };
这里的v就是一个匿名类型,注意v这里是一个变量名,并不是类的名字。前面还有一个var,这又是什么呢?这就是隐式类型局部变量。匿名类型通常用在查询表达式的select子句中,以便返回源序列中每个对象的属性子集。有关查询的更多信息,请参见LINQ查询表达式。
匿名类型是使用new运算符和一个或多个公共只读属性组成的类类型。不允许包含其他种类的类成员(如方法或事件)。匿名类型不能强制转换为除object以外的任何接口或类型。
最常见的方案是用其他类型的一些属性初始化匿名类型。在下面的示例中,假定一个名为Product的类包含Color和Price属性以及其他几个您不感兴趣的属性。Products是一个Product对象集合。匿名类型声明以new关键字开始。它初始化了一个只使用Product的两个属性的新类型。这将导致在查询中返回较少数量的数据。
var productQuery =
from prod in products
select new { prod.Color, prod.Price };
foreach (var v in productQuery)
{
Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
在将匿名类型分配给变量时,必须使用var构造初始化该变量。这是因为只有编译器能够访问匿名类型的基础名称,不能在源代码级使用这个名称。
最后,跟大家说一下,过多使用var可能使源代码的可读性变差。建议仅在必要时使用var,即仅在该变量将用于存储匿名类型或匿名类型集合时才使用它。我想var的出现其实完全是为了配合匿名类型而出现的。在linq中应用也比较多,也就是说对象是匿名类型,或者对象是难以预测的类型的时候。像这样的代码var age = 10;还是少用为好,这样类型安全,代码的可阅读性也高。