Khi nào bạn sử dụng struct thay vì một lớp? [đóng cửa]


174

Quy tắc ngón tay cái của bạn là gì khi sử dụng structs so với các lớp? Tôi đang nghĩ về định nghĩa C # của các thuật ngữ đó nhưng nếu ngôn ngữ của bạn có các khái niệm tương tự tôi cũng muốn nghe ý kiến ​​của bạn.

Tôi có xu hướng sử dụng các lớp cho hầu hết mọi thứ và chỉ sử dụng các cấu trúc khi một thứ gì đó rất đơn giản và phải là một loại giá trị, chẳng hạn như PhoneNumber hoặc một cái gì đó tương tự. Nhưng điều này có vẻ như là một sử dụng tương đối nhỏ và tôi hy vọng có nhiều trường hợp sử dụng thú vị hơn.



7
@Frustrated Không thể đánh dấu một câu hỏi là một bản sao của một cái gì đó trên một trang web khác, mặc dù nó chắc chắn có liên quan.
Adam Lear

@Anna Lear: Tôi biết, tôi đã bỏ phiếu để di chuyển không gần như trùng lặp. Khi nó được di chuyển, nó có thể được đóng lại như một bản sao đúng cách .
Thất vọngWithFormsDesigner

5
@Frustrated Tôi không nghĩ rằng nó cần phải được di chuyển.
Adam Lear

15
Đây có vẻ như là một câu hỏi phù hợp hơn nhiều với P.SE so với SO. Đó là lý do tại sao tôi hỏi nó ở đây.
RationalGeek

Câu trả lời:


169

Nguyên tắc chung cần tuân theo là các cấu trúc nên là các tập hợp nhỏ, đơn giản (một cấp) của các thuộc tính liên quan, không thể thay đổi một khi được tạo; cho bất cứ điều gì khác, sử dụng một lớp.

C # là tốt ở chỗ các cấu trúc và các lớp không có sự khác biệt rõ ràng trong khai báo ngoài từ khóa xác định; vì vậy, nếu bạn cảm thấy cần phải "nâng cấp" một cấu trúc thành một lớp hoặc ngược lại "hạ cấp" một lớp thành một cấu trúc, thì việc thay đổi từ khóa là một vấn đề đơn giản (có một vài vấn đề khác; từ bất kỳ lớp hoặc kiểu cấu trúc nào khác và chúng không thể xác định rõ ràng một hàm tạo không tham số mặc định).

Tôi nói "chủ yếu", bởi vì điều quan trọng hơn cần biết về cấu trúc là bởi vì chúng là các loại giá trị, coi chúng như các lớp (loại tham chiếu) có thể kết thúc một nửa đau. Đặc biệt, làm cho các thuộc tính của cấu trúc có thể thay đổi có thể gây ra hành vi không mong muốn.

Ví dụ: giả sử bạn có một lớp SimpleClass có hai thuộc tính A và B. Bạn khởi tạo một bản sao của lớp này, khởi tạo A và B, sau đó chuyển thể hiện sang phương thức khác. Phương thức đó tiếp tục sửa đổi A và B. Quay lại hàm gọi (hàm đã tạo ra thể hiện), A và B của cá thể của bạn sẽ có các giá trị được cung cấp cho chúng theo phương thức được gọi.

Bây giờ, bạn làm cho nó một cấu trúc. Các tính chất vẫn có thể thay đổi. Bạn thực hiện các hoạt động tương tự với cú pháp giống như trước đây, nhưng bây giờ, các giá trị mới của A và B không có trong trường hợp sau khi gọi phương thức. Chuyện gì đã xảy ra? Chà, lớp của bạn bây giờ là một cấu trúc, có nghĩa là nó là một loại giá trị. Nếu bạn chuyển một loại giá trị cho một phương thức, mặc định (không có từ khóa out hoặc ref) sẽ chuyển "theo giá trị"; một bản sao nông của cá thể được tạo ra để sử dụng bởi phương thức, và sau đó bị hủy khi phương thức được thực hiện để nguyên đối tượng ban đầu.

