软件开发大讲堂·从入门到精通-第一辑(套装共5册)
上QQ阅读APP看书,第一时间看更新

第3章 变量与常量

视频讲解:2小时27分钟


应用程序的开发离不开变量与常量的应用。变量本身被用来存储特定类型的数据,而常量则存储不变的数据值。本章详细地介绍变量的类型和基本操作,同时对常量也做了详细的讲解,讲解过程中为了便于读者理解结合了大量的举例。

通过阅读本章,您可以:

 了解变量的基本概念

 掌握变量的声明及赋值

 熟悉变量的作用域

 掌握值类型的概念及用法

 掌握引用类型的概念及用法

 熟悉枚举类型的概念及用法

 掌握常用的类型转换方式

 了解常量的概念及用法

3.1 变量的基本概念

视频讲解

变量本身被用来存储特定类型的数据,可以根据需要随时改变变量中所存储的数据值。变量具有名称、类型和值。变量名是变量在程序源代码中的标识。变量类型确定它所代表的内存的大小和类型,变量值是指它所代表的内存块中的数据。在程序的执行过程中,变量的值可以发生变化。使用变量之前必须先声明变量,即指定变量的类型和名称。

3.2 变量的声明及赋值

视频讲解

变量在使用之前,必须进行声明并赋值,本节将对变量的声明及赋值,以及变量的作用域进行详细讲解。

3.2.1 声明变量

变量的使用是程序设计中一个十分重要的环节。为什么要定义变量呢?简单地说,就是要告诉编译器(compiler)这个变量是属于哪一种数据类型,这样编译器才知道需要配置多少空间给它,以及它能存放什么样的数据。在程序运行过程中,空间内的值是变化的,这个内存空间就称为变量。声明变量就是指定变量的名称和类型,变量的声明非常重要,未经声明的变量本身并不合法,也因此没有办法在程序当中使用。在C#中,声明一个变量是由一个类型和跟在后面的一个或多个变量名组成的,多个变量之间用逗号分开,声明变量以分号结束。

【例3.1】声明一个整型变量num,然后再同时声明3个字符型变量str1、str2和str3,代码如下。

在第1行代码中,声明了一个名称为num的整型变量。在第二行代码中,声明了3个字符串型的变量,分别为str1、str2和str3。

声明变量时,还可以初始化变量,即在每个变量名后面加上给变量赋初始值的指令。

【例3.2】声明一个整型变量a,并且赋值为927。然后,再同时声明3个字符型变量,并初始化,代码如下。

在声明变量时,要注意变量名的命名规则。C#的变量名是一种标识符,应该符合标识符的命名规则。变量名是区分大小写的,下面列出变量的命名规则。

☑ 变量名只能由数字、字母和下画线组成。

☑ 变量名的第一个符号只能是字母和下画线,不能是数字。

☑ 不能使用关键字作为变量名。

☑ 一旦在一个语句块中定义了一个变量名,那么在变量的作用域内都不能再定义同名的变量。

说明

在C#语言中允许使用汉字或其他语言文字作为变量名,如“int年龄=21”,在程序运行时并不出现什么错误,但建议读者尽量不要使用这些语言文字作为变量名。

3.2.2 变量的赋值

在C#中,使用赋值运算符“=”(等号)来给变量赋值,将等号右边的值赋给左边的变量。

【例3.3】声明一个变量,并给变量赋值,代码如下。

在3.2.1节介绍的初始化变量,其实是一种特殊的赋值方式,它在声明变量的同时给变量赋值。在给变量赋值时,等号右边也可以是一个已经被赋值的变量。

【例3.4】首先声明两个变量sum和num,然后将变量sum赋值为927,最后将变量sum赋值给变量num,代码如下。

注意

在对多个同类型的变量赋同一个值时,为了节省代码的行数,可以同时对多个变量进行初始化:int a,b,c,d,e;a=b=c=d=e=0;。

3.2.3 变量的作用域

由于变量被定义出来后只是暂存在内存中,等到程序执行到某一个点后,该变量会被释放掉,也就是说变量有它的生命周期。因此,变量的作用域是指程序代码能够访问该变量的区域,若超出该区域,则在编译时会出现错误。在程序中,一般会根据变量的“有效范围”将变量分为“成员变量”和“局部变量”。

1. 成员变量

在类体中定义的变量被称为成员变量,成员变量在整个类中都有效。类的成员变量又可分为两种,即静态变量和实例变量。

