1.1 Hello world 1
1.2 程序结构
2
1.3 类型和变量
4
1.4 表达式6
1.5 语句
.... 8
1.6 类和对象
11
1.6.1 成员 12
1.6.2 可访问性 12
1.6.3 基类
13
1.6.4 字段 13
1.6.5 方法 14
1.6.5.1 参数
.. 14
1.6.5.2 方法体和局部变量
.. 15
1.6.5.3 静态方法和实例方法
......... 16
1.6.5.4 虚方法、重写方法和抽象方法
......... 17
1.6.5.5 方法重载
...... 19
1.6.6 其他函数成员 20
1.6.6.1 构造函数
...... 21
1.6.6.2 属性
.. 21
1.6.6.3 索引器
......... 22
1.6.6.4 事件
.. 22
1.6.6.5 运算符
......... 23
1.6.6.6 析构函数
...... 23
1.7 结构
.. 24
1.8 数组
.. 25
1.9 接口
.. 26
1.10 枚举
. 27
1.11 委托
. 28
1.12 属性
. 29
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) 编程的支持。现代软件设计日益依赖于自包含和自描述功能包形式的软件组件。这种组件的关键在于,它们通过属性 (property)、方法 (method) 和事件 (event) 来提供编程模型;它们具有提供了关于组件的声明性信息的属性 (attribute);同时,它们还编入了自己的文档。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 命名空间。命名空间 (namespace) 提供了一种分层的方式来组织 C# 程序和库。命名空间中包含有类型及其他命名空间 — 例如,
System 命名空间包含若干类型(如此程序中引用的
Console 类)以及若干其他命名空间(如
IO 和
Collections)。如果使用
using 指令引用了某一给定命名空间,就可以通过非限定方式使用作为命名空间成员的类型。在此程序中,正是由于使用了
using 指令,我们可以使用
Console.WriteLine 这一简化形式代替完全限定方式
System.Console.WriteLine。
“Hello, World”程序中声明的
Hello 类只有一个成员,即名为
Main 的方法。
Main 方法是使用
static 修饰符声明的。静态 (static) 方法不同于实例 (instance) 方法,后者使用关键字
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;
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; } } } }
在名为
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# 不限制一个源文件只能声明一个公共类型,也不要求源文件的名称与该源文件中声明的类型匹配。
C# 中有两种类型:值类型
(value type)和引用类型(reference type)。值类型的变量直接包含它们的数据,而引用类型的变量存储对它们的数据的引用,后者称为对象。对于引用类型,两个变量可能引用同一个对象,因此对一个变量的操作可能影响另一个变量所引用的对象。对于值类型,每个变量都有它们自己的数据副本(除 ref 和 out 参数变量外),因此对一个变量的操作不可能影响另一个变量。
C# 的值类型进一步划分为简单类型
(simple type)、枚举类型
(enum type)和结构类型
(struct 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 {...} 形式的用户定义的类型
| |
引用类型
|
类类型
|
所有其他类型的最终基类:
object
|
Unicode 字符串:
string
| ||
class
C
{...} 形式的用户定义的类型
| ||
接口类型
|
interface I {...} 形式的用户定义的类型
| |
数组类型
|
一维和多维数组,例如
int[] 和
int[,]
| |
委托类型
|
delegate T D(...) 形式的用户定义的类型
|
八种整型类型分别支持 8 位、16 位、32 位和 64 位整数值的有符号和无符号的形式。
两种浮点类型:
float 和
double,分别使用 32 位单精度和 64 位双精度的 IEEE 754 格式表示。
decimal 类型是 128 位的数据类型,适合用于财务计算和货币计算。
C# 的
bool 类型用于表示布尔值 — 为
true 或者
false 的值。
在 C# 中,字符和字符串处理使用 Unicode 编码。
char 类型表示一个 16 位 Unicode 编码单元,
string 类型表示 16 位 Unicode 编码单元的序列。
下表总结了 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 × 10
38,7 位精度
|
64
|
double
|
5.0 × 10
−324 至 1.7 × 10
308,15 位精度
| |
小数
|
128
|
decimal
|
1.0 × 10
−28 至 7.9 × 10
28,28 位精度
|
C# 程序使用类型声明
(type declaration)创建新类型。类型声明指定新类型的名称和成员。有五种类别的 C# 类型是可由用户定义的:类类型、结构类型、接口类型、枚举类型和委托类型。
类类型定义了一个包含数据成员(字段)和函数成员(方法、属性等)的数据结构。类类型支持继承和多态,这些是派生类可用来扩展和专用化基类的一种机制。
结构类型与类类型相似,表示一个带有数据成员和函数成员的结构。但是,与类不同,结构是一种值类型,并且不需要堆分配。结构类型不支持用户指定的继承,并且所有结构类型都隐式地从类型
object 继承。
接口类型定义了一个协定,作为一个函数成员的命名集合。实现某个接口的类或结构必须提供该接口的函数成员的实现。一个接口可以从多个基接口继承,而一个类或结构可以实现多个接口。
枚举类型是具有命名常量的独特的类型。每种枚举类型都具有一个基础类型,该基础类型必须是八种整型之一。枚举类型的值集和它的基础类型的值集相同。
委托类型表示对具有特定参数列表和返回类型的方法的引用。通过委托,我们能够将方法作为实体赋值给变量和作为参数传递。委托类似于在其他某些语言中的函数指针的概念,但是与函数指针不同,委托是面向对象的,并且是类型安全的。
C# 支持由任何类型组成的一维和多维数组。与其他类型不同,数组类型不必在使用之前事先声明。实际上,数组类型是通过在某个类型名后加一对方括号来构造的。例如,
int[] 是一维
int 数组,
int[,] 是二维
int 数组,
int[][] 是一维
int 数组的一维数组。
C# 的类型系统是统一的,因此任何类型的值都可以按对象处理。C# 中的每个类型直接或间接地从
object 类类型派生,而
object 是所有类型的最终基类。引用类型的值都被当作“对象”来处理,这是因为这些值可以简单地被视为是属于
object 类型。值类型的值则通过执行装箱
(boxing) 和拆箱
(unboxing) 操作亦按对象处理。下面的示例将
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 类型的通用库(例如 .NET Framework 中的集合类)既可以用于引用类型,又可以用于值类型。
C# 中存在几种变量
(variable),包括字段、数组元素、局部变量和参数。变量表示了存储位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量,如下表所示。
变量类型
|
可能的内容
|
值类型
|
类型完全相同的值
|
object
|
空引用、对任何引用类型的对象的引用,或对任何值类型的装箱值的引用
|
类类型
|
空引用、对该类类型的实例的引用,或者对从该类类型派生的类的实例的引用
|
接口类型
|
空引用、对实现该接口类型的类类型的实例的引用,或者对实现该接口类型的值类型的装箱值的引用
|
数组类型
|
空引用、对该数组类型的实例的引用,或者对兼容数组类型的实例的引用
|
委托类型
|
空引用或对该委托类型的实例的引用
|
表达式
(expression) 由操作数
(operand) 和运算符
(operator) 构成。表达式的运算符指示对操作数进行什么样的运算。运算符的示例包括
+、
-、
*、
/ 和
new。操作数的示例包括文本 (literal)、字段、局部变量和表达式。
当表达式包含多个运算符时,运算符的优先级
(precedence) 控制各运算符的计算顺序。例如,表达式
x + y * z 按
x + (y * z) 计算,因为
* 运算符的优先级高于
+ 运算符。
大多数运算符都可以重载
(overload)。运算符重载允许指定用户定义的运算符实现来执行运算,这些运算的操作数中至少有一个,甚至所有都属于用户定义的类类型或结构类型。
下表总结了
C#
运算符
,
并按优先级从高到低的顺序列出各运算符类别。同一类别中的运算符优先级相同。
类别
|
表达式
|
说明
|
基本
|
x.m
|
成员访问
|
x(...)
|
方法和委托调用
| |
x[...]
|
数组和索引器访问
| |
x++
|
后增量
| |
x--
|
后减量
| |
new T(...)
|
对象和委托创建
| |
new T[...]
|
数组创建
| |
typeof(T)
|
获得
T 的
System.Type 对象
| |
checked(x)
|
在 checked 上下文中计算表达式
| |
unchecked(x)
|
在 unchecked 上下文中计算表达式
| |
一元
|
+x
|
表达式的值相同
|
-x
|
求相反数
| |
!x
|
逻辑求反
| |
~x
|
按位求反
| |
++x
|
前增量
| |
--x
|
前减量
| |
(T)x
|
显式将
x 转换为类型
T
| |
乘除
|
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 求值
|
条件
|
x ? y : z
|
如果
x 为
true,则对
y 求值,如果
x 为
false,则对
z 求值
|
赋值
|
x = y
|
赋值
|
x
op=
y
|
复合赋值;支持的运算符有:
*=
/=
%=
+=
-=
<<=
>>=
&=
^=
|=
|
程序的操作是使用语句
(statement)
来表示的。
C#
支持几种不同的语句
,
其中许多以嵌入语句的形式定义。
块
(block) 用于在只允许使用单个语句的上下文中编写多条语句。块由位于一对大括号
{ 和
} 之间的语句列表组成。
声明语句
(declaration statement) 用于声明局部变量和常量。
表达式语句
(expression statement)
用于对表达式求值。可用作语句的表达式包括方法调用、使用
new
运算符的对象分配、使用
=
和复合赋值运算符的赋值
,
以及使用
++
和
--
运算符的增量和减量运算。
选择语句
(selection statement) 用于根据表达式的值从若干个给定的语句中选择一个来执行。这一组语句有
if 和
switch 语句。
迭代语句
(iteration statement) 用于重复执行嵌入语句。这一组语句有
while、
do、
for 和
foreach 语句。
跳转语句
(jump statement) 用于转移控制。这一组语句有
break、
continue、
goto、
throw 和
return 语句。
try...
catch 语句用于捕获在块的执行期间发生的异常,
try...
finally 语句用于指定终止代码,不管是否发生异常,该代码都始终要执行。
checked 语句和
unchecked 语句用于控制整型算术运算和转换的溢出检查上下文。
lock 语句用于获取某个给定对象的互斥锁,执行一个语句,然后释放该锁。
using 语句用于获得一个资源,执行一个语句,然后释放该资源。
下表列出了 C# 的各语句,并提供每个语句的示例。
语句
|
示例
|
局部变量声明
|
static void Main() { int a; int b = 2, c = 3; a = 1; Console.WriteLine(a + b + c); }
|
局部常量声明
|
static void Main() { const float pi = 3.1415927f; const int r = 25; Console.WriteLine(pi * r * r); }
|
表达式语句
|
static void Main() { int i; i = 123; // Expression statement Console.WriteLine(i); // Expression statement i++; // Expression statement Console.WriteLine(i); // Expression statement }
|
if 语句
|
static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine("No arguments"); } else { Console.WriteLine("One or more arguments"); } }
|
switch 语句
|
static void Main(string[] args) { int n = args.Length; switch (n) { case 0: Console.WriteLine("No arguments"); break; case 1: Console.WriteLine("One argument"); break; default: Console.WriteLine("{0} arguments", n); break; } } }
|
while 语句
|
static void Main(string[] args) { int i = 0; while (i < args.Length) { Console.WriteLine(args[i]); i++; } }
|
do 语句
|
static void Main() { string s; do { s = Console.ReadLine(); if (s != null) Console.WriteLine(s); } while (s != null); }
|
for 语句
|
static void Main(string[] args) { for (int i = 0; i < args.Length; i++) { Console.WriteLine(args[i]); } }
|
foreach 语句
|
static void Main(string[] args) { foreach (string s in args) { Console.WriteLine(s); } }
|
break 语句
|
static void Main() { while (true) { string s = Console.ReadLine(); if (s == null) break; Console.WriteLine(s); } }
|
continue 语句
|
static void Main(string[] args) { for (int i = 0; i < args.Length; i++) { if (args[i].StartsWith("/")) continue; Console.WriteLine(args[i]); } }
|
goto 语句
|
static void Main(string[] args) { int i = 0; goto check; loop: Console.WriteLine(args[i++]); check: if (i < args.Length) goto loop; }
|
return 语句
|
static int Add(int a, int b) { return a + b; }
static void Main() { Console.WriteLine(Add(1, 2)); return; }
|
throw 和
try 语句
|
static double Divide(double x, double y) { if (y == 0) throw new DivideByZeroException(); return x / y; }
static void Main(string[] args) { try { if (args.Length != 2) { throw new Exception("Two numbers required"); } double x = double.Parse(args[0]); double y = double.Parse(args[1]); Console.WriteLine(Divide(x, y)); } catch (Exception e) { Console.WriteLine(e.Message); } }
|
checked 和
unchecked 语句
|
static void Main() { int i = int.MaxValue; checked { Console.WriteLine(i + 1); // Exception } unchecked { Console.WriteLine(i + 1); // Overflow } }
|
lock 语句
|
class Account { decimal balance;
public void Withdraw(decimal amount) { lock (this) { if (amount > balance) { throw new Exception("Insufficient funds"); } balance -= amount; } } }
|
using 语句
|
static void Main() { using (TextWriter w = File.CreateText("test.txt")) { w.WriteLine("Line one"); w.WriteLine("Line two"); w.WriteLine("Line three"); } }
|
类
(class) 是最基础的 C# 类型。类是一个数据结构,将状态(字段)和操作(方法和其他函数成员)组合在一个单元中。类为动态创建的类实例
(instance) 提供了定义,实例也称为对象
(object)。类支持继承
(inheritance) 和多态性
(polymorphism),这是派生类
(derived class) 可用来扩展和专用化基类
(base class) 的机制。
使用类声明可以创建新的类。类声明以一个声明头开始,其组成方式如下:先指定类的属性和修饰符,然后是类的名称,接着是基类(如有)以及该类实现的接口。声明头后面跟着类体,它由一组位于一对大括号
{ 和
} 之间的成员声明组成。
下面是一个名为
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);
当不再使用对象时,该对象占用的内存将自动收回。在 C# 中,没有必要也不可能显式释放分配给对象的内存。
类的成员或者是静态成员
(static member),或者是实例成员
(instance member)。静态成员属于类,实例成员属于对象(类的实例)。
下表提供了类所能包含的成员种类的概述。
成员
|
说明
|
常量
|
与类关联的常数值
|
字段
|
类的变量
|
方法
|
类可执行的计算和操作
|
属性
|
与读写类的命名属性相关联的操作
|
索引器
|
与以数组方式索引类的实例相关联的操作
|
事件
|
可由类生成的通知
|
运算符
|
类所支持的转换和表达式运算符
|
构造函数
|
初始化类的实例或类本身所需的操作
|
析构函数
|
在永久丢弃类的实例之前执行的操作
|
类型
|
类所声明的嵌套类型
|
类的每个成员都有关联的可访问性,它控制能够访问该成员的程序文本区域。有五种可能的可访问性形式。下表概述了这些可访问性。
可访问性
|
含义
|
public
|
访问不受限制
|
protected
|
访问仅限于此类和从此类派生的类
|
internal
|
访问仅限于此程序
|
protected internal
|
访问仅限于此程序和从此类派生的类
|
private
|
访问仅限于此类
|
类声明可通过在类名称后面加上一个冒号和基类的名称来指定一个基类。省略基类的指定等同于从类型
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): Point(x, y) { this.z = z; } }
一个类继承它的基类的成员。继承意味着一个类隐式地包含其基类的所有成员,但基类的构造函数除外。派生类能够在继承基类的基础上添加新的成员,但是它不能移除继承成员的定义。在前面的示例中,
Point3D 类从
Point 类继承了
x 字段和
y 字段,每个
Point3D 实例都包含三个字段
x、
y 和
z。
从某个类类型到它的任何基类类型存在隐式的转换。因此,类类型的变量可以引用该类的实例或任何派生类的实例。例如,对于前面给定的类声明,
Point 类型的变量既可以引用
Point 也可以引用
Point3D:
Point a = new Point(10, 20); Point b = new Point3D(10, 20, 30);
字段是与类或类的实例关联的变量。
使用
static 修饰符声明的字段定义了一个静态字段
(static field)。一个静态字段只标识一个存储位置。对一个类无论创建了多少个实例,它的静态字段永远都只有一个副本。
不使用
static 修饰符声明的字段定义了一个实例字段
(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; } }
如上面的示例所示,可以使用
readonly 修饰符声明只读字段
(read-only field)。给
readonly 字段的赋值只能作为字段声明的组成部分出现,或在同一类中的实例构造函数或静态构造函数中出现。
方法
(method) 是一种用于实现可以由对象或类执行的计算或操作的成员。静态方法
(static method) 通过类来访问。实例方法
(instance method) 通过类的实例来访问。
方法具有一个参数
(parameter) 列表(可能为空),表示传递给该方法的值或变量引用;方法还具有一个返回类型
(return type),指定该方法计算和返回的值的类型。如果方法不返回值,则其返回类型为
void。
方法的签名
(signature) 在声明该方法的类中必须唯一。方法的签名由方法的名称及其参数的数目、修饰符和类型组成。方法的签名不包含返回类型。
参数用于向方法传递值或变量引用。方法的参数从方法被调用时指定的实参
(argument) 获取它们的实际值。有四种类型的参数:值参数、引用参数、输出参数和参数数组。
值参数
(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) 允许向方法传递可变数量的实参。参数数组使用
params 修饰符声明。只有方法的最后一个参数才可以是参数数组,并且参数数组的类型必须是一维数组类型。
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);
等价于写下面的语句。
object[] args = new object[3]; args[0] = x; args[1] = y; args[2] = z; Console.WriteLine("x={0} y={1} z={2}", args);
方法体指定了在该方法被调用时将执行的语句。
方法体可以声明仅用在该方法调用中的变量。这样的变量称为局部变量
(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 语句必须含有一个计算返回值的表达式。
使用
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 实例方法是在该类的实例上调用的。
若一个实例方法的声明中含有
virtual 修饰符,则称该方法为虚方法
(virtual method)。若其中没有
virtual 修饰符,则称该方法为非虚方法
(non-virtual method)。
在调用一个虚方法时,该调用所涉及的那个实例的运行时类型
(runtime type) 确定了要被调用的究竟是该方法的哪一个实现。在非虚方法调用中,实例的编译时类型
(compile-time type) 是决定性因素。
虚方法可以在派生类中重写
(override)。当某个实例方法声明包括
override 修饰符时,该方法将重写所继承的具有相同签名的虚方法。虚方法声明用于引入新方法,而重写方法声明则用于使现有的继承虚方法专用化(通过提供该方法的新实现)。
抽象
(abstract) 方法是没有实现的虚方法。抽象方法使用
abstract 修饰符进行声明,并且只有在同样被声明为
abstract 的类中才允许出现。抽象方法必须在每个非抽象派生类中重写。
下面的示例声明一个抽象类
Expression
,
它表示一个表达式树节点
;
它有三个派生类
Constant
、
VariableReference
和
Operation
,
它们分别实现了常量、变量引用和算术运算的表达式树节点。
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));
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( 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" } }
方法重载
(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(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(1, 1); // Invokes F(double, double) } }
正如该示例所示,总是通过显式地将实参强制转换为确切的形参类型,来选择一个特定的方法。
包含可执行代码的成员统称为类的函数成员
(function member)
。前一节描述的方法是函数成员的主要类型。本节描述
C#
支持的其他种类的函数成员:构造函数、属性、索引器、事件、运算符和析构函数。
下表演示一个名为
List 的类,它实现一个可增长的对象列表。该类包含了几种最常见的函数成员的示例。
public class List {
| |
const int defaultCapacity = 4;
|
常量
|
object[] items; int count;
|
字段
|
public List(): List(defaultCapacity) {}
public List(int capacity) { items = new object[capacity]; }
|
构造函数
|
public int Count { get { return count; } }
public string Capacity { get { return items.Length; } set { if (value < count) value = count; if (value != items.Length) { object[] newItems = new object[value]; Array.Copy(items, 0, newItems, 0, count); items = newItems; } } }
|
属性
|
public object this[int index] { get { return items[index]; } set { items[index] = value; OnListChange(); } }
|
索引器
|
public void Add(object item) { if (count == Capacity) Capacity = count * 2; items[count] = item; count++; OnChanged(); }
protected virtual void OnChanged() { if (Changed != null) Changed(this, EventArgs.Empty); }
public override bool Equals(object other) { return Equals(this, other as List); }
static bool Equals(List a, List b) { if (a == null) return b == null; if (b == null || a.count != b.count) return false; for (int i = 0; i < a.count; i++) { if (!object.Equals(a.items[i], b.items[i])) { return false; } } return true; }
|
方法
|
public event EventHandler Changed;
|
事件
|
public static bool operator ==(List a, List b) { return Equals(a, b); }
public static bool operator !=(List a, List b) { return !Equals(a, b); }
|
运算符
|
}
|
C# 支持两种构造函数:实例构造函数和静态构造函数。实例构造函数
(instance constructor) 是实现初始化类实例所需操作的成员。静态构造函数
(static constructor) 是一种用于在第一次加载类本身时实现其初始化所需操作的成员。
构造函数的声明如同方法一样
,
不过它没有返回类型
,
并且它的名称与其所属的类的名称相同。如果构造函数声明包含
static
修饰符,则它声明了一个静态构造函数。否则,它声明的是一个实例构造函数。
实例构造函数可以被重载。例如,
List 类声明了两个实例构造函数,一个无参数,另一个接受一个
int 参数。实例构造函数使用
new 运算符进行调用。下面的语句分别使用
List 类的每个构造函数分配两个
List 实例。
List list1 = new List(); List list2 = new List(10);
实例构造函数不同于其他成员,它是不能被继承的。一个类除了其中实际声明的实例构造函数外,没有其他的实例构造函数。如果没有为某个类提供任何实例构造函数,则将自动提供一个不带参数的空的实例构造函数。
属性
(propery) 是字段的自然扩展。属性和字段都是命名的成员,都具有相关的类型,且用于访问字段和属性的语法也相同。然而,与字段不同,属性不表示存储位置。相反,属性有访问器
(accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。
属性的声明与字段类似,不同的是属性声明以位于定界符
{ 和
} 之间的一个
get 访问器和/或一个
set 访问器结束,而不是以分号结束。同时具有
get 访问器和
set 访问器的属性是读写属性
(read-write property),只有
get 访问器的属性是只读属性
(read-only property),只有
set 访问器的属性是只写属性
(write-only property)。
get 访问器相当于一个具有属性类型返回值的无参数方法。除了作为赋值的目标,当在表达式中引用属性时,将调用该属性的
get 访问器以计算该属性的值。
set 访问器相当于具有一个名为
value 的参数并且没有返回类型的方法。当某个属性作为赋值的目标被引用,或者作为
++ 或
-- 的操作数被引用时,将调用
set 访问器,并传入提供新值的实参。
List 类声明了两个属性
Count 和
Capacity,它们分别是只读属性和读写属性。下面是这些属性的使用示例。
List names = new List(); 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 修饰符时,修饰符应用于该属性的访问器。
索引器
(indexer) 是这样一个成员:它使对象能够用与数组相同的方式进行索引。索引器的声明与属性类似,不同的是该成员的名称是
this,后跟一个位于定界符
[ 和
] 之间的参数列表。在索引器的访问器中可以使用这些参数。与属性类似,索引器可以是读写、只读和只写的,并且索引器的访问器可以是虚的。
该
List 类声明了单个读写索引器,该索引器接受一个
int 参数。该索引器使得通过
int 值对
List 实例进行索引成为可能。例如
List numbers = new List(); names.Add("Liz"); names.Add("Martha"); names.Add("Beth"); for (int i = 0; i < names.Count; i++) { string s = (string)names[i]; names[i] = s.ToUpper(); }
索引器可以被重载,这意味着一个类可以声明多个索引器,只要它们的参数的数量和类型不同即可。
事件
(event) 是一种使类或对象能够提供通知的成员。事件的声明与字段类似,不同的是事件的声明包含
event 关键字,并且类型必须是委托类型。
在声明事件成员的类中,事件的行为就像委托类型的字段(前提是该事件不是抽象的并且未声明访问器)。该字段存储对一个委托的引用,该委托表示已添加到该事件的事件处理程序。如果尚未添加事件处理程序,则该字段为
null。
List 类声明了一个名为
Changed 的事件成员,它指示有一个新的项已被添加到列表中。
Changed 事件由
OnChanged 虚方法引发,后者先检查该事件是否为
null(表明没有处理程序)。“引发一个事件”与“调用一个由该事件表示的委托”完全等效,因此没有用于引发事件的特殊语言构造。
客户端通过事件处理程序
(event handler) 来响应事件。事件处理程序使用
+= 运算符添加,使用
-= 运算符移除。下面的示例向
List 类的
Changed 事件添加一个事件处理程序。
using System;
class Test { static int changeCount;
static void ListChanged(object sender, EventArgs e) { changeCount++; }
static void Main() { List names = new List(); names.Changed += new EventHandler(ListChanged); names.Add("Liz"); names.Add("Martha"); names.Add("Beth"); Console.WriteLine(changeCount); // Outputs "3" } }
对于要求控制事件的底层存储的高级情形,事件声明可以显式提供
add 和
remove 访问器,它们在某种程度上类似于属性的
set 访问器。
运算符
(operator) 是一种类成员,它定义了可应用于类实例的特定表达式运算符的含义。可以定义三种运算符:一元运算符、二元运算符和转换运算符。所有运算符都必须声明为
public 和
static。
List
类声明了两个运算符
operator
==
和
operator
!=
,
从而为将那些运算符应用于
List
实例的表达式赋予了新的含义。具体而言,上述运算符将两个
List
实例的相等关系定义为逐一比较其中所包含的对象(使用所包含对象的
Equals
方法)。下面的示例使用
==
运算符比较两个
List
实例。
using System;
class Test { static void Main() { List a = new List(); a.Add(1); a.Add(2); List b = new List(); 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 未定义 operator ==,则第一个 Console.WriteLine 将输出 False,原因是 a 和 b 引用的是不同的 List 实例。
析构函数
(destructor) 是一种用于实现销毁类实例所需操作的成员。析构函数不能带参数,不能具有可访问性修饰符,也不能被显式调用。垃圾回收期间会自动调用所涉及实例的析构函数。
垃圾回收器在决定何时回收对象和运行析构函数方面允许有广泛的自由度。具体而言,析构函数调用的时机并不是确定的,析构函数可能在任何线程上执行。由于这些以及其他原因,仅当没有其他可行的解决方案时,才应在类中实现析构函数。
像类一样,结构
(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 = 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 参数,不可能创建对结构的引用,这样限制了结构的应用范围。
数组
(array) 是一种包含若干变量的数据结构,这些变量都可以通过计算索引进行访问。数组中包含的变量(又称数组的元素
(element))具有相同的类型,该类型称为数组的元素类型。
数组类型为引用类型,因此数组变量的声明只是为数组实例的引用留出空间。实际的数组实例在运行时使用
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 = new int[] {1, 2, 3};
注意数组的长度是从
{ 和
} 之间的表达式个数推断出来的。对于局部变量和字段声明,可以进一步简写,从而不必再次声明数组类型。
int[] a = {1, 2, 3};
前面的两个示例都等效于下面的示例:
int[] a = new int[3]; a[0] = 1; a[1] = 2; a[2] = 3;
接口
(interface) 定义了一个可由类和结构实现的协定。接口可以包含方法、属性、事件和索引器。接口不提供它所定义的成员的实现 — 它仅指定实现该接口的类或结构必须提供的成员。
接口可支持多重继承
(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;
在无法静态知道某个实例是否实现某个特定接口的情况下,可以使用动态类型强制转换。例如,下面的语句使用动态类型强制转换获得对象的
IControl 和
IDataBound 接口实现。由于该对象的实际类型为
EditBox,此强制转换成功。
object obj = new EditBox(); IControl control = (IControl)obj; IDataBound dataBound = (IDataBound)obj;
在前面的
EditBox 类中,来自
IControl 接口的
Paint 方法和来自
IDataBound 接口的
Bind 方法使用
public 成员来实现。C# 还支持显式接口成员实现
(explicit interface member implementation),类或结构可以使用它来避免将成员声明为
public。显式接口成员实现使用完全限定的接口成员名。例如,
EditBox 类可以使用显式接口成员实现来实现
IControl.Paint 和
IDataBound.Bind 方法,如下所示。
public class EditBox: IControl, IDataBound { void IControl.Paint() {...}
void IDataBound.Bind(Binder b) {...}
}
显式接口成员只能通过接口类型来访问。例如,要调用上面
EditBox 类提供的
IControl.Paint 实现,必须首先将
EditBox 引用转换为
IControl 接口类型。
EditBox editBox = new EditBox(); editBox.Paint(); // Error, no such method IControl control = editBox; control.Paint(); // Ok
枚举类型
(enum type) 是具有一组命名常量的独特的值类型。下面的示例声明并使用一个名为
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。枚举类型的存储格式和取值范围由其基础类型确定。一个枚举类型的值域不受它的枚举成员限制。具体而言,一个枚举的基础类型的任何一个值都可以被强制转换为该枚举类型,成为该枚举类型的一个独特的有效值。
下面的示例声明一个基础类型为
sbyte 的名为
Alignment 的枚举类型。
enum Alignment: 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;
委托类型
(delegate type) 表示对具有特定参数列表和返回类型的方法的引用。通过委托,我们能够将方法作为实体赋值给变量和作为参数传递。委托类似于在其他某些语言中的函数指针的概念,但是与函数指针不同,委托是面向对象的,并且是类型安全的。
下面的示例声明并使用一个名为
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, new Function(Square));
double[] sines = Apply(a, new Function(Math.Sin));
Multiplier m = new Multiplier(2.0); double[] doubles = Apply(a, new Function(m.Multiply)); } }
Function 委托类型的实例可以引用任何接受
double 参数并返回
double 值的方法。
Apply 方法将给定的
Function 作用于
double[] 的元素,并返回含有结果的
double[]。在
Main 方法中,
Apply 用于将三个不同的函数应用于一个
double[]。
委托可以既可以引用静态方法(例如前一示例中的
Square 或
Math.Sin),也可以引用实例方法(例如前一示例中的
m.Multiply)。引用了实例方法的委托也就引用了一个特定的对象,当通过该委托调用这个实例方法时,该对象在调用中成为
this。
委托的一个有趣且有用的属性在于,它不知道也不关心它所引用的方法的类;它所关心的仅是所引用的方法与委托具有相同的参数和返回类型。
C# 程序中的类型、成员和其他实体支持修饰符,这些修饰符控制它们的行为的某些方面。例如,方法的可访问性使用
public、
protected、
internal 和
private 修饰符控制。C# 使此功能一般化,以便能够将用户定义类型的声明信息附加到程序实体,并在运行时检索。这种附加的声明信息是程序通过定义和使用属性
(attribute) 来指定的。
下面的示例声明一个
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) {} }
此示例将一个
HelpAttribute 附加到
Widget 类,并且将另一个
HelpAttribute 附加到该类中的
Display 方法。属性类的公共构造函数控制在将属性附加到程序实体时所必须提供的信息。可以通过引用属性 (attribute) 类的公共读写属性 (property) 提供附加信息(例如前面对
Topic 属性的引用)。
下面的示例演示如何使用反射在运行时检索给定程序实体的属性信息。
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")); } }
当通过反射请求特定属性时,将使用程序源中提供的信息调用属性类的构造函数,并返回生成的属性实例。如果通过属性 (property) 提供了附加信息,那些属性 (property) 将在返回属性 (attribute) 实例之前被设置