Tại sao các cấu trúc đột biến có thể là ác Evil?


485

Sau các cuộc thảo luận ở đây về SO, tôi đã đọc nhiều lần nhận xét rằng các cấu trúc có thể thay đổi được là Evil evil (giống như trong câu trả lời cho điều này câu hỏi ).

Vấn đề thực sự với tính đột biến và cấu trúc trong C # là gì?


12
Yêu cầu các cấu trúc đột biến là xấu xa giống như tuyên bố các đột biến ints, bools và tất cả các loại giá trị khác là xấu xa. Có những trường hợp cho tính đột biến và bất biến. Những trường hợp đó xoay quanh vai trò của dữ liệu, không phải kiểu phân bổ / chia sẻ bộ nhớ.
Slipp D. Thompson

41
@slipp intboolkhông thể thay đổi ..
Blorgbeard là ra

2
... .-syntax, làm cho hoạt động với dữ liệu ref, đánh máy và dữ liệu giá trị, đánh máy tìm kiếm thậm chí cùng mặc dù họ khác biệt rõ rệt. Đây là lỗi thuộc tính của C #, không phải là cấu trúc của một số ngôn ngữ cung cấp một a[V][X] = 3.14cú pháp thay thế để thay đổi tại chỗ. Trong C #, bạn nên làm tốt hơn để cung cấp các phương thức trình biến đổi thành viên cấu trúc như 'MutateV (Action <ref Vector2> mutator) `và sử dụng nó như thế a.MutateV((v) => { v.X = 3; }) (ví dụ đơn giản hóa quá mức vì những hạn chế của C # đối với reftừ khóa, nhưng với một số cách giải quyết nên có thể) .
Slipp D. Thompson

2
@Slipp Vâng, tôi nghĩ hoàn toàn ngược lại về các loại cấu trúc này. Tại sao bạn nghĩ rằng các cấu trúc đã được triển khai trong thư viện .NET, như DateTime hoặc TimeSpan (tương tự như vậy) là bất biến? Có thể sẽ hữu ích khi chỉ thay đổi một thành viên của var như vậy, nhưng nó quá bất tiện, dẫn đến quá nhiều vấn đề. Thực tế bạn đã sai về những gì bộ xử lý calc, vì C # không biên dịch thành trình biên dịch, nó biên dịch thành IL. Trong IL (cung cấp chúng tôi đã có biến có tên x) thao tác đơn này là 4 hướng dẫn: ldloc.0(tải biến 0-index vào ...
Sushi271

1
... kiểu. Tlà loại. Ref chỉ là một từ khóa làm cho biến được truyền cho chính một phương thức chứ không phải là một bản sao của nó. Nó cũng có ý nghĩa đối với các kiểu tham chiếu, vì chúng ta có thể thay đổi biến , tức là tham chiếu bên ngoài phương thức sẽ trỏ đến đối tượng khác sau khi được thay đổi trong phương thức. Vì ref Tkhông phải là một kiểu, nhưng thời trang truyền tham số phương thức, bạn không thể đặt nó vào <>, vì chỉ có thể đặt các loại ở đó. Vì vậy, nó chỉ không chính xác. Có thể sẽ thuận tiện khi làm như vậy, có thể nhóm C # có thể thực hiện điều này cho một số phiên bản mới, nhưng ngay bây giờ họ đang làm việc trên một số ...
Sushi271

Câu trả lời:


290

Cấu trúc là các loại giá trị có nghĩa là chúng được sao chép khi chúng được truyền xung quanh.

Vì vậy, nếu bạn thay đổi một bản sao, bạn chỉ thay đổi bản sao đó, không phải bản gốc và không phải bất kỳ bản sao nào khác có thể có xung quanh.

Nếu cấu trúc của bạn là bất biến thì tất cả các bản sao tự động do được truyền theo giá trị sẽ giống nhau.

Nếu bạn muốn thay đổi nó, bạn phải có ý thức thực hiện nó bằng cách tạo một thể hiện mới của cấu trúc với dữ liệu đã sửa đổi. (không phải bản sao)


82
"Nếu cấu trúc của bạn là bất biến thì tất cả các bản sao sẽ giống nhau." Không, nó có nghĩa là bạn phải có ý thức tạo một bản sao nếu bạn muốn một giá trị khác. Điều đó có nghĩa là bạn sẽ không bị bắt khi sửa đổi bản sao với suy nghĩ bạn đang sửa đổi bản gốc.
Lucas

19
@ Tôi nghĩ rằng bạn đang nói về một loại bản sao khác không thực sự là một bản sao của nó một nội dung mới có chủ ý chứa dữ liệu khác nhau.
trampster

3
Chỉnh sửa của bạn (16 tháng sau) làm cho điều đó rõ ràng hơn một chút. Mặc dù vậy, tôi vẫn đứng "(cấu trúc bất biến) có nghĩa là bạn sẽ không bị bắt khi sửa đổi bản sao với suy nghĩ bạn đang sửa đổi bản gốc".
Lucas

6
@Lucas: Nguy hiểm của việc tạo một bản sao của một cấu trúc, sửa đổi nó và bằng cách nào đó nghĩ rằng một người đang sửa đổi bản gốc (khi thực tế là người ta đang viết một trường cấu trúc làm cho rõ ràng rằng thực tế là người ta chỉ viết một bản sao) khá nhỏ so với sự nguy hiểm khi ai đó giữ một đối tượng lớp làm phương tiện lưu giữ thông tin trong đó sẽ làm biến đổi đối tượng để cập nhật thông tin của chính nó và trong quá trình làm hỏng thông tin do một đối tượng khác nắm giữ.
supercat

2
Đoạn 3 nghe có vẻ sai hoặc không rõ ràng nhất. Nếu cấu trúc của bạn là bất biến thì đơn giản là bạn sẽ không thể sửa đổi các trường của nó hoặc các trường của bất kỳ bản sao nào được tạo. "Nếu bạn muốn thay đổi nó, bạn phải ..." điều đó cũng gây hiểu lầm, bạn không thể thay đổi bao giờ , không có ý thức hay vô thức. Tạo một thể hiện mới mà dữ liệu bạn muốn không liên quan gì đến bản sao gốc ngoài việc có cùng cấu trúc dữ liệu.
Saeb Amini

167

Bắt đầu từ đâu ;-p

Blog của Eric Lippert luôn tốt cho một trích dẫn:

Đây là một lý do khác tại sao các loại giá trị đột biến là xấu xa. Cố gắng luôn luôn làm cho các loại giá trị bất biến.

Đầu tiên, bạn có xu hướng mất các thay đổi khá dễ dàng ... ví dụ: đưa mọi thứ ra khỏi danh sách:

