C#6.0语言规范(一) 介绍

C#(发音为“See Sharp”)是一种简单,现代,面向对象,类型安全的编程语言。C#源于C语言系列,对C,C ++和Java程序员来说很熟悉。EC#International将EC#标准化为ECMA-334标准,ISO / IEC标准化为ISO / IEC 23270标准。Microsoft的.NET Framework C#编译器是这两个标准的一致性实现。

C#是一种面向对象的语言,但C#还包括对面向组件编程的支持当代软件设计越来越依赖于自包含和自描述功能包形式的软件组件。这些组件的关键是它们呈现具有属性,方法和事件的编程模型; 它们具有提供有关组件的声明性信息的属性; 他们合并了自己的文件。C#提供直接支持这些概念的语言结构,使C#成为一种非常自然的语言,可以在其中创建和使用软件组件。

几个C#功能有助于构建强大而持久的应用程序:垃圾收集自动回收未使用对象占用的内存; 异常处理提供了一种结构化和可扩展的错误检测和恢复方法; 并且语言类型安全设计使得无法从未初始化的变量读取,将数组索引超出其边界,或者执行未经检查的类型转换。

C#有一个统一的类型系统所有C#类型,包括诸如int和的原始类型double,都从单个根object类型继承因此,所有类型共享一组公共操作,并且可以以一致的方式存储,传输和操作任何类型的值。此外,C#支持用户定义的引用类型和值类型,允许动态分配对象以及轻量级结构的内联存储。

为了确保C#程序和库能够以兼容的方式随着时间的推移而发展,C#的设计中的版本控制已经得到了很大的重视许多编程语言很少关注这个问题,因此,当引入新版本的依赖库时,使用这些语言编写的程序会比必要时更频繁地中断。直接受版本控制考虑因素影响的C#设计方面包括单独的virtualoverride修饰符,方法重载决策的规则以及对显式接口成员声明的支持。

本章的其余部分描述了C#语言的基本功能。虽然后面的章节以面向细节的方式描述了规则和例外,有时甚至是数学方式,但本章的目的是为了清晰和简洁而牺牲完整性。目的是向读者提供有助于编写早期程序和阅读后续章节的语言的介绍。

你好,世界

“Hello,World”程序传统上用于介绍编程语言。这是在C#中:

1 using System;
2 
3 class Hello
4 {
5     static void Main() {
6         Console.WriteLine("Hello, World");
7     }
8 }

C#源文件通常具有文件扩展名.cs假设“Hello,World”程序存储在文件中hello.cs,可以使用命令行使用Microsoft C#编译器编译程序

  1 csc hello.cs 
产生一个名为的可执行组件 hello.exe 此应用程序运行时产生的输出是
 1 Hello, World 

“Hello,World”程序以using引用System命名空间指令开头命名空间提供了组织C#程序和库的分层方法。命名空间包含类型和其他命名空间 - 例如,System命名空间包含许多类型,例如Console程序中引用类,以及许多其他命名空间,例如IOCollections一个using引用给定的命名空间指令允许非限定方式使用该命名空间的成员的类型。由于该using指令,该程序可以Console.WriteLine用作速记System.Console.WriteLine

Hello由“Hello,World”程序声明类只有一个成员,名为MainMain使用static修饰符声明方法虽然实例方法可以使用关键字引用特定的封闭对象实例this,但静态方法在不引用特定对象的情况下操作。按照惯例,名为的静态方法Main用作程序的入口点。

程序的输出由命名空间WriteLine中的Console方法生成System此类由.NET Framework类库提供,默认情况下,它由Microsoft C#编译器自动引用。请注意,C#本身没有单独的运行时库。相反,.NET Framework是C#的运行时库。

计划结构

C#中的关键组织概念是程序命名空间类型成员程序集C#程序由一个或多个源文件组成。程序声明类型,包含成员,可以组织成命名空间。类和接口是类型的示例。字段,方法,属性和事件是成员的示例。编译C#程序时,它们将物理打包到程序集中。程序集通常具有文件扩展名,.exe或者.dll取决于它们是否实现应用程序

 1 using System;
 2 
 3 namespace Acme.Collections
 4 {
 5     public class Stack
 6     {
 7         Entry top;
 8 
 9         public void Push(object data) {
10             top = new Entry(top, data);
11         }
12 
13         public object Pop() {
14             if (top == null) throw new InvalidOperationException();
15             object result = top.data;
16             top = top.next;
17             return result;
18         }
19 
20         class Entry
21         {
22             public Entry next;
23             public object data;
24 
25             public Entry(Entry next, object data) {
26                 this.next = next;
27                 this.data = data;
28             }
29         }
30     }
31 }

声明在名为Stack的命名空间中命名的类Acme.Collections此类的完全限定名称是Acme.Collections.Stack该类包含几个成员:一个名为场top两个方法命名,PushPop,并命名为嵌套类EntryEntry班还包含三个成员:一个名为场next,一场名为data和一个构造函数。假设示例的源代码存储在文件中acme.cs,命令行

 1 csc /t:library acme.cs 

将示例编译为库(没有Main入口点的代码)并生成一个名为的程序集acme.dll

程序集包含中间语言(IL)指令形式的可执行代码,以及元数据形式的符号信息在执行之前,程序集中的IL代码将由.NET公共语言运行时的即时(JIT)编译器自动转换为特定于处理器的代码。

因为程序集是包含代码和元数据的自描述功能单元,所以#includeC#中不需要指令和头文件。特定程序集中包含的公共类型和成员只需在编译程序时引用该程序集即可在C#程序中使用。例如,此程序使用程序集中的Acme.Collections.Stackacme.dll

 1 using System;
 2 using Acme.Collections;
 3 
 4 class Test
 5 {
 6     static void Main() {
 7         Stack s = new Stack();
 8         s.Push(1);
 9         s.Push(10);
10         s.Push(100);
11         Console.WriteLine(s.Pop());
12         Console.WriteLine(s.Pop());
13         Console.WriteLine(s.Pop());
14     }
15 }

如果程序存储在文件中test.cstest.cs则编译时,acme.dll可以使用编译器/r选项引用程序集

csc /r:acme.dll test.cs

这将创建一个名为的可执行程序集test.exe,在运行时会生成输出:

1 100

2 10

3 1 

C#允许将程序的源文本存储在多个源文件中。编译多文件C#程序时,所有源文件一起处理,源文件可以自由地相互引用 - 概念上,就好像所有源文件在处理之前被连接成一个大文件一样。C#中从不需要前向声明,因为除极少数例外情况外,声明顺序无关紧要。C#不限制源文件仅声明一个公共类型,也不要求源文件的名称与源文件中声明的类型匹配。

类型和变量

C#中有两种类型:值类型引用类型值类型的变量直接包含它们的数据,而引用类型的变量存储对其数据的引用,后者称为对象。对于引用类型,两个变量可以引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于值类型,每个变量都有自己的数据副本,并且一个上的操作不可能影响另一个(除了refout参数变量之外)。

C#的值类型进一步分为简单类型枚举类型结构类型可空类型,C#的引用类型又分为类类型接口类型数组类型委托类型

下表概述了C#的类型系统。

