1. 程式人生 > >Scala學習(一)

Scala學習(一)



一、Scala論斷

Scala可以通過讓你提升你設計和使用的介面的抽象級別來幫助你管理複雜性。例如,假設你有一個String變數name,你想弄清楚是否String包含一個大寫字元。

val nameHasUpperCase = name.exists(_.isUpperCase)

Java程式碼把字串看作迴圈中逐字元步進的低層級實體。Scala程式碼把同樣的字串當作能用論斷:predicate查詢的字元高層級序列。明顯Scala程式碼更短並且——對訓練有素的眼睛來說——比Java程式碼更容易懂。因此Scala程式碼在通盤複雜度預算上能極度地變輕。它也更少給你機會犯錯。

論斷,_.isUpperCase,是一個Scala裡面函式式文字的例子。

[1]它描述了帶一個字元參量(用下劃線字元代表)的函式,並測試其是否為大寫字母。[2]

[1] 返回型別為Boolean的函式式文字被稱作論斷。

[2] 這種使用下劃線作為引數佔位符的做法會在8.5節中描述。

二、Scala變數

Scala有兩種變數,val和var。

val類似於Java裡的final變數。一旦初始化了,val就不能再賦值了。

與之對應的,var如同Java裡面的非final變數。var可以在它生命週期中被多次賦值。

三、函式

現在你已經用過了Scala的變數,或許想寫點兒函式。下面是在Scala裡面的做法:

scala> def max(x: Int, y: Int): Int = {

if (x > y) x

else y

}

max: (Int,Int)Int

函式的定義用def開始。函式名,本例中是max,跟著是括號裡帶有冒號分隔的引數列表。每個函式引數後面必須帶字首冒號的型別標註,因為Scala編譯器(還有直譯器,但之後我們將只說編譯器)沒辦法推斷函式引數型別。本例中,名叫max的函式帶有兩個引數,x和y,都是Int型別。在max引數列表的括號之後你會看到另一個“: Int”型別標註。這個東西定義了max函式的結果型別:result type[1]跟在函式結果型別之後的是一個等號和一對包含了函式體的大括號。本例中,函式體裡僅有一個if表示式,選擇x或者y,哪個較大,就當作max函式的結果。就像這裡演示的,Scala的if表示式可以像Java的三元操作符那樣產生一個值。舉例來說,Scala表示式“if (x > y) x else y”與Java裡的“(x > y) ? x : y”表現得很像。在函式體前的等號提示我們函數語言程式設計的世界觀裡,函式定義一個能產生值的表示式。函式的基本結構在

圖2.1裡面演示。

clip_image001

圖釋2.1 Scala函式的基本構成

有時候Scala編譯器會需要你定義函式的結果型別。比方說,如果函式是遞迴的,[2]你就必須顯式地定義函式結果型別。然而在max的例子裡,你可以不用寫結果型別,編譯器也能夠推斷它。[3]同樣,如果函式僅由一個句子組成,你可以可選地不寫大括號。這樣,你就可以把max函式寫成這樣:

scala> def max2(x: Int, y: Int) = if (x > y) x else y

[1] 在Java裡,從方法裡返回的值的型別被稱為返回型別。在Scala裡,同樣的概念被叫做結果型別

[2] 如果一個方法呼叫自身,就稱為遞迴。

[3] 儘管如此,就算編譯器不需要,顯式說明函式結果型別也經常是個好主意,這種型別標註可以使程式碼便於閱讀,因為讀者不用研究了函式體之後再去猜結果型別。

還有既不帶引數也不返回有用結果的函式定義:

scala> def greet() = println("Hello, world!")

greet: ()Unit

當你定義了greet()函式,直譯器會迴應一個greet: ()Unit。“greet”當然是函式名。空白的括號說明函式不帶引數。Unit是greet的結果型別。Unit的結果型別指的是函式沒有返回有用的值。Scala的Unit型別比較接近Java的void型別,而且實際上Java裡每一個返回void的方法都被對映為Scala裡返回Unit的方法。因此結果型別為Unit的方法,僅僅是為了它們的副作用而執行。在greet()的例子裡,副作用是在標準輸出上列印一句客氣的助詞。

下一步,你將把Scala程式碼放在一個檔案中並作為指令碼執行它。如果你想離開直譯器,輸入:quit或者:q。

