ASP.NET 3.5程序设计与项目实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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 运行程序后的结果

作者心得:

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