类别   描述
价值类型 简单的类型 符号整型:sbyteshortintlong
    无符号整型:byteushortuintulong
    Unicode字符: char
    IEEE浮点:floatdouble
    高精度十进制: decimal
    布尔: bool
  枚举类型 用户定义的表单类型 enum E {...}
  结构类型 用户定义的表单类型 struct S {...}
  可空类型 带有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×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#程序使用类型声明来创建新类型。类型声明指定新类型的名称和成员。C#的五种类型是用户可定义的:类类型,结构类型,接口类型,枚举类型和委托类型。

类类型定义包含数据成员(字段)和函数成员(方法,属性等)的数据结构。类类型支持单继承和多态,这是派生类可以扩展和专门化基类的机制。

结构类型类似于类类型,因为它表示具有数据成员和函数成员的结构。但是,与类不同,结构是值类型,不需要堆分配。结构类型不支持用户指定的继承,并且所有结构类型都隐式继承自类型object

接口类型将合同定义为一组命名的公共函数成员。实现接口的类或结构必须提供接口的函数成员的实现。接口可以从多个基接口继承,并且类或结构可以实现多个接口。

委托类型表示对具有特定参数列表和返回类型的方法的引用。委托使得可以将方法视为可以分配给变量并作为参数传递的实体。委托类似于在其他一些语言中找到的函数指针的概念,但与函数指针不同,委托是面向对象的,类型安全的。

类,结构,接口和委托类型都支持泛型,因此可以使用其他类型对它们进行参数化。

枚举类型是具有命名常量的不同类型。每个枚举类型都有一个底层类型,它必须是八种整数类型之一。枚举类型的值集与基础类型的值集相同。

C#支持任何类型的单维和多维数组。与上面列出的类型不同,数组类型在使用之前不必声明。而是通过使用带方括号的类型名称来构造数组类型。例如,int[]是一个一维数组intint[,]是一个二维阵列int,和int[][]是的一维数组的一维阵列int

可以使用Nullable类型之前也不必声明它们。对于每个非可空值类型,T存在相应的可空类型T?,其可以保存附加值null例如,int?是一种可以保存任何32位整数或值的类型null

C#的类型系统是统一的,任何类型的值都可以被视为一个对象。C#中的每个类型都直接或间接地从object类类型派生,并且object是所有类型的最终基类。只需将值视为类型,即可将引用类型的值视为对象object值类型的值则通过执行当作对象装箱拆箱操作。在以下示例中,将int值转换为object,然后再转换int

 1 using System;
 2 
 3 class Test
 4 {
 5     static void Main() {
 6         int i = 123;
 7         object o = i;          // Boxing
 8         int j = (int)o;        // Unboxing
 9     }
10 }

当值类型的值转换为类型时object,将分配一个对象实例(也称为“框”)来保存该值,并将该值复制到该框中。相反,当object引用转换为值类型时,将检查引用的对象是否为正确值类型的框,如果检查成功,则复制框中的值。

C#的统一类型系统有效地意味着值类型可以“按需”成为对象。由于统一,使用类型的通用库object可以与引用类型和值类型一起使用。

C#中有几种变量,包括字段,数组元素,局部变量和参数。变量表示存储位置,每个变量都有一个类型,用于确定可以在变量中存储的值,如下表所示。

变量类型 可能的内容
不可为空的值类型 该确切类型的值
可空值类型 空值或该确切类型的值
object 空引用,对任何引用类型的对象的引用,或对任何值类型的盒装值的引用
班级类型 空引用,对该类类型的实例的引用,或对从该类类型派生的类的实例的引用
接口类型 空引用,对实现该接口类型的类类型实例的引用,或对实现该接口类型的值类型的盒装值的引用
数组类型 空引用,对该数组类型的实例的引用,或对兼容数组类型的实例的引用
委托类型 空引用或对该委托类型的实例的引用

表达式

表达式操作数运算符构成表达式的运算符指示要应用于操作数的操作。运营商的例子包括+-*/,和new操作数的示例包括文字,字段,局部变量和表达式。

当表达式包含多个运算符时,运算符的优先级控制各个运算符的计算顺序。例如,表达式x + y * z的计算结果是x + (y * z)因为*运算符的优先级高于+运算符。

大多数运营商都可能过载运算符重载允许为其中一个或两个操作数是用户定义的类或结构类型的操作指定用户定义的运算符实现。

下表总结了C#的运算符,按从高到低的优先顺序列出了运算符类别。同一类别的运营商具有相同的优先权。

类别 表达 描述
x.m 会员访问权限
  x(...) 方法和委托调用
  x[...] 数组和索引器访问
  x++ 后递增
  x-- 后减
  new T(...) 对象和委托创建
  new T(...){...} 使用初始化程序创建对象
  new {...} 匿名对象初始化程序
  new T[...] 数组创建
  typeof(T) 获取System.Type对象T
  checked(x) 在检查的上下文中评估表达式
  unchecked(x) 在未选中的上下文中评估表达式
  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 true如果xTfalse返回否则
  x as T 返回x类型为T,或者null如果x不是T
平等 x == y 等于
  x != y 不相等
逻辑和 x & y 整数按位AND,布尔逻辑AND
逻辑异或 x ^ y 整数按位XOR,布尔逻辑XOR
逻辑或 `X 和`
有条件的AND x && y 评估yxtrue
条件OR `X  
无法合并 X ?? y 否则评估yif x是否nullx
条件 x ? y : z 评估y如果xtruez如果xfalse
作业或匿名功能 x = y 分配
  x op= y 复合赋值; 支持的运算符是*= /= %= += -= <<= >>= &= ^=`
  (T x) => y 匿名函数(lambda表达式)

声明

程序的动作用语句表示C#支持几种不同类型的语句,其中一些语句是根据嵌入语句定义的。

一个允许在一个单一的语句允许上下文中编写多条语句。一个块由一个在分隔符{}之间写的语句列表组成

声明语句用于声明局部变量和常量。

表达式语句用于计算表达式。可用作语句的表达式包括方法调用,使用new运算符的对象分配,使用赋值=和复合赋值运算符,使用++--运算符和await表达式的递增和递减操作

Selection语句用于根据某个表达式的值选择一些可能的语句来执行。在这组中是ifswitch语句。

迭代语句用于重复执行嵌入语句。在这组是whiledofor,和foreach语句。

跳转语句用于传输控制。在这组是breakcontinuegotothrowreturn,和yield语句。

try... catch语句用于捕获在块的执行期间发生的异常,并try... finally语句用于指定始终执行终止代码,是否发生异常。

checkedunchecked语句用于控制整型算术运算和转换的溢出检查上下文。

lock语句用于获取给定对象的互斥锁,执行语句,然后释放锁。

using语句用于获取资源,执行语句,然后处置该资源。

以下是各种陈述的示例

局部变量声明

1 static void Main() {
2    int a;
3    int b = 2, c = 3;
4    a = 1;
5    Console.WriteLine(a + b + c);
6 }

本地常量声明

1 static void Main() {
2     const float pi = 3.1415927f;
3     const int r = 25;
4     Console.WriteLine(pi * r * r);
5 }

if 声明

