Self

Self 語言，是一種基於原型的面向物件的程式設計語言，也是一個整合開發環境和執行環境，由 David Ungar 和 Randy Smith，最初在 1986 年於施樂帕羅奧多研究中心設計。Self 語言在 Smalltalk 的基礎上，採用 “槽” 取代了 “變數”，從而徹底體現了一切都是物件的風格。在實現 Self 系統的過程中，設計研究人員發展出了一種動態自適應編譯技術。

簡介

Self 語言把在概念上精簡 Smalltalk 作為設計原則。它在把訊息作為最基本的操作的同時，取消了類的概念，只有物件的概念。它把物件的特性，理解為獲取或更改特性的這兩種方法，從而把特性的概念簡化為方法，並且透過訊息來讀槽和寫槽的方式，取代了變數及其賦值。Self 提出了特質的概念，用實現了委託。

儘管 Self 系統一次執行在一個程序中，但實際上可以分成兩個部分：Self 虛擬機器和 Self 世界。Self 世界是一個 Self 物件庫，Self 物件包括資料物件和方法物件，方法物件的程式碼部份，是用一種指令非常簡單的位元組碼錶示的，位元組碼由 Self 虛擬機器來解釋。當 Self 程式從終端、檔案或者圖形使用者介面輸入到系統之中時，Self 系統把源程式解析轉化為 Self 世界裡的物件。

動態自適應編譯技術的採用，提高了 Self 程式碼的執行效率。對經常執行的方法，虛擬機器將進一步把位元組碼轉化為本機程式碼。Self 虛擬機器還提供了一些可供呼叫的，用來實現算術運算、物件複製、輸入輸出等。

Self 還擁有一個圖形使用者介面 Morphic，Self 的程式設計環境，也是基於 Morphic 來實現的。Self 在精簡語言概念的同時，也把大量的工作轉交給環境來處理，語言中的反射機制也同環境密切相關。

歷史

在 1986 年，David Ungar 和 Randy Smith 在施樂帕羅奧多研究中心，提出了 Self 語言的最初設計，並在 1987 年的 OOPSLA'87 的論文《Self：簡單性的能力》中給出了描述，此文在 2006 年被評為 1986 年到 1996 年間三個最有影響的 OOPSLA 論文之一。

1987 年初，Craig Chambers、Elgin Lee 和 Martin Rinard，在 Smalltalk 上給出了 Self 的第一個實驗性直譯器。1987 年夏，Self 專案在斯坦福大學正式開始，1988 年夏給出了第一個有效率的實現，併發布了 1.0 和 1.1 兩個版本。1991 年初，Self 專案移至 Sun 微系統，並且在 1992 年釋出了 2.0 版。1993 年 1 月，Self 3.0 版釋出。

1995 年 7 月，Self 4.0 版釋出。在這個版本中包括了一個全新的圖形使用者環境 Morphic。在 2016 年發行了 4.3 版本並可執行在 Mac OS X 和 Solaris 上。在 2010 年發行了版本 4.4，由最初團隊的某些人和獨立程式設計者形成的小組開發，它和所有後續版本可以執行在 Mac OS X 和 Linux 上。2014 年 1 月發行了 4.5 版本。2017 年 5 月發行了版本 2017.1。

Self 的發展基本已經停滯，但在發展 Self 過程中探索出的一些技術，在其他的系統中得到了應用。在 Self 的實現中採用的各種編譯最佳化技術，直接導致了 Java Hotspot 虛擬機器的產生；在 Smalltalk 的一個實現 Squeak 中，採用了 Self 圖形使用者介面 Morphic 的設計方案，放棄了 Smalltalk-80 中採用的 MVC 的方案。Self 是對 JavaScript 程式語言設計有最主要影響者之一。

基於原型程式設計

傳統的基於類的面嚮物件語言，基於了根深蒂固的二元性：

類，定義物件的基本品質（quality）和行為（Behavior）。
物件例項，是類的特定（manifestation）。

