Haruskah saya menerima koleksi kosong dalam metode saya yang mengulanginya?


40

Saya punya metode di mana semua logika dilakukan di dalam foreach loop yang berulang di atas parameter metode:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

Dalam hal ini, mengirimkan koleksi kosong menghasilkan koleksi kosong, tapi saya bertanya-tanya apakah itu tidak bijaksana.

Logika saya di sini adalah jika seseorang memanggil metode ini, maka mereka bermaksud untuk mengirimkan data, dan hanya akan mengirimkan koleksi kosong ke metode saya dalam keadaan yang salah.

Haruskah saya menangkap perilaku ini dan melemparkan pengecualian untuk itu, atau apakah itu praktik terbaik untuk mengembalikan koleksi kosong?


43
Apakah Anda yakin asumsi Anda "jika seseorang memanggil metode ini, maka mereka bermaksud mengirimkan data" benar? Mungkin mereka hanya memberikan apa yang mereka miliki untuk bekerja dengan hasilnya.
SpaceTrucker

7
BTW: "transformasi" metode Anda biasanya disebut "peta", meskipun dalam. NET (dan SQL, yang mana. NET mendapat nama dari), itu biasa disebut "pilih". "Transform" adalah sebutan untuk C ++, tapi itu bukan nama umum yang mungkin dikenali.
Jörg W Mittag

25
Saya akan membuang jika koleksinya nulltetapi tidak jika itu kosong.
CodesInChaos

21
@NickUdell - Saya melewati koleksi kosong di dalam kode saya sepanjang waktu. Lebih mudah bagi kode untuk berperilaku sama daripada menulis logika percabangan khusus untuk kasus-kasus di mana saya tidak bisa menambahkan apa pun ke koleksi saya sebelum melanjutkan.
theMayer

7
Jika Anda memeriksa dan melempar kesalahan jika koleksi kosong, maka Anda memaksa penelepon Anda juga memeriksa dan tidak meneruskan koleksi kosong ke metode Anda. Validasi harus dilakukan pada (dan hanya pada) batas sistem, jika koleksi kosong mengganggu Anda, Anda harus sudah memeriksanya jauh sebelum mencapai metode utilitas apa pun. Sekarang Anda memiliki dua cek berlebihan.
Lie Ryan

Jawaban:


175

Metode utilitas tidak boleh membuang koleksi kosong. Klien API Anda akan membenci Anda karenanya.

Koleksi bisa kosong; "pengumpulan-yang-tidak-boleh-kosong" secara konseptual adalah hal yang jauh lebih sulit untuk dikerjakan.

Mengubah koleksi kosong memiliki hasil yang jelas: koleksi kosong. (Anda bahkan dapat menghemat beberapa sampah dengan mengembalikan parameter itu sendiri.)

Ada banyak keadaan di mana modul menyimpan daftar barang yang mungkin atau mungkin belum diisi dengan sesuatu. Harus memeriksa kekosongan sebelum setiap panggilan untuk transformmenjengkelkan dan memiliki potensi untuk mengubah algoritma yang sederhana dan elegan menjadi kekacauan yang jelek.

Metode utilitas harus selalu berusaha untuk menjadi liberal dalam input mereka dan konservatif dalam output mereka.

Demi semua alasan ini, demi Tuhan, tangani koleksi kosong dengan benar. Tidak ada yang lebih menjengkelkan daripada modul pembantu yang berpikir itu tahu apa yang Anda inginkan lebih baik daripada yang Anda lakukan.


14
+1 - (lebih banyak jika saya bisa). Saya bahkan mungkin melangkah lebih jauh untuk juga bergabung nullke dalam koleksi kosong. Fungsi dengan keterbatasan implisit adalah rasa sakit.
Telastyn

6
"Tidak, kamu seharusnya tidak" ... tidak boleh menerimanya atau tidak harus membuat pengecualian?
Ben Aaronson

16
@Andy - itu tidak akan menjadi metode utilitas . Mereka akan menjadi metode bisnis atau perilaku konkret serupa.
Telastyn

38
Saya tidak berpikir mendaur ulang koleksi kosong untuk pengembalian adalah ide yang bagus. Anda hanya menyimpan sedikit memori; dan itu bisa mengakibatkan perilaku tak terduga pada penelepon. Contoh yang sedikit dibuat-buat: Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); Jika Populate1 meninggalkan koleksi kosong dan Anda mengembalikannya untuk panggilan TransformNodes pertama, output1 dan input akan menjadi koleksi yang sama, dan ketika Populaten2 dipanggil jika ia menempatkan node dalam koleksi, Anda akan berakhir dengan yang kedua set input dalam output2.
Dan Neely

6
@Andy Meski begitu, jika argumen tidak boleh kosong - jika ini adalah kesalahan untuk tidak memiliki data untuk diproses - maka saya merasa itu adalah tanggung jawab penelepon untuk memeriksa ini, ketika menyusun argumen itu. Tidak hanya membuat metode ini lebih elegan, tetapi itu berarti seseorang dapat menghasilkan pesan kesalahan yang lebih baik (konteks yang lebih baik) dan meningkatkannya dengan cara yang gagal cepat. Tidak masuk akal bagi kelas hilir untuk memverifikasi invarian penelepon ...
Andrzej Doyle

26

Saya melihat dua pertanyaan penting yang menentukan jawaban untuk ini:

  1. Bisakah fungsi Anda mengembalikan sesuatu yang bermakna dan logis ketika melewati koleksi kosong (termasuk nol )?
  2. Apa gaya pemrograman umum dalam aplikasi / perpustakaan / tim ini? (Secara khusus, bagaimana FP Anda?)

1. Pengembalian yang berarti

Intinya, jika Anda bisa mengembalikan sesuatu yang bermakna, maka jangan melemparkan pengecualian. Biarkan penelepon berurusan dengan hasilnya. Jadi jika fungsi Anda ...

  • Menghitung jumlah elemen dalam koleksi, mengembalikan 0. Itu mudah.
  • Mencari elemen yang cocok dengan kriteria tertentu, mengembalikan koleksi kosong. Tolong jangan membuang apa pun. Penelepon mungkin memiliki banyak koleksi, beberapa di antaranya kosong, beberapa tidak. Penelepon menginginkan elemen yang cocok dalam koleksi apa pun. Pengecualian hanya membuat hidup penelepon lebih sulit.
  • Sedang mencari kriteria terbesar / terkecil / paling cocok dengan-dalam-daftar. Ups. Bergantung pada pertanyaan gaya, Anda mungkin melempar pengecualian di sini atau Anda dapat mengembalikan nol . Saya benci nol (sangat banyak orang FP) tetapi ini bisa dibilang lebih bermakna di sini dan memungkinkan Anda memesan pengecualian untuk kesalahan tak terduga dalam kode Anda sendiri. Jika pemanggil tidak memeriksa nol, pengecualian yang cukup jelas akan tetap terjadi. Tapi Anda serahkan itu pada mereka.
  • Meminta item ke-n dalam koleksi atau item pertama / terakhir. Ini adalah kasus terbaik untuk pengecualian dan kasus yang paling tidak menyebabkan kebingungan dan kesulitan bagi penelepon. Kasing masih dapat dibuat untuk null jika Anda dan tim Anda terbiasa memeriksa untuk itu, untuk semua alasan yang diberikan pada poin sebelumnya, tetapi ini adalah kasing yang paling valid untuk melempar Dude. Anda Tahu Anda Harus Memeriksa Pengecualian. Jika gaya Anda lebih fungsional, maka batal atau baca terus jawaban Gaya saya .

Secara umum, bias FP saya memberi tahu saya - mengembalikan sesuatu yang bermakna - dan nol dapat memiliki arti yang valid dalam kasus ini.

2. Gaya

Apakah kode umum Anda (atau kode proyek atau kode tim) mendukung gaya fungsional? Jika tidak, pengecualian akan diharapkan dan ditangani. Jika ya, maka pertimbangkan untuk mengembalikan Tipe Opsi . Dengan jenis opsi, Anda bisa mengembalikan jawaban yang bermakna atau Tidak Ada / Tidak Ada . Dalam contoh ketiga saya dari atas, Tidak ada yang akan menjadi jawaban yang baik dalam gaya FP. Fakta bahwa fungsi mengembalikan sinyal Tipe opsi dengan jelas kepada penelepon daripada jawaban yang bermakna mungkin tidak mungkin dan penelepon harus siap untuk menghadapinya. Saya merasa itu memberi penelepon lebih banyak pilihan (jika Anda akan memaafkan permainan kata-kata).

F # adalah di mana semua keren Net anak-anak melakukan hal semacam ini tetapi C # tidak mendukung gaya ini.

tl; dr

Simpan pengecualian untuk kesalahan tak terduga di jalur kode Anda sendiri, tidak sepenuhnya input (dan legal) yang dapat diprediksi dari orang lain.


"Paling cocok" dll .: Anda mungkin memiliki metode yang menemukan objek paling cocok dalam koleksi yang cocok dengan kriteria tertentu. Metode itu akan memiliki masalah yang sama untuk koleksi yang tidak kosong, karena tidak ada yang sesuai dengan kriteria, jadi tidak perlu khawatir tentang pilihan kosong.
gnasher729

1
Tidak, itu sebabnya saya berkata ¨best fit¨ dan bukan ¨matches¨; frasa ini menyiratkan perkiraan terdekat, bukan kecocokan persis. Opsi terbesar / terkecil seharusnya menjadi petunjuk. Jika hanya ¨best fit¨ telah diminta, maka setiap koleksi dengan 1 atau lebih anggota harus dapat mengembalikan anggota. Jika hanya ada satu anggota, itu paling cocok.
bruce

1
Mengembalikan "null" dalam kebanyakan kasus lebih buruk daripada melemparkan pengecualian. Namun mengembalikan koleksi kosong itu bermakna.
Ian

1
Tidak jika Anda harus mengembalikan satu item (min / maks / kepala / ekor). Adapun null, seperti yang saya katakan, saya benci (dan lebih suka bahasa yang tidak memilikinya), tetapi di mana bahasa itu memilikinya, Anda harus dapat menghadapinya dan kode yang membagikannya. Tergantung pada konvensi pengkodean lokal, mungkin sesuai.
bruce

Tidak satu pun dari contoh Anda yang ada hubungannya dengan input kosong. Itu hanya kasus khusus situasi di mana jawabannya mungkin "tidak ada." Yang pada gilirannya harus menjawab pertanyaan OP tentang bagaimana "menangani" koleksi kosong.
djechlin

18

Seperti biasa, itu tergantung.

Apakah penting bahwa koleksinya kosong?
Sebagian besar kode penanganan pengumpulan mungkin akan mengatakan "tidak"; koleksi dapat memiliki sejumlah item di dalamnya, termasuk nol.

Sekarang, jika Anda memiliki semacam koleksi di mana itu "tidak valid" untuk tidak memiliki item di dalamnya, maka itu adalah persyaratan baru dan Anda harus memutuskan apa yang harus dilakukan tentang itu.

Pinjam beberapa logika pengujian dari dunia Database: uji untuk nol item, satu item dan dua item. Itu melayani kasus-kasus yang paling penting (menyiram kondisi dalam atau gabungan kartesian yang buruk).


Dan jika itu tidak valid untuk tidak memiliki item dalam koleksi, maka idealnya argumen tidak akan menjadi java.util.Collection, tetapi com.foo.util.NonEmptyCollectionkelas khusus , yang secara konsisten dapat mempertahankan invarian ini, dan mencegah Anda masuk ke keadaan yang tidak valid untuk memulai.
Andrzej Doyle

3
Pertanyaan ini ditandai [c #]
Nick Udell

@NickUdell tidak C # tidak mendukung pemrograman Berorientasi Objek atau konsep ruang nama bersarang? TIL.
John Dvorak

2
Itu benar. Komentar saya adalah klarifikasi karena Andrzej pasti bingung (untuk apa lagi mereka pergi ke upaya menentukan namespace untuk bahasa yang tidak dirujuk oleh pertanyaan ini?).
Nick Udell

-1 untuk menekankan "itu tergantung" lebih dari "hampir pasti ya". Tidak ada lelucon - mengomunikasikan hal yang salah tidak merusak di sini.
Djechlin

11

Sebagai soal desain yang bagus, terima sebanyak mungkin variasi input Anda sepraktis mungkin. Pengecualian hanya boleh dilontarkan ketika (input yang tidak dapat diterima disajikan ATAU kesalahan tak terduga terjadi saat memproses) DAN sebagai hasilnya program tidak dapat dilanjutkan dengan cara yang dapat diprediksi .

Dalam hal ini, diharapkan koleksi kosong akan disajikan, dan kode Anda harus menanganinya (yang sudah ada). Ini akan menjadi pelanggaran terhadap semua yang baik jika kode Anda melemparkan pengecualian di sini. Ini akan mirip dengan mengalikan 0 dengan 0 dalam matematika. Itu mubazir, tetapi mutlak diperlukan agar bisa bekerja sebagaimana mestinya.

Sekarang, lanjutkan ke argumen koleksi nol. Dalam hal ini, koleksi nol adalah kesalahan pemrograman: programmer lupa untuk menetapkan variabel. Ini adalah kasus di mana pengecualian dapat dilemparkan, karena Anda tidak dapat memprosesnya secara berarti menjadi sebuah output, dan mencoba melakukannya akan memperkenalkan perilaku yang tidak terduga. Ini akan mirip dengan membagi dengan nol dalam matematika - itu sama sekali tidak berarti.


Pernyataan tebal Anda adalah saran yang sangat buruk secara umum. Saya pikir itu benar di sini tetapi Anda perlu menjelaskan apa yang istimewa tentang keadaan ini. (Ini saran yang buruk secara umum karena mempromosikan kegagalan yang diam-diam.)
djechlin

@AAA - jelas kami tidak sedang menulis buku tentang cara mendesain perangkat lunak di sini, tapi saya tidak berpikir itu saran yang buruk sama sekali jika Anda benar-benar mengerti apa yang saya katakan. Maksud saya adalah bahwa input yang buruk menghasilkan output yang ambigu atau sewenang-wenang, Anda perlu melemparkan pengecualian. Dalam kasus di mana output benar-benar dapat diprediksi (seperti halnya di sini), maka melemparkan pengecualian akan sewenang-wenang. Kuncinya adalah jangan pernah membuat keputusan sewenang-wenang dalam program Anda, karena dari situlah perilaku yang tidak terduga berasal.
theMayer

Tapi ini kalimat pertama Anda dan satu-satunya yang disorot ... belum ada yang mengerti apa yang Anda katakan. (Saya tidak mencoba untuk menjadi terlalu agresif di sini, salah satu hal yang kami lakukan di sini adalah belajar menulis dan berkomunikasi dan saya melakukan hal yang kehilangan ketelitian pada poin kunci adalah berbahaya.)
djechlin

10

Solusi yang tepat jauh lebih sulit untuk dilihat ketika Anda hanya melihat fungsi Anda secara terpisah. Pertimbangkan fungsi Anda sebagai salah satu bagian dari masalah yang lebih besar . Salah satu solusi yang mungkin untuk contoh itu terlihat seperti ini (dalam Scala):

input.split("\\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

Pertama, Anda membagi string menjadi non-digit, memfilter string kosong, mengubah string menjadi bilangan bulat, lalu memfilter agar hanya menyimpan angka empat digit. Fungsi Anda mungkin ada map (_.toInt)di dalam pipa.

Kode ini cukup mudah karena setiap tahap dalam pipa hanya menangani string kosong atau koleksi kosong. Jika Anda meletakkan string kosong di awal, Anda mendapatkan daftar kosong di akhir. Anda tidak harus berhenti dan memeriksa nullatau pengecualian setelah setiap panggilan.

Tentu saja, itu mengira daftar output kosong tidak memiliki lebih dari satu makna. Jika Anda perlu membedakan antara output kosong yang disebabkan oleh input kosong dan yang disebabkan oleh transformasi itu sendiri, itu benar-benar mengubah banyak hal.


Memberi +1 untuk contoh yang sangat efektif (kurang dalam jawaban bagus lainnya).
Djechlin

2

Pertanyaan ini sebenarnya tentang pengecualian. Jika Anda melihatnya seperti itu dan mengabaikan koleksi kosong sebagai detail implementasi, jawabannya langsung:

1) Suatu metode harus mengeluarkan pengecualian ketika itu tidak dapat melanjutkan: baik tidak dapat melakukan tugas yang ditunjuk, atau mengembalikan nilai yang sesuai.

2) Suatu metode harus menangkap pengecualian ketika ia dapat melanjutkan meskipun gagal.

Jadi, metode pembantu Anda tidak boleh "membantu" dan membuat pengecualian kecuali itu tidak dapat melakukan pekerjaannya dengan koleksi kosong. Biarkan penelepon menentukan apakah hasilnya dapat ditangani.

Apakah itu mengembalikan koleksi kosong atau nol, sedikit lebih sulit tetapi tidak banyak: koleksi nullable harus dihindari jika memungkinkan. Tujuan dari kumpulan yang dapat dibatalkan adalah untuk menunjukkan (seperti dalam SQL) bahwa Anda tidak memiliki informasi - kumpulan anak-anak misalnya mungkin nol jika Anda tidak tahu apakah seseorang memiliki, tetapi Anda tidak tahu bahwa mereka tidak melakukannya. Tetapi jika itu penting untuk beberapa alasan, mungkin perlu variabel tambahan untuk melacaknya.


1

Metode ini dinamai TransformNodes. Dalam hal koleksi kosong sebagai input, mendapatkan kembali koleksi kosong adalah alami dan intuitif, dan masuk akal secara matematika.

Jika metode ini dinamai Maxdan dirancang untuk mengembalikan elemen maksimum, maka akan wajar untuk membuang NoSuchElementExceptionkoleksi kosong, karena maksimum tidak ada yang masuk akal secara matematis.

Jika metode ini dinamai JoinSqlColumnNamesdan dirancang untuk mengembalikan string di mana elemen-elemen bergabung dengan koma untuk digunakan dalam query SQL, maka masuk akal untuk membuang IllegalArgumentExceptionkoleksi kosong, karena pemanggil pada akhirnya akan mendapatkan kesalahan SQL pula jika ia menggunakan string dalam query SQL secara langsung tanpa pemeriksaan lebih lanjut, dan alih-alih memeriksa string kosong yang dikembalikan, ia seharusnya memeriksa koleksi kosong sebagai gantinya.


Maxtidak ada yang sering negatif tak terbatas.
Djechlin

0

Mari kita mundur dan menggunakan contoh berbeda yang menghitung rata-rata aritmatika dari suatu array nilai.

Jika array input kosong (atau nol), dapatkah Anda memenuhi permintaan pemanggil? Tidak. Apa pilihan Anda? Anda bisa:

  • present / return / throw a error. menggunakan konvensi basis kode Anda untuk kelas kesalahan itu.
  • mendokumentasikan bahwa nilai seperti nol akan dikembalikan
  • mendokumentasikan bahwa nilai tidak valid yang ditunjuk akan dikembalikan (mis. NaN)
  • mendokumentasikan bahwa nilai ajaib akan dikembalikan (mis. min atau maks untuk jenis atau nilai semoga-indikatif)
  • menyatakan hasilnya tidak ditentukan
  • menyatakan tindakan tidak terdefinisi
  • dll.

Saya katakan beri mereka kesalahan jika mereka memberi Anda input yang tidak valid dan permintaan tidak dapat diselesaikan. Maksud saya kesalahan sejak hari pertama sehingga mereka memahami persyaratan program Anda. Lagi pula, fungsi Anda tidak dalam posisi untuk merespons. Jika operasi bisa gagal (misalnya menyalin file), maka API Anda harus memberi mereka kesalahan yang bisa mereka tangani.

Sehingga dapat menentukan bagaimana perpustakaan Anda menangani permintaan dan permintaan yang salah yang mungkin gagal.

Sangat penting bagi kode Anda untuk konsisten dalam cara menangani kelas kesalahan ini.

Kategori berikutnya adalah memutuskan bagaimana perpustakaan Anda menangani permintaan yang tidak masuk akal. Kembali ke contoh serupa dengan Anda - mari kita gunakan fungsi yang menentukan apakah file yang ada di jalan: bool FileExistsAtPath(String). Jika klien melewati string kosong, bagaimana Anda menangani skenario ini? Bagaimana dengan array kosong atau null yang diteruskan void SaveDocuments(Array<Document>)? Putuskan untuk perpustakaan / basis kode Anda, dan konsistenlah. Saya kebetulan menganggap kesalahan kasus ini, dan melarang klien membuat permintaan omong kosong dengan menandainya sebagai kesalahan (melalui pernyataan). Beberapa orang akan sangat menentang gagasan / tindakan itu. Saya menemukan deteksi kesalahan ini sangat membantu. Ini sangat baik untuk menemukan masalah dalam program - dengan lokasi yang baik untuk program yang menyinggung. Program jauh lebih jelas dan benar (pertimbangkan evolusi basis kode Anda), dan jangan membakar siklus dalam fungsi yang tidak melakukan apa pun. Kode lebih kecil / bersih dengan cara ini, dan pemeriksaan umumnya didorong ke lokasi di mana masalah dapat diperkenalkan.


6
Sebagai contoh terakhir Anda, bukan tugas fungsi itu untuk menentukan apa yang tidak masuk akal. Oleh karena itu, string kosong harus mengembalikan false. Memang, itulah pendekatan yang tepat diambil oleh File.Exists .
theMayer

@ rmayer06 itu adalah tugasnya. fungsi yang direferensikan memungkinkan null, string kosong, memeriksa karakter yang tidak valid dalam string, melakukan pemotongan string, mungkin perlu membuat panggilan filesystem, mungkin perlu query lingkungan, dll. biaya tinggi, dan mereka jelas mengaburkan garis kebenaran (IMO). Saya telah melihat banyak if (!FileExists(path)) { ...create it!!!... }bug - banyak yang akan ditangkap sebelum melakukan, apakah garis kebenaran tidak kabur.
justin

2
Saya akan setuju dengan Anda, jika nama fungsi itu File.ThrowExceptionIfPathIsInvalid, tetapi siapa yang waras akan memanggil fungsi tersebut?
theMayer

Rata-rata 0 item sudah akan mengembalikan NaN atau melempar pengecualian divide-by-zero, hanya karena bagaimana fungsi tersebut didefinisikan. File dengan nama yang tidak mungkin ada, tidak akan ada atau sudah memicu kesalahan. Tidak ada alasan kuat untuk menangani kasus-kasus ini secara khusus.
cao

Seseorang bahkan dapat berargumen bahwa seluruh konsep memeriksa keberadaan file sebelum membaca atau menulisnya, tetap saja cacat. (Ada kondisi ras yang melekat.) Lebih baik hanya melanjutkan dan mencoba untuk membuka file, untuk membaca, atau dengan pengaturan hanya-buat jika Anda ingin menulis. Ini akan gagal jika nama file tidak valid atau tidak ada (atau tidak, dalam hal penulisan).
cHao

-3

Sebagai aturan praktis, fungsi standar harus dapat menerima daftar input terluas dan memberikan umpan balik padanya, ada banyak contoh di mana programmer menggunakan fungsi dengan cara yang tidak direncanakan oleh perancang, dengan pemikiran ini saya percaya fungsi tersebut harus dapat menerima tidak hanya koleksi kosong tetapi berbagai jenis input dan mengembalikan umpan balik dengan anggun, baik itu objek kesalahan dari operasi apa pun yang dilakukan pada input ...


Sangatlah salah bagi perancang untuk berasumsi bahwa koleksi akan selalu diteruskan dan langsung beralih ke pengulangan koleksi tersebut, pemeriksaan harus dilakukan untuk memastikan bahwa argumen yang diterima sesuai dengan tipe data yang diharapkan ...
Clement Mark-Aaba

3
"berbagai jenis input" terlihat tidak relevan di sini. Pertanyaan ditandai c # , bahasa yang sangat diketik. Artinya, kompiler menjamin bahwa input adalah koleksi
agas

Mohon google "aturan praktis", jawaban saya adalah peringatan umum bahwa sebagai seorang programmer Anda membuat ruang untuk kejadian yang tidak terduga atau salah, salah satunya dapat keliru melewati argumen fungsi ...
Clement Mark-Aaba

1
Ini salah atau tautologis. writePaycheckToEmployeeseharusnya tidak menerima angka negatif sebagai masukan ... tetapi jika "umpan balik" berarti "melakukan apa pun yang mungkin terjadi selanjutnya," maka ya setiap fungsi akan melakukan sesuatu yang dilakukannya selanjutnya.
Djechlin
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.