scala> :quit

$

四、函式文字

var i = 0
while (i < args.length) {
  println(args(i))
  i += 1
}

注意Scala和Java一樣,必須把while或if的布林表示式放在括號裡。(換句話說,就是不能像在Ruby裡面那樣在Scala裡這麼寫:if i < 10。在Scala裡必須寫成if (i < 10)。)另外一點與Java類似的,是如果程式碼塊僅有一個句子,大括號就是可選的。

函式式語言的一個主要特徵是,函式是第一類結構,這在Scala裡千真萬確。舉例來說,另一種(簡潔得多)列印每一個命令列引數的方法是:

args.foreach(arg => println(arg))

這行程式碼中,你在args上呼叫foreach方法,並把它傳入函式。此例中,你傳入了帶有一個叫做arg引數的函式文字:function literal。函式體是println(arg)。如果你把上述程式碼輸入到新檔案pa.scala,並使用命令執行:

$ scala pa.scala Concise is nice

你會看到:

Concise

is

nice

前例中,Scala直譯器推斷arg的型別是String,因為String是你呼叫foreach的那個陣列的元素型別。如果你喜歡更顯式的,你可以加上型別名,不過如此的話你要把引數部分包裹在括號裡(總之這是語法的普通形式):

args.foreach((arg: String) => println(arg))

執行這個指令碼的結果與前一個相同。

如果你更喜歡簡潔的而不是顯式的風格,就可以充分體會到Scala特別簡潔的優越性。如果函式文字由帶一個引數的一句話組成,你都不需要顯式命名和指定引數[1]這樣,下面的程式碼同樣有效:

args.foreach(println)

總而言之,函式文字的語法就是,括號裡的命名引數列表,右箭頭,然後是函式體。語法演示在圖2.2中。

clip_image001[5]

圖釋2.2 Scala函式文字的語法

現在,到這裡你或許想知道那些你在指令式語言如Java或C裡那麼信任的for迴圈到哪裡去了呢。為了努力引導你向函式式的方向,Scala裡只有一個指令式for(稱為for表示式:expression)的函式式近似。目前你還看不到他們全部的力量和表達方式,直到你讀到了(或者先瞄一眼)第7.3節,我們僅僅帶您在這裡領略一下。建立一個新檔案forargs.scala,輸入以下程式碼:

for (arg <- args)

println(arg)

這個表示式裡“for”之後的括號包含arg<-args。[2]<-右側的是熟悉的args陣列。<-左側的是“arg”,val的名稱(不是var)。(因為總歸是val,你只要寫arg就可,不要寫成val arg。)儘管arg可能感覺像var,因為他在每次列舉都會得到新的值,但它的確是val : arg不能在for表示式的函式體中重新賦值。取而代之,對每個args陣列的元素,一個新的arg val將被建立並初始化為元素值,然後for的函式體將被執行

如果執行forargs.scala指令碼:

$ scala forargs.scala for arg in args

可以看到:

for

arg

in

args

[1] 這種簡寫被稱為偏應用函式:partially applied function,將在8.6節裡描述。

[2] 你可以認為<-符號代表“其中”。如果要讀for(arg<-args),就讀做“對於args中的arg”。

當你在一個或多個值或變數外使用括號時,Scala會把它轉換成對名為apply的方法呼叫。於是greetStrings(i)轉換成greetStrings.apply(i)。所以Scala裡訪問陣列的元素也只不過是跟其它的一樣的方法呼叫。這個原則不僅僅侷限於陣列:任何對某些在括號中的引數的物件的應用將都被轉換為對apply方法的呼叫。當然前提是這個型別實際定義過apply方法。所以這不是一個特例,而是一個通則。

當對帶有括號幷包括一到若干引數的變數賦值時,編譯器將把它轉化為對帶有括號裡引數和等號右邊的物件的update方法的呼叫。例如,

greetStrings(0) = "Hello"

將被轉化為

greetStrings.update(0, "Hello")

因此,下列Scala程式碼與你在程式碼3.1裡的程式碼語義一致:

val greetStrings = new Array[String](3)

greetStrings.update(0, "Hello")

greetStrings.update(1, ", ")

greetStrings.update(2, "world!\n")

for (i <- 0.to(2))

print(greetStrings.apply(i))

