1. 程式人生 > >反射規則 the law of reflection

反射規則 the law of reflection

在看golang.org上一篇《the law of flection》,寫點自己的理解和筆記。

翻譯完以後還是感覺看原始的文章比較大。原因有以下幾點:

  • 很多名字找不到一個合適的翻譯,比如settable
  • 個別句子讀原句時能理解意思,但是不知道怎麼合適的組織成漢字
  • 英語的句子結構和中文不同,在翻譯的時候會不自居的用英語的結構寫出中文來,讓很多的句子讀起來難以理解
  • 文章中很多地方用詞嚴謹,特別是一些專有名詞,只是一味的翻譯成中文,會讓人混淆。比如value,很多時候不知該理解成是變數還是值。還有很多類似item,storage,it等等,實在很難找一個合適的指代。
所以這篇文章只是更多的作為參考。

型別和介面(type and interfaces)

因為介面是基於型別系統的(type system),所以首先看下一下go的型別

go是靜態型別的。每個變數都有一個靜態的型別(static type),也就是說一種型別在編譯的時候被宣告和確定了(one type known and fixed at complie time)。如果我們定義:

type MyInt int


var i int
var j MyInt

i的型別是int,j的型別是MyInt,i和j的型別是不一樣的。儘管他們有著相同的底層型別(?)(underlying type),但是它們是不能在不轉換的情況下互相賦值的。

型別裡面一個重要的分類是介面型別(interface types),它表示一系列的固定方法。一個介面變數可以儲存任何實現這個結構的實際變數(? concrete type)。一個出名的例子就是io包裡的io.Reader和io.Writer:

// Reader is the interface that wraps the basic Read method.
type Reader interface {
    Read(p []byte) (n int, err error)
}


// Writer is the interface that wraps the basic Write method.
type Writer interface {
    Write(p []byte) (n int, err error)
}

任何型別以上面的特徵(? with this signature)實現了Read(或者 Writer)方法,就可以說該型別實現了io.Read(或者io.Writer)。這就意味著介面io.Read型別的變數可以存放任何有Read方法的變數:
 var r io.Reader
 r = os.Stdin
 r = bufio.NewReader(r)
 r = new(bytes.Buffer)
 // and so on

有一點要搞清楚,不管r裡面存放的具體值是什麼,r的型別一直都是io.Read:go是靜態型別的語言,r的靜態型別是io.Read。

一個關於介面的非常重要的例子就是空介面:

interface{}

上面代表一個空的方法集,可以存放任何資料型別。因為每個型別都有零個或者更多的方法。

有人說go的介面是動態型別(dynamically typed),這是一種誤導。他們其實是靜態的型別:一個介面型別的變數一直都有同樣的靜態型別(static type),雖然在執行的時候存放在介面型別變數裡的值可能會變,但是那些值都會是實現過這個介面的。

我們需要嚴謹對待上面這些概念,因為反射和介面是緊密相連的。

關於介面的說明(the representation of an interface)

Russ Cox寫過一篇博文,是關於go語言裡的介面型別的。在這裡簡單的介紹一下:

一個介面型別的變數是成對儲存的(stores a pair):分配給變數的具體值(concrete value)和該值的型別描述(value's type descriptor)。更準確說,這個值是實現了該介面的底層具體資料(? the underlaying concrete data item),型別(type)描述表示這個資料的所有型別資訊(the type describes the full type of that item)。有例為證:

var r io.Reader
    tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
    if err != nil {
        return nil, err
    }
    r = tty

r包含了值對(the pair),【value,type】,【tty,*os.File】。這裡要注意型別*os.File繼承了不止read一個方法。儘管這裡的介面型別只提供Read一種方法,但是值(value)裡面帶有這個關於這個數值的所有資訊。這就是為什麼我們這麼做:
var w io.Writer
w = r.(io.Writer)

這裡的賦值表示式是一個型別宣告(type assertion)。這個所聲明裡,r中的元素同時也繼承了io.Writer,所以我們可以賦值給w。賦值以後,w就會存有值對(pair):【tty,*os.File】。和存放在r中的值對是一樣的。介面的靜態型別要求介面變數只能使用接口裡所定義的方法,儘管介面變數中存放的值(value)可能擁有大量額方法。

繼續上面的例子,我們還可以這麼做:

var empty interface{}
empty = w

這的空介面變數,empty,一樣的包含有同上面相同的值對(pair),【tty,*os.File】。也就是說一個空的介面能存放所有的變數,包含有關於這個變數我們所需要的所有資訊。

(我們這不需要一個型別宣告,是因為我們都知道w是符合空介面的。在這個例子裡,我們把Read轉換成Writer,我們是需要特別的型別宣告的,這是因為Writer的方法不是Reader方法的子集。)

注意一個重要的細節,存放在介面變數中的pai存放的型別一直都是【value,concrete type】,而不是【value,interface type】。介面中不儲存介面變數。

ok,我們現在可以瞭解一下反射了。

反射的第一法則(the first law of reflection)

1、從介面變數到反射物件的反射(reflection goes from interface value to reflection object)

簡單的說,反射只是檢查存放在一個介面變數裡的型別和值對(type and value pair)的機制。要了解這些,我們需要知道reflect包裡的兩個型別:Type和Value。這個兩個型別提供了訪問結構變數裡內容的途徑,兩個函式,reflect.TypeOf和reflect.ValueOf,可以獲得從一個介面資料裡面獲取reflect.Type和reflect.Value。(當然從reflect.Value裡面也能很很容易的獲得reflect.Type,但是我們現在暫時把Value和Type的概念區分開)

讓我們先來看看TypeOf:

package main


import (
    "fmt"
    "reflect"
)


func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

程式輸出是:
type: float64

你可能會好奇,這裡哪有介面?程式裡裡面我們傳遞給reflect.TypeOf的x變數時float64,而不是介面啊。但是更具godoc裡顯示,reflect.TypeOf的聲明裡包含有一個空的介面:
// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

當我們呼叫reflect.TypeOf(x)時,x首先被存放在一個空的介面中,然後被當成引數傳遞過去。reflect.Typeof拆開(unpacks)這個空的介面,獲取型別的資訊(type information)。

那麼reflect.ValueOf函式自然是用來獲取值(value)的(這裡我們忽略樣板(? boilerplate),先直關注可執行的程式碼):

 var x float64 = 3.4
    fmt.Println("value:", reflect.ValueOf(x))
程式輸出:
value: <float64 Value>

reflect.Type和reflect.ValueOf都擁有很多的方法供我們使用。一個非常重要的例子就是Value有一個Type的方法,這個方法可以從refelct.Value中獲得Type。另外一個Type和Value都有的方法是Kind,這個方法可以返回一個常量說明(? a constant indicating),類似:Uint和Float64等等。同時Value裡賣弄還有類似Int和Float的函式,能讓我們獲取其中獲取的值:
    var x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())
    fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
    fmt.Println("value:", v.Float())

程式輸出:
type: float64
kind is float64: true
value: 3.4

這裡還有類似SetInt和SetFloat的方法,但是使用這些方法前我們需要了解settability(?)這個是下面第三部分才能談到的,待會再討論。

反射庫裡面有兩個屬性需要特別指出來。第一,為了讓api簡單,Value的setter和getter方法都是基於可以用來儲存資料的最大資料型別的:比如所有的有符號整形操作都是基於int64的。也就是有,Value的Int方法返回一個int64的資料,而SetInt的方法獲取的是一個int64。用實際的例子裡說明:

    var x uint8 = 'x'
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())                            // uint8.
    fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
    x = uint8(v.Uint())                                       // v.Uint returns a uint64.

