Haruskah "Set" memiliki metode Get?


22

Mari kita buat kelas C # ini (akan hampir sama di Jawa)

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}

   public override bool Equals(object obj) {
        var item = obj as MyClass;

        if (item == null || this.A == null || item.A == null)
        {
            return false;
        }
        return this.A.equals(item.A);
   }

   public override int GetHashCode() {
        return A != null ? A.GetHashCode() : 0;
   }
}

Seperti yang Anda lihat, persamaan dua contoh hanya MyClassbergantung pada A. Jadi bisa ada dua contoh yang sama, tetapi memegang informasi yang berbeda di Bproperti mereka .

Di perpustakaan koleksi standar banyak bahasa (termasuk C # dan Java, tentu saja) ada Set( HashSetdalam C #), yang koleksi, yang dapat menampung paling banyak satu item dari setiap set instance yang sama.

Seseorang dapat menambahkan item, menghapus item dan memeriksa apakah set berisi item. Tetapi mengapa tidak mungkin untuk mendapatkan item tertentu dari set?

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

//I can do this
if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) {
    //something
}

//But I cannot do this, because Get does not exist!!!
MyClass item = mset.Get(new MyClass {A = "Hello", B = "See you"});
Console.WriteLine(item.B); //should print Bye

Satu-satunya cara untuk mengambil item saya adalah dengan mengulang seluruh koleksi dan memeriksa semua item untuk kesetaraan. Namun, ini membutuhkan O(n)waktu, bukan O(1)!

Saya belum menemukan bahasa yang mendukung dapatkan dari set sejauh ini. Semua bahasa "umum" yang saya tahu (Java, C #, Python, Scala, Haskell ...) tampaknya dirancang dengan cara yang sama: Anda dapat menambahkan item, tetapi Anda tidak dapat mengambilnya. Adakah alasan bagus mengapa semua bahasa ini tidak mendukung sesuatu yang mudah dan jelas bermanfaat? Mereka tidak mungkin salah, kan? Apakah ada bahasa yang mendukungnya? Mungkin mengambil kembali item tertentu dari set adalah salah, tetapi mengapa?


Ada beberapa pertanyaan terkait SO:

/programming/7283338/getting-an-element-from-a-set

/programming/7760364/how-to-retrieve-actual-item-from-hashsett


12
C ++ std::setmendukung pengambilan objek, jadi tidak semua bahasa "umum" seperti yang Anda gambarkan.
Pasang kembali Monica

17
Jika Anda mengklaim (dan kode) bahwa "persamaan dua instance MyClass bergantung pada A saja" maka instance lain yang memiliki nilai A yang sama dan B yang berbeda secara efektif adalah "instance tertentu", karena Anda sendiri menetapkan bahwa mereka setara dan perbedaan dalam B tidak penting; wadah "diizinkan" untuk mengembalikan instance lain karena sama.
Peteris

7
Kisah nyata: di Jawa, banyak Set<E>implementasi hanya Map<E,Boolean>di dalam.
corsiKa

10
berbicara kepada orang A : "Hai, bisakah Anda membawa Orang A ke sini, tolong"
Brad Thomas

7
Ini merusak refleksivitas ( a == bselalu benar) dalam kasus this.A == null. The if (item == null || this.A == null || item.A == null)uji "berlebihan" dan cek banyak, mungkin dalam rangka menciptakan artifisial "berkualitas tinggi" kode. Saya melihat semacam ini "pemeriksaan berlebihan" dan menjadi terlalu benar sepanjang waktu pada Tinjauan Kode.
usr

Jawaban:


66

Masalahnya di sini adalah tidak HashSetkekurangan Getmetode, melainkan karena kode Anda tidak masuk akal dari sudut pandang HashSetjenisnya.

Itu Getmetode adalah efektif, "mendapatkan saya nilai ini, silakan", dimana NET framework rakyat bijaksana akan menjawab, "eh? Anda sudah memiliki nilai yang <confused face />".

Jika Anda ingin menyimpan item dan kemudian mengambilnya berdasarkan pencocokan nilai yang sedikit berbeda, kemudian gunakan Dictionary<String, MyClass>yang dapat Anda lakukan:

var mset = new Dictionary<String, MyClass>();
mset.Add("Hello", new MyClass {A = "Hello", B = "Bye"});

var item = mset["Hello"];
Console.WriteLine(item.B); // will print Bye

Informasi kebocoran kesetaraan dari kelas yang dienkapsulasi. Jika saya ingin mengubah set properti yang terlibat Equals, saya harus mengubah kode di luar MyClass...

Ya, tapi itu karena MyClassmengamuk dengan prinsip least astonishment (POLA). Dengan fungsionalitas kesetaraan yang dienkapsulasi, sangat masuk akal untuk menganggap bahwa kode berikut ini valid:

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) 
{
    // this code is unreachable.
}