五、使用Tuple,Set或Map

另一種有用的容器物件是元組:tuple。與列表一樣,元組也是不可變的,但與列表不同,元組可以包含不同型別的元素。而列表應該是List[Int]或List[String]的樣子,元組可以同時擁有Int和String。元組很有用,比方說,如果你需要在方法裡返回多個物件。Java裡你將經常建立一個JavaBean樣子的類去裝多個返回值,Scala裡你可以簡單地返回一個元組。而且這麼做的確簡單:例項化一個裝有一些物件的新元組,只要把這些物件放在括號裡,並用逗號分隔即可。一旦你已經例項化了一個元組,你可以用點號,下劃線和一個基於1的元素索引訪問它。程式碼3.4展示了一個例子:

val pair = (99, "Luftballons")

println(pair._1)

println(pair._2)

程式碼 3.4 創造和使用元組

程式碼3.4的第一行,你建立了元組,它的第一個元素是以99為值的Int,第二個是"luftballons"為值的String。Scala推斷元組型別為Tuple2[Int, String],並把它賦給變數pair。第二行,你訪問_1欄位,從而輸出第一個元素,99。第二行的這個“.”與你用來訪問欄位或呼叫方法的點沒有區別。本例中你正用來訪問名叫_1的欄位。如果執行這個指令碼,你能看到:

99

Luftballons

元組的實際型別取決於它含有的元素數量和這些元素的型別。因此,(99, "Luftballons")的型別是Tuple2[Int, String]。('u', 'r', 'the', 1, 4, "me")是Tuple6[Char, Char, String, Int, Int, String]。[1]

訪問元組的元素

你或許想知道為什麼你不能像訪問List裡的元素那樣訪問元組的,就像pair(0)。那是因為List的apply方法始終返回同樣的型別,但是元組裡的或許型別不同。_1可以有一個結果型別,_2是另外一個,諸如此類。這些_N數字是基於1的,而不是基於0的,因為對於擁有靜態型別元組的其他語言,如Haskell和ML,從1開始是傳統的設定。

[1] 儘管理論上你可以建立任意長度的元組,然而當前Scala庫僅支援到Tupe22。

val romanNumeral = Map(

1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V"

)

println(romanNumeral(4))

六、學習識別函式式風格

第1章裡提到過,Scala允許你用指令式風格程式設計,但是鼓勵你採用一種更函式式的風格。如果你是從指令式的背景轉到Scala來的——例如,如果你是Java程式設計師——那麼學習Scala是你有可能面對的主要挑戰就是理解怎樣用函式式的風格程式設計。我們明白這種轉變會很困難,在本書中我們將竭盡所能把你向這方面引導。不過這也需要你這方面的一些工作,我們鼓勵你付出努力。如果你來自於指令式的背景,我們相信學習用函式式風格程式設計將不僅讓你變成更好的Scala程式設計師,而且還能拓展你的視野並使你變成通常意義上好的程式設計師。

通向更函式式風格路上的第一步是識別這兩種風格在程式碼上的差異。其中的一點蛛絲馬跡就是,如果程式碼包含了任何var變數,那它大概就是指令式的風格如果程式碼根本就沒有var——就是說僅僅包含val——那它大概是函式式的風格。因此向函式式風格推進的一個方式,就是嘗試不用任何var程式設計

如果你來自於指令式的背景,如Java,C++,或者C#,你或許認為var是很正統的變數而val是一種特殊型別的變數。相反,如果你來自於函式式背景,如Haskell,OCamel,或Erlang,你或許認為val是一種正統的變數而var有褻瀆神靈的血統。然而在Scala看來,val和var只不過是你工具箱裡兩種不同的工具。它們都很有用,沒有一個天生是魔鬼。Scala鼓勵你學習val,但也不會責怪你對給定的工作選擇最有效的工具。儘管或許你同意這種平衡的哲學,你或許仍然發現第一次理解如何從你的程式碼中去掉var是很挑戰的事情。

考慮下面這個改自於第2章的while迴圈例子,它使用了var並因此屬於指令式風格:

def printArgs(args: Array[String]): Unit = {

var i = 0

while (i < args.length) {

println(args(i))

i += 1

}

}

你可以通過去掉var的辦法把這個程式碼變得更函式式風格,例如,像這樣:

def printArgs(args: Array[String]): Unit = {

for (arg <- args)

println(arg)

}

或這樣:

def printArgs(args: Array[String]): Unit = {

args.foreach(println)

}

這個例子演示了減少使用var的一個好處。重構後(更函式式)的程式碼比原來(更指令式)的程式碼更簡潔,明白,也更少機會犯錯。Scala鼓勵函式式風格的原因,實際上也就是因為函式式風格可以幫助你寫出更易讀懂,更不容易犯錯的程式碼。

當然,你可以走得更遠。重構後的printArgs方法並不是函式式的,因為它有副作用——本例中,其副作用是列印到標準輸出流。函式有副作用的馬腳就是結果型別為Unit。如果某個函式不返回任何有用的值,就是說其結果型別為Unit,那麼那個函式唯一能讓世界有點兒變化的辦法就是通過某種副作用。更函式式的方式應該是定義對需列印的arg進行格式化的方法,但是僅返回格式化之後的字串,如程式碼3.9所示:

def formatArgs(args: Array[String]) = args.mkString("\n")

程式碼 3.9 沒有副作用或var的函式

現在才是真正函式式風格的了:滿眼看不到副作用或者var。能在任何可列舉的集合型別(包括陣列,列表,集和對映)上呼叫的mkString方法,返回由每個陣列元素呼叫toString產生結果組成的字串,以傳入字串間隔。因此如果args包含了三個元素,"zero","one"和"two",formatArgs將返回"zero\none\ntwo"。當然,這個函式並不像printArgs方法那樣實際列印輸出,但可以簡單地把它的結果傳遞給println來實現:

println(formatArgs(args))

每個有用的程式都可能有某種形式的副作用,因為否則就不可能對外部世界提供什麼值。偏好於無副作用的方法可以鼓勵你設計副作用程式碼最少化了的程式。這種方式的好處之一是可以有助於使你的程式更容易測試。舉例來說,要測試本節之前給出三段printArgs方法的任一個,你將需要重定義println,捕獲傳遞給它的輸出,並確信這是你希望的。相反,你可以通過檢查結果來測試formatArgs:

val res = formatArgs(Array("zero", "one", "two"))

assert(res == "zero\none\ntwo")

Scala的assert方法檢查傳入的Boolean並且如果是假,丟擲AssertionError。如果傳入的Boolean是真,assert只是靜靜地返回。你將在第十四章學習更多關於斷言和測試的東西。

雖如此說,不過請牢記在心:不管是var還是副作用都不是天生邪惡的。Scala不是強迫你用函式式風格編任何東西的純函式式語言。它是一種指令式/函式式混合的語言。你或許發現在某些情況下指令式風格更符合你手中的問題,在這時候你不應該對使用它猶豫不決。然而,為了幫助你學習如何不使用var程式設計,在第7章中我們會給你看許多有var的特殊程式碼例子和如何把這些var轉換為val。

Scala裡方法引數的一個重要特徵是它們都是val,不是var。[1]如果你想在方法裡面給引數重新賦值,結果是編譯失敗:

def add(b: Byte): Unit = {

b += 1 // 編譯不過,因為b是val

sum += b

}

[1] 引數是val的理由是val更容易講清楚。你不需要多看程式碼以確定是否val被重新賦值,而var則不然。

如果沒有發現任何顯式的返回語句,Scala方法將返回方法中最後一個計算得到的值

假如某個方法僅計算單個結果表示式,則可以去掉大括號。如果結果表示式很短,甚至可以把它放在def同一行裡。這樣改動之後,類ChecksumAccumulator看上去像這樣:

class ChecksumAccumulator {

private var sum = 0

def add(b: Byte): Unit = sum += b

def checksum(): Int = ~(sum & 0xFF) + 1

}

像ChecksumAccumulator的add方法那樣的結果型別為Unit的方法,執行的目的就是它的副作用。通常我們定義副作用為在方法外部某處改變狀態或者執行I/O活動。比方說,在add這個例子裡,副作用就是sum被重新賦值了。表達這個方法的另一種方式是去掉結果型別和等號,把方法體放在大括號裡。這種形式下,方法看上去很像過程:procedure,一種僅為了副作用而執行的方法。程式碼4.1的add方法裡演示了這種風格:

// 檔案ChecksumAccumulator.scala

