1. 程式人生 > 其它 >Vue原始碼學習_4個實用的Javascript技巧

Vue原始碼學習_4個實用的Javascript技巧

34. 反射

反射是 Go 語言的高階主題之一。

分為如下小節。

  • 什麼是反射?
  • 為何需要檢查變數,確定變數的型別?
  • reflect 包
    • reflect.Type 和 reflect.Value
    • reflect.Kind
    • NumField() 和 Field() 方法
    • Int() 和 String() 方法
  • 完整的程式
  • 我們應該使用反射嗎?

什麼是反射?

反射就是程式能夠在執行時檢查變數和值,求出它們的型別。你可能還不太懂,這沒關係。在本教程結束後,你就會清楚地理解反射,所以跟著我們的教程學習吧。

為何需要檢查變數,確定變數的型別?

在學習反射時,所有人首先面臨的疑惑就是:如果程式中每個變數都是我們自己定義的,那麼在編譯時就可以知道變數型別了,為什麼我們還需要在執行時檢查變數,求出它的型別呢?沒錯,在大多數時候都是這樣,但並非總是如此。

我來解釋一下吧。下面我們編寫一個簡單的程式。

Copy
package main

import (
    "fmt"
)

func main() {
    i := 10
    fmt.Printf("%d %T", i, i)
}

在上面的程式中,i 的型別在編譯時就知道了,然後我們在下一行打印出 i。這裡沒什麼特別之處。

現在瞭解一下,需要在執行時求得變數型別的情況。假如我們要編寫一個簡單的函式,它接收結構體作為引數,並用它來建立一個 SQL 插入查詢。

考慮下面的程式:

Copy
package main

import (
    "fmt"
)

type order struct
{ ordId int customerId int } func main() { o := order{ ordId: 1234, customerId: 567, } fmt.Println(o) }

在上面的程式中,我們需要編寫一個函式,接收結構體變數 o 作為引數,返回下面的 SQL 插入查詢。

Copy
insert into order values(1234, 567)

這個函式寫起來很簡單。我們現在編寫這個函式。

Copy
package main

import (
    "fmt"
) type order struct { ordId int customerId int } func createQuery(o order) string { i := fmt.Sprintf("insert into order values(%d, %d)", o.ordId, o.customerId) return i } func main() { o := order{ ordId: 1234, customerId: 567, } fmt.Println(createQuery(o)) }

在第 12 行,createQuery 函式用 o 的兩個欄位(ordIdcustomerId),建立了插入查詢。該程式會輸出:

Copy
insert into order values(1234, 567)

現在我們來升級這個查詢生成器。如果我們想讓它變得通用,可以適用於任何結構體型別,該怎麼辦呢?我們用程式來理解一下。

Copy
package main

type order struct {
    ordId      int
    customerId int
}

type employee struct {
    name string
    id int
    address string
    salary int
    country string
}

func createQuery(q interface{}) string {
}

func main() {

}

我們的目標就是完成 createQuery 函式(上述程式中的第 16 行),它可以接收任何結構體作為引數,根據結構體的欄位建立插入查詢。

例如,如果我們傳入下面的結構體:

Copy
o := order {
    ordId: 1234,
    customerId: 567
}

createQuery 函式應該返回:

Copy
insert into order values (1234, 567)

類似地,如果我們傳入:

Copy
 e := employee {
        name: "Naveen",
        id: 565,
        address: "Science Park Road, Singapore",
        salary: 90000,
        country: "Singapore",
    }

該函式會返回:

Copy
insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")

由於 createQuery 函式應該適用於任何結構體,因此它接收 interface{} 作為引數。為了簡單起見,我們只處理包含 stringint 型別欄位的結構體,但可以擴充套件為包含任何型別的欄位。

createQuery 函式應該適用於所有的結構體。因此,要編寫這個函式,就必須在執行時檢查傳遞過來的結構體引數的型別,找到結構體欄位,接著建立查詢。這時就需要用到反射了。在本教程的下一步,我們將會學習如何使用 reflect 包來實現它。

reflect 包

在 Go 語言中,reflect實現了執行時反射。reflect 包會幫助識別 interface{}變數的底層具體型別和具體值。這正是我們所需要的。createQuery 函式接收 interface{} 引數,根據它的具體型別和具體值,建立 SQL 查詢。這正是 reflect 包能夠幫助我們的地方。

在編寫我們通用的查詢生成器之前,我們首先需要了解 reflect 包中的幾種型別和方法。讓我們來逐個瞭解。

reflect.Type 和 reflect.Value

reflect.Type 表示 interface{} 的具體型別,而 reflect.Value 表示它的具體值。reflect.TypeOf()reflect.ValueOf() 兩個函式可以分別返回 reflect.Typereflect.Value。這兩種型別是我們建立查詢生成器的基礎。我們現在用一個簡單的例子來理解這兩種型別。

Copy
package main

import (
    "fmt"
    "reflect"
)

type order struct {
    ordId      int
    customerId int
}

func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    v := reflect.ValueOf(q)
    fmt.Println("Type ", t)
    fmt.Println("Value ", v)


}
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)

}