Untuk mencegah hal ini, MyClassperlu didokumentasikan dengan jelas bentuk kesetaraan yang aneh. Setelah melakukan itu, itu tidak lagi dienkapsulasi dan mengubah cara kerja kesetaraan akan melanggar prinsip terbuka / tertutup. Ergo, itu tidak boleh berubah dan karena itu Dictionary<String, MyClass>merupakan solusi yang baik untuk persyaratan aneh ini.


2
@ Voljta, Dalam hal itu, gunakan Dictionary<MyClass, MyClass>karena akan mengambil nilai berdasarkan kunci yang digunakan MyClass.Equals.
David Arno

8
Saya akan menggunakan yang Dictionary<MyClass, MyClass>disediakan dengan yang sesuai IEqualityComparer<MyClass>, dan menarik hubungan ekivalensi dari MyClassMengapa MyClassharus tahu tentang hubungan ini atas contohnya?
Caleth

16
@ Voljta dan komentar di sana: " meh. Mengesampingkan implementasi equals sehingga objek yang tidak sama adalah" equal "adalah masalahnya di sini. Meminta metode yang mengatakan" dapatkan saya objek yang identik dengan objek ini ", lalu mengharapkan objek yang tidak identik untuk dikembalikan tampak gila dan mudah menyebabkan masalah pemeliharaan "tepat. Itulah yang sering menjadi masalah dengan SO: jawaban yang cacat serius terangkat oleh orang-orang yang belum memikirkan implikasi dari keinginan mereka untuk perbaikan cepat pada kode mereka yang rusak ...
David Arno

6
@DavidArno: jenis yang tak terhindarkan selama kita bertahan dalam menggunakan bahasa yang membedakan antara kesetaraan dan identitas ;-) Jika Anda ingin meng-kanonik objek yang sama tetapi tidak identik maka Anda memerlukan metode yang mengatakan tidak "dapatkan aku yang identik objek ke objek ini ", tetapi" dapatkan saya objek kanonik yang sama dengan objek ini ". Siapa pun yang berpikir bahwa HashSet. Dapatkan dalam bahasa-bahasa ini berarti "dapatkan saya objek yang identik" sudah sangat salah.
Steve Jessop

4
Jawaban ini memiliki banyak pernyataan selimut seperti ...reasonable to assume.... Semua ini mungkin benar dalam 99% dari kasus tetapi masih kemampuan untuk mengambil item dari set bisa berguna. Kode dunia nyata tidak selalu dapat mematuhi prinsip-prinsip POLA dll. Sebagai contoh, jika Anda mendeduplikasi string case-insensitive Anda mungkin ingin mendapatkan item "master". Dictionary<string, string>adalah solusi, tetapi biayanya perf.
usr

24

Anda sudah memiliki item yang "dalam" set - Anda menyerahkannya sebagai kunci.

"Tapi itu bukan contoh yang saya sebut Tambahkan dengan" - Ya, tetapi Anda secara khusus mengklaim bahwa mereka sama.

A Setjuga merupakan kasus khusus dari a Map| Dictionary, dengan void sebagai tipe nilai (yah metode yang tidak berguna tidak didefinisikan, tetapi itu tidak masalah).

Struktur data yang Anda cari adalah Dictionary<X, MyClass>tempat Xentah bagaimana mengeluarkan As dari MyClasses.

Tipe C # Kamus bagus dalam hal ini, karena memungkinkan Anda untuk memasok IEqualityComparer untuk kunci.

Untuk contoh yang diberikan, saya akan memiliki yang berikut:

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}
}

