C# Language Specification 5.0 (翻譯)第三章 基本概念


C# Language Specification 5.0

應用程序啟動

擁有進入點(entry point)的程序集稱應用程序(application)。當運行一應用程序時,將創建一新應用程序域(application domain)。同一個應用程序可在同一台機器(machine)上同時運行多個實例,並且每個實例都有自己的應用程序域。

應用程序域作為應用程序狀態(application state)之容器(container),使應用程序相互隔離(isolation)。應用程序域是定義於應用及所用類庫的類型之容器與邊界。加載入不同的應用程序域的同一類型是相互涇渭分明的,而其實例化的對象也不會在應用程序域之間直接共享。比方說,對於這些類型的靜態變量,每個應用程序域都自有一份其副本,同時這些類型的靜態構造函數在應用程序域中至多運行一次。實現可以自由為應用程序域的創建和銷毀提供指定實現(implementation-specific)策略或機制。

應用程序啟動(Application startup)時,執行環境會調用一個特指的方法作為應用程序的進入點(entry point)。入口點方法一貫稱為 Main,且可為下列簽名中的一種:

  • static void Main() {...}
  • static void Main(string[] args) {...}
  • static int Main() {...}
  • static int Main(string[] args) {...}

如上所示,進入點可以選擇 int 為其返回值。這個返回值通常被用在應用程序終止(application termination,第三章第二節)時。

進入點有個可選形參。這個參數可以用任何名稱,但它的類型必須是 string[]。如果出現形參,那么當應用程序啟動時,執行環境將通過命令行參數(command-line arguments)的方式創建並傳遞指定的 string[] 實參。實參 string[] 永不為空(null),但它可能長度為零(如果命令行沒有指定實參的話)。

由於 C# 支持方法重構(overload),類或結構可以包含一個方法的多個定義,前提是每個重載版本都有不同的方法簽名。然而,在一個程序內,類或結構內不能同時存在多個叫做 Main 的方法,因為 Main 被限定為只能作為應用程序的入口點。如果提供超過一個參數或者唯一參數類型不是 string[],則允許重載多個 Main 版本。

應用程序可由多個類與結構組成,也許會有多個 Main 方法在這些類或結構內,由於 Main 的定義限定它只能是應用程序入口點,所以在這種情況下,外部機制(諸如命令行編譯器選項)必須從中選擇其一作為應用程序的進入點。

在 C# 中每一個方法都必須定義為類或結構的成員,一般情況下方法的聲明可訪問性(declared accessibility,第三章第 5.1 節)是由訪問控制修飾符(access modifiers,第十章第 3.5 節)所決定的;同樣,類型的聲明聲明可訪問性也是由其訪問控制修飾符所決定的。為了調用所給定類型的給定方法,類型與成員都必須可被訪問(accessible),然應用程序進入點是特例。不管應用程序進入點的聲明可訪問性及其閉包類型的聲明可訪問性,執行環境總能訪問到它們。

應用程序進入點方法(entry point method)不能放入泛型類聲明內。

在其它各方面,進入點方法與非進入點方法的行為類似。


應用程序終止

應用程序終止將返回控制權給執行環境。

應用程序進入點方法的返回類型是 int,該值作為應用程序終止狀態代碼(application's termination status code)返回。該代碼的目的是允許與執行環境通訊以告知成功或失敗。

如果進入點方法的返回類型是 void,遇到該方法的關門大括號 } 或執行到一個沒有表達式的返回語句(return statement),則終止狀態代碼為 0。

先於應用程序的終止,未被垃圾回收的對象的析構函數將被調用,除非這類清理工作已被取消(suppressed)(比方說通過調用庫方法GC.SuppressFinalize)。


聲明

C# 程序中的聲明定義了程序的組成元素。C# 程序有系統地使用了命名空間(namespace,第九章),命名空間可以包含類型聲明(type declarations)嵌套命名空間聲明(nested namespace declarations)。類型聲明(type declarations,第九章第六節)用於定義類(class,第十章)、結構(struct,第十章第十四節)、接口(interface,第十三章)、枚舉(enum,第十四章)以及委托(delegates,第十五章)。類型聲明中允許的成員種類取決於類型聲明的形式。比方說類聲明可以包含聲明常量(constants,第十章第四節)、字段(fields,第十章第五節)、方法(methods,第十章第六節)、屬性(properties,第十章第七節)、事件(events,第十章第八節)、索引器(indexers,第十章第九節)、操作符(operators,第十章第十節)、實例構造函數(instance constructors,第十章第十一節)、靜態構造函數(static constructors,第十章第十二節)、析構函數(destructors,第十章第十三節)和嵌套類型(nested types,第十章第 3.8 節)。

聲明在其所屬聲明空間(declaration space)內定義一個名稱。除重載成員(第三章第六節)外,同一聲明空間(declaration space)內出現兩個以上同名成員會引發「編譯時錯誤(compile-time error)」。同一聲明空不可同時包含不同類型的同名方法。比方說,聲明空間絕無包含同名字段與方法之可能。

有數種不同的聲明空間,其具體描述如下:

  • 在程序的所有源文件內,未閉包於 namespace-declaration 內的 namespace-member-declarations 是屬於一個叫做全局聲明空間(global declaration space)的組合聲明空間內的成員;
  • 在程序的所有源文件內,位於 namespace-declarations 內的 namespace-member-declarations 若擁有相同的完全限定命名空間名,則其屬於同一個組合聲明空間內;
  • 每個類、結構或接口的聲明都將創架一個新的聲明空間。名稱將通過 class-member-declarationsstruct-member-declarationsinterface-member-declarationstype-parameters 引入此聲明空間。除了重載實例構造函數和靜態構造函數,類或結構不允許聲明包含一個與其類名或結構名同名的成員。類、結構或結構允許重載聲明方法和索引器。此外類或結構允許重載聲明實例構造函數與操作符。比方說類、結構或接口可以包含多個聲明為同名的方法,這些所聲明的方法擁有不同的簽名(signature,第三章第六節)。注意,基類並不在該類的聲明空間內,基接口也不在該接口的聲明空間內,所以派生類或接口允許聲明一個與繼承成員同名的成員。這類成員隱藏(hide)了它們繼續的那些成員;
  • 每一個委托聲明都會創建一個新的聲明空間。名稱通過形參(formal parameters)fixed-parametersparameter-arrays 以及 type-parameters 引入這個聲明空間;
  • 每個枚舉聲明都會創建一個新的聲明空間。名稱通過 enum-member-declarations 引入這個聲明空間;
  • 每一個方法聲明(method declaration)、索引器聲明(indexer declaration)、操作符聲明(operator declaration)、實例構造函數聲明(instance constructor declaration)和匿名函數(anonymous function)都會創建一個名曰局部變量聲明空間(local variable declaration space)的新聲明空間。名稱通過形參 fixed-parametersparameter-arrays 以及 type-parameters 引入這個聲明空間。如果有函數成員或匿名函數成員的函數體,則將會被是為嵌套於局部變量聲明空間內。局部變量聲明控件和嵌套局部變量聲明空間內包含同名元素將引發錯誤。因此在一個閉包聲明空間內,如果包含嵌套聲明空間的話,那么不能在其內部聲明一個與閉包聲明空間內變量或常量同名的局部變量或常量。只有一種可能使得兩個聲明空間內包含同名元素,那就是這兩個聲明空間彼此不相互包含。
  • 每個 blockswitch-block,以及 forforeachusing 語句將為局部變量和局部常量創建局部變量聲明空間。名稱將通過 local-variable-declarationslocal-constant-declarations 引入這個聲明空間。注意,作為函數成員主體或匿名函數主體的塊,或是位於函數成員主體或匿名函數主體內的塊,將嵌套在本地變量聲明空間內(這個本地變量聲明空間是為其參數所聲明的)。因此,如果某個方法的局部變量和參數同名,那么將引發一個錯誤。
  • 每個 blockswitch-block 為每個標簽(label)創建相互隔離的聲明空間。名稱將通過 labeled-statements 引入該聲明空間,並通過 goto-statements 來引用。塊的標簽聲明空間(label declaration space)可包含任何嵌套塊。因此在嵌套塊內不能聲明一個與其所在閉包塊(enclosing block)標簽同名的標簽。