1 static void Main(string[] args) {
2     if (args.Length == 0) {
3         Console.WriteLine("No arguments");
4     }
5     else {
6         Console.WriteLine("One or more arguments");
7     }
8 }

switch 声明

 1 static void Main(string[] args) {
 2     int n = args.Length;
 3     switch (n) {
 4         case 0:
 5             Console.WriteLine("No arguments");
 6             break;
 7         case 1:
 8             Console.WriteLine("One argument");
 9             break;
10         default:
11             Console.WriteLine("{0} arguments", n);
12             break;
13     }
14 }

while 声明

1 static void Main(string[] args) {
2     int i = 0;
3     while (i < args.Length) {
4         Console.WriteLine(args[i]);
5         i++;
6     }
7 }

do 声明

1 static void Main() {
2     string s;
3     do {
4         s = Console.ReadLine();
5         if (s != null) Console.WriteLine(s);
6     } while (s != null);
7 }

for 声明

1 static void Main(string[] args) {
2     for (int i = 0; i < args.Length; i++) {
3         Console.WriteLine(args[i]);
4     }
5 }

foreach 声明

1 static void Main(string[] args) {
2     foreach (string s in args) {
3         Console.WriteLine(s);
4     }
5 }

break 声明

1 static void Main() {
2     while (true) {
3         string s = Console.ReadLine();
4         if (s == null) break;
5         Console.WriteLine(s);
6     }
7 }

continue 声明

1 static void Main(string[] args) {
2     for (int i = 0; i < args.Length; i++) {
3         if (args[i].StartsWith("/")) continue;
4         Console.WriteLine(args[i]);
5     }
6 }

goto 声明

1 static void Main(string[] args) {
2     int i = 0;
3     goto check;
4     loop:
5     Console.WriteLine(args[i++]);
6     check:
7     if (i < args.Length) goto loop;
8 }

return 声明

1 static int Add(int a, int b) {
2     return a + b;
3 }
4 
5 static void Main() {
6     Console.WriteLine(Add(1, 2));
7     return;
8 }

yield 声明

 1 static IEnumerable<int> Range(int from, int to) {
 2     for (int i = from; i < to; i++) {
 3         yield return i;
 4     }
 5     yield break;
 6 }
 7 
 8 static void Main() {
 9     foreach (int x in Range(-10,10)) {
10         Console.WriteLine(x);
11     }
12 }

throwtry陈述

 1 static double Divide(double x, double y) {
 2     if (y == 0) throw new DivideByZeroException();
 3     return x / y;
 4 }
 5 
 6 static void Main(string[] args) {
 7     try {
 8         if (args.Length != 2) {
 9             throw new Exception("Two numbers required");
10         }
11         double x = double.Parse(args[0]);
12         double y = double.Parse(args[1]);
13         Console.WriteLine(Divide(x, y));
14     }
15     catch (Exception e) {
16         Console.WriteLine(e.Message);
17     }
18     finally {
19         Console.WriteLine("Good bye!");
20     }
21 }

checkedunchecked陈述

1 static void Main() {
2     int i = int.MaxValue;
3     checked {
4         Console.WriteLine(i + 1);        // Exception
5     }
6     unchecked {
7         Console.WriteLine(i + 1);        // Overflow
8     }
9 }

lock 声明

 1 class Account
 2 {
 3     decimal balance;
 4     public void Withdraw(decimal amount) {
 5         lock (this) {
 6             if (amount > balance) {
 7                 throw new Exception("Insufficient funds");
 8             }
 9             balance -= amount;
10         }
11     }
12 }

using 声明

1 static void Main() {
2     using (TextWriter w = File.CreateText("test.txt")) {
3         w.WriteLine("Line one");
4         w.WriteLine("Line two");
5         w.WriteLine("Line three");
6     }
7 }

类和对象

是C#类型中最基本的类。类是一种将状态(字段)和操作(方法和其他函数成员)组合在一个单元中的数据结构。类为动态创建的类实例提供定义,也称为对象类支持继承多态,这是派生类可以扩展和专门化基类的机制

使用类声明创建新类。类声明以标头开头,该标头指定类的属性和修饰符,类的名称,基类(如果给定)以及类实现的接口。标题后跟类主体,它由在分隔符{分隔符之间写入的成员声明列表组成}

以下是一个名为的简单类的声明Point

1 public class Point
2 {
3     public int x, y;
4 
5     public Point(int x, int y) {
6         this.x = x;
7         this.y = y;
8     }
9 }

类的实例是使用new运算符创建的,该运算符为新实例分配内存,调用构造函数初始化实例,并返回对实例的引用。以下语句创建两个Point对象,并在两个变量中存储对这些对象的引用:

1 Point p1 = new Point(0, 0);

2 Point p2 = new Point(10, 20); 

当对象不再使用时,对象占用的内存将自动回收。在C#中显式释放对象既不必要也不可能。

会员

类的成员是静态成员实例成员静态成员属于类,实例成员属于对象(类的实例)。

下表概述了类可以包含的成员类型。

会员 描述
常量 与类关联的常量值
字段 班级的变量
方法 可以由班级执行的计算和操作
属性 与读取和编写类的命名属性相关的操作
索引 与索引类的实例(如数组)相关联的操作
活动 可以由类生成的通知
运营商 类支持的转换和表达式运算符
构造函数 初始化类的实例或类本身所需的操作
驱逐舰 在类的实例之前执行的操作将被永久丢弃
类型 类声明的嵌套类型

无障碍

类的每个成员都有一个关联的辅助功能,它控制能够访问该成员的程序文本区域。可访问性有五种可能的形式。这些总结在下表中。

无障碍 含义
public 访问不受限制
protected 访问仅限于此类或从此类派生的类
internal 访问仅限于此计划
protected internal 访问仅限于此程序或从此类派生的类
private 访问仅限于此课程

输入参数

类定义可以通过使用包含类型参数名称列表的尖括号跟随类名来指定一组类型参数。类型参数可以在类声明的主体中使用,以定义类的成员。在以下示例中,类型参数PairTFirstTSecond

1 public class Pair<TFirst,TSecond>
2 {
3     public TFirst First;
4     public TSecond Second;
5 }

声明为采用类型参数的类类型称为泛型类类型。结构,接口和委托类型也可以是通用的。

使用泛型类时,必须为每个类型参数提供类型参数:

1 Pair<int,string> pair = new Pair<int,string> { First = 1, Second = "two" };
2 int i = pair.First;     // TFirst is int
3 string s = pair.Second; // TSecond is string

提供类型参数的泛型类型(Pair<int,string>如上所述)称为构造类型。

基础类

类声明可以通过跟随类名和类型参数以及冒号和基类的名称来指定基类。省略基类规范与从类型派生相同object在以下示例中,基类为Point3Dis Point,基类Pointobject

 1 public class Point
 2 {
 3     public int x, y;
 4 
 5     public Point(int x, int y) {
 6         this.x = x;
 7         this.y = y;
 8     }
 9 }
10 
11 public class Point3D: Point
12 {
13     public int z;
14 
15     public Point3D(int x, int y, int z): base(x, y) {
16         this.z = z;
17     }
18 }