例如，假設車輛類 Vehicle 的物件有一個 “名字”，和進行各種動作的能力，比如 “開車上班” 和 “運送建材”。Bob's car 是類 Vehicle 的特定物件（例項），它的 “名字” 是 “Bob's car”。在理論上，你可以向 Bob's car 傳送訊息，告訴它去 “運送建材”。

這個例子展示了這種方式的一個問題：Bob 的汽車，恰巧是一個跑車，在任何意義上都不能裝載和運送建材，但這是建模 Vehicle 所必須擁有的能力。透過從 Vehicle 建立特殊化的子類，可產生一個更有用的模型；比如建立跑車類 SportsCar 和平板卡車類 FlatbedTruck。只有 FlatbedTruck 的例項需要提供 “運送建材” 的機能；SportsCar 的例項不適合這種工作，它只需要 “快速行駛”。但是，這種深入建模在設計期間，需要更多的洞察力，洞察那些可能只在引起了問題時才顯現出的事情。

這個問題是在原型（prototype）這個概念背後的動機因素之一。除非你能必然性的預測出一組物件和類，在遙遠未來時所要有的品質，你不能恰當的設計好一個類的層級。程式最終需要增加行為，實在是太頻繁了，而系統的很多節段將需要重新設計或重新構建，來以不同的方式迸發出物件。早期的面嚮物件語言如 Smalltalk 的實驗，顯示出這種問題反反覆覆的出現。系統趨向於增長到一定程度後，就變得非常僵化，因為在程式設計者的程式碼下的深層的基本類，簡直就像是逐漸變成了一個 “錯誤”；沒有變更原來的類的容易方式，就會出現嚴重的問題。

動態語言如 Smalltalk，允許透過周知的按照類的方法進行這種變更；即透過改變類，基於它的物件就可以改變它們的行為。但是，進行這種變更必須非常小心，因為基於相同類的其他物件，可能把它當作 “錯誤行為”：“錯誤” 經常是依賴於場景的，這是問題的一種形式。進一步的說，在靜態語言如 C++ 中，這裡的子類可以從超類分別的編譯，對超類的變更實際上可以破壞預編譯的子類方法；這是脆弱基類問題的另一種形式，也是的一種形式。

在 Self 和其他基於原型的程式語言中，消除了在類和物件之間的這種二元性。不再有基於某種 “類” 的一個物件 “例項”，在 Self 中，你可以複製一個現存的物件，並改變它。故而 Bob's car 可以透過製作現存的 Vehicle 物件的複本來建立，並增加 “快速行駛” 方法，建模它恰好是一輛保時捷 911 的事實。

主要用來製作複本的基本物件叫做 “原型”。這種技術被稱為是一種非常簡化的機制。如果一個現存的物件或物件的集合，被證明是個不適當的模型，程式設計者可以簡單的建立有正確行為的一個修改的物件，並轉而使用它。使用現存物件的程式碼不會改變。

語法和語義

下面簡要描述 Self 語言的語法和語義。

4.1 物件

（literal）包括：數、用 ' 包圍起來的字串物件，塊和一般的物件。物件文字用圓括號來界定。在圓括號內，物件描述構成自豎槓 | 界定的一個槽列表，隨後是在這個物件被求值時要執行的程式碼。例如：

(| 槽 1. 槽 2 | 一些程式碼)

槽（slot）是名字 - 值對，槽包含到其他物件的引用。槽列表由點號分隔的（可以為空的）一序列的槽描述符組成。在槽列表結束處的點號是可選的。槽描述符（descriptor）有兩種：

槽 ，指示將指名的資料槽初始化為求值表示式的結果，它有相同名字附加冒號的包含賦值原語的賦值槽，這兩個槽對應於其他語言中的一個讀寫變數。
槽 = 表示式，指示將指名的資料槽初始化為求值表示式的結果，這個槽對應其他語言的一個只讀變數。

在 Self 中沒有單獨的賦值運算。其他面嚮物件語言中的訪問子方法對應資料槽，變異子方法對應賦值槽。假如 myPerson 物件中有個叫做 name 的資料槽，則透過，可返回在這個槽中的值；如果它有對應的賦值槽 name:，則透過，可設定資料槽 name 的值為 'foo'。