class ChecksumAccumulator {

private var sum = 0

def add(b: Byte) { sum += b }

def checksum(): Int = ~(sum & 0xFF) + 1

}

程式碼 4.1 類ChecksumAccumulator的最終版

應該注意到令人困惑的地方是當你去掉方法體前面的等號時,它的結果型別將註定是Unit。不論方法體裡面包含什麼都不例外,因為Scala編譯器可以把任何型別轉換為Unit。例如,如果方法的最後結果是String,但方法的結果型別被宣告為Unit,那麼String將被轉變為Unit並失去它的值。下面是這個例子:

scala> def f(): Unit = "this String gets lost"

f: ()Unit

例子裡,String被轉變為Unit因為Unit是函式f宣告的結果型別。Scala編譯器會把一個以過程風格定義的方法,就是說,帶有大括號但沒有等號的,在本質上當作是顯式定義結果型別為Unit的方法。例如:

scala> def g() { "this String gets lost too" }

g: ()Unit

因此,如果你本想返回一個非Unit的值,卻忘記了等號時,那麼困惑就出現了。所以為了得到你想要的結果,你需要插入等號

scala> def h() = { "this String gets returned!" }

h: ()java.lang.String

scala> h

res0: java.lang.String = this String gets returned!

七、 分號推斷

Scala程式裡,語句末尾的分號通常是可選的。如果你願意可以輸入一個,但若一行裡僅有一個語句也可不寫。另一方面,如果一行裡寫多個語句那麼分號是需要的:

val s = "hello"; println(s)

如果你想輸入一個跨越多行的語句,多數時候你只需輸入,Scala將在正確的位置分隔語句。例如,下面的程式碼被認為是一個跨四行的語句:

if (x < 2)

println("too small")

else

println("ok")

然而,偶爾Scala也許沒有按照你的願望把句子分割成兩部分:

x

+ y

這會被分成兩個語句x和+ y。如果你希望把它作為一個語句x + y,你可以把它包裹在括號裡:

(x

+ y)

或者,你也可以把+放在行末。正是由於這個原因,當你在串接類似於+的中綴操作符,把操作符放在行尾而不是行頭是普遍的Scala風格:

x +

y +

z

如第1章所提到的,Scala比Java更面向物件的一個方面是Scala沒有靜態成員。替代品是,Scala有單例物件:singleton object。除了用object關鍵字替換了class關鍵字以外,單例物件的定義看上去就像是類定義。程式碼4.2展示了一個例子:

// 檔案ChecksumAccumulator.scala

import scala.collection.mutable.Map

object ChecksumAccumulator {

private val cache = Map[String, Int]()

def calculate(s: String): Int =

if (cache.contains(s))

cache(s)

else {

val acc = new ChecksumAccumulator

for (c <- s)

acc.add(c.toByte)

val cs = acc.checksum()

cache += (s -> cs)

cs

}

}

程式碼 4.2 類ChecksumAccumulator的伴生物件

表中的單例物件被叫做ChecksumAccumulator,與前一個例子裡的類同名。當單例物件與某個類共享同一個名稱時,他被稱作是這個類的伴生物件:companion object。你必須在同一個原始檔裡定義類和它的伴生物件。類被稱為是這個單例物件的伴生類:companion class。類和它的伴生物件可以互相訪問其私有成員。

ChecksumAccumulator單例物件有一個方法,calculate,用來計算所帶的String引數中字元的校驗和。它還有一個私有欄位,cache,一個快取之前計算過的校驗和的可變對映。[1]方法的第一行,“if (cache.contains(s))”,檢查快取,看看是否傳遞進來的字串已經作為鍵存在於對映當中。如果是,就僅僅返回對映的值,“cache(s)”。否則,執行else子句,計算校驗和。else子句的第一行定義了一個叫acc的val並用新建的ChecksumAccumulator例項初始化它。[2]下一行是個for表示式,對傳入字串的每個字元迴圈一次,並在其上呼叫toByte把字元轉換成Byte,然後傳遞給acc所指的ChecksumAccumulator例項的add方法。完成了for表示式後,下一行的方法在acc上呼叫checksum,獲得傳入字串的校驗和,並存入叫做cs的val。下一行,“cache += (s -> cs)”,傳入的字串鍵對映到整數的校驗和值,並把這個鍵-值對加入cache對映。方法的最後一個表示式,“cs”,保證了校驗和為此方法的結果