Điều này càng trở nên khó hiểu hơn nếu bạn có một kiểu tham chiếu với tư cách là thành viên của cấu trúc của bạn (không được phép, nhưng thực tế cực kỳ xấu trong hầu hết các trường hợp); lớp sẽ không được sao chép (chỉ tham chiếu cấu trúc của nó), vì vậy các thay đổi đối với cấu trúc sẽ không ảnh hưởng đến đối tượng ban đầu, nhưng thay đổi đối với lớp con của cấu trúc SILL ảnh hưởng đến thể hiện từ mã gọi. Điều này có thể rất dễ dàng đặt các cấu trúc đột biến ở các trạng thái rất không nhất quán có thể gây ra lỗi cách xa vấn đề thực sự.

Vì lý do này, hầu như mọi cơ quan có thẩm quyền trên C # đều nói rằng luôn làm cho cấu trúc của bạn trở nên bất biến; cho phép người tiêu dùng chỉ định các giá trị của các thuộc tính khi xây dựng một đối tượng và không bao giờ cung cấp bất kỳ phương tiện nào để thay đổi các giá trị của thể hiện đó. Các trường chỉ đọc, hoặc các thuộc tính chỉ nhận, là quy tắc. Nếu người tiêu dùng muốn thay đổi giá trị, họ có thể tạo một đối tượng mới dựa trên các giá trị của đối tượng cũ, với những thay đổi họ muốn hoặc họ có thể gọi một phương thức sẽ làm tương tự. Điều này buộc họ phải coi một thể hiện duy nhất của cấu trúc của bạn là một "giá trị" khái niệm, không thể chia tách và khác biệt với (nhưng có thể tương đương với) tất cả các cấu trúc khác. Nếu họ thực hiện một thao tác trên "giá trị" được lưu trữ theo loại của bạn, họ sẽ nhận được "giá trị" mới khác với giá trị ban đầu của họ,

Để có một ví dụ tốt, hãy xem loại DateTime. Bạn không thể chỉ định trực tiếp bất kỳ trường nào trong trường hợp DateTime; bạn phải tạo một phương thức mới hoặc gọi một phương thức trên phương thức hiện có sẽ tạo ra một thể hiện mới. Điều này là do ngày và giờ là một "giá trị", giống như số 5 và thay đổi thành số 5 dẫn đến giá trị mới không phải là 5. Chỉ vì 5 + 1 = 6 không có nghĩa là 5 giờ là 6 bởi vì bạn đã thêm 1 vào nó DateTimes hoạt động theo cùng một cách; 12:00 không "trở thành" 12:01 nếu bạn thêm một phút, thay vào đó bạn nhận được giá trị mới 12:01 khác với 12:00. Nếu đây là trạng thái hợp lý cho loại của bạn (ví dụ về khái niệm tốt không được tích hợp trong .NET là Tiền, Khoảng cách, Trọng lượng và các số lượng khác của UOM nơi các hoạt động phải tính đến tất cả các phần của giá trị), sau đó sử dụng một cấu trúc và thiết kế nó cho phù hợp. Trong hầu hết các trường hợp khác, các mục con của một đối tượng nên có thể thay đổi độc lập, sử dụng một lớp.


1
Câu trả lời tốt! Tôi đã chỉ cho người ta đọc nó về phương pháp và cuốn sách 'Phát triển theo hướng tên miền', điều đó có vẻ hơi liên quan đến ý kiến ​​của bạn về lý do tại sao người ta nên sử dụng các lớp hoặc cấu trúc dựa trên khái niệm mà bạn thực sự cố gắng thực hiện
Maxim Popravko

1
Chỉ cần một chi tiết nhỏ đáng nói: bạn không thể xác định hàm tạo mặc định của riêng mình trên một cấu trúc. Bạn phải thực hiện với cái khởi tạo mọi thứ về giá trị mặc định của nó. Thông thường, đó là khá nhỏ, đặc biệt là trên lớp là ứng cử viên tốt để trở thành một cấu trúc. Nhưng nó sẽ có nghĩa là việc thay đổi một lớp thành một cấu trúc có thể ngụ ý nhiều hơn là chỉ thay đổi một từ khóa.
Laurent Bourgault-Roy

