0. 文章目的 面向C#新學者,介紹命名空間(namespace)的概念以及C#中的命名空間的相關內容。 1. 閱讀基礎 理解C與C#語言的基礎語法。 理解作用域概念。 2. 名稱衝突與命名空間 2.1 一個生活例子 假設貓貓頭在北京有一個叫AAA的朋友,在上海有兩個叫AAA的朋友,上海的兩個AAA ...
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)規範導入命名空間的順序,例如可以按照命名空間的名稱導入,或者按照先內置庫→第三方庫→當前項目的順序導入等等