Mengapa mendesain bahasa modern tanpa mekanisme penanganan pengecualian?


47

Banyak bahasa modern menyediakan fitur penanganan pengecualian yang kaya , tetapi bahasa pemrograman Apple Swift tidak menyediakan mekanisme penanganan pengecualian .

Tenggelam dalam pengecualian seperti saya, saya mengalami kesulitan membungkus pikiran saya di sekitar apa artinya ini. Swift memiliki asersi, dan tentu saja mengembalikan nilai; tetapi saya mengalami kesulitan membayangkan bagaimana cara berpikir saya yang berbasis pengecualian memetakan ke sebuah dunia tanpa pengecualian (dan, dalam hal ini, mengapa dunia seperti itu diinginkan ). Adakah hal-hal yang tidak dapat saya lakukan dalam bahasa seperti Swift yang bisa saya lakukan dengan pengecualian? Apakah saya mendapatkan sesuatu dengan kehilangan pengecualian?

Bagaimana misalnya saya bisa mengekspresikan sesuatu seperti

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

dalam bahasa (Swift, misalnya) yang tidak memiliki penanganan pengecualian?


11
Anda mungkin ingin menambahkan Pergi ke daftar , jika kita abaikan saja panicyang tidak persis sama. Selain apa yang dikatakan di sana, pengecualian tidak lebih dari cara yang canggih (tapi nyaman) untuk melakukan GOTO, meskipun tidak ada yang menyebutnya seperti itu, untuk alasan yang jelas.
JensG

3
Jenis jawaban untuk pertanyaan Anda adalah bahwa Anda memerlukan dukungan bahasa untuk pengecualian untuk menuliskannya. Dukungan bahasa umumnya mencakup manajemen memori; karena pengecualian dapat dilemparkan ke mana saja dan ditangkap di mana saja, perlu ada cara untuk membuang objek yang tidak bergantung pada aliran kontrol.
Robert Harvey

1
Saya tidak cukup mengikuti Anda, @Robert. C ++ berhasil mendukung pengecualian tanpa pengumpulan sampah.
Karl Bielefeldt

3
@KarlBielefeldt: Dengan biaya besar, dari apa yang saya mengerti. Kemudian lagi, apakah ada sesuatu yang dilakukan dalam C ++ tanpa biaya besar, setidaknya dalam upaya dan pengetahuan domain yang dibutuhkan?
Robert Harvey

2
@RobertHarvey: Poin bagus. Saya salah satu dari orang-orang yang tidak berpikir cukup keras tentang hal-hal ini. Saya terbuai dengan berpikir bahwa ARC adalah GC, tetapi, tentu saja, tidak. Jadi pada dasarnya (jika saya memahami, kira-kira), pengecualian akan menjadi hal yang berantakan (C ++ meskipun) dalam bahasa di mana membuang benda bergantung pada aliran kontrol?
orome

Jawaban:


33

Dalam pemrograman tersemat, pengecualian secara tradisional tidak diizinkan, karena overhead tumpukan yang ingin Anda lakukan dianggap sebagai variabilitas yang tidak dapat diterima ketika mencoba mempertahankan kinerja waktu-nyata. Sementara smartphone secara teknis dapat dianggap sebagai platform waktu nyata, mereka cukup kuat sekarang di mana batasan lama sistem embedded tidak benar-benar berlaku lagi. Saya hanya membawanya demi ketelitian.

Pengecualian sering didukung dalam bahasa pemrograman fungsional, tetapi sangat jarang digunakan sehingga mungkin juga tidak. Salah satu alasannya adalah evaluasi malas, yang dilakukan sesekali bahkan dalam bahasa yang tidak malas secara default. Memiliki fungsi yang dieksekusi dengan tumpukan berbeda dari tempat yang harus dijalankan akan membuatnya sulit untuk menentukan di mana harus meletakkan penangan pengecualian Anda.

Alasan lain adalah fungsi kelas satu memungkinkan untuk konstruksi seperti opsi dan futures yang memberi Anda manfaat sintaksis pengecualian dengan lebih banyak fleksibilitas. Dengan kata lain, sisa bahasa cukup ekspresif sehingga pengecualian tidak membelikan Anda apa pun.

