Hukum Refleksi dalam Go

Halo, Habr! Saya mempersembahkan kepada Anda terjemahan artikel "The Laws of Reflection" dari pencipta bahasa.

Refleksi adalah kemampuan suatu program untuk mengeksplorasi strukturnya sendiri, terutama melalui tipe. Ini adalah bentuk metaprogramming dan sumber kebingungan.
Di Go, refleksi banyak digunakan, misalnya, dalam paket tes dan fmt. Di artikel ini, kami akan mencoba untuk menghilangkan "sihir" dengan menjelaskan cara kerja refleksi di Go.

Jenis dan Antarmuka


Karena refleksi didasarkan pada sistem tipe, mari menyegarkan pengetahuan kita tentang tipe di Go.
Go diketik secara statis. Setiap variabel memiliki satu dan hanya satu tipe statis yang diperbaiki pada waktu kompilasi: int, float32, *MyType, []byte ... Jika kita mendeklarasikan:

 type MyInt int var i int var j MyInt 

maka i adalah tipe int dan j adalah tipe MyInt . Variabel i dan j memiliki tipe statis yang berbeda dan, meskipun mereka memiliki tipe dasar yang sama, mereka tidak dapat ditugaskan satu sama lain tanpa konversi.

Salah satu kategori jenis penting adalah antarmuka, yang merupakan set metode tetap. Antarmuka dapat menyimpan nilai spesifik (non-antarmuka) selama nilai ini mengimplementasikan metode antarmuka. Sepasang contoh yang terkenal adalah io.Reader dan io.Writer , tipe Reader dan Writer dari paket io :

 // Reader -  ,    Read(). type Reader interface { Read(p []byte) (n int, err error) } // Writer -  ,    Write(). type Writer interface { Write(p []byte) (n int, err error) } 

Dikatakan bahwa setiap jenis yang mengimplementasikan metode Read() atau Write() dengan tanda tangan ini mengimplementasikan masing-masing io.Reader atau io.Writer . Ini berarti bahwa variabel tipe io.Reader dapat berisi nilai apa pun dari tipe Read ():

 var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer) 

Penting untuk memahami bahwa r dapat diberi nilai apa pun yang mengimplementasikan io.Reader . Go diketik secara statis, dan tipe statis r adalah io.Reader .

Contoh yang sangat penting dari jenis antarmuka adalah antarmuka kosong:

 interface{} 

Ini adalah set kosong metode ∅ dan diimplementasikan dengan nilai apa pun.
Beberapa mengatakan antarmuka Go adalah variabel yang diketik secara dinamis, tetapi ini adalah kesalahan. Mereka diketik secara statis: variabel dengan tipe antarmuka selalu memiliki tipe statis yang sama, dan meskipun pada saat dijalankan nilai yang disimpan dalam variabel antarmuka dapat mengubah jenisnya, nilai ini akan selalu memenuhi antarmuka. (Tidak ada yang undefined , NaN atau hal-hal lain yang merusak logika program.)

Ini harus dipahami - refleksi dan antarmuka terkait erat.

Representasi internal antarmuka


Russ Cox menulis posting blog terperinci tentang pengaturan antarmuka di Go. Artikel yang tak kalah bagus tentang Habr'e . Tidak perlu mengulangi keseluruhan cerita, poin-poin utama disebutkan.

Variabel tipe antarmuka memegang pasangan: nilai spesifik yang ditetapkan untuk variabel, dan deskriptor tipe untuk nilai itu. Lebih tepatnya, nilainya adalah elemen data dasar yang mengimplementasikan antarmuka, dan tipe menggambarkan tipe lengkap elemen ini. Misalnya setelah

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

r berisi, secara skematis, pasangan (, ) --> (tty, *os.File) . Perhatikan bahwa *os.File jenis mengimplementasikan metode selain Read() ; bahkan jika nilai antarmuka hanya menyediakan akses ke metode Baca (), nilai di dalamnya membawa semua informasi tentang jenis nilai ini. Inilah mengapa kita dapat melakukan hal-hal seperti itu:

 var w io.Writer w = r.(io.Writer) 

Ekspresi dalam penugasan ini adalah pernyataan tipe; ia mengklaim bahwa elemen di dalam r juga mengimplementasikan io.Writer , dan karenanya kita dapat menugaskannya ke w . Setelah ditetapkan, w akan berisi pasangan (tty, *os.File) . Ini adalah pasangan yang sama dengan di r . Tipe statis dari antarmuka menentukan metode mana yang dapat dipanggil pada variabel antarmuka, meskipun serangkaian metode yang lebih luas dapat memiliki nilai spesifik di dalamnya.