Foo foo = list[0];
foo.Name = "abc";

điều đó đã thay đổi điều gì? Không có gì hữu ích ...

Tương tự với các thuộc tính:

myObj.SomeProperty.Size = 22; // the compiler spots this one

buộc bạn phải làm:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

ít nghiêm trọng hơn, có một vấn đề kích thước; đối tượng đột biến có xu hướng có nhiều thuộc tính; Tuy nhiên, nếu bạn có một cấu trúc với hai ints, a string, a DateTimevà a bool, bạn có thể nhanh chóng ghi lại rất nhiều bộ nhớ. Với một lớp, nhiều người gọi có thể chia sẻ một tham chiếu đến cùng một thể hiện (các tham chiếu là nhỏ).


5
Vâng, nhưng trình biên dịch chỉ là ngu ngốc theo cách đó. Không cho phép gán cho các thành viên cấu trúc thuộc tính là IMHO một quyết định thiết kế ngu ngốc, bởi vì nó được phép cho ++nhà điều hành. Trong trường hợp này, trình biên dịch chỉ viết chính bài tập rõ ràng thay vì hối hả lập trình viên.
Konrad Rudolph

12
@Konrad: myObj.SomeProperty.Size = 22 sẽ sửa đổi một BẢN SAO của myObj.SomeProperty. Trình biên dịch đang cứu bạn khỏi một lỗi rõ ràng. Và nó KHÔNG được phép cho ++.
Lucas

@Lucas: Chà, trình biên dịch Mono C # chắc chắn cho phép nó - vì tôi không có Windows nên tôi không thể kiểm tra trình biên dịch của Microsoft.
Konrad Rudolph

6
@Konrad - với một ít sự gián tiếp, nó sẽ hoạt động; đó là "biến đổi một giá trị của một thứ chỉ tồn tại dưới dạng giá trị nhất thời trên ngăn xếp và sắp bay hơi vào hư vô", đó là trường hợp bị chặn.
Marc Gravell

2
@Marc Gravell: Trong đoạn mã trước, bạn kết thúc bằng một "Foo" có tên là "abc" và các thuộc tính khác là Danh sách [0], mà không làm phiền Danh sách [0]. Nếu Foo là một lớp, nó sẽ là cần thiết để sao chép nó và sau đó thay đổi bản sao. Theo tôi, vấn đề lớn với sự khác biệt giữa loại giá trị và loại là việc sử dụng dấu "." điều hành cho hai mục đích. Nếu tôi có máy khoan, các lớp có thể hỗ trợ cả "." và "->" cho các phương thức và thuộc tính, nhưng ngữ nghĩa thông thường cho "." các thuộc tính sẽ là tạo một thể hiện mới với trường thích hợp được sửa đổi.
supercat

71

Tôi sẽ không nói xấu nhưng khả năng biến đổi thường là một dấu hiệu của sự hăng hái từ phía lập trình viên để cung cấp tối đa chức năng. Trong thực tế, điều này thường không cần thiết và đến lượt nó, làm cho giao diện nhỏ hơn, dễ sử dụng hơn và khó sử dụng hơn (= mạnh mẽ hơn).

Một ví dụ về điều này là đọc / ghi và viết / ghi xung đột trong điều kiện chủng tộc. Điều này chỉ đơn giản là không thể xảy ra trong các cấu trúc bất biến, vì một ghi không phải là một hoạt động hợp lệ.

Ngoài ra, tôi khẳng định rằng tính đột biến gần như không bao giờ thực sự cần thiết , lập trình viên chỉ nghĩ rằng nó có thể sẽ có trong tương lai. Ví dụ, đơn giản là không có ý nghĩa để thay đổi một ngày. Thay vào đó, tạo một ngày mới dựa trên ngày cũ. Đây là một hoạt động giá rẻ, vì vậy hiệu suất không phải là một cân nhắc.


1
Eric Lippert nói rằng họ đang ... xem câu trả lời của tôi.
Marc Gravell

42
Nhiều như tôi tôn trọng Eric Lippert, anh ấy không phải là Chúa (hoặc ít nhất là chưa). Bài đăng blog mà bạn liên kết đến và bài đăng của bạn ở trên là những lý lẽ hợp lý để biến các cấu trúc thành bất biến như một vấn đề tất nhiên nhưng chúng thực sự rất yếu như các đối số vì không bao giờ sử dụng các cấu trúc có thể thay đổi. Bài đăng này, tuy nhiên, là +1.
Stephen Martin

2
Phát triển trong C #, bạn thường cần khả năng biến đổi ngay bây giờ và sau đó - đặc biệt là với Mô hình kinh doanh của bạn, nơi bạn muốn phát trực tuyến, v.v. để hoạt động trơn tru với các giải pháp hiện có. Tôi đã viết một bài viết về cách làm việc với dữ liệu có thể thay đổi và bất biến, giải quyết hầu hết các vấn đề xung quanh khả năng biến đổi (tôi hy vọng): rickyhelgesson.wordpress.com/2012/07/17/
Ricky Helgesson

3
@StephenMartin: Các cấu trúc đóng gói một giá trị thường là bất biến, nhưng các cấu trúc cho đến nay là phương tiện tốt nhất để đóng gói các tập hợp các biến độc lập nhưng có liên quan (như tọa độ X và Y của một điểm) không có "danh tính" như một nhóm. Struct được sử dụng cho rằng mục đích chung nên tiếp xúc với các biến của họ như là các lĩnh vực công cộng. Tôi sẽ xem xét khái niệm rằng việc sử dụng một lớp phù hợp hơn là một cấu trúc cho các mục đích như vậy là hoàn toàn sai. Các lớp bất biến thường kém hiệu quả hơn, và các lớp đột biến thường có ngữ nghĩa khủng khiếp.
supercat

2
@StephenMartin: Ví dụ, hãy xem xét một phương thức hoặc thuộc tính được cho là trả về sáu floatthành phần của một biến đổi đồ họa. Nếu một phương thức như vậy trả về một cấu trúc trường tiếp xúc với sáu thành phần, thì rõ ràng việc sửa đổi các trường của cấu trúc sẽ không sửa đổi đối tượng đồ họa mà nó được nhận. Nếu một phương thức như vậy trả về một đối tượng lớp có thể thay đổi, có thể việc thay đổi các thuộc tính của nó sẽ thay đổi đối tượng đồ họa bên dưới và có thể nó sẽ không - không ai thực sự biết.
supercat

48

Cấu trúc đột biến không phải là xấu xa.

Họ là hoàn toàn cần thiết trong hoàn cảnh hiệu suất cao. Ví dụ: khi các dòng bộ đệm và hoặc bộ sưu tập rác trở thành nút cổ chai.

