C# 入门支持文章

术语

对象(Object) 对客观世界中对象的抽象描述,是构成软件系统的基本单位。

(Class) 具有相同属性和操作的一组对象的集合,它描述的不是单个对象而是“一类”对象的共同特征。 类是面向对象技术中最重要的结构,它支持信息隐藏和封装,进而支持对抽象数据类型(Abstract Data Type, ADT)的实现。 信息隐藏是指对象的私有信息不能由外界直接访问,而只能通过该对象公开的操作来间接访问,这有助于提高程序的可靠性和安全性。 类将数据和数据上的操作封装为一个有机的整体,类的用户只关心其提供的服务,而不必了解其内部实现细节。

消息(Message)对象具有自治性和独立性,它们之间通过消息进行通信,这也是对客观世界的形象模拟。 发送消息的对象叫客户(Client),而接收消息的对象叫服务器(Server)。 按照封装原则,对象总是通过公开其某些操作来向外界提供服务;如果某客户要请求其服务,那么就需要向服务器对象发送消息,而且消息的格式必须符合约定要求。消息中至少应制定要请求的服务(操作)名,必要时还应提供输入参数和输出参数。

四大关系

  • 聚合(Aggregation):一个对象是另一个对象的组成部分,也叫部分 – 整体关系。
  • 依赖(Dependency):一个对象对另一个对象存在依赖关系,且对后者的改变可能会影响前者。
  • 泛化(Generalization):一个对象的类型是另一个对象类型的特例,也叫特殊 – 一般关系,其中特殊类表示对一般类内涵的进一步细化。从泛化关系可以引出面向对象方法中另一个重要概念——继承。
  • 一般关联(Association):对象之间存在物理或逻辑上更为一般的关联关系,主要是指一个对象使用另一个对象的服务。根据语义还可将关联关系分为多元关联和二元关联,二元关联还可进一步细分为一对一关联、一对多关联以及多对多关联等。

聚合和依赖有时也被视为特殊的关联关系。 聚合:“学生”和“班级” 依赖:“学生卡”和“学生” 泛化:“研究生”和“学生” 一般关联:“老师”和“学生”

继承(Inheritance):在泛化关系中,特殊类可自动具有一般类的属性和操作,称作“继承”;特殊类还可以定义自己的属性和操作,从而对一般类的功能进行扩充。在类的继承结构中,一般类也叫作基类或父类,特殊类也叫作派生类或子类。 继承的层次结构不宜过细过深,否则会增加理解和修改的难度。 继承具有可传递性。子类可以享受其各级父类所提供的服务,从而实现高度的可复用性;当父类的某项功能发生变化时,对其的修改会自动反映到各个子类中,这也提高了软件的可维护性。 继承还包括一种多继承的形式,即一个对象因其不同特性可属于不同父类。在面向对象的开发中,多继承具有较大的灵活性,但同时也会带来语义冲突、程序结构混乱等问题。目前,C++ 等语言支持多继承,Java 和 C# 等语言则不支持,但可通过接口等技术来间接实现多继承的功能。

多态性(Polymorohism):同一事物在不同的条件下可以表现出的不同形态。在面向对象的消息通信时,发送消息的对象只需要确定接收消息的对象能够执行指定的操作,而并不一定要知道接收方的具体类型;接收到消息之后,不同类型的对象可以作出不同的解释,执行不同的操作,从而产生不同的结果。多态性特征能够帮助我们开发灵活且易于修改的程序。

组件(Component):可以单独开发、测试和部署的软件模块。一个组件中可只有一个类,也可以包含多个类。 接口(Interface):对组件服务的抽象描述。接口是一种抽象数据类型,它所描述的是功能的“契约”,而不考虑与实现有关的任何因素。 接口一旦发布就不应再作修改,否则就会导致所有支持该接口的类型都变得无效。而组件一经发布,也不应取消它已声明支持的接口,而是只能增加新的接口。 对于服务的使用方(客户)而言,它既不关心服务提供者的实际类型,也不关心服务的具体细节,而只需要根据接口去查询和使用服务即可。接口将功能契约从实现中完全抽离出来,能够有效地实现系统职责分离,同时弥补继承和多态性的功能不足,进而实现良好的系统设计。

