.NET C#基礎(6):名稱空間 - 組織程式碼的利器

HiroMuraki發表於2022-06-09

0. 文章目的

  面向C#新學者,介紹名稱空間(namespace)的概念以及C#中的名稱空間的相關內容

 

1. 閱讀基礎

  理解C與C#語言的基礎語法

 

2. 名稱衝突與名稱空間

2.1 一個生活例子

  假設貓貓頭在北京有一個叫AAA的朋友,在上海有兩個叫AAA的朋友,上海的兩個AAA一個喜歡鹹粽子,一個喜歡甜粽子。有一天貓貓找朋友玩,朋友問道:

  “AAA最近過得怎麼樣”,

  然而貓貓頭有三個叫AAA的朋友,因此貓貓頭不確定朋友問的是哪個AAA,於是朋友改問:

  “上海的那個AAA最近過得怎麼樣”

  精確了一點,但這還不夠,因為貓貓頭在上海認識兩個叫AAA的朋友,於是朋友再次改問:

  “上海的那個喜歡鹹粽子的AAA最近過得怎麼樣。

  到這裡,貓貓頭就確定了朋友問的是哪個小明。也就是說,通過地域+喜好+姓名,貓貓頭可以確定朋友指的具體的人。

  這個例子體現的就是名稱空間的實質:限定性修飾。

2.2 從C語言的缺陷到名稱空間

(1)函式命名衝突

  在談論什麼是名稱空間之前,我們先來看一看C語言中存在的一些問題。假設你和你的小夥伴同時開發一個C程式,並且你們很巧地定義了兩個函式名相同的函式:

void Init() { }

void Init() { }

  假設這兩個函式做的事完全不同(一個用來初始化控制檯,一個用來初始化印表機)而無法合併,那麼顯然此時需要用一個辦法來區分兩個函式。經過簡單討論,你和你的小夥伴決定在每個函式名前新增函式的作用物件名字加以區分,於是你們把函式名改成了如下:

void ConsoleInit() { } // 用於初始化控制檯的Init

void PrinterInit() { } // 用於初始化印表機的Init

  隨著開發進度的推進,你們建立的同名函式可能會越來越多,最後函式名看起來很可能像下面這樣:

void ConsoleInit() { }
void ConsoleFoo() { }
void ConsoleWhatever() { }
void ConsolePrint(const char* s) { }
...

void PrinterInit() { }
void PrinterFoo() { }
void PrinterWhatever() { }
void PrinterPrint(const char* s) { }
...

  當然這樣的函式名並不是不行,但是函式名中含有不必要的冗餘資訊,使用這種函式名會使程式碼可讀性下降,更重要的是,這還會使得編寫程式碼時所需要輸入的字元量大大增加:

ConsoleInit();
ConsoleFoo();
ConsoleWhatever();
ConsolePrint("...");

  在上述例子中,你需要使用的函式前都新增了Console字首,哪怕此時其實你可以明確自己大部分時候都是在操作控制檯,無論是使用還是閱讀,這些字首對你來說只是多餘的。另一方面,假設有辦法讓編譯器為某個範圍內所有使用的函式名都自動新增‘Console’字首,則可以像下面這樣:

// 告訴編譯器為下面的函式名都新增Console字首

Init();
Foo();
Whatever();
Print("...");

  顯然此時使用函式就方便了許多。

(2)讓編譯器代勞

  基於上述理由,可以定義一種語法來告訴編譯器為接下來使用的函式名都新增指定字首,例如:

using Console; // 告訴編譯器為接下來所有的函式都新增Console字首

Init();
Foo();
Whatever();
Print("...");

  在這裡,我們設定使用using關鍵字來告訴編譯器為所有的函式都新增Console字首,這樣在編譯器看來,上述實際程式碼就如下:

ConsoleInit();
ConsoleFoo();
ConsoleWhatever();
ConsolePrint("...");

  顯然此時程式依然可以準確地呼叫合適的函式。