Tôi sẽ không gọi việc sử dụng một cấu trúc bất biến trong các trường hợp sử dụng hoàn toàn hợp lệ này là "tội ác".

Tôi có thể thấy rằng cú pháp của C # không giúp phân biệt quyền truy cập của một thành viên của loại giá trị hoặc loại tham chiếu, vì vậy tôi tất cả đều thích các cấu trúc bất biến, thực thi bất biến, trên các cấu trúc có thể thay đổi.

Tuy nhiên, thay vì chỉ đơn giản gắn nhãn các cấu trúc bất biến là "xấu xa", tôi khuyên bạn nên nắm lấy ngôn ngữ và ủng hộ quy tắc ngón tay cái hữu ích và mang tính xây dựng hơn.

Ví dụ: "structs là các loại giá trị, được sao chép theo mặc định. Bạn cần tham chiếu nếu bạn không muốn sao chép chúng" hoặc "thử làm việc với các cấu trúc chỉ đọc trước" .


8
Tôi cũng sẽ khẳng định rằng nếu người ta muốn buộc chặt một tập hợp các biến cố định cùng với băng keo để các giá trị của chúng có thể được xử lý hoặc lưu trữ riêng rẽ hoặc như một đơn vị, thì sẽ rất có ý nghĩa khi yêu cầu trình biên dịch buộc chặt một bộ cố định các biến cùng nhau (nghĩa là khai báo a structvới các trường công khai) hơn là xác định một lớp có thể được sử dụng, một cách vụng về, để đạt được cùng một kết thúc hoặc thêm một bó rác vào một cấu trúc để làm cho nó mô phỏng một lớp như vậy (thay vì có nó hành xử giống như một tập hợp các biến bị mắc kẹt với băng keo, đó là điều người ta thực sự muốn ở nơi đầu tiên)
supercat

41

Các cấu trúc với các trường hoặc thuộc tính có thể thay đổi công khai không phải là xấu.

Các phương thức cấu trúc (khác với các setters thuộc tính) đột biến "cái này" là hơi xấu, chỉ vì .net không cung cấp một phương tiện để phân biệt chúng với các phương thức không. Các phương thức cấu trúc không làm thay đổi "cái này" sẽ là bất khả xâm phạm ngay cả trên các cấu trúc chỉ đọc mà không cần sao chép phòng thủ. Các phương thức làm đột biến "cái này" hoàn toàn không nên bất khả xâm phạm trên các cấu trúc chỉ đọc. Vì .net không muốn cấm các phương thức cấu trúc không sửa đổi "điều này" khỏi việc được gọi trên các cấu trúc chỉ đọc, nhưng không muốn cho phép các cấu trúc chỉ đọc bị đột biến, nên nó sao chép các cấu trúc trong đọc chỉ bối cảnh, được cho là tồi tệ nhất của cả hai thế giới.

Mặc dù có vấn đề với việc xử lý các phương thức tự biến đổi trong bối cảnh chỉ đọc, tuy nhiên, các cấu trúc có thể thay đổi thường cung cấp ngữ nghĩa vượt trội hơn nhiều so với các loại lớp có thể thay đổi. Hãy xem xét ba chữ ký phương pháp sau:

struct PointySturation {public int x, y, z;};
lớp PointyClass {công khai int x, y, z;};

void Phương thức1 (PointySturation foo);
void Phương thức 2 (ref PointySturation foo);
void Phương thức3 (PointyClass foo);

Đối với mỗi phương pháp, trả lời các câu hỏi sau:

  1. Giả sử phương thức không sử dụng bất kỳ mã "không an toàn" nào, nó có thể sửa đổi foo không?
  2. Nếu không có tham chiếu bên ngoài nào đến 'foo' trước khi phương thức được gọi, thì có thể tồn tại tham chiếu bên ngoài không?

Đáp án:

Câu 1 ::
Method1()không (ý định rõ ràng)
Method2() : có (ý định rõ ràng)
Method3() : có (ý định không chắc chắn)
Câu 2
Method1():: không
Method2(): không (trừ khi không an toàn)
Method3() : có

Phương thức 1 không thể sửa đổi foo và không bao giờ có được tài liệu tham khảo. Phương thức 2 có một tham chiếu ngắn đến foo, nó có thể sử dụng sửa đổi các trường của foo bất kỳ số lần nào, theo bất kỳ thứ tự nào cho đến khi trả về, nhưng nó không thể duy trì tham chiếu đó. Trước khi Phương thức 2 trả về, trừ khi nó sử dụng mã không an toàn, mọi bản sao có thể được tạo từ tham chiếu 'foo' của nó sẽ biến mất. Phương thức 3, không giống như Phương thức 2, có một tài liệu tham khảo có thể chia sẻ bừa bãi cho foo và không biết nó có thể làm gì với nó. Nó có thể không thay đổi foo chút nào, nó có thể thay đổi foo và sau đó quay trở lại, hoặc nó có thể đưa ra một tham chiếu đến foo đến một chủ đề khác có thể thay đổi nó theo một cách tùy tiện vào một thời điểm tùy ý trong tương lai.

Mảng cấu trúc cung cấp ngữ nghĩa tuyệt vời. Với RectArray [500] loại Hình chữ nhật, rõ ràng và rõ ràng làm thế nào để sao chép phần tử 123 sang phần tử 456 và sau đó một thời gian sau đó đặt chiều rộng của phần tử 123 thành 555, mà không làm phiền phần tử 456. "RectArray [432] = RectArray [321] ]; ...; RectArray [123] .Width = 555; ". Biết rằng Hình chữ nhật là một cấu trúc với trường số nguyên có tên là Width sẽ cho mọi người biết tất cả những gì cần biết về các câu lệnh trên.

Bây giờ, giả sử RectClass là một lớp có cùng các trường với Hình chữ nhật và người ta muốn thực hiện các thao tác tương tự trên RectClassArray [500] loại RectClass. Có lẽ mảng được cho là chứa 500 tham chiếu bất biến được khởi tạo trước cho các đối tượng RectClass có thể thay đổi. trong trường hợp đó, mã thích hợp sẽ là một cái gì đó như "RectClassArray [321]. SetBound (RectClassArray [456]); ...; RectClassArray [321] .X = 555;". Có lẽ mảng được giả sử giữ các trường hợp sẽ không thay đổi, vì vậy mã phù hợp sẽ giống như "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321] ]); RectClassArray [321] .X = 555; " Để biết người ta phải làm gì, người ta sẽ phải biết nhiều hơn cả về RectClass (ví dụ: nó có hỗ trợ trình tạo bản sao, phương thức sao chép, v.v. ) và mục đích sử dụng của mảng. Không nơi nào gần như sạch sẽ như sử dụng một cấu trúc.