聲明名稱的文本順序一般而言是不重要的(no significance)。具體而言,聲明並使用命名空間、常量、方法、屬性、事件、索引器、操作符、實例構造函數、析構函數、靜態構造函數以及類型,其文本順序並不重要。在以下情況下聲明順序比較重要:

  • 字段聲明和局部變量聲明的聲明順序意味着其初始化器(如果有的話)的執行順序;
  • 局部變量必須在使用前定義(第三章第七節);
  • when constant-expression 被省略時,枚舉成員聲明(第十四章第三節)的順序是重要的。

命名空間的聲明空間是開放的(open ended),兩個相同完全限定名(fully qualified name)的命名空間擁有同一個聲明空間。比方說:

namespace Megacorp.Data
{
    class Customer
    {
        ...
    }
}
namespace Megacorp.Data
{
    class Order
    {
        ...
    }
}

這兩個命名空間位於同一個聲明空間內,在此例中兩個類的完全限定名分別是 Megacorp.Data.CustomerMegacorp.Data.Order。因為這兩個聲明位於同一個聲明空間內,所以如果這兩個類的完全限定名是一樣的話,會引發一個「編譯時錯誤」。

如上文所指定,塊(block)的聲明空間可以嵌套任意塊。因此,在下例中,F 方法和 G 方法會引發「編譯時錯誤」,因為外部塊中聲明了 i,那么內部塊就不能重復聲明(redeclared)。然而 H 方法和 I 方法是合法的,這是由於兩個 i 的聲明都位於相互獨立的非嵌套塊(separate non-nested blocks)內。

class A
{
    void F() {
        int i = 0;
        if (true) {
            int i = 1;			
        }
    }
    void G() {
        if (true) {
            int i = 0;
        }
        int i = 1;				
    }
    void H() {
        if (true) {
            int i = 0;
        }
        if (true) {
            int i = 1;
        }
    }
    void I() {
        for (int i = 0; i < 10; i++)
            H();
        for (int i = 0; i < 10; i++)
            H();
    }
}

成員

命名空間和類型都擁有成員。在實體開始被引用時,實體成員一般都可通過限定名稱(qualified name)引入其中,通過標記(token). 引出成員的名字。

類型的成員既可在類型聲明中聲明,也可從其基類中繼承。當一個類型繼承自其基類時,所有基類成員(除了實例構造函數、析構函數以及靜態構造函數)都將成為派生類型的成員。基類成員的聲明可訪問性並不控制成員是否可被繼承——繼承可拓展到除實例構造函數、析構函數和靜態構造函數之外的任意成員。然而。也有可能派生類型無法訪問到所繼承的成員,比方說因為其聲明可訪問性(第三章第 5.1 節)或是因為其通過類型自身聲明隱藏(第三章第 7.1.2 節)。

命名空間成員

如果命名空間與類型沒有閉包於一個命名空間,則它們將是全局命名空間(global namespace)的成員。這相當於名字直接在全局聲明空間內聲明了。

如果命名空間與類型在一個命名空間內,那么命名空間和類型將是這個外部命名空間的成員。這意味着名字直接在這個命名空間的聲明空間內聲明了。

命名空間沒有訪問限制(access restrictions)。不可以為命名空間聲明為 private、 protected 或 internal,命名空間永遠是可公開取得的(publicly accessible)。

結構成員

結構成員是結構內聲明的成員,以及直接繼承自結構基類 System.ValueType 以及間接繼承自基類 object 的成員。

簡單類型的成員通過類型別名(alias)直接對應結構類型的成員:

  • sbyte 的成員是 System.SByte 結構的成員;
  • byte 的成員是 System.Byte 結構的成員;
  • short 的成員是 System.Int16 結構的成員;
  • ushort 的成員是 System.UInt16 結構的成員;
  • int 的成員是 System.Int32 結構的成員;
  • uint 的成員是 System.UInt32 結構的成員;
  • long 的成員是 System.Int64 結構的成員;
  • ulong 的成員是 System.UInt64 結構的成員;
  • char 的成員是 System.Char 結構的成員;
  • float 的成員是 System.Single 結構的成員;
  • double 的成員是 System.Double 結構的成員;
  • decimal 的成員是 System.Decimal 結構的成員;
  • bool 的成員是 System.Boolean 結構的成員。

枚舉成員

枚舉內的成員是枚舉聲明的常量以及直接繼承自枚舉基類 System.Enum 與間接繼承自基類 System.ValueTypeobject 的成員。

類成員

在一個類中聲明的成員與繼承自基類的成員都是這個類的成員(除了沒有基類的 object 類)。繼承自基類的成員包括常量、字段、方法、屬性、事件、索引器、操作符以及基類類型,但不包括基類的實例構造函數、析構函數和靜態構造函數。基類成員的繼承並不關心它們的可訪問性。

類聲明可以包含常量、字段、方法、屬性、事件、索引器、操作符、實例構造函數、析構函數、靜態構造函數和類型。

objectstring 的成員通過別名直接對應它們的類型:

  • object 的成員是 System.Object 類的成員;
  • string 的成員是 System.String 類的成員。

接口成員

接口成員聲明於接口及其所有基接口內。嚴格來講,類 object 內的成員不是任何接口的成員(見第十三章第二節),但通過接口類型成員查找到類 object 的成員(第七章第四節)。

數組成員

數組成員繼承自類 System.Array

委托成員

委托成員繼承自類 System.Delegate

成員訪問