Saya tidak terbiasa dengan Swift, tetapi sedikit yang saya baca tentang penanganan kesalahannya menyarankan mereka dimaksudkan untuk penanganan kesalahan untuk mengikuti lebih banyak pola gaya fungsional. Saya telah melihat contoh kode dengan successdan failureblok yang sangat mirip futures.

Berikut ini contoh penggunaan Futuredari tutorial Scala ini :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Anda dapat melihatnya memiliki struktur yang kira-kira sama dengan contoh Anda menggunakan pengecualian. The futureblok seperti try. The onFailureblok seperti handler pengecualian. Dalam Scala, seperti dalam kebanyakan bahasa fungsional, Futurediimplementasikan sepenuhnya menggunakan bahasa itu sendiri. Itu tidak memerlukan sintaks khusus seperti pengecualian. Itu berarti Anda dapat menentukan konstruksi serupa Anda sendiri. Mungkin menambahkan timeoutblok, misalnya, atau fungsi logging.

Selain itu, Anda dapat meneruskan masa depan, mengembalikannya dari fungsi, menyimpannya dalam struktur data, atau apa pun. Ini nilai kelas satu. Anda tidak terbatas seperti pengecualian yang harus disebarkan langsung ke atas tumpukan.

Opsi menyelesaikan masalah penanganan kesalahan dengan cara yang sedikit berbeda, yang berfungsi lebih baik untuk beberapa kasus penggunaan. Anda tidak terjebak hanya dengan satu metode.

Itu adalah hal-hal yang Anda "dapatkan dengan kehilangan pengecualian."


1
Jadi Futurepada dasarnya adalah cara memeriksa nilai yang dikembalikan dari suatu fungsi, tanpa berhenti untuk menunggu. Seperti Swift, ini berbasis nilai kembali, tetapi tidak seperti Swift, respons terhadap nilai kembali dapat terjadi di lain waktu (sedikit seperti pengecualian). Baik?
orome

1
Anda mengerti Futurebenar, tapi saya pikir Anda mungkin salah menandai Swift. Lihat bagian pertama dari jawaban stackoverflow ini , misalnya.
Karl Bielefeldt

Hmm, saya baru mengenal Swift, jadi jawaban itu agak sulit bagi saya untuk diurai. Tetapi jika saya tidak salah: itu pada dasarnya melewati seorang pawang yang dapat dipanggil di lain waktu; Baik?
orome

Iya. Anda pada dasarnya membuat panggilan balik ketika terjadi kesalahan.
Karl Bielefeldt

Eitherakan menjadi contoh yang lebih baik IMHO
Paweł Prażak

19

Pengecualian dapat membuat kode lebih sulit untuk dipikirkan. Meskipun mereka tidak sekuat goto, mereka dapat menyebabkan banyak masalah yang sama karena sifat non-lokal mereka. Misalnya, katakan Anda memiliki kode imperatif seperti ini:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

Anda tidak bisa mengetahui apakah prosedur ini dapat memberikan pengecualian. Anda harus membaca dokumentasi masing-masing prosedur ini untuk mencari tahu. (Beberapa bahasa membuat ini sedikit lebih mudah dengan menambah jenis tanda tangan dengan informasi ini.) Kode di atas akan dikompilasi dengan baik terlepas dari apakah ada prosedur yang dilemparkan, sehingga sangat mudah untuk melupakan untuk menangani pengecualian.

Selain itu, bahkan jika tujuannya adalah untuk menyebarkan kembali pengecualian ke penelepon, orang sering perlu menambahkan kode tambahan untuk mencegah hal-hal tertinggal dalam keadaan tidak konsisten (misalnya jika pembuat kopi Anda rusak, Anda masih perlu membersihkan kekacauan dan kembali. cangkirnya!). Jadi, dalam banyak kasus kode yang menggunakan pengecualian akan terlihat sama rumitnya dengan kode yang tidak karena pembersihan tambahan yang diperlukan.