1
@ LaurentBourgault-Roy - Đúng. Có những vấn đề khác nữa; các cấu trúc không thể có một hệ thống phân cấp thừa kế, ví dụ. Họ có thể thực hiện các giao diện, nhưng không thể xuất phát từ bất kỳ thứ gì khác ngoài System.ValueType (hoàn toàn do cấu trúc của chúng).
KeithS

Là một nhà phát triển JavaScript, tôi phải hỏi hai điều. Làm thế nào các chữ nghĩa đối tượng khác với cách sử dụng điển hình của các cấu trúc và tại sao điều quan trọng là các cấu trúc đó là bất biến?
Erik Reppen

1
@ErikReppen: Trả lời cho cả hai câu hỏi của bạn: Khi một cấu trúc được truyền vào một hàm, nó được truyền theo giá trị. Với một lớp, bạn đang chuyển qua một tham chiếu đến lớp (vẫn theo giá trị). Eric Lippert thảo luận về lý do tại sao đây là một vấn đề: blog.msdn.com/b/ericlippert/archive/2008/05/14/ . Đoạn cuối của KeithS cũng bao gồm những câu hỏi này.
Brian

14

Câu trả lời: "Sử dụng một cấu trúc cho các cấu trúc dữ liệu thuần túy và một lớp cho các đối tượng có hoạt động" chắc chắn là IMO sai. Nếu một cấu trúc chứa một số lượng lớn các thuộc tính thì một lớp gần như luôn luôn phù hợp hơn. Microsoft thường nói, từ quan điểm hiệu quả, nếu loại của bạn lớn hơn 16 byte thì đó phải là một lớp.

https://stackoverflow.com/questions/1082311/why-should-a-net-struct-be-less-than-16-bytes


1
Chắc chắn rồi. Tôi sẽ nâng cấp nếu tôi có thể. Tôi không biết ý tưởng xuất phát từ các cấu trúc đó là gì cho dữ liệu và các đối tượng thì không. Một cấu trúc trong C # chỉ là một cơ chế để phân bổ trên ngăn xếp. Các bản sao của toàn bộ cấu trúc được truyền xung quanh thay vì chỉ là một bản sao của một con trỏ đối tượng.
mike30

2
@mike - Cấu trúc C # không nhất thiết phải được phân bổ trên ngăn xếp.
Lee

1
@mike Ý tưởng "structs for data" có lẽ xuất phát từ thói quen có được từ nhiều năm lập trình C. C không có lớp và cấu trúc của nó là các thùng chứa dữ liệu.
Konamiman

Rõ ràng có ít nhất 3 lập trình viên C (đã được nâng cấp) đã đọc câu trả lời của Michael K ;-)
bytedev

10

Sự khác biệt quan trọng nhất giữa a classvà a structlà những gì xảy ra trong tình huống sau:

  Điều thứ 1 = điều mới ();
  điều1.somePropertyOrField = 5;
  Điều thứ 2 = điều1;
  điều2.somePropertyOrField = 9;

Điều gì sẽ có hiệu lực của tuyên bố cuối cùng trên thing1.somePropertyOrField? Nếu Thingmột cấu trúc, và somePropertyOrFieldlà một trường công khai lộ ra, các đối tượng thing1thing2sẽ được "tách rời" khỏi nhau, vì vậy câu lệnh sau sẽ không ảnh hưởng thing1. Nếu Thinglà một lớp, sau đó thing1thing2sẽ được gắn với nhau, và vì vậy câu lệnh sau sẽ viết thing1.somePropertyOrField. Người ta nên sử dụng một cấu trúc trong trường hợp ngữ nghĩa trước sẽ có ý nghĩa hơn và nên sử dụng một lớp trong trường hợp ngữ nghĩa sau sẽ có ý nghĩa hơn.