成員聲明可用於控制對其的訪問。成員可訪問性由該成員的聲明的可訪問性(declared accessibility,第三章第 5.1 節)以及包含該成員的類型的可訪問性(如果存在的話)來確定的。

當允許訪問指定成員時,我們稱該成員是可訪問的(accessible)。相反,當不允許訪問指定成員時,我們稱該成員是不可訪問的(inaccessible)

當引發訪問的文本位於成員的可訪問域(accessibility domain,第三章第 5.2 節)內,則該成員允許被訪問。

聲明的可訪問性

成員的聲明可訪問性(declared accessibility)可為以下類型之一:

  • Public,通過在成員聲明時使用 public 修飾符來選擇之,其直觀含義為「不受限制的訪問(access not limited)」;
  • Protected,通過在成員聲明時使用 protected 修飾符來選擇之,其直觀含義為「僅限該類及其派生類型內可訪問(access limited to the containing class or types derived from the containing class)」;
  • Internal,通過在成員聲明時使用 internal 修飾符來選擇之,其直觀含義為「僅限本程序內可訪問(access limited to this program)」;
  • Protected internal(表示 Protected 或 Internal),通過在成員聲明時同時使用 protectedinternal 修飾符來選擇之,其直觀含義為「僅限本程序內或該類及其派生類性內可訪問(access limited to this program or types derived from the containing class)」;
  • Private,通過在成員聲明時使用 private 修飾符來選擇之,其直觀含義為「僅限於該類型內部訪問(access limited to the containing type)」。

聲明成員時被允許使用的可訪問性類型取決於該成員所處之上下文。而且當成員在聲明時不帶任何訪問控制修飾符(access modifiers),那么聲明所在的上下文會為其選擇一個默認的聲明可訪問性。

  • 命名空間隱式的為 public 聲明可訪問性。命名空間聲明不允許有訪問控制修飾符。
  • 編譯單元或命名空間內的類型聲明允許使用 publicinternal 聲明可訪問性,且默認的聲明可訪問性為 internal
  • 類成員可以從這五種聲明可訪問性中挑選一個,且默認的聲明可訪問性為 private。(注意,類中聲明一個某類型的成員也可以從五種聲明可訪問性中挑選一個,盡管這個類型作為命名空間下的成員在聲明時只有 publicinternal 可選。)
  • 結構成員擁有 publicinternalprivate 聲明可訪問性,且默認聲明可訪問性為 private(這是因為結構隱式密封 sealed)。結構成員如果是在這個結構內聲明的(也就是說不是繼承自其基結構),那么其聲明可訪問性不可是 protectedprotected internal。(注意,當一個類型聲明為某結構的成員時,可以從 publicinternalprivate 中選擇一個聲明可訪問性,盡管這個類型在命名空間中的聲明可訪問性只能是 publicinternal 的。)
  • 接口成員隱式的為 public 聲明可訪問性。接口成員聲明不允許有訪問控制修飾符。
  • 枚舉成員隱式的為 public 聲明可訪問性。枚舉成員聲明不允許有訪問控制修飾符。

可訪問域

成員可訪問域(accessibility domain)由其所在程序文本之區段(sections,不一定是連續的)所組成,於該域之內可訪問成員。為了定義成員的可訪問域,若成員未被聲明於一個類型內則謂之「頂級(top-level)」,若成員被聲明於另一個類型內則謂之「嵌套(nested)」。此外,程序的程序文本(program text)被定義為其所有源文件中的全部文本,而類型的程序文本也被定義為其類型(及其嵌套類型)聲明中的全部文本。

預定義類型(predefined type,諸如 objectintdouble)的可訪問域是沒有限制的(unlimited)。

頂級未綁定類型 T(第四章第 4.3 節)的可訪問域被聲明在程序 P 中應被如下定義:

  • 如果 T 的聲明可訪問性是 public 的,則其可訪問域是 P 的整個程序文本以及所有引用了 P 的程序;
  • 如果 T 的聲明可訪問性是 internal 的,則其可訪問域是 P 的整個程序文本。

從這些定義可知,頂級未綁定類型的可訪問域至少是聲明了該類型的程序的程序文本。

已創建的類型 T<A1, ..., AN> 的可訪問域是未綁定泛型類 T 的作用域與類型實參 A1, ..., AN 的作用域的交集(intersection)。

聲明在程序 P 的類型 T 內的嵌套成員 M 的可訪問類型的定義,遵照以下規則(注意,M 自身可能也是一個類型):

  • 如果 M 的聲明可訪問性是 public,那么 M 的可訪問域與 T 的可訪問域一致;
  • 如果 M 的聲明可訪問性是 protected internal,設 D 為程序文本 P 與所有派生自 T 類型的程序文本(在 P 外部聲明的)的並集(union),則 M 的可訪問域為 T 的可訪問域與 D 的交集(intersection);
  • 如果 M 的聲明可訪問性是 protected,設 D 為 T 的程序文本與所有派生自 T 類型的程序文本的並集(union),則 M 的可訪問域為 T 的可訪問域與 D 的交集(intersection);
  • 如果 M 的聲明可訪問性是 internal,則 M 的可訪問域為 T 的可訪問域與 P 的程序文本的交集(intersection);
  • 如果 M 的聲明可訪問性是 private,則 M 的可訪問域就是 T 的程序文本。

由這些定義可以得知,嵌套成員的可訪問域至少是該成員聲明所在的類型的程序文本。此外還能發現這么一點,成員的可訪問域絕不比該成員聲明所在類型的可訪問域更廣。

直觀地說,當訪問類型或成員 M 時,遵循以下步驟進行計算以確保其可被訪問到:

  • 首先,如果 M 聲明於一個類型(相反於編譯單元或命名空間)內,則當該類型不可被訪問將引發一個「編譯時錯誤」;
  • 其次,如果 M 是 piblic,則允許其被訪問;
  • 再次,如果 M 是 protected internal,那么訪問發生在 M 聲明所在的程序內或訪問發生在派生自 M 的類型(第三章第 5.3 節)進行訪問時,允許其被訪問;
  • 第四,如果 M 是 protected,那么訪問發生在 M 聲明所在的類內或訪問發生在派生自 M 的類型(第三章第 5.3 節)進行訪問時,允許其被訪問;
  • 第五,如果 M 是 internal,那么訪問發生在 M 聲明所在的程序內,允許其被訪問;
  • 第六,如果 M 是 private,那么訪問發生在 M 聲明所在的類型內,允許其被訪問;
  • 否則,類型或成員是不可訪問的,同時將引發一個「編譯時錯誤」。

在下面這個例子中,

public class A
{
    public static int X;
    internal static int Y;
    private static int Z;
}
internal class B
{
    public static int X;
    internal static int Y;
    private static int Z;
    public class C
    {
        public static int X;
        internal static int Y;
        private static int Z;
    }
    private class D
    {
        public static int X;
        internal static int Y;
        private static int Z;
    }
}