Pengecualian dapat ditiru dengan sistem tipe yang cukup kuat. Banyak bahasa yang menghindari pengecualian menggunakan nilai balik untuk mendapatkan perilaku yang sama. Ini mirip dengan bagaimana hal itu dilakukan dalam C, tetapi sistem tipe modern biasanya membuatnya lebih elegan dan juga lebih sulit untuk melupakan kondisi kesalahan. Mereka juga dapat memberikan gula sintaksis untuk membuat hal-hal yang kurang rumit, kadang-kadang hampir sebersih dengan pengecualian.

Secara khusus, dengan menanamkan penanganan kesalahan ke dalam sistem tipe daripada menerapkan sebagai fitur terpisah memungkinkan "pengecualian" digunakan untuk hal-hal lain yang bahkan tidak terkait dengan kesalahan. (Sudah diketahui bahwa penanganan pengecualian sebenarnya adalah properti dari monad.)


Benarkah sistem jenis yang dimiliki Swift, termasuk opsional, adalah jenis "sistem tipe kuat" yang mencapai ini?
orome

2
Ya, opsional dan, lebih umum, tipe penjumlahan (disebut sebagai "enum" di Swift / Rust) dapat mencapai ini. Dibutuhkan beberapa pekerjaan ekstra untuk membuatnya menyenangkan untuk digunakan, namun: di Swift, ini dicapai dengan sintaks perangkaian opsional; di Haskell, ini dicapai dengan notasi monadik.
Rufflewind

Dapatkah "sistem tipe yang cukup kuat" memberikan jejak stack, jika tidak itu cukup berguna
Paweł Prażak

1
+1 untuk menunjukkan bahwa pengecualian mengaburkan aliran kontrol. Sama seperti catatan tambahan: Dapat dipastikan apakah pengecualian sebenarnya tidak lebih jahat daripada goto: Yang gotodibatasi untuk satu fungsi, yang merupakan rentang yang cukup kecil asalkan fungsi benar-benar kecil, pengecualian bertindak lebih seperti beberapa tanda silang gotodan come from(lihat en.wikipedia.org/wiki/INTERCAL ;-)). Ini cukup banyak dapat menghubungkan dua bagian kode, mungkin melompati kode beberapa fungsi ketiga. Satu-satunya hal yang tidak bisa dilakukan, yang gotobisa, akan kembali.
cmaster

2
@ PawełPrażak Ketika bekerja dengan banyak fungsi tingkat tinggi, tumpukan jejak hampir tidak berharga. Jaminan kuat tentang input dan output dan menghindari efek samping adalah apa yang mencegah tipuan ini menyebabkan bug yang membingungkan.
Jack

11

Ada beberapa jawaban bagus di sini, tapi saya pikir satu alasan penting belum cukup ditekankan: Ketika pengecualian terjadi, objek dapat dibiarkan dalam keadaan tidak valid. Jika Anda dapat "menangkap" suatu pengecualian, maka kode handler pengecualian Anda akan dapat mengakses dan bekerja dengan objek-objek yang tidak valid. Itu akan menjadi sangat salah kecuali kode untuk objek-objek itu ditulis dengan sempurna, yang sangat, sangat sulit dilakukan.

Misalnya, bayangkan menerapkan Vector. Jika seseorang instantiate Vector Anda dengan satu set objek, tetapi pengecualian terjadi selama inisialisasi (mungkin, katakanlah, saat menyalin objek Anda ke memori yang baru dialokasikan), sangat sulit untuk benar kode implementasi vektor sedemikian rupa sehingga tidak ada memori bocor. Makalah singkat oleh Stroustroup ini membahas contoh Vektor .

Dan itu hanyalah puncak gunung es. Bagaimana jika, misalnya, Anda telah menyalin beberapa elemen, tetapi tidak semua elemen? Untuk menerapkan wadah seperti Vektor dengan benar, Anda hampir harus membuat setiap tindakan yang Anda ambil reversibel, sehingga seluruh operasi bersifat atomik (seperti transaksi basis data). Ini rumit, dan sebagian besar aplikasi salah. Dan bahkan ketika itu dilakukan dengan benar, itu sangat mempersulit proses penerapan wadah.

