類(class)是C#類型中最基礎的類型。類是一個數據結構,將狀態(字段)和行為(方法和其他函數成員)組合在一個單元中。類提供了用於動態創建類實例的定義,也就是對象(object)。類支持繼承(inheritance)和多態(polymorphism),即派生類能夠擴展和特殊化基類的機制。
使用類聲明可以創建新的類。類聲明以一個聲明頭開始,其組成方式如下:先是指定類的特性和修飾符,後跟類的名字,基類(如果有的話)的名字,以及被該類實現的接口名。聲明頭後面就是類體了,它由一組包含在大括號({})中的成員聲明組成。
下面是一個名為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#中,沒有必要也不可能顯式地釋放對象。
1.6.1 成員
類的成員或者是靜態成員(static member),或者是實例成員(instance member)。靜態成員屬於類,實例成員屬於對象(類的實例)。
表1.6提供了類所能包含的各種成員的描述。
表1.6 類 的 成 員
成 員
描 述
常數
與類關聯的常量值
字段
類的變量
方法
能夠被類執行的計算和行為
屬性
使對象能夠讀取和寫入類的命名屬性
索引器
使對象能夠用與數組相同的方式進行索引
事件
能夠被類產生的通知
運算符
類支持的轉換和表達式運算符
構造函數
初始化類的實例或者類本身
析構函數
在永久銷毀類的實例之前執行的行為
類型
被類聲明的嵌套類型
1.6.2 可訪問性
類的每個成員都有關聯的可訪問性,它控制能夠訪問該成員的程序文本區域。有5種可能的可訪問性形式。表1.7概述了類的可訪問性的意義。
表1.7 類的可訪問性
可訪問性
意 義
public
訪問不受限制
protected
訪問僅限於包含類或從包含類派生的類型
internal
訪問僅限於當前程序集
protected internal
訪問僅限於從包含類派生的當前程序集或類型
private
訪問僅限於包含類
1.6.3 基類
類的聲明可能通過在類名後加上冒號和基類的名字來指定一個基類譯注4。省略基類等同於直接從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類繼承了其基類的成員。繼承意味著類將隱式地包含其基類的所有成員(除了基類的構造函數)。派生類能夠在繼承基類的基礎上增加新的成員,但是它不能移除繼承成員的定義。在前面的示例中,Point3D類從Point類中繼承了x字段和y字段,並且每一個Point3D實例都包含三個字段x,y和z。
從類類型到它的任何基類類型都存在隱式的轉換。並且,類類型的變量能夠引用該類的實例,或者任何派生類的實例。例如,對於前面給定的類聲明,Point類型的變量能夠引用Point實例或者Point3D實例:
Point a = new Point(10, 20);
Point b = new Point3D(10, 20, 30);
1.6.4 字段
字段是與對象或類相關聯的變量。
當一個字段聲明中含有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修飾符聲明只讀字段。給readonly字段的賦值只能作為聲明的組成部分出現,或者在同一類中的實例構造函數或靜態構造函數中出現。
1.6.5 方法
方法(method)是一種用於實現可以由對象或類執行的計算或操作的成員。靜態方法(static method)只能通過類來訪問。實例方法(instance method)則要通過類的實例訪問。
方法有一個參數(parameter)列表(可能為空),表示傳遞給方法的值或者引用;方法還有返回類型(return type),用於指定由該方法計算和返回的值的類型。如果方法不返回一個值,則它的返回類型為void。
在聲明方法的類中,該方法的簽名必須是惟一的。方法的簽名由它的名稱、參數的數目、每個參數的修飾符和類型組成。返回類型不是方法簽名的組成部分。
1.6.5.1 參數
參數用於將值或者引用變量傳遞給方法。當方法被調用時,方法的參數譯注5從指定的自變量(argument)譯注6得到它們實際的值。C#有4種參數:值參數、引用參數、輸出參數和參數數組。
值參數(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); //輸出 "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); //輸出 "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);
1.6.5.2 方法體和局部變量
方法體指定方法調用時所要執行的語句。
方法體能夠聲明特定於該方法調用的變量。這樣的變量被稱為局部變量(local variable)。局部變量聲明指定類型名、變量名,可能還有初始值。下面的示例聲明了一個局部變量i,其初始值為0;另一個局部變量j沒有初始值。
using System;
class Squares
{
static void Main() {
int i = 0;
int j;
while(i < 10){
j = i * i;
Console.WriteLine("{0} x {0} = {1}", i, j);
i = i + 1;
}
}
}
C#要求局部變量在其值被獲得之前明確賦值(definitely)。例如,假設前面的變量i的聲明沒有包含初始值,那麼,在接下來對i的使用將導致編譯器報告錯誤,原因就是i在程序中沒有明確賦值。
方法能夠使用return語句將控制返回給它的調用方。如果方法是void的,則return語句不能指定表達式;如果方法是非void的,則return語句必須包含表達式,用於計算返回值。
1.6.5.3 靜態方法和實例方法
若一個方法聲明中含有static修飾符,則稱該方法為靜態方法(static method)。靜態方法不對特定實例進行操作,只能訪問靜態成員。
若一個方法聲明中沒有static修飾符,則稱該方法為實例方法(instance method)。實例方法對特定實例進行操作,既能夠訪問靜態成員,也能夠訪問實例成員。在調用實例方法的實例上,可以用 this來訪問該實例,而在靜態方法中引用this是錯誤的。
下面的Entity類具有靜態和實例兩種成員:
class Entity
{
static int nextSerialNo;
int serialNo;
public Entity() {
serialNo = nextSerialNo++;
}
public int GetSerialNo() {
return serialNo;
}
public static int GetNextSerialNo() {
return nextSerialNo;
}
public static void SetNextSerialNo(int value) {
nextSerialNo = value;
}
}
每一個Entity實例包含一個序列號(並且假定這裡省略了一些其他信息)。Entity構造函數(類似於實例方法)用下一個有效的序列號初始化新的實例。因為構造函數是一個實例成員,所以,它既可以訪問serialNo實例字段,也可以訪問nextSerialNo靜態字段。
GetNextSerialNo和SetNextSerialNo靜態方法能夠訪問nextSerialNo靜態字段,但是如果訪問serialNo實例字段就會產生錯誤。
下面的示例展示了Entity類的使用:
using System;
class Test
{
static void Main() {
Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo()); //輸出 "1000"
Console.WriteLine(e2.GetSerialNo()); //輸出 "1001"
Console.WriteLine(Entity.GetNextSerialNo()); //輸出 "1002"
}
}
注意,SetNextSerialNo和GetNextSerialNo靜態方法通過類調用,而GetSerialNo實例成員則通過類的實例調用。
1.6.5.4 虛擬方法、重寫方法和抽象方法
若一個實例方法的聲明中含有virtual修飾符,則稱該方法為虛擬方法(virtual method)。若其中沒有virtual修飾符,則稱該方法為非虛擬方法(nonvirtual method)。
在一個虛擬方法調用中,該調用所涉及的實例的運行時類型(runtime type)確定了要被調用的究竟是該方法的哪一個實現。在非虛擬方法調用中,實例的編譯時類型(compile-time type)是決定性因素。
虛擬方法可以由派生類重寫(override)譯注7實現。當一個實例方法聲明中含有override修飾符時,該方法將重寫所繼承的相同簽名的虛擬方法。虛擬方法聲明用於引入新方法,而重寫方法聲明則用於使現有的繼承虛擬方法專用化(通過提供該方法的新實現)。
抽象(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");
}
}
前面的4個類用於模型化算術表達式。例如,使用這些類的實例,表達式x+3能夠被表示為如下的形式:
Expression e = new Operation(
new VariableReference("x"),
'+',
new Constant(3));
Expression實例的Evaluate方法將被調用,以計算表達式的值,從而產生一個double值。該方法取得一個包含變量名(輸入的鍵)和值(輸入的值)的Hashtable作為其自變量。Evaluate方法是虛擬的抽象方法,意味著派生類必須重寫它並提供實際的實現。
Evaluate方法的Constant的實現只是返回保存的常數。VariableReference的實現在Hashtable中查找變量名,並且返回相應的值。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)); //輸出 "21"
Vars["x"] = 1.5;
Vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); //輸出 "16.5"
}
}
1.6.5.5 方法重載
方法重載(Method 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, dpuble y) {
Console.WriteLine("F(double, double)");
}
static void Main(){
F(); //調用F()
F(1); //調用F(int)
F(1.0); //調用F(double)
F("abc"); //調用F(object)
F((double)1); //調用F(double)
F((object)1); //調用F(object)
F(1, 1); //調用F(double, double)
}
}
如上例所示,總是通過自變量到參數類型的顯式的類型轉換,來選擇特定方法。
1.6.6 其他函數成員
類的函數成員(function member)是包含可執行語句的成員。前面部分所描述的方法是主要的函數成員。這一節討論其他幾種C#支持的函數成員:構造函數、屬性、索引器、事件、運算符、析構函數。
表1.8展示一個名為List的類,它實現一個可擴展的對象列表。這個類包含了最通用的幾種函數成員的例子。
表1.8 類的函數成員示例
public class List
{
const int defaultCapacity = 4;
常數
object[] items;
int count;
字段
(續表)
public List(): this(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.item[i], b.item[i])) {
return false;
}
}
}
方法
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);
}
運算符
}
1.6.6.1 構造函數
C#既支持實例構造函數,也支持靜態構造函數。實例構造函數(instance constructor)是實現初始化類實例所需操作的成員。靜態構造函數(static constructor)是一種在類首次加載時用於實現初始化類本身所需操作的成員。
構造函數的聲明如同方法一樣,不過,它沒有返回類型,它的名字與包含它的類名一樣。若構造函數的聲明中包含static修飾符,則它聲明了一個靜態構造函數,否則聲明實例構造函數。
實例構造函數能夠被重載。例如,List聲明了兩個實例構造函數,一個不帶參數,一個帶有一個int參數。使用new運算符可以調用實例參數。下面的語句使用各個List類的構造函數創建了兩個List實例。
List list1 = new List();
List list2 = new List(10);
實例構造函數不同於其他方法,它是不能被繼承的。並且,一個類除了自己聲明的實例構造函數外,不可能有其他的實例構造函數。如果一個類沒有聲明任何實例構造函數,則會自動地為它提供一個默認的空的實例構造函數。
1.6.6.2 屬性
屬性(property)是字段的自然擴展,兩者都是具有關聯類型的命名成員,而且訪問字段和屬性的語法是相同的。然而,屬性與字段不同,不表示存儲位置。相反,屬性有訪問器(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; //調用set訪問器
int i = names.Count; //調用get訪問器
int j = names.Capacity; //調用get訪問器
與字段和方法類似,對於實例屬性和靜態屬性,C#兩者都支持。靜態屬性是聲明中具有static修飾符,而實例屬性則沒有。
屬性的訪問器可以是虛擬的。當屬性聲明中包含virtual,abstract,override修飾符時,它們將運用到屬性訪問器。
1.6.6.3 索引器
索引器是這樣一個成員:它使對象能夠用與數組相同的方式進行索引。索引器的聲明與屬性很相似,不同之處在於成員的名字是this,後面的參數列表是在定界符([])之間。參數在索引器的訪問器中是可用的。與屬性類似,索引器可以是讀寫、只讀、只寫的,並且索引器的訪問器也可以是虛擬的。
List類聲明了單個讀寫索引器,接受一個int型的參數。通過索引器就可能用int值索引List實例。例如:
List names = 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();
}
索引器能夠被重載,意味著可以聲明多個索引器,只要它們的參數個數或類型不同。
1.6.6.4 事件
事件是使對象或類能夠提供通知的成員。事件的聲明與字段的類似,不同之處在於事件聲明包含一個event關鍵字,並且事件聲明的類型必須是委托類型。
在包含事件聲明的類中,事件可以像委托類型的字段一樣使用(這樣的事件不能是 abstract,而且不能聲明訪問器)。該字段保存了一個委托的引用,表示事件處理程序已經被添加到事件上。如果尚未添加任何事件處理程序,則該字段為null。
List類聲明了名為Changed的單個事件成員,Changed事件表明有一個新項添加到事件處理程序列表,它由OnChanged虛擬方法引發,它首先檢查事件是否為null(意思是沒有事件處理程序)。引發事件的通知正好等價於調用事件所表示的委托——因此,不需要特殊的語言構件引發事件。
客戶通過事件處理程序(event handler)響應事件。使用“+=”運算符添加或者使用“-=”移除事件處理程序。下面的示例添加一個事件處理程序到List類的Changed事件:
using System;
class Test
{
static int changeCount;
static void ListChanged(object sender, EventArgs e) {
changCount++;
}
static void Main() {
List names = new List();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(changeCount); //輸出 "3"
}
}
對於要求控制事件的底層存儲的更高級場景譯注8,事件的聲明可以顯式地提供add和remove訪問器,它們在某種程度上類似於屬性的set訪問器。
1.6.6.5 運算符
運算符(operator)是一種函數成員,用來定義可應用於類實例的特定表達式運算符的含義。有三種運算符能夠被定義:一元運算符、二元運算符和轉換運算符。所有的運算符必須聲明為public和static。
List類聲明了兩個運算符,運算符 “==”和運算符 “!=”,並且向表達式賦予新的含義,而這些表達式將這些運算符應用到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); //輸出 "True"
b.Add(3);
Console.WriteLine(a == b); //輸出 "False"
} 類(class)是C#類型中最基礎的類型。類是一個數據結構,將狀態(字段)和行為(方法和其他函數成員)組合在一個單元中。類提供了用於動態創建類實例的定義,也就是對象(object)。類支持繼承(inheritance)和多態(polymorphism),即派生類能夠擴展和特殊化基類的機制。
使用類聲明可以創建新的類。類聲明以一個聲明頭開始,其組成方式如下:先是指定類的特性和修飾符,後跟類的名字,基類(如果有的話)的名字,以及被該類實現的接口名。聲明頭後面就是類體了,它由一組包含在大括號({})中的成員聲明組成。
下面是一個名為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#中,沒有必要也不可能顯式地釋放對象。
1.6.1 成員
類的成員或者是靜態成員(static member),或者是實例成員(instance member)。靜態成員屬於類,實例成員屬於對象(類的實例)。
表1.6提供了類所能包含的各種成員的描述。
表1.6 類 的 成 員
成 員
描 述
常數
與類關聯的常量值
字段
類的變量
方法
能夠被類執行的計算和行為
屬性
使對象能夠讀取和寫入類的命名屬性
索引器
使對象能夠用與數組相同的方式進行索引
事件
能夠被類產生的通知
運算符
類支持的轉換和表達式運算符
構造函數
初始化類的實例或者類本身
析構函數
在永久銷毀類的實例之前執行的行為
類型
被類聲明的嵌套類型
1.6.2 可訪問性
類的每個成員都有關聯的可訪問性,它控制能夠訪問該成員的程序文本區域。有5種可能的可訪問性形式。表1.7概述了類的可訪問性的意義。
表1.7 類的可訪問性
可訪問性
意 義
public
訪問不受限制
protected
訪問僅限於包含類或從包含類派生的類型
internal
訪問僅限於當前程序集
protected internal
訪問僅限於從包含類派生的當前程序集或類型
private
訪問僅限於包含類
1.6.3 基類
類的聲明可能通過在類名後加上冒號和基類的名字來指定一個基類譯注4。省略基類等同於直接從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類繼承了其基類的成員。繼承意味著類將隱式地包含其基類的所有成員(除了基類的構造函數)。派生類能夠在繼承基類的基礎上增加新的成員,但是它不能移除繼承成員的定義。在前面的示例中,Point3D類從Point類中繼承了x字段和y字段,並且每一個Point3D實例都包含三個字段x,y和z。
從類類型到它的任何基類類型都存在隱式的轉換。並且,類類型的變量能夠引用該類的實例,或者任何派生類的實例。例如,對於前面給定的類聲明,Point類型的變量能夠引用Point實例或者Point3D實例:
Point a = new Point(10, 20);
Point b = new Point3D(10, 20, 30);
1.6.4 字段
字段是與對象或類相關聯的變量。
當一個字段聲明中含有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修飾符聲明只讀字段。給readonly字段的賦值只能作為聲明的組成部分出現,或者在同一類中的實例構造函數或靜態構造函數中出現。
1.6.5 方法
方法(method)是一種用於實現可以由對象或類執行的計算或操作的成員。靜態方法(static method)只能通過類來訪問。實例方法(instance method)則要通過類的實例訪問。
方法有一個參數(parameter)列表(可能為空),表示傳遞給方法的值或者引用;方法還有返回類型(return type),用於指定由該方法計算和返回的值的類型。如果方法不返回一個值,則它的返回類型為void。
在聲明方法的類中,該方法的簽名必須是惟一的。方法的簽名由它的名稱、參數的數目、每個參數的修飾符和類型組成。返回類型不是方法簽名的組成部分。
1.6.5.1 參數
參數用於將值或者引用變量傳遞給方法。當方法被調用時,方法的參數譯注5從指定的自變量(argument)譯注6得到它們實際的值。C#有4種參數:值參數、引用參數、輸出參數和參數數組。
值參數(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); //輸出 "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); //輸出 "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);
1.6.5.2 方法體和局部變量
方法體指定方法調用時所要執行的語句。
方法體能夠聲明特定於該方法調用的變量。這樣的變量被稱為局部變量(local variable)。局部變量聲明指定類型名、變量名,可能還有初始值。下面的示例聲明了一個局部變量i,其初始值為0;另一個局部變量j沒有初始值。
using System;
class Squares
{
static void Main() {
int i = 0;
int j;
while(i < 10){
j = i * i;
Console.WriteLine("{0} x {0} = {1}", i, j);
i = i + 1;
}
}
}
C#要求局部變量在其值被獲得之前明確賦值(definitely)。例如,假設前面的變量i的聲明沒有包含初始值,那麼,在接下來對i的使用將導致編譯器報告錯誤,原因就是i在程序中沒有明確賦值。
方法能夠使用return語句將控制返回給它的調用方。如果方法是void的,則return語句不能指定表達式;如果方法是非void的,則return語句必須包含表達式,用於計算返回值。
1.6.5.3 靜態方法和實例方法
若一個方法聲明中含有static修飾符,則稱該方法為靜態方法(static method)。靜態方法不對特定實例進行操作,只能訪問靜態成員。
若一個方法聲明中沒有static修飾符,則稱該方法為實例方法(instance method)。實例方法對特定實例進行操作,既能夠訪問靜態成員,也能夠訪問實例成員。在調用實例方法的實例上,可以用 this來訪問該實例,而在靜態方法中引用this是錯誤的。
下面的Entity類具有靜態和實例兩種成員:
class Entity
{
static int nextSerialNo;
int serialNo;
public Entity() {
serialNo = nextSerialNo++;
}
public int GetSerialNo() {
return serialNo;
}
public static int GetNextSerialNo() {
return nextSerialNo;
}
public static void SetNextSerialNo(int value) {
nextSerialNo = value;
}
}
每一個Entity實例包含一個序列號(並且假定這裡省略了一些其他信息)。Entity構造函數(類似於實例方法)用下一個有效的序列號初始化新的實例。因為構造函數是一個實例成員,所以,它既可以訪問serialNo實例字段,也可以訪問nextSerialNo靜態字段。
GetNextSerialNo和SetNextSerialNo靜態方法能夠訪問nextSerialNo靜態字段,但是如果訪問serialNo實例字段就會產生錯誤。
下面的示例展示了Entity類的使用:
using System;
class Test
{
static void Main() {
Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo()); //輸出 "1000"
Console.WriteLine(e2.GetSerialNo()); //輸出 "1001"
Console.WriteLine(Entity.GetNextSerialNo()); //輸出 "1002"
}
}
注意,SetNextSerialNo和GetNextSerialNo靜態方法通過類調用,而GetSerialNo實例成員則通過類的實例調用。
1.6.5.4 虛擬方法、重寫方法和抽象方法
若一個實例方法的聲明中含有virtual修飾符,則稱該方法為虛擬方法(virtual method)。若其中沒有virtual修飾符,則稱該方法為非虛擬方法(nonvirtual method)。
在一個虛擬方法調用中,該調用所涉及的實例的運行時類型(runtime type)確定了要被調用的究竟是該方法的哪一個實現。在非虛擬方法調用中,實例的編譯時類型(compile-time type)是決定性因素。
虛擬方法可以由派生類重寫(override)譯注7實現。當一個實例方法聲明中含有override修飾符時,該方法將重寫所繼承的相同簽名的虛擬方法。虛擬方法聲明用於引入新方法,而重寫方法聲明則用於使現有的繼承虛擬方法專用化(通過提供該方法的新實現)。
抽象(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");
}
}
前面的4個類用於模型化算術表達式。例如,使用這些類的實例,表達式x+3能夠被表示為如下的形式:
Expression e = new Operation(
new VariableReference("x"),
'+',
new Constant(3));
Expression實例的Evaluate方法將被調用,以計算表達式的值,從而產生一個double值。該方法取得一個包含變量名(輸入的鍵)和值(輸入的值)的Hashtable作為其自變量。Evaluate方法是虛擬的抽象方法,意味著派生類必須重寫它並提供實際的實現。
Evaluate方法的Constant的實現只是返回保存的常數。VariableReference的實現在Hashtable中查找變量名,並且返回相應的值。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)); //輸出 "21"
Vars["x"] = 1.5;
Vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); //輸出 "16.5"
}
}
1.6.5.5 方法重載
方法重載(Method 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, dpuble y) {
Console.WriteLine("F(double, double)");
}
static void Main(){
F(); //調用F()
F(1); //調用F(int)
F(1.0); //調用F(double)
F("abc"); //調用F(object)
F((double)1); //調用F(double)
F((object)1); //調用F(object)
F(1, 1); //調用F(double, double)
}
}
如上例所示,總是通過自變量到參數類型的顯式的類型轉換,來選擇特定方法。
1.6.6 其他函數成員
類的函數成員(function member)是包含可執行語句的成員。前面部分所描述的方法是主要的函數成員。這一節討論其他幾種C#支持的函數成員:構造函數、屬性、索引器、事件、運算符、析構函數。
表1.8展示一個名為List的類,它實現一個可擴展的對象列表。這個類包含了最通用的幾種函數成員的例子。
表1.8 類的函數成員示例
public class List
{
const int defaultCapacity = 4;
常數
object[] items;
int count;
字段
(續表)
public List(): this(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.item[i], b.item[i])) {
return false;
}
}
}
方法
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);
}
運算符
}
1.6.6.1 構造函數
C#既支持實例構造函數,也支持靜態構造函數。實例構造函數(instance constructor)是實現初始化類實例所需操作的成員。靜態構造函數(static constructor)是一種在類首次加載時用於實現初始化類本身所需操作的成員。
構造函數的聲明如同方法一樣,不過,它沒有返回類型,它的名字與包含它的類名一樣。若構造函數的聲明中包含static修飾符,則它聲明了一個靜態構造函數,否則聲明實例構造函數。
實例構造函數能夠被重載。例如,List聲明了兩個實例構造函數,一個不帶參數,一個帶有一個int參數。使用new運算符可以調用實例參數。下面的語句使用各個List類的構造函數創建了兩個List實例。
List list1 = new List();
List list2 = new List(10);
實例構造函數不同於其他方法,它是不能被繼承的。並且,一個類除了自己聲明的實例構造函數外,不可能有其他的實例構造函數。如果一個類沒有聲明任何實例構造函數,則會自動地為它提供一個默認的空的實例構造函數。
1.6.6.2 屬性
屬性(property)是字段的自然擴展,兩者都是具有關聯類型的命名成員,而且訪問字段和屬性的語法是相同的。然而,屬性與字段不同,不表示存儲位置。相反,屬性有訪問器(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; //調用set訪問器
int i = names.Count; //調用get訪問器
int j = names.Capacity; //調用get訪問器
與字段和方法類似,對於實例屬性和靜態屬性,C#兩者都支持。靜態屬性是聲明中具有static修飾符,而實例屬性則沒有。
屬性的訪問器可以是虛擬的。當屬性聲明中包含virtual,abstract,override修飾符時,它們將運用到屬性訪問器。
1.6.6.3 索引器
索引器是這樣一個成員:它使對象能夠用與數組相同的方式進行索引。索引器的聲明與屬性很相似,不同之處在於成員的名字是this,後面的參數列表是在定界符([])之間。參數在索引器的訪問器中是可用的。與屬性類似,索引器可以是讀寫、只讀、只寫的,並且索引器的訪問器也可以是虛擬的。
List類聲明了單個讀寫索引器,接受一個int型的參數。通過索引器就可能用int值索引List實例。例如:
List names = 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();
}
索引器能夠被重載,意味著可以聲明多個索引器,只要它們的參數個數或類型不同。
1.6.6.4 事件
事件是使對象或類能夠提供通知的成員。事件的聲明與字段的類似,不同之處在於事件聲明包含一個event關鍵字,並且事件聲明的類型必須是委托類型。
在包含事件聲明的類中,事件可以像委托類型的字段一樣使用(這樣的事件不能是 abstract,而且不能聲明訪問器)。該字段保存了一個委托的引用,表示事件處理程序已經被添加到事件上。如果尚未添加任何事件處理程序,則該字段為null。
List類聲明了名為Changed的單個事件成員,Changed事件表明有一個新項添加到事件處理程序列表,它由OnChanged虛擬方法引發,它首先檢查事件是否為null(意思是沒有事件處理程序)。引發事件的通知正好等價於調用事件所表示的委托——因此,不需要特殊的語言構件引發事件。
客戶通過事件處理程序(event handler)響應事件。使用“+=”運算符添加或者使用“-=”移除事件處理程序。下面的示例添加一個事件處理程序到List類的Changed事件:
using System;
class Test
{
static int changeCount;
static void ListChanged(object sender, EventArgs e) {
changCount++;
}
static void Main() {
List names = new List();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(changeCount); //輸出 "3"
}
}
對於要求控制事件的底層存儲的更高級場景譯注8,事件的聲明可以顯式地提供add和remove訪問器,它們在某種程度上類似於屬性的set訪問器。
1.6.6.5 運算符
運算符(operator)是一種函數成員,用來定義可應用於類實例的特定表達式運算符的含義。有三種運算符能夠被定義:一元運算符、二元運算符和轉換運算符。所有的運算符必須聲明為public和static。
List類聲明了兩個運算符,運算符 “==”和運算符 “!=”,並且向表達式賦予新的含義,而這些表達式將這些運算符應用到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); //輸出 "True"
b.Add(3);
Console.WriteLine(a == b); //輸出 "False"
}
}
第一個Console.WriteLine輸出True,原因是兩個List集合對象包含個數和值都相同的對象。假如List沒有定義運算符 “==”,那麼第一個Console.WriteLine將輸出False,因為a和b引用不同的List實例。
1.6.6.6 析構函數
析構函數(destructor)是用於實現析構類實例所需操作的成員。析構函數不能帶參數,不能具有可訪問性修飾符,也不能被顯式地調用。垃圾回收期間會自動調用所涉及實例的析構函數。
垃圾回收器在決定何時回收對象和運行析構函數方面采取寬松的策略。特別指出,析構函數的調用時機是不確定的,並且析構函數可能運行在任何線程上。由於這些或者其他原因,只有沒有其他可行的解決方案,類才實現析構函數。
}
第一個Console.WriteLine輸出True,原因是兩個List集合對象包含個數和值都相同的對象。假如List沒有定義運算符 “==”,那麼第一個Console.WriteLine將輸出False,因為a和b引用不同的List實例。
1.6.6.6 析構函數
析構函數(destructor)是用於實現析構類實例所需操作的成員。析構函數不能帶參數,不能具有可訪問性修飾符,也不能被顯式地調用。垃圾回收期間會自動調用所涉及實例的析構函數。
垃圾回收器在決定何時回收對象和運行析構函數方面采取寬松的策略。特別指出,析構函數的調用時機是不確定的,並且析構函數可能運行在任何線程上。由於這些或者其他原因,只有沒有其他可行的解決方案,類才實現析構函數。