Lưu ý rằng trong khi một số người khuyên rằng mong muốn tạo ra thứ gì đó có thể thay đổi là một lý lẽ ủng hộ nó là lớp, tôi sẽ đề xuất điều ngược lại là đúng: nếu thứ gì đó tồn tại với mục đích giữ một số dữ liệu sẽ có thể thay đổi được, và nếu không rõ liệu các thể hiện có được gắn vào bất cứ thứ gì hay không, thì vật đó phải là một cấu trúc (có thể có các trường được phơi bày) để làm rõ rằng các thể hiện đó không được gắn với bất kỳ thứ gì khác.

Ví dụ, hãy xem xét các tuyên bố:

  Person somePerson = myP People.GetPerson ("123-45-6789");
  somePerson.Name = "Mary Johnson"; // Đã là "Mary Smith"

Tuyên bố thứ hai sẽ thay đổi thông tin được lưu trữ trong myPeople? Nếu Personlà một cấu trúc trường tiếp xúc, nó sẽ không, và thực tế là nó sẽ không phải là một hậu quả rõ ràng của việc nó là một cấu trúc trường tiếp xúc ; Nếu Personlà một cấu trúc và một người muốn cập nhật myPeople, rõ ràng người ta sẽ phải làm một cái gì đó như thế nào myPeople.UpdatePerson("123-45-6789", somePerson). PersonTuy nhiên, nếu là một lớp, có thể khó hơn nhiều để xác định liệu đoạn mã trên sẽ không bao giờ cập nhật nội dung của MyPeople, luôn cập nhật nó hay đôi khi cập nhật nó.

Liên quan đến khái niệm rằng các cấu trúc nên là "bất biến", tôi không đồng ý nói chung. Có các trường hợp sử dụng hợp lệ cho các cấu trúc "bất biến" (trong đó các bất biến được thi hành trong một hàm tạo) nhưng yêu cầu toàn bộ cấu trúc phải được viết lại bất cứ khi nào bất kỳ phần nào của nó thay đổi là lúng túng, lãng phí và dễ gây ra lỗi hơn là chỉ đơn giản là phơi bày lĩnh vực trực tiếp. Ví dụ, hãy xem xét một PhoneNumbercấu trúc có các trường, bao gồm các trường khác AreaCodeExchange, giả sử một trường có một List<PhoneNumber>. Hiệu quả của những điều sau đây phải khá rõ ràng:

  for (int i = 0; i <myList.Count; i ++)
  {
    PhoneNumber theNumber = myList [i];
    if (theNumber.AreaCode == "312")
    {
      chuỗi newExchange = "";
      if (new312to708Exchanges.TryGetValue (theNumber.Exchange), ra newExchange)
      {
        theNumber.AreaCode = "708";
        theNumber.Exchange = newExchange;
        myList [i] = theNumber;
      }
    }
  }

Lưu ý rằng không có gì trong đoạn mã trên biết hoặc quan tâm đến bất kỳ lĩnh vực nào PhoneNumberkhác ngoài AreaCodeExchange. Nếu PhoneNumberlà một cấu trúc được gọi là "bất biến", thì nó sẽ cần một withXXphương thức cho mỗi trường, nó sẽ trả về một thể hiện cấu trúc mới giữ giá trị truyền vào trong trường được chỉ định, hoặc nếu không thì sẽ cần thiết cho mã như trên để biết về mọi lĩnh vực trong cấu trúc. Không chính xác hấp dẫn.

BTW, có ít nhất hai trường hợp trong đó các cấu trúc có thể giữ hợp lý các tham chiếu đến các loại có thể thay đổi.

  1. Các ngữ nghĩa của cấu trúc chỉ ra rằng nó lưu trữ danh tính của đối tượng được đề cập, chứ không phải là một phím tắt để giữ các thuộc tính của đối tượng. Ví dụ, một `KeyValuePair` sẽ giữ danh tính của một số nút nhất định, nhưng sẽ không được dự kiến ​​sẽ giữ thông tin liên tục về các vị trí của các nút đó, trạng thái tô sáng, v.v.
  2. Cấu trúc biết rằng nó giữ tham chiếu duy nhất cho đối tượng đó, không ai khác sẽ nhận được một tham chiếu và bất kỳ đột biến nào sẽ được thực hiện cho đối tượng đó sẽ được thực hiện trước khi tham chiếu đến nó được lưu trữ ở bất cứ đâu.