第二是屬性,反射物件的Kind方法描述的是底層型別(underlying type),不是靜態型別。如果一個反射物件包含有一個使用者自定義型別,如下:
    type MyInt int
    var x MyInt = 7
    v := reflect.ValueOf(x)
v的Kind方法返回值依舊是reflect.Int,儘管x的靜態型別是MyInt而不是int。也就是說,Kind方法不能識別基於int的MyInt型別,儘管Type是可以的。

反射的第二法則(the second law of reflection)

2、從反射物件到介面變數的反射(reflection goes from reflection object to interface value)

就像物理世界中的反射,Go中的反射也有自己反轉(inverse)。

給我一個reflect.Value,我們就可以通過Interface這個方法重新得到一個介面。實際上,這個方法吧type和value資訊打包成一個介面引用,並且返回:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

因此我們可以這麼說:
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)
把有物件v所表示的float64的值答應出來了。

我們還可以加以改進。fmt.Println,fmt.Printf等等函式的引數都是以一個空的介面變數傳遞過去的,也就是說就像我們在之前的例子裡一樣,這個引數在fmt包裡被解包(unpacked)了。所以只要給上面的函式直接傳遞Interface方法的返回值就能正確的輸出內容:

fmt.Println(v.Interface())

(為什麼不直接使用fmt.Println(v)?這是因為v其實是一個reflect.Value;我們需要的是裡面儲存的確定的值(? concrete value))。既然我們的值的型別是float64,我們甚至可以使用浮點數型別:
fmt.Printf("value is %7.1e\n", v.Interface())

然後我們得到了:
3.4e+00

再說一遍,我們不需要把v.Interface的結果型別轉換(? type-assert)。這個空的介面值李阿敏包含有具體資料的型別資訊,Printf可以從中獲取的到。

簡單的說,Interface是函式ValueOf的反轉,只可惜的是它的結果都是靜態型別interface{}

小結:從介面反射到反射變數,然後又反射回來。

反射的第三法則(the third law of reflection)

3、要想修改一個反射物件,值必須是可設定的(to modify a reflection object,the value must be settable)

第三條法則是很為微妙(subtle)和讓人困惑的(confusing),但是我們從第一法則(principles)來看的話的,也是很容搞懂的。

下面這些程式碼無法執行,但是值得我們學習:

 var x float64 = 3.4
 v := reflect.ValueOf(x)
 v.SetFloat(7.1) // Error: will panic

如果你運行了這段程式碼,會觸發panic,產生一段含混不清的提示:
panic: reflect.Value.SetFloat using unaddressable value

其實問題並不是常量7.1無法定址,是因為v是不可設定的(not settable)。可設定(settableility)是反射Value的一個屬性,但是不是所有的反射Value都有這個屬性。

Value的CanSet方法可以判斷Value可不可設定,如下面的例子:

    var x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("settability of v:", v.CanSet())

輸出:
settability of v: false

對一個不可設定(non-settable)的Value使用set就會報錯。那麼什麼是可設定性(settabiliy)?

可設定性有點像可定址(addressability),但是更加嚴格,它是表示一個反射物件可不可修改建立這個反射物件原始儲存資料(storage)的屬性

。可設定性取決於反射物件中儲存的是不是原始的資料。當我們輸入一下程式碼:

    var x float64 = 3.4
    v := reflect.ValueOf(x)

我們傳遞給reflection.ValueOf的是一個x的拷貝,所以作為reflect.ValueOf的引數用來反射物件的介面,其實x的拷貝,而不是x本身。因此如果下面的程式碼:
v.SetFloat(7.1)

是允許執行成功的,也不睡更新x的,儘管看起來v是由x建立的一樣。其實這樣操作會更新x的拷貝在記憶體中的值,而x本身是不會受到影響的。這樣的話會顯得讓人困惑,而且一點用的都沒有。所以這種方法就被規定為非法的,可設定屬性是不允許這麼操作的。

