Jenis Data Generik
Kami menggunakan generik untuk membuat definisi deklarasi item, seperti structs dan functions, yang kemudian dapat kita gunakan dengan banyak jenis data konkret yang berbeda. Di Cairo, kita dapat menggunakan generik saat mendefinisikan functions, structs, enums, traits, implementasi, dan metode! Pada bab ini, kita akan melihat bagaimana cara menggunakan tipe generik secara efektif dengan semua hal tersebut.
Fungsi Generik
Saat mendefinisikan fungsi yang menggunakan generik, kita menempatkan generik pada tanda tangan fungsi, di mana kita biasanya menentukan jenis data dari parameter dan nilai kembali. Sebagai contoh, bayangkan kita ingin membuat sebuah fungsi yang, dengan dua Array
item, akan mengembalikan yang terbesar. Jika kita perlu melakukan operasi ini untuk daftar jenis yang berbeda, maka kita harus mendefinisikan kembali fungsi tersebut setiap kali. Untungnya, kita dapat mengimplementasikan fungsi ini sekali menggunakan generik dan melanjutkan ke tugas lain.
{{#include ../listings/ch08-generic-types-and-traits/no_listing_01_missing_tdrop/src/lib.cairo}}
Fungsi largest_list
membandingkan dua daftar dengan tipe yang sama dan mengembalikan yang memiliki lebih banyak elemen serta menghapus yang lain. Jika Anda mengompilasi kode sebelumnya, Anda akan melihat bahwa itu akan gagal dengan kesalahan yang mengatakan bahwa tidak ada traits yang ditentukan untuk menghapus array dari jenis generik. Hal ini terjadi karena kompiler tidak memiliki cara untuk menjamin bahwa Array<T>
dapat dihapus saat menjalankan fungsi main
. Untuk menghapus array dari T
, kompiler harus tahu cara menghapus T
. Ini dapat diperbaiki dengan menentukan dalam tanda tangan fungsi largest_list
bahwa T
harus mengimplementasikan trait drop. Definisi fungsi largest_list
yang benar adalah sebagai berikut:
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_02_with_tdrop/src/lib.cairo}}
Fungsi largest_list
baru mencakup dalam definisinya persyaratan bahwa jenis generik apa pun yang ditempatkan di sana, itu harus dapat dihapus. Fungsi main
tetap tidak berubah, kompiler cukup cerdas untuk menyimpulkan jenis konkret apa yang digunakan dan apakah itu mengimplementasikan trait Drop
.
Kendala untuk Jenis Generik
Saat mendefinisikan jenis generik, berguna untuk memiliki informasi tentang mereka. Mengetahui trait mana yang diimplementasikan oleh jenis generik memungkinkan kita menggunakan mereka secara lebih efektif dalam logika fungsi dengan biaya membatasi jenis generik yang dapat digunakan dengan fungsi tersebut. Kita melihat contoh ini sebelumnya dengan menambahkan implementasi TDrop
sebagai bagian dari argumen generik dari largest_list
. Sementara TDrop
ditambahkan untuk memenuhi persyaratan kompiler, kita juga dapat menambahkan kendala untuk memperkaya logika fungsi kita.
Bayangkan kita ingin, diberikan sebuah daftar elemen dari suatu jenis generik T
, mencari elemen terkecil di antara mereka. Awalnya, kita tahu bahwa untuk elemen jenis T
dapat dibandingkan, itu harus mengimplementasikan trait PartialOrd
. Fungsi yang dihasilkan akan menjadi:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_03_missing_tcopy/src/lib.cairo}}
Fungsi smallest_element
menggunakan jenis generik T
yang mengimplementasikan trait PartialOrd
, mengambil snapshot dari Array<T>
sebagai parameter, dan mengembalikan salinan elemen terkecil. Karena parameter berjenis @Array<T>
, kita tidak perlu lagi menghapusnya pada akhir eksekusi dan oleh karena itu tidak memerlukan implementasi trait Drop
untuk T
juga. Mengapa ini tidak dikompilasi?
Saat mengindeks pada list
, nilai menghasilkan snap dari elemen yang diindeks, kecuali jika PartialOrd
diimplementasikan untuk @T
, kita perlu melepaskan elemen tersebut menggunakan *
. Operasi *
memerlukan salinan dari @T
ke T
, yang berarti bahwa T
harus mengimplementasikan trait Copy
. Setelah menyalin elemen bertipe @T
menjadi T
, sekarang ada variabel dengan tipe T
yang perlu dihapus, memerlukan T
untuk mengimplementasikan trait Drop
juga. Kita harus menambahkan implementasi trait Drop
dan Copy
untuk fungsi menjadi benar. Setelah memperbarui fungsi smallest_element
, kode yang dihasilkan akan menjadi:
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_04_with_tcopy/src/lib.cairo}}
Parameter Implementasi Generik Anonim (+
operator)
Sampai sekarang, kita selalu menentukan nama untuk setiap implementasi trait generik yang diperlukan: TPartialOrd
untuk PartialOrd<T>
, TDrop
untuk Drop<T>
, dan TCopy
untuk Copy<T>
.
Namun, sebagian besar waktu, kita tidak menggunakan implementasi dalam tubuh fungsi; kita hanya menggunakannya sebagai batasan. Dalam kasus-kasus ini, kita dapat menggunakan operator +
untuk menentukan bahwa jenis generik harus mengimplementasikan suatu trait tanpa memberi nama implementasinya. Ini disebut sebagai parameter implementasi generik anonim.
Sebagai contoh, +PartialOrd<T>
setara dengan impl TPartialOrd: PartialOrd<T>
.
Kita dapat menulis ulang tanda tangan fungsi smallest_element
sebagai berikut:
{{#rustdoc_include ../listings/ch08-generic-types-and-traits/no_listing_05_with_anonymous_impl/src/lib.cairo:1}}
Structs
Kita juga dapat mendefinisikan structs untuk menggunakan parameter jenis generik untuk satu atau lebih field menggunakan sintaks <>
, mirip dengan definisi fungsi. Pertama, kita mendeklarasikan nama parameter jenis di dalam tanda kurung sudut setelah nama struct. Kemudian kita menggunakan jenis generik dalam definisi struct di mana kita seharusnya menentukan jenis data konkret. Contoh kode berikut menunjukkan definisi Wallet<T>
yang memiliki field balance
bertipe T
.
{{#include ../listings/ch08-generic-types-and-traits/no_listing_06_derive_generics/src/lib.cairo}}
Kode di atas menghasilkan trait Drop
untuk tipe Wallet
secara otomatis. Ini setara dengan menulis kode berikut:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_07_drop_explicit/src/lib.cairo}}
Kita menghindari menggunakan macro derive
untuk implementasi Drop
dari Wallet
dan sebaliknya menentukan implementasi WalletDrop
kita sendiri. Perhatikan bahwa kita harus mendefinisikan, seperti fungsi, jenis generik tambahan untuk WalletDrop
yang menyatakan bahwa T
juga mengimplementasikan trait Drop
. Kita basically mengatakan bahwa struct Wallet<T>
dapat dihapus selama T
juga dapat dihapus.
Terakhir, jika kita ingin menambahkan field ke Wallet
yang mewakili Addressnya dan kita ingin field tersebut berbeda dari T
tetapi juga generik, kita dapat dengan mudah menambahkan jenis generik lain di antara <>
:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_08_two_generics/src/lib.cairo}}
Kita menambahkan definisi jenis generik U
ke dalam struct Wallet
dan kemudian mengassign jenis ini ke member field address
yang baru. Perhatikan bahwa atribut derive
untuk trait Drop
juga berfungsi untuk U
.
Enums
Seperti yang kita lakukan dengan structs, kita dapat mendefinisikan enums untuk menyimpan jenis data generik dalam variasi mereka. Contohnya adalah enum Option<T>
yang disediakan oleh perpustakaan inti Cairo:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_09_option/src/lib.cairo}}
Enum Option<T>
adalah generik terhadap jenis T
dan memiliki dua variasi: Some
, yang menyimpan satu nilai bertipe T
, dan None
yang tidak menyimpan nilai apa pun. Dengan menggunakan enum Option<T>
, kita dapat menyatakan konsep abstrak dari nilai opsional dan karena nilai tersebut memiliki jenis generik T
, kita dapat menggunakan abstraksi ini dengan jenis apa pun.
Enums juga dapat menggunakan beberapa jenis generik, seperti definisi enum Result<T, E>
yang disediakan oleh perpustakaan inti:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_10_result/src/lib.cairo}}
Enum Result<T, E>
memiliki dua jenis generik, T
dan E
, dan dua variasi: Ok
yang menyimpan nilai bertipe T
dan Err
yang menyimpan nilai bertipe E
. Definisi ini memudahkan penggunaan enum Result
di mana pun kita memiliki operasi yang mungkin berhasil (dengan mengembalikan nilai bertipe T
) atau gagal (dengan mengembalikan nilai bertipe E
).
Metode Generik
Kita dapat mengimplementasikan metode pada structs dan enums, dan menggunakan jenis generik dalam definisinya juga. Dengan menggunakan definisi sebelumnya dari struct Wallet<T>
, kita mendefinisikan metode balance
untuknya:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_11_generic_methods/src/lib.cairo}}
Pertama, kita mendefinisikan trait WalletTrait<T>
menggunakan jenis generik T
yang mendefinisikan metode yang mengembalikan snapshot dari field balance
dari Wallet
. Kemudian kita memberikan implementasi untuk trait tersebut pada WalletImpl<T>
. Perhatikan bahwa Anda perlu menyertakan jenis generik di kedua definisi trait dan implementasi.
Kita juga dapat menentukan kendala pada jenis generik saat mendefinisikan metode pada jenis tersebut. Kita bisa, misalnya, mengimplementasikan metode hanya untuk instance Wallet<u128>
daripada Wallet<T>
. Pada contoh kode, kita mendefinisikan implementasi untuk Wallet yang memiliki tipe konkret u128
untuk field balance
.
{{#include ../listings/ch08-generic-types-and-traits/no_listing_12_constrained_generics/src/lib.cairo}}
Metode baru receive
menambah ukuran saldo dari setiap instance Wallet<u128>
. Perhatikan bahwa kita mengubah fungsi main
membuat w
menjadi variabel yang dapat diubah agar dapat memperbarui saldo. Jika kita mengubah inisialisasi w
dengan mengubah tipe balance
, kode sebelumnya tidak akan dikompilasi.
Cairo memungkinkan kita mendefinisikan metode generik di dalam trait generik juga. Menggunakan implementasi sebelumnya dari Wallet<U, V>
, kita akan mendefinisikan sebuah trait yang memilih dua Wallet dari jenis generik yang berbeda dan membuat yang baru dengan jenis generik masing-masing. Pertama, mari kita menulis ulang definisi struct:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_13_not_compiling/src/lib.cairo:struct}}
Selanjutnya, kita akan secara naif mendefinisikan trait dan implementasinya:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_13_not_compiling/src/lib.cairo:trait_impl}}
Kita membuat trait WalletMixTrait<T1, U1>
dengan metode mixup<T2, U2>
yang diberikan sebuah instance Wallet<T1, U1>
dan Wallet<T2, U2>
membuat sebuah Wallet<T1, U2>
baru. Seperti yang dijelaskan pada tanda tangan mixup
, baik self
maupun other
akan di-drop pada akhir fungsi, itulah alasan mengapa kode ini tidak akan dikompilasi. Jika Anda telah mengikuti dari awal hingga sekarang, Anda akan tahu bahwa kita harus menambahkan persyaratan untuk semua jenis generik tersebut, menentukan bahwa mereka akan mengimplementasikan trait Drop
agar kompiler tahu cara menjatuhkan instance Wallet<T, U>
. Implementasi yang diperbarui adalah sebagai berikut:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_14_compiling/src/lib.cairo:trait_impl}}
Kita menambahkan persyaratan untuk T1
dan U1
agar dapat di-drop pada deklarasi WalletMixImpl
. Kemudian kita lakukan hal yang sama untuk T2
dan U2
, kali ini sebagai bagian dari tanda tangan mixup
. Sekarang kita dapat mencoba fungsi mixup
:
{{#include ../listings/ch08-generic-types-and-traits/no_listing_14_compiling/src/lib.cairo:main}}
Pertama, kita membuat dua instance: satu dari Wallet<bool, u128>
dan yang lainnya dari Wallet<felt252, u8>
. Kemudian, kita memanggil mixup
dan membuat instance baru Wallet<bool, u8>
.