Trong kịch bản cũ, IDENTITY của đối tượng sẽ là bất biến; trong phần hai, lớp của đối tượng lồng nhau có thể không thực thi tính bất biến, nhưng cấu trúc giữ tham chiếu sẽ.


7

Tôi có xu hướng chỉ sử dụng các cấu trúc khi có một phương thức là cần thiết do đó làm cho một lớp có vẻ quá "nặng". Vì vậy, đôi khi tôi coi cấu trúc là các vật nhẹ. THƯỞNG: điều này không phải lúc nào cũng hoạt động và không phải lúc nào cũng là điều tốt nhất để làm, vì vậy nó thực sự phụ thuộc vào tình huống!

TÀI NGUYÊN EXTRA

Để giải thích chính thức hơn, MSDN cho biết: http://msdn.microsoft.com/en-us/l Library / ms229017.aspx Liên kết này xác minh những gì tôi đã đề cập ở trên, cấu trúc chỉ nên được sử dụng để chứa một lượng nhỏ dữ liệu.


5
Các câu hỏi trên một trang web khác (ngay cả khi trang web đó là Stack Overflow ) không được tính là trùng lặp. Vui lòng mở rộng câu trả lời của bạn để bao gồm một câu trả lời thực tế và không chỉ liên kết đến những nơi khác. Nếu các liên kết thay đổi, câu trả lời của bạn như được viết bây giờ sẽ vô dụng và chúng tôi muốn lưu giữ thông tin và làm cho các lập trình viên khép kín nhất có thể. Cảm ơn!
Adam Lear

2
@Anna Lear Cảm ơn đã cho tôi biết, từ bây giờ sẽ ghi nhớ điều đó!
rrazd

2
-1 "Tôi có xu hướng chỉ sử dụng các cấu trúc khi có một phương thức là cần thiết do đó làm cho một lớp có vẻ quá" nặng "." ... nặng? Điều đó thực sự có ý nghĩa gì? ... Và bạn có thực sự nói rằng chỉ cần có một phương thức thì có lẽ nó nên là một cấu trúc?
bytedev

2

Sử dụng một cấu trúc cho các cấu trúc dữ liệu thuần túy và một lớp cho các đối tượng có hoạt động.

Nếu cấu trúc dữ liệu của bạn không cần kiểm soát truy cập và không có hoạt động đặc biệt nào ngoài get / set, hãy sử dụng một cấu trúc. Điều này làm cho rõ ràng rằng tất cả các cấu trúc đó là một thùng chứa dữ liệu.

Nếu cấu trúc của bạn có các hoạt động sửa đổi cấu trúc theo bất kỳ cách phức tạp hơn, hãy sử dụng một lớp. Một lớp cũng có thể tương tác với các lớp khác thông qua các đối số cho các phương thức.

Hãy nghĩ về nó như sự khác biệt giữa một viên gạch và một chiếc máy bay. Một viên gạch là một cấu trúc; Nó có chiều dài, chiều rộng và chiều cao. Nó không thể làm gì nhiều. Một chiếc máy bay có thể có nhiều hoạt động làm thay đổi nó bằng cách nào đó, như di chuyển các bề mặt điều khiển và thay đổi lực đẩy của động cơ. Nó sẽ tốt hơn khi là một lớp học.


2
"Sử dụng một cấu trúc cho các cấu trúc dữ liệu thuần túy và một lớp cho các đối tượng có hoạt động" trong C #, điều này là vô nghĩa. Xem câu trả lời của tôi dưới đây.
bytedev

1
"Sử dụng một cấu trúc cho các cấu trúc dữ liệu thuần túy và một lớp cho các đối tượng có hoạt động." Điều này đúng với C / C ++, không phải C #
taktak004
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.