开发方法

面向对象的分析(Object Oriented Analysis, OOA)

运用面向对象的方法对目标系统进行分析和理解,找出描述问题域和系统责任所需要的对象,定义对象的基本框架(包括对象的属性、操作以及它们之间的关系),最后得到能够满足用户需求的系统分析模型。其任务如下:

  1. 识别问题域中的对象和类。通过对问题域和系统责任的深入分析,尽可能地找出与应用有关的对象和类,并从中筛选出真正有用的对象和类。
  2. 确定结构。找出对象和类中存在的各种整体 – 部分结构和一般 – 特殊结构,并进一步确定这些结构组合而成的多重结构。
  3. 确定主题。如果系统包含大量对象和类,那么可划分出不同的应用主题域,并按照主题域对分析模型进行分解。
  4. 定义属性。识别各个对象的属性,确定其名称、类型和限制,并在此基础上找出对象之间的实例连接。
  5. 定义服务,识别各个对象所提供的服务,确定其名称、功能和使用约定,并在此基础上找出对象之间的消息连接。

OOA 的结果是系统分析说明书,其中包括使用类图和对象图等描述的系统静态模型,使用例图、活动图和交互图等描述的系统动态模型,以及对象和类的规约描述。模型应尽量与问题域保持一致,而不考虑与目标系统实现有关的因素(如使用的编程语言、数据库平台和操作系统等)。

面向对象的设计(Object Oriented Design, OOD)

以系统分析模型为基础,运用面向对象的方法进行系统设计,解决与系统实现有关的一系列问题,最后得到符合具体实现条件的系统设计模型。其任务如下:

  1. 问题域设计。对问题域中的分析结果作进一步的细化、改进和增补,包括对模型中的对象和类、结构、属性、操作等进行组合和分解,并根据面向对象的设计原则增加必要的新元素类、属性和关系。这部分主要包括以下设计内容:
    • 复用设计,即寻找可复用的类和设计模式,提高开发效率和质量。
    • 考虑对象和类的共同特征,增加一般类以建立共同协议。
    • 调整继承结构,如减少继承层次、将多继承转换为单继承、调整多态性等。
    • 改进性能,如分解复杂类、合并通信频繁的类、减少并发类等。
    • 调整关联关系,如将多元关联转换为二元关联、将多对多关联转换为一对多关联等。
    • 调整和完善属性,包括确定属性的类型、初始值和可访问性等。
    • 构造和优化算法。
  2. 用户界面设计。对软件系统的用户进行分析,对用户界面的表现形式和交互方式进行设计。这部分主要包括以下设计内容:
    • 用户分类。
    • 人机交互场景描述。
    • 系统命令的组织。
    • 详细的输入和输出设计。
    • 在需要时可增加用于人机交互的对象和类,并使用面向对象的方法对其进行设计。
  3. 任务管理设计。当系统中存在多任务(进程)并发行为时,需要定义、选择和调整这些任务,从而简化系统的控制结构。这部分主要包括以下设计内容:
    • 识别系统任务,包括事件驱动任务、时钟驱动任务、优先任务和关键任务等。
    • 确定人物之间的通信机制。
    • 任务的协调和控制。
  4. 数据管理设计。识别系统需要存储的数据内容和结构,确定对这些数据的访问和管理方法。这部分主要包括以下设计内容:
    • 数据的存储方式设计,目前主要有文件系统和数据库系统两种方式。
    • 永久性类(Persistent Class)的存储设计,包括其用于存储管理的属性和操作设计。
    • 永久性类之间关系的存储设计。

OOA 和 OOD 之间不强调严格的阶段划分,设计模型是对分析模型的逐步细化,主要是在问题域和系统责任的分析基础上解决各种与实现有关的问题。OOA 阶段一些不能确定的问题可以遗留到 OOD 阶段解决,开发过程中也允许反复和迭代。

基础代码

using System;
namespace Helloworld
{
    public class Program
    {
        public static void Main()
        {
            Console.WriteLine("Hello world!")
            string s = "Hello world!";
            Console.WriteLine(s);
        }
    }
}