類和成員擁有如下可訪問域:

  • AA.X 的可訪問域不受限制;
  • A.YBB.XB.YB.CB.C.X 以及 B.C.Y 的可訪問域是程序文本所在的程序內;
  • A.Z 的可訪問域是 A 的程序文本;
  • B.ZB.D 的可訪問域是 B 的程序文本,包括 B.CB.D 的程序文本;
  • B.C.Z 的可訪問域是 B.C 的程序文本;
  • B.D.XB.D.Y 的可訪問域是 B 的程序文本,包括 B.CB.D 的程序文本;
  • B.D.Z 的可訪問域是 B.D 的程序文本;

如示例所示,成員的可訪問域永不會大於其所在類型(的可訪問域)。比方說即便成員 X 的聲明可訪問性都是 public,但除了 A.X 外其余成員都受制於其所在類型。

如第三章第四節所解釋的,所有來自基類的成員(除了實例構造函數、析構函數和靜態構析函數)由派生類型所繼承。這甚至包括了基類的私有成員。然而,所包含的私有成員的可訪問域只能在其聲明的類型內部的程序文本內。比方說:

class A
{
    int x;
    static void F(B b) {
        b.x = 1;        // 正確
    }
}
class B: A
{
    static void F(B b) {
        b.x = 1;        // 錯誤,x 不可訪問
    }
}

B 類從 A 類繼承了私有(private)成員 x。因為這個成員是私有的,所以它只能在 A 的類主體內部可被訪問。因此,可以通過 A.F 方法訪問 b.x,但不能通過 B.F 訪問 b.x

實例成員的受保護訪問

當一個以 protected 修飾的實例成員被程序文本之外的類訪問時,或者當一個以 protected internal 丟失的實例成員被程序文本之外的類訪問時,訪問必須發生在此類的派生類內。此外,這個訪問必須通過在派生類實例或構造自它的類型的實例來訪問。這個限制(restriction)組織了一個派生類訪問另一個派生類的 protected 成員(即使它們派生自同一個基類)。

假設 B 為基類(它聲明了一個受保護的(protected)實例成員 M),並設 B 為其派生類。在 D 的類主體(class-body)內部,須按照以下形式之一訪問 M:

  • M 形式的非限定(unqualified)的 type-nameprimary-expression
  • E.M 形式的 primary-expression,假設類型 E 為 T 或其派生類,T 為 類型 D 或構造自 D 的類型;
  • base.M 形式的 primary-expression

除這些形式的訪問外,派生類可以在構造初始化器(constructor-initializer,第十章第 11.1 節)內訪問到受保護的(protected)基類實例構造函數

public class A
{
    protected int x;
    static void F(A a, B b) {
        a.x = 1;        // 正確
        b.x = 1;        // 正確
    }
}
public class B: A
{
    static void F(A a, B b) {
        a.x = 1;        // 錯誤,必須通過 B 的實例訪問
        b.x = 1;        // 正確
    }
}

上例 A 中,可以通過 A 和 B 訪問到 x,因為這兩次訪問都發生在 A 的實例或其派生類中。然而,在 B 中,不能通過 A 的實例去訪問 x,因為 A 不是 B 的派生類。

class C<T>
{
    protected T x;
}
class D<T>: C<T>
{
    static void F() {
        D<T> dt = new D<T>();
        D<int> di = new D<int>();
        D<string> ds = new D<string>();
        dt.x = default(T);
        di.x = 123;
        ds.x = "test";
    }
}

上例中的三個對 x 的賦值動作都是合法的,因為它們都通過構造自泛型類型的類類型的實例進行的。

可訪問性約束

在 C# 語言的一些構造中要求類型至少與其成員或其它類型具有相同的可訪問性(be at least as accessible as)。如果 T 的可訪問域(accessibility domain)是 M 的可訪問域的超集(superset),那么我們可以說類型 T 至少擁有與成員或類型 M 相同的可訪問性。換句話說,如果在任何 M 可被訪問的上下文中,T 都可被訪問,那么 T 至少擁有 M 的可訪問性。

有以下這些可訪問性約束:

  • 類類型的直接基類必須至少與該類類型自身具有相同的可訪問性;
  • 接口顯式繼承的基接口必須至少與該接口類型自身具有相同的可訪問性;
  • 委托類型的返回類型與形參類型必須至少與該委托自身具有相同的可訪問性;
  • 常量類型必須至少與該常量自身具有相同的可訪問性;
  • 字段類型必須至少與該字段自身擁有相同的可訪問性;
  • 方法的返回類型與形參類型必須至少與該方法自身具有相同的可訪問性;
  • 屬性類型必須至少與該屬性自身具有相同的可訪問性;
  • 事件類型必須至少與該事件自身具有相同的可訪問性;
  • 索引器的類型和形參類型必須至少與其自身具有相同的可訪問性;
  • 操作符的返回類型和形參類型必須至少與其自身具有相同的可訪問性;
  • 實例構造函數的參數類型必須至少與其自身具有相同的可訪問性。

在下例中,

class A {...}
public class B: A {...}

B 類將出現「編譯時錯誤」,因為 A 類不具備至少與 B 類相同的可訪問性。同樣,在下例中,

class A {...}
public class B
{
    A F() {...}
    internal A G() {...}
    public A H() {...}
}

H 方法將出現「編譯時錯誤」,因為 H 方法所返回的類型 A 不具備至少與該方法相同的可訪問性。


簽名與重載

方法、實例構造器、索引器和操作符由其簽名來鑒定:

  • 方法簽名由方法名、類型參數成員以及形參的類型與種類(值 value、引用 reference 或輸出 output),按照從左到右的順序構成。為此,出現在方法形參內的類型參數的識別並不根據它們的名字,而是依次根據它們在方法類型實參類表中的位置。
  • 實例構造函數的簽名由每個形參的類型和種類(值 value、引用 reference 或輸出 output)從左到右構成。具體來說,實例構造函數簽名不包含最右側 params 修飾符(params modifier)指定的參數。
  • 索引器簽名由其每個形參的類型自左而右地構成。具體來說,索引器簽名不包含元素類型,也不包含最右側 params 修飾符(params modifier)指定的參數。
  • 操作符簽名由簽名符名和形參類型自左而右構成。具體來說,操作符簽名不包含結果類型。

簽名能使用類、結構或接口的成員的重載機制:

  • 允許類、結構或接口通過聲明多個同名方法對其重載,只要它們的簽名在類、結構或接口中是唯一的即可;
  • 允許類或結構的實例構造函數通過聲明多個實例構造函數對其重載,只要它們的簽名在類或結構中是唯一的即可;
  • 允許類、結構或接口中的索引器通過聲明多個索引器對其重載,只要它們的簽名在類、結構或接口中是唯一的即可;
  • 允許類或結構的操作符通過聲明多個操作符對其重載,只要它們的簽名在類或結構中是唯一的即可。