public class MyClassEquivalentAs : IEqualityComparer<MyClass>{
   public override bool Equals(MyClass left, MyClass right) {
        if (Object.ReferenceEquals(left, null) && Object.ReferenceEquals(right, null))
        {
            return true;
        }
        else if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
        {
            return false;
        }
        return left.A == right.A;
   }

   public override int GetHashCode(MyClass obj) {
        return obj?.A != null ? obj.A.GetHashCode() : 0;
   }
}

Dengan demikian digunakan:

var mset = new Dictionary<MyClass, MyClass>(new MyClassEquivalentAs());
var bye = new MyClass {A = "Hello", B = "Bye"};
var seeyou = new MyClass {A = "Hello", B = "See you"};
mset.Add(bye);

if (mset.Contains(seeyou)) {
    //something
}

MyClass item = mset[seeyou];
Console.WriteLine(item.B); // prints Bye

Ada beberapa situasi di mana mungkin menguntungkan untuk kode yang memiliki objek yang cocok dengan kunci, untuk menggantinya dengan referensi ke objek yang digunakan sebagai kunci. Misalnya, jika banyak string diketahui cocok dengan string dalam koleksi hash, mengganti referensi ke semua string dengan referensi ke string yang ada dalam koleksi bisa menjadi kemenangan kinerja.
supercat

@supercat hari ini yang diraih dengan a Dictionary<String, String>.
MikeFHay

@ MikeFHay: Ya, tapi sepertinya sedikit tidak perlu harus menyimpan setiap referensi string dua kali.
supercat

2
@supercat Jika maksud Anda string identik , itu hanya string interning. Gunakan barang bawaan. Jika Anda bermaksud semacam representasi "kanonik" (representasi yang tidak dapat dicapai dengan menggunakan teknik pengubahan kasus sederhana, dll.), Sepertinya Anda pada dasarnya memerlukan indeks (dalam arti DB menggunakan istilah tersebut). Saya tidak melihat masalah dengan menyimpan setiap "bentuk non-kanonik" sebagai kunci yang memetakan ke bentuk kanonik. (Saya pikir ini berlaku sama baiknya jika bentuk "kanonik" bukan string.) Jika ini bukan yang Anda bicarakan, maka Anda benar-benar kehilangan saya.
jpmc26

1
Kustom Comparerdan Dictionary<MyClass, MyClass>merupakan solusi pragmatis. Di Jawa, hal yang sama dapat dicapai oleh TreeSetatau TreeMapditambah kebiasaan Comparator.
Markus Kull

19

Masalah Anda adalah bahwa Anda memiliki dua konsep kesetaraan yang kontradiktif:

  • kesetaraan aktual, di mana semua bidang sama
  • mengatur kesetaraan keanggotaan, di mana hanya A yang sama

Jika Anda akan menggunakan relasi kesetaraan yang sebenarnya di set Anda, masalah mengambil item tertentu dari set tidak muncul - untuk memeriksa apakah suatu objek ada di set, Anda sudah memiliki objek itu. Oleh karena itu tidak perlu untuk mengambil contoh tertentu dari set, dengan asumsi Anda menggunakan hubungan kesetaraan yang benar.

Kita juga bisa berargumen bahwa suatu himpunan adalah tipe data abstrak yang didefinisikan murni oleh S contains xatau x is-element-of Srelasi ("fungsi karakteristik"). Jika Anda ingin operasi lain, Anda sebenarnya tidak mencari satu set.

Apa yang terjadi cukup sering - tetapi apa yang bukan merupakan himpunan - adalah bahwa kita mengelompokkan semua objek ke dalam kelas kesetaraan yang berbeda . Objek di setiap kelas atau subset tersebut hanya setara, tidak sama. Kami dapat mewakili setiap kelas ekivalensi melalui anggota subset mana pun, dan kemudian diinginkan untuk mengambil elemen yang mewakili. Ini akan menjadi pemetaan dari kelas ekivalensi ke elemen perwakilan.

Dalam C #, sebuah kamus dapat menggunakan hubungan kesetaraan eksplisit, saya pikir. Kalau tidak, hubungan seperti itu dapat diimplementasikan dengan menulis kelas pembungkus cepat. Kodesemu:

// The type you actually want to store
class MyClass { ... }

