
2.2 基本语法
语法是一种程序语言最基本的定义规范,只有按照语法给出的规则才能编写出正确的程序。C#程序基本语法包括:数据类型的种类,变量与常量的声明和使用以及语句的基本组成表达式和运算符。
2.2.1 数据类型
C#的数据类型包括值类型、引用类型和指针类型。指针类型是不安全类型,一般不推荐使用。
1.值类型
值类型包括简单类型(如字符型、浮点型和整数类等)、枚举类型和结构类型。所有的值类型都隐含地声明了一个公共的无参数的构造函数,这个构造函数返回一个初始为零的值类型的实例。例如,对于字符型,默认值是“\x0000”;对于float,默认值是0.0F。
(1) 简单类型:它是C#预先定义的结构类型,简单类型用关键字定义,这些关键字仅仅是在System命名空间里预定义的结构类型的化名,比如关键字int对应System.Int32。简单类型包括如表2-1所示数据类型。
表2-1 简单数据类型

(2) 集合类型:它是C#中一种轻量级的值类型,用来表达一组特定的值的集合行为,以enum关键字进行声明。
(3) 结构类型:它是用来封装小型的相关变量组,把它们封装成一个实体来统一使用,以struct关键字进行声明。
2.引用类型
引用类型包括类类型、对象类型、字符串类型、接口类型、委托类型和数组类型等。引用类型与值类型的不同之处是值类型的变量值直接包含数据,而引用类型的变量把它们的引用存储在对象中。类类型、对象类型和数组类型在后面的章节有详细介绍。
(1) 字符串类型:直接从object中继承而来的密封类。String类型的值可以写成字符串文字的形式。例如:"123"、"hello world"是字符串类型。
(2) 接口类型:一个接口声明一个只有抽象成员的引用类型,接口仅仅存在方法标志,但没有执行代码,以关键字interface进行声明。
(3) 委托类型:委托引用一种静态的方法或对象实例,引用该对象的实例方法,与C/C++中的指针类似,以关键字delegate进行声明。
作者心得:
在C#中,所有数据类型都是基于基本对象Object来实现,因此它们之间在允许的范围内是可以相互转化的。这就是装箱和拆箱,在2.2.4节会进行详细介绍。
2.2.2 变量和常量
1.变量
所谓变量,就是在程序的运行过程中其值可以被改变的量,变量的类型可以是任何一种C#的数据类型。所有值类型的变量具有实际存在于内存中的值,也就是说当将一个值赋给变量时就是在执行对该值的拷贝。变量的定义格式为:
变量数据类型 变量名(标识符);
或者
变量数据类型 变量名(标识符) =变量值;
标识符就是变量名,变量的标志。
其中,第一个定义只是声明了一个变量,并没有对变量进行赋值,此时变量使用默认值。第二个声明定义变量的同时对变量进行了初始化,变量值应该和变量数据类型一致。例如:
int a=10; \\ 声明了一个整数类型的变量a,并对其赋值为10 double b,c; \\ 两个double类型的变量 int d=100,e=200; \\ 定义了两个整数类型的变量,并对变量进行了赋值 double f=a+b+c+d+e; \\ 把前面定义的变量相加,然后赋给一个double类型的变量
作者心得:
当几个不同数值类型的变量进行运算时,低精度的变量会自动转化为高精度的变量类型。
2.常量
所谓常量,就是在程序的运行过程中其值不能被改变的量。常量的类型也可以是任何一种C#的数据类型。常量的定义格式为:
const 常量数据类型 常量名(标识符)=常量值;
其中,const关键字表示声明一个常量,“常量名”就是标识符,用于唯一的标识该常量。常量名要有代表意义,不能过于简练或者复杂。
“常量值”的类型要和常量数据类型一致,如果定义的是字符串型,“常量值”就应该是字符串类型,否则会发生错误。例如:
const double PI=3.1415926; // 定义了一个double类型的常量 const string VERSION = "Visual Studio 2008"; //定义了一个字符串型的常量
作者心得:
常量一旦定义,用户在后面的代码中如果试图改变常量的值,编译器会发现这个错误导致代码无法编译通过。
2.2.3 表达式和运算符
1.表达式
表达式是可以运算的代码片段,表达式可以包括运算符、方法调用等,表达式是程序语句的基本组成部分,例如:
int num = 5; //定义一个整型变量num,并对其赋值 string str = “你好,世界!”; //定义一个字符串变量,并对其赋值
2.运算符
运算符是数据运算的术语和符号,它接受一个或多个称为操作数的表达式作为输入并返回值。C#中的运算符非常多,从操作数上划分运算符大致分为以下3类。
● 一元运算符:处理一个操作数,只有几个一元运算符。
● 二元运算符:处理两个操作数,大多数运算符都是二元运算符。
● 三元运算符:处理三个操作数,只有一个三元运算符。
从功能上划分,运算符主要分为:算术运算符,赋值运算符,关系运算符,条件运算符,位运算符和逻辑运算符。
例如:
i ++; //一元运算,变量i自动加1 num = 2 + 3; //二元运算,变量num等于2加3的和 result = a > b ? 100 : -10//三元运算,条件运算符,根据条件的真假来决定运算的正确性
表达式中的运算符按照运算符优先级的特定顺序计算,表2-2按优先顺序列出常用运算符的优先级别,在表2-2中,上面的比下面的具有较高的优先级别。
表2-2 常用运算符

