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 :
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:
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())
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.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:
bvt
Sebagai contoh:
y := v.Interface().(float64)
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)
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 .