Chắc chắn, không có cách nào hay cho bất kỳ lớp container nào ngoài một mảng để cung cấp ngữ nghĩa rõ ràng của một mảng cấu trúc. Điều tốt nhất có thể làm, nếu một người muốn một bộ sưu tập được lập chỉ mục với một chuỗi, có lẽ sẽ cung cấp một phương thức "ActOnItem" chung để chấp nhận một chuỗi cho chỉ mục, một tham số chung và một đại biểu sẽ được thông qua bằng cách tham chiếu cả tham số chung và mục bộ sưu tập. Điều đó sẽ cho phép gần như cùng một ngữ nghĩa như các mảng cấu trúc, nhưng trừ khi mọi người có thể theo đuổi vb.net và C # để cung cấp một cú pháp hay, mã sẽ trông có vẻ lộn xộn ngay cả khi nó có hiệu suất hợp lý (truyền tham số chung sẽ cho phép sử dụng một ủy nhiệm tĩnh và sẽ tránh mọi nhu cầu tạo bất kỳ thể hiện lớp tạm thời nào).

Cá nhân, tôi cảm thấy căm ghét Eric Lippert et al. phun ra liên quan đến các loại giá trị đột biến. Họ cung cấp ngữ nghĩa sạch hơn nhiều so với các loại tham chiếu lăng nhăng được sử dụng ở mọi nơi. Mặc dù có một số hạn chế với sự hỗ trợ của .net đối với các loại giá trị, có nhiều trường hợp các loại giá trị có thể thay đổi phù hợp hơn bất kỳ loại thực thể nào khác.


1
@Ron Warholic: không phải ai cũng biết rằng Ai đó là hình chữ nhật. Nó có thể là một số loại khác có thể được đánh máy ngầm từ Hình chữ nhật. Mặc dù, loại duy nhất do hệ thống xác định có thể được đánh máy ngầm từ Hình chữ nhật là Hình chữ nhật và trình biên dịch sẽ vặn vẹo nếu một người cố gắng chuyển các trường của Hình chữ nhật cho nhà xây dựng của Hình chữ nhật (vì trước đây là Đơn và Số nguyên sau) , có thể có các cấu trúc do người dùng định nghĩa cho phép các kiểu chữ ngầm như vậy. BTW, câu lệnh đầu tiên sẽ hoạt động tốt như nhau cho dù Ai đó là Hình chữ nhật hay Hình chữ nhật.
supercat

Tất cả những gì bạn đã thể hiện là trong một ví dụ giả định, bạn tin rằng một phương pháp rõ ràng hơn. Nếu chúng tôi lấy ví dụ của bạn với Rectangletôi, tôi có thể dễ dàng đưa ra một câu hỏi chung nơi bạn có hành vi không rõ ràng . Hãy xem xét rằng WinForms thực hiện một Rectangleloại có thể thay đổi được sử dụng trong thuộc tính của biểu mẫu Bounds. Nếu tôi muốn thay đổi giới hạn, tôi sẽ muốn sử dụng cú pháp hay của bạn: form.Bounds.X = 10;Tuy nhiên, điều này thay đổi chính xác không có gì trên biểu mẫu (và tạo ra một lỗi đáng yêu thông báo cho bạn về điều đó). Sự không nhất quán là nguyên nhân của lập trình và là lý do tại sao muốn bất biến.
Ron Warholic

3
@Ron Warholic: BTW, tôi sẽ thích để có thể nói "form.Bounds.X = 10;" và nó chỉ hoạt động, nhưng hệ thống không cung cấp bất kỳ cách làm sạch nào. Một quy ước để hiển thị các thuộc tính loại giá trị như các phương thức chấp nhận các cuộc gọi lại có thể cung cấp mã sạch hơn, hiệu quả và chính xác hơn bất kỳ cách tiếp cận nào sử dụng các lớp.
supercat

4
Câu trả lời này sâu sắc hơn nhiều so với một vài câu trả lời được bình chọn hàng đầu. Thật vô lý khi lập luận chống lại các loại giá trị có thể thay đổi dựa trên khái niệm "những gì bạn mong đợi" đã xảy ra khi bạn trộn lẫn bí danh và đột biến. Đó là một điều khủng khiếp để làm dù sao đi nữa !
Eamon Nerbonne

2
@supercat: Ai biết được, có thể tính năng hoàn trả lại mà họ đang nói về C # 7 có thể bao gồm cơ sở đó (tôi thực sự không nhìn vào nó một cách chi tiết, nhưng bề ngoài nghe có vẻ tương tự).
Eamon Nerbonne

24

Các loại giá trị về cơ bản đại diện cho các khái niệm bất biến. Fx, thật vô nghĩa khi có một giá trị toán học như số nguyên, vectơ, v.v. và sau đó có thể sửa đổi nó. Điều đó sẽ giống như xác định lại ý nghĩa của một giá trị. Thay vì thay đổi một loại giá trị, sẽ có ý nghĩa hơn khi gán một giá trị duy nhất khác. Hãy nghĩ về thực tế rằng các loại giá trị được so sánh bằng cách so sánh tất cả các giá trị thuộc tính của nó. Vấn đề là nếu các thuộc tính giống nhau thì đó là đại diện phổ quát của giá trị đó.

Như Konrad đề cập, việc thay đổi một ngày cũng không có ý nghĩa gì, vì giá trị đại diện cho điểm duy nhất đó theo thời gian và không phải là một thể hiện của một đối tượng thời gian có bất kỳ trạng thái hoặc phụ thuộc ngữ cảnh nào.

Hy vọng điều này có ý nghĩa với bạn. Đó là nhiều hơn về khái niệm bạn cố gắng nắm bắt với các loại giá trị hơn là chi tiết thực tế, để chắc chắn.


Chà, họ nên đại diện cho các khái niệm bất biến, ít nhất là ;-p
Marc Gravell

3
Chà, tôi cho rằng họ có thể đã biến System.Drawing.Point thành bất biến nhưng đó sẽ là một lỗi thiết kế nghiêm trọng IMHO. Tôi nghĩ rằng điểm thực sự là một loại giá trị nguyên mẫu và chúng có thể thay đổi. Và họ không gây ra bất kỳ vấn đề nào cho bất cứ ai ngoài 101 người mới bắt đầu lập trình thực sự sớm.
Stephen Martin

3
Về nguyên tắc tôi nghĩ rằng điểm cũng nên bất biến nhưng nếu nó làm cho loại khó hơn hoặc kém thanh lịch hơn để sử dụng thì tất nhiên điều đó cũng phải được xem xét. Không có điểm nào trong việc xây dựng mã giúp duy trì các hoàng tử tốt nhất nếu không ai muốn sử dụng chúng;)
Morten Christiansen