(2)更進一步

  既然可以讓編譯器在呼叫函式時自動為其新增字首,那麼為何不讓編譯器為也在我們定義函式時為函式名自動新增字首?所以,還可以定義一種語法來告訴編譯器為定義的函式的函式名自動新增字首,在這裡,我們假設使用namespace(很快你就知道為什麼了)關鍵字指示需要新增的字首,並讓編譯器在其後的程式碼塊中定義的所有函式都新增所需的字首,就像如下:

namespace Console // 在後面程式碼塊中定義的函式前新增Console
{
    void Init() { }
    void Foo() { }
    void Whatever() { }
    void Print(const char* s) { }
}

  現在我們規定上述語法相當於告訴編譯器:編譯時為所有在程式碼塊中定義的函式自動新增Console字首,所以在編譯器進行自動轉換後,上述的程式碼就會像下面這樣:

void ConsoleInit() { }
void ConsoleFoo() { }
void ConsoleWhatever() { }
void ConsolePrint(const char* s) { }

  使用這種語法,像下面這樣定義函式不僅簡化了函式名,同時可以避免了潛在的函式名衝突:

namespace Console // 告訴編譯器為其後程式碼塊中所有的定義的函式新增Console字首
{
    void Init() { }
    // ...
}

namespace Printer // 告訴編譯器為其後程式碼塊中所有的定義的函式新增Printer字首
{
    void Init() { }
    // ...
}

  甚至更近一步,可以再允許使用巢狀語法新增字首:

namespace MeAndFriend
{
    namespace Console // 告訴編譯器為程式碼塊中所有的函式新增Console字首
    {
        void Init() { }
        // ... 
    }

    namespace Printer // 告訴編譯器為程式碼塊中所有的函式新增Printer字首
    {
        void Init() { }
        // ...
    }
}

  例如這樣上述程式碼就會由編譯器生成為類似‘MeAndFriendConsoleInit’與‘MeAndFriendPrinterInit’這樣的函式名。顯然這種語法大幅減少了需要輸入的內容,並且結合前面的using語法,呼叫時也方便:

using MeAndFriendConsole; // 告訴編譯器接下來所有的函式預設都有MeAndFriendConsole字首

Init();
... // 其他同樣需要MeAndFriendConsole字首的函式

2.3 名稱空間

  上面的例子中提到的那些所謂由編譯器自動的‘字首’,我們可以給它一個好聽的名字:名稱空間(namepsace),或者也可以稱其為名稱空間/名字空間。從上述例子可以看出,名稱空間其實就是精準定位到具體成員所用的限定修飾。名稱空間其實不是必須的東西,例如如果是為了避免函式名衝突,你完全可以通過為函式名新增各種限定詞的來避開衝突,然而正如前面所看到的,如果每個函式在定義和呼叫的時候都要輸入如此多的和函式所做的事無關的附加資訊,那麼輸入和閱讀程式碼都是額外的負擔,並且可能會對以後可能的程式碼修改帶來諸多不便,而名稱空間的出現以及相關語法支援在很大程度上減緩了這一問題。

 

3. C#中的名稱空間

  名稱空間是如此有用的東西,以至於許多現代化的程式語言都有類似名稱空間的設計。C#自然也有一套自己的名稱空間體系,MSDN上對名稱空間的定義是‘包含一組相關物件的作用域’,這一概念有點抽象,接下來我們從具體的使用中來理解。

3.1 使用

3.1.1 宣告名稱空間:namespace關鍵字

(1)基本名稱空間

  要在C#中宣告一個名稱空間,只需要使用namespace關鍵字並加上名稱空間的名稱與一對花括號(即程式碼塊)即可:

namespace Alpha
{
    
}

  在該名稱空間的程式碼塊中定義的型別都會作為屬於名稱空間的型別,例如:

namespace Alpha
{
    class Foo
    {

    }
}

  上述程式碼宣告瞭一個名稱空間Alpha,並在Alpha下定義了一個Foo物件。按照名稱空間的實際意義來說,如果用句點.來連線名稱空間與型別名,那麼Foo型別的完整名稱就是Alpha.Program。利用名稱空間,可以像下面這樣定義相同的型別名而不會發生衝突:

