Apa kelemahan dari tipe yang tidak dapat diubah?

12

Saya melihat diri saya menggunakan lebih banyak tipe yang tidak dapat diubah ketika instance kelas tidak diharapkan untuk diubah . Ini membutuhkan lebih banyak pekerjaan (lihat contoh di bawah), tetapi membuatnya lebih mudah untuk menggunakan tipe-tipe dalam lingkungan multithreaded.

Pada saat yang sama, saya jarang melihat tipe yang tidak dapat diubah dalam aplikasi lain, bahkan ketika kemampuan berubah tidak menguntungkan siapa pun.

Pertanyaan: Mengapa tipe yang tidak dapat diubah sangat jarang digunakan dalam aplikasi lain?

  • Apakah ini karena lebih lama menulis kode untuk tipe yang tidak dapat diubah,
  • Atau apakah saya kehilangan sesuatu dan ada beberapa kelemahan penting saat menggunakan jenis yang tidak dapat diubah?

Contoh dari kehidupan nyata

Katakanlah Anda dapatkan Weatherdari RESTful API seperti itu:

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

Apa yang biasanya kita lihat adalah (baris dan komentar baru dihapus untuk mempersingkat kode):

public sealed class Weather
{
    public City CorrespondingCity { get; set; }
    public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
    public int PrecipitationRisk { get; set; }
    public int Temperature { get; set; }
}

Di sisi lain, saya akan menulis dengan cara ini, mengingat mendapatkan Weatherdari API, kemudian mengubahnya akan aneh: mengubah Temperatureatau Skytidak akan mengubah cuaca di dunia nyata, dan perubahan CorrespondingCityjuga tidak masuk akal.

public sealed class Weather
{
    private readonly City correspondingCity;
    private readonly SkyState sky;
    private readonly int precipitationRisk;
    private readonly int temperature;

    public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
        int temperature)
    {
        this.correspondingCity = correspondingCity;
        this.sky = sky;
        this.precipitationRisk = precipitationRisk;
        this.temperature = temperature;
    }

    public City CorrespondingCity { get { return this.correspondingCity; } }
    public SkyState Sky { get { return this.sky; } }
    public int PrecipitationRisk { get { return this.precipitationRisk; } }
    public int Temperature { get { return this.temperature; } }
}
Arseni Mourzenko
sumber
3
"Itu membutuhkan lebih banyak pekerjaan" - kutipan diperlukan. Dalam pengalaman saya itu membutuhkan lebih sedikit kerja.
Konrad Rudolph
1
@KonradRudolph: dengan lebih banyak pekerjaan , maksud saya lebih banyak kode untuk menulis untuk membuat kelas yang tidak dapat diubah. Contoh dari pertanyaan saya menggambarkan hal ini, dengan 7 baris untuk kelas yang bisa berubah dan 19 untuk yang tidak bisa diubah.
Arseni Mourzenko
Anda dapat mengurangi pengetikan kode dengan menggunakan fitur Cuplikan Kode di Visual Studio jika Anda menggunakannya. Anda dapat membuat cuplikan khusus dan membiarkan IDE menentukan bidang dan properti secara bersamaan dengan beberapa tombol. Jenis yang tidak dapat diubah sangat penting untuk multithreading dan digunakan secara luas dalam bahasa seperti Scala.
Mert Akcakaya
@Mert: Cuplikan kode bagus untuk hal-hal sederhana. Menulis cuplikan kode yang akan membangun kelas penuh dengan komentar bidang dan properti serta urutan yang benar bukanlah tugas yang mudah.
Arseni Mourzenko
5
Saya tidak setuju dengan contoh yang diberikan, versi abadi melakukan lebih banyak hal dan berbeda. Anda bisa menghapus variabel tingkat instance dengan mendeklarasikan properti dengan accessors {get; private set;}, dan bahkan yang bisa berubah harus memiliki konstruktor, karena semua bidang itu harus selalu disetel dan mengapa Anda tidak memaksakan itu? Membuat dua perubahan yang sangat masuk akal membawa mereka ke fitur dan paritas LoC.
Phoshi

Jawaban:

16

Saya memprogram dalam C # dan Objective-C. Saya sangat suka mengetik, tetapi dalam kehidupan nyata saya selalu terpaksa membatasi penggunaannya, terutama untuk tipe data, karena alasan berikut:

  1. Upaya implementasi membandingkan dengan tipe yang bisa berubah. Dengan tipe yang tidak dapat diubah, Anda harus memiliki konstruktor yang membutuhkan argumen untuk semua properti. Teladan Anda adalah contoh yang baik. Coba bayangkan Anda memiliki 10 kelas, masing-masing memiliki 5-10 properti. Untuk mempermudah, Anda mungkin perlu memiliki kelas pembangun untuk membuat atau membuat contoh yang diubah dengan cara yang mirip dengan StringBuilderatau UriBuilderdalam C #, atau WeatherBuilderdalam kasus Anda. Ini adalah alasan utama bagi saya karena banyak kelas yang saya desain tidak sepadan dengan usaha seperti itu.
  2. Kegunaan konsumen . Tipe yang tidak berubah lebih sulit digunakan dibandingkan dengan tipe yang bisa berubah. Instansiasi membutuhkan inisialisasi semua nilai. Kekekalan juga berarti bahwa kita tidak dapat mengirimkan instance ke suatu metode untuk mengubah nilainya tanpa menggunakan builder, dan jika kita perlu memiliki builder maka kelemahannya ada pada (1) saya.
  3. Kompatibilitas dengan kerangka bahasa. Banyak kerangka kerja data memerlukan tipe yang bisa berubah dan konstruktor default untuk beroperasi. Misalnya, Anda tidak dapat melakukan kueri LINQ-to-SQL bersarang dengan tipe yang tidak dapat diubah, dan Anda tidak dapat mengikat properti yang akan diedit dalam editor seperti TextBox Windows Forms.

Singkatnya, kekekalan baik untuk objek yang berperilaku seperti nilai, atau hanya memiliki beberapa properti. Sebelum membuat sesuatu yang abadi, Anda harus mempertimbangkan upaya yang diperlukan dan kegunaan kelas itu sendiri setelah membuatnya tidak berubah.

tia
sumber
11
"Instansiasi membutuhkan semua nilai sebelumnya.": Juga tipe yang bisa berubah tidak, kecuali jika Anda menerima risiko memiliki objek dengan bidang yang tidak diinisialisasi mengambang di sekitar ...
Giorgio
@Iorgio Untuk tipe yang bisa berubah, konstruktor default harus menginisialisasi instance ke keadaan default dan keadaan instance dapat diubah kemudian setelah instantiation.
tia
8
Untuk tipe yang tidak dapat diubah, Anda dapat memiliki konstruktor default yang sama, dan membuat salinan nanti menggunakan konstruktor lain. Jika nilai default adalah valid untuk tipe yang bisa berubah, mereka juga harus valid untuk tipe yang tidak dapat diubah karena dalam kedua kasus Anda memodelkan entitas yang sama. Atau apa bedanya?
Giorgio
1
Satu hal lagi yang perlu dipertimbangkan adalah apa yang dilambangkan oleh tipe. Kontrak data tidak menghasilkan tipe yang tidak dapat diubah karena semua poin ini, tetapi tipe layanan yang mendapatkan inisialisasi dengan dependensi atau hanya membaca data dan kemudian melakukan operasi sangat bagus untuk ketidakterbandingan karena operasi akan bekerja secara konsisten dan kondisi layanan tidak dapat berubah menjadi risiko itu.
Kevin
1
Saat ini saya kode dalam F #, di mana imutabilitas adalah default (karenanya lebih mudah untuk diterapkan). Saya menemukan bahwa poin Anda 3 adalah rintangan besar. Segera setelah Anda menggunakan sebagian besar pustaka .Net standar, Anda harus melewati rintangan. (Dan jika mereka menggunakan refleksi dan memotong imutabilitas ... argh!)
Guran
5

Umumnya jenis yang tidak dapat diubah yang dibuat dalam bahasa yang tidak berputar di sekitar ketidakmampuan akan cenderung menghabiskan lebih banyak waktu pengembang untuk membuat serta berpotensi digunakan jika mereka memerlukan beberapa jenis "pembangun" objek untuk mengekspresikan perubahan yang diinginkan (ini tidak berarti bahwa keseluruhan pekerjaan akan lebih banyak, tetapi ada biaya di muka dalam kasus ini). Terlepas dari apakah bahasa membuatnya sangat mudah untuk membuat tipe yang tidak dapat diubah atau tidak, itu akan cenderung selalu membutuhkan beberapa pemrosesan dan overhead memori untuk tipe data yang tidak sepele.

Membuat Fungsi Tanpa Efek Samping

Jika Anda bekerja dalam bahasa yang tidak berputar di sekitar ketidakberdayaan, maka saya pikir pendekatan pragmatis bukanlah berusaha membuat setiap jenis data tunggal tidak dapat diubah. Pola pikir yang berpotensi jauh lebih produktif yang memberi Anda banyak manfaat yang sama adalah fokus pada memaksimalkan jumlah fungsi dalam sistem Anda yang menyebabkan efek samping nol .

Sebagai contoh sederhana, jika Anda memiliki fungsi yang menyebabkan efek samping seperti ini:

// Make 'x' the absolute value of itself.
void make_abs(int& x);

Maka kita tidak memerlukan tipe data integer yang tidak dapat diubah yang melarang operator seperti penugasan pasca inisialisasi untuk membuat fungsi tersebut menghindari efek samping. Kita bisa melakukan ini:

// Returns the absolute value of 'x'.
int abs(int x);

Sekarang fungsi tidak berantakan dengan xatau apa pun di luar ruang lingkupnya, dan dalam kasus sepele ini kita bahkan mungkin mencukur beberapa siklus dengan menghindari overhead yang terkait dengan tipuan / aliasing. Paling tidak versi kedua seharusnya tidak lebih mahal secara komputasi daripada yang pertama.

Hal-hal Yang Mahal untuk Menyalin Sepenuhnya

Tentu saja sebagian besar kasus tidak sepele ini jika kita ingin menghindari membuat fungsi menyebabkan efek samping. Kasus penggunaan dunia nyata yang kompleks mungkin lebih seperti ini:

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

Pada titik mana mesh mungkin memerlukan beberapa ratus megabyte memori dengan lebih dari seratus ribu poligon, bahkan lebih banyak simpul dan tepi, beberapa peta tekstur, target morf, dll. Akan sangat mahal untuk menyalin seluruh mesh hanya untuk membuat ini transformberfungsi bebas dari efek samping, seperti:

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

Dan dalam kasus ini di mana menyalin sesuatu secara keseluruhan biasanya akan menjadi overhead epik di mana saya merasa berguna untuk berubah Meshmenjadi struktur data yang persisten dan tipe yang tidak dapat diubah dengan "pembangun" analogis untuk membuat versi modifikasi dari itu sehingga dapat dengan mudah menyalin dan menghitung jumlah bagian yang tidak unik. Semuanya dengan fokus untuk dapat menulis fungsi mesh yang bebas dari efek samping.

Struktur Data Persisten

Dan dalam kasus-kasus ini di mana menyalin semuanya sangat mahal, saya menemukan upaya mendesain yang kekal Meshuntuk benar-benar melunasi walaupun biayanya sedikit curam di muka, karena itu tidak hanya menyederhanakan keamanan thread. Ini juga menyederhanakan pengeditan non-destruktif (memungkinkan pengguna untuk melapisi operasi mesh tanpa memodifikasi salinan aslinya), membatalkan sistem (sekarang sistem undo hanya dapat menyimpan salinan mesh yang tidak berubah sebelum perubahan yang dilakukan oleh operasi tanpa meledakkan memori gunakan), dan pengecualian-keselamatan (sekarang jika pengecualian terjadi pada fungsi di atas, fungsi tidak harus memutar kembali dan membatalkan semua efek sampingnya karena tidak menyebabkan apapun untuk memulai).

Saya yakin dapat mengatakan dalam kasus-kasus ini bahwa waktu yang diperlukan untuk membuat struktur data yang besar dan kekal ini menghemat lebih banyak waktu daripada biayanya, karena saya telah membandingkan biaya perawatan dari desain baru ini dengan yang sebelumnya yang berkisar seputar kemampuan berubah-ubah dan fungsi yang menyebabkan efek samping, dan desain sebelumnya yang bisa berubah biaya jauh lebih banyak waktu dan jauh lebih rentan terhadap kesalahan manusia, terutama di daerah yang benar-benar menggoda bagi pengembang untuk mengabaikan selama waktu krisis, seperti pengecualian-keselamatan.

Jadi saya pikir tipe data yang tidak dapat diubah benar-benar terbayar dalam kasus-kasus ini, tetapi tidak semuanya harus dibuat tidak dapat diubah untuk membuat sebagian besar fungsi dalam sistem Anda bebas dari efek samping. Banyak hal yang cukup murah untuk disalin secara penuh. Juga banyak aplikasi dunia nyata yang perlu menyebabkan beberapa efek samping di sana-sini (paling tidak seperti menyimpan file), tetapi biasanya ada lebih banyak fungsi yang bisa tanpa efek samping.

Maksud dari memiliki beberapa tipe data yang tidak dapat diubah kepada saya adalah untuk memastikan bahwa kita dapat menulis jumlah fungsi maksimum agar bebas dari efek samping tanpa menimbulkan overhead epik dalam bentuk penyalinan mendalam struktur data besar-besaran kiri dan kanan penuh ketika hanya sebagian kecil dari mereka perlu dimodifikasi. Memiliki struktur data yang terus-menerus dalam kasus-kasus itu kemudian berakhir menjadi detail optimisasi untuk memungkinkan kita menulis fungsi kita agar bebas dari efek samping tanpa membayar biaya besar untuk melakukannya.

Overhead Abadi

Sekarang secara konseptual versi yang bisa berubah akan selalu memiliki keunggulan dalam efisiensi. Selalu ada overhead komputasi yang terkait dengan struktur data yang tidak dapat diubah. Tapi saya menganggapnya sebagai pertukaran yang layak dalam kasus-kasus yang saya jelaskan di atas, dan Anda dapat fokus untuk membuat biaya overhead yang minimal. Saya lebih suka jenis pendekatan di mana kebenaran menjadi mudah dan optimasi menjadi lebih sulit daripada optimasi menjadi lebih mudah tetapi kebenaran menjadi lebih sulit. Ini hampir tidak demoralisasi untuk memiliki kode yang berfungsi sempurna dengan benar membutuhkan beberapa tune up lebih dari kode yang tidak berfungsi dengan benar di tempat pertama, tidak peduli seberapa cepat itu mencapai hasil yang salah.


sumber
3

Satu-satunya kelemahan yang dapat saya pikirkan adalah bahwa dalam teori penggunaan data tidak berubah mungkin lebih lambat dari yang bisa berubah - lebih lambat untuk membuat contoh baru, dan mengumpulkan yang sebelumnya daripada memodifikasi yang sudah ada.

"Masalah" lainnya adalah Anda tidak bisa hanya menggunakan tipe yang tidak dapat diubah. Pada akhirnya Anda harus menggambarkan keadaan dan Anda harus menggunakan tipe yang bisa berubah untuk melakukannya - tanpa mengubah keadaan Anda tidak dapat melakukan pekerjaan apa pun.

Tetapi masih aturan umum adalah untuk menggunakan tipe yang tidak berubah di mana pun Anda bisa dan membuat tipe bisa berubah hanya ketika benar-benar ada alasan untuk melakukannya ...

Dan untuk menjawab pertanyaan " Mengapa tipe yang tidak dapat diubah sangat jarang digunakan dalam aplikasi lain? " - Saya benar-benar tidak berpikir mereka ... di mana pun Anda melihat semua orang merekomendasikan membuat kelas Anda sama abadi seperti yang bisa mereka ... misalnya: http://www.javapractices.com/topic/TopicAction.do?Id=29

mrpyo
sumber
1
Kedua masalah Anda tidak ada di Haskell.
Florian Margaine
@FlorianMargaine Bisakah Anda menguraikan?
mrpyo
Kelambatan ini tidak benar berkat kompiler yang pintar. Dan di Haskell, bahkan I / O adalah melalui API abadi.
Florian Margaine
2
Masalah yang lebih mendasar daripada kecepatan adalah bahwa sulit bagi benda-benda yang tidak dapat diubah untuk mempertahankan identitas sementara keadaan mereka berubah. Jika Carobjek yang bisa berubah terus diperbarui dengan lokasi mobil fisik tertentu, maka jika saya memiliki referensi ke objek itu saya bisa mengetahui keberadaan mobil itu dengan cepat dan mudah. Jika Cartidak berubah, menemukan keberadaan saat ini kemungkinan akan jauh lebih sulit.
supercat
Anda harus mengkodekan dengan cukup cerdas kadang-kadang agar kompiler mengetahui bahwa tidak ada referensi ke objek sebelumnya yang tertinggal dan dengan demikian dapat memodifikasinya, atau melakukan transformasi deforestasi, dkk. Terutama di program yang lebih besar. Dan seperti yang dikatakan @supercat, identitas memang bisa menjadi masalah.
Macke
0

Untuk memodelkan sistem dunia nyata mana pun yang bisa berubah, keadaan yang bisa berubah perlu dikodekan di suatu tempat, entah bagaimana. Ada tiga cara utama suatu objek bisa tahan keadaan berubah-ubah:

  • Menggunakan referensi yang bisa berubah ke objek yang tidak dapat diubah
  • Menggunakan referensi abadi ke objek yang bisa berubah
  • Menggunakan referensi yang bisa berubah ke objek yang bisa berubah

Penggunaan pertama memudahkan objek untuk membuat snapshot abadi dari kondisi saat ini. Menggunakan yang kedua memudahkan objek untuk membuat tampilan langsung dari kondisi saat ini. Menggunakan yang ketiga kadang-kadang dapat membuat tindakan tertentu lebih efisien dalam kasus-kasus di mana ada sedikit kebutuhan yang diharapkan untuk snapshot abadi atau pandangan langsung.

Selain fakta bahwa memperbarui status yang disimpan menggunakan referensi yang bisa berubah ke objek yang tidak dapat diubah seringkali lebih lambat daripada memperbarui status yang disimpan dengan menggunakan objek yang bisa diubah, menggunakan referensi yang bisa berubah akan mengharuskan seseorang untuk melupakan kemungkinan membangun pandangan hidup yang murah dari negara tersebut. Jika seseorang tidak perlu membuat tampilan langsung, itu tidak masalah; Namun, jika seseorang perlu membuat tampilan langsung, ketidakmampuan untuk menggunakan referensi yang tidak dapat diubah akan membuat semua operasi dengan tampilan - keduanya membaca dan menulis- jauh lebih lambat dari yang seharusnya. Jika kebutuhan untuk snapshot yang tidak dapat diubah melebihi kebutuhan untuk tayangan langsung, peningkatan kinerja dari snapshot yang tidak berubah dapat membenarkan hit kinerja untuk tayangan langsung, tetapi jika seseorang membutuhkan tayangan langsung dan tidak memerlukan snapshot, menggunakan referensi yang tidak dapat diubah ke objek yang dapat diubah adalah cara untuk pergi.

supercat
sumber
0

Dalam kasus Anda jawabannya terutama karena C # memiliki dukungan yang buruk untuk Kekekalan ...

Akan lebih bagus jika:

  • semuanya akan berubah secara default kecuali disebutkan sebaliknya (yaitu dengan kata kunci 'bisa berubah'), mencampur tipe tidak dapat berubah dan dapat berubah membingungkan

  • metode mutasi ( With) akan tersedia secara otomatis - meskipun ini sudah dapat dicapai lihat Dengan

  • akan ada cara untuk mengatakan hasil dari pemanggilan metode tertentu (yaitu ImmutableList<T>.Add) tidak dapat dibuang atau setidaknya akan menghasilkan peringatan

  • Dan terutama jika kompilator dapat memastikan imutabilitas sebanyak yang diminta (lihat https://github.com/dotnet/roslyn/issues/159 )

kofifus
sumber
1
Kembali ke poin ketiga, ReSharper memiliki MustUseReturnValueAttributeatribut khusus yang melakukan hal itu. PureAttributememiliki efek yang sama dan bahkan lebih baik untuk ini.
Sebastian Redl
-1

Mengapa tipe yang tidak dapat diubah sangat jarang digunakan dalam aplikasi lain?

Ketidakpedulian? Kurang pengalaman?

Objek yang tidak dapat berubah secara luas dianggap sebagai yang unggul saat ini, tetapi ini merupakan perkembangan yang relatif baru. Insinyur yang tidak mengikuti perkembangan terbaru, atau hanya terjebak dalam 'apa yang mereka ketahui' tidak akan menggunakannya. Dan memang butuh sedikit perubahan desain untuk menggunakannya secara efektif. Jika aplikasi sudah tua, atau insinyur lemah dalam keterampilan desain maka menggunakannya mungkin canggung atau menyusahkan.

Telastyn
sumber
"Insinyur yang belum mengikuti perkembangan terbaru": Orang bisa mengatakan bahwa seorang insinyur juga harus belajar tentang teknologi non-mainstream. Gagasan imutabilitas baru-baru ini menjadi arus utama, tetapi ini adalah ide yang cukup lama dan didukung (jika tidak ditegakkan) oleh bahasa yang lebih tua seperti Skema, SML, Haskell. Jadi siapa pun yang terbiasa melihat di luar bahasa arus utama dapat mempelajarinya bahkan 30 tahun yang lalu.
Giorgio
@Iorgio: di beberapa negara, banyak insinyur masih menulis kode C # tanpa LINQ, tanpa FP, tanpa iterator dan tanpa obat generik, jadi sebenarnya, mereka entah bagaimana melewatkan semua yang terjadi dengan C # sejak 2003. Jika mereka bahkan tidak tahu bahasa preferensi mereka , Saya sulit membayangkan mereka tahu bahasa non-mainstream.
Arseni Mourzenko
@MainMa: Bagus bahwa Anda menulis kata insinyur dalam huruf miring.
Giorgio
@Giorgio: di negara saya, mereka juga disebut arsitek , konsultan , dan banyak istilah lain yang terkenal, tidak pernah ditulis dalam huruf miring. Di perusahaan tempat saya bekerja saat ini, saya disebut analis developer , dan saya diharapkan menghabiskan waktu menulis CSS untuk kode HTML lama yang jelek. Gelar pekerjaan mengganggu pada banyak tingkatan.
Arseni Mourzenko
1
@MainMa: Saya setuju. Gelar-gelar seperti insinyur atau konsultan sering hanya kata-kata pendek tanpa makna yang diterima secara luas. Mereka sering digunakan untuk membuat seseorang atau posisi mereka lebih penting / bergengsi daripada yang sebenarnya.
Giorgio