类继承其基类的成员。继承意味着类隐式包含其基类的所有成员,但实例和静态构造函数以及基类的析构函数除外。派生类可以向其继承的成员添加新成员,但不能删除继承成员的定义。在前面的例子中,Point3D继承xy从领域Point,每一个Point3D实例都包含三个字段,xy,和z

存在从类类型到其任何基类类型的隐式转换。因此,类类型的变量可以引用该类的实例或任何派生类的实例。例如,给定前面的类声明,类型的变量Point可以引用a Point或a Point3D

1 Point a = new Point(10, 20);

2 Point b = new Point3D(10, 20, 30); 

字段

字段是与类或类的实例关联的变量。

使用static修饰符声明的字段定义静态字段静态字段只标识一个存储位置。无论创建了多少个类实例,都只有一个静态字段的副本。

声明没有static修饰符的字段定义实例字段类的每个实例都包含该类的所有实例字段的单独副本。

在下面的例子中,每个实例Color类具有的一个单独的副本rgb实例字段,但只有一个的副本BlackWhiteRedGreen,和Blue静态字段:

 1 public class Color
 2 {
 3     public static readonly Color Black = new Color(0, 0, 0);
 4     public static readonly Color White = new Color(255, 255, 255);
 5     public static readonly Color Red = new Color(255, 0, 0);
 6     public static readonly Color Green = new Color(0, 255, 0);
 7     public static readonly Color Blue = new Color(0, 0, 255);
 8     private byte r, g, b;
 9 
10     public Color(byte r, byte g, byte b) {
11         this.r = r;
12         this.g = g;
13         this.b = b;
14     }
15 }

如前面的示例所示,可以使用修饰符声明只读字段readonlyreadonly字段的赋值只能作为字段声明的一部分或在同一类的构造函数中出现。

方法

方法是实现可以由对象或类执行的计算或操作的部件。通过类访问静态方法通过类的实例访问实例方法

方法有一个(可能是空的)参数列表,它表示传递给方法的值或变量引用,以及一个返回类型,它指定方法计算和返回的值的类型。方法的返回类型是void它不返回值。

与类型类似,方法也可能有一组类型参数,在调用方法时必须为其指定类型参数。与类型不同,类型参数通常可以从方法调用的参数推断出来,不需要明确给出。

方法签名在声明方法的类中必须是唯一的。方法的签名包括方法的名称,类型参数的数量以及数量,修饰符和参数类型。方法的签名不包括返回类型。

参数

参数用于将值或变量引用传递给方法。方法的参数调用方法时指定的参数中获取其实际值有四种参数:值参数,参考参数,输出参数和参数数组。

值参数被用于输入参数的传递。值参数对应于一个局部变量,该局部变量从为参数传递的参数中获取其初始值。对value参数的修改不会影响为参数传递的参数。

通过指定默认值,可以选择值参数,以便可以省略相应的参数。

基准参数是用于输入和输出参数的传递。为参考参数传递的参数必须是变量,并且在执行方法期间,引用参数表示与参数变量相同的存储位置。使用ref修饰符声明引用参数以下示例显示了ref参数的使用

 1 using System;
 2 
 3 class Test
 4 {
 5     static void Swap(ref int x, ref int y) {
 6         int temp = x;
 7         x = y;
 8         y = temp;
 9     }
10 
11     static void Main() {
12         int i = 1, j = 2;
13         Swap(ref i, ref j);
14         Console.WriteLine("{0} {1}", i, j);            // Outputs "2 1"
15     }
16 }

一个输出参数被用于输出参数的传递。输出参数类似于引用参数,除了调用者提供的参数的初始值不重要。使用out修饰符声明输出参数以下示例显示了out参数的使用

 1 using System;
 2 
 3 class Test
 4 {
 5     static void Divide(int x, int y, out int result, out int remainder) {
 6         result = x / y;
 7         remainder = x % y;
 8     }
 9 
10     static void Main() {
11         int res, rem;
12         Divide(10, 3, out res, out rem);
13         Console.WriteLine("{0} {1}", res, rem);    // Outputs "3 1"
14     }
15 }

参数数组允许将传递给方法的参数个数可变。使用params修饰符声明参数数组只有方法的最后一个参数可以是参数数组,参数数组的类型必须是一维数组类型。该类WriteWriteLine方法System.Console是参数数组使用的很好的例子。它们声明如下。

1 public class Console
2 {
3     public static void Write(string fmt, params object[] args) {...}
4     public static void WriteLine(string fmt, params object[] args) {...}
5     ...
6 }

在使用参数数组的方法中,参数数组的行为与数组类型的常规参数完全相同。但是,在使用参数数组调用方法时,可以传递参数数组类型的单个参数或参数数组的元素类型的任意数量的参数。在后一种情况下,将使用给定的参数自动创建和初始化数组实例。这个例子

1 Console.WriteLine("x={0} y={1} z={2}", x, y, z);

相当于写下面的内容。

1 string s = "x={0} y={1} z={2}";
2 object[] args = new object[3];
3 args[0] = x;
4 args[1] = y;
5 args[2] = z;
6 Console.WriteLine(s, args);

方法体和局部变量

方法的主体指定在调用方法时要执行的语句。

方法体可以声明特定于方法调用的变量。这些变量称为局部变量局部变量声明指定类型名称,变量名称以及可能的初始值。以下示例声明一个i初始值为零的局部变量和一个j没有初始值的局部变量

 1 using System;
 2 
 3 class Squares
 4 {
 5     static void Main() {
 6         int i = 0;
 7         int j;
 8         while (i < 10) {
 9             j = i * i;
10             Console.WriteLine("{0} x {0} = {1}", i, j);
11             i = i + 1;
12         }
13     }
14 }

C#需要在可以获得其值之前明确赋值例如,如果前面的声明i不包含初始值,则编译器将报告后续用法的错误,i因为i在程序中的那些点处不会明确分配。

方法可以使用return语句将控制权返回给其调用者。在返回的方法中voidreturn语句不能指定表达式。在返回非的方法中voidreturn语句必须包含计算返回值的表达式。

静态和实例方法

使用static修饰符声明的方法静态方法静态方法不能在特定实例上运行,只能直接访问静态成员。

声明没有static修饰符的方法实例方法实例方法在特定实例上运行,并且可以访问静态成员和实例成员。可以显式访问调用实例方法的实例thisthis在静态方法中引用是错误的

以下Entity类具有静态成员和实例成员。

 1 class Entity
 2 {
 3     static int nextSerialNo;
 4     int serialNo;
 5 
 6     public Entity() {
 7         serialNo = nextSerialNo++;
 8     }
 9 
10     public int GetSerialNo() {
11         return serialNo;
12     }
13 
14     public static int GetNextSerialNo() {
15         return nextSerialNo;
16     }
17 
18     public static void SetNextSerialNo(int value) {
19         nextSerialNo = value;
20     }
21 }

每个Entity实例都包含一个序列号(可能还有其他一些未在此处显示的信息)。Entity构造(类似于实例方法)初始化与下一个可用的序号新实例。因为构造函数是实例成员,所以允许访问serialNo实例字段和nextSerialNo静态字段。

GetNextSerialNoSetNextSerialNo静态方法可以访问nextSerialNo静态字段,但是对他们直接访问这将是一个错误serialNo实例字段。