一段在终端输出两次“Hello world!”的代码。

入门

C# 语言对大小写敏感。例如:关键字“using”不能写成“USING”,“namespace”也不能写成“Namespace”。 不同层级的代码使用 Tab 进行缩进,可大大提高可读性。

单行注释符号为 //,在此符号之后至该行结束的所有内容将“无效化”,用于开发者注释,不会被执行,可以使用中文。 多行注释符号为 /**/,在两个星号中的所有内容被视为注释。 特别注意的是,注释不可以嵌套。因此无论你有多少个 /* 开头,都是在一个注释内,只能用一个 */ 结尾,多余的 */ 会被视为非法代码。

// 这一行为注释,不会被执行。
using System;	// 这一行在注释符号后的不会被执行。
/* 在这里的为注释,不会执行。 */
/*/* 错误示例!这里为注释。*/ 但是这里已经不是注释了 */

命名空间(namespace)

程序中常常需要定义很多的类型,为了便于类型的组织和管理,C# 引入了命名空间。 一组类型可以属于一个命名空间,而一个命名空间也可嵌套在另一个命名空间中,从而形成一个逻辑结构,类似于文件资源管理器中的目录式文件系统组织方式。

using System;

这里代表引用了一个 .NET 类库中的“System”的命名空间。之后程序就可以自由使用该命名空间下定义的各种类型。

namespace Helloworld{
}

使用 namespace 创建了一个新的命名空间,括号内的全部内容都属于这个命名空间。

类(class)

类是最基本的一种数据类型,其属性叫做“字段”(Field),类的操作叫做“方法”(Method)。

public class Program{
  …
}

为定义一个名为“Program”的类。

public static void Main()
{
	Console.WriteLine("Hello world!");
}

这个名为 Program 的类中包含了一个名为“Main”的方法。 这个方法旨在调用 Console 类中的 WriteLine 执行文本输出,该方法的输入参数是用一对双引号括起来的字符串,表示要输出的文本内容。定义显式字符串对象则可用下面的代码,其中 string 代表字符串的类型,而 s 为该类型的一个对象(变量)。

public static void Main()
{
	string s = "Hello world!";
	Console.WriteLine(s);
}

由于我们一开始声明了引用 .NET 类库的 System 的命名空间,所以我们可以在 Main 方法中直接使用该类里面的 Console 类。它表示对控制台窗口的抽象。如果我们不在开头引用 System,也可在这行代码中指定命名空间:

System.Console.WriteLine("Helloworld!");

下面是 Console 类的一些常用的输入/输出方法:

方法输入参数返回值作用
Read整数读入下一个字符
ReadKeyConsoleKey 对象读入一个字符
ReadLine字符串读入一行文本,至换行符结束
Write任何输出一行文本
WriteLine任何输出一行文本,并在结尾处自动换行

动态链接库(Dynamic Link Library)

程序的功能是通过执行方法代码来实现的,每个方法都是从其第一行代码开始执行,直到最后一行代码结束,期间可以通过代码来调用其他方法,从而完成各式各样的操作。应用程序的执行必须要有一个起点,在 C# 中,这个起点为 Main 方法。程序每次都会从 Main 方法的第一行代码开始执行,直到 Main 方法结束时停止运行。如果包含多个 Main,需要指定其中一个 Main 作为程序的主方法。

我们日常使用的 Windows 系统中的程序通常用可执行文件(文件后缀 *.exe)打开。这种文件格式是程序集(Assembly)中的一种。人们使用代码编写的程序是源程序文件,它必须经过编译后才能执行。编译生成的程序模块便是程序集。一个软件系统可以是一个程序集,但更多时候是多个相互调用的程序及组成的集合。因此,还有一种程序集叫做动态链接库。

动态链接库缩写为 dll(文件后缀 *.dll),是一个包含可由多个程序,同时使用的代码和数据的库,主要为其他程序提供各种类型和服务,其本身不能直接启动,因此这种程序可以不包含 Main 方法。 使用 DLL 可以让程序使用较少的资源,当多个程序使用同一个函数库时,DLL 可以减少在磁盘和物理内存中加载的代码的重复量。这不仅可以大大影响在前台运行的程序,而且可以大大影响其他在 Windows 操作系统上运行的程序。 推广模块式体系结构DLL 有助于促进模块式程序的开发。