namespace Alpha
{
    class Foo
    {

    }
}

namespace Beta
{
    class Foo
    {

    }
}

  儘管上述程式碼中出現了兩個名稱為Foo的類,但兩者的完整型別名分別為Alpha.Foo與Beta.Foo,在程式看來這是兩個完全不同的型別。編譯器在編譯時會將型別名替換為完整的型別名(即名稱空間+型別名的組合),因此程式執行時可以準確定位到具體型別而不會出現混亂。

  為了方便後文的闡述,我們將這種‘以所屬名稱空間的完整名稱.型別名格式表達的型別名’稱為‘完整型別名’。

(2)巢狀名稱空間

  可以巢狀宣告名稱空間:

namespace Alpha
{
    namespace Beta
    {
        class Program
        {   

        }
    }
}

  此時上述程式碼中的Program型別的完整型別名為Alpha.Beta.Program。不過巢狀名稱空間會浪費大量的列縮排(按照格式規範,每一級程式碼塊中的程式碼需要縮排4個空格,因此每多一層名稱空間就會導致所有程式碼多縮排4個空格),因此還可以通過使用句點.來連線名稱空間以表示名稱空間的巢狀關係,對於上述名稱空間的巢狀也可以採用下述宣告方法:

namespace Alpha.Beta
{
    class Program
    {   

    }
}

  接著從概念上來講,Alpha是根空間,而Beta則是Alpha的子空間。此外,所有名稱空間都有一個共同的根空間,被稱為全域性名稱空間,它是隱式且匿名的,全域性名稱空間下的內容可以在不新增額外限定的情況下直接訪問。例如,下面是一個位於全域性名稱空間的類:

class Foo
{

}

  此後若需要使用此Foo型別則可以直接使用其型別名‘Foo’,而不需要新增額外的限定(前提是不與使用時所處的名稱空間型別名衝突,如果衝突需要新增額外限定,後文會提到)。簡單來說可以視Foo型別沒有所屬的名稱空間,但從概念上來講,型別Foo依然屬於一個名稱空間,只不過這個名稱空間是隱式且匿名的。

3.1.2 使用名稱空間:using關鍵字

(1)using指令

  同一個名稱空間下的型別之間可以直接使用其型別名訪問,例如對於以下型別定義:

namespace Alpha
{
    class Foo { }
}

  型別Foo的完整型別名是Alpha.Foo,但在Alpha名稱空間內使用Foo型別時可以直接使用其型別名稱‘Foo’:

namespace Alpha
{
    class Foo { }

    class Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo();
        }
    }
}

  這一規則同樣適用於其子空間:

namespace Alpha
{
    class Foo { }  // 定義為Alpha空間下的Foo型別

    namespace Beta // Beta是Alpha的子空間
    {
        class Program
        {
            static void Main(string[] args)
            {
                Foo foo = new Foo();  // 同樣可以直接使用類名指示型別
            }
        }
    }
}

  另一方面,如果名稱空間要使用其子空間中定義的型別,則可以通過子空間名.型別名訪問,也就是相當於以本名稱空間為起點使用目標型別,例如:

namespace Alpha
{
    namespace Beta     // Beta是Alpha的巢狀名稱空間
    {
        class Cat { }  // 定義在Alpha.Beta下的Cat類
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            Beta.Cat cat = new Beta.Cat(); // 使用子空間名+型別名指定型別
        }
    }
}

  然而,如果要在其他名稱空間中使用Alpha名稱空間下的Foo,則需要使用其完整型別名Alpha.Foo,例如在Test名稱空間下使用Alpha.Foo:

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Alpha.Foo foo = new Alpha.Foo(); // 使用完整型別名
        }
    }
}

  顯然使用完整型別名是一件很繁瑣的事,C#自然提供了相應的解決方法。如果要想在如同Alpha名稱空間中一樣簡單地直接使用Foo,可以使用using指令:

using Alpha; // using指令,匯入Alpha名稱空間

namespace Beta
{
    class Program
    {
        static void Main(string[] args)
        {
            Foo foo1 = GetFoo();
        }
        static Foo GetFoo() { ... }
        static void CheckFoo(Foo foo) { ... }
    }
}

  在using關鍵字後面跟隨名稱空間名,可以將指定的名稱空間‘匯入’到本檔案中。所謂‘匯入’就是告訴編譯器如果程式碼中如果出現了型別名,那麼除了在本名稱空間範圍查詢型別外,還可以在由using指令匯入過的名稱空間範圍查詢。當編譯器查詢到指定型別後會將型別名替換為其完整型別名。也就是說,上述的using Alpha指令告訴編譯器可以在Alpha名稱空間中查詢型別,因此當編譯器發現Main方法的Foo型別沒有在Beta名稱空間中定義時,會從Alpha名稱空間中查詢,在Alpha下發現Foo後,編譯器將Main中的Foo替換為其完整型別名Alpha.Foo。因此上述程式碼在編譯後等同於去掉using指令並將所有的Foo替換為Alpha.Foo的編譯結果。

  另外,可以同時使用多個using語句來匯入多個名稱空間,並且using的順序不影響程式行為,不會出現類似‘後面using的名稱空間覆蓋前面using的名稱空間中具有相同名稱的型別’的問題。另外,同一個using指令可以重複宣告,儘管從實際來說這一行為沒有意義。因此,下述的using宣告的作用都是一致的:

// 1. 先Alpha再Beta
using Alpha;
using Beta;

// 2. 先Beta再Alpha
using Beta;
using Alpha;

// 3. 重複使用相同的using指令,可行,但無意義,編譯器會警告
using Alpha;
using Alpha;
using Beta;
using Beta;
using Beta;

  從實際行為來說,using指令只是告訴編譯器如果程式碼中如果出現了型別名,那麼除了在本名稱空間範圍內查詢對應型別外,還可以在由using指令匯入過的名稱空間範圍內查詢。using指令的作用域是檔案範圍,也就是說一個using指令對使用該using指令的整個檔案都有效,基於這個原因,using指令被要求放置在檔案開頭以清楚描述其行為。

(2)using別名

  你可以使用using為型別定義別名:

using Alias = Alpha.Foo;

Alias foo = new Alias(); // 等同於Alpha.Foo foo = new Alpha.Foo()

  通過使用using <別名> = <完整型別名>,可以為指定型別指定一個別名,在其後的程式碼中可以使用該別名來指代該型別,例如上述程式碼中為Alpha.Foo型別指定了別名Alias,編譯器在遇到程式碼中出現使用Alias型別的地方就會將其替換為Alpha.Foo。另外,using別名也適用於泛型:

using CatList = System.Collections.Generic.List<Cat>;

CatList cats = new CatList(); 

  using別名作用域也是整個檔案,因此基於同樣的原因,using別名的宣告也要求放在檔案開頭。

3.1.3 global關鍵字

  預設情況下,編譯器獲取查詢型別時會優先以當前名稱空間為起點查詢:

class Foo { }     // 位於全域性名稱空間的Foo

namespace Alpha
{
    class Foo { } // 位於名稱空間Alpha的Foo

    class Program // 位於名稱空間Alpha的Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo(); // 此時的Foo是Alpha.Foo,因為編譯器優先從當前名稱空間查詢
        }
    }
}

  上述程式碼中Main方法中的Foo是Alpha.Foo,原因是編譯器首先以名稱空間Alpha為起點查詢‘Foo’時就發現了Foo的型別定義,因此不會再去查詢全域性名稱空間中的Foo。此時如果要使用全域性名稱空間中的Foo,則需要告知編譯器從全域性名稱空間開始查詢(而非當前名稱空間),可在通過在型別名前新增global::達到此目的:

global::Foo foo = new global::Foo(); // 告訴編譯器從全域性名稱空間開始查詢,此時Foo就是位於全域性名稱空間的那個Foo

  可以認為,global就是全域性名稱空間的‘名字’,只不過後面需要接::而不是.(寫過C++的朋友可能會對這一語法感到頗為熟悉)。另外,可以結合global與using指令進行全域性的名稱空間匯入,這在後文會提到。

3.2 名稱空間衝突

(1)匯入的名稱空間與當前名稱空間存在型別名衝突

  有時候匯入的名稱空間中可能存在與當前名稱空間中衝突的型別名,例如:

  檔案1內容:

namespace Alpha
{
    class Foo { }
}

  檔案2內容:

using Alpha;

namespace Test
{
    class Foo { }
    class Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo(); // Alpha.Foo還是Test.Foo?
        }
    }
}

  檔案1中的Alpha名稱空間中定義了一個Foo物件,檔案2中使用using指令匯入了Alpha名稱空間,但同時在其名稱空間Test下也定義了一個Foo,並且Main方法中使用的不是完整型別名,那麼上述程式碼使用的應該是哪一個Foo?答案是Test.Foo,也就是本名稱空間下的Foo。原因在前文提到過,就是編譯器獲取型別時會優先以當前名稱空間為起點查詢,只有當前名稱空間下找不到才會從匯入過的名稱空間中查詢。因此,此時如果要使用Alpha下的Foo,依然需要其使用完整型別名:

Alpha.Foo foo = new Alpha.Foo();

  注意此時using別名無效,原因同樣是編譯器獲取型別時會優先以當前名稱空間為起點查詢,這比using別名的優先順序高。

(2)匯入的名稱空間之間存在型別名衝突

  多個using指令匯入的名稱空間之間也可能出現型別名衝突,例如兩個檔案的檔案內容如下:

  檔案1內容:

namespace Alpha
{
    class Foo { }
    class Cat { }
}

namespace Beta
{
    class Foo { }
    class Dog { }
}

  檔案2內容:

using Alpha;
using Beta;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Cat cat = new Cat(); // 是Alpha.Cat
            Dog dog = new Dog(); // 是Beta.Dog
            Foo foo = new Foo(); // Alpha.Foo還是Beta.Foo?
        }
    }
}

  檔案2中使用兩個using指令分別匯入了Alpha於Beta名稱空間,並在Main方法中使用了這兩個名稱空間下的型別。其中Cat只在Alpha名稱空間下定義過,因此可以確認其完整型別名,同理Dog也可以。然而由於Alpha和Beta同時定義了Foo型別,並且using的順序不影響程式行為,因此此時編譯器無法確認Foo到底應該使用Alpha還是Beta名稱空間下的版本。要解決這類問題,同樣需要使用完整型別名:

Alpha.Foo foo = new Alpha.Foo();
Beta.Foo foo = new Beta.Foo();

  當然,此時也可以使用using別名來指定Foo所代表的型別:

using Foo = Alpha.Foo; // 將Foo作為Alpha.Foo的別名
using Foo = Beta.Foo;  // 或者將Foo作為Beta.Foo的別名

3.3 特殊名稱空間

3.3.1 static名稱空間

  static名稱空間用於簡化靜態類的成員呼叫。例如有以下靜態類:

namespace Hello
{
    static class Speaker 
    {
        public static void Say();
    }
}

  在另一個檔案中使用此靜態類:

using Hello;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Speaker.Say();
            Speaker.Say();
            Speaker.Say();
        }
    }
}

  上述用法沒有問題,但是靜態類不需要例項化,並且靜態類在很多時候只是起到對程式碼的組織作用。換句話說,靜態類的類名有時候其實並不重要,可以省略。為此,C#提供了一種特殊的using指令讓程式設計師在呼叫靜態類成員時可以省略其類名:

using static Hello.Speaker; // using static + 靜態類的完整型別名

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Say();
            Say();
            Say();
        }
    }
}

  上述程式碼中使用了using static + 靜態類的完整型別名向當前檔案匯入靜態類,告訴編譯器接下來的程式碼中使用的方法或欄位如果沒在本型別中找到定義,則可以從匯入過的靜態類的成員中查詢。因此,上面的Main方法呼叫Say方法時,編譯器發現Program型別中沒有定義過Say方法,於是嘗試從使用using static匯入過的Hello.Speaker靜態類中查詢名為Say的方法。

3.3.2 作用於檔案範圍的名稱空間

  普通的名稱空間宣告作用範圍是其後面的程式碼塊,但你也可以宣告作用於整個檔案範圍的名稱空間,宣告後所有在該檔案下定義的型別都將納入此名稱空間:

namespace Alpha; // 將Alpha宣告為檔案作用域的名稱空間

class Cat { }
class Dog { }

  宣告作用於檔案範圍的名稱空間和作用域程式碼塊的名稱空間語法相似,其最大的優點在於其可以減少格式化程式碼樣式時所需的列縮排量。需要說明的是,宣告作用於檔案範圍的名稱空間有如下限制:

  1. 只能宣告一次檔案範圍的名稱空間,這是顯而易見的
  2. 不能再宣告普通名稱空間,也就是說,下述程式碼無效,Beta也不會視為Alpha的子空間:
namespace Alpha;

namespace Beta
{

}

3.3.3 全域性名稱空間

  預設的using指令作用域是檔案,也就是說一個using指令的宣告只對使用了該using指令的這一個檔案有效。但有時候一個名稱空間可能會頻繁用於多個檔案,例如System名稱空間相當常用,很多檔案都需要額外新增using System來匯入此名稱空間,這有時候會為編碼帶來枯燥的體驗,為此,C#提供了一種名為全域性using的匯入方法,按此using匯入的名稱空間會作用於整個專案,只需要在using指令前新增global關鍵字即可將名稱空間其作為全域性名稱空間匯入:

global using System;

  在一個專案中的任意一個檔案中使用以上using宣告後,該專案中所有的檔案都會預設匯入過System名稱空間。另外,語法規定全域性using必須位於普通using之前,因此建議將全域性using寫入到單個檔案中。

 

4. 名稱空間雜談

4.1 完整型別名的查詢流程

  編譯器會按以下流順序查詢型別:

  (上述流程中,如果在某一藍色塊找到了所需的型別就跳出,否則按箭頭順序進入下一個藍色塊查詢)

  不需要刻意記憶該流程,上述流程最主要的意義在於說明名稱空間的一些行為。本身的意義不大,實際程式碼編寫時根據IDE提示與執行結果可輕易解決相關問題。

4.2 名稱空間的“命名”

  MSDN上給出了名稱空間的命名建議

<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]

  1. 在名稱空間名稱前加上公司名稱或個人名稱。
  2. 在名稱空間名稱的第二層使用穩定的、與版本無關的產品名稱。
  3. 使用帕斯卡命名法,並使用句點分隔名稱空間元件。不過,如果品牌名有自身的大小寫規則,則遵循使用品牌名。
  4. 在適當的情況下使用複數名稱空間名稱,首字母縮寫單詞例外。
  5. 名稱空間不應該與其作用域內的型別名相同(例如不應該在名稱空間Foo下定義Foo型別)。

  示例:

namespace Microsoft.Office.PowerPoint { }

4.3 namespace與using位置

  C#對使用namespace與using語句出現的位置有一些要求,通常一個可能的順序如下:

global using System; // 全域性using指令

namespace Alpha;     // 作用於檔案範圍的名稱空間

using Alpha;         // using指令

using IntList = System.Collections.Generic.List<int>; // using別名

namespace Beta       // 普通名稱空間
{

}

  具體的順序不需要刻意記憶,若順序不符合要求編譯器會給出提示。

4.4 完全限定名稱空間

  我們將以全域性名稱空間為根空間表示的名稱空間稱為‘完全限定名稱空間’,例如對於以下名稱空間:

namespace A
{
    namespace B
    {
        namespace C
        {
            
        }
    }
}

  如果要表示上述名稱空間宣告下的名稱空間C,可以使用A.B.C,但如果此時位於名稱空間A中,那麼也可以使用B.C,但是其中只有A.B.C這一表示才是表示的完全限定名稱空間。通過完全限定名稱空間可以從全域性名稱空間開始準確定位到某一名稱空間。

4.5 隱式全域性名稱空間匯入

  現在新建C#專案後,你會發現專案的csproj檔案裡有這樣一行配置:

<ImplicitUsings>enable</ImplicitUsings>

  當專案開啟ImplicitUsings時,其作用相當於為你的專案引入了一個對常用名稱空間進行全域性匯入的檔案,也就是說相當於在你的專案中加入了有類似如下內容的檔案:

global using System;
global using System.Collections.Generic;
...

  這一功能是對全域性using指令的實際應用,參照於此,你也可以定義一個全域性匯入自己常用的名稱空間的檔案,並按需要新增到自己的專案中。

4.6 誤區

  雖然本文最開始的例子中,我們為C語言假想的using語句的作用是‘視接下來所有的函式都有某一字首’,C#中的名稱空間的表現似乎也確實如此。然而,僅僅是這麼認為的話會讓人誤認為下面的程式碼可以通過編譯:

  檔案1內容:

namespace A.B
{
    class Foo { }
}

  檔案2內容:

using A;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            B.Foo foo = new B.Foo(); // 看起來B.Foo在新增using的A.字首後,就是A.B.Foo了
        }
    }
}

  在上述檔案2中,使用using宣告匯入了名稱空間A,然後在Main方法中嘗試使用B.Foo來表示A.B.Foo型別。然而這是無法通過編譯的,如果這一行為允許,那麼考慮下面程式碼:

  檔案1內容:

namespace A.B
{
    class Foo { }
}

namespace B
{
    class Foo { }
}

  檔案2內容:

using A;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            B.Foo foo = new B.Foo(); // 此時的foo是B.Foo還是A.B.Foo
        }
    }
}

  由於Test名稱空間下沒有B.Foo的匹配項,因此現在編譯器會從using宣告匯入過的名稱空間中查詢型別。問題在於此時的B.Foo到底是作為B.Foo的完整型別名,還是A.B.Foo的部分型別名?為了避免這一令人迷惑的情況,C#不允許上述做法。可以認為,當你決定在型別名中加入名稱空間,並且型別所屬的名稱空間不是當前名稱空間的子空間,就只能使用其完整型別名。如果型別所屬的名稱空間是當前名稱空間的子空間則可以使用子空間名.型別名來指示型別:

namespace B
{
    class Foo
    {
        public void Tell() { }
    }
}



namespace A
{
    namespace B
    {
        class Foo { }
    }
    class Program
    {
        static void Main(string[] args)
        {
            B.Foo foo = new B.Foo(); // B.Foo的完整型別名是A.B.Foo
            foo.Tell();
        } 
    }
}

  由於編譯器會優先以當前名稱空間為起點查詢型別,而上述情況下編譯器可以從名稱空間A下查詢到B.Foo,故會選擇優先使用。如果要使用完整型別名為B.Foo的型別,則新增使用global::即可。

4.7 使用建議

(1)一個檔案中應只宣告一個名稱空間

(2)儘可能避免用巢狀宣告名稱空間,而是使用句點.表示名稱空間的巢狀關係:

namespace Alpha      // 巢狀宣告名稱空間
{
    namespace Beta
    {

    }
}

namespace Alpha.Beta // 使用.來表示名稱空間巢狀關係
{

}

(3)靈活使用Using別名來避免不必要的型別定義與簡化型別名

using IntList = System.Collections.Generic.List<int>; // 表示一個Int列表,但沒有額外的型別定義,同時簡化了型別名

(4)規範匯入名稱空間的順序,例如可以按照名稱空間的名稱匯入,或者按照先內建庫→第三方庫→當前專案的順序匯入等等

相關文章