以下示例显示了Entity该类的用法

 1 using System;
 2 
 3 class Test
 4 {
 5     static void Main() {
 6         Entity.SetNextSerialNo(1000);
 7         Entity e1 = new Entity();
 8         Entity e2 = new Entity();
 9         Console.WriteLine(e1.GetSerialNo());           // Outputs "1000"
10         Console.WriteLine(e2.GetSerialNo());           // Outputs "1001"
11         Console.WriteLine(Entity.GetNextSerialNo());   // Outputs "1002"
12     }
13 }

请注意,在类上调用SetNextSerialNoGetNextSerialNo静态方法,而在类的GetSerialNo实例上调用实例方法。

虚拟,覆盖和抽象方法

当实例方法声明包含virtual修饰符时,该方法被称为虚方法当不存在virtual修饰符时,该方法被称为非虚方法

调用虚方法,进行该调用的实例运行时类型决定了要调用的实际方法实现。在非虚方法调用中,实例编译时类型是决定因素。

可以在派生类中重写虚方法当实例方法声明包含override修饰符时,该方法将覆盖具有相同签名的继承虚拟方法。虚拟方法声明引入了新方法,而覆盖方法声明通过提供该方法的新实现来专门化现有的继承虚拟方法。

一个抽象的方法是没有实现一个虚拟的方法。使用abstract修饰符声明抽象方法,并且只允许在声明的类中使用abstract必须在每个非抽象派生类中重写抽象方法。

下面的示例声明一个抽象类,Expression,它代表一个表达式树节点和三个派生类ConstantVariableReference以及Operation,它实现了常量,变量引用和算术运算表达式树的节点。(这类似于,但不要与表达式树类型中引入的表达式树类型混淆)。

 1 using System;
 2 using System.Collections;
 3 
 4 public abstract class Expression
 5 {
 6     public abstract double Evaluate(Hashtable vars);
 7 }
 8 
 9 public class Constant: Expression
10 {
11     double value;
12 
13     public Constant(double value) {
14         this.value = value;
15     }
16 
17     public override double Evaluate(Hashtable vars) {
18         return value;
19     }
20 }
21 
22 public class VariableReference: Expression
23 {
24     string name;
25 
26     public VariableReference(string name) {
27         this.name = name;
28     }
29 
30     public override double Evaluate(Hashtable vars) {
31         object value = vars[name];
32         if (value == null) {
33             throw new Exception("Unknown variable: " + name);
34         }
35         return Convert.ToDouble(value);
36     }
37 }
38 
39 public class Operation: Expression
40 {
41     Expression left;
42     char op;
43     Expression right;
44 
45     public Operation(Expression left, char op, Expression right) {
46         this.left = left;
47         this.op = op;
48         this.right = right;
49     }
50 
51     public override double Evaluate(Hashtable vars) {
52         double x = left.Evaluate(vars);
53         double y = right.Evaluate(vars);
54         switch (op) {
55             case '+': return x + y;
56             case '-': return x - y;
57             case '*': return x * y;
58             case '/': return x / y;
59         }
60         throw new Exception("Unknown operator");
61     }
62 }

前四个类可用于对算术表达式进行建模。例如,使用这些类的实例,表达式x + 3可以表示如下。

1 Expression e = new Operation(
2     new VariableReference("x"),
3     '+',
4     new Constant(3));

调用实例Evaluate方法Expression来评估给定的表达式并生成一个double值。该方法将参数a作为参数Hashtable包含变量名称(作为条目的键)和值(作为条目的值)。Evaluate方法是一种虚拟抽象方法,这意味着非抽象派生类必须覆盖它以提供实际实现。

一个Constant人的实现Evaluate简单地返回存储的常量。一个VariableReference实现在哈希表中查找变量名并返回结果值。一个Operation实现首先评估左右操作数(通过递归调用它们的Evaluate方法),然后执行给定的算术运算。

以下程序使用Expression类来计算x * (y + 2)不同值x的表达式y

 1 using System;
 2 using System.Collections;
 3 
 4 class Test
 5 {
 6     static void Main() {
 7         Expression e = new Operation(
 8             new VariableReference("x"),
 9             '*',
10             new Operation(
11                 new VariableReference("y"),
12                 '+',
13                 new Constant(2)
14             )
15         );
16         Hashtable vars = new Hashtable();
17         vars["x"] = 3;
18         vars["y"] = 5;
19         Console.WriteLine(e.Evaluate(vars));        // Outputs "21"
20         vars["x"] = 1.5;
21         vars["y"] = 9;
22         Console.WriteLine(e.Evaluate(vars));        // Outputs "16.5"
23     }
24 }

方法重载

方法重载允许同一类中的多个方法具有相同的名称,只要它们具有唯一的签名即可。在编译重载方法的调用时,编译器使用重载决策来确定要调用的特定方法。如果找不到单个最佳匹配,则重载决策会找到最匹配参数的一种方法或报告错误。以下示例显示了有效的重载决策。Main方法中每次调用的注释都显示实际调用的方法。

 1 class Test
 2 {
 3     static void F() {
 4         Console.WriteLine("F()");
 5     }
 6 
 7     static void F(object x) {
 8         Console.WriteLine("F(object)");
 9     }
10 
11     static void F(int x) {
12         Console.WriteLine("F(int)");
13     }
14 
15     static void F(double x) {
16         Console.WriteLine("F(double)");
17     }
18 
19     static void F<T>(T x) {
20         Console.WriteLine("F<T>(T)");
21     }
22 
23     static void F(double x, double y) {
24         Console.WriteLine("F(double, double)");
25     }
26 
27     static void Main() {
28         F();                 // Invokes F()
29         F(1);                // Invokes F(int)
30         F(1.0);              // Invokes F(double)
31         F("abc");            // Invokes F(object)
32         F((double)1);        // Invokes F(double)
33         F((object)1);        // Invokes F(object)
34         F<int>(1);           // Invokes F<T>(T)
35         F(1, 1);             // Invokes F(double, double)
36     }
37 }

如示例所示,始终可以通过将参数显式地转换为确切的参数类型和/或显式提供类型参数来选择特定方法。

其他功能成员

包含可执行代码的成员统称为类的函数成员上一节描述了方法,它们是函数成员的主要类型。本节介绍C#支持的其他类型的函数成员:构造函数,属性,索引器,事件,运算符和析构函数。

