一、C# 程序是一组类型声明
如果泛泛地描述 C 和 C++ 程序源代码的特征,可以说 C 程序是组函数和数据类型,C++ 程序是组函数和类,而 C# 程序是组类型声明。
- C# 程序或 DLL 的源代码是一组类型声明。
- 对于可执行程序,类型声明中必须有一个包含 Main 方法的类。
- 命名空间是是一种将相关的类型声明分组并命名的方法。因为程序是一组相关的类型声明,所以通常在你创建的命名空间内部声明程序类型。
例如,下面是一个由 3 个类型声明组成的程序。这 3 个类型被声明在一个名为 MyProgram
的新命名空间内部。
namespace MyProgram //创建新的命名空间
{
DeclarationOfTypeA //声明类型
Declaration0fTypeB //声明类型
class C //声明类型
{
static void Main()
{
...
}
}
}
二、类型是一种模板
既然 C# 程序就是一组类型声明,那么学习 C# 就是学习如何创建和使用类型。所以,我们首先要了解什么是类型。
可以把类型想象成一个用来创建数据结构的模板。模板本身并不是数据结构,但它详细说明了由该模板构造的对象的特征。
类型由下面的元素定义:
- 名称;
- 用于保存数据成员的数据结构;
- 一些行为及约束条件。
例如,下图阐明了 short
类型和 int
类型的组成元素。
三、实例化类型
从某个类型模板创建实际的对象,称为实例化该类型。
- 通过实例化类型而创建的对象被称为类型的对象或类型的实例。这两个术语可以互换。
- 在 C# 程序中,每个数据项都是某种类型的实例。这些类型可以是语言自带的,可以是 BCL 或其他库提供的,也可以是程序员定义的。
下图阐明了两种预定义类型对象的实例化。
四、数据成员和函数成员
像 short
、int
和 long
这样的类型称为简单类型。这种类型只能存储一个数据项。
其他的类型可以存储多个数据项。比如数组(array)类型就可以存储多个同类型的数据项。这些数据项称为数组元素。可以通过数字来引用这些元素,这些数字称为索引。
成员的类别
然而另外一些类型可以包含许多不同类型的数据项。这些类型中的数据项个体称为成员,并且与数组中使用数字来引用成员不同,这些成员有独特的名称。
有两种成员:数据成员和函数成员。
- 数据成员 保存了与这个类的对象或整个类相关的数据。
- 函数成员 执行代码。函数成员定义类型的行为。
例如,下图列出了类型 XYZ 的一些数据成员和函数成员。它包含两个数据成员和两个函数成员。
五、预定义类型
C# 提供了 16 种预定义类型,其中包括 13 种简单类型和 3 种非简单类型。
所有预定义类型的名称都由全小写的字母组成。预定义的简单类型包括以下 3 种。
- 11种数值类型。
- 不同长度的有符号和无符号整数类型。
- 浮点数类型
float
和double
。 - 一种称为
decimal
的高精度小数类型。与float
和double
不同,decimal
类型可以准确地表示分数。decimal
类型常用于货币的计算。
- 一种 Unicode 字符类型
char
。 - 一种布尔类型
bool
。bool
类型表示布尔值并且必须为true
或false
。
说明: 与 C 和 C++ 不同,在 C# 中的数值类型不具有布尔意义。
3 种非简单类型如下。
-
string
,它是一个 Unicode 字符数组。 -
object
,它是所有其他类型的基类。 -
dynamic
,使用动态语言编写的程序集时使用。
下图展示了 16 种预定义类型。
预定义类型的补充
所有预定义类型都直接映射到底层的 .NET 类型。C# 的类型名称就是 .NET 类型的别名,所以使用 .NET 的类型名称也符合 C# 语法,不过并不鼓助这样做。在 C# 程序中,应该尽量使用 C# 类型名称而不是 .NET 类型名称。
预定义简单类型表示一个单一的数据项。下表列出了这些类型,同时列出了它们的取值范围和对应的底层 .NET 类型。
名称 | 含义 | 范围 | .NET 框架类型 | 默认值 |
---|---|---|---|---|
sbyte | 8 位有符号整数 | -128 ~ 127 | System.SByte | 0 |
byte | 8 位无符号整数 | 0 ~ 255 | System.Byte | 0 |
short | 16 位有符号整数 | -32,768 ~ 32,767 | System.Int16 | 0 |
ushort | 16 位无符号整数 | 0 ~ 65,535 | System.UInt16 | 0 |
int | 32 位无符号整数 | -2,147,483,648 ~ 2,147,483,647 | System.Int32 | 0 |
uint | 32 位无符号整数 | 0 ~ 4,294,967,295 | System.UInt32 | 0 |
long | 64 位有符号整数 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 | System.Int64 | 0 |
ulong | 64 位无符号整数 | 0 ~ 18,446,744,073,709,551,615 | System.UInt64 | 0 |
float | 单精度浮点数,4 个字节,大约 6-9 位数字 | ±1.5 x 10?45 至 ±3.4 x 1038 | System.Single | 0.0f |
double | 双精度浮点数,8 个字节,大约 15-17 位数字 | ±5.0 × 10?324 到 ±1.7 × 10308 | System.Double | 0.0d |
decimal | 小数类型的有效数字精度位大约 28-29 位,16 个字节 | ±1.0 x 10-28 至 ±7.9228 x 1028 | System.Decimal | 0m |
bool | 布尔型 | true / false | SystemBoolean | false |
char | Unicode 字符串 | U+0000 ~ U+ffff | System.Char | \x0000 |
预定义非简单类型稍微复杂一些,下表展示预定义非简单类型。
名称 | 含义 | .NET 框架 |
---|---|---|
object | 所有其他类型的基类,包括简单类型 | System.Object |
string | 0 个或多个 Unicode 字符所组成的序列 | System.String |
dynamic | 在使用动态语言编写的程序集时使用 | 无相应的 .NET 类型 |
六、用户定义类型
除了 C# 提供的 16 种预定义类型,还可以创建自己的用户定义类型。有 6 种类型可以由用户自己创建,它们是:
- 类类型(class);
- 结构类型(struct);
- 数组类型(array);
- 枚举类型(enum);
- 委托类型(delegate);
- 接口类型(interface)。
类型通过类型声明创建,类型声明包含以下信息:
- 要创建的类型的种类;
- 新类型的名称;
- 对类型中每个成员的声明(名称和规格),
array
和delegate
类型除外,它们不含有命名成员。
一旦声明了类型,就可以创建和使用这种类型的对象,就像它们是预定义类型一样。下图概括了预定义类型和用户定义类型的使用。使用预定义类型是一个单步过程,简单地实例化对象
即可。使用用户定义类型是一个两步过程:必须先声明类型,然后实例化该类型的对象。
七、栈和堆
程序运行时,它的数据必须存储在内存中。一个数据项需要多大的内存、存储在什么地方,以及如何存储都依赖于该数据项的类型。
运行中的程序使用两个内存区域来存储数据:栈和堆。
7.1 栈
栈是一个内存数组,是一个 LIFO(Last-In First-Out,后进先出)的数据结构。栈存储几种类型的数据:
- 某些类型变量的值;
- 程序当前的执行环境;
- 传递给方法的参数。
系统管理所有的栈操作。作为程序员,你不需要显示地对它做任何事情。但了解栈的基本功能可以更好地了解程序在运行时在做什么,并能更好地了解 C# 文档和著作。
栈的特征
栈有如下几个普遍特征。
- 数据只能从栈的顶端插入和删除。
- 把数据放到栈顶称为入栈(push)。
- 从栈顶删除数据称为出栈(pop)。
下图展示了栈的相关术语:
7.2 堆
堆是一块内存区域,在堆里可以分配大块的内存用于存储某种类型的数据对象。与栈不同,堆里的内存能够以任意顺序存人和移除。下图展示了一个在堆里放了 4 项数据的程序。
虽然程序可以在堆里保存数据,但并不能显式地删除它们。CLR 的自动垃吸回收器在判断出程序的代码将不会再访问某数据项时,会自动清除无主的堆对象,我们因此可以不再操心这项使用其他编程语言时非常容易出错的工作了。下图阐明了垃圾回收的过程。
八、值类型和引用类型
数据项的类型定义了存储数据需要的内存大小及组成该类型的数据成员。类型还决定了对象在内存中的存储位置——栈或堆。
类型被分为两种:值类型和引用类型,这两种类型的对象在内存中的存储方式不同。
- 值类型只需要一段单独的内存,用于存储实际的数据。
-
引用类型需要两段内存。
- 第一段存储实际的数据,它总是位于堆中。
- 第二段是一个引用,指向数据在堆中的存放位置。
下图展示了每种类型的单个数据项是如何存储的。对于值类型,数据存放在栈里。对于引用类型,实际数据存放在堆里而引用存放在栈里。
8.1 存储引用类型对象的成员
上图阐明了当数据不是另一个对象的成员时如何存储。如果它是另一个对象的成员,那么它的存储会有些不同。
- 引用类型对象的数据部分始终存放在堆里,如上图所示。
- 值类型对象,或引用类型数据的引用部分可以存放在堆里,也可以存放在栈里,这依赖于实际环境。
例如,假设有一个引用类型的实例,名称为 MyType
,它有两个成员:一个值类型成员和一个引用类型成员。它将如何存储呢?是否是值类型的成员存储在栈里,而引用类型的成员如上图所示的那样在栈和堆之间分成两半呢?答案是否定的。
请记住,对于一个引用类型,其实例的数据部分始终存放在堆里。既然两个成员都是对象数据的一部分,那么它们都会被存放在堆里,无论它们是值类型还是引用类型。下图阐明了 MyType
的情形。
- 尽管成员 A 是值类型,但它也是
MyType
实例数据的一部分,因此和对象的数据一起被存放在堆里。 - 成员 B 是引用类型,所以它的数据部分会始终存放在堆里,正如图中“数据”框所示。不同的是,它的引用部分也被存放在堆里,封装在
MyType
对象的数据部分中。
说明 对于引用类型的任何对象,它所有的数据成员都存放在堆里,无论它们是值类型还是引用类型。
8.2 C# 类型的分类
下表列出了 C# 中可以使用的所有类型以及它们的类别:值类型或引用类型。每种引用类型都将在后面的内容中阐述。
- | 值类型 | 引用类型 |
---|---|---|
预定义类型 | sbyte、byte、short、ushort、int、uint、long、ulong、float、double、decimal、char、bool | object、string、dynamic |
用户定义类型 | struct、enum | class、interface、delegate、array |
九、变量
一种多用途的编程语言必须允许程序存取数据,而这正是通过变量实现的。
- 变量是一个名称,表示程序执行时存储在内存中的数据。
- C# 提供了 4 种变量,每一种都将详细讨论。 下表列出了变量的种类。
名称 | 描述 |
---|---|
局部变量 | 在方法的作用域保存临时数据,不是类型的成员 |
字段 | 保存和类型或类型实例相关的数据,是类型的成员 |
参数 | 用于从一个方法到另一个方法传递数据的临时变量,不是类型的成员 |
数组元素 | (通常是)同类数据项构成的有序集合的一个成员,可以为局部变量,也可以为类型的成员。 |
9.1 变量声明
变量在使用之前必须声明。变量声明定义了变量,并完成两件事:
- 给变量命名,并为它关联一种类型;
- 让编译器为它分配一块内存。
一个简单的变量声明至少需要一个类型和一个名称。下面的声明定义了一个名为 var2
的 int
类型的变量:
int var2; // int 表示类型 var2 表示名称
下图展现了 4 个变量的声明以及它们在栈中的位置。
-
变量初始化语句
除声明变量的名称和类型以外,声明还能把它的内存初始化为一个明确的值。
变量初始化语句(variable initializer)由一个等号后面跟一个初始值组成,如:
int var2 = 17; // 17 就是初始值
无初始化语句的局部变量有一个未定义的值,在赋值之前不能使用。试图使用未定义的局部变量会导致编译器产生一条错误消息。
下图在左边展示了许多局部变量声明,在右边展示了栈的构造结果。一些变量有初始化语句,其他的变量没有。由于自动初始化,图中的变量
dealer1
的值为null
,变量var1
的值为0
,前提是这两个变量不是在方法内声明的。 -
自动初始化
一些类型的变量如果在声明时没有初始化语句,那么会被自动设为默认值,而另一些则不能。没有自动初始化为默认值的变量在程序为它赋值之前包含未定义值。下表展示了哪种类型的变量会被自动初始化以及哪种类型的变量不会被初始化。
变量类型 存储位置 自动初始化 用途 局部变量 栈或者栈和堆 否 用于函数成员内部的局部计算 类字段 堆 是 类的成员 结构字段 栈或堆 是 结构的成员 参数 栈 否 用于把值传入或者传出方法 数组元素 堆 是 数组的成员
9.2 多变量声明
可以在单个声明语句中声明多个变量。
- 多变量声明中的变量必须类型相同。
- 变量名必须用逗号分隔,可以在变量名后包含初始化语句。
例如,下面的代码展示了两条有效的多变量声明语句。注意,只要使用逗号分开,初始化的变量就可以和未初始化的变量混在一起。最后一条声明语句是有问题的,因为它企图在一条语句中声明两个不同类型的变量。
// 声明一些变量,有的被初始化,有的未被初始化
int var3 = 7,var4,var5 = 3;
double var6,var7 = 6.52;
int var8,float var9; // 错误!多变量声明的变量类型必须相同
9.3 使用变量的值
变量名代表该变量保存的值,可以通过使用变量名来使用值。
例如,在下面的语句中,变量名 var2
表示变量所存储的值。当语句执行的时候,会从内存中获取该值。
Console.WriteLine("{0}",var2);
十、静态类型和 dynamic 关键字
你可能已经注意到了,每一个变量都包括变量类型。这样编译器就可以确定运行时需要的内存总量以及哪些部分应该存在栈上,哪些部分应该存在堆上。变量的类型在编译的时候确定并且不能在运行时修改。这叫作静态类型。
但是,不是所有的语言都是静态类型的,诸如 IronPython 和 IronRuby 之类的脚本语言是动态类型的。也就是说,变量的类型直到运行时才会被解析。由于它们是 .NET 语言,所以 C# 程序需要能够使用这些语言编写的程序集。问题是,程序集中的类型到运行时才会被解析,而 C# 又要引用这样的类型并且需要在编译的时候解析类型。
针对这个问题,C# 语言的设计者增加了 dynamic
关键字,代表一个特定的 C# 类型,它知道如何在运行时解析自身。
在编译时,编译器不会对 dynamic
类型的变量做类型检查。相反,它将与该变量及该变量的操作有关的所有信息打包。在运行时,会对这些信息进行检查,以确保它与变量所代表的实际类型一致。否则,将在运行时抛出异常。
十一、可空类型
在某些情况下,特别是使用数据库的时候,你希望表示变量目前未保存有效的值。对于引用类型,这很简单,可以把变量设置为 null
。但定义值类型的变量时,不管它的内容是否有有效的意义,其内存都会进行分配。
对于这种情况,你可能会使用一个布尔指示器来和变量关联,如果值有效,则设置为 true
,否则就设置为 false
。
可空类型允许创建可以标记为有效或无效的值类型变量,这样就可以在使用它之前确定值的有效性。普通的值类型称作非可空类型。
---(完)