如果你是Java程式設計師,考慮單例物件的一種方式是把它當作是或許你在Java中寫過的任何靜態方法之家。可以在單例物件上用類似的語法呼叫方法:單例物件名,點,方法名。例如,可以如下方式呼叫ChecksumAccumulator單例物件的calculate方法:

ChecksumAccumulator.calculate("Every value is an object.")

然而單例物件不只是靜態方法的收容站。它同樣是個第一類的物件。因此你可以把單例物件的名字看作是貼在物件上的“名籤”:

clip_image001[7]

定義單例物件不是定義型別(在Scala的抽象層次上說)。如果只是ChecksumAccumulator物件的定義,你就建不了ChecksumAccumulator型別的變數。寧願這麼說,ChecksumAccumulator型別是由單例物件的伴生類定義的。然而,單例物件擴充套件了超類並可以混入特質。由於每個單例物件都是超類的例項並混入了特質,你可以通過這些型別呼叫它的方法,用這些型別的變數指代它,並把它傳遞給需要這些型別的方法。我們將在第十二章展示一些繼承自類和特質的單例物件的例子。

類和單例物件間的一個差別是,單例物件不帶引數,而類可以。因為你不能用new關鍵字例項化一個單例物件,你沒機會傳遞給它引數。每個單例物件都被作為由一個靜態變數指向的虛構類:synthetic class的一個例項來實現,因此它們與Java靜態類有著相同的初始化語法。[3]特別要指出的是,單例物件會在第一次被訪問的時候初始化。

不與伴生類共享名稱的單例物件被稱為孤立物件:standalone object。由於很多種原因你會用到它,包括把相關的功能方法收集在一起,或定義一個Scala應用的入口點。下一段會說明這個用例。

[1] 這裡我們使用了快取例子來說明帶有域的單例物件。像這樣的快取是通過記憶體換計算時間的方式做到效能的優化。通常意義上說,只有遇到了快取能解決的效能問題時,才可能用到這樣的例子,而且應該使用弱對映(weak map),如scala.Collection.jcl的WeakHashMap,這樣如果記憶體稀缺的話,快取裡的條目就會被垃圾回收機制回收掉。

[2] 因為關鍵字new只用來例項化類,所以這裡創造的新物件是ChecksumAccumulator類的一個例項,而不是同名的單例物件。

[3] 虛構類的名字是物件名加上一個美元符號。因此單例物件ChecksumAccumulator的虛構類是ChecksumAccumulator$。

Scala提供了一個特質,scala.Application,可以節省你一些手指的輸入工作。儘管我們還沒有完全提供給你去搞明白它如何工作的所有需要知道的東西,不過我們還是認為你可能想要知道它。程式碼4.4展示了一個例子:

import ChecksumAccumulator.calculate

object FallWinterSpringSummer extends Application {

for (season <- List("fall", "winter", "spring"))

println(season +": "+ calculate(season))

}

程式碼 4.4 使用Application特質

使用這個特質的方法是,首先在你的單例物件名後面寫上“extends Application” 。然後代之以main方法,你可以把想要放在main方法裡的程式碼直接放在單例物件的大括號之間。就這麼簡單。之後可以像對其它程式那樣編譯和執行。

這種方式之所以能奏效是因為特質Application聲明瞭帶有合適的簽名的main方法,並由你的單例物件繼承,使它可以像個Scala程式那樣用。大括號之間的程式碼被收集進了單例物件的主構造器:primary constructor,並在類被初始化時被執行。如果你不明白所有這些指的是什麼也不用著急。之後的章節會解釋這些,目前可以暫時不求甚解。

繼承自Application比寫個顯式的main方法要短,不過它也有些缺點。首先,如果想訪問命令列引數的話就不能用它,因為args陣列不可訪問。比如,因為Summer程式使用了命令列引數,所以它必須帶有顯式的main方法,如程式碼4.3所示。第二,因為某些JVM執行緒模型裡的侷限,如果你的程式是多執行緒的就需要顯式的main方法。最後,某些JVM的實現沒有優化被Application特質執行的物件的初始化程式碼。因此只有當你的程式相對簡單和單執行緒情況下你才可以繼承Application特質。