以下代码显示了一个名为的泛型类List<T>,它实现了一个可增长的对象列表。该类包含几种最常见的函数成员的几个示例。

 1 public class List<T> {
 2     // Constant...
 3     const int defaultCapacity = 4;
 4 
 5     // Fields...
 6     T[] items;
 7     int count;
 8 
 9     // Constructors...
10     public List(int capacity = defaultCapacity) {
11         items = new T[capacity];
12     }
13 
14     // Properties...
15     public int Count {
16         get { return count; }
17     }
18     public int Capacity {
19         get {
20             return items.Length;
21         }
22         set {
23             if (value < count) value = count;
24             if (value != items.Length) {
25                 T[] newItems = new T[value];
26                 Array.Copy(items, 0, newItems, 0, count);
27                 items = newItems;
28             }
29         }
30     }
31 
32     // Indexer...
33     public T this[int index] {
34         get {
35             return items[index];
36         }
37         set {
38             items[index] = value;
39             OnChanged();
40         }
41     }
42 
43     // Methods...
44     public void Add(T item) {
45         if (count == Capacity) Capacity = count * 2;
46         items[count] = item;
47         count++;
48         OnChanged();
49     }
50     protected virtual void OnChanged() {
51         if (Changed != null) Changed(this, EventArgs.Empty);
52     }
53     public override bool Equals(object other) {
54         return Equals(this, other as List<T>);
55     }
56     static bool Equals(List<T> a, List<T> b) {
57         if (a == null) return b == null;
58         if (b == null || a.count != b.count) return false;
59         for (int i = 0; i < a.count; i++) {
60             if (!object.Equals(a.items[i], b.items[i])) {
61                 return false;
62             }
63         }
64         return true;
65     }
66 
67     // Event...
68     public event EventHandler Changed;
69 
70     // Operators...
71     public static bool operator ==(List<T> a, List<T> b) {
72         return Equals(a, b);
73     }
74     public static bool operator !=(List<T> a, List<T> b) {
75         return !Equals(a, b);
76     }
77 }

构造函数

C#支持实例和静态构造函数。一个实例构造函数是实现初始化类实例所需操作的成员。一个静态构造函数是实现初始化类本身是第一次加载时,需要操作的成员。

构造函数被声明为一个没有返回类型且与包含类名称相同的方法。如果构造函数声明包含static修饰符,则它声明一个静态构造函数。否则,它声明一个实例构造函数。

实例构造函数可以重载。例如,List<T>该类声明了两个实例构造函数,一个没有参数,另一个带int参数。使用new运算符调用实例构造函数以下语句List<string>使用类的每个构造函数分配两个实例List

1 List<string> list1 = new List<string>();
2 List<string> list2 = new List<string>(10);

与其他成员不同,实例构造函数不是继承的,并且除了在类中实际声明的实例之外,类没有实例构造函数。如果没有为类提供实例构造函数,则会自动提供没有参数的空构造函数。

属性

属性是字段的自然扩展。两者都是具有关联类型的命名成员,访问字段和属性的语法是相同的。但是,与字段不同,属性不表示存储位置。相反,属性具有访问器,用于指定在读取或写入值时要执行的语句。

一个属性被声明像场,除了声明与结束get存取和/或set分隔符之间写入存取{}代替分号结尾。具有get访问者和set访问者的属性是读写属性,只有get访问者的属性只读属性,只有set访问者的属性是只写属性

get访问对应于具有属性类型的返回值的参数方法。除了作为赋值的目标之外,在表达式中引用属性时,将get调用属性访问者来计算属性的值。

set存取对应于与命名的单个参数的方法value和无返回类型。当属性被作为赋值的目标或作为操作数引用++--,所述set访问器与提供新值的参数。

List<T>类声明了两个属性,CountCapacity,这是只读和读写,分别。以下是使用这些属性的示例。

1 List<string> names = new List<string>();
2 names.Capacity = 100;            // Invokes set accessor
3 int i = names.Count;             // Invokes get accessor
4 int j = names.Capacity;          // Invokes get accessor

与字段和方法类似,C#支持实例属性和静态属性。使用static修饰符声明静态属性,并在没有它的情况下声明实例属性。

属性的访问者可以是虚拟的。当属性声明包含virtualabstractoverride改性剂,它适用于该属性的访问(一个或多个)。

索引

一个分度器是使得对象以相同的方式作为数组要索引的部件。索引器的声明相似,但该成员的名称属性this,然后分隔符之间的参数列表[]参数在索引器的访问器中可用。与属性类似,索引器可以是读写,只读和只写,索引器的访问器可以是虚拟的。

List类声明了单个读写索引器接受一个int参数。索引器可以List使用int索引实例例如

1 List<string> names = new List<string>();
2 names.Add("Liz");
3 names.Add("Martha");
4 names.Add("Beth");
5 for (int i = 0; i < names.Count; i++) {
6     string s = names[i];
7     names[i] = s.ToUpper();
8 }

索引器可以重载,这意味着只要参数的数量或类型不同,类就可以声明多个索引器。

活动

一个事件是一种使类或对象,以提供通知的成员。事件声明为字段,但声明包含event关键字且类型必须是委托类型。

在声明事件成员的类中,事件的行为就像委托类型的字段(假设事件不是抽象的,并且不声明访问者)。该字段存储对委托的引用,该委托表示已添加到事件的事件处理程序。如果没有事件句柄,则该字段为null

List<T>类声明了一个事件成员叫Changed,这预示着一个新的项目已被添加到列表中。Changed事件由OnChangedvirtual方法引发,该方法首先检查事件是否为null(意味着不存在处理程序)。提出事件的概念恰好等同于调用事件所代表的委托 - 因此,没有用于引发事件的特殊语言结构。

客户端通过事件处理程序对事件做出反应事件处理程序使用+=操作员连接,并使用-=操作员删除以下示例将事件处理程序附加到a的Changed事件List<string>

 1 using System;
 2 
 3 class Test
 4 {
 5     static int changeCount;
 6 
 7     static void ListChanged(object sender, EventArgs e) {
 8         changeCount++;
 9     }
10 
11     static void Main() {
12         List<string> names = new List<string>();
13         names.Changed += new EventHandler(ListChanged);
14         names.Add("Liz");
15         names.Add("Martha");
16         names.Add("Beth");
17         Console.WriteLine(changeCount);        // Outputs "3"
18     }
19 }

对于需要控制事件的底层存储的高级方案,事件声明可以显式提供addremove访问,这有点类似于set属性访问者。

运营商

操作者是定义施加特定表达式运算符一类的实例的含义的构件。可以定义三种运算符:一元运算符,二元运算符和转换运算符。必须将所有运算符声明为publicstatic

List<T>类声明了两个运算operator==operator!=,从而赋予了新的含义应用于那些运营商的表达List情况。具体来说,运算符定义两个List<T>实例的相等性,即使用它们的Equals方法比较每个包含的对象以下示例使用==运算符比较两个List<int>实例。

 1 using System;
 2 
 3 class Test
 4 {
 5     static void Main() {
 6         List<int> a = new List<int>();
 7         a.Add(1);
 8         a.Add(2);
 9         List<int> b = new List<int>();
10         b.Add(1);
11         b.Add(2);
12         Console.WriteLine(a == b);        // Outputs "True"
13         b.Add(3);
14         Console.WriteLine(a == b);        // Outputs "False"
15     }
16 }

第一个Console.WriteLine输出True是因为两个列表包含相同数量的对象,它们具有相同顺序的相同值。如果List<T>没有定义operator==,第一个Console.WriteLine将有输出False因为ab引用不同的List<int>实例。

驱逐舰

析构函数是一种用于实现销毁一个类的实例所需操作的部件。析构函数不能包含参数,它们不能具有可访问性修饰符,也不能显式调用它们。在垃圾回收期间自动调用实例的析构函数。

允许垃圾收集器在决定何时收集对象和运行析构函数时有很大的自由度。具体来说,析构函数调用的时间不是确定性的,并且可以在任何线程上执行析构函数。由于这些原因和其他原因,只有在没有其他解决方案可行时,类才应实现析构函数。