Melanjutkan, kita dapat melakukan hal berikut:

 var empty interface{} empty = w 

dan nilai kosong dari bidang kosong lagi akan berisi pasangan yang sama (tty, *os.File) . Ini nyaman: antarmuka kosong dapat berisi nilai apa pun dan semua informasi yang kita perlukan darinya.

Kami tidak memerlukan pernyataan tipe di sini, karena diketahui bahwa w memenuhi antarmuka kosong. Dalam contoh di mana kami mentransfer nilai dari Reader ke Writer , kami perlu secara eksplisit menggunakan pernyataan tipe, karena metode Writer bukan merupakan subset dari Reader . Mencoba mengonversi nilai yang tidak cocok dengan antarmuka akan menyebabkan kepanikan.

Satu detail penting adalah bahwa pasangan di dalam antarmuka selalu memiliki formulir (nilai, tipe spesifik) dan tidak dapat memiliki formulir (nilai, antarmuka). Antarmuka tidak mendukung antarmuka sebagai nilai.

Sekarang kita siap untuk belajar refleksi.

Hukum refleksi pertama mencerminkan


  • Refleksi meluas dari antarmuka ke refleksi objek.

Pada tingkat dasar, mencerminkan hanyalah sebuah mekanisme untuk memeriksa sepasang jenis dan nilai yang disimpan dalam variabel antarmuka. Untuk memulai, ada dua jenis yang perlu kita ketahui: reflect.Type dan reflect.Value . Kedua jenis ini menyediakan akses ke konten variabel antarmuka dan masing-masing dikembalikan oleh fungsi sederhana, reflect.TypeOf () dan reflect.ValueOf (). Mereka mengekstrak bagian dari arti antarmuka. (Selain itu, reflect.Value mudah untuk mendapatkan reflect.Type , tetapi jangan gabungkan konsep Value dan Type saat ini.)

Mari kita mulai dengan TypeOf() :

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

Program akan menampilkan
type: float64

Program ini mirip dengan melewatkan variabel float64 x sederhana untuk float64 x reflect.TypeOf() . Apakah Anda melihat antarmuka? Dan itu - reflect.TypeOf() menerima antarmuka kosong, sesuai dengan deklarasi fungsi:

 // TypeOf()  reflect.Type    . func TypeOf(i interface{}) Type 

Ketika kita memanggil reflect.TypeOf(x) , x pertama kali disimpan dalam antarmuka kosong, yang kemudian diteruskan sebagai argumen; reflect.TypeOf() membongkar antarmuka kosong ini untuk mengembalikan informasi jenis.

Fungsi reflect.ValueOf() , tentu saja, mengembalikan nilai (selanjutnya kami akan mengabaikan template dan fokus pada kode):

 var x float64 = 3.4 fmt.Println("value:", reflect.ValueOf(x).String()) 

akan dicetak
value: <float64 Value>
(Kami memanggil metode String() secara eksplisit karena, secara default, paket fmt membongkar untuk reflect.Value dan mencetak nilai tertentu.)
reflect.Type dan reflect.Type keduanya memiliki banyak metode, yang memungkinkan Anda untuk menjelajahi dan memodifikasinya. Salah satu contoh penting adalah reflect.Value memiliki metode Type() yang mengembalikan tipe nilai. reflect.Type dan reflect.Value memiliki metode Kind() yang mengembalikan konstanta yang menunjukkan elemen primitif mana yang disimpan: Uint, Float64, Slice ... Konstanta ini dideklarasikan dalam enumerasi dalam paket mencerminkan. Metode Value dengan nama seperti Int() dan Float() memungkinkan kami untuk mengeluarkan nilai (seperti int64 dan float64) yang terlampir di dalam:

 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()) 

akan dicetak

 type: float64 kind is float64: true value: 3.4 

Ada juga metode seperti SetInt() dan SetFloat() , tetapi untuk menggunakannya kita perlu memahami settability, topik dari hukum refleksi ketiga.