【例3.5】声明静态变量和实例变量,实例代码如下。

其中,x为实例变量,y为静态变量(也称类变量)。如果在成员变量的类型前面加上关键字static,这样的成员变量称为静态变量。静态变量的有效范围可以跨类,甚至可达到整个应用程序之内。对于静态变量,除了能在定义它的类内存取,还能直接以“类名.静态变量”的方式在其他类内使用。

2. 局部变量

在类的方法体中定义的变量(方法内部定义,“{”与“}”之间的代码中声明的变量)称为局部变量。局部变量只在当前代码块中有效。

在类的方法中声明的变量,包括方法的参数,都属于局部变量。局部变量只有在当前定义的方法内有效,不能用于类的其他方法中。局部变量的生命周期取决于方法,当方法被调用时,C#编译器为方法中的局部变量分配内存空间,当该方法的调用结束后,则会释放方法中局部变量占用的内存空间,局部变量也将会销毁。

变量的有效范围如图3.1所示。

图3.1 变量的有效范围

【例3.6】创建一个控制台应用程序,使用for循环将从0~20的数字显示出来。然后在for语句中声明变量i,此时i就是局部变量,其作用域只限于for循环体内,代码如下。(实例位置:资源包\TM\sl\3\1)

程序运行结果为“0~20的数字”。

互动练习:某著名的在线通信软件公司出过这么一道面试题,说“经理有3个女儿,她们的年龄和是13岁,年龄的乘积等于经理的年龄。有位员工知道经理年龄,但是不能确定他3个女儿都是多大,这时,经理跟他说,我只有一个女儿超过5岁,于是那位员工就知道了经理3个女儿的年龄,那么经理的3个女儿都分别是多大?”。使用C#推算经理3个女儿的年龄。

3.3 数据类型

视频讲解

C#中的变量类型根据其定义可以分为两种:一种是值类型;另一种是引用类型。这两种类型的差异在于数据的存储方式,值类型的变量本身直接存储数据。而引用类型则存储实际数据的引用,程序通过此引用找到真正的数据,在以下内容中将会对这些类型进行详细讲解。

3.3.1 值类型

值类型变量直接存储其数据值,主要包含整数类型、浮点类型以及布尔类型等。值类型变量在栈中进行分配,因此效率很高,使用值类型主要目的是为了提高性能。值类型具有如下特性。

☑ 值类型变量都存储在栈中。

☑ 访问值类型变量时,一般都是直接访问其实例。

☑ 每个值类型变量都有自己的数据副本,因此对一个值类型变量的操作不会影响其他变量。

☑ 复制值类型变量时,复制的是变量的值,而不是变量的地址。

☑ 值类型变量不能为null,必须具有一个确定的值。

值类型是从System.ValueType类继承而来的类型,下面详细介绍值类型中包含的几种数据类型。

1. 整数类型

整数类型用来存储整数数值,即没有小数部分的数值。可以是正数,也可以是负数。整型数据在C#程序中有3种表示形式,分别为十进制、八进制和十六进制。

☑ 十进制:十进制的表现形式大家都很熟悉,如120、0、-127。

注意

不能以0作为十进制数的开头(0除外)。

☑ 八进制:如0123(转换成十进制数为83)、-0123(转换成十进制数为-83)。

注意

八进制必须以0开头。

☑ 十六进制:如0x25(转换成十进制数为37)、0Xb01e(转换成十进制数为45086)。

注意

十六进制必须以0X或0x开头。

在C#中内置的整数类型如表3.1所示。

表3.1 C#内置的整数类型

byte类型以及short类型是范围比较小的整数,如果正整数的范围没有超过65535,声明为ushort类型即可,当然更小的数值直接以byte类型作处理即可。只是使用这种类型时必须特别注意数值的大小,否则可能会导致运算溢出的错误。

【例3.7】创建一个控制台应用程序,在其中声明一个int类型的变量ls并初始化为927、一个byte类型的变量shj并初始化为255,最后输出,代码如下。(实例位置:资源包\TM\sl\3\2)

程序运行结果为:

此时,如果将byte类型的变量shj赋值为266,重新编译程序,就会出现错误提示。主要原因是byte类型的变量是8位无符号整数,它的范围为0~255,266已经超出了byte类型的范围,所以编译程序会出现错误提示。

注意

在定义局部变量时,要对其进行初始化。

2. 浮点类型

浮点类型变量主要用于处理含有小数的数值数据,浮点类型主要包含float和double两种数值类型。表3.2列出了这两种数值类型的描述信息。