using陈述提供了一种更好的对象破坏方法。

结构

与类一样,结构体是可以包含数据成员和函数成员的数据结构,但与类不同,结构体是值类型,不需要堆分配。结构类型的变量直接存储结构的数据,而类类型的变量存储对动态分配的对象的引用。结构类型不支持用户指定的继承,并且所有结构类型都隐式继承自类型object

结构对于具有值语义的小型数据结构特别有用。复数,坐标系中的点或字典中的键值对都是结构的好例子。对小型数据结构使用结构而不是类可以使应用程序执行的内存分配数量产生很大差异。例如,以下程序创建并初始化100个点的数组。通过Point实现为类,实例化101个单独的对象 - 一个用于数组,一个用于100个元素。

 1 class Point
 2 {
 3     public int x, y;
 4 
 5     public Point(int x, int y) {
 6         this.x = x;
 7         this.y = y;
 8     }
 9 }
10 
11 class Test
12 {
13     static void Main() {
14         Point[] points = new Point[100];
15         for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
16     }
17 }

另一种方法是制作Point一个结构。

1 struct Point
2 {
3     public int x, y;
4 
5     public Point(int x, int y) {
6         this.x = x;
7         this.y = y;
8     }
9 }

现在,只实例化一个对象 - 数组的对象 - Point实例以串联方式存储在数组中。

使用new运算符调用Struct构造函数,但这并不意味着正在分配内存。结构构造函数只是返回结构值本身(通常在堆栈上的临时位置),而不是动态分配对象并返回对它的引用,然后根据需要复制该值。

对于类,两个变量可以引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于结构体,变量每个都有自己的数据副本,并且对一个变量的操作不可能影响另一个。例如,以下代码片段生成的输出取决于Point是类还是结构。

1 Point a = new Point(10, 10);
2 Point b = a;
3 a.x = 20;
4 Console.WriteLine(b.x);

Point是类,则输出是20因为ab引用相同的对象。如果Point是结构,则输出是10因为赋值ab创建值的副本,并且此副本不受后续赋值的影响a.x

前面的例子强调了结构的两个局限性。首先,复制整个结构通常比复制对象引用效率低,因此对于结构而言,赋值和值参数传递可能比使用引用类型更昂贵。其次,除了refout参数之外,不可能创建对结构的引用,结构在许多情况下排除了它们的用法。

数组

一个阵列是一种数据结构,它包含的一定数量的通过计算索引访问的变量。包含在数组中的变量(也称为数组的元素)都是相同的类型,这种类型称为数组元素类型

数组类型是引用类型,数组变量的声明只是为数组实例的引用留出空间。实际的数组实例是在运行时使用new运算符动态创建的new操作指定新数组实例长度,然后在实例的生存期内固定该实例长度数组的元素的索引范围从0Length - 1new操作者自动初始化数组的默认值,其中,例如,对所有的数字类型和零元素null的所有引用类型。

以下示例创建int元素数组,初始化数组,并打印出数组的内容。

 1 using System;
 2 
 3 class Test
 4 {
 5     static void Main() {
 6         int[] a = new int[10];
 7         for (int i = 0; i < a.Length; i++) {
 8             a[i] = i * i;
 9         }
10         for (int i = 0; i < a.Length; i++) {
11             Console.WriteLine("a[{0}] = {1}", i, a[i]);
12         }
13     }
14 }

此示例在单维数组上创建和操作C#还支持多维数组数组类型的维数(也称为数组类型的等级)是一加上在数组类型的方括号之间写入的逗号数。以下示例分配一维,二维和三维数组。

1 int[] a1 = new int[10];
2 int[,] a2 = new int[10, 5];
3 int[,,] a3 = new int[10, 5, 2];

a1数组包含10个元素,该a2数组包含50(10×5)个元素,该a3数组包含100(10×5×2)个元素。

数组的元素类型可以是任何类型,包括数组类型。具有数组类型元素的数组有时称为锯齿状数组,因为元素数组的长度不必全部相同。以下示例分配一组数组int

1 int[][] a = new int[3][];
2 a[0] = new int[10];
3 a[1] = new int[5];
4 a[2] = new int[20];

第一行创建一个包含三个元素的数组,每个元素都有一个类型int[],每个元素的初始值都是null随后的行然后通过引用不同长度的各个数组实例来初始化这三个元素。

new操作者允许使用指定的数组元素的初始值数组初始化,这是分隔符之间写入表达式列表{}以下示例int[]使用三个元素分配和初始化a 

 1 int[] a = new int[] {1, 2, 3}; 

请注意,数组的长度是从{之间的表达式数推断出来的}可以进一步缩短局部变量和字段声明,以便不必重新调整数组类型。

 1 int[] a = {1, 2, 3}; 

前面的两个示例都等同于以下内容:

1 int[] t = new int[3];
2 t[0] = 1;
3 t[1] = 2;
4 t[2] = 3;
5 int[] a = t;

接口

接口定义可以由类和结构来实现的合同。接口可以包含方法,属性,事件和索引器。接口不提供它定义的成员的实现 - 它仅指定必须由实现接口的类或结构提供的成员。

接口可以采用多重继承在以下示例中,接口IComboBox继承自ITextBoxIListBox

 1 interface IControl
 2 {
 3     void Paint();
 4 }
 5 
 6 interface ITextBox: IControl
 7 {
 8     void SetText(string text);
 9 }
10 
11 interface IListBox: IControl
12 {
13     void SetItems(string[] items);
14 }
15 
16 interface IComboBox: ITextBox, IListBox {}

类和结构可以实现多个接口。在以下示例中,该类EditBox实现了IControlIDataBound

 1 interface IDataBound
 2 {
 3     void Bind(Binder b);
 4 }
 5 
 6 public class EditBox: IControl, IDataBound
 7 {
 8     public void Paint() {...}
 9     public void Bind(Binder b) {...}
10 }

当类或结构实现特定接口时,该类或结构的实例可以隐式转换为该接口类型。例如

1 EditBox editBox = new EditBox();

2 IControl control = editBox;

3 IDataBound dataBound = editBox; 

如果实例不是静态地知道实现特定接口,则可以使用动态类型转换。例如,以下语句使用动态类型转换来获取对象IControlIDataBound接口实现。因为对象的实际类型是EditBox,所以强制转换成功。

1 object obj = new EditBox();

2 IControl control = (IControl)obj;

3 IDataBound dataBound = (IDataBound)obj; 

在前面的EditBox类中,Paint从该方法IControl接口和Bind从所述方法IDataBound接口使用实现的public成员。C#还支持显式接口成员实现,类或结构可以使用它来避免创建成员public使用完全限定的接口成员名称编写显式接口成员实现。例如,EditBox该类可以使用显式接口成员实现来实现IControl.PaintIDataBound.Bind方法,如下所示。

1 public class EditBox: IControl, IDataBound
2 {
3     void IControl.Paint() {...}
4     void IDataBound.Bind(Binder b) {...}
5 }

只能通过接口类型访问显式接口成员。例如,IControl.Paint前一个EditBox提供的实现只能通过首先将EditBox引用转换为IControl接口类型来调用