任何槽都可以透過增加星號字尾，來製成父槽。星號不是槽名字的一部份，在將名字與訊息進行匹配時候忽略它。例如一個初始化了的可變的點可以定義為：

(| parent* = traits point.

x

一個物件的程式碼是以點號分隔的一序列的表示式。尾隨的點號是可選的。每個表示式有一系列的訊息傳送和文字組成。在一個物件的程式碼中最後的表示式，可以前導著指示返回的 ^ 算符。

一個真正的空物件，指示為 (| |) 或簡單的 ，它根本不接收任何訊息。

4.2 訊息

透過訊息訪問槽的語法，類似於 Smalltalk，有三類訊息可以獲得：

一元：接收者 槽名字

二元：接收者 算符 引數

關鍵字：接收者 關鍵字 1: 引數 1 關鍵字 2: 引數 2

所有訊息都返回結果，所以顯式指定的接收者和引數自身可以是其他訊息的結果。下面是 Self 版本的 hello world 程式例子：

'Hello, World!' print.

組合可以透過使用圓括號來進行強制。在缺乏明確組合的情況下，一元訊息具有最高優先順序，其次是二元訊息，而關鍵字訊息最低。一元訊息從左至右複合。二元訊息對於同一個算符從左至右結合，例如 3 + 4 + 7 被解釋為 (3 + 4) + 7，而對於不同的算符沒有結合性，例如 3 + 4 7 是非法的，而必須顯式的寫為要麼 (3 + 4) 7 要麼 3 + (4 * 7)。

關鍵字訊息的第一部份必須開始於小寫字母，而後續部份都必須開始於大寫字母。例如表示式：

5 min: 4 Max: 7

是一個單一的訊息 min:Max:，它被髮送給 5 並具有引數 4 和 7，而表示式：

5 min: 4 max: 7

涉及兩個訊息：第一個訊息 max: 被髮送給 4 並接受 7 作為它的引數，而接下來訊息 min: 被髮送給 5，並接受 4 max: 7 的結果作為它的引數。關鍵字訊息從右至左結合，例如：

5 min: 6 min: 7 Max: 8 Max: 9 min: 10 Max: 11

被解釋為：

5 min: (6 min: 7 Max: 8 Max: (9 min: 10 Max: 11))

由於很多訊息被髮送給當前訊息接收者 self，故而可以將 self 作為隱含接收者而不需要顯式的寫出。

4.3 方法

方法（method）是除了引數槽區域性槽之外，還包含程式碼的物件。引數（argument）槽名字開始於一個冒號，它不是槽名字的一部分，在將名字與訊息進行匹配時候忽略它。引數槽總是隻讀的，並且不能對它們指定初始化者。下面例子是計算平方的方法物件：

(| :arg | arg * arg)

一個普通方法（簡稱方法），是不嵌入到其他程式碼之中的方法，它只能存放在只讀槽中。普通方法總是有一個叫做 self 的隱含的父引數槽。Self 的普通方法等價於 Smalltalk 的方法。

如果一個槽包含一個方法，在求值這個槽來響應發來的訊息的時候，這個方法物件被（clone），從而新建它的一個活動（activation）物件，它包含這個方法的引數槽和區域性槽；複製體的 self 父槽，初始化為這個訊息的接收者；複製體如果有引數槽，將它們初始化為實際引數；在這個新的活動物件的上下文中，執行這個方法的程式碼。例如計算點的加法的一個方法：

(| + arg =

((clone x: x + arg x) y: y + arg y )

)

可以被無歧義的分析，其含義同於：

(| + =

(| :arg | (clone x: ((x + (arg x)))) y: ((y + (arg y))) ).

)

這裡出現了三個隱含接收者一元訊息 clone、x 和 y。

作為語法約定，引數名字可以直接寫在槽名字中對應關鍵字之後，它不再帶有字首冒號，從而隱含的宣告引數槽。例如下面的方法定義：

(| ifTrue: False: =

(| :b1. :b2 | b1 value).

)

可以等價的定義為：

(| ifTrue: b1 False: b2 =

(b1 value).

)

返回算符 ^ 的出現或缺席，不影響普通方法的行為，因為普通方法總是會返回它最終的表示式的值。

4.4 塊

塊是 Self 的閉包，Self 就像 Smalltalk，使用 “塊” 用於控制流程和其他職責。塊文字的寫法，除了方括號替代了圓括號之外，類似於其他物件文字。例如嵌入在下列表達式中的塊：

1 to: 5 i By: 2 j Do: k print

一個塊文字定義兩個物件：一個塊資料物件，和它包圍的一個塊方法物件。

塊資料物件，它有包含塊方法物件的一個槽，其選擇子也就是槽名字，由引數數目決定，沒有引數是 value，一個引數是 value:，兩個引數是 value:With:，隨著引數增多 With: 個數也隨之遞增。此外，它還有一個叫 parent * 的父槽，指向含有塊物件都共享的行為的那個物件（traits block）。
塊方法物件，它含有這個塊的程式碼。不同於普通方法物件，它不包含 self 槽，轉而有一個匿名父槽，它被初始化指向在詞法上處於外圍的塊或方法的活動物件。匿名的含義，是這個槽的名字在 Self 層面是不可見的，而不能顯式的訪問。作為結果，在一個塊方法內傳送的隱含接收者訊息，被限定在這個塊所在表示式的詞法作用域之內，而非這個塊經過可能有的多次轉送，最終向它傳送適當的 value 訊息變體之時的那個作用域。

相應的，塊求值分為如下兩個階段：

在這個塊被求值的時候，即它被用作傳送訊息的引數，比如例子中 to:By:Do: 訊息的引數之時，建立塊物件（即塊資料物件）；這個塊被淺層複製（clone），並將指向這個塊在詞法上外圍的活動記錄，即當前的活動記錄的一個指標交給它匿名儲存。
在向這個塊傳送適當的 value 訊息變體的時候，求值這個塊方法；這個塊方法接著被淺層複製，並填充此複製體的方法槽，使用第一階段確定的指標來初始化匿名父槽，最後執行這個塊的程式碼。

在塊中，返回算符 ^ 導致從包含這個塊的普通方法中返回控制權，立即終止這個方法的活動，這個塊的活動，和在其間的所有活動。這種返回叫做 “非區域性返回”，因為它可以穿越很多活動。普通方法求值的結果，是非區域性返回所返回的值。

4.5 委託

在理論上，所有 Self 物件都是獨立實體，Self 既沒有類也沒有元類。對任何特定物件的變更，都不影響任何其他物件，但是在某些情況下，卻需要它們有關聯。正常的一個物件，只能理解對應於它的區域性槽的訊息，但擁有一個或更多的指示父（parent）物件的槽，物件可以將任何自身不理解的訊息，委託（delegate）給父物件。

Self 採用這種方式，處理在基於類的語言中使用繼承來擔負的責任。委託還可以用來實現一些特徵，比如名稱空間和詞法作用域。透過下面的例子展示委託與傳統的類的不同之處：

myObject parent: someOtherObject.

這個句子透過改變與叫做 parent 的父槽關聯的值，在執行時間改變 myObject 的 “類”。不同於繼承或詞法作用域，委託物件可以在執行時間修改。

4.6 特質

例如，假定在一個簡單的賬簿應用中，定義了一個物件叫做 “銀行帳號”（bank account）。通常建立的這個物件，具有內部的方法，比如說 “存款”（deposit）和 “取款”（withdraw），和任何它所需要的資料槽，比如說 “餘額”（balance）。這只是一個原型，它只在使用方式上特殊，因為它恰好是一個全功能的銀行帳號。

為 “Bob 的賬戶” 製作銀行帳號物件的複製品（clone），將建立一個新物件，它在起初時完全同於原型。在這種情況下，將複製（copy）包括方法和任何資料的槽。但更常用的解決方案，是首先建立叫做它的特質（trait）物件的一個簡單物件，它包含通常與一個類有關的專案。

在這個例子中，“銀行賬戶” 將沒有存款和取款方法，而是委託給一個父物件來做這些。採用這種方式，可以製作銀行帳號物件的很多複本，但是我們仍可以透過改變它所委託的特質物件中的槽，來改變它們全體的行為。

4.7 Self 世界

當處在於提示符下鍵入表示式的場景時，由叫做 “大廳”（lobby）的一個物件，引領使用者進入 Self 世界。當建立一個新物件的指令碼被讀入系統的時候，指令碼中的表示式都在大廳的上下文中求值。就是說大廳是這個指令碼中所有傳送給 self 的訊息的接收者。

要引用在指令碼中的某個現存的物件，必須透過傳送一個訊息到大廳才可以訪問到它。大廳的 traits、globals 和 mixins 槽，是從大廳可以訪問的物件名稱空間的根。大廳的 lobby 槽允許大廳自身透過名字來提及。路徑名字是一個一元選擇子的序列，它描述從大廳到這個物件的路徑。路徑名字也是可以在大廳的上下文中求值的表示式，它產出這個物件。

例如，原型列表的完全路徑名字是 globals list。因為 globals 是父槽，它可以從路徑名字表達式中省略，生成簡短路徑名字 list。大廳的 traits 不是父槽，特質物件的名字必須開始於字首 traits，因此列表的特質物件必須稱呼為 traits list。

不是所有物件都有路徑名字，只有那些從大廳可以到達的物件才有，這些物件稱為 “周知的”。大廳向用戶提供三類物件：

特質（traits）物件，封裝共享行為的物件，典型的每個原型物件，都有一個關聯的同名特質物件，用來描述它的行為的共享部份。任何 Self 實現都需要為整數、浮點數、字串和塊提供特質物件。Sefl 世界中多數具體物件派生自兩種個頂層特質物件：不唯一性物件承襲自 traits clonable，而唯一性物件承襲自 traits oddball。唯一性的物件透過返回自身，來響應訊息 copy，並使用同一性來測試相等。
全域性（globals）物件，即不唯一性的原型物件，和 “每種只有一個” 的獨特（oddball）物件。一些物件比如 true、false 和 nil 是唯一性的，在系統中它們只需要有一個。因為一個 oddball 不需要在它的很多例項間共享它的行為，它不需要有分立的特質物件和原型物件。很多 oddball 物件從 traits oddball 繼承 copy 方法，它返回物件自身而非一個新複本。
混入（mixins）物件，即小而無父物件的行為束（bundle），是持有共享行為的一種物件，通常用來混入那些有其他父物件的物件之中。混入物件的一個例子是 mixins identity。兩個物件測試相等，通常基於在一個共同的域（domain）內是否有相同的值。例如，在數的域內 3.0 = 3，即使它們不是相同的物件甚至不是同種類的物件。但是在一些域中，兩個物件相等當且僅當它們是相同的物件，例如兩個程序即使有相同的狀態也不被當作是相等除非它們是同一個。在這種情況上，使用同一性比較來實現相等測試，並混入 mixins identity 來得到想要的行為。

在大廳的 defaultBehavior 槽中，定義了系統中大多數物件所繼承的預設行為。

4.8 新建物件

有兩個訊息與物件複製有關：

，，返回包含著與最初物件完全相同的槽和程式碼的一個新物件。它用在物件內部，客戶應當使用 copy。
，複製接收者，可能具有嵌入的複製或初始化。

考慮一個圖形使用者介面有關的例子：

(desktop activeWindow) draw: (labelWidget copy label: 'Hello, World!').

首先進行的是，它向桌面物件 desktop 傳送訊息 activeWindow，從其擁有的一個視窗列表中返回活動視窗。按從內向外從左至右的次序，接著是，透過 copy 訊息製作標籤元件物件 labelWidget 的一個複本，接著向它傳送一個訊息，將 Hello, World! 放入它的用作 “標籤” 的 label 槽中。最後將返回的這個元件，傳送到這個活動視窗用於 “繪製” 的 draw 槽中。

4.9 增加槽

在 Self 中的物件，可以透過包括新加的槽來修改。這可以透過被推薦使用的圖形程式設計環境來做，或者直接使用原語AddSlots:。原語與正常關鍵字訊息有相同的語法，但是它的名字開始於下劃線字元。給AddSlots: 的引數是一個物件文字，它的槽將被複制進入接收者。例如，在大廳中新增叫做 newObject 的物件，並初始化這裡舉出的它的叫做 entries 的槽，採用如下這樣的表示式：

_AddSlots: (| newObject = (| entries

因為_AddSlots: 原語未指定接收者，這裡的訊息隱含接收者 self 是大廳，由它來理解產生初始值的訊息 list copy，list 是周知的原型物件。

例子

在下面的例子中，將基於類語言中叫做 Vehicle 的一個簡單的車輛類，重新構造為 Self 中的 vehicle 物件，從而能夠區分出在轎車和卡車之間共有的行為：

_AddSlots: (| vehicle

在大廳中建立了一個叫做 vehicle 的槽，它的值是一個物件文字，同時還建立了一個叫做 vehicle: 的賦值槽。作為 vehicle 槽初始值的物件文字，也就是新建的叫做 vehicle 的物件，包括了一個單一的父槽 parent，沒有相應的 parent:，它委託了 traits clonable 物件，這個頂層的特質物件可以理解與複製有關的訊息。

然後向這個新建物件繼續增加 name 槽和相應的 name: 槽：

vehicle _AddSlots: (| name

在大廳中從 vehicle 物件建立一個跑車物件 sportsCar，並接著向 sportsCar 增加 vehicle 所沒有的用於 “開車上班” 的一個新的方法槽 driveToWork：

_AddSlots: (| sportsCar

在大廳中從 sportsCar 物件建立一個保時捷 911 物件 porsche911，接著向新建物件 porsche911 傳送一個訊息改變它的 name 槽的值：

_AddSlots: (| porsche911

物件 porsche911 與它的原型物件 sportsCar，仍有著完全相同的槽，但是其中的一個槽有著不同的值。

環境

Self 的一個特徵，是它基於了早期 Smalltalk 系統所用的某種虛擬機器系統。就是說，程式不是像 C 語言中那樣的獨立實體，而是需要它們的整體記憶體環境來執行。這要求應用程式被裝載入儲存記憶體的（chunk）之中，這叫做 “快照” 或。這種方式的缺點，是映像有時很大並且笨重；但是除錯一個映像，經常被除錯一個傳統程式要簡單，因為執行時狀態更容易檢查和修改。在基於原始碼和基於映像的開發之間的不同，是類似於在面向類的和麵向原型的面向物件程式設計之間的區別。

此外，環境是為了讓在系統之中的物件能快速和可持續的變更而定製的。重新構建一個 “類” 設計，就像從現存的祖先拖動出來方法放入新造的之中一樣容易。簡單任務像測試方法，可以透過製作複本來處理，拖動方法進入這個複本，接著變更它。不同於傳統系統，只有變更了的物件有新程式碼，不需要重建任何東西來測試它。如果這個方法有效，可以簡單的把它拖動回祖先之中。

6.1 效能

Self 的 VM 實現的效能，在某些測試之中大約是最佳化的 C 程式速度的一半。這是透過即時編譯技術達到的，它是在 Self 研究中首創並改進的，能夠使高階語言表現得這麼好。

6.2 垃圾收集

Self 的垃圾收集器使用分代垃圾回收，它按年齡分離物件。透過使用記憶體管理系統記錄頁面寫，可以維護一個寫屏障。這個技術給出了卓越的效能，儘管在執行一些時間之後，出現完全的垃圾收集，要花相當可觀的時間。

6.3 最佳化

執行系統選擇性的扁平化呼叫結構。這給出適當的自身提速，但允許了對不同調用者型別的型別資訊和多版本的程式碼的大量快取。這去除了對做很多方法查詢的需要，並允許條件分支語句和硬編碼呼叫被插入，這經常能給出類似 C 語言的效能，而又不失去語言層面的通用性，但要建立在完全的垃圾收集系統之上。