C#(念作 See Sharp
)是一种简单、现代、面向对象并且类型安全的编程语言。C# 源于 C 语言家族,因此 C、C++ 和 java 工程师们能迅速上手。ECMA 国际[1](ECMA International)发布的 ECMA-334 规范[2]和由国际标准化组织[3](ISO)及国际电工委员会[4](IEC)发布的 ISO/IEC 23270 规范使 C# 语言成为一种标准化的语言,微软 .NET Framework C# 编译器就是遵照了这两个标准而实现的。
C# 不仅是一门面向对象的编程语言,同时它也为面向组件(component-oriented)[5]编程提供了支持。现代软件设计越来越多地依赖于以自包含(self-contained)[6]与自描述(self-describing)[7]功能包形式的软件组件。这些组件的关键之处在于它们呈现出一种带有属性(PRoperties)、方法(methods)和事件(events)的编程模型、对其进行说明的特性(attributes)并整合有它们自身的文档。C# 所提供的语言结构能直接支持这些概念,使得 C# 语言能轻而易举地玩转软件组件。
以下 C# 特点有助于构建鲁棒性[8]与持久性的应用程序:垃圾回收机制(garbage collection)能自动回收内存中的无用对象,异常处理机制(exception handling)提供了结构化的可扩展的错误侦测与恢复方法,而类型安全(type-safe)的设计又使它不会去获取到未被初始化的变量、获得数组维度外的索引或者执行未经检验的类型强转换等状况。
C# 拥有一个统一类型系统(unified type system)。所有的 C# 类型,包括 int
和 double
在内的基元类型都继承自同一个根: object
类型。因此,所有类型均能使用一组共用的操作方法且它们的值可以以一致的方式进行存储、传输和操作。此外 C# 提供了支持用户自定义的引用类型和值类型,不仅允许对轻量级的结构、还允许对对象动态分配。
为了确保 C# 程序和库能以兼容的方式演变发展,C# 的设计非常强调版本管理(versioning)。在这方面其他许多编程语言并未投入过多关注,而结果是当新版本的依赖库被引入后,那些编程语言写就的程序往往会崩溃。在 C# 的设计方面,它立即就受到了版本管理的影响,考虑到了包括区分虚修饰符 virtual
与重载修饰符 override
、接口重载规则以及支持显式地接口成员声明等问题。
本章的剩余部分将描述 C# 语言的精华所在。通过接下来的章节将详细地甚至会带有数学精神地描述它们的规则与例外,本章节将力争为读者展现一幅简洁明了的 C# 图卷,目的是为了让读者通览这门语言,以便读者能早日编写代码并阅读余下诸章。
Hello World
程序是每一门编程语言引言部分必有的传统精彩项目。这是 C# 自家的 Hello World 程序:
using System;class Hello{ static void Main() { Console.WriteLine("Hello, World"); }}
C# 的源文件使用 .cs
作为其后缀名,因此 Hello, World
程序的代码被存储在 hello.cs
文件内,微软的 C# 编译器[9]能通过命令行
csc hello.cs
来实现对其编译并生成一个名为 hello.exe
的可执行程序集[10]。这个程序集运行后将在屏幕上显示为:
Hello, World
Hello, World
程序始于一个 using
指令,它用于引入命名空间 System
。命名空间提供了一个有层次有条理的 C# 程序和库。命名空间包含了类型和其他命名空间,例如命名空间 System
就包含了 Console
,以及类似 IO
和 Collections
这样的命名空间。通过 using
指令引入一个命名空间后将可以使用这个命名空间下的所有成员类型。因此在引入 System
命名空间后,程序可以使用 Console.WriteLine
而不必完整地输入 System.Console.WriteLine
。
「Hello, World」程序的 Hello
类中只声明了一个成员——一个名叫 Main
的静态方法(static),实例方法可以使用保留关键字 this
来引用一个特定的包装对象,但静态方法不能。按照惯例,静态方法 Main
是程序的入口点。命名空间 System
下 Console
类的 WriteLine
方法最后在屏幕上输出了「Hello, World」字样。这个类(Console)由 .NET Framework 类库提供,并且通常是自动被微软 C# 编译器引用的。要注意,C# 并没有和「运行时」库分离开,恰恰相反,.NET Framework 自身就是一个 C# 的「运行时」库。
C# 的组织概念之关键在于程序、命名空间、类型、成员以及程序集(assemblies),C# 的程序由至少一个文件组成,程序中声明了包含有成员的类型并可命名空间化。类型有如类(Classes)和接口(Interfaces),成员则有如字段(Fields)、方法(Methods)、属性(Properties)和事件(Events)。当 C# 程序被编译,它们将被物理地打包到一个程序集中。程序集的后缀名如 .exe
或 .dll
,这取决于它们是实现了应用(applications)还是类库(Libraries)。举个例子:
using System;namespace Acme.Collections{ public class Stack { Entry top; public void Push(object data) { top = new Entry(top, data); } public object Pop() { if (top == null) throw new InvalidOperationException(); object result = top.data; top = top.next; return result; } class Entry { public Entry next; public object data; public Entry(Entry next, object data) { this.next = next; this.data = data; } } }}
上述代码片段声明了一个名曰 Stack
的类,其命名空间为 Acme.Collections
,因此它的完全限定名叫做 Acme.Collections.Stack
。这个类包含了好几个成员:一个字段 top
、两个方法 Push
与 Pop
以及一个内部类 Entry
。而内部类 Entry
则又包含了三个成员:两个字段 next
与 data
和一个构造函数。如果源代码被保存在 acme.cs
文件中,那么命令行输入:
csc /t:library acme.cs
编译这段代码并使用参数 /t
来显式地指明编译为库(代码中不包含 Main 入口点),由此将生成名曰 acme.dll
的程序集。程序及所包含的可执行代码由 IL(Intermediate Language)[11]指令和符号信息元数据(metadata)[12]构成。在被执行前,程序集内的 IL 代码由 .NET CLR(Common Language Runtime)[13]自动即时编译(JIT compiler,Just-In-Time compiler)转换为针对处理器定制的代码。
由于程序集是一种功能上包含有代码和元数据的自描述单元,故 C# 不需要使用 #include
指令和头文件(header files)。其内包含有公开类型和成员的特殊的程序集在程序被编译时能被轻松引用。比如说,程序从 acme.dll
程序集中调用 Acme.Collections.Stack
类:
using System;using Acme.Collections;class Test{ static void Main() { Stack s = new Stack(); s.Push(1); s.Push(10); s.Push(100); Console.WriteLine(s.Pop()); Console.WriteLine(s.Pop()); Console.WriteLine(s.Pop()); }}
若程序被存于名为 text.cs
的文件内,则当其被编译时,acme.dll
程序集需要使用参数 /r
来引入程序:
csc /r:acme.dll test.cs
如此,将创建名为 text.exe
的可执行程序集,并可运行且输出接口如下:
100101
C# 允许程序的源代码被保存在多个源文件内。当一个包含多个文件的 C# 程序被编译,所有的源文件都会被一起处理且这些源文件能被直接相互引用,因为在被编译处理之前,这些文件会被连接到一个更大的文件内。在 C# 中并不需要前置声明(Forward Declaration),这是因为在大多数情况下声明的顺序是无所谓的。同时 C# 既不限制源代码内是否只能声明一个公开类型,也不限制源代码的文件名必须与其内声明的类型一致[14]。
在 C# 中有两种类型:值类型和引用类型,值类型变量直接包含其自身数据,而引用类型(如对象等)则存其引用对象的内存地址。对于引用类型而言,极有可能出现两个不同的变量指向同一个对象的情况,同时也极有可能出现因为修改了一个引用类型的对象的值导致另一个引用类型受到影响的情况。而对于值类型而言,每一个变量都是其值的副本,修改一个值对象的值不可能影响到其他值对象(除非使用了 ref
或 out
参数)。
C# 的值类型能被更进一步地分为简单类型、枚举类型、结构类型和可空类型,引用类型则可被进一步分为类、接口、数组和委托。下表可一览 C# 的类型系统。
这八种整数类型提供了 8 位、16 位、32 位和 64 位的有符号与无符号的形式。
两种浮点数类型:float
和 double
分别被 IEEE 754 规范描述为 32 位长度的单精度浮点数和 64 位的多精度浮点数。高精度类型 decimal
是一个 128 位长度的数据类型,适合被用于金融、财务与钱币的计算。bool
被用于描述布尔值——一种即是或非的值。字符(character)和字符串(string)在 C# 中使用 Unicode 编码。字符类型 char
描述为一个 UTF-16 代码单元,而字符串类型 string
被描述为一个连续的 UTF-16 代码单元。下表简述了 C# 的数字类型:
在 C# 中创建一个新类型需要对其进行声明(类型声明,type declarations),并为其指定名称与成员。C# 有五种用户可定义的类型大类,它们分别是:类(class)、结构(struct)、接口(interface)、枚举(enum)和委托(delegate)。
类所定义的数据结构包含了数据成员(字段)和函数成员(方法、属性等)。类支持单继承和多态性,派生类能扩展和特化基类。
结构是一种简单的类,它代表了一种含有数据成员与函数成员的结构。然而,不像类,结构是值类型(而不是引用类型),不会去申请分配堆(heap)。结构不支持用户指定的继承,且所有的结构类型都隐式地继承自 object
类型。
接口定义了功能成员的名称集合的契约,实现了接口的类或结构必须提供接口中所定义的函数成员的实现。一个接口可以继承自多个接口,一个类或结构可以继承自多个接口。
委托类型定义了方法的参数列表和返回类型细节。委托将一个方法变为一个独立实体,使其能被指定变量并如参数般被传递。委托与其他语言的方法指针很像,但不像后者,委托是面向对象(object-oriented)[15]且类型安全(type-safe)[16]的。
类、结构、接口和委托类型都支持泛型(generics)。
枚举类型的名字是直接确定的,所有枚举类型必须以八种整数类型之一为其基础类型,枚举值的集合即为其底层类型值的集合(笔者注:此处「集合」原文为「set」,指数学意义上的集合,意味着每一个成员都是唯一的,这与 array 或 collections 不同)。
C# 支持任意类型的一维或多维数组。与上面所讲的类型不同,数组类型不需要在使用前声明,而是在类型名称后紧跟着方括号 []
的形式来创建。例如 int[]
是一个 int 类型的一维数组,int[,]
是 int 类型的二维数组,int[][]
是 int 类型的多维数组。
可空类型在使用前不需要声明,每一种不可空的值类型 T
都对应一个可空类型的版本 T?
,后者在前者的基础上增加了对 null
值的支持。举例来说,int?
能被赋值为任意 32 位整数或 null
值。
C# 的任意类型的值都可被视作一个对象 object
。每一种类型都直接或间接地派生自 object
类,而 object
则是所有类型的基类。引用类型的值也就是其所指向的 object
对象的值。值类型的值之所以能被视作对象,是因为装箱(boxing)[17]和<
新闻热点
疑难解答