3
Các loại giá trị rất hữu ích để biểu diễn các khái niệm bất biến đơn giản, nhưng cấu trúc trường tiếp xúc là loại tốt nhất để sử dụng để giữ hoặc chuyển xung quanh các tập hợp nhỏ cố định của các giá trị độc lập nhưng có liên quan (như tọa độ của một điểm). Một vị trí lưu trữ của loại giá trị như vậy gói gọn các giá trị của các trường của nó và không có gì khác. Ngược lại, một vị trí lưu trữ của loại tham chiếu có thể thay đổi có thể được sử dụng cho mục đích giữ trạng thái của đối tượng có thể thay đổi, nhưng cũng gói gọn danh tính của tất cả các tham chiếu khác trong toàn vũ trụ tồn tại cùng một đối tượng đó.
supercat

4
Các loại Giá trị cơ bản đại diện cho các khái niệm bất biến. Không, họ không. Một trong những ứng dụng lâu đời nhất và hữu ích nhất của biến có giá trị là một inttrình vòng lặp, sẽ hoàn toàn vô dụng nếu nó không thay đổi. Tôi nghĩ rằng bạn đang kết hợp các trình triển khai / trình biên dịch / thời gian chạy của các loại giá trị, với các biến số được gõ vào một loại giá trị - loại này chắc chắn có thể thay đổi theo bất kỳ giá trị nào có thể.
Slipp D. Thompson

19

Có một vài trường hợp góc khác có thể dẫn đến hành vi không thể đoán trước theo quan điểm của lập trình viên.

Các loại giá trị bất biến và các trường chỉ đọc

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this() 
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass 
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass 
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

Các loại giá trị có thể thay đổi và mảng

Giả sử chúng ta có một mảng Mutablecấu trúc của chúng ta và chúng ta đang gọi IncrementIphương thức cho phần tử đầu tiên của mảng đó. Hành vi nào bạn mong đợi từ cuộc gọi này? Nó nên thay đổi giá trị của mảng hay chỉ là một bản sao?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

Vì vậy, các cấu trúc đột biến không phải là xấu xa miễn là bạn và phần còn lại của nhóm hiểu rõ những gì bạn đang làm. Nhưng có quá nhiều trường hợp góc khi hành vi chương trình sẽ khác với những gì được mong đợi, điều đó có thể dẫn đến những lỗi khó sản xuất và khó hiểu.


5
Điều gì sẽ xảy ra, nếu các ngôn ngữ .net có hỗ trợ loại giá trị tốt hơn một chút, thì các phương thức cấu trúc sẽ bị cấm không được thay đổi 'cái này' trừ khi chúng được tuyên bố rõ ràng là làm như vậy và các phương thức được khai báo chỉ nên bị cấm trong chỉ đọc bối cảnh. Mảng của các cấu trúc đột biến cung cấp ngữ nghĩa hữu ích mà không thể đạt được một cách hiệu quả thông qua các phương tiện khác.
supercat

2
đây là những ví dụ tốt về vấn đề rất tinh tế sẽ phát sinh từ các cấu trúc đột biến. Tôi sẽ không mong đợi bất kỳ hành vi này. Tại sao một mảng sẽ cung cấp cho bạn một tham chiếu, nhưng một giao diện cung cấp cho bạn một giá trị? Tôi đã có thể nghĩ, ngoài các giá trị mọi lúc (đó là điều tôi thực sự mong đợi), thì ít nhất nó sẽ là cách khác: giao diện đưa ra các tham chiếu; mảng đưa ra các giá trị ...
Dave Cousineau 6/212

@Sahuagin: Thật không may, không có cơ chế tiêu chuẩn nào mà giao diện có thể hiển thị tham chiếu. Có nhiều cách .net có thể cho phép những việc đó được thực hiện một cách an toàn và hữu ích (ví dụ: bằng cách xác định cấu trúc "ArrayRef <T>" đặc biệt chứa a T[]và một chỉ số nguyên và cung cấp quyền truy cập vào thuộc tính loại ArrayRef<T>sẽ được hiểu là truy cập vào phần tử mảng thích hợp) [nếu một lớp muốn phơi bày ArrayRef<T>cho bất kỳ mục đích nào khác, nó có thể cung cấp một phương thức - trái ngược với một thuộc tính - để truy xuất nó]. Thật không may, không có quy định như vậy tồn tại.
supercat

2
Ôi trời ... điều này làm cho những mưu đồ đột biến trở nên xấu xa!
nawfal

1
Tôi thích câu trả lời này vì nó chứa thông tin rất có giá trị không rõ ràng. Nhưng thực sự, đây không phải là một lập luận chống lại các cấu trúc có thể thay đổi như một số yêu cầu. Vâng, những gì chúng ta thấy ở đây là một "hố tuyệt vọng" như Eric sẽ đặt nó, nhưng nguồn gốc của sự tuyệt vọng này không phải là sự biến đổi. Nguồn gốc của sự tuyệt vọng là các phương pháp tự biến đổi cấu trúc . (Về lý do tại sao các mảng và danh sách hoạt động khác nhau bởi vì về cơ bản, một toán tử tính toán một địa chỉ bộ nhớ và cái kia là một thuộc tính. Nói chung tất cả sẽ trở nên rõ ràng khi bạn hiểu rằng "tham chiếu" là một giá trị địa chỉ .)
AnorZaken

18

Nếu bạn đã từng lập trình bằng một ngôn ngữ như C / C ++, các cấu trúc sẽ ổn khi sử dụng như là biến đổi. Chỉ cần vượt qua chúng với ref, xung quanh và không có gì có thể đi sai. Vấn đề duy nhất tôi tìm thấy là các hạn chế của trình biên dịch C # và trong một số trường hợp, tôi không thể ép buộc điều ngu ngốc đó là sử dụng tham chiếu đến struct, thay vì Copy (như khi struct là một phần của lớp C # ).

Vì vậy, các cấu trúc đột biến không phải là xấu xa, C # đã làm cho chúng trở nên xấu xa. Tôi sử dụng các cấu trúc đột biến trong C ++ mọi lúc và chúng rất thuận tiện và trực quan. Ngược lại, C # đã khiến tôi từ bỏ hoàn toàn các cấu trúc với tư cách là thành viên của các lớp vì cách chúng xử lý các đối tượng. Sự tiện lợi của họ đã khiến chúng ta phải trả giá.