Pustaka refleksi memiliki beberapa properti yang perlu Anda sorot. Pertama, untuk menjaga API tetap sederhana, metode Value "getter" dan "setter" bekerja pada tipe terbesar yang dapat berisi nilai: int64 untuk semua bilangan bulat yang int64 . Yaitu, metode Int() dari nilai Value mengembalikan int64 , dan nilai SetInt() mengambil int64 ; konversi ke tipe aktual mungkin diperlukan:

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

akan

 type: uint8 kind is uint8: true 

Di sini v.Uint() akan mengembalikan uint64 , pernyataan tipe eksplisit diperlukan.

Properti kedua adalah bahwa Kind() mencerminkan objek menggambarkan tipe dasar, bukan tipe statis. Jika objek refleksi berisi nilai tipe integer yang ditentukan pengguna, seperti pada

 type MyInt int var x MyInt = 7 v := reflect.ValueOf(x) // v   Value. 

v.Kind() == reflect.Int , meskipun tipe statis x adalah MyInt , bukan int . Dengan kata lain, Kind() tidak dapat membedakan int dari MyInt , MyInt Type() . Kind hanya dapat menerima nilai tipe bawaan.

Hukum refleksi kedua mencerminkan


  • Refleksi meluas dari objek pantulan ke antarmuka.

Seperti refleksi fisik, pantulan dalam Go menciptakan kebalikannya.

Memiliki reflect.Value , kita dapat mengembalikan nilai antarmuka menggunakan metode Interface() ; Metode ini mengemas informasi jenis dan nilai kembali ke antarmuka dan mengembalikan hasilnya:

 // Interface   v  interface{}. func (v Value) Interface() interface{} 
bvt
Sebagai contoh:

 y := v.Interface().(float64) // y   float64. fmt.Println(y) 

mencetak nilai float64 diwakili oleh objek refleksi v .
Namun, kita dapat melakukan yang lebih baik lagi. Argumen di fmt.Println() dan fmt.Printf() dilewatkan sebagai antarmuka kosong, yang kemudian dibongkar oleh paket fmt secara internal, seperti pada contoh sebelumnya. Oleh karena itu, semua yang diperlukan untuk mencetak konten reflect.Value dengan benar adalah meneruskan hasil dari metode Interface() ke fungsi output yang diformat:

 fmt.Println(v.Interface()) 

(Mengapa tidak fmt.Println(v) ? Karena v adalah tipe reflect.Value ; kami ingin mendapatkan nilai yang terkandung di dalamnya.) Karena nilai kami adalah float64 , kami bahkan dapat menggunakan format floating point jika kami mau:

 fmt.Printf("value is %7.1e\n", v.Interface()) 

akan menampilkan dalam kasus tertentu
3.4e+00

Sekali lagi, tidak perlu v.Interface() tipe hasil v.Interface() di float64 ; nilai antarmuka kosong berisi informasi tentang nilai spesifik di dalamnya, dan fmt.Printf() mengembalikannya.
Singkatnya, metode Interface() adalah kebalikan dari fungsi ValueOf() , kecuali bahwa hasilnya selalu dari interface{} tipe statis interface{} .

Ulangi: Refleksi meluas dari nilai antarmuka ke objek refleksi dan sebaliknya.

Hukum ketiga refleksi refleksi


  • Untuk mengubah objek refleksi, nilainya harus dapat disetel.

Hukum ketiga adalah yang paling halus dan membingungkan. Kami mulai dengan prinsip pertama.
Kode ini tidak berfungsi, tetapi perlu diperhatikan.

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

Jika Anda menjalankan kode ini, kode itu akan macet karena panik dengan pesan penting:
panic: reflect.Value.SetFloat
Masalahnya bukan bahwa 7.1 literal tidak ditangani; inilah yang v tidak dapat diinstal. reflect.Value adalah properti dari reflect.Value , dan tidak setiap reflect.Value memilikinya.
Metode reflect.Value.CanSet() yang ditetapkan; dalam kasus kami:

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

akan dicetak:
settability of v: false

Terjadi kesalahan saat memanggil metode Set() pada nilai yang tidak dikelola. Tapi apa itu instalabilitas?

Keberlanjutan agak mirip dengan addressability, tetapi lebih ketat. Ini adalah properti tempat objek refleksi dapat mengubah nilai yang disimpan yang digunakan untuk membuat objek refleksi. Keberlanjutan ditentukan oleh apakah objek refleksi berisi elemen sumber, atau hanya salinannya. Ketika kita menulis:

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

