0. 文章目的
面向C#新學者,介紹名稱空間(namespace)的概念以及C#中的名稱空間的相關內容。
1. 閱讀基礎
理解C與C#語言的基礎語法。
理解作用域概念。
2. 名稱衝突與名稱空間
2.1 一個生活例子
假設貓貓頭在北京有一個叫AAA的朋友,在上海有兩個叫AAA的朋友,上海的兩個AAA一個喜歡鹹粽子,一個喜歡甜粽子。有一天貓貓找朋友玩,朋友問道:
“AAA最近過得怎麼樣”,
然而貓貓頭有三個叫AAA的朋友,因此貓貓頭不確定朋友問的是哪個AAA,於是朋友改問:
“上海的那個AAA最近過得怎麼樣”
精確了一點,但這還不夠,因為貓貓頭在上海認識兩個叫AAA的朋友,於是朋友再次改問:
“上海的那個喜歡鹹粽子的AAA最近過得怎麼樣。
到這裡,貓貓頭就確定了朋友問的是哪個小明。也就是說,通過地域+喜好+姓名,貓貓頭可以確定朋友指的具體的人。
這個例子中,通過一層一層的限定修飾,我們從逐漸精確定位到了指定的AAA。在現實中,通過各種各樣的限定修飾,我們可以區分具有相似名稱的人或物,而對於程式來說也是如此。
2.2 從C語言的缺陷到名稱空間
(1)函式命名衝突
在談論什麼是名稱空間之前,我們先來看一看C語言中存在的一些問題。假設你和你的小夥伴同時開發一個C程式,並且你們很巧地定義了兩個函式名相同的函式:
void Init() { } // 初始化控制檯
void Init() { } // 初始化印表機
假設這兩個函式做的事完全不同(一個用來初始化控制檯(Console),一個用來初始化印表機(Printer))而無法合併,那麼顯然此時需要用一個辦法來區分兩個函式。經過簡單討論,你和你的小夥伴決定在每個函式名前新增函式的作用物件名字加以區分,於是你們把函式名改成了如下:
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(); // 在編譯器看來是ConsoleInit
Foo(); // 在編譯器看來是ConsoleFoo
Whatever(); // 在編譯器看來是ConsoleWhatever
Print("..."); // 在編譯器看來是ConsolePrint
}
此時就可以不用輸入很多不必要的Console字首,使用函式就方便了許多。
(2)讓編譯器代勞
基於上述理由,可以定義一種語法來告訴編譯器為接下來使用的函式名都新增指定字首,例如:
// 使用namespace關鍵字告訴編譯器為其後程式碼塊中所有的函式名都新增Console字首
namespace Console
{
Init();
Foo();
Whatever();
Print("...");
}
在這裡,我們設定使用namespace關鍵字來告訴編譯器為後面程式碼塊中所有的函式都新增其後面指定的Console字首,這樣在編譯器看來,上述實際程式碼就如下:
ConsoleInit();
ConsoleFoo();
ConsoleWhatever();
ConsolePrint("...");
顯然此時程式依然可以準確地呼叫合適的函式。同樣,既然可以讓編譯器在呼叫函式時自動為其新增字首,那麼自然也可以讓其在定義函式時也為函式名自動新增字首:
namespace Console // 為其後程式碼塊中的成員自動新增Console字首
{
void Init() { ... }
void Foo() { ... }
void Whatever() { ... }
void Print(const char* s) { ... }
}
這樣,在編譯器進行自動轉換後,上述的程式碼就會像下面這樣:
void ConsoleInit() { }
void ConsoleFoo() { }
void ConsoleWhatever() { }
void ConsolePrint(const char* s) { }
有了這種自動新增字首的語法後,那麼在對控制檯進行相關的操作時,就可以像下面這樣操作了:
// 使用namespace關鍵字告訴編譯器為其後程式碼塊中所有的函式名都新增Console字首
namespace Console
{
void Init() // 定義init函式(函式全名是ConsoleInit)
{
...
}
void Launch() // 定義Launch函式(函式全名是ConsoleLaunch),並在函式中呼叫前面定義的Init方法
{
Init(); // 該Init即ConsoleInit
...
}
}
而對印表機進行相關操作時,也只需要:
// 使用namespace關鍵字告訴編譯器為其後程式碼塊中所有的函式名都新增Printer字首
namespace Printer
{
void Init() // 定義init函式(函式全名是PrinterInit)
{
...
}
void Launch() // 定義Launch函式(函式全名是PrinterLaunch),並在函式中呼叫前面定義的Init方法
{
Init(); // 該Init即PrinterInit
...
}
}
顯然,有了自動新增字首的語法後,定義和使用函式都方便了許多。
更近一步,還可以再允許使用巢狀語法新增字首:
namespace MeAndFriend // 為其後程式碼塊中的成員自動新增MeAndFriend字首
{
namespace Console // 為其後程式碼塊中的成員自動新增Console字首
{
void Init() { } // 此時Init實際應該叫MeAndFriendConsoleInit
// ...
}
namespace Printer // 為其後程式碼塊中的成員自動新增Printer字首
{
void Init() { } // 此時Init實際應該叫MeAndFriendPrinterInit
// ...
}
}
這樣,例如上述程式碼就會由編譯器生成類似‘MeAndFriendConsoleInit’與‘MeAndFriendPrinterInit’這樣本來會很長的函式名。
2.3 作用域與名稱空間
在上述程式碼中,我們定義了一個語法來表示一個為某一個程式碼塊中的成員新增字首:
// 告訴編譯器程式碼塊中的成員都預設有Console字首
namespace Console
{
...
}
此時在編譯器看來,所有處於namespace Console後面的程式碼塊作用域中的成員都自帶Console字首。而對作用域來說,通過namespace關鍵字,我們為其提供了一個名字Console,也就是說,這是一個有名字的作用域,而這種有名字的作用域,就是所謂的名稱空間(或者也可以稱其為名稱空間/名字空間)。名稱空間(namespace)中的“命名(name)”部分就是一個限定修飾詞,而其“空間(space)”就是這一限定修飾詞的作用域。
看起來名稱空間似乎不是必須的東西,例如如果是為了避免函式名衝突,那麼完全可以手動為函式名新增各種限定詞來避開衝突,然而正如前面所看到的,如果每個函式在定義和呼叫的時候都要輸入如此多的和函式所做的事無關的附加資訊,那麼這對於輸入和閱讀程式碼都是額外的負擔,並且可能會對以後可能的程式碼修改帶來諸多不便,而名稱空間這一概念的出現在很大程度上緩解了這一問題。
3. C#中的名稱空間
名稱空間是如此有用的東西,以至於不少現代化的程式語言都有類似名稱空間的設計。C#自然也有一套自己的名稱空間體系,MSDN上對名稱空間的定義是‘包含一組相關物件的作用域’,這一概念有點抽象,接下來我們從具體的使用中來理解。
3.1 基本使用
3.1.1 namespace關鍵字
(1)全域性名稱空間
預設情況下,存在一個被稱為全域性名稱空間的根空間,這個空間是匿名隱式的,有全域性的作用域。因此如果一個型別沒有定義在任何宣告的名稱空間下,則預設其直接位於全域性名稱空間。
(2)宣告名稱空間
顯然如果只有全域性名稱空間沒有太大的意義,應該還要能宣告特定的名稱空間,要在C#中宣告一個名稱空間,只需要使用namespace關鍵字並加上空間名與一對花括號(即定義程式碼塊)即可,下述程式碼宣告瞭一個名稱空間Alpha:
namespace Alpha
{
}
同樣,名稱空間也可以巢狀宣告:
namespace Alpha
{
namespace Beta
{
}
}
(Beta是巢狀在Alpha中的一個子空間,而Alpha則是Beta的父空間)
不過按照格式規範,如果像上述那樣巢狀名稱空間的話,在格式化程式碼樣式時會浪費大量的列縮排(每一級程式碼塊中的程式碼需要縮排4個空格,因此每多一層名稱空間就會導致所有程式碼多縮排4個空格)。因此還可以通過使用句點.
來連線名稱空間以表示名稱空間的巢狀關係,上述巢狀名稱空間也可以採用下述宣告方法:
namespace Alpha.Beta
{
}
需要說明的是,所有名稱空間都可以視為全域性名稱空間的子空間,而如果我們從全域性名稱空間開始書寫一個名稱空間,則將這一名稱空間名稱為“完全限定名稱空間”。如在上述名稱空間的定義下,當表示Beta名稱空間時,Alpha.Beta就是一個完全限定名稱空間,而Beta則不是完全限定名稱空間。另外,全域性名稱空間雖然是匿名的,但是可以使用global關鍵字來指代,並在其後使用::
(而不是.
)連線子空間,因此,完全限定名稱空間Alpha.Beta也可以表示為:
global::Alpha.Beta
(寫過C++的朋友應該有一種熟悉感)
(2)在名稱空間中定義型別
在一個名稱空間的程式碼塊作用域內定義的型別都會歸屬到該名稱空間,例如下述程式碼中,Foo屬於Alpha名稱空間:
namespace Alpha
{
class Foo
{
}
}
上述程式碼在名稱空間Alpha下定義了一個Foo物件,此時若按我們在前文對名稱空間的實際作用的解釋來看,用句點.
來連線名稱空間與型別名,那麼Foo型別的完整名稱應該是Alpha.Foo
,也就是說,名稱空間的名字和該空間下的型別名可以共同組成一個更為明確的型別名,因此,像下面這樣定義不會發生衝突:
namespace Alpha
{
class Foo
{
}
}
namespace Beta
{
class Foo
{
}
}
儘管上述程式碼中出現了兩個名稱為Foo的類,但兩個Foo的完整名稱分別為Alpha.Foo與Beta.Foo,在編譯器看來這可以是兩個完全不同的型別。型別的完整名為其所屬的完全限定名稱空間加上型別名,例如對於以下位於巢狀名稱空間Alpha.Beta的Foo類:
// 或者簡化的巢狀寫法
// namespace Alpha.Beta
namespace Alpha
{
namespace Beta
{
class Foo // Alpha.Beta.Foo
{
}
}
}
Foo型別的完整名應該是Alpha.Beta.Foo
而不是Beta.Foo,為了方便後文的闡述,我們將這種‘以完全限定名稱空間.型別名
格式表達的型別名’稱為‘完整型別名’。
3.1.2 using關鍵字
(1)跨名稱空間訪問
位於同一個名稱空間作用域中的型別之間可以直接使用型別名訪問,例如:
namespace Alpha
{
class Foo { }
class Program
{
static void Main(string[] args)
{
Foo foo = new Foo(); // 直接使用Foo表示Alpha.Foo
}
}
}
型別Foo的完整型別名是Alpha.Foo,但由於Program類也處在Alpha名稱空間作用域內,因此可以直接使用Foo來表示Alpha.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(); // 使用完整型別名
}
}
}
(2):using指令
顯然跨名稱空間使用型別時使用完整型別名是一件很繁瑣的事,C#自然提供了相應的解決方法。對於上面的例子,如果要想如同在Alpha名稱空間中一樣簡單地直接使用Foo表示Alpha.Foo,可以使用using指令來達到這一目的:
using Alpha; // using指令,匯入Alpha名稱空間
namespace Beta
{
class Program
{
static void Main(string[] args)
{
Foo foo = GetFoo();
}
static Foo GetFoo() { ... }
static void CheckFoo(Foo foo) { ... }
}
}
(預設情況下,由於using指令影響的範圍是使用該using指令的整個檔案,因此using指令被要求放置在檔案開頭以清楚描述其行為)
在using關鍵字後面跟隨名稱空間名,表示在當前檔案中‘使用指定名稱空間的作用域’,或者說,把指定名稱空間的作用域匯入到當前檔案,為了方便,後文中將這一行為稱為‘匯入名稱空間’。因此上述程式碼使用using指令匯入名稱空間Alpha後,就會使用Alpha的作用域,此時程式碼就像下圖這樣:
由於此時程式碼可以視為在Alpha名稱空間的作用域中,因此可以直接使用Foo來表示Alpha.Foo。
另外,可以同時使用多個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 Alpha;
namespace Beta
{
class Foo { } // Beta.Foo
}
Foo的完整型別名依然是Beta.Foo,而不會因為匯入了Alpha變成Alpha.Beta.Foo。
(2) 全域性using宣告
預設的using指令作用域是檔案,也就是說一個using指令的宣告只對使用了該using指令的這一個檔案有效。但有時候一個名稱空間可能會頻繁用於多個檔案,例如System名稱空間相當常用,很多檔案都需要額外新增using System來匯入此名稱空間,這有時候會為編碼帶來枯燥的體驗,為此,C#提供了一種名為全域性using的匯入方法,按此using匯入的名稱空間會作用於整個專案,只需要在using指令前新增global關鍵字即可將名稱空間其作為全域性名稱空間匯入:
global using System;
在一個專案中的任意一個檔案中使用以上using宣告後,該專案中所有的檔案都會預設匯入過System名稱空間。另外,語法規定全域性using必須位於普通using之前。通常建議將全域性using寫入到單個檔案中。
(3)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關鍵字
在介紹global關鍵字之前,需要說一下編譯器查詢型別的過程:
- 以使用該型別的名稱空間為起點查詢目標型別
- 若上一個步驟中沒有找到則嘗試查詢using別名
- 若上一個步驟中沒有找到則在using匯入過的名稱空間中查詢
- 若上一個步驟中沒有找到則在該名稱空間的父空間進行查詢
- 若父空間中依然沒找到則繼續在父空間的父空間查詢,直到查詢到全域性名稱空間:
這不難理解,因為通常內部的作用域的成員會覆蓋外部的作用域的同名成員。以具體程式碼為例:
class Foo { } // 位於全域性名稱空間的Foo
namespace Alpha
{
class Foo { } // 位於名稱空間Alpha的Foo
namespace Beta
{
class Foo { } // 位於名稱空間Beta的Foo
class Program // 位於名稱空間Beta的Program
{
static void Main(string[] args)
{
Foo foo = new Foo(); // 此時的Foo是Beta.Foo
}
}
}
}
上述程式碼中Main方法中的Foo是Beta.Foo,原因是當編譯器以Main方法所屬的Program類所屬的名稱空間Beta為起點查詢型別Foo時,在Beta下就查詢到了Foo的定義,於是停止繼續向上查詢,也就是說,不會繼續向上查詢到Alpha或全域性名稱空間中的Foo。此時如果要使用全域性名稱空間中的Foo,則需要告知編譯器應當直接從全域性名稱空間開始查詢(而非當前名稱空間),可在通過在型別名前新增global::
達到此目的:
global::Foo foo = new global::Foo(); // 告訴編譯器從全域性名稱空間開始查詢,此時Foo就是位於全域性名稱空間的那個Foo
關於global關鍵字其實已在前文提過,這裡只是提一下它的一些實際作用。
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。原因在前文提到過,編譯器查詢型別時會從本名稱空間為起點向上查詢,此時編譯器在名稱空間Test下就發現了Foo的定義,故不會繼續查詢到Alpha名稱空間。此時如果要使用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方法時,可以像程式碼在Speaker靜態類一樣使用Say的方法,某種意義上說,可以認為是將靜態類類名視為了一個名稱空間。另外,如果類中定義了和靜態類中重名的方法,則優先使用類中定義的方法,此時若要使用靜態類中的方法依然需要使用類名.方法名來呼叫:
using static Hello.Speaker; // using static + 靜態類的完整型別名
namespace Test
{
class Program
{
static void Main(string[] args)
{
Say(); // 呼叫的是Program類中的Say
Speaker.Say(); // 此時只能按類名.方法名呼叫
}
static void Say() { ... } // 和靜態類Speaker的Say方法重名
}
}
3.3.2 作用於檔案範圍的名稱空間
普通的名稱空間宣告作用範圍是其後面的程式碼塊,但你也可以宣告作用於整個檔案範圍的名稱空間,宣告後所有在該檔案下定義的型別都將納入此名稱空間:
namespace Alpha; // 將Alpha宣告為檔案作用域的名稱空間
class Cat { }
class Dog { }
宣告作用於檔案範圍的名稱空間和作用域程式碼塊的名稱空間語法相似,其最大的優點在於其可以減少格式化程式碼樣式時所需的列縮排量。需要說明的是,宣告作用於檔案範圍的名稱空間有如下限制:
- 只能宣告一次檔案範圍的名稱空間,這是顯而易見的
- 不能再宣告普通名稱空間,也就是說,下述程式碼無效,Beta也不會視為Alpha的子空間:
namespace Alpha;
namespace Beta
{
}
4. 名稱空間雜談
4.1 型別查詢流程
查詢型別時,編譯器會按照下述流程查詢型別:
4.2 名稱空間的“命名”
MSDN上給出了名稱空間的命名建議:
<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]
- 在名稱空間名稱前加上公司名稱或個人名稱。
- 在名稱空間名稱的第二層使用穩定的、與版本無關的產品名稱。
- 使用帕斯卡命名法,並使用句點分隔名稱空間元件。不過,如果品牌名有自身的大小寫規則,則遵循使用品牌名。
- 在適當的情況下使用複數名稱空間名稱,首字母縮寫單詞例外。
- 名稱空間不應該與其作用域內的型別名相同(例如不應該在名稱空間Foo下定義Foo型別)。
示例:
namespace Microsoft.Office.PowerPoint { }
namespace Newtonsoft.Json.Linq { }
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 隱式全域性名稱空間匯入
現在新建C#專案後,你會發現專案的csproj檔案裡有這樣一行配置:
<ImplicitUsings>enable</ImplicitUsings>
當專案開啟ImplicitUsings時,其作用相當於為你的專案引入了一個對常用名稱空間進行全域性匯入的檔案,也就是說相當於在你的專案中加入了有類似如下內容的檔案:
global using System;
global using System.Collections.Generic;
...
這一功能是對全域性using指令的實際應用,參照於此,你也可以定義一個全域性匯入自己常用的名稱空間的檔案,並按需要新增到自己的專案中。
4.5 誤區
雖然本文最開始的例子中,我們為C語言假想的using語句的作用是‘視接下來所有的函式都有某一字首’,C#中的名稱空間的表現似乎也確實如此。然而,僅僅是這麼認為的話會讓人誤認為下面的程式碼可以通過編譯:
檔案1內容:
namespace Alpha.Beta
{
class Foo { }
}
檔案2內容:
using Alpha;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Beta.Foo foo = new Beta.Foo(); // 看起來Beta.Foo在新增using匯入的Alpha後,就是Alpha.Beta.Foo了
}
}
}
在上述檔案2中,使用using宣告匯入了名稱空間Alpha,然後在Main方法中嘗試使用Beta.Foo來表示Alpha.Beta.Foo型別。這樣看上去似乎沒什麼問題,然而這是無法通過編譯的,不應該認為編譯器會將using匯入的名稱空間和型別名中的名稱空間部分進行組合(如認為Alpha和Beta.Foo的Beta會組合成Alpha.Beta),因為這可能引起歧義,考慮下面程式碼:
檔案1內容:
namespace Alpha.Beta
{
class Foo { }
}
namespace Beta
{
class Foo { }
}
檔案2內容:
using Alpha;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Beta.Foo foo = new Beta.Foo(); // 此時的foo是Beta.Foo還是Alpha.Beta.Foo
}
}
}
上述程式碼中的Beta.Foo到底是作為Beta.Foo的完整型別名,還是Alpha.Beta.Foo的部分型別名?顯然這是不明確的,為了避免這一令人迷惑的情況,編譯器不會對名稱空間進行自動組合。可以認為,如果要使用using匯入的名稱空間中的型別,就只能使用不帶任何名稱空間要素的型別名。
4.6 使用建議
(1)一個檔案中應只宣告一個名稱空間
(2)儘可能避免用巢狀宣告名稱空間,而是使用句點.表示名稱空間的巢狀關係:
namespace Alpha // 巢狀宣告名稱空間
{
namespace Beta
{
}
}
namespace Alpha.Beta // 使用.來表示名稱空間巢狀關係
{
}
(3)靈活使用Using別名來避免不必要的型別定義與簡化型別名
using IntList = System.Collections.Generic.List<int>; // 表示一個Int列表,但沒有額外的型別定義,同時簡化了型別名
(4)規範匯入名稱空間的順序,例如可以按照名稱空間的名稱匯入,或者按照先內建庫→第三方庫→當前專案的順序匯入等等