泛型
軟件工程中,我們不僅要?jiǎng)?chuàng)建定義良好且一致的 API,同時(shí)也要考慮可重用性。 組件不僅能夠支持當(dāng)前的數(shù)據(jù)類型,同時(shí)也能支持未來(lái)的數(shù)據(jù)類型,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供了十分靈活的功能。
在像 C# 和 Java 這樣的語(yǔ)言中,可以使用泛型來(lái)創(chuàng)建可重用的組件,一個(gè)組件可以支持多種類型的數(shù)據(jù)。 這樣用戶就可以以自己的數(shù)據(jù)類型來(lái)使用組件。
基礎(chǔ)示例
下面來(lái)創(chuàng)建第一個(gè)使用泛型的例子:identity 函數(shù)。 這個(gè)函數(shù)會(huì)返回任何傳入它的值。 你可以把這個(gè)函數(shù)當(dāng)成是 echo 命令。
不用泛型的話,這個(gè)函數(shù)可能是下面這樣:
function identity(arg: number): number {
return arg
}
或者,我們使用 any 類型來(lái)定義函數(shù):
function identity(arg: any): any {
return arg
}
使用 any 類型會(huì)導(dǎo)致這個(gè)函數(shù)可以接收任何類型的 arg 參數(shù),但是這樣就丟失了一些信息:傳入的類型與返回的類型應(yīng)該是相同的。如果我們傳入一個(gè)數(shù)字,我們只知道任何類型的值都有可能被返回。
因此,我們需要一種方法使返回值的類型與傳入?yún)?shù)的類型是相同的。這里,我們使用了類型變量,它是一種特殊的變量,只用于表示類型而不是值。
function identity<T>(arg: T): T {
return arg
}
我們給 identity 添加了類型變量 T。 T 幫助我們捕獲用戶傳入的類型(比如:number),之后我們就可以使用這個(gè)類型。 之后我們?cè)俅问褂昧?T 當(dāng)做返回值類型?,F(xiàn)在我們可以知道參數(shù)類型與返回值類型是相同的了。這允許我們跟蹤函數(shù)里使用的類型的信息。
我們把這個(gè)版本的 identity 函數(shù)叫做泛型,因?yàn)樗梢赃m用于多個(gè)類型。 不同于使用 any,它不會(huì)丟失信息,像第一個(gè)例子那像保持準(zhǔn)確性,傳入數(shù)值類型并返回?cái)?shù)值類型。
我們定義了泛型函數(shù)后,可以用兩種方法使用。 第一種是,傳入所有的參數(shù),包含類型參數(shù):
let output = identity<string>('myString')
這里我們明確的指定了 T 是 string 類型,并做為一個(gè)參數(shù)傳給函數(shù),使用了 <> 括起來(lái)而不是 ()。
第二種方法更普遍。利用了類型推論 -- 即編譯器會(huì)根據(jù)傳入的參數(shù)自動(dòng)地幫助我們確定 T 的類型:
let output = identity('myString')
注意我們沒必要使用尖括號(hào)(<>)來(lái)明確地傳入類型;編譯器可以查看 myString 的值,然后把 T 設(shè)置為它的類型。 類型推論幫助我們保持代碼精簡(jiǎn)和高可讀性。如果編譯器不能夠自動(dòng)地推斷出類型的話,只能像上面那樣明確的傳入 T 的類型,在一些復(fù)雜的情況下,這是可能出現(xiàn)的。
使用泛型變量
使用泛型創(chuàng)建像 identity 這樣的泛型函數(shù)時(shí),編譯器要求你在函數(shù)體必須正確的使用這個(gè)通用的類型。 換句話說(shuō),你必須把這些參數(shù)當(dāng)做是任意或所有類型。
看下之前 identity 例子:
function identity<T>(arg: T): T {
return arg
}
如果我們想打印出 arg 的長(zhǎng)度。 我們很可能會(huì)這樣做:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length)
return arg
}
如果這么做,編譯器會(huì)報(bào)錯(cuò)說(shuō)我們使用了 arg 的 .length 屬性,但是沒有地方指明 arg 具有這個(gè)屬性。記住,這些類型變量代表的是任意類型,所以使用這個(gè)函數(shù)的人可能傳入的是個(gè)數(shù)字,而數(shù)字是沒有 .length 屬性的。
現(xiàn)在假設(shè)我們想操作 T 類型的數(shù)組而不直接是 T。由于我們操作的是數(shù)組,所以 .length 屬性是應(yīng)該存在的。我們可以像創(chuàng)建其它數(shù)組一樣創(chuàng)建這個(gè)數(shù)組:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length)
return arg
}
你可以這樣理解 loggingIdentity 的類型:泛型函數(shù) loggingIdentity,接收類型參數(shù) T 和參數(shù) arg,它是個(gè)元素類型是 T 的數(shù)組,并返回元素類型是T 的數(shù)組。 如果我們傳入數(shù)字?jǐn)?shù)組,將返回一個(gè)數(shù)字?jǐn)?shù)組,因?yàn)榇藭r(shí) T 的的類型為 number。 這可以讓我們把泛型變量 T 當(dāng)做類型的一部分使用,而不是整個(gè)類型,增加了靈活性。
泛型類型
上一節(jié),我們創(chuàng)建了 identity 通用函數(shù),可以適用于不同的類型。 在這節(jié),我們研究一下函數(shù)本身的類型,以及如何創(chuàng)建泛型接口。
泛型函數(shù)的類型與非泛型函數(shù)的類型沒什么不同,只是有一個(gè)類型參數(shù)在最前面,像函數(shù)聲明一樣:
function identity<T>(arg: T): T {
return arg
}
let myIdentity: <T>(arg: T) => T = identity
我們也可以使用不同的泛型參數(shù)名,只要在數(shù)量上和使用方式上能對(duì)應(yīng)上就可以。
function identity<T>(arg: T): T {
return arg
}
let myIdentity: <U>(arg: U) => U = identity
我們還可以使用帶有調(diào)用簽名的對(duì)象字面量來(lái)定義泛型函數(shù):
function identity<T>(arg: T): T {
return arg
}
let myIdentity: {<T>(arg: T): T} = identity
這引導(dǎo)我們?nèi)懙谝粋€(gè)泛型接口了。我們把上面例子里的對(duì)象字面量拿出來(lái)做為一個(gè)接口:
interface GenericIdentityFn {
<T>(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn = identity
我們甚至可以把泛型參數(shù)當(dāng)作整個(gè)接口的一個(gè)參數(shù)。 這樣我們就能清楚的知道使用的具體是哪個(gè)泛型類型(比如: Dictionary<string> 而不只是Dictionary)。這樣接口里的其它成員也能知道這個(gè)參數(shù)的類型了。
interface GenericIdentityFn<T> {
(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn<number> = identity
注意,我們的示例做了少許改動(dòng)。 不再描述泛型函數(shù),而是把非泛型函數(shù)簽名作為泛型類型一部分。 當(dāng)我們使用 GenericIdentityFn 的時(shí)候,還得傳入一個(gè)類型參數(shù)來(lái)指定泛型類型(這里是:number),鎖定了之后代碼里使用的類型。對(duì)于描述哪部分類型屬于泛型部分來(lái)說(shuō),理解何時(shí)把參數(shù)放在調(diào)用簽名里和何時(shí)放在接口上是很有幫助的。
除了泛型接口,我們還可以創(chuàng)建泛型類。 注意,無(wú)法創(chuàng)建泛型枚舉和泛型命名空間。
泛型類
泛型類看上去與泛型接口差不多。 泛型類使用( <>)括起泛型類型,跟在類名后面。
class GenericNumber<T> {
zeroValue: T
add: (x: T, y: T) => T
}
let myGenericNumber = new GenericNumber<number>()
myGenericNumber.zeroValue = 0
myGenericNumber.add = function(x, y) {
return x + y
}
GenericNumber 類的使用是十分直觀的,并且你可能已經(jīng)注意到了,沒有什么去限制它只能使用 number 類型。 也可以使用字符串或其它更復(fù)雜的類型。
let stringNumeric = new GenericNumber<string>()
stringNumeric.zeroValue = ''
stringNumeric.add = function(x, y) {
return x + y
}
console.log(stringNumeric.add(stringNumeric.zeroValue, 'test'))
與接口一樣,直接把泛型類型放在類后面,可以幫助我們確認(rèn)類的所有屬性都在使用相同的類型。
我們?cè)?a href="/chapter2/class" target="_blank">類那節(jié)說(shuō)過(guò),類有兩部分:靜態(tài)部分和實(shí)例部分。 泛型類指的是實(shí)例部分的類型,所以類的靜態(tài)屬性不能使用這個(gè)泛型類型。
泛型約束
我們有時(shí)候想操作某類型的一組值,并且我們知道這組值具有什么樣的屬性。在 loggingIdentity 例子中,我們想訪問 arg 的 length 屬性,但是編譯器并不能證明每種類型都有 length 屬性,所以就報(bào)錯(cuò)了。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length)
return arg
}
相比于操作 any 所有類型,我們想要限制函數(shù)去處理任意帶有 .length 屬性的所有類型。 只要傳入的類型有這個(gè)屬性,我們就允許,就是說(shuō)至少包含這一屬性。為此,我們需要列出對(duì)于 T 的約束要求。
我們定義一個(gè)接口來(lái)描述約束條件,創(chuàng)建一個(gè)包含 .length 屬性的接口,使用這個(gè)接口和 extends 關(guān)鍵字來(lái)實(shí)現(xiàn)約束:
interface Lengthwise {
length: number
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length) // OK
return arg
}
現(xiàn)在這個(gè)泛型函數(shù)被定義了約束,因此它不再是適用于任意類型:
loggingIdentity(3); // Error
我們需要傳入符合約束類型的值,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3}) // OK
在泛型約束中使用類型參數(shù)
你可以聲明一個(gè)類型參數(shù),且它被另一個(gè)類型參數(shù)所約束。 比如,現(xiàn)在我們想要用屬性名從對(duì)象里獲取這個(gè)屬性。 并且我們想要確保這個(gè)屬性存在于對(duì)象 obj 上,因此我們需要在這兩個(gè)類型之間使用約束。
function getProperty<T, K extends keyof T> (obj: T, key: K ) {
return obj[key]
}
let x = {a: 1, b: 2, c: 3, d: 4}
getProperty(x, 'a') // okay
getProperty(x, 'm') // error
高級(jí)類型
交叉類型
交叉類型是將多個(gè)類型合并為一個(gè)類型。 這讓我們可以把現(xiàn)有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。 例如,Person & Loggable 同時(shí)是 Person 和 Loggable。 就是說(shuō)這個(gè)類型的對(duì)象同時(shí)擁有了這兩種類型的成員。
我們大多是在混入(mixins)或其它不適合典型面向?qū)ο竽P偷牡胤娇吹浇徊骖愋偷氖褂谩?(在 JavaScript 里發(fā)生這種情況的場(chǎng)合很多?。?下面是如何創(chuàng)建混入的一個(gè)簡(jiǎn)單例子:
function extend<T, U> (first: T, second: U): T & U {
let result = {} as T & U
for (let id in first) {
result[id] = first[id] as any
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
result[id] = second[id] as any
}
}
return result
}
class Person {
constructor (public name: string) {
}
}
interface Loggable {
log (): void
}
class ConsoleLogger implements Loggable {
log () {
// ...
}
}
var jim = extend(new Person('Jim'), new ConsoleLogger())
var n = jim.name
jim.log()
聯(lián)合類型
聯(lián)合類型與交叉類型很有關(guān)聯(lián),但是使用上卻完全不同。 偶爾你會(huì)遇到這種情況,一個(gè)代碼庫(kù)希望傳入 number 或 string 類型的參數(shù)。 例如下面的函數(shù):
function padLeft(value: string, padding: any) {
if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if (typeof padding === 'string') {
return padding + value
}
throw new Error(`Expected string or number, got '${padding}'.`)
}
padLeft('Hello world', 4) // returns " Hello world"
padLeft 存在一個(gè)問題,padding 參數(shù)的類型指定成了 any。 這就是說(shuō)我們可以傳入一個(gè)既不是 number 也不是 string 類型的參數(shù),但是 TypeScript 卻不報(bào)錯(cuò)。
let indentedString = padLeft('Hello world', true) // 編譯階段通過(guò),運(yùn)行時(shí)報(bào)錯(cuò)
為了解決這個(gè)問題,我們可以使用 聯(lián)合類型做為 padding 的參數(shù):
function padLeft(value: string, padding: string | number) {
// ...
}
let indentedString = padLeft('Hello world', true) // 編譯階段報(bào)錯(cuò)
聯(lián)合類型表示一個(gè)值可以是幾種類型之一。我們用豎線(|)分隔每個(gè)類型,所以 number | string 表示一個(gè)值可以是 number 或 string。
如果一個(gè)值是聯(lián)合類型,我們只能訪問此聯(lián)合類型的所有類型里共有的成員。
interface Bird {
fly()
layEggs()
}
interface Fish {
swim()
layEggs()
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet()
pet.layEggs() // okay
pet.swim() // error
這里的聯(lián)合類型可能有點(diǎn)復(fù)雜:如果一個(gè)值的類型是 A | B,我們能夠確定的是它包含了 A 和 B 中共有的成員。這個(gè)例子里,Fish 具有一個(gè) swim 方法,我們不能確定一個(gè) Bird | Fish 類型的變量是否有 swim方法。 如果變量在運(yùn)行時(shí)是 Bird 類型,那么調(diào)用 pet.swim() 就出錯(cuò)了。
類型保護(hù)
聯(lián)合類型適合于那些值可以為不同類型的情況。 但當(dāng)我們想確切地了解是否為 Fish 或者是 Bird 時(shí)怎么辦? JavaScript 里常用來(lái)區(qū)分這 2 個(gè)可能值的方法是檢查成員是否存在。如之前提及的,我們只能訪問聯(lián)合類型中共同擁有的成員。
let pet = getSmallPet()
// 每一個(gè)成員訪問都會(huì)報(bào)錯(cuò)
if (pet.swim) {
pet.swim()
} else if (pet.fly) {
pet.fly()
}
為了讓這段代碼工作,我們要使用類型斷言:
let pet = getSmallPet()
if ((pet as Fish).swim) {
(pet as Fish).swim()
} else {
(pet as Bird).fly()
}
用戶自定義的類型保護(hù)
這里可以注意到我們不得不多次使用類型斷言。如果我們一旦檢查過(guò)類型,就能在之后的每個(gè)分支里清楚地知道 pet 的類型的話就好了。
TypeScript 里的類型保護(hù)機(jī)制讓它成為了現(xiàn)實(shí)。 類型保護(hù)就是一些表達(dá)式,它們會(huì)在運(yùn)行時(shí)檢查以確保在某個(gè)作用域里的類型。定義一個(gè)類型保護(hù),我們只要簡(jiǎn)單地定義一個(gè)函數(shù),它的返回值是一個(gè)類型謂詞:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}
在這個(gè)例子里,pet is Fish 就是類型謂詞。謂詞為 parameterName is Type 這種形式, parameterName 必須是來(lái)自于當(dāng)前函數(shù)簽名里的一個(gè)參數(shù)名。
每當(dāng)使用一些變量調(diào)用 isFish 時(shí),TypeScript 會(huì)將變量縮減為那個(gè)具體的類型。
if (isFish(pet)) {
pet.swim()
}
else {
pet.fly()
}
注意 TypeScript 不僅知道在 if 分支里 pet 是 Fish 類型;它還清楚在 else 分支里,一定不是 Fish類型而是 Bird 類型。
typeof 類型保護(hù)
現(xiàn)在我們回過(guò)頭來(lái)看看怎么使用聯(lián)合類型書寫 padLeft 代碼。我們可以像下面這樣利用類型斷言來(lái)寫:
function isNumber (x: any):x is string {
return typeof x === 'number'
}
function isString (x: any): x is string {
return typeof x === 'string'
}
function padLeft (value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(' ') + value
}
if (isString(padding)) {
return padding + value
}
throw new Error(`Expected string or number, got '${padding}'.`)
}
然而,你必須要定義一個(gè)函數(shù)來(lái)判斷類型是否是原始類型,但這并不必要。其實(shí)我們不必將 typeof x === 'number'抽象成一個(gè)函數(shù),因?yàn)?TypeScript 可以將它識(shí)別為一個(gè)類型保護(hù)。 也就是說(shuō)我們可以直接在代碼里檢查類型了。
function padLeft (value: string, padding: string | number) {
if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value
}
if (typeof padding === 'string') {
return padding + value
}
throw new Error(`Expected string or number, got '${padding}'.`)
}
這些 typeof 類型保護(hù)只有兩種形式能被識(shí)別:typeof v === "typename" 和 typeof v !== "typename", "typename"必須是 "number", "string","boolean" 或 "symbol"。 但是 TypeScript 并不會(huì)阻止你與其它字符串比較,只是 TypeScript 不會(huì)把那些表達(dá)式識(shí)別為類型保護(hù)。
instanceof 類型保護(hù)
如果你已經(jīng)閱讀了 typeof 類型保護(hù)并且對(duì) JavaScript 里的 instanceof 操作符熟悉的話,你可能已經(jīng)猜到了這節(jié)要講的內(nèi)容。
instanceof 類型保護(hù)是通過(guò)構(gòu)造函數(shù)來(lái)細(xì)化類型的一種方式。我們把之前的例子做一個(gè)小小的改造:
class Bird {
fly () {
console.log('bird fly')
}
layEggs () {
console.log('bird lay eggs')
}
}
class Fish {
swim () {
console.log('fish swim')
}
layEggs () {
console.log('fish lay eggs')
}
}
function getRandomPet () {
return Math.random() > 0.5 ? new Bird() : new Fish()
}
let pet = getRandomPet()
if (pet instanceof Bird) {
pet.fly()
}
if (pet instanceof Fish) {
pet.swim()
}
可以為 null 的類型
TypeScript 具有兩種特殊的類型,null 和 undefined,它們分別具有值 null 和 undefined。我們?cè)?a href="/chapter2/type" target="_blank">基礎(chǔ)類型一節(jié)里已經(jīng)做過(guò)簡(jiǎn)要說(shuō)明。 默認(rèn)情況下,類型檢查器認(rèn)為 null 與 undefined 可以賦值給任何類型。 null 與 undefined 是所有其它類型的一個(gè)有效值。 這也意味著,你阻止不了將它們賦值給其它類型,就算是你想要阻止這種情況也不行。null的發(fā)明者,Tony Hoare,稱它為價(jià)值億萬(wàn)美金的錯(cuò)誤。
--strictNullChecks 標(biāo)記可以解決此錯(cuò)誤:當(dāng)你聲明一個(gè)變量時(shí),它不會(huì)自動(dòng)地包含 null 或 undefined。 你可以使用聯(lián)合類型明確的包含它們:
let s = 'foo'
s = null // 錯(cuò)誤, 'null'不能賦值給'string'
let sn: string | null = 'bar'
sn = null // 可以
sn = undefined // error, 'undefined'不能賦值給'string | null'
注意,按照 JavaScript 的語(yǔ)義,TypeScript 會(huì)把 null 和 undefined 區(qū)別對(duì)待。string | null,string | undefined 和 string | undefined | null 是不同的類型。
可選參數(shù)和可選屬性
使用了 --strictNullChecks,可選參數(shù)會(huì)被自動(dòng)地加上 | undefined:
function f(x: number, y?: number) {
return x + (y || 0)
}
f(1, 2)
f(1)
f(1, undefined)
f(1, null) // error, 'null' 不能賦值給 'number | undefined'
可選屬性也會(huì)有同樣的處理:
class C {
a: number
b?: number
}
let c = new C()
c.a = 12
c.a = undefined // error, 'undefined' 不能賦值給 'number'
c.b = 13
c.b = undefined // ok
c.b = null // error, 'null' 不能賦值給 'number | undefined'
類型保護(hù)和類型斷言
由于可以為 null 的類型能和其它類型定義為聯(lián)合類型,那么你需要使用類型保護(hù)來(lái)去除 null。幸運(yùn)地是這與在 JavaScript 里寫的代碼一致:
function f(sn: string | null): string {
if (sn === null) {
return 'default'
} else {
return sn
}
}
這里很明顯地去除了 null,你也可以使用短路運(yùn)算符:
function f(sn: string | null): string {
return sn || 'default'
}
如果編譯器不能夠去除 null 或 undefined,你可以使用類型斷言手動(dòng)去除。語(yǔ)法是添加 ! 后綴: identifier! 從 identifier 的類型里去除了 null 和 undefined:
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet // error, 'name' 可能為 null
}
name = name || 'Bob'
return postfix('great')
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet // ok
}
name = name || 'Bob'
return postfix('great')
}
broken(null)
本例使用了嵌套函數(shù),因?yàn)榫幾g器無(wú)法去除嵌套函數(shù)的 null(除非是立即調(diào)用的函數(shù)表達(dá)式)。因?yàn)樗鼰o(wú)法跟蹤所有對(duì)嵌套函數(shù)的調(diào)用,尤其是你將內(nèi)層函數(shù)做為外層函數(shù)的返回值。如果無(wú)法知道函數(shù)在哪里被調(diào)用,就無(wú)法知道調(diào)用時(shí) name 的類型。
字符串字面量類型
字符串字面量類型允許你指定字符串必須具有的確切值。在實(shí)際應(yīng)用中,字符串字面量類型可以與聯(lián)合類型,類型保護(hù)很好的配合。通過(guò)結(jié)合使用這些特性,你可以實(shí)現(xiàn)類似枚舉類型的字符串。
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'
class UIElement {
animate (dx: number, dy: number, easing: Easing) {
if (easing === 'ease-in') {
// ...
} else if (easing === 'ease-out') {
} else if (easing === 'ease-in-out') {
} else {
// error! 不能傳入 null 或者 undefined.
}
}
}
let button = new UIElement()
button.animate(0, 0, 'ease-in')
button.animate(0, 0, 'uneasy') // error
你只能從三種允許的字符中選擇其一來(lái)做為參數(shù)傳遞,傳入其它值則會(huì)產(chǎn)生錯(cuò)誤。
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
總結(jié)
那么到這里,我們的 TypeScript 常用語(yǔ)法學(xué)習(xí)就告一段落了,當(dāng)然 TypeScript 還有其他的語(yǔ)法我們并沒有講,我們只是講了 TypeScript 的一些常用語(yǔ)法,你們把這些知識(shí)學(xué)會(huì)已經(jīng)足以開發(fā)一般的應(yīng)用了。如果你在使用 TypeScript 開發(fā)項(xiàng)目中遇到了其他的 TypeScript 語(yǔ)法知識(shí),你可以通過(guò) TypeScript 的官網(wǎng)文檔學(xué)習(xí)。因?yàn)閷W(xué)基礎(chǔ)最好的方法還是去閱讀它的官網(wǎng)文檔,敲上面的小例子。其實(shí)我們課程的基礎(chǔ)知識(shí)結(jié)構(gòu)也是大部分參考了官網(wǎng)文檔,要記住學(xué)習(xí)一門技術(shù)的基礎(chǔ)官網(wǎng)文檔永遠(yuǎn)是最好的第一手資料。
但是 TypeScript 的學(xué)習(xí)不能僅僅靠看官網(wǎng)文檔,你還需要?jiǎng)邮謱?shí)踐,在實(shí)踐中你才能真正掌握 TypeScript。相信很多同學(xué)學(xué)習(xí)到這里已經(jīng)迫不及待想要大展身手了。