Mengapa benda dilewatkan dengan referensi?


31

Seorang rekan kerja muda yang sedang mempelajari OO telah bertanya kepada saya mengapa setiap objek dilewatkan dengan referensi, yang merupakan kebalikan dari tipe atau struct primitif. Ini adalah karakteristik umum dari bahasa seperti Java dan C #.

Saya tidak dapat menemukan jawaban yang bagus untuknya.

Apa motivasi untuk keputusan desain ini? Apakah pengembang bahasa ini lelah karena harus membuat pointer dan mengetik setiap saat?


2
Apakah Anda bertanya mengapa Java dan C # Anda lulus parameter dengan referensi, bukan oleh nilai, atau dengan referensi, bukan oleh pointer?
robert

@ Robert, ada perbedaan tingkat tinggi di antara "referensi bukan dengan pointer"? Apakah Anda pikir saya harus mengubah judul menjadi sesuatu seperti 'mengapa objek selalu menjadi rujukan? "?
Gustavo Cardoso

Referensi adalah petunjuk.
compman

2
@Anto: Referensi Java sama sekali identik dengan pointer C yang digunakan dengan benar (digunakan dengan benar: tidak mengetik, tidak diatur ke memori yang tidak valid, tidak diatur oleh literal).
Zan Lynx

5
Juga benar-benar bertele-tele, judulnya tidak benar (setidaknya sejauh menyangkut .net). Objek TIDAK dilewatkan oleh referensi, referensi dilewatkan oleh nilai. Ketika Anda melewati objek ke metode nilai referensi disalin ke referensi baru di dalam tubuh metode. Saya pikir ini memalukan bahwa "objek dilewatkan dengan referensi" telah memasuki daftar kutipan programmer umum ketika itu tidak benar dan mengarah pada pemahaman yang lebih buruk tentang referensi untuk programmer baru yang memulai.
SecretDeveloper

Jawaban:


15

Alasan dasar sampai pada ini:

  • Pointer bersifat teknis untuk mendapatkan yang benar
  • Anda perlu petunjuk untuk menerapkan struktur data tertentu
  • Anda perlu petunjuk untuk menjadi efisien dalam penggunaan memori
  • Anda tidak perlu pengindeksan memori manual untuk bekerja jika Anda tidak menggunakan perangkat keras secara langsung.

Karena itu, referensi.


32

Jawaban sederhana:

Meminimalkan konsumsi memori
dan
waktu CPU dalam menciptakan dan melakukan salinan yang mendalam dari setiap objek yang dilewatkan di suatu tempat.


Saya setuju dengan Anda, tetapi saya pikir ada juga beberapa motivasi desain estetika atau OO.
Gustavo Cardoso

9
@Gustavo Cardoso: "beberapa motivasi desain estetika atau OO". Tidak. Itu hanya optimasi.
S.Lott

6
@ S.Lott: Tidak, masuk akal dalam istilah OO untuk melewati referensi karena, secara semantis, Anda tidak ingin membuat salinan objek. Anda ingin meneruskan objek alih-alih salinannya. Jika Anda melewati nilai, itu menghancurkan metafora OO karena Anda memiliki semua klon objek yang dihasilkan di semua tempat yang tidak masuk akal di tingkat yang lebih tinggi.
intuited

@ Gustavo: Saya pikir kami berdebat tentang hal yang sama. Anda menyebutkan semantik OOP dan merujuk pada metafora OOP sebagai alasan tambahan saya sendiri. Sepertinya saya bahwa pencipta OOP membuatnya seperti yang mereka lakukan untuk "meminimalkan konsumsi memori" dan "Menghemat waktu CPU"
Tim

14

Di C ++, Anda memiliki dua opsi utama: kembali dengan nilai atau kembali dengan pointer. Mari kita lihat yang pertama:

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

Dengan asumsi kompiler Anda tidak cukup pintar untuk menggunakan optimasi nilai balik, yang terjadi di sini adalah ini:

  • newObj dibangun sebagai objek sementara dan ditempatkan pada stack lokal.
  • Salinan newObj dibuat dan dikembalikan.

Kami telah membuat salinan objek tanpa tujuan. Ini buang waktu pemrosesan.

Mari kita lihat return-by-pointer sebagai gantinya:

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

Kami telah menghilangkan salinan yang berlebihan, tetapi sekarang kami telah memperkenalkan masalah lain: kami telah membuat objek di heap yang tidak akan hancur secara otomatis. Kita harus menghadapinya sendiri:

MyClass someObj = getNewObject();
delete someObj;

Mengetahui siapa yang bertanggung jawab untuk menghapus objek yang dialokasikan dengan cara ini adalah sesuatu yang hanya dapat dikomunikasikan dengan komentar atau dengan konvensi. Dengan mudah menyebabkan kebocoran memori.

Banyak solusi telah disarankan untuk menyelesaikan dua masalah ini - optimasi nilai kembali (di mana kompiler cukup pintar untuk tidak membuat salinan berlebihan dalam nilai-demi-nilai), meneruskan referensi ke metode (sehingga fungsi menyuntikkan ke dalam objek yang sudah ada daripada membuat yang baru), smart pointer (sehingga pertanyaan tentang kepemilikan diperdebatkan).

Pembuat Java / C menyadari bahwa selalu mengembalikan objek dengan referensi adalah solusi yang lebih baik, terutama jika bahasa mendukungnya secara asli. Ini terkait dengan banyak fitur lain yang dimiliki bahasa, seperti pengumpulan sampah, dll.


Return-by-value sudah cukup buruk, tetapi pass-by-value bahkan lebih buruk ketika menyangkut objek, dan saya pikir itu adalah masalah nyata yang mereka coba hindari.
Mason Wheeler

pasti Anda memiliki poin yang valid. Tapi masalah desain OO yang @Mason tunjukkan adalah motivasi terakhir dari perubahan. Tidak ada arti untuk menjaga perbedaan antara referensi dan nilai ketika Anda hanya ingin menggunakan referensi.
Gustavo Cardoso

10

Banyak jawaban lain punya info bagus. Saya ingin menambahkan satu poin penting tentang kloning yang hanya dibahas sebagian.

Menggunakan referensi itu cerdas. Menyalin sesuatu itu berbahaya.

Seperti yang orang lain katakan, di Jawa, tidak ada "klon" alami. Ini bukan hanya fitur yang hilang. Anda tidak akan pernah mau hanya menyalin * mau tak mau (baik dangkal atau dalam) setiap properti dalam suatu objek. Bagaimana jika properti itu adalah koneksi basis data? Anda tidak bisa hanya "mengkloning" koneksi basis data lagi daripada mengkloning manusia. Inisialisasi ada karena suatu alasan.

Salinan yang dalam adalah masalah mereka sendiri - seberapa dalam Anda benar - benar pergi? Anda pasti tidak dapat menyalin apa pun yang statis (termasuk Classobjek apa pun ).

Jadi untuk alasan yang sama mengapa tidak ada kloning alami, objek yang dilewatkan sebagai salinan akan membuat kegilaan . Bahkan jika Anda dapat "mengkloning" koneksi DB - bagaimana Anda sekarang memastikan bahwa itu ditutup?


* Lihat komentar - Dengan pernyataan "tidak pernah" ini, maksud saya klon-otomatis yang mengkloning setiap properti. Java tidak menyediakannya, dan mungkin bukan ide yang baik bagi Anda sebagai pengguna bahasa untuk membuat sendiri, karena alasan yang tercantum di sini. Mengkloning hanya bidang non-transien akan menjadi awal, tetapi bahkan kemudian Anda harus rajin mendefinisikan transientmana yang sesuai.


Saya mengalami kesulitan memahami lompatan dari keberatan yang baik ke kloning dalam kondisi tertentu untuk pernyataan bahwa itu tidak pernah diperlukan. Dan saya jumpai situasi di mana duplikat yang tepat diperlukan, di mana tidak ada fungsi statis di mana yang terlibat, tidak ada IO atau koneksi terbuka bisa di masalah ... Saya memahami risiko kloning, tapi aku tidak bisa melihat selimut tidak pernah .
Inca

2
@Inca - Anda mungkin salah paham. Diimplementasikan dengan sengaja clonebaik-baik saja. Maksudnya "mau tak mau" maksud saya menyalin semua properti tanpa memikirkannya - tanpa maksud yang disengaja. Perancang bahasa Jawa memaksa niat ini dengan mengharuskan implementasi yang dibuat pengguna clone.
Nicole

Menggunakan referensi ke objek yang tidak berubah adalah pintar. Membuat nilai-nilai sederhana seperti Date bisa berubah, dan kemudian membuat beberapa referensi tidak bisa.
kevin cline

@NickC: Alasan utama "mengkloning sesuatu mau tak mau" berbahaya adalah bahwa bahasa / kerangka kerja seperti Java dan .net tidak memiliki cara untuk menunjukkan secara deklaratif apakah referensi merangkum keadaan yang dapat berubah, identitas, keduanya, atau tidak sama sekali. Jika bidang berisi referensi objek yang merangkum keadaan bisa berubah tetapi bukan identitas, kloning objek mengharuskan objek yang memegang status diduplikasi, dan referensi ke duplikat yang disimpan di bidang. Jika referensi merangkum identitas tetapi tidak bisa berubah keadaan, bidang dalam salinan harus merujuk ke objek yang sama seperti pada aslinya.
supercat

Aspek copy yang mendalam adalah poin penting. Menyalin objek bermasalah ketika mereka berisi referensi ke objek lain, terutama jika grafik objek berisi objek yang bisa berubah.
Caleb

4

Objek selalu direferensikan di Jawa. Mereka tidak pernah melewati diri mereka sendiri.

Satu keuntungannya adalah ini menyederhanakan bahasa. Objek C ++ dapat direpresentasikan sebagai nilai atau referensi, menciptakan kebutuhan untuk menggunakan dua operator yang berbeda untuk mengakses anggota: .dan ->. (Ada alasan mengapa ini tidak dapat dikonsolidasikan; misalnya, smart pointer adalah nilai yang merupakan referensi, dan harus membuatnya berbeda.) Java hanya perlu ..

Alasan lain adalah bahwa polimorfisme harus dilakukan dengan referensi, bukan nilai; objek yang diperlakukan oleh nilai ada di sana, dan memiliki tipe tetap. Dimungkinkan untuk mengacaukannya dalam C ++.

Juga, Java dapat mengganti tugas default / salin / apa pun. Dalam C ++, ini adalah salinan yang kurang lebih dalam, sedangkan di Jawa itu adalah penugasan pointer sederhana / salin / apa pun, dengan .clone()dan jika Anda perlu menyalin.


Terkadang menjadi sangat jelek ketika Anda menggunakan '(* objek) ->'
Gustavo Cardoso

1
Perlu dicatat bahwa C ++ membedakan antara pointer, referensi dan nilai. SomeClass * adalah pointer ke objek. SomeClass & adalah referensi ke suatu objek. SomeClass adalah tipe nilai.
Semut

Saya sudah menanyakan @Rober pada pertanyaan awal, tetapi saya akan melakukannya juga di sini: perbedaan antara * dan & pada C ++ hanyalah hal teknis tingkat rendah, bukan? Apakah mereka, pada level tinggi, secara semantik mereka sama.
Gustavo Cardoso

3
@ Gustavo Cardoso: Perbedaannya adalah semantik; pada tingkat teknis yang rendah mereka umumnya identik. Pointer menunjuk ke suatu objek, atau NULL (nilai buruk yang ditentukan). Kecuali const, nilainya dapat diubah untuk menunjuk ke objek lain. Referensi adalah nama lain untuk suatu objek, tidak bisa NULL, dan tidak bisa diulang. Ini umumnya diimplementasikan dengan penggunaan pointer sederhana, tapi itu detail implementasi.
David Thornley

+1 untuk "polimorfisme harus dilakukan dengan referensi." Itu adalah detail yang sangat krusial yang diabaikan oleh sebagian besar jawaban lainnya.
Doval

4

Pernyataan awal Anda tentang objek C # yang diteruskan oleh referensi tidak benar. Dalam C #, objek adalah tipe referensi, tetapi secara default mereka dilewatkan oleh nilai seperti tipe nilai. Dalam kasus tipe referensi, "nilai" yang sedang disalin sebagai parameter metode pass-by-value adalah referensi itu sendiri, jadi perubahan properti di dalam metode akan tercermin di luar ruang lingkup metode.

Namun, jika Anda menetapkan ulang variabel parameter itu sendiri di dalam suatu metode, Anda akan melihat bahwa perubahan ini tidak tercermin di luar ruang lingkup metode. Sebaliknya, jika Anda benar-benar melewati parameter dengan referensi menggunakan refkata kunci, perilaku ini berfungsi seperti yang diharapkan.


3

Jawaban cepat

Para perancang bahasa Jawa dan sejenisnya ingin menerapkan konsep "semuanya adalah objek". Dan melewatkan data sebagai referensi sangat cepat dan tidak menghabiskan banyak memori.

Tambahan komentar membosankan yang diperpanjang

Selain itu, bahasa-bahasa tersebut menggunakan referensi objek (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), kebenarannya adalah bahwa referensi objek adalah penunjuk ke objek yang disamarkan. Nilai nol, alokasi memori, salinan referensi tanpa menyalin seluruh data suatu objek, semuanya adalah pointer objek, bukan objek biasa !!!

Dalam Object Pascal (bukan Delphi), anc C ++ (bukan Java, bukan C #), sebuah objek dapat dideklarasikan sebagai variabel alokasi statis, dan juga dengan variabel alokasi dinamis, melalui penggunaan pointer ("referensi objek" tanpa " sintaksis gula "). Setiap kasus menggunakan sintaks tertentu, dan tidak ada cara untuk bingung seperti di Jawa "dan teman". Dalam bahasa-bahasa tersebut, suatu objek dapat dilewatkan sebagai nilai atau sebagai referensi.

Pemrogram tahu kapan sintaks pointer diperlukan, dan kapan tidak diperlukan, tetapi dalam bahasa Jawa dan bahasa yang sama, ini membingungkan.

Sebelum Java ada atau menjadi arus utama, banyak programmer mempelajari OO dalam C ++ tanpa pointer, melewati nilai atau dengan referensi ketika diperlukan. Ketika beralih dari belajar ke aplikasi bisnis., Maka, mereka biasanya menggunakan pointer objek. Perpustakaan QT adalah contoh yang bagus untuk itu.

Ketika saya belajar Java, saya mencoba mengikuti semuanya adalah konsep objek, tetapi menjadi bingung dalam pengkodean. Akhirnya, saya berkata "ok, ini adalah objek yang dialokasikan secara dinamis dengan pointer dengan sintaks dari objek yang dialokasikan secara statis", dan tidak mengalami kesulitan untuk kode lagi.


3

Java dan C # mengambil kendali atas memori tingkat rendah dari Anda. "Tumpukan" tempat benda-benda yang Anda ciptakan berada menjalani kehidupannya sendiri; misalnya, pengumpul sampah menuai benda kapan pun dia mau.

Karena ada lapisan terpisah dari tipuan antara program Anda dan "tumpukan" itu, dua cara untuk merujuk ke objek, dengan nilai dan dengan penunjuk (seperti dalam C ++), menjadi tidak dapat dibedakan : Anda selalu merujuk ke objek "dengan penunjuk" ke suatu tempat di tumpukan. Itu sebabnya pendekatan desain seperti itu menjadikan pass-by-reference sebagai semantik standar penugasan. Java, C #, Ruby, dan sebagainya.

Di atas hanya menyangkut bahasa imperatif. Dalam bahasa yang disebutkan di atas kontrol atas memori akan diteruskan ke runtime, tapi desain bahasa juga mengatakan "hey, tetapi sebenarnya, ada adalah memori, dan ada yang benda-benda, dan mereka melakukan menempati memori". Bahasa fungsional abstrak lebih jauh, dengan mengecualikan konsep "memori" dari definisi mereka. Itu sebabnya pass-by-reference tidak selalu berlaku untuk semua bahasa di mana Anda tidak mengontrol memori tingkat rendah.


2

Saya dapat memikirkan beberapa alasan:

  • Menyalin tipe primitif sepele, biasanya diterjemahkan menjadi satu instruksi mesin.

  • Menyalin objek tidak sepele, objek dapat berisi anggota yang merupakan objek itu sendiri. Menyalin objek mahal dalam waktu dan memori CPU. Bahkan ada beberapa cara menyalin objek tergantung pada konteksnya.

  • Melewati objek dengan referensi itu murah dan itu juga menjadi berguna ketika Anda ingin berbagi / memperbarui informasi objek antara banyak klien objek.

  • Struktur data yang kompleks (terutama yang bersifat rekursif) memerlukan petunjuk. Melewati objek dengan referensi hanyalah cara yang lebih aman untuk melewati pointer.


1

Karena jika tidak, fungsi harus dapat secara otomatis membuat salinan (jelas dalam) dari segala jenis objek yang diteruskan ke sana. Dan biasanya itu tidak bisa ditebak untuk membuatnya. Jadi Anda harus mendefinisikan implementasi metode salin / klon untuk semua objek / kelas Anda.


Bisakah itu hanya menyalin dangkal dan menyimpan nilai aktual dan pointer ke objek lain?
Gustavo Cardoso

#Gustavo Cardoso, maka Anda dapat memodifikasi objek lain melalui yang ini, apakah yang Anda harapkan dari objek yang tidak dilewatkan sebagai referensi?
David

0

Karena Java dirancang sebagai C ++ yang lebih baik, dan C # dirancang sebagai Java yang lebih baik, dan para pengembang bahasa ini bosan dengan model objek C ++ yang rusak secara fundamental, di mana objek adalah tipe nilai.

Dua dari tiga prinsip dasar pemrograman berorientasi objek adalah pewarisan dan polimorfisme, dan memperlakukan objek sebagai tipe nilai alih-alih tipe referensi mendatangkan malapetaka pada keduanya. Ketika Anda melewatkan objek ke fungsi sebagai parameter, kompiler perlu tahu berapa byte yang harus dilewati. Ketika objek Anda adalah tipe referensi, jawabannya sederhana: ukuran pointer, sama untuk semua objek. Tetapi ketika objek Anda adalah tipe nilai, itu harus melewati ukuran sebenarnya dari nilai. Karena kelas turunan dapat menambahkan bidang baru, ini berarti sizeof (diturunkan)! = Sizeof (basis), dan polimorfisme keluar jendela.

Berikut adalah program C ++ sepele yang menunjukkan masalah:

#include <iostream> 
class Parent 
{ 
public: 
   int a;
   int b;
   int c;
   Parent(int ia, int ib, int ic) { 
      a = ia; b = ib; c = ic;
   };
   virtual void doSomething(void) { 
      std::cout << "Parent doSomething" << std::endl;
   }
};

class Child : public Parent {
public:
   int d;
   int e;
   Child(int id, int ie) : Parent(1,2,3) { 
      d = id; e = ie;
   };
   virtual void doSomething(void) {
      std::cout << "Child doSomething : D = " << d << std::endl;
   }
};

void foo(Parent a) {
   a.doSomething();
}

int main(void)
{
   Child c(4, 5);
   foo(c);
   return 0;
}

Output dari program ini bukanlah seperti program yang setara dalam bahasa OO yang waras, karena Anda tidak dapat melewatkan objek turunan berdasarkan nilai ke fungsi yang mengharapkan objek dasar, sehingga kompilator membuat copy constructor dan pass tersembunyi salinan bagian Orangtua dari objek Anak , bukannya melewati objek Anak seperti yang Anda perintahkan. Gotcha semantik tersembunyi seperti ini adalah alasan mengapa melewatkan objek dengan nilai harus dihindari dalam C ++ dan tidak mungkin sama sekali di hampir setiap bahasa OO lainnya.


Poin yang sangat bagus. Namun, saya fokus pada masalah pengembalian karena mengatasinya membutuhkan sedikit usaha; program ini dapat diperbaiki dengan penambahan satu ampersand: void foo (Induk & a)
Ant

OO benar-benar tidak berfungsi dengan baik tanpa petunjuk
Gustavo Cardoso

-1.00000000000000
P Shved

5
Penting untuk diingat bahwa Java adalah pass-by-value (ia melewati referensi objek dengan nilai, sementara primitif dilewatkan murni oleh nilai).
Nicole

3
@Pavel Shved - "keduanya lebih baik dari satu!" Atau, dengan kata lain, lebih banyak tali untuk digantung.
Nicole

0

Karena tidak akan ada polimorfisme sebaliknya.

Di Pemrograman OO, Anda dapat membuat Derivedkelas yang lebih besar dari yang Basesatu, dan kemudian meneruskannya ke fungsi yang mengharapkan Basesatu. Cukup sepele ya?

Kecuali bahwa ukuran argumen fungsi tetap, dan ditentukan pada waktu kompilasi. Anda dapat memperdebatkan semua yang Anda inginkan, kode yang dapat dieksekusi seperti ini, dan bahasa perlu dijalankan pada satu titik atau yang lain (bahasa yang ditafsirkan murni tidak dibatasi oleh ini ...)

Sekarang, ada satu bagian data yang terdefinisi dengan baik di komputer: alamat sel memori, biasanya dinyatakan sebagai satu atau dua "kata". Itu terlihat baik sebagai pointer atau referensi dalam bahasa pemrograman.

Jadi untuk melewatkan objek yang panjangnya sewenang-wenang, hal yang paling sederhana untuk dilakukan adalah melewatkan pointer / referensi ke objek ini.

Ini adalah batasan teknis Pemrograman OO.

Tapi karena untuk tipe besar, Anda biasanya lebih suka melewati referensi untuk menghindari penyalinan, itu umumnya tidak dianggap sebagai pukulan besar :)

Namun ada satu konsekuensi penting, di Java atau C #, ketika meneruskan objek ke suatu metode, Anda tidak tahu apakah objek Anda akan dimodifikasi oleh metode tersebut atau tidak. Itu membuat debugging / paralelisasi yang lebih sulit, dan ini adalah masalah Bahasa Fungsional dan Referensi Transparansi mencoba untuk mengatasi -> menyalin tidak terlalu buruk (ketika masuk akal).


-1

Jawabannya ada dalam nama (well anyways). Referensi (seperti alamat) hanya merujuk ke sesuatu yang lain, suatu nilai adalah salinan dari sesuatu yang lain. Saya yakin seseorang mungkin menyebutkan sesuatu dengan efek berikut tetapi akan ada keadaan di mana satu dan tidak yang lain cocok (Keamanan memori vs Efisiensi Memori). Ini semua tentang mengelola memori, memori, memori ...... MEMORY! : D


-2

Baiklah jadi saya tidak mengatakan ini adalah mengapa objek adalah tipe referensi atau lulus dengan referensi tapi saya bisa memberi Anda sebuah contoh mengapa ini adalah ide yang sangat bagus dalam jangka panjang.

Jika saya tidak salah, ketika Anda mewarisi kelas di C ++, semua metode dan properti dari kelas itu secara fisik disalin ke kelas anak. Itu seperti menulis isi kelas itu lagi di dalam kelas anak.

Jadi ini berarti bahwa ukuran total data di kelas anak Anda adalah kombinasi dari barang-barang di kelas induk dan kelas turunan.

EG: #termasuk

class Top 
{   
    int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Middle : Top 
{   
    int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Bottom : Middle
{   
    int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

int main()
{   
    using namespace std;

    int arr[20];
    cout << "Size of array of 20 ints: " << sizeof(arr) << endl;

    Top top;
    Middle middle;
    Bottom bottom;

    cout << "Size of Top Class: " << sizeof(top) << endl;
    cout << "Size of middle Class: " << sizeof(middle) << endl;
    cout << "Size of bottom Class: " << sizeof(bottom) << endl;

}   

Yang akan menunjukkan kepada Anda:

Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240

Ini berarti bahwa jika Anda memiliki hierarki besar dari beberapa kelas, ukuran total objek, seperti yang dinyatakan di sini akan menjadi kombinasi dari semua kelas tersebut. Jelas, benda-benda ini akan sangat besar dalam banyak kasus.

Solusinya, saya percaya, adalah membuatnya di heap dan menggunakan pointer. Ini berarti bahwa ukuran objek kelas dengan beberapa orang tua akan dapat dikelola, dalam arti tertentu.

Inilah sebabnya mengapa menggunakan referensi akan menjadi metode yang lebih disukai untuk melakukan ini.


1
ini tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam 13 jawaban sebelumnya
agas
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.