例如,需要在一个新的程序中引用刚刚的 Helloworld,可用如下代码:

Helloworld.Program.Main();

之前在显式字符串中有提到字符串的类型:

public static void Main()
{
		string s = "Hello world!";
		Console.WriteLine(s);
}

这里来介绍一下数据类型。

数据类型包括 值类型 和 引用类型 两大类。 值类型包括整数、字符、布尔值等 简单值 类型以及结构(Struct)和枚举(Enum)两种 复合值 类型。 引用类型包括类、接口(Interface)、委托(Delegate)和数组。 这些类本质上都是面向对象的。

简单值

整数类型

整数类型是对数学中的整数的抽象,但受到计算机存储限制,程序设计语言中的值类型总是要设置取值范围限制。C# 的限制如下:

  • int:32 位整数,取值范围为 -2147483648($-2^{31}$)~ 2147483647($2^{31}-1$)。
  • uint:32 位无符号整数(正整数),取值范围为 0 ~ 4294967295($2^{32}-1$)。
  • long:64 位整数,取值范围为 -9223372036854775808($-2^{63}$)~ 9223372036854775807($2^{63}-1$)。
  • ulong:64 位无符号整数(正整数),取值范围为 0 ~ 18446744073709551615($2^{64}-1$)。
  • short:16 位短整数,取值范围为 -32768($-2^{15}$)~ 32767($2^{15}-1$)。
  • ushort:16 位无符号短整数(正整数),取值范围为 0 ~ 65535($2^{16}-1$)。
  • sbyte:8 位字节型整数,取值范围为 -128($-2^{7}$) ~ 127($2^{7}-1$)。
  • byte:8 位无符号字节型整数,取值范围为 0 ~ 255($2^{8}-1$)。
字符类型

C# 中使用 char 来表示字符类型。由于使用 Unicode 字符集(16位)(万国码),char 类型不仅包含基本的 ASCII 字符,还能够表示汉字等多国语言符号。char 类型实际上仍以整数的方式进行存储,因此一个字符会占用 16 位字节的内存空间。由于一些符号在 C# 中会被用到,因此额外指定使用这些符号或一些特殊字符用转义符来代替:

\'				单引号
\''				双引号
\\				单斜杠
\a				警报(Alert)
\b				退格(Backspace)
\e				取消(Esc)
\n				换行(Newline)
\r				回车(Return)
\f				换页
\t				水平 Tab
\v				垂直 Tab
\0				空字符
实数类型
  • float:32 位单精度浮点数类型,取值范围 $\pm 1.5\times 10^{-45}$ ~ $\pm 3.4\times 10^{38}$。
  • double:64 位双精度浮点数类型,取值范围为 $\pm 5.0\times 10^{-324}$ ~ $\pm 1.7\times 10^{308}$。
  • decimal:128 位十进制小数类型,取值范围为 $\pm 1.0\times 10^{-28}$ ~ $\pm 7.9\times 10^{28}$。

特别地,为了让编译器知道以何种类型处理数值,float 和 decimal 类型的变量值应在小数后分别加上后缀 F 和 M。例如:

double x = 1.2;
float y = -0.5F;
decimal z = 3.2M;

实数类型使用的位数较多,相应的计算也会消耗更多的资源,因此在程序中应尽量使用低精度的类型。如果可以,使用整数来代替实数类型,例如货币计算中的精确到元的小数后两位,我们使用分作为单位,100 分 = 1 元即优化运算。 此外,计算机中的实数运算也受到精度和范围的限制,有时候你甚至不能得到数学上完全正确的解。

布尔类型

bool 表示布尔类型,其取值只有两个:true(真)或 false(假)。其在计算机内部实际上是用二进制 1 和 0 表示的,因此运算效率最高。

复合值类型

结构(struct)

