程式語言中的 6 個有趣特性
Java 是一門不斷髮展的語言,這是一件好事。然而,其他語言的一些特性也是值得研究的。語言的結構是人們思考問題的方式,也是人們設計解決方案的方式。學習或至少熟悉其他語言是借鑑其設計的好方法。
Java 是我學習的第一門語言並且是我專業使用的語言。它是我大約十五年以來的主要謀生手段。然而,它並不是我多年來學習和使用的唯一語言:例如,很久以前,我必須開發JavaScript程式碼來實現動態使用者介面。當時,它被稱為 Dhtml……幾年前,我還自學了 Kotlin,並且從未停止過使用它。去年,在一家新公司工作時,我嘗試了 Clojure,但沒有成功。
在上述所有場景中,Java 仍然是我學習和評判其他語言的基準。以下是一些有趣的語言特性,我認為這些特性對於來自 Java 背景的人都頗具思想挑戰性。
JavaScript:原型
JavaScript 是我和 Java 一起使用的第一種語言。儘管 JavaScript 已經發展這麼多年了,但它有一個實現起來非常奇怪的常見特性:新物件的例項化。
在 Java 中,首先建立要一個類:
publicclassPerson{
privatefinalStringname;
privatefinal LocalDate birthdate;
publicPerson(Stringname, LocalDate birthdate){
this.name = name;
this.birthdate = birthdate;
}
publicStringgetName(){
returnname;
}
publicLocalDategetBirthdate(){
returnbirthdate;
}
}
然後,就可以繼續建立該類的例項了:
varperson1 =newPerson("John Doe", LocalDate.now());
varperson2 =newPerson("Jane Doe", LocalDate.now());
JavaScript 與 Java 的語法非常相似:
classPerson{
constructor(name, birthdate) {
this.name = name;
this.birthdate = birthdate;
}
}
letperson1 =newPerson("John Doe",Date.now());
letperson2 =newPerson("Jane Doe",Date.now());
相似之處到此為止。由於 JavaScript 具有動態特性,所以可以向現有例項中新增屬性和函式。
person1.debug=function(){
console.debug(this);
}
person1.debug();
但是,這些只能新增到某個例項中。其他例項會缺少這些補充屬性或函式:
person2.debug();// Throws TypeError: person2.debug is not a function
要將函式(或屬性)新增到所有例項(無論是現在的還是將來的)中,都需要利用原型的概念:
Person.prototype.debug= function() {
console.debug(this);
}
person1.debug();
person2.debug();
let person3 = new Person("Nicolas", Date.now());
person3.debug();
Kotlin:擴充套件函式 / 屬性
幾年前,我開始嘗試著自學 Android。我發現這種體驗對開發人員來說不太友好:當然,我瞭解它其中一個目標是儘可能減少記憶體佔用,但這是以非常簡潔的 API 為代價的。
我記得當時我必須呼叫帶有很多引數的方法,其中大多數引數為null。在嘗試尋找到一種方法來解決這個問題時,找到了 Kotlin 的擴充套件屬性:帶有預設引數。我後來停止了 Android 的學習,但仍繼續使用 Kotlin。
我喜歡 Kotlin。很多人都稱讚 Kotlin 的 null 安全性(null-safety)實現。但對我來說,我喜歡它,並不是因為它是 null 安全的,而是因為別的。
假設我們經常需要將字串首字母改成大寫。在 Java 中實現這一目的的方法是使用靜態方法建立一個類:
publicclassStringUtils{
publicstatic String capitalize(Stringstring) {
var character =string.substring(0,1).toUpperCase();
var rest =string.substring(1,string.length() -1).toLowerCase();
returncharacter + rest;
}
}
在早期,每個專案幾乎都具有 StringUtils 和 DateUtils 類。幸運的是,現有的庫提供了最常用的功能,例如Apache Commons Lang和Guava。然而,它們仍遵循相同的設計原則,即遵循基於靜態方法的設計原則。這很糟糕,因為 Java 被認為是一種面嚮物件語言。不幸的是,靜態方法不是面向物件的。
在擴充套件函式和屬性的幫助下,Kotlin 允許將行為、狀態分別新增到現有的類中。語法非常簡單,並且與面向物件的方法完全相容:
funString.capitalize(): String {
valcharacter = substring(0,1).toUpperCase()
valrest = substring(1, length -1).toLowerCase()
returncharacter + rest
}
在編寫 Kotlin程式碼時,我經常使用這個。
在底層,Kotlin 編譯器生成與 Java 程式碼類似的位元組碼。這僅僅是語法糖,但是從設計的角度來看,與 Java 程式碼相比,它是一個巨大的改進!
Go:隱式介面實現
在大多數面嚮物件語言(Java、Scala、Kotlin 等)中,類可以實現一個契約(也稱為介面)。這樣,客戶端程式碼可以引用該介面,而無需關心任何特定的實現。
publicinterface Shape {
floatarea();
floatperimeter();
defaultvoiddisplay(){
System.out.println(this);
System.out.println(perimeter());
System.out.println(area());
}
}
publicclassRectangleimplementsShape{
publicfinalfloatwidth;
publicfinalfloatheight;
publicRectangle(floatwidth,floatheight){
this.width=width;
this.height=height;
}
@Override
publicfloatarea(){
returnwidth*height;//(1)
}
@Override
publicfloatperimeter(){
return2*width+2*height;//(1)
}
publicstaticvoidmain(String... args){
varrect=newRectangle(2.0f,3.0f);
rect.display();
}
}
(1)處為了精確起見,應該使用 BigDecimal ,但這不是重點
重點是:由於 Rectangle 實現了 Shape,所以可以在 Rectangle 的任何例項上呼叫在 Shape 上定義的 display() 方法。
Go 不是一種面嚮物件語言:它沒有類的概念。它提供了結構體,並且函式可以與這種結構體相關聯。它還提供了介面,該介面可以使用結構體來實現。
然而,Java 實現介面的方式是顯式的:Rectangle 類宣告它實現了 Shape。相反,Go 的方式是隱式的。實現介面所有函式的結構體隱式地實現了該介面。
這可以轉換為如下程式碼:
packagemain
import(
"fmt"
)
typeshapeinterface{//(1)
area()float32
perimeter()float32
}
typerectanglestruct{//(2)
widthfloat32
heightfloat32
}
func(rect rectangle)area()float32{//(3)
returnrect.width * rect.height
}
func(rect rectangle)perimeter()float32{//(3)
return2* rect.width +2* rect.height
}
funcdisplay(shape shape){//(4)
fmt.Println(shape)
fmt.Println(shape.perimeter())
fmt.Println(shape.area())
}
funcmain(){
rect := rectangle{width:2, height:3}
display(rect)//(5)
}
(1)定義 shape 介面
(2)定義 rectangle 結構體
(3)將兩個 shape 函式新增到 rectangle 中
(4)display() 方法只接收一個 shape 引數
(5)因為 rectangle 實現了 shape 的所有函式,並且由於是隱式實現的,所以 rect 也是一個 shape。因此,呼叫 display() 方法並將 rect 作為引數進行傳遞是完全合法的
Clojure:“依賴型別”
我之前的公司對 Clojure 投入了大量的資金。正因為如此,我努力學習過這門語言,甚至還寫了幾篇文章來總結我對它的理解。
Clojure 深受LISP的啟發。因此,表示式用圓括號括起來,首先執行位於圓括號內部的方法。此外,Clojure 是一種動態型別語言:它們雖然有型別,但沒有宣告。
另一方面,該語言提供了基於契約的程式設計。可以指定前置條件和後置條件:它們在執行時計算。這些條件可以進行型別檢查, 例如,檢查引數是字串還是布林值等?甚至可以進行更進一步地檢查,類似於 _dependent 型別:
在電腦科學和邏輯學中,依賴型別是其定義依賴於某個值的型別。“整數對”是一種型別。由於對值的依賴,“第二個大於第一個的整數對”也是依賴型別。
— 維基百科:https://en.wikipedia.org/wiki/Dependent_type
它在執行時強制執行,因此它不能被真正稱為依賴型別。然而,這是我所接觸過的語言中最接近依賴型別的一種了。
之前,我曾詳細寫過一篇關於依賴型別和基於契約程式設計的文章。
Elixir :模式匹配
一些語言吹噓自己提供了模式匹配的特性。通常,模式匹配可用於計算變數,例如,在 Kotlin 中:
varstatusCode: Int
val errorMessage =when(statusCode) {
401->"Unauthorized"
403->"Forbidden"
500->"Internal Server Error"
else->"Unrecognized Status Code"
}
這個用法是類固醇上(steroids)的 switch 語句。然而,一般來說,模式匹配的應用要廣泛得多。在下面的程式碼片段中,首先檢查常規 HTTP 狀態錯誤碼,如果沒有找到,則預設設成更通用的錯誤資訊
val errorMessage =when{
statusCode== 401 ->"Unauthorized"
statusCode== 403 ->"Forbidden"
statusCode- 400 < 100 ->"Client Error"
statusCode== 500 ->"Internal Server Error"
statusCode- 500 < 100 ->"Server Error"
else->"Unrecognized Status Code"
}
不過,它是有限制的。
Elixir 是一種在 Erlang OTP 上執行的動態型別語言,它將模式匹配提升到了一個全新的水平。Elixir 的模式匹配可用於簡單的變數析構
{a,b, c} = {:hello,"world",42}
a 將被賦值成 :hello,b 被賦值成 “world”,c 被賦值成 42。
它還可以對集合進行更高階的析構:
[head | tail] = [1,2,3]
head 被賦值成 1,tail 被賦值成 [2, 3]。
然而,對於函式過載來說,它甚至更是如此。作為一種函式式語言,Elixir 沒有用於迴圈的關鍵字(for 或 while),迴圈需要使用遞迴來實現。
舉個例子,我們使用遞迴來計算 List 的大小。在 Java 中,這是很容易的,因為有一個 size() 方法,但是 Elixir API 沒有提供這樣的功能。讓我們用如下的虛擬碼來實現該功能,Elixir 也是採用這種遞迴的方法。
publicintlengthOf(List<?>item){
return lengthOf(0,items);
}
privateintlengthOf(intsize, List<?>items){
if(items.isEmpty()) {
return size;
}else{
return lengthOf(size+ 1,items.remove(0));
}
}
幾乎可以將它逐行的轉換成 Elixir:
def length_of(list),do: length_of(0,list)
defp length_of(size,list)do
if[]==listdo
size
else
[_|tail]=list//(1)
length_of(size+ 1,tail)
end
end
(1)變數析構的模式匹配。表頭的值被賦值給 _ 變數,這意味著以後就無法引用它了,因為它沒有用處了。
然而,如前所述,Elixir 模式匹配也適用於函式過載。因此,Elixir 的命名方式將是:
deflist_len(list),do:list_len(0,list)
defplist_len(size, []),do: size//(1)
defplist_len(size,list)do//(2)
[_|tail]=list
list_len(size+ 1,tail)
end
(1)如果列表為空,則呼叫此方法
(2)否則呼叫此函式
注意,模式是按照宣告的順序進行評估的:在上面的程式碼段中, Elixir 首先評估具有空列表的函式,如果不匹配,才評估第二個函式,即列表不為空。如果要以相反的順序宣告函式,則每次都會對非空列表進行匹配操作。
廣州vi設計http://www.maiqicn.com 我的007辦公資源網站https://www.wode007.com
Python:for 推導式
Python 是一種動態型別語言。與 Java 一樣,Python 通過 for 關鍵字提供迴圈功能。下面的程式碼片段迴圈遍歷集合中的所有項,並逐個列印它們。
fornin[1,2,3,4,5]:
print(n)
要在新集合中收集所有項,可以先建立一個空集合,然後在迴圈中新增每個項到空集合中:
numbers = []
fornin[1,2,3,4,5]:
numbers.append(n)
print(numbers)
然而,可以使用一個精美的 Python 特性:for 推導式(for comprehensions)。雖然它與標準迴圈使用相同的 for 關鍵字,但是 for 推導式是一個能獲得相同結果的函式式構造器。
numbers = [nfornin[1,2,3,4,5]]
print(numbers)
上面片段的輸出是 [1, 2, 3, 4, 5] 。
也可以轉換每個項。例如,下面的程式碼段將計算每個項的平方:
numbers = [n **2fornin[1,2,3,4,5]]
print(numbers)
輸出是 [1, 4, 9, 16, 25]。
for 推導式的一個好處是能夠使用條件語句。例如,下面的程式碼片段將只過濾偶數項,然後將其平方:
numbers = [n **2fornin[1,2,3,4,5]ifn %2==0]
print(numbers)
輸出是 [4, 16]。
最後,for 推導式允許使用笛卡爾積。
numbers = [a:nfornin[1,2,3]forain['a','b']]
print(numbers)
它將會輸出 [(‘a’, 1), (‘b’, 1), (‘a’, 2), (‘b’, 2), (‘a’, 3), (‘b’, 3)]。
以上的 for 推導式也被稱為列表推導式(list comprehensions),因為它們是為了建立新的列表而設計的。Map 推導式(Map comprehension)也是非常相似的,目的是為了創造 map。