可能這些看起來有些怪異(bizarre),但其實是很簡單的。這其實是一種很常見的情況,只是有個不同的外在而已。想想當我們把x傳遞給一個函式的時候:
f(x)
因為我們傳遞給函式的是x的拷貝而不是x的本身,所以我們知道f不能修改x的值。如果我們想f能修改x的值,那麼我們就必須給這個函式傳遞x的的地址了(也就是x的指標)
f(&x)

這樣看起來就顯得很簡單很熟悉了,反射也是同樣的運作方式的。如果我們也想讓反射修改x的話,我們也必須給反射庫(reflection library)一個我們想修改的值得指標。

讓我們這操作一下試試。首先我們像往常一樣初始化x,然後建立一個指向x的反射變數p:

    var x float64 = 3.4
    p := reflect.ValueOf(&x) // Note: take the address of x.
    fmt.Println("type of p:", p.Type())
    fmt.Println("settability of p:", p.CanSet())

那麼輸出就是:
type of p: *float64
settability of p: false

p這個反射是不可設定的,但是p也也不是我想設定的資料,我們要設定的實際是*p。想要得到p所指向的資料,我們使用Value的Elem方法。這個方法可以通過指標間接訪問資料,並且儲存在反射Value的變數v中:
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

現在v就是一個科設定的反射物件了,有輸出為證;
settability of v: true

既然v代表的是x,我們終於可以使用v.SetFloat修改x的值了:
    v.SetFloat(7.1)
    fmt.Println(v.Interface())
    fmt.Println(x)

如我們預測的一樣,輸出是:
7.1
7.1

儘管反射執行著語言的功能,通過反射中Type和Values可以掩蓋住具體的細節,但是可能還是難以去理解。你只需要記住了,Values需要一個數據的地址才能修改反射所代表的資料。

結構體(Structs)

在我們之前的例子裡,v並不是指標本身,它只是指標的派生而已。這種方法適用於用反射來修改結構體的域(field)。只要我們有這個結構體的體質,我們就能修改修改這個結構體的域。

這裡有一個簡單額例子來分析一下一個結構體變數t。因為我們待會要修改這個結構體,所以我們先用結構體的指標建立反射物件。我們用typeOf儲存它的型別,用直接的方法(? straightforward method calls)迭代出所有的域【參考反射包獲取更多的細節】。注意我們這裡匯出了結構體域的名字,但是域本身還是reflect.Value物件的:

    type T struct {
        A int
        B string
    }
    t := T{23, "skidoo"}
    s := reflect.ValueOf(&t).Elem()
    typeOfT := s.Type()
    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f.Type(), f.Interface())
    }

程式的輸出是:
0: A int = 23
1: B string = skidoo

需要注意的是我們這裡使用的可設定性(? there's one more point about settability introduced in passing here):這裡把T的域名(field name)匯出來,是因為一個結構體只有匯出的域才能設定。

因為s中儲存著一個可設定的反射物件,所以我們可以修改結構體的域:

    s.Field(0).SetInt(77)
    s.Field(1).SetString("Sunset Strip")
    fmt.Println("t is now", t)

結果就是:
 is now {77 Sunset Strip}

如果你把程式改成s是由t建立的而不是&t,那麼在你呼叫SetInt和SetString時程式就報錯,因為此時t的域是不可修改的。

小結(conclusion)

反射的規則:
  1. 由介面到反射變數的反射
  2. 由反射物件到介面的反射
  3. 要修改一個反射物件,value必須是可設定的
一旦說你瞭解了這些規則,儘管難以理解,但是Go會變的更容易使用。這是一個強大的工具,在使用的時候要注意,在不是絕對必要的必要的時候不要使用。

關於反射我們還有很多沒有涉及——在channels中的傳送和接收、分配記憶體、使用slice和map、呼叫方法和函式——但是這篇文章內容已經足夠多了。我們會在後面文章裡涉及這些內容中的部分。