kami meneruskan salinan x ke reflect.ValueOf() , sehingga antarmuka dibuat sebagai argumen untuk reflect.ValueOf() - ini adalah salinan x , bukan x itu sendiri. Jadi, jika pernyataan:

 v.SetFloat(7.1) 

jika dijalankan, itu tidak akan memperbarui x , meskipun v sepertinya itu dibuat dari x . Sebagai gantinya, ia akan memperbarui salinan x tersimpan di dalam nilai v , dan x itu sendiri tidak akan terpengaruh. Ini dilarang agar tidak menimbulkan masalah, dan instalabilitas adalah properti yang digunakan untuk mencegah masalah.

Ini seharusnya tidak aneh. Ini adalah situasi umum dalam pakaian yang tidak biasa. Pertimbangkan untuk meneruskan x ke fungsi:
f(x)

Kami tidak berharap f() dapat mengubah x , karena kami melewati salinan nilai x , bukan x itu sendiri. Jika kita ingin f() secara langsung mengubah x , kita harus meneruskan sebuah pointer ke x ke fungsi kita:
f(&x)

Ini mudah dan akrab, dan refleksi bekerja dengan cara yang sama. Jika kita ingin mengubah x menggunakan refleksi, kita harus menyediakan pointer perpustakaan dengan nilai yang ingin kita ubah.

Ayo lakukan. Pertama, kita menginisialisasi x seperti biasa, dan kemudian membuat reflect.Value p yang menunjuk ke sana.

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

akan menampilkan
type of p: *float64
settability of p: false


Objek Refleksi p tidak dapat diatur, tetapi bukan p yang ingin kita atur, itu adalah pointer *p . Untuk mendapatkan poin p , kita memanggil metode Value.Elem() , yang mengambil nilai secara tidak langsung melalui pointer, dan menyimpan hasilnya dalam reflect.Value v :

 v := p.Elem() fmt.Println("settability of v:", v.CanSet()) 

Sekarang v adalah objek yang dapat diinstal;
settability of v: true
dan karena itu merepresentasikan x , kita akhirnya bisa menggunakan v.SetFloat() untuk mengubah nilai x :

 v.SetFloat(7.1) fmt.Println(v.Interface()) fmt.Println(x) 

kesimpulan seperti yang diharapkan
7.1
7.1

Refleksi mungkin sulit untuk dipahami, tetapi ia melakukan persis seperti apa bahasa itu, meskipun dengan bantuan reflect.Type dan reflection.Value , yang dapat menyembunyikan apa yang terjadi. Perlu diingat bahwa reflection.Value memerlukan alamat variabel untuk mengubahnya.

Struktur


Dalam contoh kita sebelumnya, v bukan pointer, itu hanya diturunkan darinya. Cara umum untuk membuat situasi ini adalah menggunakan refleksi untuk mengubah bidang struktur. Selama kita memiliki alamat struktur, kita dapat mengubah bidangnya.

Berikut adalah contoh sederhana yang menganalisis nilai struktur t . Kami membuat objek refleksi dengan alamat struktur untuk memodifikasinya nanti. Kemudian atur typeOfT ke tipenya dan lakukan iterasi pada field menggunakan pemanggilan metode sederhana (lihat paket untuk penjelasan terperinci ). Perhatikan bahwa kami mengekstraksi nama bidang dari tipe struktur, tetapi bidang itu sendiri adalah reflect.Value biasa.

 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()) } 

Program akan menampilkan
0: A int = 23
1: B string = skidoo

Satu hal lagi tentang installabilitas ditunjukkan di sini: nama bidang T dalam huruf besar (diekspor), karena hanya bidang yang diekspor yang dapat diatur.
Karena s berisi objek refleksi yang dapat diinstal, kita dapat mengubah bidang struktur.

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

Hasil:
t is now {77 Sunset Strip}
Jika kita mengubah program sehingga s dibuat dari t daripada &t , panggilan ke SetInt() dan SetString() akan berakhir dengan panik, karena bidang t tidak akan dapat diatur.

Kesimpulan


Ingat hukum refleksi:

  • Refleksi meluas dari antarmuka ke refleksi objek.
  • Refleksi meluas dari refleksi objek ke antarmuka.
  • Untuk mengubah objek refleksi, nilai harus ditetapkan.

Diposting oleh Rob Pike .

Source: https://habr.com/ru/post/id415171/


All Articles