在上面的程式中,第 13 行的 createQuery 函式接收 interface{} 作為引數。在第 14 行,reflect.TypeOf 接收了引數 interface{},返回了reflect.Type,它包含了傳入的 interface{} 引數的具體型別。同樣地,在第 15 行,reflect.ValueOf函式接收引數 interface{},並返回了 reflect.Value,它包含了傳來的 interface{} 的具體值。

上述程式會列印:

Copy
Type  main.order
Value  {456 56}

從輸出我們可以看到,程式列印了介面的具體型別和具體值。

relfect.Kind

reflect 包中還有一個重要的型別:Kind

在反射包中,KindType 的型別可能看起來很相似,但在下面程式中,可以很清楚地看出它們的不同之處。

Copy
package main

import (
    "fmt"
    "reflect"
)

type order struct {
    ordId      int
    customerId int
}

func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    k := t.Kind()
    fmt.Println("Type ", t)
    fmt.Println("Kind ", k)


}
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)

}

上述程式會輸出:

Copy
Type  main.order
Kind  struct

我想你應該很清楚兩者的區別了。Type 表示 interface{} 的實際型別(在這裡是 main.Order),而 Kind 表示該型別的特定類別(在這裡是 struct)。

NumField() 和 Field() 方法

NumField()方法返回結構體中欄位的數量,而 Field(i int)方法返回欄位 ireflect.Value

Copy
package main

import (
    "fmt"
    "reflect"
)

type order struct {
    ordId      int
    customerId int
}

func createQuery(q interface{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        v := reflect.ValueOf(q)
        fmt.Println("Number of fields", v.NumField())
        for i := 0; i < v.NumField(); i++ {
            fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
        }
    }

}
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
}

在上面的程式中,因為 NumField 方法只能在結構體上使用,我們在第 14 行首先檢查了 q 的類別是 struct。程式的其他程式碼很容易看懂,不作解釋。該程式會輸出:

Copy
Number of fields 2
Field:0 type:reflect.Value value:456
Field:1 type:reflect.Value value:56

Int() 和 String() 方法

IntString可以幫助我們分別取出 reflect.Value 作為 int64string

Copy
package main

import (
    "fmt"
    "reflect"
)

func main() {
    a := 56
    x := reflect.ValueOf(a).Int()
    fmt.Printf("type:%T value:%v\n", x, x)
    b := "Naveen"
    y := reflect.ValueOf(b).String()
    fmt.Printf("type:%T value:%v\n", y, y)

}

在上面程式中的第 10 行,我們取出 reflect.Value,並轉換為 int64,而在第 13 行,我們取出 reflect.Value 並將其轉換為 string。該程式會輸出:

Copy
type:int64 value:56
type:string value:Naveen

完整的程式

現在我們已經具備足夠多的知識,來完成我們的查詢生成器了,我們來實現它把。

Copy
package main

import (
    "fmt"
    "reflect"
)

type order struct {
    ordId      int
    customerId int
}

type employee struct {
    name    string
    id      int
    address string
    salary  int
    country string
}

func createQuery(q interface{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        t := reflect.TypeOf(q).Name()
        query := fmt.Sprintf("insert into %s values(", t)
        v := reflect.ValueOf(q)
        for i := 0; i < v.NumField(); i++ {
            switch v.Field(i).Kind() {
            case reflect.Int:
                if i == 0 {
                    query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
                } else {
                    query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
                }
            case reflect.String:
                if i == 0 {
                    query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
                } else {
                    query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
                }
            default:
                fmt.Println("Unsupported type")
                return
            }
        }
        query = fmt.Sprintf("%s)", query)
        fmt.Println(query)
        return

    }
    fmt.Println("unsupported type")
}

func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)

    e := employee{
        name:    "Naveen",
        id:      565,
        address: "Coimbatore",
        salary:  90000,
        country: "India",
    }
    createQuery(e)
    i := 90
    createQuery(i)

}

在第 22 行,我們首先檢查了傳來的引數是否是一個結構體。在第 23 行,我們使用了 Name() 方法,從該結構體的 reflect.Type 獲取了結構體的名字。接下來一行,我們用 t 來建立查詢。

在第 28 行,case 語句檢查了當前欄位是否為 reflect.Int,如果是的話,我們會取到該欄位的值,並使用 Int() 方法轉換為 int64。if else 語句用於處理邊界情況。請新增日誌來理解為什麼需要它。在第 34 行,我們用來相同的邏輯來取到 string

我們還作了額外的檢查,以防止 createQuery 函式傳入不支援的型別時,程式發生崩潰。程式的其他程式碼是自解釋性的。我建議你在合適的地方新增日誌,檢查輸出,來更好地理解這個程式。

該程式會輸出:

Copy
insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type

至於向輸出的查詢中新增欄位名,我們把它留給讀者作為練習。請嘗試著修改程式,打印出以下格式的查詢。

Copy
insert into order(ordId, customerId) values(456, 56)

我們應該使用反射嗎?

我們已經展示了反射的實際應用,現在考慮一個很現實的問題。我們應該使用反射嗎?我想引用 Rob Pike關於使用反射的格言,來回答這個問題。

清晰優於聰明。而反射並不是一目瞭然的。

反射是 Go 語言中非常強大和高階的概念,我們應該小心謹慎地使用它。使用反射編寫清晰和可維護的程式碼是十分困難的。你應該儘可能避免使用它,只在必須用到它時,才使用反射。