例如:
int num = 4 + 5 * 2 + 6 / 2 //数学运算,先算乘除后算加减,结果是17
作者心得:
仅仅依靠优先级来安排数据的运算顺序是可靠的,大部分情况下考虑使用()来进行强制优先级,凡是用()括起来的比其他运算符都有高的优先级。
范例2.2
Program.cs
在该程序中定义两个双精度变量,通过控制台分别输入他们的值,然后分别对这两个变量做一系列数学运算。
代码路径:ShiLi2-2\Program.cs
1 double firstNumber,secondNumber; //定义两个双精度变量 2 Console.WriteLine("请输入一个数字:"); 3 //将用户输入的第一个数字转换成double类型,并存储在firstNumber中 4 firstNumber=Convert.ToDouble(Console.ReadLine()); 5 Console.WriteLine("请输入第二个数字"); 6 //将用户输入的第二个数字转换成double类型,并存储在secondNumber中 7 secondNumber=Convert.ToDouble(Console.ReadLine()); 8 Console.WriteLine("{0}+{1}={2}",firstNumber,secondNumber,firstNumber+ 9 secondNumber); //求和 10 Console.WriteLine("{0}-{1}={2}", firstNumber, secondNumber, firstNumber - 11 secondNumber); //求差 12 Console.WriteLine("{0}*{1}={2}", firstNumber, secondNumber, firstNumber * 13 secondNumber); //求积 14 Console.WriteLine("{0}/{1}={2}", firstNumber, secondNumber, firstNumber / 15 secondNumber); //求商 16 Console.WriteLine("{0}%{1}={2}", firstNumber, secondNumber, firstNumber % 17 secondNumber); //求余 18 Console.ReadKey();
运行以上代码输出结果是:
请输入一个数字:
10
请输入第二个数字
6
10+6=16
10-6=4
10*6=60
10/6=1.66666666666667
10%6=4
2.2.4 装箱和拆箱
装箱和取消装箱使值类型能够被视为对象。对值类型装箱将把该值类型打包到Object引用类型的一个实例中。这使得值类型可以存储于垃圾回收堆中。取消装箱将从对象中提取值类型,取消装箱又经常被称作“拆箱”。例如:
int i = 123; //定义一个值类型变量 object o=(object)i; // 装箱 o = 123; //对装箱后的对象操作 i=(int)o; //取消装箱
从装箱和取消装箱的定义可以看出,这两种行为主要用于进行值类型和引用类型之间的相互转化的情况。
作者心得:
相对于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象。此外,拆箱所需的强制转换也需要进行大量的计算。因此,读者在进行装箱和拆箱操作时应该考虑到该操作对性能的影响。
2.2.5 泛型
C#中的泛型类似C++的模板,它在一定程度上能够提高应用程序的效率。使用泛型可以定义类型安全的数据结构,而无需使用具体实际的数据类型。通过使用泛型,能够将数据类型参数化,以此完成代码重用的目标。
2.2.5.1 使用系统的泛型类
通常情况下,泛型常见于集合应用中。在System.Collections.Generic名称空间中,包含了一些基于泛型的容器类,例如System.Collections.Generic.Stack、System. Collections.Generic. Dictionary、System.Collections.Generic.List和System.Collections. Generic.Queue等,这些类库可以在集合中实现泛型。
创建泛型的格式:
1) 类名 <Type> 变量名 =new 类名 <Type>(); 2) List<String>l2=new ArrayList();
第1行是泛型创建的格式,使用泛型类必须指定实际的类型,并在<>角括号中指定实际的类型。第2行根据格式创建了String类型的泛型类的集合类型l2,这意味着l2支能存储String类型的数据。
泛型的的使用如下面的代码:
1) List<String>l=new ArrayList(); 2) l.add("小明"); 3) l.add("小王"); 4) String s=l.get(0)
第1行,使用泛型List<String> l=new ArrayList()创建ArrayList的对象l,然后第2、3行使用ArrayList类的add方法,传入二个String类型的参数:小明和小王。这里只能传泛型中定义的类型,否则报错。所以第四行使用ArrayList类的方法get取得元素时就不需要类型转换了,因为所有的元素类型只能是String类型。
C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类。我们把为所有类型参数提供参数的泛型类型称为封闭构造泛型类型,简称封闭类。不同的封闭类的本地代码是不一样的。按照这个原理,可以这样认为:泛型类的不同封闭类是不同的数据类型。
2.2.5.2 创建泛型
除了使用系统的泛型类之外,读者可以编写自己的泛型类。下面我们来介绍泛型类和普通类的区别。
1.静态构造函数
静态构造函数的规则:只能有一个,且不能有参数,它只能被.NET运行时自动调用,而不能人工调用。
泛型中的静态构造函数的原理和非泛型类是一样的,只需把泛型中的不同的封闭类理解为不同的类即可。以下两种情况可激发静态的构造函数:
● 特定的封闭类第一次被实例化。
● 特定封闭类中任一静态成员变量被调用。
2.静态成员变量
在C#1.0中,类的静态成员变量在不同的类实例间是共享的,并且它是通过类名访问的。C#2.0中由于引进了泛型,导致静态成员变量的机制出现了一些变化:静态成员变量在相同封闭类间共享,不同的封闭类间不共享。
这也非常容易理解,因为不同的封闭类虽然有相同的类名称,但由于分别传入了不同的数据类型,他们是完全不同的类,比如:
Stack<int> a = new Stack<int>(); Stack<int> b = new Stack<int>(); Stack<long> c = new Stack<long>();
类实例a和b是同一类型,它们之间共享静态成员变量,但类实例c却是和a、b完全不同的类型,所以不能和a、b共享静态成员变量。
3.数据类型的约束
在编写泛型类时,一般情况下,通用数据类型T是不能适应所有类型的。但如何才能限制调用者传入的数据类型呢?这就需要对传入的数据类型进行约束,约束的方式是指定T的祖先,即继承的接口或类。因为C#的单根继承性,所以约束可以有多个接口,但最多只能有一个类,并且类必须在接口之前。
由于通用类型T是从object继承来的,所以他在类Node的编写中只能调用object类的方法,这给程序的编写造成了困难。比如你的类设计只需要支持两种数据类型int和string,并且在类中需要对T类型的变量比较大小,但这些却无法实现,因为object是没有比较大小的方法的。为了解决这个问题,只需对T进行IComparable约束,这时在类Node里就可以对T的实例执行CompareTo方法了。这个问题可以扩展到其他用户自定义的数据类型。
如果在类Node里需要对T重新进行实例化该怎么办呢?因为类Node中不知道类T到底有哪些构造函数。为了解决这个问题,需要用到new约束,需要注意的是,new约束只能是无参数的,所以也要求相应的类Stack必须有一个无参构造函数,否则编译失败。
4.泛型的使用
上面我们了解了泛型的基本概念和创建泛型的方法,下面我们通过一个具体的例子来演示如何使用泛型。
范例2.3
ShiLi2-10\Program.cs
本实例使用泛型类List实现学生信息录入的功能,在控制台根据提示输入学生的学号、姓名和年龄,确认录入信息后,显示该学生的信息。
代码路径:ShiLi2-10\Program.cs
1. class Program 2. { 3. public class Student 4. { 5. private int id; 6. private String name; 7. private int age; 8. public Student(){} 9. public Student(int id, String name, int age) 10. { 11. this.id=id; 12. this.name=name; 13. this.age=age; 14. } 15. public void print(List<Student>stud) 16. { 17. foreach(Student s in stud) 18. { 19. Console.WriteLine("学生的学号是:"+s.id); 20. Console.WriteLine("学生的姓名是:"+s.name); 21. Console.WriteLine("学生的年龄是:"+s.age); 22. } 23. } 24. static void Main(string[]args) 25. { 26. List<Student>stud=new List<Student>(); 27. do 28. { 29. Console.WriteLine("请输入学生学号:"); 30. int id=Convert.ToInt32(Console.ReadLine()); 31. Console.WriteLine("请输入学生姓名:"); 32. string name=Console.ReadLine(); 33. Console.WriteLine("请输入学生年龄:"); 34. int age=Convert.ToInt32(Console.ReadLine()); 35. Console.WriteLine("请输入学生家庭住址:"); 36. Student s=new Student(id, name, age); 37. stud.Add(s); 38. Console.WriteLine("是否录入学生信息?Y/N"); 39. }while(Console.ReadLine().ToLower()=="y"); 40. Student studnet=new Student(); 41. studnet.print(stud); 42. } 43. } 44. }
程序说明:第3行自定义封装了学生类,第5行到第7行定义了3个私有字段表示学生的学号、姓名和年龄。第8行定义不带参数的构造函数,第9行到第13行在构造函数内对三个字段进行初始化。第10行,编写一种方法print通过foreach语句循环打印学生的信息。
第26行使用泛型初始化泛型集合对象stud,对象类型规定为我们定义的Student类型。第29行到第35行获得输入的数据。第37行调用stud对象的Add方法将学生信息添加到泛型集合中。第41行调用Student对象的print方法打印学生信息。
以上代码运行后会出现如图2-1所示的结果。

图2-1 运行程序后的结果
作者心得:
泛型的编程模式可以大大提高代码的开发效率,虽然其概念比较复杂且难以理解,但读者只要按照上面的示例代码编写自己通用的类型即可。泛型其实就是提供一种模板,而在实际应用中,只要按照这个模板编码即可。