// A equivalence class of MyClass objects,
// with regards to a particular equivalence relation.
// This relation is implemented in EquivalenceClass.Equals()
class EquivalenceClass {
  public MyClass instance { get; }
  public override bool Equals(object o) { ... } // compare instance.A
  public override int GetHashCode() { ... } // hash instance.A
  public static EquivalenceClass of(MyClass o) { return new EquivalenceClass { instance = o }; }
}

// The set-like object mapping equivalence classes
// to a particular representing element.
class EquivalenceHashSet {
  private Dictionary<EquivalenceClass, MyClass> dict = ...;
  public void Add(MyClass o) { dict.Add(EquivalenceClass.of(o), o)}
  public bool Contains(MyClass o) { return dict.Contains(EquivalenceClass.of(o)); }
  public MyClass Get(MyClass o) { return dict.Get(EquivalenceClass.of(o)); }
}

"ambil instance tertentu dari set" Saya pikir ini akan menyampaikan apa yang Anda maksud lebih langsung jika Anda mengubah "instance" menjadi "anggota." Hanya saran kecil. =) +1
jpmc26

7

Tetapi mengapa tidak mungkin untuk mendapatkan item tertentu dari set?

Karena bukan itu gunanya set.

Biarkan saya ulangi contohnya.

"Saya memiliki HashSet yang saya inginkan untuk menyimpan objek MyClass dan saya ingin bisa mendapatkannya dengan menggunakan properti A yang sama dengan properti objek A".

Jika ganti "HashSet" dengan "Collection", "objek" dengan "Values" dan "property A" dengan "Key", kalimatnya menjadi:

"Saya memiliki Koleksi yang ingin saya simpan Nilai MyClass dan saya ingin bisa mendapatkannya dengan menggunakan Kunci yang sama dengan Kunci objek".

Yang dijelaskan adalah Kamus. Pertanyaan aktual yang diajukan adalah "Mengapa saya tidak bisa memperlakukan HashSet sebagai Kamus?"

Jawabannya adalah mereka tidak digunakan untuk hal yang sama. Alasan untuk menggunakan set adalah untuk menjamin keunikan konten individu itu, jika tidak, Anda bisa menggunakan Daftar atau array. Perilaku yang dijelaskan dalam pertanyaan adalah untuk apa Kamus. Semua desainer bahasa tidak mengacau. Mereka tidak menyediakan metode get karena jika Anda memiliki objek dan itu dalam set, mereka setara, yang berarti Anda akan "mendapatkan" objek yang setara. Berargumen bahwa HashSet harus diimplementasikan sedemikian rupa sehingga Anda bisa "mendapatkan" objek yang tidak setara yang Anda tetapkan sama adalah tidak starter ketika bahasa menyediakan struktur data lain yang memungkinkan Anda untuk melakukan itu.

Catatan tentang OOP dan komentar / jawaban kesetaraan. Tidak apa-apa jika kunci pemetaan menjadi properti / anggota dari nilai yang disimpan dalam Kamus. Sebagai contoh: memiliki Guid sebagai kunci dan juga properti yang digunakan untuk metode yang sama adalah sangat masuk akal. Apa yang tidak masuk akal adalah memiliki nilai yang berbeda untuk properti lainnya. Saya menemukan bahwa jika saya menuju ke arah itu, saya mungkin perlu memikirkan kembali struktur kelas saya.


6

Begitu Anda menimpa sama dengan Anda lebih baik menimpa kode hash. Segera setelah Anda melakukan ini, "contoh" Anda seharusnya tidak pernah mengubah keadaan internal lagi.

Jika Anda tidak menimpa sama dan hashcode, identitas objek VM digunakan untuk menentukan kesetaraan. Jika Anda meletakkan objek ini ke dalam Set Anda dapat menemukannya lagi.

Mengubah nilai suatu objek yang digunakan untuk menentukan kesetaraan akan menyebabkan ketidaklacakan objek ini dalam struktur berbasis hash.

Jadi Setter pada A berbahaya.

Sekarang Anda tidak memiliki B yang tidak berpartisipasi dalam kesetaraan. Masalahnya di sini secara teknis tidak semantik. Karena secara teknis mengubah B adalah netral terhadap fakta kesetaraan. Semantik B harus seperti bendera "versi".