像 int、double 这些简单值类型都在 .NET 库中预定义了。很多情况下,人们需要将不同的简单值类型组合起来使用,这时可使用结构类型。 例如:“复数”在 .NET 中并没有这个类型的定义,但是我们可以在 double 类型的基础上进行定义。我们用两个 double 类型的字段 a,b 分别组成复数的实部和虚部。

struct ComplexNumber
{
    public double a;
    public double b;
}

这里的 public 代表公有成员,与私有成员 private 相对。我们可以用圆点连接符来访问其公有成员“.”:

ComplexNumber c1;
c1.a = 2.5;
c1.b = 5;

结构类型支持信息隐藏和封装,也可以包含成员方法,支持对象创建和消息通信;不过它不支持继承和多态,因此是一种“部分面向对象”的类型。下面展示一个包含成员方法 Write 在终端输出两个复数的示例程序:

using System;
namespace ComplexNumberSample{
    class Program{
        static void Main(){
            ComplexNumber c1;             // 定义结构变量 c1
            c1.a = 2.5;
            c1.b = 5;
            ComplexNumber c2 = c1;        // 定义结构变量 c2
            c2.a = c2.b;
            Console.Write("c1 = ");
            c1.Write();
            Console.Write("c2 = ");
            c2.Write();
        }
    }
}

struct ComplexNumber{
    public double a;
    public double b;

    public void Write(){
        Console.WriteLine("(0)+(1)i", a, b);
    }
}

上面最后一行代码使用了 Console 类的 WriteLine 方法的格式化输出功能,表示用方法的第 2 个参数值替换第 1 个参数中的 {0} 标记、用第 3 个参数值替换第 1 个参数中的 {1} 标记,……,以此类推。这个程序输出如下:

c1 = 1.5+3i
c2 = 3+3i

实际上,前面介绍的简单值类型在 .NET 库内部都是以结构的方式来定义的,这些结构不仅包含了变量所存储的值,还提供了类型本身的有关消息。例如它们都提供了 MinValue 和 MaxValue 字段来表示类型的最小值和最大值,实数类型还提供了 NegativeInfinity 和 PositiveInfinity 字段来表示正无穷大和负无穷大,此外它们还能够通过成员方法 ToString 来得到数值的字符串表示,通过方法 Parse 来将字符串转换为数值。下面展示一组示例:

short a = short.MaxValue;       // x = 32767
short b = short.MinValue;       // x = -32768
ushort c = ushort.MinValue;     // y = 0
double x = double.PositiveInfinity;
double y = -x;                  // y = double.NegativeInfinity;
string s = a.ToString();        // s = "32767"
int i = int.Prase(s);           // i = 32767
枚举(enum)

若要对变量的取值范围作特殊的限定,可以将其定义为枚举类型,在其中“枚举”出所有可能的取值项。 例如:考虑“星期”这个概念,其取值可以是星期一到星期日,那么可用如下方式定义枚举类型 Weekday:

enum Weekday{
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

枚举类型的变量值是通过“类型名”加连接符“.”再加“枚举项”来使用的,例如星期一是“Weekday.Monday”。

Weekday w1 = Weekday.Monday;

与 C 和 C++ 一样,起始值为 0。在这段代码中,Monday 对应的整数为 0。 C# 还允许在程序中明确指定枚举值对应的整数值。例如对于下面的定义,Monday ~ Sunday 所对应的整数值将分别为 1 ~ 7:

enum Weekday{
    Monday = 1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}

特别地,程序中不应当滥用枚举类型。对于可以使用整数类型的内容,例如“月份”在 1 ~ 12 范围,应当直接使用整数类型并在程序中控制逻辑范围,而不是将它们定义为枚举。

类(class)

结构是值类型,而类是引用类型。 值类型在声明变量时便会分配内存。如果只声明引用类型的变量,得到的就是一个什么都没有的空对象(null),当然,也就不能访问其成员。 当值类型的一个变量赋值给另一个变量,原始变量的数据会被复制给新变量,之后两个变量是相互独立的。 当引用类型的一个变量赋值给另一个变量时,新变量实际上只是包含了原始对象的指针。其中任何一个变量的修改都会影响到另一个变量。

回到之前的 ComplexNumber。我们可以通过把关键字 struct 修改为 class 将 ComplexNumber 的类型从结构换成类。

using System;
namespace ComplexNumberSample
{
    class Program{
        static void Main(){
            ComplexNumber c1 = new ComplexNumber();     // 创建 c1 对象
            c1.a = 2.5;
            c1.b = 5;
            ComplexNumber c2 = c1;                      // 创建 c2 对象
            c2.a = c2.b;
            Console.Write("c1 = ");
            c1.Write();
            Console.Write("c2 = ");
            c2.Write();
        }
    }
    class ComplexNumber{
        public double a;
        public double b;

