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#设计方面包括单独的virtual
和override
修饰符,方法重载决策的规则以及对显式接口成员声明的支持。
本章的其余部分描述了C#语言的基本功能。虽然后面的章节以面向细节的方式描述了规则和例外,有时甚至是数学方式,但本章的目的是为了清晰和简洁而牺牲完整性。目的是向读者提供有助于编写早期程序和阅读后续章节的语言的介绍。
Hello , World
“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#编译器编译程序
csc hello.cs
hello.exe
。此应用程序运行时产生的输出是Hello, World
“Hello,World”程序以using
引用System
命名空间的指令开头。命名空间提供了组织C#程序和库的分层方法。命名空间包含类型和其他命名空间 - 例如,System
命名空间包含许多类型,例如Console
程序中引用的类,以及许多其他命名空间,例如IO
和Collections
。一个using
引用给定的命名空间指令允许非限定方式使用该命名空间的成员的类型。由于该using
指令,该程序可以Console.WriteLine
用作速记System.Console.WriteLine
。
Hello
由“Hello,World”程序声明的类只有一个成员,名为Main
。Main
使用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
两个方法命名,Push
并Pop
,并命名为嵌套类Entry
。该Entry
班还包含三个成员:一个名为场next
,一场名为data
和一个构造函数。假设示例的源代码存储在文件中acme.cs
,命令行
csc /t:library acme.cs
将示例编译为库(没有Main
入口点的代码)并生成一个名为的程序集acme.dll
。
程序集包含中间语言(IL)指令形式的可执行代码,以及元数据形式的符号信息。在执行之前,程序集中的IL代码将由.NET公共语言运行时的即时(JIT)编译器自动转换为特定于处理器的代码。
因为程序集是包含代码和元数据的自描述功能单元,所以#include
C#中不需要指令和头文件。特定程序集中包含的公共类型和成员只需在编译程序时引用该程序集即可在C#程序中使用。例如,此程序使用程序集中的Acme.Collections.Stack
类acme.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.cs
,test.cs
则编译时,acme.dll
可以使用编译器/r
选项引用程序集:
csc /r:acme.dll test.cs
这将创建一个名为的可执行程序集test.exe
,在运行时会生成输出:
1 100 2 10 3 1
C#允许将程序的源文本存储在多个源文件中。编译多文件C#程序时,所有源文件一起处理,源文件可以自由地相互引用 - 概念上,就好像所有源文件在处理之前被连接成一个大文件一样。C#中从不需要前向声明,因为除极少数例外情况外,声明顺序无关紧要。C#不限制源文件仅声明一个公共类型,也不要求源文件的名称与源文件中声明的类型匹配。
类型和变量
C#中有两种类型:值类型和引用类型。值类型的变量直接包含它们的数据,而引用类型的变量存储对其数据的引用,后者称为对象。对于引用类型,两个变量可以引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于值类型,每个变量都有自己的数据副本,并且一个上的操作不可能影响另一个(除了ref
和out
参数变量之外)。
C#的值类型进一步分为简单类型,枚举类型,结构类型和可空类型,C#的引用类型又分为类类型,接口类型,数组类型和委托类型。
下表概述了C#的类型系统。
类别 | 描述 | |
---|---|---|
价值类型 | 简单的类型 | 符号整型:sbyte ,short ,int ,long |
无符号整型:byte ,ushort ,uint ,ulong |
||
Unicode字符: char |
||
IEEE浮点:float ,double |
||
高精度十进制: 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[]
是一个一维数组int
,int[,]
是一个二维阵列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 如果x 是T ,false 则返回,否则 |
|
x as T |
返回x 类型为T ,或者null 如果x 不是T |
|
平等 | x == y |
等于 |
x != y |
不相等 | |
逻辑和 | x & y |
整数按位AND,布尔逻辑AND |
逻辑异或 | x ^ y |
整数按位XOR,布尔逻辑XOR |
逻辑或 | `X | 和` |
有条件的AND | x && y |
评估y 仅x 是true |
条件OR | `X | |
无法合并 | X ?? y |
否则,评估y if x 是否null x |
条件 | x ? y : z |
评估y 如果x 是true ,z 如果x 是false |
作业或匿名功能 | x = y |
分配 |
x op= y |
复合赋值; 支持的运算符是*= /= %= += -= <<= >>= &= ^= ` |
|
(T x) => y |
匿名函数(lambda表达式) |
声明
程序的动作用语句表示。C#支持几种不同类型的语句,其中一些语句是根据嵌入语句定义的。
一个块允许在一个单一的语句允许上下文中编写多条语句。一个块由一个在分隔符{
和}
。之间写的语句列表组成。
声明语句用于声明局部变量和常量。
表达式语句用于计算表达式。可用作语句的表达式包括方法调用,使用new
运算符的对象分配,使用赋值=
和复合赋值运算符,使用++
和--
运算符和await表达式的递增和递减操作。
Selection语句用于根据某个表达式的值选择一些可能的语句来执行。在这组中是if
和switch
语句。
迭代语句用于重复执行嵌入语句。在这组是while
,do
,for
,和foreach
语句。
跳转语句用于传输控制。在这组是break
,continue
,goto
,throw
,return
,和yield
语句。
该try
... catch
语句用于捕获在块的执行期间发生的异常,并try
... finally
语句用于指定始终执行终止代码,是否发生异常。
该checked
和unchecked
语句用于控制整型算术运算和转换的溢出检查上下文。
该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 }
throw
和try 声明
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 }
checked
和unchecked 声明
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 |
访问仅限于此课程 |
输入参数
类定义可以通过使用包含类型参数名称列表的尖括号跟随类名来指定一组类型参数。类型参数可以在类声明的主体中使用,以定义类的成员。在以下示例中,类型参数Pair
是TFirst
和TSecond
:
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
。在以下示例中,基类为Point3D
is Point
,基类Point
为object
:
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
继承x
并y
从领域Point
,每一个Point3D
实例都包含三个字段,x
,y
,和z
。
存在从类类型到其任何基类类型的隐式转换。因此,类类型的变量可以引用该类的实例或任何派生类的实例。例如,给定前面的类声明,类型的变量Point
可以引用a Point
或a Point3D
:
1 Point a = new Point(10, 20); 2 Point b = new Point3D(10, 20, 30);
字段
字段是与类或类的实例关联的变量。
使用static
修饰符声明的字段定义静态字段。静态字段只标识一个存储位置。无论创建了多少个类实例,都只有一个静态字段的副本。
声明没有static
修饰符的字段定义实例字段。类的每个实例都包含该类的所有实例字段的单独副本。
在下面的例子中,每个实例Color
类具有的一个单独的副本r
,g
和b
实例字段,但只有一个的副本Black
,White
,Red
,Green
,和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 }
如前面的示例所示,可以使用修饰符声明只读字段readonly
。对readonly
字段的赋值只能作为字段声明的一部分或在同一类的构造函数中出现。
方法
甲方法是实现可以由对象或类执行的计算或操作的部件。通过类访问静态方法。通过类的实例访问实例方法。
方法有一个(可能是空的)参数列表,它表示传递给方法的值或变量引用,以及一个返回类型,它指定方法计算和返回的值的类型。方法的返回类型是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
修饰符声明参数数组。只有方法的最后一个参数可以是参数数组,参数数组的类型必须是一维数组类型。该类的Write
和WriteLine
方法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
语句将控制权返回给其调用者。在返回的方法中void
,return
语句不能指定表达式。在返回非的方法中void
,return
语句必须包含计算返回值的表达式。
静态和实例方法
使用static
修饰符声明的方法是静态方法。静态方法不能在特定实例上运行,只能直接访问静态成员。
声明没有static
修饰符的方法是实例方法。实例方法在特定实例上运行,并且可以访问静态成员和实例成员。可以显式访问调用实例方法的实例this
。this
在静态方法中引用是错误的。
以下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
静态字段。
在GetNextSerialNo
与SetNextSerialNo
静态方法可以访问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 }
请注意,在类上调用SetNextSerialNo
和GetNextSerialNo
静态方法,而在类的GetSerialNo
实例上调用实例方法。
虚方法,覆盖和抽象方法
当实例方法声明包含virtual
修饰符时,该方法被称为虚方法。当不存在virtual
修饰符时,该方法被称为非虚方法。
调用虚方法时,进行该调用的实例的运行时类型决定了要调用的实际方法实现。在非虚方法调用中,实例的编译时类型是决定因素。
可以在派生类中重写虚方法。当实例方法声明包含override
修饰符时,该方法将覆盖具有相同签名的继承虚拟方法。虚拟方法声明引入了新方法,而覆盖方法声明通过提供该方法的新实现来专门化现有的继承虚拟方法。
一个抽象的方法是没有实现一个虚拟的方法。使用abstract
修饰符声明抽象方法,并且只允许在声明的类中使用abstract
。必须在每个非抽象派生类中重写抽象方法。
下面的示例声明一个抽象类,Expression
,它代表一个表达式树节点和三个派生类Constant
,VariableReference
以及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>
类声明了两个属性,Count
和Capacity
,这是只读和读写,分别。以下是使用这些属性的示例。
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
修饰符声明静态属性,并在没有它的情况下声明实例属性。
属性的访问者可以是虚拟的。当属性声明包含virtual
,abstract
或override
改性剂,它适用于该属性的访问(一个或多个)。
索引
一个分度器是使得对象以相同的方式作为数组要索引的部件。索引器的声明相似,但该成员的名称属性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
事件由OnChanged
virtual方法引发,该方法首先检查事件是否为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 }
对于需要控制事件的底层存储的高级方案,事件声明可以显式提供add
和remove
访问,这有点类似于set
属性的访问者。
运算符
操作数是定义施加特定表达式运算符一类的实例的含义构件。可以定义三种运算符:一元运算符,二元运算符和转换运算符。必须将所有运算符声明为public
和static
。
在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
因为a
并b
引用不同的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
因为a
并b
引用相同的对象。如果Point
是结构,则输出是10
因为赋值a
将b
创建值的副本,并且此副本不受后续赋值的影响a.x
。
前面的例子强调了结构的两个局限性。首先,复制整个结构通常比复制对象引用效率低,因此对于结构而言,赋值和值参数传递可能比使用引用类型更昂贵。其次,除了ref
和out
参数之外,不可能创建对结构的引用,结构在许多情况下排除了它们的用法。
数组
一个阵列是一种数据结构,它包含的一定数量的通过计算索引访问的变量。包含在数组中的变量(也称为数组的元素)都是相同的类型,这种类型称为数组的元素类型。
数组类型是引用类型,数组变量的声明只是为数组实例的引用留出空间。实际的数组实例是在运行时使用new
运算符动态创建的。该new
操作指定新数组实例的长度,然后在实例的生存期内固定该实例的长度。数组的元素的索引范围从0
到Length - 1
。的new
操作者自动初始化数组的默认值,其中,例如,对所有的数字类型和零元素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 。
int[] a = new int[] {1, 2, 3};
请注意,数组的长度是从{
和之间的表达式数推断出来的}
。可以进一步缩短局部变量和字段声明,以便不必重新调整数组类型。
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
继承自ITextBox
和IListBox
。
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
实现了IControl
和IDataBound
。
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;
如果实例不是静态地知道实现特定接口,则可以使用动态类型转换。例如,以下语句使用动态类型转换来获取对象IControl
和IDataBound
接口实现。因为对象的实际类型是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.Paint
和IDataBound.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
与三个恒定值,Red
,Green
,和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
隐式转换为任何枚举类型。因此,允许以下内容。
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[]
。
委托可以引用静态方法(例如Square
或Math.Sin
在前面的示例中)或实例方法(例如m.Multiply
在前面的示例中)。引用实例方法的委托也引用特定对象,并且当通过委托调用实例方法时,该对象变为this
调用。
代理也可以使用匿名函数创建,这些函数是动态创建的“内联方法”。匿名函数可以查看周围方法的局部变量。因此,无需使用Multiplier
类,就可以更轻松地编写上面的乘数示例:
double[] doubles = Apply(a, (double x) => x * 2.0);
委托的一个有趣且有用的属性是它不知道或不关心它引用的方法的类; 重要的是引用的方法具有与委托相同的参数和返回类型。
属性
C#程序中的类型,成员和其他实体支持控制其行为的某些方面的修饰符。例如,一种方法的可访问性使用受控public
,protected
,internal
,和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附加HelpAttribute
到Widget
类,将另一个附加到类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 }
当通过反射请求特定属性时,将使用程序源中提供的信息调用属性类的构造函数,并返回结果属性实例。如果通过属性提供了其他信息,则在返回属性实例之前将这些属性设置为给定值。