Intinya adalah:

Jika Anda memiliki dua objek yang sama dengan A tetapi tidak B Anda memiliki asumsi bahwa salah satu objek ini lebih baru daripada yang lain. Jika B tidak memiliki informasi versi, asumsi ini disembunyikan dalam algoritme Anda KAPAN Anda memutuskan untuk "menimpa / memperbarui" objek ini dalam Set. Lokasi kode sumber ini di mana hal ini terjadi mungkin tidak jelas sehingga pengembang akan mengalami kesulitan untuk mengidentifikasi hubungan antara objek X dan objek Y yang berbeda dari X di B.

Jika B memiliki informasi versi, Anda mengekspos asumsi yang sebelumnya hanya secara implisit diturunkan dari kode. Sekarang Anda bisa melihat, objek Y itu adalah versi X. yang lebih baru

Pikirkan tentang diri Anda: Identitas Anda tetap sepanjang hidup Anda, mungkin beberapa properti berubah (misalnya warna rambut Anda ;-)). Tentu Anda dapat berasumsi bahwa jika Anda memiliki dua foto, satu dengan rambut cokelat satu dengan rambut abu-abu, Anda mungkin lebih muda di foto dengan rambut cokelat. Tapi mungkin Anda sudah mewarnai rambut Anda? Masalahnya adalah: ANDA mungkin tahu bahwa Anda mewarnai rambut Anda. Mungkin orang lain? Untuk memasukkan ini ke dalam konteks yang valid, Anda harus memperkenalkan usia properti (versi). Maka Anda, Anda secara semantik eksplisit dan tidak beragama.

Untuk menghindari operasi tersembunyi "mengganti yang lama dengan objek baru" suatu Set seharusnya tidak memiliki Metode-get. Jika Anda menginginkan perilaku seperti ini, Anda harus membuatnya eksplisit dengan menghapus objek lama dan menambahkan objek baru.

BTW: Apa artinya jika Anda memasukkan objek yang sama dengan objek yang ingin Anda dapatkan? Itu tidak masuk akal. Jagalah semantik Anda dan jangan lakukan ini meskipun secara teknis tidak ada yang akan menghalangi Anda.


7
"Begitu Anda menimpa sama dengan Anda lebih baik menimpa kode hash. Segera setelah Anda melakukan ini" contoh "Anda tidak boleh mengubah keadaan internal lagi." Pernyataan itu bernilai +100, di sana.
David Arno

+1 untuk menunjukkan bahaya kesetaraan dan kode hash tergantung pada keadaan yang bisa berubah
Hulk

3

Khususnya di Jawa, HashSetpada awalnya diimplementasikan menggunakan HashMappula, dan hanya mengabaikan nilai. Jadi desain awal tidak mengantisipasi keuntungan apa pun dalam menyediakan metode get to HashSet. Jika Anda ingin menyimpan dan mengambil nilai kanonik di antara berbagai objek yang sama, maka Anda hanya menggunakan HashMapdiri Anda sendiri.

Saya belum memperbarui informasi implementasi seperti itu, jadi saya tidak bisa mengatakan apakah alasan ini masih berlaku sepenuhnya di Jawa, apalagi di C # dll. Tetapi bahkan jika HashSet diterapkan kembali untuk menggunakan memori lebih sedikit daripada HashMap, dalam hal apapun itu akan menjadi perubahan besar untuk menambahkan metode baru ke Setantarmuka. Jadi itu cukup banyak rasa sakit untuk mendapatkan yang tidak semua orang lihat layak untuk dimiliki.


Nah, di Jawa bisa dimungkinkan untuk memberikan defaultimplementasi untuk melakukan ini dengan cara yang tidak melanggar. Sepertinya tidak ada perubahan yang sangat berguna.
Hulk

@ Hulk: Saya mungkin salah, tapi saya pikir implementasi default apa pun akan sangat tidak efisien, karena seperti yang dikatakan penanya, "Satu-satunya cara untuk mengambil item saya adalah dengan mengulangi seluruh koleksi dan memeriksa semua item untuk kesetaraan". Jadi bagusnya, Anda bisa melakukannya dengan cara yang kompatibel-mundur, tetapi menambahkan gotcha bahwa fungsi get yang dihasilkan hanya menjamin untuk dijalankan dalam O(n)perbandingan bahkan jika fungsi hash memberikan distribusi yang baik. Kemudian implementasi Setyang menimpa implementasi default di antarmuka, termasuk HashSet, dapat memberikan jaminan yang lebih baik.
Steve Jessop

Setuju - Saya pikir itu bukan ide yang baik. Akan ada prioritas untuk perilaku semacam ini meskipun - List.get (int index) atau - untuk memilih implementasi standar yang ditambahkan baru-baru ini List.sort . Jaminan kompleksitas maksimum disediakan oleh antarmuka, tetapi beberapa implementasi mungkin jauh lebih baik daripada yang lain.
Hulk

2

Ada bahasa utama yang himpunannya memiliki properti yang Anda inginkan.

Dalam C ++, std::setadalah set yang dipesan. Ini memiliki .findmetode yang mencari elemen berdasarkan operator pemesanan <atau bool(T,T)fungsi biner yang Anda berikan. Anda dapat menggunakan find untuk mengimplementasikan operasi get yang Anda inginkan.

Bahkan, jika bool(T,T)fungsi yang Anda berikan memiliki bendera khusus di atasnya (is_transparent ), Anda bisa mengirimkan objek dari tipe yang berbeda yang fungsinya terlalu banyak. Itu berarti Anda tidak harus memasukkan data "dummy" ke dalam kolom kedua, cukup pastikan operasi pemesanan yang Anda gunakan dapat memesan antara tipe pencarian dan tipe yang diatur.

Ini memungkinkan efisien:

std::set< std::string, my_string_compare > strings;
strings.find( 7 );

dimana my_string_compare mengerti bagaimana cara memesan bilangan bulat dan string tanpa terlebih dahulu mengubah bilangan bulat menjadi string (dengan biaya potensial).

Untuk unordered_set(kumpulan hash C ++), tidak ada flag transparan yang setara (belum). Anda harus lulus dalam Tsebuahunordered_set<T>.find metode. Itu dapat ditambahkan, tetapi hash membutuhkan ==dan hasher, tidak seperti set memerintahkan yang hanya membutuhkan pemesanan.

Pola umum adalah bahwa wadah akan melakukan pencarian, kemudian memberi Anda "iterator" untuk elemen itu di dalam wadah. Pada titik mana Anda bisa mendapatkan elemen dalam set, atau menghapusnya, dll.

Singkatnya, tidak semua kontainer standar bahasa memiliki kekurangan yang Anda gambarkan. Wadah berbasis iterator pustaka standar C ++ tidak, dan setidaknya beberapa wadah sudah ada sebelum bahasa lain yang Anda deskripsikan, dan kemampuan untuk mendapatkan bahkan lebih efisien daripada bagaimana Anda menggambarkan telah ditambahkan. Tidak ada yang salah dengan desain Anda, atau menginginkan operasi itu; desainer Set yang Anda gunakan sama sekali tidak menyediakan antarmuka itu.

Wadah standar C ++ tempat dirancang untuk membungkus dengan bersih operasi tingkat rendah dari kode C linting tangan yang setara, yang dirancang agar sesuai dengan cara Anda dapat menulisnya secara efisien dalam perakitan. Iteratornya adalah abstraksi dari pointer C-style. Bahasa yang Anda sebutkan semuanya telah pindah dari pointer sebagai konsep, sehingga mereka tidak menggunakan abstraksi iterator.

Ada kemungkinan bahwa fakta bahwa C ++ tidak memiliki cacat ini adalah kecelakaan desain. Jalur iterator-centric berarti bahwa untuk berinteraksi dengan item dalam wadah asosiatif Anda pertama kali mendapatkan iterator ke elemen, lalu Anda menggunakan iterator untuk berbicara tentang entri dalam wadah.

Biayanya adalah bahwa ada aturan pembatalan iterasi yang harus Anda lacak, dan beberapa operasi memerlukan 2 langkah alih-alih satu (yang membuat ribut kode klien). Keuntungannya adalah abstraksi yang kuat memungkinkan penggunaan yang lebih maju daripada yang dimiliki oleh para perancang API semula.

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.