表3.2 浮点类型及描述

如果不做任何设置,包含小数点的数值都被认为是double类型,例如9.27,没有特别指定的情况下,这个数值是double类型。如果要将数值以float类型来处理,就应该通过强制使用f或F将其指定为float类型。

【例3.8】下面的代码就是将数值强制指定为float类型。

如果要将数值强制指定为double类型,则应该使用d或D进行设置,但加不加d或D没有硬性规定,可以加也可以不加。

【例3.9】下面的代码就是将数值强制指定为double类型。

注意

如果需要使用float类型变量时,必须在数值的后面跟随f或F,否则编译器会直接将其作为double类型处理。也可以在double类型的值前面加上(float),对其进行强制转换。

3. 布尔类型

布尔类型主要用来表示true和false值,一个布尔类型的变量,其值只能是true或者false,不能将其他的值指定给布尔类型变量,布尔类型变量不能与其他类型之间进行转换。布尔类型通常被用在流程控制中作为判断条件。

【例3.10】将927赋值给布尔类型变量x,代码如下。

这样赋值显然是错误的,编译器会返回错误提示“常量值927无法转换为bool”。布尔类型变量大多数被应用到流程控制语句当中,例如,循环语句或者if语句等。

说明

在定义全局变量时,如果没有特定的要求不用对其进行初始化,整数类型和浮点类型的默认初始化为0,布尔类型的初始化为false。

闯关训练:开发财务系统时,通过值类型创建存储流动资金金额的临时性变量。

3.3.2 引用类型

引用类型是构建C#应用程序的主要对象类型数据。在应用程序执行的过程中,预先定义的对象类型以new创建对象实例,并且存储在堆中。堆是一种由系统弹性配置的内存空间,没有特定大小及存活时间,因此可以被弹性地运用于对象的访问。引用类型就类似于生活中的代理商,代理商没有自己的产品,而是代理厂家的产品,使其就好像是自己的产品一样。

引用类型具有如下特征。

☑ 必须在托管堆中为引用类型变量分配内存。

☑ 使用new关键字来创建引用类型变量。

☑ 在托管堆中分配的每个对象都有与之相关联的附加成员,这些成员必须被初始化。

☑ 引用类型变量是由垃圾回收机制来管理的。

☑ 多个引用类型变量可以引用同一对象,这种情形下,对一个变量的操作会影响另一个变量所引用的同一对象。

☑ 引用类型被赋值前的值都是null。

所有被称为“类”的都是引用类型,主要包括类、接口、数组和委托。下面通过一个实例来演示如何使用引用类型。

【例3.11】创建一个控制台应用程序,在其中创建一个类C,在此类中建立一个字段Value,并初始化为0,然后在程序的其他位置通过new关键字创建对此类的引用类型变量,最后输出,代码如下。(实例位置:资源包\TM\sl\3\3)

程序运行结果如下。

3.3.3 值类型与引用类型的区别

从概念上看,值类型直接存储其值,而引用类型存储对其值的引用。这两种类型存储在内存的不同地方。在C#中,必须在设计类型时就决定类型实例的行为。如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常。

从内存空间上看,值类型是在栈中操作,而引用类型则在堆中分配存储单元。栈在编译时就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存的大小。因此,值类型总是在内存中占用一个预定义的字节数。而引用类型的变量则在堆中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。

也就是说值类型相当于现金,要用就直接用,而引类型相当于存折,要用得先去银行取。

说明

C#的所有值类型均隐式派生自System.ValueType,而System.ValueType直接派生于System.Object。即System.ValueType本身是一个类类型,而不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。

下面以一段代码来详细讲解一下值类型与引用类型的区别,代码如下。

运行结果如图3.2所示。

图3.2 值类型与引用类型

从图3.2中可以看出,当改变了Stamp_1.Age的值时,age没跟着变,而在改变了Stamp_2.Name的值后,guru.Name却跟着变了,这就是值类型和引用类型的区别。在声明age值类型变量时,将Stamp_1.Age的值赋给它,这时,编译器在栈上分配了一块空间,然后把Stamp_1.Age的值填进去,二者没有任何关联,就像在计算机中复制文件一样,只是把Stamp_1.Age的值复制给age了。而引用类型则不同,在声明guru时把Stamp_2赋给它。前面说过,引用类型包含的只是堆上数据区域地址的引用,其实就是把Stamp_2的引用也赋给guru,因此它们指向了同一块内存区域。既然是指向同一块区域,不管修改谁,另一个的值都会跟着改变。就像信用卡跟亲情卡一样,用亲情卡取了钱,与之关联的信用卡账上也会跟着发生变化。

