1. 介绍
C#(读作“See Sharp”)是一种简洁、现代、面向对象且类型安全的编程语言。C# 起源于 C 语言家族,因此,对于 C、C++ 和 Java 程序员,可以很快熟悉这种新的语言。C# 已经分别由 ECMA International 和 ISO/IEC 组织接受并确立了标准,它们分别是 ECMA-334 标准和 ISO/IEC 23270 标准。Microsoft 用于 .NET Framework 的 C# 编译器就是根据这两个标准实现的。
C# 是面向对象的语言,然而 C# 进一步提供了对面向组件 (component-oriented) 编程的支持。现代软件设计日益依赖于自包含和自描述功能包形式的软件组件。这种组件的关键在于,它们通过属性、方法和事件来提供编程模型;它们具有提供了关于组件的声明性信息的特性;同时,它们还编入了自己的文档。C# 提供的语言构造直接支持这些概念,这使得 C# 语言自然而然成为创建和使用软件组件之选。
有助于构造健壮、持久的应用程序的若干 C# 特性:垃圾回收(Garbage collection) 将自动回收不再使用的对象所占用的内存;异常处理 (exception handling) 提供了结构化和可扩展的错误检测和恢复方法;类型安全 (type-safe) 的语言设计则避免了读取未初始化的变量、数组索引超出边界或执行未经检查的类型强制转换等情形。
C# 具有一个同一类型系统 (unified type system)。所有 C# 类型(包括诸如 int 和 double 之类的基元类型)都继承于单个根类型:object。因此,所有类型都共享一组通用操作,并且任何类型的值都能够以一致的方式进行存储、传递和操作。此外,C# 同时支持用户定义的引用类型和值类型,既允许对象的动态分配,也允许轻量结构的内联存储。
为了确保 C# 程序和库能够以兼容的方式逐步演进,C# 的设计中充分强调了版本控制 (versioning)。许多编程语言不太重视这一点,导致采用那些语言编写的程序常常因为其所依赖的库的更新而无法正常工作。C# 的设计在某些方面直接考虑到版本控制的需要,其中包括单独使用的 virtual 和 override 修饰符、方法重载决策规则以及对显式接口成员声明的支持。
本章的其余部分将描述 C#语言的基本特征。尽管后面的章节会更为详尽,有时甚至逻辑缜密地对规则和例外情况进行描述,但本章的描述力求简洁明了,因而难免会牺牲完整性。这样做是为了向读者提供关于该语言的概貌,一方面使读者能尽快上手编写程序,另一方面为阅读后续章节提供指导。
按照约定俗成的惯例,我们先从“Hello,World”程序着手介绍这一编程语言。下面是它的 C# 程序:
using System;
class Hello
{
static void Main() {
Console.WriteLine("Hello,World");
}
}
C# 源文件的扩展名通常是 .cs。假定“Hello, World”程序存储在文件 hello.cs 中,可以使用下面的命令行调用 Microsoft C# 编译器编译这个程序:
csc hello.cs
编译后将生成一个名为 hello.exe 的可执行程序集。当此应用程序运行时,输出结果如下:
Hello, World
“Hello, World”程序的开头是一个 using 指令,它引用了 System 命名空间。命名空间提供了一种分层的方式来组织 C# 程序和库。命名空间中包含有类型及其他命名空间 — 例如,System 命名空间包含若干类型(如此程序中引用的 Console 类)以及若干其他命名空间(如 IO 和 Collections)。如果使用 using 指令引用了某一给定命名空间,就可以通过非限定方式使用作为该命名空间成员的类型。正是由于使用了 using 指令,该程序才可以采用 Console.WriteLine 这一简化形式代替 System.Console.WriteLine。
“Hello, World”程序中声明的 Hello 类只有一个成员,即名为 Main 的方法。Main 方法是使用 static 修饰符声明的。实例方法可以使用关键字 this 来引用特定的封闭对象实例,而静态方法的操作不需要引用特定对象。按照惯例,名为 Main 的静态方法将作为程序的入口点。
该程序的输出由 System命名空间中的 Console 类的 WriteLine 方法产生。此类由 .NET Framework 类库提供,默认情况下,Microsoft C# 编译器自动引用该类库。注意,C# 语言本身没有单独的运行库。事实上,.NET Framework 就是 C# 运行库。
C# 中的组织结构的关键概念是程序 (program)、命名空间 (namespace)、类型 (type)、成员 (member) 和程序集 (assembly)。C# 程序由一个或多个源文件组成。程序中声明类型,类型包含成员,并且可按命名空间进行组织。类和接口就是类型的示例。字段 (field)、方法、属性和事件是成员的示例。在编译 C# 程序时,它们被物理地打包为程序集。程序集通常具有文件扩展名 .exe 或 .dll,具体取决于它们是实现应用程序 (application) 还是实现库 (library)。
下面的示例
using System;
namespaceAcme.Collections
{
public class Stack
{
Entry top;
public void Push(object data) {
top = new Entry(top, data);
}
public object Pop() {
if (top == null) throw newInvalidOperationException();
object result = top.data;
top = top.next;
return result;
}
class Entry
{
public Entry next;
public object data;
public Entry(Entry next, objectdata) {
this.next = next;
this.data = data;
}
}
}
}
在名为 Acme.Collections 的命名空间中声明了一个名为Stack 的类。Acme.Collections.Stack 是此类的完全限定名。该类包含几个成员:一个名为 top 的字段,两个分别名为 Push 和 Pop 的方法和一个名为 Entry 的嵌套类。Entry 类还包含三个成员:一个名为 next 的字段、一个名为 data 的字段和一个构造函数。假定将此示例的源代码存储在文件 acme.cs 中,执行以下命令行:
csc/t:library acme.cs
将此示例编译为一个库(没有Main 入口点的代码),并产生一个名为 acme.dll 的程序集。
程序集包含中间语言(Intermediate Language, IL) 指令形式的可执行代码和元数据 (metadata) 形式的符号信息。在执行程序集之前,.NET 公共语言运行时的实时 (JIT) 编译器将程序集中的 IL 代码自动转换为特定于处理器的代码。
由于程序集是一个自描述的功能单元,它既包含代码又包含元数据,因此,C# 中不需要 #include 指令和头文件。若要在 C# 程序中使用某特定程序集中包含的公共类型和成员,只需在编译程序时引用该程序集即可。例如,下面的程序将使用来自 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());
}
}
如果此程序存储在文件 test.cs中,那么在编译 test.cs 时,可以使用编译器的 /r 选项引用 acme.dll 程序集:
csc /r:acme.dll test.cs
这样将创建名为 test.exe的可执行程序集,运行结果如下:
100
10
1
C# 允许将一个程序的源文本存储在多个源文件中。在编译多个文件组成的 C# 程序时,所有源文件将一起处理,并且源文件可以自由地相互引用 — 从概念上讲,就像是在处理之前将所有源文件合并为一个大文件。C# 中从不需要前向声明,因为除了极少数的例外情况,声明顺序无关紧要。C# 不限制一个源文件只能声明一个公共类型,也不要求源文件的名称与该源文件中声明的类型匹配。
1.3 类型和变量
C# 中的类型有两种:值类型 (value type) 和引用类型 (reference type)。值类型的变量直接包含它们的数据,而引用类型的变量存储对它们的数据的引用,后者称为对象。对于引用类型,两个变量可能引用同一个对象,因此对一个变量的操作可能影响另一个变量所引用的对象。对于值类型,每个变量都有它们自己的数据副本(除 ref 和 out 参数变量外),因此对一个变量的操作不可能影响另一个变量。
C# 的值类型进一步划分为简单类型 (simple type)、枚举类型 (enum type)、结构类型 (struct type) 和可以为 null 的类型 (nullable type),C# 的引用类型进一步划分为类类型 (class type)、接口类型 (interface type)、数组类型 (array type) 和委托类型 (delegate type)。
下表为 C# 类型系统的概述。
类别 |
说明 |
|
值 |
简单类型 |
有符号整型:sbyte、short、int、long |
无符号整型:byte、ushort、uint、ulong |
||
Unicode 字符:char |
||
IEEE 浮点:float、double |
||
高精度小数型:decimal |
||
布尔:bool |
||
枚举类型 |
enum E {...} 形式的用户定义的类型 |
|
结构类型 |
struct S {...} 形式的用户定义的类型 |
|
可以为 null 的类型 |
其他所有具有 null 值的值类型的扩展 |
|
引用 |
类类型 |
所有其他类型的最终基类:object |
Unicode 字符串:string |
||
class C {...} 形式的用户定义的类型 |
||
接口类型 |
interface I {...} 形式的用户定义的类型 |
|
数组类型 |
一维和多维数组,例如 int[] 和 int[,] |
|
委托类型 |
delegate int D(...) 形式的用户定义的类型 |
八种整型类型分别支持 8 位、16 位、32 位和 64 位整数值的有符号和无符号的形式。
两种浮点类型:float 和 double,分别使用 32 位单精度和 64 位双精度的 IEEE 754 格式表示。
decimal 类型是 128 位的数据类型,适合用于财务计算和货币计算。
C# 的 bool 类型用于表示值为 true 或 false 的布尔值。
在 C# 中,字符和字符串处理使用 Unicode 编码。char 类型表示一个 UTF-16 编码单元,string类型表示 UTF-16 编码单元的序列。
下表总结了 C# 的数值类型。
类别 |
位数 |
类型 |
范围/精度 |
有符号整型 |
8 |
sbyte |
–128...127 |
16 |
short |
–32,768...32,767 |
|
32 |
int |
–2,147,483,648...2,147,483,647 |
|
64 |
long |
–9,223,372,036,854,775,808...9,223,372,036,854,775,807 |
|
无符号整型 |
8 |
byte |
0...255 |
16 |
ushort |
0...65,535 |
|
32 |
uint |
0...4,294,967,295 |
|
64 |
ulong |
0...18,446,744,073,709,551,615 |
|
浮点 |
32 |
float |
1.5 × 10−45 至 3.4 × 1038,7 位精度 |
64 |
double |
1.5 × 10−324 至 1.7 × 10308,15 位精度 |
|
十进制小数 |
128 |
decimal |
1.0 × 10−28 至 7.9 × 1028,28 位精度 |
C# 程序使用类型声明 (type declaration) 创建新类型。类型声明指定新类型的名称和成员。在 C# 类型分类中,有五类是用户可定义的:类类型 (class type)、结构类型 (struct type)、接口类型 (interface type)、枚举类型 (enum type) 和委托类型 (delegate type)。
类类型定义了一个包含数据成员(字段)和函数成员(方法、属性等)的数据结构。类类型支持单一继承和多态,这些是派生类可用来扩展和专用化基类的机制。
结构类型与类类型相似,表示一个带有数据成员和函数成员的结构。但是,与类不同,结构是一种值类型,并且不需要堆分配。结构类型不支持用户指定的继承,并且所有结构类型都隐式地从类型 object 继承。
接口类型定义了一个协定,作为一个公共函数成员的命名集。实现某个接口的类或结构必须提供该接口的函数成员的实现。一个接口可以从多个基接口继承,而一个类或结构可以实现多个接口。
委托类型表示对具有特定参数列表和返回类型的方法的引用。通过委托,我们能够将方法作为实体赋值给变量和作为参数传递。委托类似于在其他某些语言中的函数指针的概念,但是与函数指针不同,委托是面向对象的,并且是类型安全的。
类类型、结构类型、接口类型和委托类型都支持泛型,因此可以通过其他类型将其参数化。
枚举类型是具有命名常量的独特的类型。每种枚举类型都具有一个基础类型,该基础类型必须是八种整型之一。枚举类型的值集和它的基础类型的值集相同。
C# 支持由任何类型组成的一维和多维数组。与以上列出的类型不同,数组类型不必声明就可以使用。实际上,数组类型是通过在某个类型名后加一对方括号来构造的。例如,int[] 是一维 int 数组,int[,]是二维 int 数组,int[][]是一维 int 数组的一维数组。
可以为 null 的类型也不必声明就可以使用。对于每个不可以为 null 的值类型 T,都有一个相应的可以为 null 的类型 T?,该类型可以容纳附加值 null。例如,int? 类型可以容纳任何 32 位整数或 null 值。
C# 的类型系统是统一的,因此任何类型的值都可以按对象处理。C# 中的每个类型直接或间接地从 object 类类型派生,而 object 是所有类型的最终基类。引用类型的值都被视为 object 类型,被简单地当作对象来处理。值类型的值则通过对其执行装箱和拆箱操作按对象处理。下面的示例将 int 值转换为 object,然后又转换回 int。
using System;
class Test
{
static void Main() {
int i = 123;
object o = i; // Boxing
int j = (int)o; // Unboxing
}
}
当将值类型的值转换为类型 object时,将分配一个对象实例(也称为“箱子”)以包含该值,并将值复制到该箱子中。反过来,当将一个 object 引用强制转换为值类型时,将检查所引用的对象是否含有正确的值类型,如果有,则将箱子中的值复制出来。
C# 的统一类型系统实际上意味着值类型可以“按需”转换为对象。因为统一,所以使用类型 object 的通用库可以与引用类型和值类型一同使用。
C# 中存在几种变量 (variable),包括字段、数组元素、局部变量和参数。变量表示了存储位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量,如下表所示。
变量类型 |
可能的内容 |
不可以为 null 的值类型 |
类型完全相同的值 |
可以为 null 的值类型 |
null 值或类型完全相同的值 |
object |
null 引用、对任何引用类型的对象的引用,或者对任何值类型的装箱值的引用 |
类类型 |
null 引用、对该类类型的实例的引用,或者对从该类类型派生的类的实例的引用 |
接口类型 |
null 引用、对实现该接口类型的类类型的实例的引用,或者对实现该接口类型的值类型的装箱值的引用 |
数组类型 |
null 引用、对该数组类型的实例的引用,或者对兼容数组类型的实例的引用 |
委托类型 |
null 引用或对该委托类型的实例的引用 |
1.4 表达式
表达式由操作数(operand) 和运算符(operator) 构成。表达式的运算符指示对操作数适用什么样的运算。运算符的示例包括+、-、*、/ 和 new。操作数的示例包括文本、字段、局部变量和表达式。
当表达式包含多个运算符时,运算符的优先级 (precedence) 控制各运算符的计算顺序。例如,表达式 x + y * z 按 x + (y * z) 计算,因为 * 运算符的优先级高于 + 运算符。
大多数运算符都可以重载(overload)。运算符重载允许指定用户定义的运算符实现来执行运算,这些运算的操作数中至少有一个,甚至所有操作数都属于用户定义的类类型或结构类型。
下表总结了 C# 运算符,并按优先级从高到低的顺序列出各运算符类别。同一类别中的运算符优先级相同。
类别 |
表达式 |
说明 |
基本 |
x.m |
成员访问 |
x(...) |
方法和委托调用 |
|
x[...] |
数组和索引器访问 |
|
x++ |
后增量 |
|
x-- |
后减量 |
|
new T(...) |
对象和委托创建 |
|
new T(...){...} |
使用初始值设定项创建对象 |
|
new {...} |
匿名对象初始值设定项 |
|
new T[...] |
数组创建 |
|
typeof(T) |
获取 T 的 System.Type 对象 |
|
checked(x) |
在 checked 上下文中计算表达式 |
|
unchecked(x) |
在 unchecked 上下文中计算表达式 |
|
default(T) |
获取类型 T 的默认值 |
|
delegate {...} |
匿名函数(匿名方法) |
|
一元 |
+x |
恒等 |
-x |
求相反数 |
|
!x |
逻辑求反 |
|
~x |
按位求反 |
|
++x |
前增量 |
|
--x |
前减量 |
|
(T)x |
将 x 显式转换为类型 T |
|
await x |
异步等待 x 完成 |
|
乘法 |
x * y |
乘法 |
x / y |
除法 |
|
x % y |
求余 |
加减 |
x + y |
加法、字符串串联、委托组合 |
x – y |
减法、委托移除 |
|
移位 |
x << y |
左移 |
x >> y |
右移 |
|
关系和类型检测 |
x < y |
小于 |
x > y |
大于 |
|
x <= y |
小于或等于 |
|
x >= y |
大于或等于 |
|
x is T |
如果 x 为 T,则返回 true,否则返回 false |
|
x as T |
返回转换为类型 T 的 x,如果 x 不是 T 则返回 null |
|
相等 |
x == y |
等于 |
x != y |
不等于 |
|
逻辑 AND |
x & y |
整型按位 AND,布尔逻辑 AND |
逻辑 XOR |
x ^ y |
整型按位 XOR,布尔逻辑 XOR |
逻辑 OR |
x | y |
整型按位 OR,布尔逻辑 OR |
条件 AND |
x && y |
仅当 x 为 true 时,才对 y 求值 |
条件 OR |
x || y |
仅当 x 为 false 时,才对 y 求值 |
null 合并 |
X ?? y |
如果 x 为 null,则计算结果为 y,否则计算结果为 x |
条件 |
x ? y : z |
如果 x 为 true,则对 y 求值;如果 x 为 false,则对 z 求值 |
赋值或匿名函数 |
x = y |
赋值 |
x op= y |
复合赋值;支持的运算符有: *= /= %= += -= <<= >>= &= ^= |= |
|
(T x) => y |
匿名函数(lambda 表达式) |
1.5 语句
程序的操作是使用语句(statement) 来表示的。C# 支持几种不同的语句,其中许多以嵌入语句的形式定义。
block 用于在只允许使用单个语句的上下文中编写多条语句。块由位于一对大括号 { 和 } 之间的语句列表组成。
声明语句(declaration statement) 用于声明局部变量和常量。
表达式语句(expression statement) 用于对表达式求值。可用作语句的表达式包括方法调用、使用 new 运算符的对象分配、使用 = 和复合赋值运算符的赋值、使用 ++ 和 -- 运算符的增量和减量运算以及 await 表达式。
选择语句(selection statement) 用于根据表达式的值从若干个给定的语句中选择一个来执行。这一组中有 if 和 switch 语句。
迭代语句(iteration statement) 用于重复执行嵌入语句。这一组中有 while、do、for 和 foreach语句。
跳转语句 (jumpstatement) 用于转移控制。这一组中有 break、continue、goto、throw、return 和 yield 语句。
try...catch 语句用于捕获在块的执行期间发生的异常,try...finally语句用于指定终止代码,不管是否发生异常,该代码都始终要执行。
checked 语句和 unchecked 语句用于控制整型算术运算和转换的溢出检查上下文。
lock 语句用于获取某个给定对象的互斥锁,执行一个语句,然后释放该锁。
using 语句用于获得一个资源,执行一个语句,然后释放该资源。
下表列出了 C# 的各语句,并提供每个语句的示例。
语句 |
示例 |
局部变量声明 |
static void Main() { |
局部常量声明 |
static void Main() { |
表达式语句 |
static void Main() { |
if语句 |
static void Main(string[] args) { |
switch语句 |
static void Main(string[] args) { |
while语句 |
static void Main(string[] args) { |
do语句 |
static void Main() { |
for语句 |
static void Main(string[] args) { |
foreach语句 |
static void Main(string[] args) { |
break语句 |
static void Main() { |
continue语句 |
static void Main(string[] args) { |
goto语句 |
static void Main(string[] args) { |
return语句 |
static int Add(int a, int b) { static void Main() { |
yield语句 |
static IEnumerable<int> Range(int from, int to) { static void Main() { |
throw 和 try |
static double Divide(double x, double y) { static void Main(string[] args) { |
checked 和 unchecked 语句 |
static void Main() { |
lock语句 |
class Account public void Withdraw(decimal amount) { |
using语句 |
static void Main() { |
1.6 类和对象
类 (class) 是最基础的 C# 类型。类是一个数据结构,将状态(字段)和操作(方法和其他函数成员)组合在一个单元中。类为动态创建的类实例 (instance) 提供了定义,实例也称为对象 (object)。类支持继承 (inheritance) 和多态性 (polymorphism),这是派生类 (derived class) 可用来扩展和专用化基类 (base class) 的机制。
使用类声明可以创建新的类。类声明以一个声明头开始,其组成方式如下:先指定类的特性和修饰符,然后是类的名称,接着是基类(如有)以及该类实现的接口。声明头后面跟着类体,它由一组位于一对大括号 { 和 } 之间的成员声明组成。
下面是一个名为 Point 的简单类的声明:
public classPoint
{
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);
当不再使用对象时,该对象占用的内存将自动收回。在 C# 中,没有必要也不可能显式释放分配给对象的内存。
1.6.1 成员
类的成员或者是静态成员(static member),或者是实例成员(instance member)。静态成员属于类,实例成员属于对象(类的实例)。
下表提供了类所能包含的成员种类的概述。
成员 |
说明 |
常量 |
与类关联的常量值 |
字段 |
类的变量 |
方法 |
类可执行的计算和操作 |
属性 |
与读写类的命名属性相关联的操作 |
索引器 |
与以数组方式索引类的实例相关联的操作 |
事件 |
可由类生成的通知 |
运算符 |
类所支持的转换和表达式运算符 |
构造函数 |
初始化类的实例或类本身所需的操作 |
析构函数 |
在永久丢弃类的实例之前执行的操作 |
类型 |
类所声明的嵌套类型 |
1.6.2 可访问性
类的每个成员都有关联的可访问性,它控制能够访问该成员的程序文本区域。有五种可能的可访问性形式。下表概述了这些可访问性。
可访问性 |
含义 |
public |
访问不受限制 |
protected |
访问仅限于此类或从此类派生的类 |
internal |
访问仅限于此程序 |
protected internal |
访问仅限于此程序或从此类派生的类 |
private |
访问仅限于此类 |
1.6.3 类型形参
类定义可以通过在类名后添加用尖括号括起来的类型参数名称列表来指定一组类型参数。类型参数可用于在类声明体中定义类的成员。在下面的示例中,Pair 的类型参数为 TFirst 和 TSecond:
public class Pair<TFirst,TSecond>
{
public TFirst First;
publicTSecond Second;
}
要声明为采用类型参数的类类型称为泛型类类型。结构类型、接口类型和委托类型也可以是泛型。
当使用泛型类时,必须为每个类型参数提供类型实参:
Pair<int,string> pair = newPair<int,string> { First = 1, Second = “two” };
int i = pair.First; // TFirst is int
string s = pair.Second; // TSecond is string
提供了类型实参的泛型类型(例如上面的 Pair<int,string>)称为构造的类型。
1.6.4 基类
类声明可通过在类名和类型参数后面添加一个冒号和基类的名称来指定一个基类。省略基类的指定等同于从类型 object 派生。在下面的示例中,Point3D的基类是 Point,而 Point 的基类是 object:
public classPoint
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public classPoint3D: Point
{
public int z;
public Point3D(int x, int y, int z): base(x, y){
this.z = z;
}
}
类继承其基类的成员。继承意味着一个类隐式地将它的基类的所有成员当作自已的成员,但基类的实例构造函数、静态构造函数和析构函数除外。派生类能够在继承基类的基础上添加新的成员,但是它不能移除继承成员的定义。在前面的示例中,Point3D从 Point 继承了 x 和 y 字段,并且每个 Point3D 实例均包含三个字段:x、y 和 z。
从某个类类型到它的任何基类类型存在隐式的转换。因此,类类型的变量可以引用该类的实例或任何派生类的实例。例如,对于前面给定的类声明,Point 类型的变量既可以引用 Point 也可以引用 Point3D:
Point a = newPoint(10, 20);
Point b = new Point3D(10, 20, 30);
1.6.5 字段
字段是与类或类的实例关联的变量。
使用 static修饰符声明的字段定义了一个静态字段 (static field)。一个静态字段只标识一个存储位置。无论对一个类创建多少个实例,它的静态字段永远都只有一个副本。
不使用 static修饰符声明的字段定义了一个实例字段 (instance field)。类的每个实例都为该类的所有实例字段包含一个单独副本。
在下面的示例中,Color 类的每个实例都有实例字段 r、g 和 b 的单独副本,但是 Black、White、Red、Green 和 Blue 静态字段只存在一个副本:
public classColor
{
public static readonly Color Black = newColor(0, 0, 0);
public static readonly Color White = newColor(255, 255, 255);
public static readonly Color Red = newColor(255, 0, 0);
public static readonly Color Green = newColor(0, 255, 0);
public static readonly Color Blue = newColor(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;
}
}
如上面的示例所示,可以使用readonly 修饰符声明只读字段 (read-only field)。给 readonly 字段的赋值只能作为字段声明的组成部分出现,或在同一个类中的构造函数中出现。
1.6.6 方法
方法 (method) 是一种成员,用于实现可由对象或类执行的计算或操作。静态方法 (static method) 通过类来访问。实例方法 (instance method) 通过类的实例来访问。
方法具有一个参数(parameter) 列表(可以为空),表示传递给该方法的值或变量引用;方法还具有一个返回类型 (return type),指定该方法计算和返回的值的类型。如果方法不返回值,则其返回类型为 void。
与类型一样,方法也可以有一组类型参数,当调用方法时必须为类型参数指定类型实参。与类型不同的是,类型实参经常可以从方法调用的实参推断出,而无需显式指定。
方法的签名(signature) 在声明该方法的类中必须唯一。方法的签名由方法的名称、类型参数的数目以及该方法的参数的数目、修饰符和类型组成。方法的签名不包含返回类型。
1.6.6.1 参数
参数用于向方法传递值或变量引用。方法的参数从调用该方法时指定的实参 (argument) 获取它们的实际值。有四类参数:值参数、引用参数、输出参数和参数数组。
值参数 (valueparameter) 用于传递输入参数。一个值参数相当于一个局部变量,只是它的初始值来自为该形参传递的实参。对值参数的修改不影响为该形参传递的实参。
值参数可以是可选的,通过指定默认值可以省略对应的实参。
引用参数(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"
}
}
输出参数 (outputparameter) 用于传递输出参数。对于输出参数来说,调用方提供的实参的初始值并不重要。除此之外,输出参数与引用参数类似。输出参数是用 out 修饰符声明的。下面的示例演示 out 参数的用法。
using System;
class Test
{
static void Divide(int x, int y, out intresult, 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 "31"
}
}
参数数组(parameter array) 允许向方法传递可变数量的实参。参数数组使用 params 修饰符声明。只有方法的最后一个参数才可以是参数数组,并且参数数组的类型必须是一维数组类型。System.Console 类的 Write 和 WriteLine 方法就是参数数组用法的很好示例。它们的声明如下。
public classConsole
{
public static void Write(string fmt,params object[] args) {...}
public static void WriteLine(string fmt, paramsobject[] 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);
1.6.6.2 方法体和局部变量
方法体指定了在调用该方法时将执行的语句。
方法体可以声明仅用在该方法调用中的变量。这样的变量称为局部变量 (local variable)。局部变量声明指定了类型名称、变量名称,还可指定初始值。下面的示例声明一个初始值为零的局部变量 i 和一个没有初始值的变量 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 的使用报错,因为 i 在程序中的这些位置还没有明确赋值。
方法可以使用 return语句将控制返回到它的调用方。在返回 void 的方法中,return语句不能指定表达式。在返回非 void 的方法中,return语句必须含有一个计算返回值的表达式。
1.6.6.3 静态方法和实例方法
使用 static修饰符声明的方法为静态方法 (static method)。静态方法不对特定实例进行操作,并且只能直接访问静态成员。
不使用 static修饰符声明的方法为实例方法 (instance method)。实例方法对特定实例进行操作,并且能够访问静态成员和实例成员。在调用实例方法的实例上,可以通过 this 显式地访问该实例。而在静态方法中引用 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实例都包含一个序号(我们假定这里省略了一些其他信息)。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 实例方法是在该类的实例上调用的。
1.6.6.4 虚方法、重写方法和抽象方法
若一个实例方法的声明中含有virtual 修饰符,则称该方法为虚方法。若其中没有 virtual 修饰符,则称该方法为非虚方法。
在调用一个虚方法时,该调用所涉及的实例的运行时类型 (runtime type) 确定了要实际调用的方法实现。在非虚方法调用中,实例的编译时类型 (compile-time type) 负责做出此决定。
虚方法可以在派生类中重写(override)。当某个实例方法声明包括 override修饰符时,该方法将重写所继承的具有相同签名的虚方法。虚方法声明用于引入新方法,而重写方法声明则用于使现有的继承虚方法专用化(通过提供该方法的新实现)。
抽象 (abstract)方法是没有实现的虚方法。抽象方法使用 abstract 修饰符进行声明,并且只允许出现在同样被声明为 abstract 的类中。抽象方法必须在每个非抽象派生类中重写。
下面的示例声明一个抽象类 Expression,它表示一个表达式目录树节点;它有三个派生类 Constant、VariableReference 和 Operation,它们分别实现了常量、变量引用和算术运算的表达式目录树节点。(这与第 4.6 节中介绍的表达式树类型相似,但不要混淆)。
using System;
using System.Collections;
publicabstract class Expression
{
public abstract double Evaluate(Hashtablevars);
}
public classConstant: 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 newException("Unknown variable: " + name);
}
return Convert.ToDouble(value);
}
}
public classOperation: 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("Unknownoperator");
}
}
上面的四个类可用于为算术表达式建模。例如,使用这些类的实例,表达式 x + 3 可如下表示。
Expression e= new Operation(
new VariableReference("x"),
'+',
new Constant(3));
代码中调用了 Expression 实例的 Evaluate方法,以计算给定表达式的值,从而生成一个 double 值。该方法接受一个包含变量名称(作为哈希表项的键)和值(作为项的值)的 Hashtable 作为实参。Evaluate方法是一个虚抽象方法,意味着非抽象派生类必须重写该方法以提供实际的实现。
Constant 的 Evaluate 实现只是返回所存储的常量。VariableReference 的实现在哈希表中查找变量名称,并返回产生的值。Operation 的实现先对左操作数和右操作数求值(通过递归调用它们的 Evaluate 方法),然后执行给定的算术运算。
下面的程序使用 Expression 类,对于不同的 x 和 y 值,计算表达式 x * (y + 2) 的值。
using System;
using System.Collections;
class Test
{
static void Main() {
Expression e = new Operation(
newVariableReference("x"),
'*',
new Operation(
newVariableReference("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"
}
}
1.6.6.5 方法重载
方法重载(overloading) 允许同一类中的多个方法具有相同名称,条件是这些方法具有唯一的签名。在编译一个重载方法的调用时,编译器使用重载决策 (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) }
}
正如该示例所示,总是通过显式地将实参强制转换为确切的参数类型和/或显式地提供类型实参,来选择一个特定的方法。
1.6.7 其他函数成员
包含可执行代码的成员统称为类的函数成员 (function member)。前一节描述的方法是函数成员的主要类型。本节介绍了 C# 支持的其他类型的函数成员:构造函数、属性、索引器、事件、运算符和析构函数。
下表演示一个名为 List<T> 的泛型类,它实现一个可增长的对象列表。该类包含了几种最常见的函数成员的示例。
public class List<T> |
|
const int defaultCapacity = 4; |
常量 |
T[] items; |
字段 |
public List(int capacity = defaultCapacity) { |
构造函数 |
public int Count { public int Capacity { |
属性 |
public T this[int index] { |
索引器 |
public void Add(T item) { protected virtual void OnChanged() { public override bool Equals(object other) { static bool Equals(List<T> a, List<T> b) { |
方法 |
public event EventHandler Changed; |
事件 |
public static bool operator ==(List<T> a, List<T> b) { public static bool operator !=(List<T> a, List<T> b) { |
运算符 |
} |
1.6.7.1 构造函数
C# 支持两种构造函数:实例构造函数和静态构造函数。实例构造函数 (instance constructor) 是实现初始化类实例所需操作的成员。静态构造函数 (static constructor) 是一种用于在第一次加载类本身时实现其初始化所需操作的成员。
构造函数的声明如同方法一样,不过它没有返回类型,并且它的名称与其所属的类的名称相同。如果构造函数声明包含 static 修饰符,则它声明了一个静态构造函数。否则,它声明的是一个实例构造函数。
实例构造函数可以被重载。例如,List<T> 类声明了两个实例构造函数,一个无参数,另一个接受一个 int 参数。实例构造函数使用 new 运算符进行调用。下面的语句分别使用 List<string> 类的每个构造函数分配两个 List 实例。
List<string>list1 = new List<string>();
List<string> list2 = new List<string>(10);
实例构造函数不同于其他成员,它是不能被继承的。一个类除了其中实际声明的实例构造函数外,没有其他的实例构造函数。如果没有为某个类提供任何实例构造函数,则将自动提供一个不带参数的空的实例构造函数。
1.6.7.2 属性
属性 (property)是字段的自然扩展。属性和字段都是命名的成员,都具有相关的类型,且用于访问字段和属性的语法也相同。然而,与字段不同,属性不表示存储位置。相反,属性有访问器 (accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。
属性的声明与字段类似,不同的是属性声明以位于定界符 { 和 } 之间的一个 get 访问器和/或一个 set 访问器结束,而不是以分号结束。同时具有 get 访问器和 set 访问器的属性是读写属性 (read-write property),只有 get 访问器的属性是只读属性 (read-only property),只有 set 访问器的属性是只写属性 (write-only property)。
get 访问器相当于一个具有属性类型返回值的无形参方法。除了作为赋值的目标,当在表达式中引用属性时,将调用该属性的 get 访问器以计算该属性的值。
set 访问器相当于具有一个名为 value 的参数并且没有返回类型的方法。当某个属性作为赋值的目标被引用,或者作为 ++ 或 -- 的操作数被引用时,将调用 set 访问器,并传入提供新值的实参。
List<T> 类声明了两个属性 Count和 Capacity,它们分别是只读属性和读写属性。下面是这些属性的使用示例。
List<string>names = new List<string>();
names.Capacity = 100; //Invokes set accessor
int i = names.Count; //Invokes get accessor
int j = names.Capacity; //Invokes get accessor
与字段和方法相似,C# 同时支持实例属性和静态属性。静态属性使用 static 修饰符声明,而实例属性的声明不带该修饰符。
属性的访问器可以是虚的。当属性声明包括 virtual、abstract 或 override 修饰符时,修饰符应用于该属性的访问器。
1.6.7.3 索引器
索引器 (indexer)是这样一个成员:它支持按照索引数组的方法来索引对象。索引器的声明与属性类似,不同的是该成员的名称是 this,后跟一个位于定界符 [ 和 ] 之间的参数列表。在索引器的访问器中可以使用这些参数。与属性类似,索引器可以是读写、只读和只写的,并且索引器的访问器可以是虚的。
该 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();
}
索引器可以被重载,这意味着一个类可以声明多个索引器,只要其参数的数量和类型不同即可。
1.6.7.4 事件
事件 (event) 是一种使类或对象能够提供通知的成员。事件的声明与字段类似,不同的是事件的声明包含 event 关键字,并且类型必须是委托类型。
在声明事件成员的类中,事件的行为就像委托类型的字段(前提是该事件不是抽象的并且未声明访问器)。该字段存储对一个委托的引用,该委托表示已添加到该事件的事件处理程序。如果尚未添加事件处理程序,则该字段为 null。
List<T> 类声明了一个名为 Changed 的事件成员,它指示已将一个新项添加到列表中。Changed事件由 OnChanged 虚方法引发,后者先检查该事件是否为 null(表明没有处理程序)。“引发一个事件”与“调用一个由该事件表示的委托”这两个概念完全等效,因此没有用于引发事件的特殊语言构造。
客户端通过事件处理程序(event handler) 来响应事件。事件处理程序使用+= 运算符附加,使用 -= 运算符移除。下面的示例向 List<string> 类的 Changed 事件附加一个事件处理程序。
using System;
class Test
{
static int changeCount;
static void ListChanged(object sender,EventArgs e) {
changeCount++;
}
static void Main() {
List<string> names = newList<string>();
names.Changed += newEventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(changeCount); // Outputs "3"
}
}
对于要求控制事件的底层存储的高级情形,事件声明可以显式提供 add 和 remove访问器,它们在某种程度上类似于属性的 set 访问器。
1.6.7.5 运算符
运算符(operator) 是一种类成员,它定义了可应用于类实例的特定表达式运算符的含义。可以定义三类运算符:一元运算符、二元运算符和转换运算符。所有运算符都必须声明为 public 和 static。
List<T> 类声明了两个运算符 operator == 和 operator !=,从而为将那些运算符应用于 List 实例的表达式赋予了新的含义。具体而言,上述运算符将两个 List<T> 实例的相等关系定义为逐一比较其中所包含的对象(使用所包含对象的 Equals 方法)。下面的示例使用 == 运算符比较两个 List<int> 实例。
using System;
class Test
{
static void Main() {
List<int> a = newList<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> 未定义 operator ==,则第一个 Console.WriteLine 将输出 False,原因是 a 和 b 引用的是不同的 List<int> 实例。
1.6.7.6 析构函数
析构函数(destructor) 是一种用于实现销毁类实例所需操作的成员。析构函数不能带参数,不能具有可访问性修饰符,也不能被显式调用。垃圾回收期间会自动调用所涉及实例的析构函数。
垃圾回收器在决定何时回收对象和运行析构函数方面允许有广泛的自由度。具体而言,析构函数调用的时机并不是确定的,析构函数可以在任何线程上执行。由于这些以及其他原因,仅当没有其他可行的解决方案时,才应在类中实现析构函数。
using 语句提供了更好的对象析构方法。
1.7 结构
像类一样,结构(struct) 是能够包含数据成员和函数成员的数据结构。但是与类不同,结构是值类型,不需要堆分配。结构类型的变量直接存储该结构的数据,而类类型的变量则存储对动态分配的对象的引用。结构类型不支持用户指定的继承,并且所有结构类型都隐式地从类型 object 继承。
结构对于具有值语义的小型数据结构尤为有用。复数、坐标系中的点或字典中的“键-值”对都是结构的典型示例。对小型数据结构而言,使用结构而不使用类会大大节省需要为应用程序分配的内存数量。例如,下面的程序创建并初始化一个含有 100 个点的数组。对于作为类实现的 Point,实例化了 101 个单独对象,其中,数组需要一个,其 100 个元素每个都需要一个。
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 Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
现在,只有一个对象被实例化(即用于数组的那个对象),而 Point 实例内联存储在数组中。
结构构造函数也是使用 new 运算符调用,但是这并不意味着会分配内存。结构构造函数并不动态分配对象并返回对它的引用,而是直接返回结构值本身(通常是堆栈上的一个临时位置),然后根据需要复制该结构值。
对于类,两个变量可能引用同一对象,因此对一个变量进行的操作可能影响另一个变量所引用的对象。对于结构,每个变量都有自己的数据副本,对一个变量的操作不会影响另一个变量。例如,下面的代码段产生的输出取决于 Point 是类还是结构。
Point a = newPoint(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 参数,不可能创建对结构的引用,这样限制了结构的应用范围。
1.8 数组
数组 (array) 是一种包含若干变量的数据结构,这些变量都可以通过计算索引进行访问。数组中包含的变量(又称数组的元素)具有相同的类型,该类型称为数组的元素类型。
数组类型为引用类型,因此数组变量的声明只是为数组实例的引用留出空间。实际的数组实例在运行时使用 new 运算符动态创建。new 运算符指定新数组实例的长度 (length),它在该实例的生存期内是固定不变的。数组元素的索引范围从 0 到 Length - 1。new 运算符自动将数组的元素初始化为它们的默认值,例如将所有数值类型初始化为零,将所有引用类型初始化为 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 array)。数组类型的维数也称为数组类型的秩 (rank),它是数组类型的方括号之间的逗号个数加 1。下面的示例分别分配一个一维数组、一个二维数组和一个三维数组。
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 运算符允许使用数组初始值设定项 (array initializer) 指定数组元素的初始值,数组初始值设定项是在一个位于定界符 { 和 } 之间的表达式列表。下面的示例分配并初始化具有三个元素的 int[]。
int[] a = newint[] {1, 2, 3};
注意数组的长度是从 { 和 } 之间的表达式个数推断出来的。对于局部变量和字段声明,可以进一步简写,从而不必再次声明数组类型。
int[] a = {1,2, 3};
前面的两个示例都等效于下面的示例:
int[] t = newint[3];
t[0] = 1;
t[1] = 2;
t[2] = 3;
int[] a = t;
1.9 接口
接口(interface) 定义了一个可由类和结构实现的协定。接口可以包含方法、属性、事件和索引器。接口不提供它所定义的成员的实现 — 它仅指定实现该接口的类或结构必须提供的成员。
接口可支持多重继承。在下面的示例中,接口 IComboBox 同时从 ITextBox 和 IListBox 继承。
interfaceIControl
{
void Paint();
}
interfaceITextBox: IControl
{
void SetText(string text);
}
interfaceIListBox: IControl
{
void SetItems(string[] items);
}
interfaceIComboBox: ITextBox, IListBox {}
类和结构可以实现多个接口。在下面的示例中,类 EditBox 实现了 IControl 和 IDataBound。
interfaceIDataBound
{
void Bind(Binder b);
}
public classEditBox: IControl, IDataBound
{
public void Paint() {...}
public void Bind(Binder b) {...}
}
当类或结构实现某个特定接口时,该类或结构的实例可以隐式地转换为该接口类型。例如
EditBoxeditBox = new EditBox();
IControl control = editBox;
IDataBound dataBound = editBox;
在无法静态知道某个实例是否实现某个特定接口的情况下,可以使用动态类型强制转换。例如,下面的语句使用动态类型强制转换获取对象的 IControl 和 IDataBound 接口实现。由于该对象的实际类型为 EditBox,此强制转换成功。
object obj =new EditBox();
IControl control = (IControl)obj;
IDataBound dataBound = (IDataBound)obj;
在前面的 EditBox类中,来自 IControl 接口的 Paint 方法和来自 IDataBound 接口的 Bind 方法是使用 public 成员实现的。C# 还支持显式接口成员实现,类或结构可以使用它来避免将成员声明为 public。显式接口成员实现使用完全限定的接口成员名。例如,EditBox类可以使用显式接口成员实现来实现 IControl.Paint 和 IDataBound.Bind 方法,如下所示。
public classEditBox: IControl, IDataBound
{
void IControl.Paint() {...}
void IDataBound.Bind(Binder b) {...}
}
显式接口成员只能通过接口类型来访问。例如,要调用上面 EditBox 类提供的 IControl.Paint 实现,必须首先将 EditBox 引用转换为 IControl 接口类型。
EditBoxeditBox = new EditBox();
editBox.Paint(); //Error, no such method
IControl control = editBox;
control.Paint(); // Ok
1.10 枚举
枚举类型 (enumtype) 是具有一组命名常量的独特的值类型。下面的示例声明并使用一个名为 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("Unknowncolor");
break;
}
}
static void Main() {
Color c = Color.Red;
PrintColor(c);
PrintColor(Color.Blue);
}
}
每个枚举类型都有一个相应的整型类型,称为该枚举类型的基础类型 (underlying type)。没有显式声明基础类型的枚举类型所对应的基础类型是 int。枚举类型的存储格式和取值范围由其基础类型确定。一个枚举类型的值域不受它的枚举成员限制。具体而言,一个枚举的基础类型的任何一个值都可以被强制转换为该枚举类型,成为该枚举类型的一个独特的有效值。
下面的示例声明了一个名为 Alignment、基础类型为 sbyte 的枚举类型。
enumAlignment: sbyte
{
Left = -1,
Center = 0,
Right = 1
}
如前面的示例所示,枚举成员的声明中包含常量表达式,用于指定该成员的值。每个枚举成员的常数值必须在该枚举的基础类型的范围之内。如果枚举成员声明未显式指定一个值,该成员将被赋予值零(如果它是该枚举类型中的第一个值)或前一个枚举成员(按照文本顺序)的值加 1。
可以使用类型强制转换将枚举值转换为整型值,反之亦然。例如
int i = (int)Color.Blue; // int i = 2;
Color c = (Color)2; //Color c = Color.Blue;
任何枚举类型的默认值都是转换为该枚举类型的整型值零。在变量被自动初始化为默认值的情况下,该默认值就是赋予枚举类型的变量的值。为了便于获得枚举类型的默认值,文本 0 隐式地转换为任何枚举类型。因此,下面的语句是允许的。
Color c = 0;
1.11 委托
委托类型(delegate type) 表示对具有特定参数列表和返回类型的方法的引用。通过委托,我们能够将方法作为实体赋值给变量和作为参数传递。委托类似于在其他某些语言中的函数指针的概念,但是与函数指针不同,委托是面向对象的,并且是类型安全的。
下面的示例声明并使用一个名为Function 的委托类型。
using System;
delegatedouble Function(double x);
classMultiplier
{
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 = newdouble[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);
}
}
Function 委托类型的实例可以引用任何接受 double 实参并返回 double 值的方法。Apply 方法将给定的 Function 应用于 double[] 的元素,并返回含有结果的 double[]。在 Main 方法中,Apply 用于将三个不同的函数应用于一个 double[]。
委托既可以引用静态方法(例如前一示例中的 Square 或 Math.Sin),也可以引用实例方法(例如前一示例中的 m.Multiply)。引用了实例方法的委托也就引用了一个特定的对象,当通过该委托调用这个实例方法时,该对象在调用中成为 this。
也可以使用匿名函数创建委托,这是即时创建的“内联方法”。匿名函数可以查看外层方法的局部变量。因此,可以在不使用 Multiplier 类的情况下更容易地写出上面的乘法器示例:
double[] doubles = Apply(a, (double x) => x * 2.0);
委托的一个有趣且有用的属性在于,它不知道也不关心它所引用的方法的类;它仅关心所引用的方法是否与委托具有相同的参数和返回类型。
1.12 特性
C# 程序中的类型、成员和其他实体都支持修饰符,这些修饰符控制它们的行为的某些方面。例如,方法的可访问性是使用 public、protected、internal 和 private 修饰符来控制的。C# 使此功能一般化,以便能够将用户定义类型的声明信息附加到程序实体,并在运行时检索。这种附加的声明信息是程序通过定义和使用特性 (attribute) 来指定的。
下面的示例声明一个 HelpAttribute 特性,该特性可放置在程序实体上,以便提供指向其关联文档的链接。
using System;
public classHelpAttribute: 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) {}
}
此示例将一个 HelpAttribute 附加到 Widget类,并且将另一个 HelpAttribute 附加到该类中的 Display 方法。特性类的公共构造函数控制在将特性附加到程序实体时,必须提供的信息。可以通过引用特性类的公共读写属性提供附加信息,例如前面对 Topic 属性的引用。
下面的示例演示如何使用反射在运行时检索给定程序实体的特性信息。
using System;
using System.Reflection;
class Test
{
static void ShowHelp(MemberInfo member) {
HelpAttribute a =Attribute.GetCustomAttribute(member,
typeof(HelpAttribute)) asHelpAttribute;
if (a == null) {
Console.WriteLine("Nohelp for {0}", member);
}
else {
Console.WriteLine("Helpfor {0}:", member);
Console.WriteLine(" Url={0}, Topic={1}", a.Url, a.Topic);
}
}
static void Main() {
ShowHelp(typeof(Widget));
ShowHelp(typeof(Widget).GetMethod("Display"));
}
}
当通过反射请求特定特性时,将使用程序源中提供的信息调用特性类的构造函数,并返回生成的特性实例。如果通过属性提供了附加信息,那些属性将在返回特性实例之前被设置为给定的值。