雖然 outref 參數修飾符是構成簽名的一部分,但不能僅通過 refout 修飾符來區分成員簽名。如果兩個成名聲明為相同簽名的類型(即使它們內的方法的形參修飾符從 out 變為 ref),那么將出現一個「編譯時錯誤」。為了簽名匹配的其他用途(比如隱藏 hidden 或重寫 overriding),refout 是構成簽名的一部分,以便相互不匹配。(這個限制允許 C# 程序能輕易轉換為能運行在公共語言基礎結構(Common Language Infrastructure,CLI)上,CLI 並未提供方式去定義只通過 refout 進行區別的方法)。

由於簽名的緣故,object 類型和 dynamic 類型被認為是一樣的。因此不能僅通過 objectdynamic 來區別在單一類型中的成員聲明。

下例展示了一組重載的成員聲明及其簽名。

interface ITest
{
    void F();                  // F()
    void F(int x);             // F(int)
    void F(ref int x);         // F(ref int)
    void F(out int x);         // F(out int)   錯誤
    void F(int x, int y);      // F(int, int)
    int F(string s);           // F(string)
    int F(int x);              // F(int)       錯誤
    void F(string[] a);        // F(string[])
    void F(params string[] a); // F(string[])  錯誤
}

注意,任何 refout 參數修飾符(第十章第 6.1 節)都是簽名的一部分。因此,F(int)F(ref int) 都是唯一的簽名。但是 F(ref int)F(out int) 不能定義在同一個接口內,因為簽名不能僅從 refout 加以區分。同樣的,返回類型和 params 修飾符都不是簽名的一部分,所以不可能僅基於返回類型或是否包含 params 修飾符來區別重載,故而上面的方法聲明 F(int)F(params string[]) 會引發一個「編譯時錯誤」。


作用域

名稱的作用域(scope)是一個程序文本區域(region),在其中可以用過名稱引用實體聲明而不對該名稱加以限定條件(qualification)。作用域可嵌套,內部作用域可重聲明外部作用域名稱的含義(但這並不會移除在第三章第三節中對其的限制——在嵌套塊內不能聲明與閉包塊(enclosing block)內局部變量同名的局部變量)。因此可以說此外部作用域的名稱在該內部作用域覆蓋的程序文本區域內是隱藏(hidden)的,只能通過限定名來訪問外部名稱。

  • namespace-member-declaration(第九章第五節)聲明的命名空間成員的作用域——如果沒有其它 namespace-declaration 對其閉包的話——是整個(entire)程序文本。
  • namespace-declaration 內的 namespace-member-declaration 聲明的命名空間成員的作用域是——如果假設該命名空間成員聲明的完全限定名為 N——完全限定名為 N 或以 N 為始、后跟一個句號(period)的每個 namespace-declarationnamespace-body
  • extern-alias-directive 定義的名稱的作用域擴展到直接包含其編譯單元或命名空間主體的 using-directivesglobal-attributesnamespace-member-declarationsextern-alias-directive 並不會為底層聲明空間(underlying declaration space)增加任何成員。換句話說 extern-alias-directive 並不具傳遞性,相反其只會影響到在其內出現的 compilation-unitnamespace-body
  • using-directive(第九章第四節)定義或導入的名稱的作用域擴展到出現 using-directivecompilation-unitnamespace-body 的整個 namespace-member-declarationsusing-directive 能使零或多個命名空間或類型名在特定的 compilation-unitnamespace-body 變得可用,但並不會為底層聲明空間(underlying declaration space)增加任何成員。換句話說 using-directive 並不具傳遞性,相反其只會影響到在其出現的 compilation-unitnamespace-body
  • 由一個在 class-declaration(第十章第一節)中的 type-parameter-list 所聲明的類型形參的作用域是該 class-declarationclass-basetype-parameter-constraints-clauses 以及 class-body
  • 由一個在 struct-declaration(第十一章第一節)中的 type-parameter-list 所聲明的類型形參的作用域是該 struct-declarationstruct-interfacestype-parameter-constraints-clauses 以及 struct-body
  • 由一個在 interface-declaration(第十三章第一節)中的 type-parameter-list 所聲明的類型形參的作用域是該 interface-declarationinterface-basetype-parameter-constraints-clauses 以及 interface-body
  • 由一個在 delegate-declaration(第十五章第一節)中的 type-parameter-list 所聲明的類型形參的作用域是該 delegate-declarationreturn-typeformal-parameter-list 以及 type-parameter-constraints-clauses
  • class-member-declaration(第十章第 1.6 節)所聲明的成員的作用域位於該聲明所出現的 class-body 之內。此外,類成員的作用域擴展到包含該成員且為可訪問的(accessibility,第三章第 5.2 節)派生類的 class-body
  • struct-member-declaration(第十一章第二節)聲明的成員的作用域位於該聲明出現的 struct-body 之內。
  • enum-member-declaration(第十四章第三節)所聲明的成員的作用域位於該聲明出現的 enum-body 內。
  • 位於 method-declaration(第十章第六節)內所聲明之參數的作用域是該 method-declarationmethod-body
  • 位於 indexer-declaration(第十章第九節)內所聲明之參數的作用域是該 indexer-declarationaccessor-declarations
  • 位於 operator-declaration(第十章第十節)內所聲明之參數的作用域是該 operator-declarationblock
  • 位於 constructor-declaration(第十章第十一節)內所聲明之參數的作用域是該 constructor-declarationconstructor-initializerblock
  • 位於 lambda-declaration(第七章第十五節)內所聲明之參數的作用域是該 lambda-expressionlambda-expression-body
  • 位於 anonymous-method-expression(第七章第十五節)內所聲明之參數的作用域是該 anonymous-method-expressionblock
  • 位於 labeled-statement(第八章第四節)內所聲明之標簽的作用域是該聲明所在的 block
  • 位於 local-variable-declaration(第八章第 5.1 節)內所聲明之局部變量的作用域是該聲明所在的 block
  • 位於 switch 語句(第八章第 8.3 節)的 switch-block 內所聲明的局部變量的作用域是該 switch-block
  • 位於 for 語句(第八章第 8.3 節)的 for-initializer 內所聲明的局部變量的作用域是該語句的 for-initializerfor-conditionfor-iterator 以及所含之 statement
  • 位於 local-constant-declaration(第八章第 5.2 節)內聲明的局部變量的作用域是該聲明出現的 block。在該局部變量的 constant-declarator 之前的文本位置上引用該局部變量將出現一個「編譯時錯誤」。
  • 作為 foreach-statementusing-statementlock-statementquery-expression 一部分所聲明的變量的作用域由給定構造(construct)的擴展(expansion)所決定。

在命名空間、類、結構或枚舉成員的作用域內,可以在位於該成員聲明之前的文本位置上引用該成員。比方說

class A
{
    void F() {
        i = 1;
    }
    int i = 0;
}

在此,F 在 i 聲明之前引用它是合法的。

在局部變量的作用域內,當引用局部變量的文本位置(textual position)在該局部變量聲明之前(local-variable-declarator)會出現一個「編譯時錯誤」。比方說

class A
{
    int i = 0;
    void F() {
        i = 1;   // 錯誤,在使用前先聲明
        int i;
        i = 2;
    }
    void G() {
        int j = (j = 1);     // 有效
    }
    void H() {
        int a = 1, b = ++a;  // 有效
    }
}

在上面方法 F 中,第一個對 i 的賦值實際上並不會引用外部作用域的字段。相反,它引用了本地局部變量,而其結果是引發一個「編譯時錯誤」,因為在文本上要求先聲明變量。在方法 G 中,為 j 進行聲明的同時在其初始化器內使用 j 是有效的,這是因為並沒有在 local-variable-declarator 之前使用。在方法 H 中,后面的 local-variable-declarator 正確引用了局部變量(該局部變量在同一個 local-variable-declarator 內前面那個 local-variable-declarator 中聲明了)。

本地布局變量的作用域規則被設計為保證(guarantee)表達式上下文(expression context)中使用的名稱的含義與在塊中使用的含義是相同的。如果局部變量的作用域只從其聲明之處其,到塊尾部截止,那么在上例中,第一個賦值將分配給實例變量(instance variable),第二次賦值將分配給局部變量,如果塊中的語句之后重新排列,會引發「編譯時錯誤」。

塊(block)中名稱的含義可能因名稱的使用上下文的不同而有所不同。在下例中,

using System;
class A {}
class Test
{
    static void Main() {
        string A = "hello, world";
        string s = A;         // 表達式上下文
        Type t = typeof(A);   // 類型上下文
        Console.WriteLine(s); // 輸出 "hello, world"
        Console.WriteLine(t); // 輸出 "A"
    }
}

在表達式上下文中的名稱 A 引用本地變量 A,在類型上下文中引用類型 A

名稱隱藏

實體(entity)的作用域(scope)往往比實體聲明空間包含(encompasses)更多的程序文本。具體來說,實體的作用域可能會引入新的聲明空間,而其中或許會包含與該實體同名的實體。這類聲明導致原始實體變為隱藏的(hidden)。相反,如果實體沒有被隱藏,那么我們說它是可見的(visible)

當作用域通過嵌套交叉(overlap)或當作用域通過繼承交叉,那么會導致名稱隱藏。兩個被隱藏的類型的特征在下面兩節中進行介紹。

通過嵌套隱藏

在命名空間內嵌套命名空間或類型,或者在類或結構內嵌套類型,以及性參與局部變量的聲明,都將導致嵌套隱藏(hiding through nesting)名稱。舉個例子。

class A
{
    int i = 0;
    void F() {
        int i = 1;
    }
    void G() {
        i = 1;
    }
}

在方法 F 內,實例變量 i 被局部變量 i 所隱藏,但在方法 G 內,i 依舊引用實例變量。

當內部作用域的名稱隱藏了外部作用域的名稱時,它將隱藏該名稱的所有重載。下例中,

class Outer
{
    static void F(int i) {}
    static void F(string s) {}
    class Inner
    {
        void G() {
            F(1);        // 調用 Outer.Inner.F
            F("Hello");  // 錯誤
        }
        static void F(long l) {}
    }
}

調用(call)F(1) 將調用(invokes)內部聲明的 F,這是因為所有外部出現的 F 都被內部聲明所隱藏。同樣的,調用 F("Hello") 的結果是出現「編譯時錯誤」。

通過繼承隱藏

當類或結構重新聲明(redeclare)從基類繼承的名稱時,會發生通過繼承隱藏(hiding through inheritance)其名稱。這種類型的名稱隱藏采取以下形式:

  • 常量、字段、屬性、事件或類型引入類或結構后,會隱藏所有基類中的同名成員。
  • 方法引入類或結構將隱藏所有同名的非方法基類成員(non-method base class members)和所有同簽名(方法名、參數數量、修飾符與類型)的基類方法。
  • 索引器引入類或結構將隱藏所有同簽名(參數數量與類型)的基類索引。

管理運算符聲明(governing operator declarations,第十章第十節)的規則使其不可能在派生類中聲明一個與其基類內同簽名的運算符。因此,操作符不能相互隱藏。

與從作用域外部對名稱進行隱藏相反,通過繼承的作用域隱藏名稱可訪問性會報出警告。下例中,

class Base
{
    public void F() {}
}
class Derived: Base
{
    public void F() {}  // 警告,隱藏了繼承到的名稱
}

派生類 DerivedF 的聲明會導致一個警告。隱藏所繼承的名稱實際上不是一個錯誤,因為這會妨礙基類自身的單獨改進。比方說,上述情況可能會發生因為 Base 的后續版本可能會引入一個 F 方法(而這個方法在之前版本中沒有)。如果上述情況是錯誤的,那么對單獨版本控制的基類的任何變化都會潛在導致派生類變得無效。

因隱藏繼承到的名字而引發的警告可通過 new 修飾符消除:

class Base
{
    public void F() {}
}
class Derived: Base
{
    new public void F() {}
}

修飾符 new 表示派生類 Derived 中的 F 是「新的」,它將隱藏所繼承到的(同簽名)成員。新成員的聲明將隱藏其所繼承到的,當且僅當位於新成員的作用域內。

class Base
{
    public static void F() {}
}
class Derived: Base
{
    new private static void F() {} // 只會隱藏 Derived 內的 Base.F
}
class MoreDerived: Derived
{
    static void G() { F(); }       // 調用 Base.F
}

上例中,派生類中的 F 聲明隱藏了(hide)其從基類繼承到的 F。但由於派生類中新 F 的可訪問性是 private,因此它的作用域達不到 MoreDerived。因此 MoreDerived.G 調用(call) F() 方法依舊合法,它將調用 Base.F


命名空間與類型名

C# 程序的上下文要求指定命名空間名標記 namespace-name 或類型名標記 type-name

namespace-name

namespace-name 標記是一個引用命名空間的 namespace-or-type-name 標記,它根據如下描述執行解析工作(resolution),namespace-namenamespace-or-type-name 標記必須引用一個命名空間,不然將會出現「編譯時錯誤」。沒有類型實參(type arguments,第四章第 4.1 節)可以在 namespace-name 中出現(只有類型才能具有類型實參)。

type-name 標記是一個引用類型的 namespace-or-type-name 標記,它根據如下描述執行解析工作,type-namenamespace-or-type-name 標記必須引用一個類型,不然會出現「編譯時錯誤」。

如果 namespace-or-type-name 標記是 qualified-alias-member 標記,則其含義如第九章第七節所述。否則 namespace-or-type-name 標記有以下四種形式:

  • I
  • I<A1, ..., AK>
  • N.I
  • N.I<A1, ..., AK>

其中 I 是單一修飾符(single identifier),Nnamespace-or-type-name<A1, ..., AK> 是可選的 type-argument-list。當沒有 type-argument-list 被指定時,K 將為零(zero)。

namespace-or-type-name 的含義取決於如下:

  • 如果 namespace-or-type-name 標記是 II<A1, ..., AK> 的形式的話:
    • 如果 K 是零(zero), namespace-or-type-name 標記出現在一個泛型方法聲明(generic method declaration,第十章第六節)內且其聲明還包含名為 I 的類型形參(type parameter,第十章第 1.3 節),那么 namespace-or-type-name 引用該類型形參;
    • 不然的話,如果 namespace-or-type-name 標記出現在類型聲明內,那么類型 T 的每個實例(第十章第 3.1 節),從該類型聲明的實例類型開始,每個閉包類(enclosing class)或結構聲明(如果有的話)的實例類型繼續如下過程:
      • 如果 K 是零,T 的聲明包含名為 I 的類型形參,那么 namespace-or-type-name 標記引用該類型形參;
      • 不然的話,如果 namespace-or-type-name 出現在類型聲明主體(body)內部,同時 T 或或其任意基類包含具有名稱 IK 個類型形參的嵌套可訪問類型(nested accessible type),那么 namespace-or-type-name 標記引用該給定類型實參(type arguments)構造的類型。如果存在多個該類型,則選擇類型聲明於較大程序派生類型內的那個類型。要注意,當確定了 namespace-or-type-name 標記的含義之后,非類型成員(non-type members,包括常量、字段、方法、屬性、索引器、操作符、實例構造函數、析構函數以及靜態構造函數)以及帶有不同數量類型形參的類型成員都將被忽略。
    • 如果前述步驟都不成功,則對每個命名空間 N,起始於出現 namespace-or-type-name 標記的命名空間,持續到每一個閉包命名空間(如果有的話),直至止於全局命名空間(global namespace),對以下步驟進行運算直至定位到(located)一個實體:
      • 如果 K 是零,同時 IN 內命名空間的名稱,則:
        • 如果 namespace-or-type-name 出現的位置閉包於名為 N 的命名空間定義內,同時該命名空間定義包含了一個名為 I 的與某命名空間或類型相關的 extern-alias-directive 標記或 using-alias-directive 標記,那么 namespace-or-type-name 標記將模棱兩可並會出現一個「編譯時錯誤」。
        • 否則的話,namespace-or-type-name 引用 N 內的命名空間 I
      • 不然的話,如果 N 包含擁有名為 I 且有 K 個類型形參的可訪問類型,則:
        • 如果 K 是零且 namespace-or-type-name 標記出現的位置閉包於名為 N 的命名空間聲明內,同時命名空間聲明包含名為 I 的與某類型或命名空間相關的 extern-alias-directiveusing-alias-directive 標記,那么 namespace-or-type-name 標記將模棱兩可並會出現一個「編譯時錯誤」。
        • 否則的話,namespace-or-type-name 標記引用由給定類型實參構建的類型。
      • 再不然的話,如果 namespace-or-type-name 標記出現的位置閉包於名為 N 的命名空間聲明內的話,則:
        • 如果 K 是零並且命名空間包含了名為 I 的與某個導入的命名空間或類型相關的 extern-alias-directiveusing-alias-directive 標記,那么 namespace-or-type-name 將引用該命名空間或類型。
        • 不然的話,如果由 using-namespace-directives 標記導入命名空間聲明的命名空間確切包含一個名為 I 的有 K 個類型形參,那么 namespace-or-type-name 將引用由該給定類型實參構造的類型。
        • 否則,如果由 using-namespace-directives 標記導入命名空間聲明的命名空間包含超過一個名為 I 的有 K 個類型形參的類型,那么 namespace-or-type-name 標記將模棱兩可並會出現一個「編譯時錯誤」。
      • 否則的話,說明 namespace-or-type-name 標記未被定義,同時報出一個「編譯時錯誤」。
    • 否則,形式為 N.IN.I<A1, ..., AK>.Nnamespace-or-type-name 首先解析(resolved)為 namespace-or-type-name。如果 N 的解析(resolution)不成功,那么會出現一個「編譯時錯誤」。否則,N.IN.I<A1, ..., AK> 將按如下進行解析:
      • 如果 K 是零,並且 N 引用一個命名空間,同時 N 包含一個名為 I 的嵌套命名空間,那么 namespace-or-type-name 標記將引用該嵌套命名空間。
      • 不然的話,如果 N 引用一個命名空間,同時 N 包含一個擁有名為 I 且有 K 個類型形參的可訪問類型,那么 namespace-or-type-name 將引用該由指定類型實參構造的類型。
      • 再不然的話,如果 N 引用一個(可能是構造的)類或結構類型,並且 N 或其任何一個基類包含一個嵌套的名為 I 且有 K 個類型形參的可訪問類,那么 namespace-or-type-name 將引用該由給定類型實參構造的類型。如果存在多個該類型,則選擇類型聲明於較大程序派生類型內的那個類型。需要注意的是,如果 N.I 的含義確定為 解析 N 基類的指定一部分,那么 N 的直接基類將被視為對象(object,第十章第 1.4.1 節)。
      • 否則的話,N.I 是個非法的 namespace-or-type-name 標記,並會出現一個「編譯時錯誤」。

只有在以下情況下才允許 namespace-or-type-name 引用一個靜態類(第十章第 1.1.3 節)

  • namespace-or-type-nameT.I 形式的 namespace-or-type-name 中的 T,或者
  • namespace-or-type-nametypeof(T) 形式的 typeof-expression(第七章第 5.11 節)中的 T。

完全限定名

每個命名空間和類型都擁有一個完全限定名(fully qualified name),它是一個唯一標識(uniquely identifies),用於相互間進行區分。命名空間或類型 N 的完全限定名遵照以下規則決定:

  • 如果 N 是一個全局命名空間成員,則其完全限定名為 N
  • 否則的話,其完全限定名為 S.N(S 為 N 所聲明的命名空間或類型之完全限定名)。

換句話說,N 的完全限定名是指向(lead to)N 的標識符(identifiers)的完整分層路徑(complete hierarchical path)。因為每個命名空間或類型的成員都必須有一個唯一的名稱,因此如果把它們放在命名空間或類型的完全限定名之后,這樣所形成的名稱也總是唯一的。

在下例中演示了多個命名空間和類型的聲明以及與其相關的完全限定名。

class A {}          // A
namespace X         // X
{
    class B         // X.B
    {
        class C {}  // X.B.C
    }
    namespace Y	    // X.Y
    {
        class D {}  // X.Y.D
    }
}
namespace X.Y       // X.Y
{
    class E {}      // X.Y.E
}

自動內存管理

C# 使用(employs)自動內存管理(automatic memory management),這解放了開發人員必須手動分配與釋放對象內存戰勇的麻煩。自動內存管理策略由垃圾回收器(garbage collector)實現。對象的內存管理生命周期如下:
0. 當對象被創建時,為它分配內存,運行構造函數,視該對象為存活(live)對象。

  1. 如果一個對象(或其任何一個部分)不能在后續執行(continuation of execution)中被訪問——除了析構函數——那么這個對象將被視為不再使用並符合銷毀條件(eligible for destruction)。C# 編譯器和垃圾回收器會選擇分析代碼,並確定哪個對象的引用在未來會被使用。比方說,如果某范圍內某個局部變量是某對象的唯一存在的引用,但在當前執行點(current execution point)之后的所有過程中(procedure)都不會再引用這個變量,那么垃圾回收器可能(但不是絕對)會理解為這個對象不再被使用。
  2. 一旦對象符合銷毀條件(eligible for destruction),那么在稍后某個時間(at some unspecified later time)將運行這個對象的析構函數(第十章第十三節)(如果有析構函數的話)。在正常情況下,對象的析構函數只會運行一次(once only),但特定實現(implementation-specific)的 APIs 可能會允許忽視(overridden)這個行為。
  3. 一旦運行該對象的析構函數,那么這個對象(或其任何一部分)都不能在后續執行中被訪問到——包括運行中的析構函數——這個對象將被視為不可訪問(inaccessible)並符合回收條件(eligible for collection)。
  4. 最后,在該對象符合回收條件后的某個時刻,垃圾回收器釋放分配給該對象的內存。

垃圾回收器維護着對象使用(object usage)的信息,透過這些信息為內存管理作出決策,諸如何處內存存放了新建對象,何時遷移(relocate)一個對象以及一個對象何時開始不被使用或不可訪問。

和其它有垃圾回收器的語言類似,C# 的設計也在努力使垃圾回收器能實現更廣泛的(wide range)內存管理策略(memory management policies)比方說,C# 並不強求析構函數一定要運行,並不強求對象一滿足條件就要立即被回收,並不強求析構函數一定要按照某個特定順序執行或是在某個特定線程上執行。

垃圾回收器的行為是可控的,在某種程度上講,可以通過 System.GC 上的靜態方法(來實現)。通過這個類可以請求執行一次回收操作、運行(或不運行)析構函數,等。

由於垃圾回收器在決定「何時回收(collect)對象並執行析構函數」這一點上充分自由(allowed wide latitude),因此一個合格的實現也許會產生與下面所示代碼有所不同的輸出。在程序

using System;
class A
{
    ~A() {
        Console.WriteLine("Destruct instance of A");
    }
}
class B
{
    object Ref;
    public B(object o) {
        Ref = o;
    }
    ~B() {
        Console.WriteLine("Destruct instance of B");
    }
}
class Test
{
    static void Main() {
        B b = new B(new A());
        b = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

中,創建了 A 和 B 的實例。當 null 值被賦予變量 b 之后,這些資源成為符合回收條件,這是由於自此之后用戶所寫的任何代碼都不可能訪問到它們。輸出的結果可能是

Destruct instance of A
Destruct instance of B

或者

Destruct instance of B
Destruct instance of A

,這是由於語言並未對對象被垃圾回收的順序強加限制。

盡管很相近,但區別「符合銷毀條件(eligible for destruction)」與「符合回收條件(eligible for collection)」還是比較重要的,比方說:

using System;
class A
{
    ~A() {
        Console.WriteLine("Destruct instance of A");
    }
    public void F() {
        Console.WriteLine("A.F");
        Test.RefA = this;
    }
}
class B
{
    public A Ref;
    ~B() {
        Console.WriteLine("Destruct instance of B");
        Ref.F();
    }
}
class Test
{
    public static A RefA;
    public static B RefB;
    static void Main() {
        RefB = new B();
        RefA = new A();
        RefB.Ref = RefA;
        RefB = null;
        RefA = null;
        // A and B now eligible for destruction
        GC.Collect();
        GC.WaitForPendingFinalizers();
        // B now eligible for collection, but A is not
        if (RefA != null)
            Console.WriteLine("RefA is not null");
    }
}

在上面程序中,如果立即回收器(garbage collector)選擇在運行 B 的析構函數前先運行 A 的析構函數,那么這個程序也許會輸出:

Destruct instance of A
Destruct instance of B
A.F
RefA is not null

注意,盡管 A 的實例未被使用,依舊調用了 A 的析構函數,同時 A 的方法也有可能被其它析構函數調用(此例中為 F)。同時還要注意,析構函數的運行可能導致對象在主程序中再次變的可訪問。在此例中,B 析構函數的運行導致了先前並未使用的 A 實例在通過引用 Test.RefA 時變得再次可訪問。在調用 WaitForPendingFinalizers 之后,B 的實例便符合回收條件了,但由於引用了 Test.RefA,所以實例 A 還不能被回收。

為了避免混淆和意外行為,建議類的析構函數僅對自己內部的字段數據進行清理,不要去干涉其它所引用的對象或靜態字段。

另一個替代析構函數的方法是讓類實現 System.IDisposable 接口。這將允許對象的客戶端(client of the object)決定何時釋放自身資源,通常會在 using 語句(第八章第十三節)通過資源的方式訪問該對象。


執行順序

C# 程序執行處理是這樣進行的,每一個執行線程的副作用都保持在臨界執行點(critical execution points)上。副作用被定義為:無定性字段(volatile field)的讀寫、非無定性變量(non-volatile variable)的寫入、外部資源(external resource)的寫入以及拋出異常。按照這個副作用定義的順序,臨界執行點分別是指:引用一個無定性字段(volatile fields,第十章第 5.3 節)、引用 lock 語句(第八章第十二節)以及引用線程的創建與終止。執行環境在遵照下列限制的前提下自由改變執行順序:

  • 在執行線程中保持數據依賴性。也就是說,計算每一個變量的值時,就好似在線程里所有語句都按照原本程序的順序執行的。
  • 保留初始化的排序規則(第十章第 5.4 節和第十章第 5.5 節)。
  • 對於無定性的(volatile)讀和寫(第十章第 5.3 節),副作用(side effects)的順序保持不變。另外,如果執行環境可以推斷(deduce)一個表達式的值不會被使用並且不會產生有效的(needed)副作用(包括所有因調用方法或訪問無定性字段所導致的副作用)的話,那么就不需要去計算表達式的每一個部分。當程序執行被一個異步(asynchronous)事件(諸如由另一個現成拋出異常)中斷(interrupted),就不能保證(guaranteed)可觀察(observable)到副作用是否會以原有的程序順序出現。

__EOF__

C# Language Specification 5.0 翻譯計划


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM