Đối tượng biến đổi và bất biến


173

Tôi đang cố gắng để có được đầu của tôi xung quanh các đối tượng có thể thay đổi và bất biến. Việc sử dụng các đối tượng có thể thay đổi sẽ nhận được rất nhiều báo chí xấu (ví dụ: trả về một chuỗi các chuỗi từ một phương thức) nhưng tôi gặp khó khăn trong việc hiểu những tác động tiêu cực của việc này là gì. Các thực hành tốt nhất xung quanh việc sử dụng các đối tượng có thể thay đổi là gì? Bạn nên tránh chúng bất cứ khi nào có thể?


stringlà bất biến, ít nhất là trong .NET, và tôi nghĩ trong nhiều ngôn ngữ hiện đại khác cũng vậy.
Domenic

4
nó phụ thuộc vào chuỗi thực sự trong ngôn ngữ - erlang 'chuỗi' chỉ là một mảng của int và "chuỗi" haskell là các mảng ký tự.
Chii

4
Chuỗi Ruby là một mảng byte có thể thay đổi . Hoàn toàn khiến tôi phát điên rằng bạn có thể thay đổi chúng tại chỗ.
Daniel Spiewak

6
Daniel - tại sao? Bạn có thấy mình làm điều đó một cách tình cờ?

5
@DanielSpiewak Giải pháp cho các loại hạt điều khiển mà bạn có thể thay đổi chuỗi tại chỗ rất đơn giản: chỉ cần không làm điều đó. Giải pháp cho việc điều khiển các loại hạt vì bạn không thể thay đổi chuỗi tại chỗ không đơn giản.
Kaz

Câu trả lời:


162

Vâng, có một vài khía cạnh này.

  1. Các đối tượng có thể thay đổi mà không có danh tính tham chiếu có thể gây ra lỗi tại các thời điểm kỳ lạ. Ví dụ, hãy xem xét một Personbean với equalsphương thức dựa trên giá trị :

    Map<Person, String> map = ...
    Person p = new Person();
    map.put(p, "Hey, there!");
    
    p.setName("Daniel");
    map.get(p);       // => null
    

    Trường Personhợp bị "mất" trong bản đồ khi được sử dụng làm khóa vì tính công hashCodebằng và giá trị của nó dựa trên các giá trị có thể thay đổi. Những giá trị đó đã thay đổi bên ngoài bản đồ và tất cả các băm trở nên lỗi thời. Các nhà lý luận thích đàn hạc về điểm này, nhưng trong thực tế tôi không thấy nó có quá nhiều vấn đề.

  2. Một khía cạnh khác là "tính hợp lý" hợp lý của mã của bạn. Đây là một thuật ngữ khó định nghĩa, bao gồm mọi thứ từ khả năng đọc đến lưu lượng. Nhìn chung, bạn sẽ có thể nhìn vào một đoạn mã và dễ dàng hiểu nó làm gì. Nhưng quan trọng hơn thế, bạn sẽ có thể thuyết phục bản thân rằng nó làm những gì nó làm một cách chính xác . Khi các đối tượng có thể thay đổi độc lập trên các "miền" mã khác nhau, đôi khi việc theo dõi xem đâu là lý do tại sao và tại sao (" hành động ma quái ở khoảng cách xa "). Đây là một khái niệm khó khăn hơn để minh họa, nhưng nó là thứ thường phải đối mặt trong các kiến ​​trúc lớn hơn, phức tạp hơn.

  3. Cuối cùng, các đối tượng đột biến là kẻ giết người trong các tình huống đồng thời. Bất cứ khi nào bạn truy cập một đối tượng có thể thay đổi từ các luồng riêng biệt, bạn phải xử lý khóa. Điều này làm giảm thông lượng và làm cho mã của bạn khó bảo trì hơn đáng kể . Một hệ thống đủ phức tạp sẽ thổi bay vấn đề này đến mức không thể duy trì được (gần như đối với các chuyên gia đồng thời).

Các đối tượng bất biến (và đặc biệt hơn là các bộ sưu tập bất biến) tránh tất cả các vấn đề này. Khi bạn hiểu được cách chúng hoạt động, mã của bạn sẽ phát triển thành thứ gì đó dễ đọc hơn, dễ bảo trì hơn và ít có khả năng thất bại theo những cách kỳ lạ và không thể đoán trước. Các đối tượng bất biến thậm chí còn dễ dàng hơn để kiểm tra, không chỉ do tính dễ bị chế giễu của chúng, mà còn do các mẫu mã mà chúng có xu hướng thực thi. Nói tóm lại, họ thực hành tốt xung quanh!

Như đã nói, tôi hầu như không quá nhiệt tình trong vấn đề này. Một số vấn đề không được mô hình độc đáo khi mọi thứ đều bất biến. Nhưng tôi nghĩ rằng bạn nên cố gắng đẩy càng nhiều mã của mình theo hướng đó càng tốt, tất nhiên là giả sử rằng bạn đang sử dụng một ngôn ngữ khiến điều này trở thành ý kiến ​​có thể thực hiện được (C / C ++ làm cho điều này trở nên rất khó khăn, cũng như Java) . Tóm lại: những lợi thế phụ thuộc phần nào vào vấn đề của bạn, nhưng tôi sẽ có xu hướng thích sự bất biến.


11
Phản ứng tuyệt vời. Tuy nhiên, một câu hỏi nhỏ: C ++ có hỗ trợ tốt cho tính bất biến không? Không phải là tính năng chính xác đủ?
Dimitri C.

1
@DimitriC.: C ++ thực sự có một số tính năng cơ bản hơn, đáng chú ý nhất là sự phân biệt tốt hơn giữa các vị trí lưu trữ được cho là gói gọn trạng thái không chia sẻ với các trạng thái đóng gói nhận dạng đối tượng.
supercat

2
Trong trường hợp lập trình Java Joshua Bloch mang đến lời giải thích tốt về chủ đề này trong cuốn sách nổi tiếng Java hiệu quả (Mục 15)
dMathieuD

27

Đối tượng bất biến so với Bộ sưu tập bất biến

Một trong những điểm tốt hơn trong cuộc tranh luận về các đối tượng có thể thay đổi và bất biến là khả năng mở rộng khái niệm bất biến cho các bộ sưu tập. Một đối tượng bất biến là một đối tượng thường đại diện cho một cấu trúc logic duy nhất của dữ liệu (ví dụ: một chuỗi bất biến). Khi bạn có một tham chiếu đến một đối tượng bất biến, nội dung của đối tượng sẽ không thay đổi.

Một bộ sưu tập bất biến là một bộ sưu tập không bao giờ thay đổi.

Khi tôi thực hiện một thao tác trên một bộ sưu tập có thể thay đổi, sau đó tôi thay đổi bộ sưu tập tại chỗ và tất cả các thực thể có tham chiếu đến bộ sưu tập sẽ thấy sự thay đổi.

Khi tôi thực hiện một thao tác trên một bộ sưu tập bất biến, một tham chiếu được trả về một bộ sưu tập mới phản ánh sự thay đổi. Tất cả các thực thể có tham chiếu đến các phiên bản trước của bộ sưu tập sẽ không thấy sự thay đổi.

Việc triển khai thông minh không nhất thiết phải sao chép (sao chép) toàn bộ bộ sưu tập để cung cấp sự bất biến đó. Ví dụ đơn giản nhất là ngăn xếp được triển khai như một danh sách liên kết đơn và các hoạt động đẩy / pop. Bạn có thể sử dụng lại tất cả các nút từ bộ sưu tập trước trong bộ sưu tập mới, chỉ thêm một nút duy nhất cho việc đẩy và nhân bản không có nút nào cho cửa sổ bật lên. Hoạt động Push_tail trên một danh sách liên kết đơn, mặt khác, không đơn giản hoặc hiệu quả.

Các biến / tham chiếu không thay đổi so với Mutable

Một số ngôn ngữ chức năng lấy khái niệm bất biến cho chính các tham chiếu đối tượng, chỉ cho phép một phép gán tham chiếu duy nhất.

  • Trong Erlang, điều này đúng với tất cả các "biến". Tôi chỉ có thể gán các đối tượng cho một tham chiếu một lần. Nếu tôi vận hành trên một bộ sưu tập, tôi sẽ không thể gán lại bộ sưu tập mới cho tham chiếu cũ (tên biến).
  • Scala cũng xây dựng ngôn ngữ này với ngôn ngữ với tất cả các tham chiếu được khai báo bằng var hoặc val , vals chỉ là một phép gán duy nhất và quảng bá một kiểu chức năng, nhưng các vars cho phép cấu trúc chương trình giống như C hoặc Java hơn.
  • Khai báo var / val là bắt buộc, trong khi nhiều ngôn ngữ truyền thống sử dụng các sửa đổi tùy chọn như cuối cùng trong java và const trong C.

Dễ phát triển so với hiệu suất

Hầu như luôn luôn lý do để sử dụng một đối tượng bất biến là để thúc đẩy lập trình miễn phí hiệu ứng phụ và lý luận đơn giản về mã (đặc biệt là trong môi trường song song / đồng thời cao). Bạn không phải lo lắng về việc dữ liệu cơ bản bị thay đổi bởi một thực thể khác nếu đối tượng là bất biến.

Hạn chế chính là hiệu suất. Dưới đây là một bài viết về một bài kiểm tra đơn giản mà tôi đã làm trong Java so sánh một số vật thể bất biến và có thể thay đổi trong một vấn đề đồ chơi.

Các vấn đề về hiệu năng có rất nhiều trong các ứng dụng, nhưng không phải tất cả, đó là lý do tại sao nhiều gói số lớn, chẳng hạn như lớp Numpy Array trong Python, cho phép cập nhật tại chỗ các mảng lớn. Điều này sẽ rất quan trọng đối với các khu vực ứng dụng sử dụng các phép toán ma trận và vectơ lớn. Vấn đề lớn song song dữ liệu lớn và tính toán này đạt được tốc độ lớn bằng cách vận hành tại chỗ.


12

Kiểm tra bài đăng trên blog này: http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . Nó giải thích tại sao các vật thể bất biến tốt hơn là có thể thay đổi. Nói ngắn gọn:

  • đối tượng bất biến đơn giản hơn để xây dựng, kiểm tra và sử dụng
  • đối tượng thực sự bất biến luôn an toàn chủ đề
  • chúng giúp tránh khớp nối tạm thời
  • sử dụng của họ là miễn phí tác dụng phụ (không có bản sao phòng thủ)
  • vấn đề đột biến danh tính là tránh
  • họ luôn có nguyên tử thất bại
  • chúng dễ dàng hơn nhiều để lưu trữ

10

Đối tượng bất biến là một khái niệm rất mạnh mẽ. Chúng lấy đi rất nhiều gánh nặng của việc cố gắng giữ các đối tượng / biến nhất quán cho tất cả các máy khách.

Bạn có thể sử dụng chúng cho các đối tượng mức độ thấp, không đa hình - như lớp CPoint - được sử dụng chủ yếu với ngữ nghĩa giá trị.

Hoặc bạn có thể sử dụng chúng cho các giao diện đa hình, mức độ cao - như một hàm IF đại diện cho một hàm toán học - được sử dụng riêng cho ngữ nghĩa đối tượng.

Ưu điểm lớn nhất: tính bất biến + ngữ nghĩa đối tượng + con trỏ thông minh làm cho quyền sở hữu đối tượng không thành vấn đề, tất cả các máy khách của đối tượng đều có bản sao riêng của chúng theo mặc định. Ngẫu nhiên điều này cũng có nghĩa là hành vi xác định trong sự hiện diện của đồng thời.

Nhược điểm: khi được sử dụng với các đối tượng chứa nhiều dữ liệu, việc tiêu thụ bộ nhớ có thể trở thành một vấn đề. Một giải pháp cho vấn đề này có thể là duy trì hoạt động trên một đối tượng tượng trưng và thực hiện đánh giá lười biếng. Tuy nhiên, điều này sau đó có thể dẫn đến các chuỗi tính toán tượng trưng, ​​có thể ảnh hưởng tiêu cực đến hiệu suất nếu giao diện không được thiết kế để phù hợp với các hoạt động tượng trưng. Một cái gì đó chắc chắn nên tránh trong trường hợp này là trả lại khối lượng lớn bộ nhớ từ một phương thức. Kết hợp với các hoạt động tượng trưng được xâu chuỗi, điều này có thể dẫn đến tiêu thụ bộ nhớ lớn và suy giảm hiệu suất.

Vì vậy, các đối tượng bất biến chắc chắn là cách suy nghĩ chính của tôi về thiết kế hướng đối tượng, nhưng chúng không phải là một giáo điều. Họ giải quyết rất nhiều vấn đề cho khách hàng của các đối tượng, nhưng cũng tạo ra nhiều vấn đề, đặc biệt là cho những người thực hiện.


Tôi nghĩ rằng tôi đã hiểu nhầm Phần 4 vì lợi thế lớn nhất: tính bất biến + ngữ nghĩa đối tượng + con trỏ thông minh làm cho quyền sở hữu đối tượng trở thành điểm "moot". Vì vậy, nó gây tranh cãi? Tôi nghĩ rằng bạn đang sử dụng "moot" không chính xác ... Xem câu tiếp theo là đối tượng đã ngụ ý "hành vi xác định" từ hành vi "tranh luận" (tranh luận) của nó.
Benjamin

Bạn nói đúng, tôi đã sử dụng 'moot' không chính xác. Hãy xem xét nó đã thay đổi :)
QBziZ

6

Bạn nên xác định ngôn ngữ bạn đang nói về. Đối với các ngôn ngữ cấp thấp như C hoặc C ++, tôi thích sử dụng các đối tượng có thể thay đổi để tiết kiệm không gian và giảm tốc độ bộ nhớ. Trong các ngôn ngữ cấp cao hơn, các đối tượng không thay đổi giúp dễ dàng suy luận về hành vi của mã (đặc biệt là mã đa luồng) vì không có "hành động ma quái ở khoảng cách xa".


Bạn đang đề xuất rằng các chủ đề bị vướng víu lượng tử? Điều đó khá khó khăn :) Các chủ đề thực sự, nếu bạn nghĩ về nó, khá gần để bị vướng mắc. Một thay đổi mà một chủ đề làm cho ảnh hưởng đến khác. +1
ndrewxie

4

Một đối tượng có thể thay đổi chỉ đơn giản là một đối tượng có thể được sửa đổi sau khi nó được tạo / khởi tạo, so với một đối tượng không thay đổi không thể sửa đổi (xem trang Wikipedia về chủ đề này). Một ví dụ về điều này trong ngôn ngữ lập trình là danh sách và bộ dữ liệu của Pythons. Danh sách có thể được sửa đổi (ví dụ: các mục mới có thể được thêm sau khi được tạo) trong khi các bộ dữ liệu không thể.

Tôi thực sự không nghĩ rằng có một câu trả lời rõ ràng về việc câu trả lời nào tốt hơn cho mọi tình huống. Cả hai đều có vị trí của họ.


1

Nếu một loại lớp là có thể thay đổi, một biến của loại lớp đó có thể có một số ý nghĩa khác nhau. Ví dụ: giả sử một đối tượng foocó một trường int[] arrvà nó giữ tham chiếu đến việc int[3]giữ các số {5, 7, 9}. Mặc dù loại trường được biết đến, có ít nhất bốn thứ khác nhau mà nó có thể đại diện:

  • Một tham chiếu có khả năng chia sẻ, tất cả những người nắm giữ chỉ quan tâm rằng nó gói gọn các giá trị 5, 7 và 9. Nếu foomuốn arrgói gọn các giá trị khác nhau, nó phải thay thế nó bằng một mảng khác chứa các giá trị mong muốn. Nếu muốn tạo một bản sao foo, người ta có thể cung cấp cho bản sao một tham chiếu đến arrhoặc một mảng mới chứa các giá trị {1,2,3}, tùy theo điều kiện nào thuận tiện hơn.

  • Tham chiếu duy nhất, ở bất kỳ đâu trong vũ trụ, đến một mảng bao gói các giá trị 5, 7 và 9. tập hợp ba vị trí lưu trữ mà tại thời điểm này giữ các giá trị 5, 7 và 9; nếu foomuốn nó đóng gói các giá trị 5, 8 và 9, nó có thể thay đổi mục thứ hai trong mảng đó hoặc tạo một mảng mới giữ các giá trị 5, 8 và 9 và từ bỏ giá trị cũ. Lưu ý rằng nếu muốn tạo một bản sao foo, một bản sao phải thay thế arrbằng một tham chiếu đến một mảng mới foo.arrđể duy trì như là tham chiếu duy nhất cho mảng đó ở bất cứ đâu trong vũ trụ.

  • Một tham chiếu đến một mảng thuộc sở hữu của một số đối tượng khác đã phơi bày nó foovì một số lý do (ví dụ có lẽ nó muốn foolưu trữ một số dữ liệu ở đó). Trong kịch bản này, arrkhông gói gọn nội dung của mảng, mà là danh tính của nó . Bởi vì thay thế arrbằng một tham chiếu đến một mảng mới sẽ thay đổi hoàn toàn ý nghĩa của nó, một bản sao của foonên giữ một tham chiếu đến cùng một mảng.

  • Một tham chiếu đến một mảng trong đó foolà chủ sở hữu duy nhất, nhưng đối với các tham chiếu đó được giữ bởi đối tượng khác vì một lý do nào đó (ví dụ: nó muốn có đối tượng khác để lưu trữ dữ liệu ở đó - flipside của trường hợp trước). Trong kịch bản này, arrgói gọn cả danh tính của mảng và nội dung của nó. Thay thế arrbằng một tham chiếu đến một mảng mới sẽ thay đổi hoàn toàn ý nghĩa của nó, nhưng việc arrtham chiếu đến một bản sao foo.arrsẽ vi phạm giả định foolà chủ sở hữu duy nhất. Do đó không có cách nào để sao chép foo.

Về lý thuyết, int[]nên là một loại đơn giản được xác định rõ ràng, nhưng nó có bốn ý nghĩa rất khác nhau. Ngược lại, một tham chiếu đến một đối tượng bất biến (ví dụ String) thường chỉ có một ý nghĩa. Phần lớn "sức mạnh" của các vật thể bất biến bắt nguồn từ thực tế đó.


1

Trường hợp có thể thay đổi được thông qua tham chiếu.

Trường hợp bất biến được thông qua bởi giá trị.

Ví dụ trừu tượng. Giả sử tồn tại một tệp có tên txtfile trong ổ cứng của tôi. Bây giờ, khi bạn hỏi txtfile từ tôi, tôi có thể trả lại ở hai chế độ:

  1. Tạo một lối tắt đến txtfile và pas phím tắt cho bạn, hoặc
  2. Lấy một bản sao cho txtfile và pas copy cho bạn.

Trong chế độ đầu tiên, txtfile được trả về là một tệp có thể thay đổi, bởi vì khi bạn thay đổi tệp lối tắt, bạn cũng thay đổi trong tệp gốc. Ưu điểm của chế độ này là mỗi phím tắt được trả về cần ít bộ nhớ hơn (trên RAM hoặc trong ổ cứng) và nhược điểm là mọi người (không chỉ tôi, chủ sở hữu) đều có quyền sửa đổi nội dung tệp.

Trong chế độ thứ hai, txtfile được trả về là một tệp không thay đổi, bởi vì tất cả các thay đổi trong tệp đã nhận không tham chiếu đến tệp gốc. Ưu điểm của chế độ này là chỉ tôi (chủ sở hữu) có thể sửa đổi tệp gốc và nhược điểm là mỗi bản sao được yêu cầu trả lại bộ nhớ (trong RAM hoặc trong ổ cứng).


Điều này không đúng. Các trường hợp bất biến thực sự có thể được thông qua bằng cách tham chiếu, và thường là như vậy. Sao chép chủ yếu được thực hiện khi bạn cần cập nhật đối tượng hoặc cấu trúc dữ liệu.
Jamon Holmgren

0

Nếu bạn trả về các tham chiếu của một mảng hoặc chuỗi, thì thế giới bên ngoài có thể sửa đổi nội dung trong đối tượng đó và do đó làm cho nó trở thành đối tượng có thể thay đổi (có thể sửa đổi).


0

Phương tiện bất biến không thể thay đổi, và có thể thay đổi có nghĩa là bạn có thể thay đổi.

Các đối tượng khác với nguyên thủy trong Java. Nguyên thủy được xây dựng theo kiểu (boolean, int, v.v.) và các đối tượng (lớp) là kiểu do người dùng tạo.

Nguyên thủy và đối tượng có thể thay đổi hoặc bất biến khi được định nghĩa là các biến thành viên trong quá trình thực hiện một lớp.

Nhiều người cho rằng các biến nguyên thủy và biến đối tượng có bộ sửa đổi cuối cùng trước chúng là bất biến, tuy nhiên, điều này không chính xác. Vì vậy, cuối cùng hầu như không có nghĩa là bất biến cho các biến. Xem ví dụ ở đây
http://www.siteconsortium.com/h/D0000F.php .


0

Immutable object- là trạng thái đối tượng không thể thay đổi sau khi tạo. Đối tượng là bất biến khi tất cả các lĩnh vực của nó là bất biến

Chủ đề an toàn

Ưu điểm chính của đối tượng Bất biến là nó tự nhiên cho môi trường đồng thời. Vấn đề lớn nhất trong tương tranh là shared resourcecó thể thay đổi bất kỳ luồng nào. Nhưng nếu một đối tượng là bất biến thì read-onlyđó là hoạt động an toàn của luồng. Mọi sửa đổi của một đối tượng bất biến ban đầu đều trả về một bản sao

tác dụng phụ miễn phí

Là một nhà phát triển, bạn hoàn toàn chắc chắn rằng trạng thái bất biến của đối tượng không thể thay đổi từ bất kỳ nơi nào (có mục đích hay không)

Bất lợi:

Sao chép đối tượng là hoạt động nặng nề hơn thay đổi một đối tượng có thể thay đổi, đó là lý do tại sao nó có một số dấu chân hiệu suất

Để tạo một immutableđối tượng bạn nên sử dụng:

  1. Trình độ ngôn ngữ. Mỗi ngôn ngữ chứa các công cụ để giúp bạn với nó. Ví dụ: Java có finalprimitives, Swift có letstruct[Giới thiệu] . Ngôn ngữ định nghĩa một loại biến. Ví dụ: Java có primitivereferenceloại, Swift có valuereference[Giới thiệu] . Đối với các đối tượng bất biến thuận tiện hơn là primitivesvaluegõ tạo một bản sao theo mặc định. Đối với referenceloại, nó khó hơn (vì bạn có thể thay đổi trạng thái của đối tượng ra khỏi nó) nhưng có thể. Ví dụ: bạn có thể sử dụng clonemẫu ở cấp độ nhà phát triển

  2. Cấp độ nhà phát triển. Là nhà phát triển, bạn không nên cung cấp giao diện để thay đổi trạng thái

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.