3.3.4 枚举类型

枚举类型是一种独特的值类型,它用于声明一组具有相同性质的常量,编写与日期相关的应用程序时,经常需要使用年、月、日、星期等日期数据,可以将这些数据组织成多个不同名称的枚举类型。使用枚举可以增加程序的可读性和可维护性。同时,枚举类型可以避免类型错误。

说明

在定义枚举类型时,如果不对其进行赋值,默认情况下,第一个枚举数的值为0,后面每个枚举数的值依次递增1。

在C#中使用关键字enum类声明枚举,其形式如下。

其中,大括号“{}”中的内容为枚举值列表,每个枚举值均对应一个枚举值名称,value1~valueN为整数数据类型,list1~listN则为枚举值的标识名称。下面通过一个实例来演示如何使用枚举类型。

【例3.12】创建一个控制台应用程序,通过使用枚举来判断当前系统日期是星期几,代码如下。(实例位置:资源包\TM\sl\3\4)

程序运行的结果为“今天是星期三”。

查看程序运行的结果,因为当前日期是2019年5月22日星期三,所以输出的结果显示当天是星期三。程序首先通过enum关键字建立一个枚举,枚举值名称分别代表一周的七天,如果枚举值名称是Sun,说明其代表的是一周中的星期日,其枚举值为0,以此类推。然后,声明一个int类型的变量k,用于获取当前表示的日期是星期几。最后,调用switch语句,输出当天是星期几。

3.3.5 类型转换

类型转换就是将一种类型转换成另一种类型,转换可以是隐式转换,也可以是显式转换,本节将详细介绍这两种转换方式,并讲解有关装箱和拆箱的内容。

说明

要理解类型转换,读者可以这么想象,大脑前面是一片内存,源和目标分别是两个大小不同的内存块(由变量及数据的类型来决定),将源数据赋值给目标内存的过程,就是用目标内存块去套取源内存中的数据,能套多少算多少。

1. 隐式转换

所谓隐式转换就是不需要声明就能进行的转换。进行隐式转换时,编译器不需要进行检查就能自动进行转换。表3.3列出了可以进行隐式转换的数据类型。

表3.3 隐式类型转换表

从int、uint、long或ulong到float,以及从long或ulong到double的转换可能导致精度损失,但是不会影响其数量级。其他的隐式转换不会丢失任何信息。

说明

当一种类型的值转换为大小相等或更大的另一类型时,则发生扩大转换;当一种类型的值转换为较小的另一种类型时,则发生收缩转换。

【例3.13】将int类型的值隐式转换成long类型,代码如下。

2. 显式转换

显式转换也可以称为强制转换,需要在代码中明确地声明要转换的类型。如果要把高精度的变量的值赋给低精度的变量,就需要使用显式转换。表3.4列出了需要进行显式转换的数据类型。

表3.4 显式类型转换表

由于显式转换包括所有隐式转换和显式转换,因此总是可以使用强制转换表达式从任何数值类型转换为任何其他的数值类型。

【例3.14】创建一个控制台应用程序,将double类型的x进行显式类型转换,代码如下。(实例位置:资源包\TM\sl\3\5)

程序运行结果为19810927。

也可以通过Convert关键字进行显式类型转换,上述例子还可以通过下面的代码实现。

【例3.15】创建一个控制台应用程序,通过Convert关键字进行显式类型转换,代码如下。

3. 装箱和拆箱

将值类型转换为引用类型的过程叫作装箱,相反,将引用类型转换为值类型的过程叫作拆箱,下面将通过例子详细介绍装箱与拆箱的过程。

(1)装箱。装箱允许将值类型隐式转换成引用类型,下面通过一个实例演示如何进行装箱操作。

【例3.16】创建一个控制台应用程序,声明一个整型变量i并赋值为2019,然后将其复制到装箱对象obj中,最后再改变变量i的值,代码如下。(实例位置:资源包\TM\sl\3\6)

程序的运行结果为:

从程序运行结果可以看出,值类型变量的值复制到装箱得到的对象中,装箱后改变值类型变量的值,并不会影响装箱对象的值。

(2)拆箱。拆箱允许将引用类型显式转换为值类型,下面通过一个示例演示拆箱的过程。

