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
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]和拆箱(unboxing)[18]操作的存在。在下面的例子中,int 类型将被转为 object
对象并重新转换为 int 类型:
using System;class Test{ static void Main() { int i = 123; object o = i; // Boxing int j = (int)o; // Unboxing }}
当一个值类型的值转换为 object
类型(一个对象的实例)时,值类型的值将被保持并置入其中,这个过程被称为装箱(box)。反之,当一个 object
引用转换为值类型,将检测引用对象是否为一个正确的值对象的包装。如果检测通过,则将包装内的值复制而出。
C# 统一的类型系统意味着能将一个值类型「按需」变为一个对象,这是源于 C# 统一的通用库能被用于引用类型与值类型。在 C# 中有好几大类的变量,包括字段、数组元素、局部变量以及参数等。变量是数据的存储容器,每一个变量都有一个明确的类型,这意味着对应类型的值将被存储在这个变量中,如下表所示:
表达式 Expressions
表达式由操作数和操作符构成,操作符指明了如何去操作相应的操作数。操作符包括 +
、 -
、 *
、 /
以及 new
等,操作数包括字面量、字段、局部变量以及其他表达式等。当一个表达式中包含多个运算符时,表达式将根据运算符的优先操作顺序对其求值。例如表达式 x + y * z
等视作 x + (y * z)
,这是因为乘法运算 *
的优先级高于加法运算 +
。
大多数运算符可被重载(overload),运算符重载允许用户指定自定义的对一个或两个运算数(诸如类或结构类型)的运算实现。
下表总结了 C# 的操作,列表按照操作符目录的优先级进行了排序。在同一目录下的操作符表示相同的优先级。
语句 Statements
程序的执行在于语句(statements)。C# 支持多种不同的语句,其中一些语句定义成了嵌入式语句。
区块(block)允许将多条语句写在一个被大括号 {...}
包含起来的上下文之中,且性如单条语句般。
声明语句(declaration statements)用于局部变量与常量的声明。
表达式语句(expression statements)用于计算表达式,比如方法调用、用 new
操作符来创建对象、用 =
来赋值、用 ++
和 --
来执行自增或自减操作以及 await
表达式等。
选择语句(selection statements)用于从一组基于表达式计算而出的值中选择其一,选择语句有 if
和 switch
语句。
迭代语句(iteration statements)用于反复执行其内嵌语句,迭代语句有:while
、do
、for
以及 foreach
语句。
跳转语句(jump statements)用于转移控制,跳转语句有:break
、continue
、goto
、throw
、return
以及 yield
语句。
try...catch
语句用于代码块执行期间捕获异常,try...finally
语句用于指明无论在执行期间是否发生异常,终结器(finalization)代码始终会被执行。
check
和 uncheck
表达式用于控制整形(integral-type)在算术计算和转换期间的一出检验上下文(overflow checking context)。
lock
语句用于为所给定的对象获得互斥锁(mutual-exclusion lock)、执行其内语句并释放该锁定。
using
语句用于获得资源、执行其内语句并释放资源。
下表展示了 C# 的各种语句并为其举例。
类与对象
类(class)是最基本的 C# 类型,是结合了状态(字段)和动作(方法及其它函数成员)于一体的数据结构。类提供了一种动态构建类实例(也称对象 object)的定义,类提供了继承(inheritance)和多态(polymorphism),可以通过派生类(derived classes)来扩展和具体化基类(base classes)。
新类通过类的定义创建,类定义始于类头(header),它由指定的属性、类的修饰符、类名、基类(如果有的话)以及所实现的接口组成。紧随其后的是类的实际部分(body),由一组写于大括号 {...}
之间的成员声明组成。
下面是 Point 类的定义:
public class Point{ public int x, y; public Point(int x, int y) { this.x = x; this.y = y; }}
用 new
操作符创建实例,这个实例将被分配相应的内存空间,调用构造函数初始化实例并返回一个引用实例的内存地址。下面的语句是创建两个 Point 对象并将其(引用的内存地址)赋予两个变量:
Point p1 = new Point(0, 0);Point p2 = new Point(10, 20);
当对象不再被使用,其所占用的内存将会被自动回收,因此不需要去手工销毁对象。
成员
类的成员可以是静态成员(static members),也可以是实例成员(instance members)。静态成员属于类,实例成员属于对象(类的实例)。下表概括了类成员的类别
访问控制修饰符
类成员的访问控制修饰符控制着程序代码区块对其访问的权限。下表对五种访问控制修饰符做了简介。
类型参数
在定义类时,可为其指定一组类型参数,以尖括号 <...>
包裹并紧随类名之后。尖括号内的类型能用在类成员定义时的声明中。在下例中,我们使用了一对(Pair)类型参数 TFirst
和 TSecond
:
public class Pair<TFirst,TSecond>{ public TFirst First; public TSecond Second;}
声明了类型形参的类(class)被称为泛型类(generic class type)。结构、接口和委托都可使用泛型。在使用泛型时,类型实参(type arguments)必须使用类型形参中所提供的类型,比如:
Pair<int,string> pair = new Pair<int,string> { First = 1, Second = “two” };int i = pair.First; // TFirst is intstring s = pair.Second; // TSecond is string
如上例 Pair<int, string>
般泛型类型实参名曰构造类型(constructed type)。
基类 Base classes
在定义类时,可为其指定一个基类,只要将基类名紧跟在类名与类型形参(type parameters)之后并以冒号 :
分割。如果省略了基类,那么类会直接派生自 System.Object
对象类。在下例中,Point3D
的基类是 Point
,而 Point
的基类是 object
:
public class Point{ public int x, y; public Point(int x, int y) { this.x = x; this.y = y; }}public class Point3D: Point{ public int z; public Point3D(int x, int y, int z): base(x, y) { this.z = z; }}
类可以继承其基类的方法。继承,意味着它隐式地包含了所有其基类的成员,除了实例构造函数、静态构造函数外和基类的析构函数(destructors)。派生类可以在继承自基类的成员中添加新的成员,但不能移除它们。前例中 Point3D
继承了 Point
类的 x
和 y
字段,Point3D
实例包含了三个字段:x
、y
和 z
。
一个类能隐式地转换为它的任意基类。因此一个声明为某类实例的变量可以引用这个类的所有派生类。如先前所声明的类,类型为 Point
的变量可以引用 Point
和 Point3D
的实例:
Point a = new Point(10, 20);Point b = new Point3D(10, 20, 30);
字段 Fields
字段是与类或其实例相关的变量。
使用静态修饰符 static
定义的字段称作静态字段(static field)。静态字段指向一个(唯一一个)存储空间,不管类被创建了多少个实例,静态字段的副本有且仅有一个。
如果没有使用静态修饰符来声明这个字段,那么这个字段叫做实例字段(instance field)。不同的类实例中的实例字段相互隔离。
在下例中,每一个 Color
类的实例都拥有相互隔离的实例字段 r、g、b;但对于 Black、White、Red、Green 和 Blue 这类静态字段来说,他们只有一份副本。
public class Color{ public static readonly Color Black = new Color(0, 0, 0); public static readonly Color White = new Color(255, 255, 255); public static readonly Color Red = new Color(255, 0, 0); public static readonly Color Green = new Color(0, 255, 0); public static readonly Color Blue = new Color(0, 0, 255); private byte r, g, b; public Color(byte r, byte g, byte b) { this.r = r; this.g = g; this.b = b; }}
如上所示,只读(read-only)字段在定义时使用 readonly
修饰符。在一个类中,只读的字段只能被赋值一次,要么在声明字段的时候,要么在构造的时候。
方法 Methods
方法是实现运算或执行动作的类成员。静态方法(static methods)直接通过类来调用,实例方法(instance methods)则通过实例类来调用。
方法的参数列表(list of parameters)用于向方法内部传递值或变量引用,参数列表可为空;方法的返回类型(return type)用于指定计算完的结果的类型。当方法返回类型为 void
时表示它不返回一个值。
类似于类型,方法也可以拥有一组类型形参,只要在调用的时候明确指明类型实参的值即可。但也有与类型不一样的地方,比方说类型实参可以由参数推断而不必去明确指定。
在类的内部,方法签名(signature)必须唯一。方法签名由方法名、类型形参及其数量、修饰符和参数列表类型组成。类型签名不包含返回值类型。
形参 Parameters
形参用于向方法内部传递值或变量引用,方法被执行时再从指定的实参(arguments)[19]中获得实际的值。形参有四种类型:值形参、引用形参、输出形参以及形参数组。
值形参(value parameter)用于向方法内输入参数,相当于是个在内部声明的局部变量,修改值形参不会影响已传入方法内部的实参。值形参是可选的,可以具体指明一个默认值,这样当我们实参省略时,可以使用该默认值。
引用参数(reference parameter)用于向内、向外传递参数。为引用参数传递的实参必须是变量,且在方法执行期间,引用参数意味着参数变量的存储位置是一样的。引用参数声明的时候带有 ref
修饰符。在下例中我们演示了如何使用 ref
参数。
using System;class Test{ static void Swap(ref int x, ref int y) { int temp = x; x = y; y = temp; } static void Main() { int i = 1, j = 2; Swap(ref i, ref j); Console.WriteLine("{0} {1}", i, j); // Outputs "2 1" }}
输出参数(output parameter)用于向外传递参数。输出参数类似之前介绍的引用参数,只不过无所谓参数调用的值的初始化。输出参数使用 out 修饰符来声明。下例我们演示了如何使用 out
参数。
using System;class Test{ static void Divide(int x, int y, out int result, out int remainder) { result = x / y; remainder = x % y; } static void Main() { int res, rem; Divide(10, 3, out res, out rem); Console.WriteLine("{0} {1}", res, rem); // Outputs "3 1" }}
参数数组(parameter array)允许数量可变的参数传递给方法。参数数组使用 param
修饰符来声明的。不过参数数组只可用于方法的最后一个参数,并且参数数组必须是一个一维数组。举一个比较适合的例子,System.Console 类中的 Write
和 WriteLine
方法就是用了参数数组,它们是如下声明的:
public class Console{ public static void Write(string fmt, params object[] args) {...} public static void WriteLine(string fmt, params object[] args) {...} ...}
当一个方法使用了参数数组,参数数组的行为完全类似一个常规数组类型的参数。然而,在调用具有参数数组的方法时,参数数组可能传递只有单个参数的参数数组到方法内,也可能传递多个参数的参数数组到方法内。对于后者来说,数组实力将会自动创建并完成给定参数的初始化工作。比方说:
Console.WriteLine("x={0} y={1} z={2}", x, y, z);
上面这段代码等价于:
string s = "x={0} y={1} z={2}";object[] args = new object[3];args[0] = x;args[1] = y;args[2] = z;Console.WriteLine(s, args);
方法体与局部变量
调用方法时实际执行的是方法体内部的那些语句。启用方法后,方法体内部所声明的那些变量被叫做局部变量(local variables,也称本地变量)。声明局部变量时要指定它的类型、变量名,也可以为其初始化一个值。在下面这个例子中,我们声明了一个名叫 i
的局部变量并初始化为 0
,又声明了一个局部变量 j
但没给它初始化:
using System;class Squares{ static void Main() { int i = 0; int j; while (i < 10) { j = i * i; Console.WriteLine("{0} x {0} = {1}", i, j); i = i + 1; } }}
C# 要求一个局部变量在获取其值之前被明确赋值(definitely assigned)。例如,之前那个例子中没有明确地给 i 初始化一个值,那么 i 在被后续使用时编译器会报告一个错误(error),因为在那个程序点 i 并没有被明确赋值。
方法可以使用 return
语句以便将控制权返回给其调用者。如果方法返回 void
,那么 return
语句不能指定一个表达式。在一个不返回 void 的方法(non-void)中,返回语句必须包含一个表达式,以便计算其返回值。
静态方法和实例方法
带有 static
修饰符的方法声明被叫做静态方法(static method)。静态方法不会对一个特定的实例进行操作,只能直接使用静态成员。
声明方法时不带 static 修饰符的叫做实例方法(instance method)。实例方法将操作一个特定的实例,同时它可以访问类的静态成员和实例成员。当一个实例方法被调用的时候,可以显式使用 this
来获取这个实例方法所在的实例类。但如果是一个静态方法,那么将报错。
下例 Entity 类既有静态成员,也有实例成员。
class Entity{ static int nextSerialNo; int serialNo; public Entity() { serialNo = nextSerialNo++; } public int GetSerialNo() { return serialNo; } public static int GetNextSerialNo() { return nextSerialNo; } public static void SetNextSerialNo(int value) { nextSerialNo = value; }}
每一个 Entity 实例都包含了“序列号(serial number)”[20]。Entity 的构造函数(长得比较像实例方法)用下一个可用的序列号初始化一个新的实例。因为构造函数是一个实例成员,所以它被允许访问实例字段 serialNo
和静态字段 nextSerialNo
。
静态方法 GetNextSerialNo
和 SetNextSerialNo
能访问静态字段 nextSerialNo
,但不能访问实例字段 serialNo
,否则会报错。
下例中,我们展示了如何使用 Entity 类:
using System;class Test{ static void Main() { Entity.SetNextSerialNo(1000); Entity e1 = new Entity(); Entity e2 = new Entity(); Console.WriteLine(e1.GetSerialNo()); // Outputs "1000" Console.WriteLine(e2.GetSerialNo()); // Outputs "1001" Console.WriteLine(Entity.GetNextSerialNo()); // Outputs "1002" }}
注意,静态方法 SetNextSerialNo
和 GetNextSerialNo
在类上调用,而实例方法 GetSerialNo
在类的实例上被调用。
虚、重写与抽象方法
当一个实例方法声明的时候包含了 virtual
修饰符,那么这个方法就叫做虚方法(virtual method),反之则叫做非虚方法(non-virtual method)。
当虚方法被调用,该调用的实例的运行时(run-time)将确定哪个实际的方法实现用于调用。对于一个非虚方法的调用来说,编译时(compile-time)就已确定调用谁了。
虚方法能被派生类(derived class)重写(override)。当方法声明时带有 override
修饰符,这个方法将重写它继承来的具有相同签名的基类虚方法。虚方法的声明意味着出现了一个新方法,而派生类内的重写方法则是对其进行了细致化,为其提供了新的实现。
抽象方法(abstract method)相当于不带实现的虚方法。抽象方法声明的时候使用 abstract
修饰符,而且只能在抽象类中声明抽象方法。抽象方法必须在抽象类的非抽象派生类中重写。
在下例中,我们定义了一个抽象类,名叫 Expression,它表示一个表达式树的节点,并有三个派生类:Constant
、VariableReference
和 Operation
。三个派生类分别实现了表达式树为常数(constants)、变量引用(variable references)和算术运算(arithmetic operations)[21]。
using System;using System.Collections;public abstract class Expression{ public abstract double Evaluate(Hashtable vars);}public class Constant: Expression{ double value; public Constant(double value) { this.value = value; } public override double Evaluate(Hashtable vars) { return value; }}public class VariableReference: Expression{ string name; public VariableReference(string name) { this.name = name; } public override double Evaluate(Hashtable vars) { object value = vars[name]; if (value == null) { throw new Exception("Unknown variable: " + name); } return Convert.ToDouble(value); }}public class Operation: Expression{ Expression left; char op; Expression right; public Operation(Expression left, char op, Expression right) { this.left = left; this.op = op; this.right = right; } public override double Evaluate(Hashtable vars) { double x = left.Evaluate(vars); double y = right.Evaluate(vars); switch (op) { case '+': return x + y; case '-': return x - y; case '*': return x * y; case '/': return x / y; } throw new Exception("Unknown operator"); }}
上面这四个类可用于算数表达式,例如用这几个类的实例来计算 x + 3
的话,可以写成这样:
Expression e = new Operation( new VariableReference("x"), '+', new Constant(3));
这个表达式实例的 Evaluate
方法被调用,用来计算一个给定的表达式并返回一个 double 类型的结果。包含了变量名的哈希表(变量名作为 Hashtabl
e 的键名)作为一个参数传入方法。Evaluate 方法是一个抽象方法,所以 Expression 的非抽象派生类必须重写之以便提供一个确切的实现。
Constant 实现类中的 Evaluate 方法知识返回一个相同的常数值。VariableReference 实现类中的 Evaluate 方法慧聪哈希表中寻找键命为指定变量名的值,并将其值返回。Operation 实现类首先计算左右操作数(通过递归的方式调用它们的 Evaluate 方法),然后执行给定的算数运算。
下面程序使用了 Expression 类去计算表达式 x*(y+2)
:
using System;using System.Collections;class Test{ static void Main() { Expression e = new Operation( new VariableReference("x"), '*', new Operation( new VariableReference("y"), '+', new Constant(2) ) ); Hashtable vars = new Hashtable(); vars["x"] = 3; vars["y"] = 5; Console.WriteLine(e.Evaluate(vars)); // Outputs "21" vars["x"] = 1.5; vars["y"] = 9; Console.WriteLine(e.Evaluate(vars)); // Outputs "16.5" }}
方法重载
方法重载允许多个使用相同名字的方法在同一个类之中,只要他们的签名是唯一的即可。当编译重载方法的调用时,编译器会使用重载策略来确定到底调用哪一个方法。重载策略(overload resolution)用于寻找第一个最佳的匹配参数的方法,如果没有匹配到最佳方法时将报错。在下例中我们展示了重载策略的效果。在 Main 方法中每一个调用都有相应的注释,显示了我们到底调用了哪个重载方法。
class Test{ static void F(){ Console.WriteLine("F()"); } static void F(object x) { Console.WriteLine("F(object)"); } static void F(int x) { Console.WriteLine("F(int)"); } static void F(double x) { Console.WriteLine("F(double)"); } static void F<T>(T x) { Console.WriteLine("F<T>(T)"); } static void F(double x, double y) { Console.WriteLine("F(double, double)"); } static void Main() { F(); // Invokes F() F(1); // Invokes F(int) F(1.0); // Invokes F(double) F("abc"); // Invokes F(object) F((double)1); // Invokes F(double) F((object)1); // Invokes F(object) F<int>(1); // Invokes F<T>(T) F(1, 1); // Invokes F(double, double) }}
如上例所示,一个特定的方法总可以选择通过显式参数强制转换为精确的参数类型和/或显式提供类型参数。
其它函数成员
包括可执行代码在内的成员被统称为类的成员函数(function members)。前面一节描述的方法(describes)是函数成员的主要成员。在本节中,我们将介绍其他几种函数成员:构造函数(constructors)、属性(properties)、索引器(indexers)、事件(events)、操作符(operators)以及析构函数(destructors)。
在下表中,我们列举了可增长列表泛型类 List<T>。这张表包含了不少例子,包含了大多数种类的函数成员。
构造函数 Constructors
C# 支持实例构造函数和静态构造函数。实例构造函数(instance constructor)用于执行类实例初始化操作,静态构造函数(static constructor)用于执行类自身初始化操作(只有在第一次加载时才执行)。
构造函数定义得很像一个方法,不过没有返回类型,并且名称与类名一致。如果构造函数用 static
修饰符定义,那么它叫做静态构造函数;相反,不带 static 修饰符的则是实例构造函数。
实例构造函数可以被重载(overloaded),比方说 List<T> 类声明了两个实例构造函数,一个没有参数,另一个包括一个 int 参数。实例构造函数通过 new 操作符来调用。下面两句语句将分别使用 List<string> 的两个实例构造函数分配实例:
List<string> list1 = new List<string>();List<string> list2 = new List<string>(10);
不似其它成员,实例构造函数不能被继承。没有声明实例构造函数的类不同于那些声明了实例构造函数的类,如果类没有声明实例构造函数,那么一个没有参数的空实例构造函数将会自动创建。
属性 Properties
属性是字段的自然扩展。都在名称前有相关类型,并且访问字段和属性的语法也是一样的。然而不同之处在于属性不表示存储位置。相反,属性由访问器(accessors)来指定读写其值时具体执行的语句。
属性声明酷似字段,唯声明结尾使用了 get
访问器和/或 set
访问器耳。get/set 访问器被写于一对大括号 {...}
而且不以分号 ;
结尾。属性的读写依靠 get/set 访问器。如果既有 get 访问器又有 set 访问器,那么这个属性就是可读写的(read-write property);属性只有 get 访问器则为只读(read-only property),属性只有 set 访问器则为只写(write-only property)。
get
访问器相当于一个返回指定类型(属性的类型)的无参方法。除了被用来赋值,当属性引用一个表达式时,属性的 get 访问器在被调用时会计算这个表达式的值。
set
访问器相当于一个没有返回类型的有一个名为 value
的参数的方法。当属性被赋值,或者被 ++
或 --
操作时,set 访问器会带着一个含有新值的参数再被调用。
List<T> 声明了两个属性,Count
和 Capacity
,前者只读(read-only),后者可读写(read-write)。下例对它们的用法做了示范:
List<string> names = new List<string>();names.Capacity = 100; // Invokes set accessorint i = names.Count; // Invokes get accessorint j = names.Capacity; // Invokes get accessor
与字段和方法类似,C# 支持实例属性和静态属性。静态属性声明的时候带有 static 修饰符,而实例属性则不带这个修饰符。
属性访问器(accessor)可以是虚的(virtual)。当一个属性声明时带着虚(virtual)、抽象(abstract)或重写(override)修饰符时,它也将应用于属性的访问器。
索引器 Indexers
索引器拥有可以让对象拥有像数组那样索引其值的能力。索引器定义类似属性,不过它的参数名写在一对方括号 [...]
之间。访问器内的参数可以被索引器使用。和属性一样,索引器也能被修饰为可读写(read-write)、只读(read-only)或只写(write-only),同时索引的访问器也可以是虚的(virtual)。
List 类定义了一个可读写的索引器,它接受一个 int 参数。索引器可以利用一个 int 值来索引 List 的实例,如下例所示:
List<string> names = new List<string>();names.Add("Liz");names.Add("Martha");names.Add("Beth");for (int i = 0; i < names.Count; i++) { string s = names[i]; names[i] = s.ToUpper();}
索引器能被重载(overloaded),这意味着一个类可以拥有多个索引器,只要它们的参数的数量或类型不同即可。
事件 Events
事件使得类或对象能够发出通知(notifications),事件声明的时候有点儿像字段,不过事件声明时包含一个事件关键词,而且必须是委托类型。
在类中声明的事件成员的行为类似委托类型的字段(非抽象且未声明访问器)。字段存储了对委托的引用,这意味着事件处理器(event handlers)已被添加到事件中。如果目前没有事件处理器,字段为空(null)。
类 List<T> 声明了一个 Changed 事件成员,当一个新项被加入到列表中时,它将被触发。Changed
事件由虚方法(virtual method) OnChanged
引发(raising),在引发之前,它首先会检查事件是否为空(意味着目前还不存在事件处理器)。引发(raising)事件的概念完全等效于调用委托所表示的事件,因此没有特别的语言构造用于引发事件。
客户端通过事件处理器(event handlers)对事件作出反应。事件处理器通过 += 操作符添加、通过 -= 操作符移除。下例介绍了给 List<string> 的 Changed 事件添加一个事件处理器的方法:
using System;class Test{ static int changeCount; static void ListChanged(object sender, EventArgs e) { changeCount++; } static void Main() { List<string> names = new List<string>(); names.Changed += new EventHandler(ListChanged); names.Add("Liz"); names.Add("Martha"); names.Add("Beth"); Console.WriteLine(changeCount); // Outputs "3" }}
对于那些更高级的底层存储控制方案,事件声明可以显式地提供 add
和 remove
访问器,这一点有点儿像属性的 set 访问器。
操作符 Operators
操作符是类实例的成员,它定义了特定表达式运算符应用于一个类实例的含义。我们可以定义三种操作符:一元操作符(unary operators)、二元操作符(binary operators)和转换操作符(conversion operators)。所有操作符都必须声明为 public 和 static。
具体来讲,操作符定义了对两个 List<T> 实例的逐个成员使用其 Equals
方法进行了比较。下例中,我们使用了操作符 == 来比较两个 List<int> 的实例:
using System;class Test{ static void Main() { List<int> a = new List<int>(); a.Add(1); a.Add(2); List<int> b = new List<int>(); b.Add(1); b.Add(2); Console.WriteLine(a == b); // Outputs "True" b.Add(3); Console.WriteLine(a == b); // Outputs "False" }}
第一个 Console.WriteLine
输出了 True,这是因为两个列表都包含了相同数量、相同值、相同顺序的对象。由于 List<T> 没有定义 == 操作符,所以第二个 Console.WriteLine
将输出 False,因为 a 和 b 引用的是不同的 List<int> 实例[22]。
析构函数 Destructors
析构函数用于类实例的销毁。析构函数不能包含参数、访问控制修饰符,且不可被显式调用。在垃圾回收(garbage collection)的时候,类实例的析构函数会被自动调用。
垃圾回收机制被许可自行决定何时回收对象并运行析构函数。具体来说,析构函数的调用时机是不确定的,同时析构函数可在任何线程上执行。基于此(及其它更多原因),当别无其他解决方案时,类应该自己实现析构函数。
使用 using 语句是个更好的实践,using 语句结束后将调用对象的析构函数。
结构 Structs
如类那般,结构也是一种能够包含数据成员和功能成员的数据结构,但结构是值类型,并且数据不会放入堆。结构变量直接存储结构的数据,而类的变量则将数据存在一个动态对象中。结构不支持使用者指定继承,所有结构都隐式继承自 object
类型。
结构(structs)特别适用于拥有值含义的小型数据结构(small data structures)。复数(complex numbers)、坐标(points in a coordinate system)或字典中的键值对(key-value pairs)都适合使用结构。对小型数据结构来说,结构比类更适合,这是因为结构能森请分配一大块不同的内存用于应用程序的执行。举例来说,在下面的程序中,我们创建并初始化了一个含有 100 个 Point 的数组。Point 的实现基于类,101 个相互独立的对象被实例化——其中数组一个,剩下一百个属于数组的元素。
class Point{ public int x, y; public Point(int x, int y) { this.x = x; this.y = y; }}class Test{ static void Main() { Point[] points = new Point[100]; for (int i = 0; i < 100; i++) points[i] = new Point(i, i); }}
另一个方法是将 Point 变为一个结构(struct):
struct Point{ public int x, y; public Point(int x, int y) { this.x = x; this.y = y; }}
如此一来,只有一个对象被实例化——数组——其余的 Point 实现都被存储在数组中。
结构构造函数在使用 new 操作符时被调用,但这并不意味着内存已被分配。与「动态分配一个对象并返回其内存地址用于变量引用」不同,结构的构造函数只不过简单地返回结构自身的值(通常位于堆栈的临时位置),而且这个值在必要时将会被拷贝。
对于类来说,它可能会出现两个变量指向同一个对象甚至操作其中一个变量引用的对象会影响到另一个变量。但对于结构而言,每一个变量都拥有它们自己的数据副本,因此不会出现操作其一后影响其它变量的情况。例如下面的代码片段,对于结构和类将产生不同的结果:
Point a = new Point(10, 10);Point b = a;a.x = 20;Console.WriteLine(b.x);
如果 Point 是一个类,那么它将输出 20,因为 a 和 b 引用了同一个对象;如果 Point 是一个结构,那么将输出 10,因为 a 向 b 赋值等同于创建了一个数据副本,对 a.x
进行操作后不会影响到 b 中的数据。
在前例中有两点需要注意:
- 整个复制一个结构的效率要比复制一个引用对象低,所以相对于引用类型,结构的赋值和参数传递的成本可能会更高一些;
- 除了
ref
和out
参数,结构不可被引用,这种用法被大多数场景所排除。
数组 Arrays
数组是包含了一组变量的、通过计算索引来获得值的数据结构。数组中的变量也被称为数组元素(elements),它们必须是相同类型的,这个类型被叫做数组元素类型(element type)。
数组类型是引用类型,数组变量的声明是为了留出相应的内存空间以便对数组实例进行引用。真正的数组实例用 new 操作符动态创建于运行时(run-time)。new 操作符指定了新的数组实例的长度(length),这个长度在数组实例的生命周期期间是固定的。数组元素索引的范围从 0 到元素总数减一位(Length - 1)。new 操作符自动初始化数组元素为其默认值(比如所有数值类型的默认值为 0,引用类型的默认值为 null)。
在下例中,我们创建了一个包含一组 int 类型元素的数组,对其初始化并打印其内容:
using System;class Test{ static void Main() { int[] a = new int[10]; for (int i = 0; i < a.Length; i++) { a[i] = i * i; } for (int i = 0; i < a.Length; i++) { Console.WriteLine("a[{0}] = {1}", i, a[i]); } }}
这个例子创建并操作了一维数组(single-dimensional array)。C# 同样支持多维数组(multi-dimensional arrays)。维度的数值以及数组中类型的排序(rank)都可通过一个被包裹于一对方括号 [...]
内的逗号 ,
得知。在下例中定义了一个一维数组、一个两维数组和一个三维数组。
int[] a1 = new int[10];int[,] a2 = new int[10, 5];int[,,] a3 = new int[10, 5, 2];
数组 a1 包含了 10 个元素,数组 a2 包含了 50 个(10 × 5)元素,数组 a3 包含了 100 个(10 × 5 × 2)元素。
数组的元素类型可以是任何类型,甚至可以是一个数组类型。包含了一个数组类型的元素的数组有时被称为交错数组(jagged array),因为其数组元素的长度是不必一致的。在下例中,我们创建了一个 int 数组:
int[][] a = new int[3][];a[0] = new int[10];a[1] = new int[5];a[2] = new int[20];
代码第一行创建了有三个元素的数组,每个 int[] 的值都初始化为 null。随后几行对其进行初始化,三个元素指向了三个不同长度的数组实例。
new 操作符可以用指定的数组初始化器为数组进行初始化,数组初始化器由一组写于大括号 {...}
内的表达式组成。下例中,我们创建并用三个元素对 int[] 进行了初始化:
int[] a = new int[] {1, 2, 3};
注意,数组的长度是从包裹于大括号 {...}
的表达式中来推断而来的。本地变量和字段声明可以进行缩写,不必重复声明数组类型。
int[] a = {1, 2, 3};
上面两例等同于下面这个例子:
int[] t = new int[3];t[0] = 1;t[1] = 2;t[2] = 3;int[] a = t;
接口 Interfaces
接口定义了可被类与结构实现的一组约定,可以包含方法、属性、事件和索引器。类不提供其所定义的方法的实现,它仅仅明确了类或结构必须提供的成员。
接口能够继承多个接口(multiple inheritance),在下例中,一个叫 IComboBox
的接口继承了两个接口:ITextBox
和 IListBox
。
interface IControl{ void Paint();}interface ITextBox: IControl{ void SetText(string text);}interface IListBox: IControl{ void SetItems(string[] items);}interface IComboBox: ITextBox, IListBox {}
类和结构能实现多个接口,在下例中,类 EditBox 继承了两个接口:IControl 和 IDataBound。
interface IDataBound{ void Bind(Binder b);}public class EditBox: IControl, IDataBound{ public void Paint() {...} public void Bind(Binder b) {...}}
当类或结构实现一个指定的接口时,其实例可隐式转为接口类型,例如:
EditBox editBox = new EditBox();IControl control = editBox;IDataBound dataBound = editBox;
非静态类的实例能动态类型转换(dynamic type casts)为一个特定的接口。在下面这个例子中,obj 能动态转换并获得一个对象的 IControl 接口实例和 IDataBound 接口实例,因为对象的真实类型是 EditBox,所以转换成功。
object obj = new EditBox();IControl control = (IControl)obj;IDataBound dataBound = (IDataBound)obj;
在前面这个 EditBox 类中,来自于 IControl 接口的Paint 方法和来自于 IDataBound 接口的 Bind 方法的实现都使用了 public 成员。C# 也提供了显式的接口成员实现方案,使得类或结构避免了将方法暴露在外部。显式的接口乘员实现需要写出完全限定的接口乘员名,例如 EditBox 类可以显式地通过实现 IControl.Painr 方法和 IDataBound.Bind 方法,如下例所示:
public class EditBox: IControl, IDataBound{ void IControl.Paint() {...} void IDataBound.Bind(Binder b) {...}}
显式的接口成员只能通过接口类型访问到。比方说,由前例中 EditBox 类所提供的 IControl.Paint 实现只能在首先转为 IControl 接口类型后才能被调用 :
EditBox editBox = new EditBox();editBox.Paint(); // Error, no such methodIControl control = editBox;control.Paint(); // Ok
枚举 Enums
枚举类型是一种明确了值类型的具名常量集合。在下例中,我们声明了一个 Color 枚举,它拥有三个枚举常量值:Red、Green 和 Blue。
using System;enum Color{ Red, Green, Blue}class Test{ static void PrintColor(Color color) { switch (color) { case Color.Red: Console.WriteLine("Red"); break; case Color.Green: Console.WriteLine("Green"); break; case Color.Blue: Console.WriteLine("Blue"); break; default: Console.WriteLine("Unknown color"); break; } } static void Main() { Color c = Color.Red; PrintColor(c); PrintColor(Color.Blue); }}
每一个枚举类型都对应一个整形类型,叫做枚举类型的基础类型(underlying type)。如果枚举类型没有显式地声明其基础类型,那么它的基础类型将是 int 型。枚举类型的存储格式与值范围由其基础类型所决定。枚举之值集可不受其成员所限。特别指出的是,任意一个与枚举的基础类型同类型的值都可被转为枚举类型,并且是一个明确的有效的枚举值。
在下例中,我们声明了一个叫做 Alignment 的枚举,并指明其基础类型为 sbyte
。
enum Alignment: sbyte{ Left = -1, Center = 0, Right = 1}
如上所示,枚举值声明时使用了常量表达式,并为其指明了一个值。这些为枚举成员指定的值都必须为枚举声明时指定的那个基础类型(此处是 sbyte
)。如果在声明枚举的时候不为其指定一个值,那么枚举值默认从 0 开始(如果是第一个枚举成员)或在前一个枚举值的基础上加一(如果不是第一个枚举成员)。
枚举值可以转为整形,或从整形转为枚举值:
int i = (int)Color.Blue; // int i = 2;Color c = (Color)2; // Color c = Color.Blue;
所有枚举的默认值都可以用整形数字 0 来转换,在初始化的时候系统会自动为枚举设置默认值。为了让枚举的值能被轻易获得,字面量 0
能被隐式地转为任意枚举类型。故,我们可以这么写:
Color c = 0;
委托 Delegates
委托类型意味着对应一个带有指定参数列表和返回值的方法。委托能把方法像实体那样去处理,比如赋给一个变量或是当作参数传递。委托类似于其他编程语言中的方法指针,但和方法指针不同的在于委托是面向对象且类型安全的。
在下面这个例子中,我们声明并使用了一个名叫 Function
的委托类型。
using System;delegate double Function(double x);class Multiplier{ double factor; public Multiplier(double factor) { this.factor = factor; } public double Multiply(double x) { return x * factor; }}class Test{ static double Square(double x) { return x * x; } static double[] Apply(double[] a, Function f) { double[] result = new double[a.Length]; for (int i = 0; i < a.Length; i++) result[i] = f(a[i]); return result; } static void Main() { double[] a = {0.0, 0.5, 1.0}; double[] squares = Apply(a, Square); double[] sines = Apply(a, Math.Sin); Multiplier m = new Multiplier(2.0); double[] doubles = Apply(a, m.Multiply); }}
这个函数委托类型的实例能引用任意一个带 double 类型参数并返回 double 类型返回值的方法。Apply 方法为 double[]
元素提供了一个 Function,并返回一个 double[]
类型的结果。在 Mian 方法中 Apply 为 double[]
提供了三个不同的功能。
委托能引用(指向)一个静态方法(比如说上例中的 Square
或 Math.Sin
)或一个实例方法(比方说上例中的 m.Multiply
)。指向一个实例方法的委托同样指向于一个指定的对象,当实例对象被委托调用时彼对象可在调用中变为 this
。
委托同样可以由内联方法(inline methods)式的匿名函数来动态创建。匿名函数能够使用环境方法中的本地变量。因此在下面的例子中可以非常简单地实现乘法计算,而不必使用 Multiplier 类:
double[] doubles = Apply(a, (double x) => x * 2.0);
委托的另一个有用属性是它不需要知道或不需要关心它所指向的那个方法所在的类。只要那个方法与委托拥有相同的参数类型和返回类型即可。
特性 Attributes
在 C# 之中,类型、成员以及其他实体都支持控制其某些行为特征的修饰符,比方说用 public
、protected
、internal
和 private
来修饰方法的访问控制。在运行时中,C# 以可用户自定义的可附加于程序入口的信息声明来总结这种能力。程序通过定义和使用特性(attributes)来指定这些额外的信息声明。
下面的例子里,我们声明了一个 HelpAttribute
特性,把它放在程序实体之前,用来指定相关帮助文档的链接地址。
using System;public class HelpAttribute: Attribute{ string url; string topic; public HelpAttribute(string url) { this.url = url; } public string Url { get { return url; } } public string Topic { get { return topic; } set { topic = value; } }}
所有的特性类都派生自 .NET Framework 提供的 System.Attribute
基类。使用特性是会后,把特性的名称、一些实参放进一组方括号 [...]
内并放在相关声明之前。当特性的名字以 Attribute
结尾,那么在使用特性的时候可以省略这个单词。比方说,HelpAttribute
特性在实际使用中可以写成下面这个样子:
[Help("http://msdn.microsoft.com/.../MyClass.htm")]public class Widget{ [Help("http://msdn.microsoft.com/.../MyClass.htm", Topic = "Display")] public void Display(string text) {}}
上面这个例子中,Widget 类上附加了一个 HelpAttribute
特性,Display 方法上附加了另一个 HelpAttribute
特性。特性依附于程序入口时须供其公开构造函数以信息。另外,额外的信息可以通过特性类的读写属性来传入(就像前面例子里的那个 Topic
属性)。
下例展示了特性信息在一个给定的程序入口是如何在运行时(run-time)环境中被反射取出的。
using System;using System.Reflection;class Test{ static void ShowHelp(MemberInfo member) { HelpAttribute a = Attribute.GetCustomAttribute(member, typeof(HelpAttribute)) as HelpAttribute; if (a == null) { Console.WriteLine("No help for {0}", member); } else { Console.WriteLine("Help for {0}:", member); Console.WriteLine(" Url={0}, Topic={1}", a.Url, a.Topic); } } static void Main() { ShowHelp(typeof(Widget)); ShowHelp(typeof(Widget).GetMethod("Display")); }}
当一个特性通过反射(reflection)被请求时,特性类的构造函数将带着给定的信息启动并返回一个处理完毕后的特性实例结果。如果通过其属性提供更多的信息,那么这些额外的信息将在返回特性实例结果之前设置到特性类内。
[1] ECMA 国际:是一家国际性会员制度的信息和电信标准组织。1994 年之前,名为欧洲计算机制造商协会(European Computer Manufacturers Association)。因为计算机的国际化,组织的标准牵涉到很多其他国家,因此组织决定改名表明其国际性。现名称已不属于首字母缩略字
[2] ECMA-334 规范:该国际标准建立在惠普、英特尔、微软提交的,描述性的 C# 语言的基础上 ,这是微软内部开发的意见书。这种语言的主要发明者,海杰尔斯伯格(Anders Hejlsberg),Scott Wiltamuth, Peter Golde 。首次广泛分布实施的 C# 是由微软发布于 2000 年 7 月,作为。NET架构的一部分。
[3] 国际标准化组织:简称 ISO(International Organization for Standardization),是一个全球性的非政府组织,是国际标准化领域中一个十分重要的组织。ISO 一来源于希腊语“ISOS”,即“EQUAL”——平等之意。ISO 国际标准组织成立于 1946 年,中国是 ISO 的正式成员,代表中国参加 ISO 的国家机构是中国国家技术监督局(CSBTS)
[4] 国际电工委员会:成立于 1906 年,至 2015 年已有 109 年的历史,是世界上成立最早的国际性电工标准化机构,负责有关电气工程和电子工程领域中的国际标准化工作。国际电工委员会的总部最初位于伦敦,1948 年搬到了位于日内瓦的现总部处。1887-1900 年召开的 6 次国际电工会议上,与会专家一致认为有必要建立一个永久性的国际电工标准化机构,以解决用电安全和电工产品标准化问题。1904 年在美国圣路易召开的国际电工会议上通过了关于建立永久性机构的决议。1906 年 6 月,13 个国家的代表集会伦敦,起草了 IEC 章程和议事规则,正式成立了国际电工委员会。1947 年作为一个电工部门并入国际标准化组织(ISO),1976 年又从 ISO 中分立出来。
[5] 面向组件:是一种软件工程实践,设计时通常要求组件之间高内聚,松耦合。其接口可能是 OO 的,调用方式可能是以 Service 的方式。基于组件开发关注系统层次、子系统边界和子系统间通讯的的设计,处于代码层面但不像OOP的一样是时刻需要运用的东西。
[6] 自包含:组件不依赖其他组件,能够以独立的方式供外部使用,组件重用时不需要包含其他的可重用组件。
[7] 自描述:当前组件包含了自身与其他组件交互相关的描述信息,不需要其他的配置文件或者额外信息来描述。
[8] 鲁棒性:鲁棒是 Robust
的音译,即健壮和强壮之意,它是在异常和危险情况下系统生存的关键。所谓“鲁棒性”,是指控制系统在一定(结构,大小)的参数摄动下,维持其它某些性能的特性。根据对性能的不同定义,可分为稳定鲁棒性和性能鲁棒性。
[9] 编译器:编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)。对于 C#、VB 等高级语言而言,此时编译器完成的功能是把源码(SourceCode)编译成通用中间语言(MSIL/CIL)的字节码(ByteCode)。最后运行的时候通过通用语言运行库的转换,编程最终可以被CPU直接计算的机器码(NativeCode)。
[10] 程序集:经由编译器编译得到的,供 CLR 进一步编译执行的那个中间产物,在 Windows 系统中,它一般表现为 .dll
或者是 .exe
的格式,但是要注意,它们跟普通意义上的WIN32可执行程序是完全不同的东西,程序集必须依靠 CLR 才能顺利执行。
[11] IL:Intermediate Language,.NET 框架中中间语言的缩写。使用中间语言的优点有两点,一是可以实现平台无关性,既与特定 CPU 无关;二是只要把 .NET 框架某种语言编译成IL代码,就实现 .NET 框架中语言之间的交互操作。(《C# 程序设计及应用教程》(第2版),马骏 主编)
[12] 元数据:又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,是关于数据的数据。
[13] CLR:公共语言运行库 Common Language Runtime,和 Java 虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。CLR 存在两种不同的翻译名称:公共语言运行库和公共语言运行时。
[14] Java 要求每一个文件只能包含一个类,并且文件名等于类名。
[15] 面向对象:面向对象(Object Oriented,OO)是软件开发方法。面向对象的概念和应用已超越了程序设计和软件开发,扩展到如数据库系统、交互式界面、应用结构、应用平台、分布式系统、网络管理结构、CAD 技术、人工智能等领域。面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。
[16] 类型安全:类型安全代码指访问被授权可以访问的内存位置,类型安全的代码具备定义良好的数据类型。在实时(JIT)编译期间,可选的验证过程检查要实时编译为本机代码的方法的元数据和Microsoft中间语言(MSIL),以验证它们是否为类型安全,如果代码具有忽略验证的权限,则将跳过此过程。尽管类型安全验证对于运行托管代码不是强制的,但类型安全在程序集隔离和安全性强制中起着至关重要的作用。如果代码是类型安全的,则公共语言运行库可以将程序集彼此间完全隔离。这种隔离有助于确保程序集之间不会产生负面影响,且提高应用程序的可靠性。微软提供的 PEVerify 工具可以被用来确保 CLR 将只执行代码是可验证的类型安全的。
[17] 装箱:值类型实例到对象的转换,它暗示在运行时实例将携带完整的类型信息,并在堆中分配。Microsoft 中间语言 (MSIL) 指令集的 box 指令,通过复制值类型,并将它嵌入到新分配的对象中,将值类型转换为引用类型。
[18] 拆箱:利用装箱和拆箱功能,可通过允许值类型的任何值与Object 类型的值相互转换,将值类型与引用类型链接起来。
[19] 形参(parameters)与实参(arguments):形参全称“形式参数”,是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传递的参数。实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使实参获得确定值。
[20] 即上例中的字段 serialNo
。
[21] 这里这个例子只是随意举出的,不要和《第四章第六节:表达式树类型》弄混淆了。
[22] 此处原文笔误,写成了「the first Console.WriteLine would have...」,译者注明。
修订历史
- 2015/07/07,完稿;
- 2015/07/09,第一次修订。
__EOF__