第 79 講:C# 2 之外部別名
本文的內(nèi)容比較特殊。它基本上在你平時使用代碼里完全用不到;相反,它更多的用途是幫助和輔助編譯器執(zhí)行和生成代碼和分析代碼。如果你想要了解這一塊的內(nèi)容,你可以試著參考本文來理解它們;如果你不想了解的話,本文的內(nèi)容是可以完全忽略,也基本上沒有問題。
Part 1 命名空間的根:global
關鍵字
要說這個詞的起源,我們就得說說,命名空間的取消命名空間二義性的問題。
命名空間是為了規(guī)劃整個項目里不同代碼的“邏輯文件夾”名稱,它不需要綁定真正的文件夾就可以完成管理和分類存儲不同功能的、不同類型的 API。但是,命名空間是我們可以隨意取名的。而我們知道,System
以及派生下來的命名空間表示系統(tǒng)自帶的 API,也就是 .NET 原生提供和支持的 API。
可是,我們也有可能在自己的項目里創(chuàng)建和帶有這樣的 API,然后用上這個 System
作為命名空間的一部分。那么,存在兩種情況:
System
出現(xiàn)在你定義的命名空間的最開頭;System
出現(xiàn)在你定義的命名空間的中間,作為子命名空間名稱出現(xiàn)。
第一種非常麻煩,因此我們稍后說明;而第二種說起來要略微簡單一些,因此我們先說第二種情況。
這個第二個情況,指的是你定義命名空間的時候,用上了諸如
這樣的定義。是的,System
放在了 Sunnie.Game.System
的非最開頭的其余地方,作為子命名空間名稱出現(xiàn)。在這種情況下,你完全無法使用任何 System
命名空間下的 .NET 自帶 API(即庫 API)。
那么,你非得用的話,C# 1 是做不到的。在 C# 2 里,為了解決這一點,C# 新創(chuàng)建了一個關鍵字 global
,用在命名空間的最開頭,表示你使用的是,緊挨著 global
關鍵字后的這個 System
,是整個你表達的命名空間的開頭。啥意思呢?比如這樣的代碼:
global
關鍵字的用法和語法。global
關鍵字后緊跟的是兩個冒號 ::
。::
稍微放一邊,我來說一下這是什么意思。在命名空間定義的時候,我們是寫的 Sunnie.Game.System
,而此時的 System
是在非最開頭的地方;而我們要想引用一個系統(tǒng)自帶的庫 API,我們必然知道,System
是作開頭的。那么,global
緊挨著的這個寫法 global::System
就基本上清楚了:它表示 System
是命名空間的開頭的類型引用。那么,我們這里 global::System.Console.WriteLine
的整個這一部分內(nèi)容下,global::
保證的是 System
引用的是最開始就得是 System
的命名空間,而我們自己定義的命名空間名稱里,是把 System
放在非最開頭的地方的,因此從這個角度來說,我們完美區(qū)分和辨別開了兩個命名空間的引用。所以,如果你在定義命名空間的時候在非最開頭的地方使用了 System
單詞,那么你只需要在你的代碼里使用 global::
前綴來補充說明你引用的 API 和類型,就可以說明和區(qū)分開你此時用的是庫 API。
可能你會問我,為什么會有這樣的機制?那我就來說說這一點。還記得我前文的內(nèi)容講述的內(nèi)容嗎?如果你使用
using
指令來引用了System
命名空間的話,假設你沒有引用System.Collections
命名空間,也可以省略掉同樣的前半截:System.
。正是因為這樣的原因,命名空間在代碼使用層面來說,是可以省略和書寫簡略寫法的。因此,出現(xiàn)在代碼里的類型的使用一共有三種書寫格式:
只寫類型名:這種是明確了類型的具體命名空間的時候;
類型名前面帶有不全的命名空間部分:這種是引用了一部分,但沒有全部完整引用命名空間下來的時候;
類型名前面帶有完整的命名空間:這種屬于完全無法確定類型的存儲位置。
從這個角度來說,因為語法的靈活性,我們只看
System
是無從確定它從哪里開始和派生,它可以是你引用了using Sunnie.Game;
后的“產(chǎn)物”,也可以是System
系統(tǒng) API 的命名空間。所以我們才會有global
來區(qū)分這樣的情況。
說完了這里,我們來說一下 ::
符號。::
是一個全新的運算符。說實話,這個運算符早就有了,C# 的原生語法里就包含這個運算符,但因為用得很少,因此我們整個教程都沒有對這個內(nèi)容作任何的提及,或者說提及相當少。::
叫命名空間別名限定符(Namespace Alias Qualifier),專門用在命名空間的別名上。別名?是的,就是前文我們說過的命名空間別名的內(nèi)容:
collection
是類型別名,因此我們除了可以用 .
以外,仍然可以使用 ::
這是因為我們這里的 ::
是命名空間別名上才會用到的、將別名和正常的子命名空間名稱(或是類型名稱)連接起來的一個符號。不過,.
和 ::
的區(qū)別是,.
是常見的,所以 C# 絕大多數(shù)時候都用 .
來連接,包括這里的命名空間別名的時候。因為命名空間別名定義下來后,它總歸也是命名空間名稱,因此我們?nèi)匀豢梢杂?C# 原生的語法規(guī)則(即使用 .
連接命名空間和類型)來完成這個工作。只是,C# 里的正常規(guī)則是,當使用命名空間別名的時候,命名空間別名要和子命名空間名稱(或者是正常類型名稱)連起來,需要用 ::
。是的,區(qū)別在于,用的場景不一樣而已。
回到前文。global
關鍵字后,為什么用的是 ::
呢?這就得說一下,global
表示啥了。C# 2 里發(fā)明了這種機制,讓 global
充當和表示“命名空間的根”。換句話說,任何一個開始,都是從 global
這個邏輯的命名空間的“根”所派生下來的情況,包括我們自定義的命名空間名稱,以及庫 API 的命名空間名稱。global
實際上起到的作用是辨別和區(qū)分是不是從最開始引用的命名空間名。比如前面的 collection.ArrayList
,這個 collection
就可能是開頭,也可能不是開頭。
如果一個數(shù)據(jù)類型沒有任何的命名空間的定義,那么它默認就是在 global
為別名的命名空間下的。
比如這樣的代碼。我們的實例化語句里使用的是 global::Z
這樣的引用格式來表示 Z
的完整命名空間。當然,這種情況在 C# 原生語法里是省略 global
不寫的情況,即直接寫 Z z = new Z()
的,只不過 C# 2 帶來了 global
這個機制后,這里的 Z
我們就可以通過這樣 global
是根命名空間的概念了,那么寫成 global::Z
也就不是錯誤的語法了。
那么,問題來了。global
我們?nèi)匀唤凶龅氖敲臻g別名,而命名空間別名可以用 .
也可以用 ::
來限定使用 API 的所在命名空間的??蓡栴}是,我寫 global.Z
而不是 global::Z
的時候,編譯器居然跟我說這么寫不對:

它說,無法找到類型或命名空間名稱為 global
的情況,并問你是不是少了什么 using
指令。奇了怪了。global
是關鍵字,哪里來的命名空間的引用?難不成你還寫 using global;
?笑話。

事實的真相是,只能用 ::
而不能用 .
。這是為什么呢?下面我們就要來說說,較為復雜的第一種情況了。
Part 2 程序集別名:extern alias
指令
如果我自己定義命名空間的時候,把 System
放在我自定義的命名空間的最開頭,像是這樣:
對,沒錯。這個假設是我自己寫代碼的時候?qū)懮先サ?,而不?.NET 的 API。為了解釋和表達我們后面的語法機制,我先來拋出一個問題,并在稍后解釋。
我們知道,System
里有 Console
類型,用來控制和封裝一個控制臺的相關內(nèi)容。而我在不小心或者不知道的情況下,自己在寫代碼的時候,自己又定義了一個 Console
類型,而且你還不小心連命名空間都寫的是一樣的:
using System;
來引用 Console
,還是使用 System.Console
,都無法區(qū)分和辨別到底我想用的是自己寫的 Console
類型還是庫里自帶的 Console

這個時候,我們需要一種特殊的機制來完成這個任務:外部別名(External Alias)。
假設我們單獨使用一個項目存儲 Console
這樣的、全部以 System
作為命名空間的類型。我們試著使用引用機制,將項目和項目引用關聯(lián)起來。

假設它位于一個單獨的項目里,假設項目是這個:

Console

我們選擇“添加項目引用”。

找到項目,并點擊 OK。
依次展開解決方案資源管理器的當前項目,就可以發(fā)現(xiàn),項目已經(jīng)導入進來了。

Temp
項目就是我們用上了 System

接著,看到“別名”這一項。

在這里填一個你喜歡的標識符。這個標識符就是整個程序集的別名了。這個程序集別名就意味著你里面所使用到的任何一個數(shù)據(jù)類型,它的根的命名空間名稱都是走這個別名定義的這個名字所派生下來的。比如我這里填入了一個 another
。
然后,返回到你要使用這個類型的文件。在文件的最開頭(using
指令的上面),加上 extern alias
指令,語法是這樣的:
another
名稱,然后它指向的就是我們這里自己定義的 Console
類型了;而我們還是仍然想要用 .NET 自帶的 Console
類型的話,直接書寫 Console.WriteLine
extern alias
指令,我們就可以完美區(qū)分開不同的 System

那么問題來了。我整個操作,都是基于單獨的一個 Temp
項目。這是為什么呢?為啥我講一個特性還單獨創(chuàng)建一個新的項目來解釋?這是因為,項目和項目關聯(lián)用的是 dll 后綴的文件。而系統(tǒng)自帶的 API 也是 dll 文件。也就是說,你要想使用各種各樣的 API,自然就是引用各處下載得到的這些 dll 文件,然后才可以使用起來的。而為了區(qū)分開 System
好了,本文就說到這里。外部別名就是一種取消引用二義性的機制,它只在極端情況下會存在;而解決辦法就是通過在引用 dll 文件和項目的過程之中,改變別名的方式,來完成區(qū)分不同的文件來源,就可以在代碼里區(qū)分開來了。
欸,這文章還沒完啊。好像還沒解釋 another
這個別名后面為啥是雙冒號 ::
而不是小數(shù)點 .
。我一句話來說明清楚原因:這種別名是特殊的,這就是我前文提到的“絕大多數(shù)情況”的對立情況。唯有這一種情況下,我們必須使用雙冒號 ::
來限制和引用命名空間,而 .
在這里是行不通的。那么,為什么呢?你可以仔細對比 another
這里的用法,以及前面說到的 global
關鍵字的用法。對比起來就可以發(fā)現(xiàn),其實它們的機制是完全相同的——你完全可以把前面提到的 global
想象成默認的命名空間的根;而如果你創(chuàng)建了自己的 dll 文件或項目的引用的話,你可以為 dll 文件引用或項目引用單獨取一個自己定義的名字。這樣就可以避開使用 global
,而是使用別的名字,來引用不同的類型。從另外一個方面來說,不論是 global
也好,還是 another
也好,它們都是整個 dll 文件或項目引用的命名空間的根,而且它還是以別名出現(xiàn)。因此,這里我們需要用,也只能用 ::
來限定。只有這一種情況下,::
不能換成 .
。