Có các trường lớp của các kiểu cấu trúc thường có thể là một mẫu rất hữu ích, mặc dù phải thừa nhận rằng có một số hạn chế. Hiệu suất sẽ bị suy giảm nếu một người sử dụng các thuộc tính thay vì các trường hoặc sử dụng readonly, nhưng nếu một người tránh làm những điều đó thì các lớp của các loại cấu trúc là tốt. Giới hạn thực sự cơ bản duy nhất của các cấu trúc là trường cấu trúc của loại lớp có thể thay đổi như int[]có thể gói gọn danh tính hoặc một tập hợp các giá trị không thay đổi, nhưng không thể được sử dụng để đóng gói các giá trị có thể thay đổi mà không đóng gói một nhận dạng không mong muốn.
supercat

13

Nếu bạn sử dụng các cấu trúc được dự định cho (trong C #, Visual Basic 6, Pascal / Delphi, kiểu cấu trúc C ++ (hoặc các lớp) khi chúng không được sử dụng làm con trỏ), bạn sẽ thấy rằng cấu trúc không nhiều hơn một biến ghép . Điều này có nghĩa là: bạn sẽ coi chúng như một tập hợp các biến, dưới một tên chung (một biến bản ghi mà bạn tham chiếu từ các thành viên).

Tôi biết rằng sẽ khiến nhiều người nhầm lẫn với OOP, nhưng điều đó không đủ lý do để nói những thứ như vậy vốn là xấu xa, nếu được sử dụng đúng cách. Một số cấu trúc không thể thay đổi được như họ dự định (đây là trường hợp của Python namedtuple), nhưng đó là một mô hình khác để xem xét.

Có: cấu trúc liên quan đến rất nhiều bộ nhớ, nhưng nó sẽ không chính xác hơn bộ nhớ bằng cách thực hiện:

point.x = point.x + 1

so với:

point = Point(point.x + 1, point.y)

Tiêu thụ bộ nhớ sẽ ít nhất là như nhau, hoặc thậm chí nhiều hơn trong trường hợp không thể thay đổi (mặc dù trường hợp đó là tạm thời, đối với ngăn xếp hiện tại, tùy thuộc vào ngôn ngữ).

Nhưng, cuối cùng, cấu trúc là cấu trúc , không phải đối tượng. Trong POO, thuộc tính chính của một đối tượng là danh tính của chúng , mà hầu hết thời gian không nhiều hơn địa chỉ bộ nhớ của nó. Struct là viết tắt của cấu trúc dữ liệu (không phải là một đối tượng thích hợp và vì vậy dù sao họ cũng không có danh tính) và dữ liệu có thể được sửa đổi. Trong các ngôn ngữ khác, bản ghi (thay vì struct , như trường hợp của Pascal) là từ và giữ cùng một mục đích: chỉ là một biến bản ghi dữ liệu, dự định được đọc từ tệp, được sửa đổi và đổ vào tệp (đó là chính sử dụng và, trong nhiều ngôn ngữ, bạn thậm chí có thể xác định căn chỉnh dữ liệu trong bản ghi, trong khi đó không nhất thiết là trường hợp được gọi đúng là Đối tượng).

Bạn muốn một ví dụ tốt? Cấu trúc được sử dụng để đọc các tập tin dễ dàng. Python có thư viện này bởi vì, nó hướng đối tượng và không hỗ trợ các cấu trúc, nên nó phải thực hiện nó theo một cách khác, điều này hơi xấu. Các ngôn ngữ thực hiện cấu trúc có tính năng đó ... tích hợp. Hãy thử đọc một tiêu đề bitmap với cấu trúc phù hợp bằng các ngôn ngữ như Pascal hoặc C. Sẽ dễ dàng (nếu cấu trúc được xây dựng và căn chỉnh chính xác; trong Pascal, bạn sẽ không sử dụng truy cập dựa trên bản ghi mà có chức năng đọc dữ liệu nhị phân tùy ý). Vì vậy, đối với các tệp và truy cập bộ nhớ trực tiếp (cục bộ), các cấu trúc tốt hơn các đối tượng. Cho đến ngày nay, chúng ta đã quen với JSON và XML và vì vậy chúng ta quên việc sử dụng các tệp nhị phân (và như một tác dụng phụ, việc sử dụng các cấu trúc). Nhưng có: họ tồn tại, và có một mục đích.

Họ không xấu xa. Chỉ cần sử dụng chúng cho đúng mục đích.

Nếu bạn nghĩ về búa, bạn sẽ muốn coi ốc vít là đinh, để tìm ốc vít khó cắm vào tường hơn, và đó sẽ là lỗi của ốc vít, và chúng sẽ là những kẻ xấu.


12

Hãy tưởng tượng bạn có một mảng gồm 1.000.000 cấu trúc. Mỗi cấu trúc đại diện cho một vốn chủ sở hữu với các công cụ như price_price, Offer_price (có lẽ là số thập phân), v.v., điều này được tạo bởi C # / VB.

Hãy tưởng tượng mảng đó được tạo trong một khối bộ nhớ được phân bổ trong heap không được quản lý để một số luồng mã gốc khác có thể truy cập đồng thời vào mảng (có lẽ một số mã hoàn hảo cao đang làm toán).

Hãy tưởng tượng mã C # / VB đang lắng nghe nguồn cấp thị trường về sự thay đổi giá, mã đó có thể phải truy cập vào một số phần tử của mảng (đối với bất kỳ bảo mật nào) và sau đó sửa đổi một số trường giá.

Hãy tưởng tượng điều này đang được thực hiện hàng chục hoặc thậm chí hàng trăm ngàn lần mỗi giây.

Hãy đối mặt với sự thật, trong trường hợp này, chúng tôi thực sự muốn các cấu trúc này có thể thay đổi được, chúng cần phải được chia sẻ bởi một số mã gốc khác nên việc tạo các bản sao sẽ không giúp ích gì; chúng cần phải được thực hiện bởi vì việc tạo một bản sao của cấu trúc 120 byte ở các tốc độ này là sự mất trí, đặc biệt là khi một bản cập nhật thực sự có thể tác động chỉ một hoặc hai byte.

Hugo


3
Đúng, nhưng trong trường hợp này, lý do để sử dụng một cấu trúc là vì làm như vậy được áp đặt cho thiết kế ứng dụng bởi các ràng buộc bên ngoài (những người sử dụng mã gốc). Mọi thứ khác mà bạn mô tả về các đối tượng này cho thấy chúng rõ ràng phải là các lớp trong C # hoặc VB.NET.
Jon Hanna

6
Tôi không chắc tại sao một số người nghĩ rằng những thứ đó nên là đối tượng của lớp. Nếu tất cả các vị trí mảng được điền với các thể hiện riêng biệt của tham chiếu, sử dụng một loại lớp sẽ thêm mười hai hoặc hai mươi bốn byte vào yêu cầu bộ nhớ và truy cập tuần tự trên một mảng các tham chiếu đối tượng lớp có thể chậm hơn nhiều so với truy cập tuần tự trên một mảng các cấu trúc.
supercat

9

Khi một cái gì đó có thể được đột biến, nó có được một cảm giác đồng nhất.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Bởi vì Personcó thể thay đổi, nên nghĩ về việc thay đổi vị trí của Eric sẽ tốt hơn là nhân bản Eric, di chuyển bản sao và phá hủy bản gốc . Cả hai hoạt động sẽ thành công trong việc thay đổi nội dung của eric.position, nhưng một hoạt động trực quan hơn so với hoạt động khác. Tương tự như vậy, sẽ trực quan hơn khi vượt qua Eric (như một tài liệu tham khảo) cho các phương pháp để sửa đổi anh ta. Đưa ra một phương pháp một bản sao của Eric hầu như sẽ luôn gây ngạc nhiên. Bất cứ ai muốn đột biến Personđều phải nhớ yêu cầu tham khảo Personhoặc họ sẽ làm sai.

Nếu bạn làm cho loại bất biến, vấn đề sẽ biến mất; nếu tôi không thể sửa đổi eric, điều đó sẽ không khác biệt gì đối với tôi dù tôi nhận được erichay là một bản sao eric. Tổng quát hơn, một loại an toàn để vượt qua giá trị nếu tất cả trạng thái quan sát được của nó được giữ trong các thành viên:

  • bất biến
  • loại tham khảo
  • an toàn để vượt qua giá trị

Nếu các điều kiện đó được đáp ứng thì loại giá trị có thể thay đổi hoạt động giống như loại tham chiếu vì bản sao nông sẽ vẫn cho phép người nhận sửa đổi dữ liệu gốc.

Trực giác của một bất biến Personphụ thuộc vào những gì bạn đang cố gắng làm mặc dù. Nếu Personchỉ đại diện cho một tập hợp dữ liệu về một người, thì không có gì là không trực quan về nó; Personcác biến thực sự đại diện cho các giá trị trừu tượng , không phải các đối tượng. (Trong trường hợp đó, có lẽ sẽ thích hợp hơn để đổi tên thành PersonData.) NếuPerson thực sự là mô hình hóa bản thân một người, ý tưởng liên tục tạo và di chuyển bản sao là ngớ ngẩn ngay cả khi bạn tránh được cạm bẫy khi nghĩ rằng bạn đang sửa đổi bản gốc. Trong trường hợp đó, có lẽ sẽ tự nhiên hơn khi chỉ cần tạo Personmột kiểu tham chiếu (nghĩa là một lớp.)

Cấp, vì lập trình chức năng đã dạy chúng ta có những lợi ích để biến mọi thứ thành bất biến (không ai có thể bí mật giữ tham chiếu ericvà biến đổi anh ta), nhưng vì đó không phải là thành ngữ trong OOP nên nó vẫn không trực quan với bất kỳ ai làm việc với bạn mã.


3
Quan điểm của bạn về bản sắc là một điều tốt; có thể đáng lưu ý rằng danh tính chỉ có liên quan khi có nhiều tài liệu tham khảo tồn tại. Nếu foogiữ tham chiếu duy nhất đến mục tiêu của nó ở bất cứ đâu trong vũ trụ và không có gì chiếm được giá trị băm nhận dạng của đối tượng đó, thì trường biến đổi foo.Xtương đương về mặt ngữ nghĩa với việc tạo foođiểm đến một đối tượng mới giống như đối tượng trước đây, nhưng với Xgiữ giá trị mong muốn. Với các loại lớp, nhìn chung rất khó để biết liệu nhiều tài liệu tham khảo có tồn tại một thứ gì đó hay không, nhưng với các cấu trúc thì thật dễ dàng: chúng không.
supercat

1
Nếu Thinglà một loại lớp có thể thay đổi, một Thing[] ý chí sẽ gói gọn các danh tính đối tượng - dù người ta có muốn hay không - trừ khi người ta có thể đảm bảo rằng không có Thingmảng nào tồn tại bất kỳ tham chiếu bên ngoài nào. Nếu người ta không muốn các phần tử mảng đóng gói danh tính, thì người ta thường phải đảm bảo rằng không có mục nào chứa tham chiếu sẽ bị biến đổi hoặc không có tham chiếu bên ngoài nào tồn tại với bất kỳ mục nào mà nó giữ [phương pháp lai cũng có thể hoạt động ]. Không có cách tiếp cận nào là thuận tiện khủng khiếp. Nếu Thinglà một cấu trúc, Thing[]chỉ đóng gói các giá trị.
supercat

Đối với các đối tượng, danh tính của họ đến từ vị trí của họ. Các thể hiện của loại tham chiếu có danh tính của chúng nhờ vào vị trí của chúng trong bộ nhớ và bạn chỉ chuyển xung quanh danh tính của chúng (một tham chiếu), chứ không phải dữ liệu của chúng, trong khi các loại giá trị có danh tính của chúng ở nơi bên ngoài nơi chúng được lưu trữ. Danh tính của loại giá trị Eric của bạn chỉ đến từ biến nơi anh ta được lưu trữ. Nếu bạn vượt qua anh ta, anh ta sẽ mất danh tính.
IllidanS4 muốn Monica trở lại vào

6

Nó không liên quan gì đến các cấu trúc (và không phải với C #), nhưng trong Java, bạn có thể gặp vấn đề với các đối tượng có thể thay đổi khi chúng là các khóa trong bản đồ băm. Nếu bạn thay đổi chúng sau khi thêm chúng vào bản đồ và nó thay đổi mã băm của nó , điều xấu có thể xảy ra.


3
Điều đó đúng nếu bạn cũng sử dụng một lớp làm chìa khóa trong bản đồ.
Marc Gravell

6

Cá nhân khi tôi nhìn vào mã, những thứ sau đây trông khá khó hiểu với tôi:

data.value.set (data.value.get () + 1);

thay vì chỉ đơn giản

dữ liệu.value ++; hoặc data.value = data.value + 1;

Đóng gói dữ liệu rất hữu ích khi chuyển một lớp xung quanh và bạn muốn đảm bảo giá trị được sửa đổi theo kiểu được kiểm soát. Tuy nhiên, khi bạn đã thiết lập công khai và nhận các hàm ít hơn nhiều so với đặt giá trị cho những gì đã được truyền vào, làm thế nào đây là một cải tiến so với việc chuyển một cấu trúc dữ liệu công cộng xung quanh?

Khi tôi tạo một cấu trúc riêng bên trong một lớp, tôi đã tạo cấu trúc đó để tổ chức một tập hợp các biến thành một nhóm. Tôi muốn có thể sửa đổi cấu trúc đó trong phạm vi lớp, không nhận được các bản sao của cấu trúc đó và tạo các thể hiện mới.

Đối với tôi điều này ngăn việc sử dụng hợp lệ các cấu trúc đang được sử dụng để tổ chức các biến công khai, nếu tôi muốn kiểm soát truy cập tôi sẽ sử dụng một lớp.


1
Đi thẳng vào vấn đề! Cấu trúc là các đơn vị tổ chức mà không có hạn chế kiểm soát truy cập! Thật không may, C # đã làm cho chúng vô dụng cho mục đích này!
ThunderGr

điều này hoàn toàn bỏ lỡ điểm khi cả hai ví dụ của bạn hiển thị các cấu trúc có thể thay đổi.
vidstige

C # khiến chúng trở nên vô dụng cho mục đích này bởi vì đó không phải là mục đích của các cấu trúc
Luiz Felipe

6

Có một số vấn đề với ví dụ của ông Eric Lippert. Nó được cố gắng để minh họa điểm mà các cấu trúc được sao chép và làm thế nào đó có thể là một vấn đề nếu bạn không cẩn thận. Nhìn vào ví dụ tôi thấy nó là kết quả của một thói quen lập trình xấu và không thực sự là vấn đề với cả struct hoặc class.

  1. Một cấu trúc được cho là chỉ có các thành viên công cộng và không yêu cầu đóng gói. Nếu có thì nó thực sự phải là một loại / lớp. Bạn thực sự không cần hai cấu trúc để nói cùng một điều.

  2. Nếu bạn có lớp bao quanh một cấu trúc, bạn sẽ gọi một phương thức trong lớp để thay đổi cấu trúc thành viên. Đây là những gì tôi sẽ làm như một thói quen lập trình tốt.

Một thực hiện đúng sẽ như sau.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

Có vẻ như đó là một vấn đề với thói quen lập trình trái ngược với một vấn đề với chính struct. Structs được cho là có thể thay đổi, đó là ý tưởng và ý định.

Kết quả của những thay đổi voila hành xử như mong đợi:

1 2 3 Nhấn phím bất kỳ để tiếp tục. . .


4
Không có gì sai khi thiết kế các cấu trúc mờ nhỏ để hành xử như các đối tượng lớp bất biến; các hướng dẫn MSDN là hợp lý khi một người đang cố gắng tạo ra một cái gì đó hoạt động như một đối tượng . Cấu trúc phù hợp trong một số trường hợp người ta cần những thứ nhẹ hoạt động như vật thể, và trong trường hợp người ta cần một loạt các biến bị mắc kẹt với băng keo. Tuy nhiên, vì một số lý do, nhiều người không nhận ra rằng các cấu trúc có hai cách sử dụng riêng biệt và các hướng dẫn phù hợp cho cái này là không phù hợp với cái kia.
supercat

5

Có nhiều ưu điểm và nhược điểm đối với dữ liệu có thể thay đổi. Bất lợi triệu đô là răng cưa. Nếu cùng một giá trị đang được sử dụng ở nhiều nơi và một trong số chúng thay đổi nó, thì nó sẽ xuất hiện để thay đổi một cách kỳ diệu đến những nơi khác đang sử dụng nó. Điều này có liên quan đến, nhưng không giống với điều kiện chủng tộc.

Lợi thế triệu đô là đôi khi. Trạng thái có thể thay đổi có thể cho phép bạn ẩn thông tin thay đổi khỏi mã mà không cần biết về nó.

Nghệ thuật của thông dịch viên đi sâu vào những sự đánh đổi này một cách chi tiết, và đưa ra một số ví dụ.


cấu trúc không được đặt bí danh trong c #. Mỗi bài tập cấu trúc là một bản sao.
đệ quy

@recursive: Trong một số trường hợp, đó là một lợi thế lớn của các cấu trúc có thể thay đổi và một trong số đó khiến tôi đặt câu hỏi về khái niệm rằng các cấu trúc không nên có thể thay đổi. Thực tế là các trình biên dịch đôi khi hoàn toàn sao chép các cấu trúc không làm giảm tính hữu dụng của các cấu trúc có thể thay đổi.
supercat

1

Tôi không tin rằng họ xấu xa nếu sử dụng đúng cách. Tôi sẽ không đưa nó vào mã sản xuất của mình, nhưng tôi sẽ làm một cái gì đó giống như các thử nghiệm đơn vị có cấu trúc, trong đó tuổi thọ của một cấu trúc tương đối nhỏ.

Sử dụng ví dụ Eric, có lẽ bạn muốn tạo một phiên bản thứ hai của Eric đó, nhưng thực hiện các điều chỉnh, vì đó là bản chất của thử nghiệm của bạn (nghĩa là sao chép, sau đó sửa đổi). Không có vấn đề gì xảy ra với phiên bản đầu tiên của Eric nếu chúng ta chỉ sử dụng Eric2 cho phần còn lại của tập lệnh thử nghiệm, trừ khi bạn có kế hoạch sử dụng anh ấy làm so sánh thử nghiệm.

Điều này sẽ rất hữu ích để kiểm tra hoặc sửa đổi mã kế thừa mà nông xác định một đối tượng cụ thể (điểm của cấu trúc), nhưng bằng cách có một cấu trúc bất biến, điều này ngăn cản việc sử dụng nó một cách khó chịu.


Như tôi thấy, một cấu trúc nằm ở trung tâm của nó là một loạt các biến được gắn với nhau bằng băng keo. .NET có thể có cấu trúc để giả vờ là một thứ gì đó không phải là một loạt các biến bị mắc kẹt với băng keo, và tôi sẽ đề nghị rằng khi thực tế một loại sẽ giả vờ là một thứ khác không phải là một loạt các biến được gắn với nhau với băng keo nên hoạt động như một đối tượng thống nhất (mà đối với một cấu trúc sẽ ngụ ý tính bất biến), nhưng đôi khi rất hữu ích khi gắn một loạt các biến với nhau bằng băng keo. Ngay cả trong mã sản xuất, tôi sẽ cân nhắc tốt hơn khi có một loại ...
supercat 19/12/13

... rõ ràng không có ngữ nghĩa nào ngoài "mỗi trường chứa điều cuối cùng được viết cho nó", đẩy tất cả ngữ nghĩa vào mã sử dụng cấu trúc, hơn là cố gắng để cấu trúc làm được nhiều hơn. Ví dụ, được đưa ra một Range<T>loại với các thành viên MinimumMaximumcác trường loại Tvà mã Range<double> myRange = foo.getRange();, bất kỳ đảm bảo nào về những gì MinimumMaximumchứa nên đến từ foo.GetRange();. Có Rangecấu trúc trường tiếp xúc sẽ làm rõ rằng nó sẽ không thêm bất kỳ hành vi nào của chính nó.
supercat
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.