【例3.17】创建一个控制台应用程序,声明一个整型变量i并赋值为112,然后将其复制到装箱对象obj中,最后,进行拆箱操作将装箱对象obj赋值给整型变量j,代码如下。(实例位置:资源包\TM\sl\3\7)

程序运行结果为:

查看程序运行结果,不难看出,拆箱后得到的值类型数据的值与装箱对象相等。需要读者注意的是,在执行拆箱操作时要符合类型一致的原则,否则会出现异常。

说明

装箱是将一个值类型转换为一个对象类型(object),而拆箱则是将一个对象类型显式转换为一个值类型。对于装箱而言,它是将被装箱的值类型复制一个副本来转换;而对于拆箱而言,需要注意类型的兼容性,例如,不能将一个值为“string”的object类型转换为int类型。

互动练习:尝试使用C#制作一个可以在窗体上画桃花的游戏,具体要求为:在窗体的左侧显示桃花的3种状态:花骨朵、花蕾、开花,然后用鼠标单击某一种状态,即可在右侧显示的桃枝上绘制桃花的相应状态。

3.4 常量

视频讲解

常量就是其值固定不变的量,而且常量的值在编译时就已经确定了。常量的类型只能为下列类型之一:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string等。C#中使用关键字const定义常量,并且在创建常量时必须设置它的初始值。常量就相当于每个公民的身份证号,一旦设置就不允许修改。

【例3.18】声明一个正确的常量,同时再声明一个错误的常量,以便读者对比参考,代码如下。

与变量不同,常量在整个程序中只能被赋值一次。在为所有的对象共享值时,常量是非常有用的。下面通过一个例子演示常量与变量的差异。

【例3.19】创建一个控制台应用程序,首先声明一个变量MyInt并且赋值为927,然后再声明一个常量MyWInt并赋值为112,最后将变量MyInt赋值为1039,关键代码如下。(实例位置:资源包\TM\sl\3\8)

执行程序,输出的结果为:

变量MyInt的初始化值为927,而常量MyWInt的值等于112,由于变量的值是可以修改的,所以变量MyInt可以重新被赋值为1039后输出。通过查看输出结果,可以看到变量MyInt的值已经被修改,如果尝试修改常量MyWInt的值,编译器会出现错误信息,阻止进行这样的操作。

3.5 小结

本章重点讲解了变量和常量,通过大量的举例说明,使读者更好地理解所学知识的用法。在阅读本章时,要重点掌握值类型、引用类型和枚举类型的概念及用法,并且要了解如何进行类型转换。了解变量的基本知识后,要掌握如何对变量进行操作,了解变量的作用域以及如何为变量赋值。本章最后对常量进行了详细的叙述,包括常量的概念及常量的基本类型。

3.6 实践与练习

(1)尝试开发一个程序,在该程序中建立一个静态方法,在静态方法中声明一个局部变量,并对其赋值,然后输出。(答案位置:资源包\TM\sl\3\9)

(2)尝试开发一个程序,要求声明一个常量,然后试着更改这个常量的值,看会引发什么错误。(答案位置:资源包\TM\sl\3\10)

3.7 动手纠错

(1)运行“资源包\TM\排错练习\03\01”文件夹下的程序,出现“常量值300无法转换为byte”的错误提示,请根据注释改正程序。

(2)运行“资源包\TM\排错练习\03\02”文件夹下的程序,出现“意外的字符‘;’”的错误提示,请根据注释改正程序。

(3)运行“资源包\TM\排错练习\03\03”文件夹下的程序,出现“不能隐式地将double类型转换为float类型;请使用F后缀创建此类型”的错误提示,请根据注释改正程序。

(4)运行“资源包\TM\排错练习\03\04”文件夹下的程序,出现“无法将类型double隐式转换为int。存在一个显式转换(是否缺少强制转换?)”的错误提示,请根据注释改正程序。

(5)运行“资源包\TM\排错练习\03\05”文件夹下的程序,出现“未处理InvalidCastException指定的转换无效”的错误提示,请根据注释改正程序。

(6)运行“资源包\TM\排错练习\03\06”文件夹下的程序,出现“赋值号左边必须是变量、属性或索引器”的错误提示,请根据注释改正程序。

(7)运行“资源包\TM\排错练习\03\07”文件夹下的程序,出现“无法使用实例引用访问成员Test07.Test.PI;请改用类型名称对其加以限定”的错误提示,请根据注释改正程序。