Jadi beberapa bahasa modern telah memutuskan itu tidak layak. Karat, misalnya, memang memiliki pengecualian, tetapi tidak dapat "ditangkap", jadi tidak ada cara bagi kode untuk berinteraksi dengan objek dalam keadaan tidak valid.


1
tujuan tangkapan adalah untuk membuat objek konsisten (atau menghentikan sesuatu jika tidak mungkin) setelah kesalahan terjadi.
JoulinRouge

@JoulinRouge, saya tahu. Tetapi beberapa bahasa telah memutuskan untuk tidak memberi Anda kesempatan itu, tetapi malah merusak seluruh proses. Perancang bahasa tersebut tahu jenis pembersihan yang ingin Anda lakukan tetapi telah menyimpulkan terlalu sulit untuk memberikannya kepada Anda, dan pertukaran yang dilakukan tidak akan sia-sia. Saya sadar Anda mungkin tidak setuju dengan pilihan mereka ... tetapi berharga mengetahui bahwa mereka membuat pilihan secara sadar karena alasan-alasan khusus ini.
Charlie Flowers

6

Satu hal yang saya awalnya terkejut tentang bahasa Rust adalah tidak mendukung pengecualian tangkapan. Anda bisa melempar pengecualian, tetapi hanya runtime yang bisa menangkapnya saat tugas (pikirkan utas, tetapi tidak selalu utas OS terpisah) mati; jika Anda memulai tugas sendiri, maka Anda dapat bertanya apakah itu keluar secara normal atau apakah itu fail!()diedit.

Karena itu tidak failsering terlalu idiomatis . Beberapa kasus di mana hal itu terjadi adalah, misalnya, di harness pengujian (yang tidak tahu seperti apa kode pengguna), sebagai tingkat atas dari kompiler (kebanyakan garpu kompiler sebagai gantinya), atau ketika meminta panggilan balik pada input pengguna.

Sebaliknya, idiom umum adalah dengan menggunakan satu Resulttemplate yang secara eksplisit melewatkan kesalahan yang harus ditangani. Ini dibuat secara signifikan lebih mudah dengan para try!makro , yang dapat membungkus ekspresi apapun bahwa hasil sebuah Hasil dan menghasilkan lengan sukses jika ada satu, atau kembali awal dari fungsi.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}

1
Jadi adakah adil untuk mengatakan bahwa ini juga (seperti pendekatan Go) mirip dengan Swift, yang pernah assert, tetapi tidak catch?
orome

1
Di Swift, coba! berarti: Ya saya tahu ini bisa gagal, tapi saya yakin tidak akan, jadi saya tidak menanganinya, dan jika gagal maka kode saya salah, jadi tolong lumpuh dalam kasus itu.
gnasher729

6

Menurut pendapat saya, pengecualian adalah alat penting untuk mendeteksi kesalahan kode pada saat dijalankan. Baik dalam pengujian dan dalam produksi. Buat pesan mereka cukup jelas sehingga dikombinasikan dengan jejak tumpukan Anda dapat mengetahui apa yang terjadi dari log.

Pengecualian sebagian besar merupakan alat pengembangan dan cara untuk mendapatkan laporan kesalahan yang wajar dari produksi dalam kasus yang tidak terduga.

Terlepas dari pemisahan kekhawatiran (jalan bahagia dengan hanya kesalahan yang diharapkan vs. jatuh hingga mencapai beberapa penangan generik untuk kesalahan tak terduga) menjadi hal yang baik, membuat kode Anda lebih mudah dibaca dan dipelihara, sebenarnya tidak mungkin untuk menyiapkan kode Anda untuk semua kemungkinan kasus yang tak terduga, bahkan dengan membengkaknya dengan kode penanganan kesalahan untuk menyelesaikan pembacaan tidak terbaca.

Itu sebenarnya arti dari "tak terduga".

Btw., Apa yang diharapkan dan apa yang tidak adalah keputusan yang hanya dapat dibuat di situs panggilan. Itulah sebabnya pengecualian yang diperiksa di Jawa tidak berhasil - keputusan dibuat pada saat mengembangkan API, ketika sama sekali tidak jelas apa yang diharapkan atau tidak terduga.

Contoh sederhana: API peta hash dapat memiliki dua metode:

Value get(Key)

dan

Option<Value> getOption(key)

yang pertama melempar pengecualian jika tidak ditemukan, yang terakhir memberi Anda nilai opsional. Dalam beberapa kasus, yang terakhir lebih masuk akal, tetapi dalam kasus lain, kode Anda sinmply harus mengharapkan ada nilai untuk kunci yang diberikan, jadi jika tidak ada, itu adalah kesalahan yang tidak dapat diperbaiki oleh kode ini karena dasar asumsi telah gagal. Dalam hal ini sebenarnya perilaku yang diinginkan untuk keluar dari jalur kode dan turun ke beberapa penangan generik jika panggilan gagal.

Kode seharusnya tidak mencoba menangani asumsi dasar yang gagal.

Kecuali dengan memeriksa mereka dan melemparkan pengecualian yang bisa dibaca dengan baik, tentu saja.

Mengecualikan pengecualian bukanlah kejahatan tetapi menangkapnya mungkin. Jangan mencoba memperbaiki kesalahan yang tidak terduga. Tangkap pengecualian di beberapa tempat di mana Anda ingin melanjutkan loop atau operasi, catat, mungkin melaporkan kesalahan yang tidak diketahui, dan hanya itu.

Menangkap balok di semua tempat adalah ide yang sangat buruk.

Rancang API Anda dengan cara yang memudahkan Anda mengekspresikan niat Anda, yaitu menyatakan apakah Anda mengharapkan kasus tertentu, seperti kunci tidak ditemukan, atau tidak. Pengguna API Anda kemudian dapat memilih panggilan lempar untuk kasus yang benar-benar tidak terduga.

Saya kira alasan orang membenci pengecualian dan melangkah terlalu jauh dengan menghilangkan alat penting ini untuk otomatisasi penanganan kesalahan dan pemisahan kekhawatiran yang lebih baik dari bahasa baru adalah pengalaman buruk.

Itu, dan beberapa kesalahpahaman tentang apa yang sebenarnya baik untuk mereka.

Mensimulasi mereka dengan melakukan SEMUANYA melalui pengikatan monadik membuat kode Anda kurang mudah dibaca dan dikelola, dan Anda berakhir tanpa jejak tumpukan, yang membuat pendekatan ini CURAH.

Penanganan kesalahan gaya fungsional sangat bagus untuk kasus kesalahan yang diharapkan.

Biarkan penanganan pengecualian secara otomatis menangani yang lainnya, itulah gunanya :)


3

Swift menggunakan prinsip yang sama di sini sebagai Objective-C, hanya lebih konsekuensinya. Di Objective-C, pengecualian mengindikasikan kesalahan pemrograman. Mereka tidak ditangani kecuali dengan alat pelaporan kerusakan. "Pengecualian penanganan" dilakukan dengan memperbaiki kode. (Ada beberapa pengecualian ahem. Misalnya dalam komunikasi antar proses. Tapi itu cukup langka dan banyak orang tidak pernah mengalami itu. Dan Objective-C sebenarnya telah mencoba / menangkap / akhirnya / melempar, tetapi Anda jarang menggunakannya). Swift baru saja menghilangkan kemungkinan untuk menangkap pengecualian.

Swift memiliki fitur yang tampak seperti penanganan pengecualian tetapi hanya penanganan kesalahan yang ditegakkan. Secara historis, Objective-C memiliki pola penanganan kesalahan yang cukup luas: Suatu metode akan mengembalikan BOOL (YA untuk sukses) atau referensi objek (nil untuk kegagalan, bukan nihil untuk sukses), dan memiliki parameter "pointer ke NSError *" yang akan digunakan untuk menyimpan referensi NSError. Swift secara otomatis mengubah panggilan ke metode seperti itu menjadi sesuatu yang tampak seperti penanganan pengecualian.

Secara umum, fungsi Swift dapat dengan mudah mengembalikan alternatif, seperti hasil jika fungsi berfungsi dengan baik dan kesalahan jika gagal; yang membuat penanganan kesalahan jauh lebih mudah. Tetapi jawaban untuk pertanyaan awal: Desainer Swift jelas merasa bahwa menciptakan bahasa yang aman, dan menulis kode yang berhasil dalam bahasa seperti itu, lebih mudah jika bahasa tersebut tidak memiliki pengecualian.


Untuk Swift, ini adalah jawaban yang benar, IMO. Swift harus tetap kompatibel dengan kerangka kerja sistem Objective-C yang ada, jadi di bawah kap mereka tidak memiliki pengecualian tradisional. Saya menulis posting blog beberapa waktu lalu tentang bagaimana ObjC bekerja untuk penanganan kesalahan: orangejuiceliberationfront.com/…
uliwitness

2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

Dalam C Anda akan berakhir dengan sesuatu seperti di atas.

Adakah hal-hal yang tidak dapat saya lakukan di Swift yang dapat saya lakukan dengan pengecualian?

Tidak, tidak ada apa-apa. Anda akhirnya menangani kode hasil alih-alih pengecualian.
Pengecualian memungkinkan Anda untuk mengatur ulang kode Anda sehingga penanganan kesalahan terpisah dari kode jalur bahagia Anda, tetapi hanya itu saja.


Dan, juga, panggilan itu untuk ...throw_ioerror()mengembalikan kesalahan daripada melemparkan pengecualian?
orome

1
@raxacoricofallapatorius Jika kita mengklaim pengecualian tidak ada maka saya menganggap program mengikuti pola yang biasa mengembalikan kode kesalahan pada kegagalan dan 0 pada keberhasilan.
stonemetal

1
@stonemetal Beberapa bahasa, seperti Rust dan Haskell, menggunakan sistem tipe untuk mengembalikan sesuatu yang lebih bermakna daripada kode kesalahan, tanpa menambahkan titik keluar tersembunyi seperti yang dilakukan pengecualian. Fungsi Karat, dapat, misalnya, mengembalikan Result<T, E>enum, yang bisa berupa Ok<T>, atau Err<E>, dengan Tmenjadi tipe yang Anda inginkan jika ada, dan Emenjadi tipe yang mewakili kesalahan. Pencocokan pola dan beberapa metode tertentu menyederhanakan penanganan keberhasilan dan kegagalan. Singkatnya, jangan menganggap kurangnya pengecualian secara otomatis berarti kode kesalahan.
8bittree

1

Selain jawaban Charlie:

Contoh-contoh penanganan pengecualian yang dinyatakan yang Anda lihat di banyak manual dan buku ini, terlihat sangat cerdas hanya pada contoh yang sangat kecil.

Bahkan jika Anda mengesampingkan argumen tentang keadaan objek yang tidak valid, mereka selalu menimbulkan rasa sakit yang sangat besar ketika berhadapan dengan aplikasi besar.

Misalnya, ketika Anda harus berurusan dengan IO, menggunakan beberapa kriptografi, Anda mungkin memiliki 20 jenis pengecualian yang dapat dibuang dari 50 metode. Bayangkan jumlah kode penanganan pengecualian yang Anda perlukan. Penanganan pengecualian akan membutuhkan kode beberapa kali lebih banyak daripada kode itu sendiri.

Pada kenyataannya Anda tahu kapan pengecualian tidak dapat muncul dan Anda tidak perlu menulis begitu banyak penanganan pengecualian, jadi Anda hanya menggunakan beberapa solusi untuk mengabaikan pengecualian yang dinyatakan. Dalam praktik saya, hanya sekitar 5% dari pengecualian yang dinyatakan perlu ditangani dalam kode untuk memiliki aplikasi yang andal.


Pada kenyataannya, pengecualian ini seringkali dapat ditangani hanya di satu tempat. Misalnya, dalam fungsi "unduh data pembaruan" jika SSL gagal atau DNS tidak dapat dipecahkan atau server web mengembalikan 404, itu tidak masalah, tangkap di bagian atas dan laporkan kesalahan tersebut kepada pengguna.
Zan Lynx
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.