1 EditBox editBox = new EditBox();
2 editBox.Paint();                        // Error, no such method
3 IControl control = editBox;
4 control.Paint();                        // Ok

枚举

一个枚举类型是一个独特的值类型与一组命名为常量。下面的示例声明和使用名为枚举类型Color与三个恒定值,RedGreen,和Blue

 1 using System;
 2 
 3 enum Color
 4 {
 5     Red,
 6     Green,
 7     Blue
 8 }
 9 
10 class Test
11 {
12     static void PrintColor(Color color) {
13         switch (color) {
14             case Color.Red:
15                 Console.WriteLine("Red");
16                 break;
17             case Color.Green:
18                 Console.WriteLine("Green");
19                 break;
20             case Color.Blue:
21                 Console.WriteLine("Blue");
22                 break;
23             default:
24                 Console.WriteLine("Unknown color");
25                 break;
26         }
27     }
28 
29     static void Main() {
30         Color c = Color.Red;
31         PrintColor(c);
32         PrintColor(Color.Blue);
33     }
34 }

每个枚举类型都有一个相应的整数类型,称为枚举类型基础类型。未明确声明基础类型的枚举类型具有基础类型int枚举类型的存储格式和可能值的范围由其基础类型确定。枚举类型可以采用的值集不受其枚举成员的限制。特别是,枚举的基础类型的任何值都可以强制转换为枚举类型,并且是该枚举类型的唯一有效值。

以下示例声明了一个以Alignment底层类型为名称的枚举类型sbyte

1 enum Alignment: sbyte
2 {
3     Left = -1,
4     Center = 0,
5     Right = 1
6 }

如前面的示例所示,枚举成员声明可以包含指定成员值的常量表达式。每个枚举成员的常量值必须在枚举的基础类型的范围内。当枚举成员声明未明确指定值时,该成员的值为零(如果它是枚举类型中的第一个成员)或文本前面的枚举成员的值加1。

枚举值可以使用类型转换转换为整数值,反之亦然。例如

1 int i = (int)Color.Blue;        // int i = 2;
2 Color c = (Color)2;             // Color c = Color.Blue;

任何枚举类型的默认值是转换为枚举类型的整数值零。在变量自动初始化为默认值的情况下,这是给予枚举类型变量的值。为了使枚举类型的默认值易于使用,文字0隐式转换为任何枚举类型。因此,允许以下内容。

 1 Color c = 0; 

代表

一个委托类型代表与特定参数列表和返回类型的方法的引用。委托使得可以将方法视为可以分配给变量并作为参数传递的实体。委托类似于在其他一些语言中找到的函数指针的概念,但与函数指针不同,委托是面向对象的,类型安全的。

以下示例声明并使用名为的委托类型Function

 1 using System;
 2 
 3 delegate double Function(double x);
 4 
 5 class Multiplier
 6 {
 7     double factor;
 8 
 9     public Multiplier(double factor) {
10         this.factor = factor;
11     }
12 
13     public double Multiply(double x) {
14         return x * factor;
15     }
16 }
17 
18 class Test
19 {
20     static double Square(double x) {
21         return x * x;
22     }
23 
24     static double[] Apply(double[] a, Function f) {
25         double[] result = new double[a.Length];
26         for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
27         return result;
28     }
29 
30     static void Main() {
31         double[] a = {0.0, 0.5, 1.0};
32         double[] squares = Apply(a, Square);
33         double[] sines = Apply(a, Math.Sin);
34         Multiplier m = new Multiplier(2.0);
35         double[] doubles =  Apply(a, m.Multiply);
36     }
37 }

Function委托类型的实例可以引用任何接受double参数并返回double值的方法。Apply方法将给定Function的元素应用于a double[],返回a double[]与结果。在该Main方法中,Apply用于将三种不同的函数应用于a double[]

委托可以引用静态方法(例如SquareMath.Sin在前面的示例中)或实例方法(例如m.Multiply在前面的示例中)。引用实例方法的委托也引用特定对象,并且当通过委托调用实例方法时,该对象变为this调用。

代理也可以使用匿名函数创建,这些函数是动态创建的“内联方法”。匿名函数可以查看周围方法的局部变量。因此,无需使用Multiplier,就可以更轻松地编写上面的乘数示例

 1 double[] doubles = Apply(a, (double x) => x * 2.0); 

委托的一个有趣且有用的属性是它不知道或不关心它引用的方法的类; 重要的是引用的方法具有与委托相同的参数和返回类型。

属性

C#程序中的类型,成员和其他实体支持控制其行为的某些方面的修饰符。例如,一种方法的可访问性使用受控publicprotectedinternal,和private改性剂。C#概括了此功能,以便用户定义的声明性信息类型可以附加到程序实体并在运行时检索。程序通过定义和使用属性来指定此附加声明性信息

以下示例声明了一个HelpAttribute可放置在程序实体上属性,以提供指向其相关文档的链接。

 1 using System;
 2 
 3 public class HelpAttribute: Attribute
 4 {
 5     string url;
 6     string topic;
 7 
 8     public HelpAttribute(string url) {
 9         this.url = url;
10     }
11 
12     public string Url {
13         get { return url; }
14     }
15 
16     public string Topic {
17         get { return topic; }
18         set { topic = value; }
19     }
20 }

所有属性类都派生自System.Attribute.NET Framework提供基类。可以通过在关联声明之前的方括号内提供其名称以及任何参数来应用属性。如果属性的名称结束Attribute,则在引用该属性时可以省略该部分名称。例如,HelpAttribute属性可以如下使用。

1 [Help("http://msdn.microsoft.com/.../MyClass.htm")]
2 public class Widget
3 {
4     [Help("http://msdn.microsoft.com/.../MyClass.htm", Topic = "Display")]
5     public void Display(string text) {}
6 }

此示例将a附加HelpAttributeWidget类,将另一个附加HelpAttribute中的Display方法。属性类的公共构造函数控制将属性附加到程序实体时必须提供的信息。可以通过引用属性类的公共读写属性(例如Topic先前属性的引用来提供附加信息

以下示例显示如何使用反射在运行时检索给定程序实体的属性信息。

 1 using System;
 2 using System.Reflection;
 3 
 4 class Test
 5 {
 6     static void ShowHelp(MemberInfo member) {
 7         HelpAttribute a = Attribute.GetCustomAttribute(member,
 8             typeof(HelpAttribute)) as HelpAttribute;
 9         if (a == null) {
10             Console.WriteLine("No help for {0}", member);
11         }
12         else {
13             Console.WriteLine("Help for {0}:", member);
14             Console.WriteLine("  Url={0}, Topic={1}", a.Url, a.Topic);
15         }
16     }
17 
18     static void Main() {
19         ShowHelp(typeof(Widget));
20         ShowHelp(typeof(Widget).GetMethod("Display"));
21     }
22 }

当通过反射请求特定属性时,将使用程序源中提供的信息调用属性类的构造函数,并返回结果属性实例。如果通过属性提供了其他信息,则在返回属性实例之前将这些属性设置为给定值。

猜你喜欢

转载自www.cnblogs.com/strengthen/p/9715923.html
今日推荐