        public void Write(){
            Console.WriteLine("(0)+(1)i", a, b);
        }
    }
}

注意,改写后再次编译运行,输出结果会发生变化:

c1 = 3+3i
c2 = 3+3i

类是一种完全面向对象的类型,使用它能够充分享受继承和多态性等面向对象的优越性。定义一个新的类时,如果要使其从另一个类中继承,那么应在新类的名称后接冒号再接基类的名称。 下面展示一段包含基类 Student 和其派生类 Graduate 的代码以展示前文内容:

class Student               // 基类
{
    public string name;
    public int age;
    public int grade;
    public void Register() { }
}

class Graduate : Student    // 派生类
{
    public string supervisor;
}

在这里,Graduate 继承了 Student 的 3 个字段和 1 个方法,而 supervisor 是 Graduate 类自己独有的。

特别地,string 类型其实也是 .NET 库中定义的一个类,但 C# 对字符串有特殊的处理方式:对于两个 string 变量 s1 和 s2,语句“s1=s2”并不会使他们指向同一个对象,因此不存在修改一个字符串而影响到另一个字符串的情况。但有的 string 对象仍是引用类型;和其他对象类型一样,可将空值 null 赋值给 string 变量,而这对于任何值类型的变量都是不允许的。

数组(array)

数组是一种聚合类型,它表示具有相同类型的一组对象的集合,这些对象叫做数组元素。数组元素的类型可以是值类型,也可以是引用类型。

一位数组的声明是在数组元素的类型声明后加上一对中括号。 多维数组的声明是在元素类型后的中括号里增加逗号,并在初始化时指定每一维的长度。 作为引用类型,数组对象同样需要初始化之后才能使用,而且 new 关键字后面的类型声明的中括号里需要指定数组的长度。下面是一段示例:

nums = new int[3];                      // 新建一个长度为 3 的数组
nums[0] = 3;                            // 数组的第 1 个元素赋值为 3
nums[1] = 6;                            // 数组的第 2 个元素赋值为 6
nums[2] = 9;                            // 数组的第 3 个元素赋值为 9
Console.WriteLine(nums[1]);             // 输出数组的第二个元素值
int[] xs = new int[3] {10, 20, 30};     // 初始化数组时便给数组赋值
double[] ys = new double[4] {1.25, 3.5, 3.75, 5};
int[] samexs = {10, 20, 30};            // 代码的简写方式
double[] sameys = {1.25, 3.5, 3.75, 5};
Console.WriteLine(nums.Length);         // Length 属性判断数组长度,这里会输出 3

int[,] nums = new int[3, 2];            // 定义一个多维数组
nums[0, 0] = 1;
nums[0, 1] = 2;
nums[1, 0] = 3;
nums[1, 1] = 5;
nums[1, 2] = 8;                         // 错误:数组越界!
Console.WriteLine(nums.GetLength(0));   // Length 会返回数组的总长度,而 GetLength 可以返回每一维的长度。

当数组元素的类型为值类型时,数据直接存放在数组中;而当数组元素的类型为引用类型时,数组中存放的只是各个引用对象的地址,这时不排除多个数组元素指向同一个对象的可能。

为了提高程序的可读性,创建数组时还是应当尽量写出数组各维的长度。此外,数组维数过高不仅会占用系统资源,还会增加程序复杂度以及数组越界的危险性,因此要控制使用三维以上的多维数组。

类型转换

相容的数据类型的变量之间可用进行类型转换。有的转换是系统默认的,这叫做隐式(Implicit)转换;有的转换则需要明确指定转换的类型,这叫做显式(Explicit)转换。显式转换不能保证成功,还有可能发生信息丢失,使用时要加以注意。

值之间类型的转换

从低精度的简单值到高精度的简单值可以进行隐式转换,这包括以下三种情况:

  • 如果一种整数类型的取值范围被另一种整数类型的取值范围所涵盖,那么从前者到后者可进行隐式转换。
  • 从整数类型到实数类型可进行隐式转换。
  • 从 float 类型到 double 类型可进行隐式转换。
  • 从 char 类型到 ushort、uint、int、ulong、long、float、double、demical 这些类型可进行隐式转换(这和 ushort 类型到其他类型的隐式转换是等价的)。

不满足上述隐式转换条件的简单值类型直接只能进行显式转换,转换的办法是在要转换的值之前加上一对圆括号,并在括号中写上要转换到的目标类型。例如:

double y1 = 13.56;
int y2 = (int)y1;

显式转换要注意值的范围。例如,将实数转换为整数后,实数原来的小数部分就会丢失。要额外注意下面这种情况:

long z1 = 100;
int z2 = (int)z1;
short z3 = (short)z2;
sbyte z4 = (sbyte)z3;

执行完上述的转换后,z1 ~ z4 的值都还是 100。但如果将 z1 的值改为 1000,z2 和 z3 的值仍然不变,但 z4 的值就会变成 -24。这是因为整数 1000 对应的 16 位二进制码为 0000001111101000,但转换到 8 为整数类型 sbyte 时就只剩下后 8 位 11101000,对应十进制整数就是 -24.这时程序不会报错,有经验的开发人员也能推断出预期结果,但在程序中应避免这种情况的出现,而绝不能将其作为一种非常规的“技巧”使用。

整数类型和 char 类型之间转换所表达的是字符的整数编码,此时要注意数字字符和数字本身的区别。C# 还规定从各种整数类型到 char 类型都只能进行显式转换,如下:

int x = 6;
char a = (char)x;       // 显式转换
Console.WriteLine(a);
char b = '6';
int y = b;              // 隐式转换
Console.WriteLine(b);
Console.WriteLine(y);

整数编码 6 对应的字符是黑桃符号“♠”,因此第 3 行代码将输出该符号;而字符“6”对应的编码是 54,因此第 6 行和第 7 行代码将分别输出 6 和 54(Console 类会自动根据参数的类型来选择输出的格式)。

自定义的枚举类型在本质上也是整数,但枚举类型和整数类型之间必须使用显式转换,唯一的例外是常数 0 可以直接赋值给枚举变量。

此外,布尔类型与整数等其他类型之间不存在任何转换关系(这和 C/C++ 等语言中的布尔类型是不同的)。

引用类型之间的转换

引用类型之间转换的基本原则是:从子类的对象到父类的对象可以进行隐式转换;而从父类的对象到子类的对象只能进行显式转换,且不一定成功。因为特殊对象同时也是一般对象,而一般对象不能保证是特殊对象。转换之后,两个变量指向仍是同一个对象。

C# 对数组的转换规定如下:如果两个数组的维数相同,而它们的数组元素之间可用进行引用转换,那么两个数组之间也可以进行引用转换,且转换方式与数组元素之间的转换方式相同。 注意:在元素为值类型的数组之间(如 int[] 数组和 long[] 数组之间)不能进行任何转换。

值类型和引用类型之间的转换

在 C++ 等传统语言当中,整数、布尔值等值类型属于语言的内置类型,而这些内置类型本身不是面向对象的。 C# 采用了一种全新的观点:程序设计语言的整个类型系统是一个有机的整体,所有的变量都是对象,所有的类型都有一个共同的基类—— object 类。

object 类本身是引用类型,那么其他引用类型都可以与它进行转换。object 同时又是所有值类型的基类,那么所有值类型的变量都可以隐式转换为 object 类型,这个过程叫做 装箱(Boxing),而 object 类型可以显示转换到值类型,这个过程叫做 拆箱(Unboxing)。这样值类型和引用类型两部分就有机地联系在了一起。下面为演示代码:

int x = 3;
object y = x;       // 装箱
int z = (int)y;     // 拆箱

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注