Khi nào tôi nên sử dụng struct chứ không phải là một lớp trong C #?


1391

Khi nào bạn nên sử dụng struct và không class trong C #? Mô hình khái niệm của tôi là các cấu trúc được sử dụng trong thời gian khi vật phẩm chỉ là một tập hợp các loại giá trị . Một cách hợp lý để giữ tất cả chúng lại với nhau thành một tổng thể gắn kết.

Tôi đã xem qua các quy tắc ở đây :

  • Một cấu trúc nên đại diện cho một giá trị duy nhất.
  • Một cấu trúc nên có dung lượng bộ nhớ nhỏ hơn 16 byte.
  • Một cấu trúc không nên được thay đổi sau khi tạo.

Làm những quy tắc này làm việc? Một cấu trúc có nghĩa là gì?


248
System.Drawing.Rectanglevi phạm cả ba quy tắc này.
ChrisW

4
có khá nhiều trò chơi thương mại được viết bằng C #, điểm quan trọng là chúng được sử dụng cho mã được tối ưu hóa
BlackTigerX

25
Các cấu trúc cung cấp hiệu suất tốt hơn khi bạn có các bộ sưu tập nhỏ các loại giá trị mà bạn muốn nhóm lại với nhau. Điều này xảy ra mọi lúc trong lập trình trò chơi, ví dụ, một đỉnh trong mô hình 3D sẽ có vị trí, tọa độ kết cấu và bình thường, nó thường sẽ không thay đổi. Một mô hình duy nhất có thể có một vài nghìn đỉnh hoặc nó có thể có một tá, nhưng các cấu trúc cung cấp ít chi phí tổng thể hơn trong kịch bản sử dụng này. Tôi đã xác minh điều này thông qua thiết kế động cơ của riêng tôi.
Chris D.


4
@ChrisW Tôi thấy, nhưng không phải những giá trị đó đại diện cho một hình chữ nhật, đó là, một giá trị "duy nhất"? Giống như Vector3D hoặc Color, chúng cũng có một vài giá trị bên trong, nhưng tôi nghĩ chúng đại diện cho các giá trị đơn lẻ?
Marson Mao

Câu trả lời:


604

Nguồn được OP tham chiếu có một số điểm đáng tin cậy ... nhưng còn Microsoft - quan điểm về việc sử dụng struct là gì? Tôi đã tìm kiếm một số học hỏi thêm từ Microsoft , và đây là những gì tôi tìm thấy:

Xem xét việc xác định cấu trúc thay vì một lớp nếu các thể hiện của loại nhỏ và thường tồn tại trong thời gian ngắn hoặc thường được nhúng trong các đối tượng khác.

Không xác định cấu trúc trừ khi loại có tất cả các đặc điểm sau:

  1. Nó đại diện một cách hợp lý một giá trị duy nhất, tương tự như các kiểu nguyên thủy (số nguyên, gấp đôi, v.v.).
  2. Nó có kích thước cá thể nhỏ hơn 16 byte.
  3. Nó là bất biến.
  4. Nó sẽ không phải được đóng hộp thường xuyên.

Microsoft luôn vi phạm các quy tắc đó

Được rồi, # 2 và # 3 nào. Từ điển yêu quý của chúng tôi có 2 cấu trúc nội bộ:

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //<Tkey, TValue>
{
    //  View code at *Reference Source
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

* Nguồn tham khảo

Nguồn 'JonnyCantCode.com' có 3 trên 4 - khá dễ tha thứ vì số 4 có lẽ sẽ không thành vấn đề. Nếu bạn thấy mình đấm bốc một cấu trúc, hãy suy nghĩ lại về kiến ​​trúc của bạn.

Hãy xem lý do tại sao Microsoft sẽ sử dụng các cấu trúc này:

  1. Mỗi cấu trúc, EntryEnumerator, đại diện cho các giá trị duy nhất.
  2. Tốc độ
  3. Entrykhông bao giờ được thông qua như một tham số bên ngoài lớp Dictionary. Điều tra sâu hơn cho thấy rằng để đáp ứng việc triển khai IEnumerable, Dictionary sử dụng Enumeratorcấu trúc mà nó sao chép mỗi khi một điều tra viên được yêu cầu ... có ý nghĩa.
  4. Nội bộ cho lớp Từ điển. Enumeratorlà công khai vì Từ điển là vô số và phải có khả năng truy cập như nhau đối với việc triển khai giao diện IEnumerator - ví dụ: IEnumerator getter.

Cập nhật - Ngoài ra, nhận ra rằng khi một cấu trúc thực hiện một giao diện - như Enumerator thực hiện - và được chuyển sang loại được triển khai đó, cấu trúc đó trở thành một kiểu tham chiếu và được chuyển sang heap. Nội bộ để các lớp từ điển, Enumerator vẫn còn là một loại giá trị. Tuy nhiên, ngay khi một phương thức gọi GetEnumerator(), kiểu tham chiếu IEnumeratorđược trả về.

Những gì chúng ta không thấy ở đây là bất kỳ nỗ lực hoặc bằng chứng nào về yêu cầu để giữ cho các cấu trúc không thay đổi hoặc duy trì kích thước cá thể chỉ từ 16 byte trở xuống:

  1. Không có gì trong các cấu trúc trên được tuyên bố readonly- không phải là bất biến
  2. Kích thước của các cấu trúc này có thể hơn 16 byte
  3. Entrycó một cuộc đời không xác định (từ Add(), để Remove(), Clear()hoặc thu gom rác thải);

Và ... 4. Cả hai cấu trúc lưu trữ TKey và TValue, mà tất cả chúng ta đều biết là hoàn toàn có khả năng là loại tham chiếu (thêm thông tin tiền thưởng)

Các khóa băm mặc dù, từ điển nhanh một phần vì việc tạo ra một cấu trúc nhanh hơn một loại tham chiếu. Ở đây, tôi có một Dictionary<int, int>kho lưu trữ 300.000 số nguyên ngẫu nhiên với các khóa tăng dần.

Dung lượng: 312874
MemSize: 2660827 byte
Đã hoàn thành Thay đổi kích thước: 5ms
Tổng thời gian để điền: 889ms

Dung lượng : số phần tử khả dụng trước khi mảng bên trong phải được thay đổi kích thước.

MemSize : được xác định bằng cách tuần tự hóa từ điển vào MemoryStream và nhận độ dài byte (đủ chính xác cho mục đích của chúng tôi).

Đã hoàn thành thay đổi kích thước : thời gian cần thiết để thay đổi kích thước mảng bên trong từ 150862 phần tử thành 312874 phần tử. Khi bạn hình dung rằng mỗi phần tử được sao chép liên tục thông qua Array.CopyTo(), điều đó không quá tồi tệ.

Tổng thời gian để điền : thừa nhận bị sai lệch do đăng nhập và một OnResizesự kiện tôi đã thêm vào nguồn; tuy nhiên, vẫn rất ấn tượng để lấp đầy 300k số nguyên trong khi thay đổi kích thước 15 lần trong quá trình hoạt động. Vì tò mò, tổng thời gian để lấp đầy là gì nếu tôi đã biết năng lực? 13ms

Vì vậy, bây giờ, nếu Entrylà một lớp học thì sao? Những thời điểm hoặc số liệu này thực sự khác nhau nhiều như vậy?

Dung lượng: 312874
MemSize: 2660827 byte
Đã hoàn thành Thay đổi kích thước: 26ms
Tổng thời gian để điền: 964ms

Rõ ràng, sự khác biệt lớn là trong việc thay đổi kích thước. Bất kỳ sự khác biệt nếu từ điển được khởi tạo với năng lực? Không đủ để quan tâm đến ... 12ms .

Điều gì xảy ra là, bởi vì Entrylà một cấu trúc, nó không yêu cầu khởi tạo như một kiểu tham chiếu. Đây là cả vẻ đẹp và nguyên nhân của loại giá trị. Để sử dụng Entrylàm loại tham chiếu, tôi đã phải chèn đoạn mã sau:

/*
 *  Added to satisfy initialization of entry elements --
 *  this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i < prime ; i++)
{
    destinationArray[i] = new Entry( );
}
/*  *********************************************** */  

Lý do tôi phải khởi tạo từng phần tử mảng Entrynhư một kiểu tham chiếu có thể được tìm thấy tại MSDN: Architecture Design . Nói ngắn gọn:

Không cung cấp một hàm tạo mặc định cho cấu trúc.

Nếu một cấu trúc xác định một hàm tạo mặc định, khi các mảng của cấu trúc được tạo, thời gian chạy ngôn ngữ chung sẽ tự động thực thi hàm tạo mặc định trên mỗi phần tử mảng.

Một số trình biên dịch, chẳng hạn như trình biên dịch C #, không cho phép các cấu trúc có các hàm tạo mặc định.

Nó thực sự là khá đơn giản và chúng tôi sẽ mượn từ Asimov của Ba luật của Robotics :

  1. Cấu trúc phải an toàn để sử dụng
  2. Cấu trúc phải thực hiện chức năng của nó một cách hiệu quả, trừ khi điều này sẽ vi phạm quy tắc số 1
  3. Cấu trúc phải được giữ nguyên trong quá trình sử dụng trừ khi cần phải phá hủy để đáp ứng quy tắc số 1

... Chúng ta lấy gì từ điều này : tóm lại, chịu trách nhiệm với việc sử dụng các loại giá trị. Chúng nhanh chóng và hiệu quả, nhưng có khả năng gây ra nhiều hành vi bất ngờ nếu không được duy trì đúng cách (tức là các bản sao không chủ ý).


8
Đối với các quy tắc của Microsoft, quy tắc về tính bất biến dường như được thiết kế để không khuyến khích sử dụng các loại giá trị theo cách mà hành vi của họ sẽ khác với các loại tham chiếu, mặc dù thực tế là ngữ nghĩa giá trị có thể thay đổi từng phần có thể hữu ích . Nếu có một loại có thể thay đổi được thì sẽ dễ dàng làm việc hơn và nếu các vị trí lưu trữ của loại đó phải được tách ra một cách hợp lý với nhau, thì loại đó phải là một cấu trúc "có thể thay đổi".
supercat


2
Thực tế là nhiều loại của Microsoft vi phạm các quy tắc đó không thể hiện vấn đề với các loại đó, mà chỉ cho thấy rằng các quy tắc không nên áp dụng cho tất cả các loại cấu trúc. Nếu một cấu trúc đại diện cho một thực thể duy nhất [như với Decimalhoặc DateTime], thì nếu nó không tuân theo ba quy tắc khác, thì nó sẽ được thay thế bằng một lớp. Nếu một cấu trúc chứa một tập hợp các biến cố định, mỗi biến có thể chứa bất kỳ giá trị nào hợp lệ cho kiểu của nó [ví dụ Rectangle], thì nó phải tuân theo các quy tắc khác nhau , một số trong số đó trái với các quy tắc cho các cấu trúc "giá trị đơn" .
supercat

4
@IAb bát: Một số người sẽ biện minh cho Dictionaryloại mục nhập trên cơ sở rằng đó chỉ là loại nội bộ, hiệu suất được coi là quan trọng hơn ngữ nghĩa hoặc một số lý do khác. Quan điểm của tôi là một loại như Rectanglenên có nội dung của nó được hiển thị dưới dạng các trường có thể chỉnh sửa riêng lẻ không phải là "vì" lợi ích hiệu năng vượt trội hơn các khiếm khuyết ngữ nghĩa kết quả, nhưng vì loại này đại diện cho một tập hợp các giá trị độc lập cố định và do đó, cấu trúc có thể thay đổi là cả hiệu suất cao hơn và vượt trội về mặt ngữ nghĩa .
supercat

2
@supercat: Tôi đồng ý ... và toàn bộ câu trả lời của tôi là 'nguyên tắc' khá yếu và nên sử dụng kiến ​​thức đầy đủ và hiểu biết về các hành vi. Xem câu trả lời của tôi về cấu trúc có thể thay đổi tại đây: stackoverflow.com/questions/8108920/
Khăn

155

Bất cứ khi nào bạn:

  1. không cần đa hình,
  2. muốn ngữ nghĩa giá trị, và
  3. muốn tránh phân bổ đống và chi phí thu gom rác liên quan.

Tuy nhiên, sự cảnh báo là các cấu trúc (lớn tùy ý) đắt hơn để vượt qua so với tham chiếu lớp (thường là một từ máy), vì vậy các lớp học có thể nhanh hơn trong thực tế.


1
Đó chỉ là một "cảnh báo". Cũng nên xem xét "nâng" các loại giá trị và các trường hợp như (Guid)null(không sao để chuyển null thành loại tham chiếu), trong số những thứ khác.

1
đắt hơn trong C / C ++? trong C ++, cách được đề xuất là truyền các đối tượng theo giá trị
Ion Todirel

@IonTodirel Không phải vì lý do an toàn bộ nhớ, hơn là hiệu năng sao? Nó luôn luôn là một sự đánh đổi, nhưng vượt qua 32 B bằng stack luôn luôn (TM) sẽ chậm hơn so với việc chuyển tham chiếu 4 B bằng cách đăng ký. Tuy nhiên , cũng lưu ý rằng việc sử dụng "giá trị / tham chiếu" hơi khác một chút trong C # và C ++ - khi bạn chuyển tham chiếu đến một đối tượng, bạn vẫn chuyển qua giá trị, mặc dù bạn đang chuyển một tham chiếu (bạn ' Về cơ bản, việc chuyển giá trị của tham chiếu, không phải là tham chiếu đến tham chiếu, về cơ bản). Đó không phải là giá trị ngữ nghĩa , nhưng về mặt kỹ thuật là "vượt qua giá trị".
Luaan

@Luaan Sao chép chỉ là một khía cạnh của chi phí. Việc xác định thêm do con trỏ / tham chiếu cũng có chi phí cho mỗi lần truy cập. Trong một số trường hợp, cấu trúc thậm chí có thể được di chuyển và do đó thậm chí không cần phải sao chép.
Onur

@Onur thật thú vị. Làm thế nào để bạn "di chuyển" mà không cần sao chép? Tôi nghĩ rằng lệnh asm "Mov" không thực sự "di chuyển". Nó sao chép.
Cầu thủ chạy cánh Sendon

148

Tôi không đồng ý với các quy tắc được đưa ra trong bài viết gốc. Dưới đây là quy tắc của tôi:

1) Bạn sử dụng các cấu trúc cho hiệu suất khi được lưu trữ trong mảng. (xem thêm Khi nào cấu trúc câu trả lời? )

2) Bạn cần chúng trong mã truyền dữ liệu có cấu trúc đến / từ C / C ++

3) Không sử dụng cấu trúc trừ khi bạn cần chúng:

  • Chúng hành xử khác với "đối tượng bình thường" ( kiểu tham chiếu ) theo sự phân công và khi chuyển qua làm đối số, điều này có thể dẫn đến hành vi không mong muốn; Điều này đặc biệt nguy hiểm nếu người nhìn vào mã không biết họ đang xử lý một cấu trúc.
  • Họ không thể được thừa kế.
  • Vượt qua các cấu trúc như các đối số đắt hơn các lớp.

4
+1 Có, tôi hoàn toàn đồng ý với # 1 (đây là một lợi thế rất lớn khi xử lý những thứ như hình ảnh, v.v.) và để chỉ ra rằng chúng khác với "đối tượng bình thường" và có cách biết điều này ngoại trừ kiến ​​thức hiện có hoặc kiểm tra loại chính nó. Ngoài ra, bạn không thể truyền giá trị null cho loại cấu trúc :-) Đây thực sự là một trường hợp mà tôi gần như muốn có một số 'Hungary' cho các loại giá trị không cốt lõi hoặc từ khóa 'struct' bắt buộc tại trang web khai báo biến .

@pst: Đúng là người ta phải biết một cái gì đó là một struct để biết nó sẽ hoạt động như thế nào, nhưng nếu một cái gì đó là structvới các lĩnh vực tiếp xúc, đó là tất cả mọi người phải biết. Nếu một đối tượng phơi bày một thuộc tính của kiểu cấu trúc trường tiếp xúc và nếu mã đọc cấu trúc đó thành một biến và sửa đổi, người ta có thể dự đoán một cách an toàn rằng hành động đó sẽ không ảnh hưởng đến đối tượng có thuộc tính được đọc trừ khi hoặc cho đến khi cấu trúc được viết trở lại. Ngược lại, nếu thuộc tính là một loại lớp có thể thay đổi, đọc nó và sửa đổi nó có thể cập nhật đối tượng cơ bản như mong đợi, nhưng ...
supercat

... nó cũng có thể không thay đổi gì, hoặc nó có thể thay đổi hoặc làm hỏng các đối tượng mà người ta không có ý định thay đổi. Có mã có ngữ nghĩa nói "thay đổi biến này tất cả những gì bạn thích; thay đổi sẽ không làm gì cho đến khi bạn lưu trữ chúng một cách rõ ràng ở đâu đó" có vẻ rõ ràng hơn là có mã "Bạn đang tham chiếu đến một đối tượng nào đó, có thể chia sẻ với bất kỳ số nào về các tài liệu tham khảo khác, hoặc có thể không được chia sẻ chút nào, bạn sẽ phải tìm ra ai khác có thể có tài liệu tham khảo cho đối tượng này để biết điều gì sẽ xảy ra nếu bạn thay đổi nó. "
supercat

Tại chỗ với # 1. Một danh sách đầy đủ các cấu trúc có thể nén dữ liệu có liên quan nhiều hơn vào bộ đệm L1 / L2 so với danh sách đầy đủ các tham chiếu đối tượng, (đối với cấu trúc có kích thước phù hợp).
Matt Stephenson

2
Kế thừa hiếm khi là công cụ phù hợp cho công việc và lý luận quá nhiều về hiệu suất mà không có hồ sơ là một ý tưởng tồi. Thứ nhất, cấu trúc có thể được thông qua bằng cách tham khảo. Thứ hai, chuyển qua tham chiếu hoặc theo giá trị hiếm khi là một vấn đề hiệu suất đáng kể. Cuối cùng, bạn không tính đến việc phân bổ đống và thu gom rác bổ sung cần diễn ra cho một lớp. Cá nhân, tôi thích nghĩ về các cấu trúc như dữ liệu cũ và các lớp như là những thứ thực hiện mọi thứ (các đối tượng) mặc dù bạn cũng có thể định nghĩa các phương thức trên các cấu trúc.
weberc2

88

Sử dụng một cấu trúc khi bạn muốn ngữ nghĩa giá trị trái ngược với ngữ nghĩa tham chiếu.

Biên tập

Không chắc chắn tại sao mọi người lại hạ thấp điều này nhưng đây là một điểm hợp lệ và được đưa ra trước khi op làm rõ câu hỏi của anh ta, và đó là lý do cơ bản cơ bản nhất cho một cấu trúc.

Nếu bạn cần ngữ nghĩa tham khảo, bạn cần một lớp không phải là một cấu trúc.


13
Tất cả mọi người biết rằng. Có vẻ như anh ấy đang tìm kiếm nhiều hơn một câu trả lời "struct is a value type".
TheSmurf

21
Đây là trường hợp cơ bản nhất và nên được nêu cho bất kỳ ai đọc bài đăng này và không biết điều đó.
JoshBerke

3
Không phải câu trả lời này không đúng; nó rõ ràng là Đó không thực sự là vấn đề.
TheSmurf

55
@Josh: Đối với bất kỳ ai chưa biết về nó, chỉ cần nói đó là một câu trả lời không đầy đủ, vì rất có thể họ cũng không biết ý nghĩa của nó.
TheSmurf

1
Tôi vừa mới đánh giá thấp điều này bởi vì tôi nghĩ một trong những câu trả lời khác nên được đặt lên hàng đầu - bất kỳ câu trả lời nào có nội dung "Để xen kẽ với mã không được quản lý, nếu không thì tránh".
Daniel Earwicker

59

Ngoài câu trả lời "đó là một giá trị", một kịch bản cụ thể để sử dụng cấu trúc là khi bạn biết rằng bạn có một bộ dữ liệu gây ra sự cố thu gom rác và bạn có rất nhiều đối tượng. Ví dụ, một danh sách lớn / mảng các thể hiện Person. Ẩn dụ tự nhiên ở đây là một lớp, nhưng nếu bạn có số lượng lớn cá thể Người tồn tại lâu dài, cuối cùng họ có thể làm tắc nghẽn GEN-2 và gây ra các quầy hàng của GC. Nếu kịch bản đảm bảo nó, một cách tiếp cận tiềm năng ở đây là sử dụng một mảng (không phải danh sách) các cấu trúc Person , tức là Person[]. Bây giờ, thay vì có hàng triệu đối tượng trong GEN-2, bạn có một đoạn đơn trên LOH (Tôi giả sử không có chuỗi nào ở đây - tức là một giá trị thuần túy không có bất kỳ tham chiếu nào). Điều này có rất ít tác động của GC.

Làm việc với dữ liệu này thật khó xử, vì dữ liệu có thể quá cỡ cho một cấu trúc và bạn không muốn sao chép giá trị chất béo mọi lúc. Tuy nhiên, truy cập nó trực tiếp trong một mảng không sao chép cấu trúc - nó nằm tại chỗ (tương phản với một bộ chỉ mục danh sách, sao chép). Điều này có nghĩa là rất nhiều công việc với các chỉ mục:

int index = ...
int id = peopleArray[index].Id;

Lưu ý rằng việc giữ các giá trị bất biến sẽ giúp ích ở đây. Đối với logic phức tạp hơn, hãy sử dụng một phương thức có tham số by-ref:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

Một lần nữa, đây là tại chỗ - chúng tôi đã không sao chép giá trị.

Trong các kịch bản rất cụ thể, chiến thuật này có thể rất thành công; tuy nhiên, đó là một scernario khá tiên tiến chỉ nên được thử nếu bạn biết bạn đang làm gì và tại sao. Mặc định ở đây sẽ là một lớp.


+1 Câu trả lời thú vị. Bạn có sẵn sàng chia sẻ bất kỳ giai thoại trong thế giới thực nào về cách tiếp cận như vậy đang được sử dụng không?
Jordão

@Jordao trên điện thoại di động, nhưng tìm kiếm google cho: + gravell + "tấn công bởi GC"
Marc Gravell

1
Cảm ơn rất nhiều. Tôi tìm thấy nó ở đây .
Jordão

2
@MarcGravell Tại sao bạn lại đề cập: sử dụng một mảng (không phải danh sách) ? ListTôi tin rằng, sử dụng một Arrayhậu trường. không
Royi Namir

4
@RoyiNamir Tôi cũng tò mò về điều này, nhưng tôi tin rằng câu trả lời nằm trong đoạn thứ hai của câu trả lời của Marc. "Tuy nhiên, việc truy cập nó trực tiếp trong một mảng không sao chép cấu trúc - nó nằm tại chỗ (tương phản với một bộ chỉ mục danh sách, sao chép)."
user1323245

40

Từ đặc tả ngôn ngữ C # :

1.7 Cấu trúc

Giống như các lớp, các cấu trúc là các cấu trúc dữ liệu có thể chứa các thành viên dữ liệu và các thành viên hàm, nhưng không giống như các lớp, các cấu trúc là các loại giá trị và không yêu cầu phân bổ heap. Một biến của kiểu cấu trúc lưu trữ trực tiếp dữ liệu của cấu trúc, trong khi đó một biến của kiểu lớp lưu trữ một tham chiếu đến một đối tượng được phân bổ động. Các kiểu cấu trúc không hỗ trợ kế thừa do người dùng chỉ định và tất cả các kiểu cấu trúc đều thừa kế hoàn toàn từ đối tượng kiểu.

Cấu trúc đặc biệt hữu ích cho các cấu trúc dữ liệu nhỏ có ngữ nghĩa giá trị. Số phức, điểm trong hệ tọa độ hoặc cặp giá trị khóa trong từ điển đều là những ví dụ hay về cấu trúc. Việc sử dụng các cấu trúc chứ không phải các lớp cho các cấu trúc dữ liệu nhỏ có thể tạo ra sự khác biệt lớn về số lượng cấp phát bộ nhớ mà ứng dụng thực hiện. Ví dụ, chương trình sau đây tạo và khởi tạo một mảng 100 điểm. Với Point được triển khai như một lớp, 101 đối tượng riêng biệt được khởi tạo một đối tượng cho một mảng và một đối tượng cho 100 phần tử.

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

Một cách khác là biến Point thành một cấu trúc.

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

Bây giờ, chỉ có một đối tượng được khởi tạo ngay lập tức một đối tượng cho mảng Mảng và các thể hiện Điểm được lưu trữ nội tuyến trong mảng.

Các hàm tạo cấu trúc được gọi với toán tử mới, nhưng điều đó không có nghĩa là bộ nhớ đang được phân bổ. Thay vì tự động phân bổ một đối tượng và trả về một tham chiếu cho nó, một hàm tạo cấu trúc chỉ đơn giản trả về chính giá trị cấu trúc (thường ở vị trí tạm thời trên ngăn xếp) và sau đó giá trị này được sao chép khi cần thiết.

Với các lớp, hai biến có thể tham chiếu cùng một đối tượng và do đó có thể các hoạt động trên một biến ảnh hưởng đến đối tượng được tham chiếu bởi biến kia. Với các cấu trúc, mỗi biến có một bản sao dữ liệu riêng và các thao tác trên một ảnh hưởng đến các biến khác là không thể. Ví dụ, đầu ra được tạo bởi đoạn mã sau tùy thuộc vào việc Điểm là một lớp hay một cấu trúc.

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

Nếu Điểm là một lớp, đầu ra là 20 vì a và b tham chiếu cùng một đối tượng. Nếu Điểm là một cấu trúc, đầu ra là 10 vì việc gán a cho b tạo ra một bản sao của giá trị và bản sao này không bị ảnh hưởng bởi việc gán tiếp theo cho ax

Ví dụ trước nhấn mạnh hai trong số những hạn chế của cấu trúc. Đầu tiên, sao chép toàn bộ cấu trúc thường kém hiệu quả hơn so với sao chép tham chiếu đối tượng, do đó việc truyền tham số và giá trị truyền có thể tốn kém hơn với các cấu trúc so với các loại tham chiếu. Thứ hai, ngoại trừ các tham số ref và out, không thể tạo các tham chiếu đến các cấu trúc, quy định việc sử dụng chúng trong một số tình huống.


4
Mặc dù thực tế là các tham chiếu đến các cấu trúc không thể được duy trì đôi khi là một hạn chế, nó cũng là một đặc tính rất hữu ích. Một trong những điểm yếu lớn của .net là không có cách nào tốt để chuyển mã bên ngoài tham chiếu đến một đối tượng có thể thay đổi mà không mất quyền kiểm soát đối tượng đó. Ngược lại, người ta có thể đưa ra một phương thức bên ngoài một cách an toàn cho một refcấu trúc có thể thay đổi và biết rằng mọi đột biến mà phương thức bên ngoài sẽ thực hiện trên nó sẽ được thực hiện trước khi nó trả về. Thật tệ .net không có bất kỳ khái niệm nào về các tham số phù du và giá trị trả về hàm, vì ...
supercat

4
... Điều đó sẽ cho phép các ngữ nghĩa thuận lợi của các cấu trúc được truyền qua refđể đạt được với các đối tượng lớp. Về cơ bản, các biến cục bộ, tham số và giá trị trả về hàm có thể là bền vững (mặc định), có thể trả về hoặc phù du. Mã sẽ bị cấm sao chép những thứ phù du vào bất cứ thứ gì tồn tại lâu hơn phạm vi hiện tại. Những thứ có thể trả về sẽ giống như những thứ phù du ngoại trừ việc chúng có thể được trả về từ một hàm. Giá trị trả về của hàm sẽ bị ràng buộc bởi các hạn chế chặt chẽ nhất áp dụng cho bất kỳ tham số "có thể trả lại" nào của nó.
supercat

34

Các cấu trúc tốt cho việc biểu diễn dữ liệu nguyên tử, trong đó dữ liệu đã nói có thể được sao chép nhiều lần bằng mã. Nhân bản một đối tượng nói chung tốn kém hơn so với sao chép một cấu trúc, vì nó liên quan đến việc phân bổ bộ nhớ, chạy hàm tạo và xử lý / thu gom rác khi thực hiện với nó.


4
Có, nhưng các cấu trúc lớn có thể đắt hơn các tham chiếu lớp (khi chuyển qua các phương thức).
Alex

27

Đây là một quy tắc cơ bản.

  • Nếu tất cả các trường thành viên là các loại giá trị tạo ra một cấu trúc .

  • Nếu bất kỳ trường thành viên nào là kiểu tham chiếu, hãy tạo một lớp . Điều này là do trường loại tham chiếu sẽ cần phân bổ heap.

Sơ đồ

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}

3
Các kiểu tham chiếu bất biến như stringtương đương về mặt ngữ nghĩa với các giá trị và việc lưu trữ một tham chiếu đến một đối tượng bất biến vào một trường không đòi hỏi phải phân bổ heap. Sự khác biệt giữa một cấu trúc với các trường công khai được phơi bày và một đối tượng lớp với các trường công khai được hiển thị là được cung cấp chuỗi mã var q=p; p.X=4; q.X=5;, p.Xsẽ có giá trị 4 nếu alà loại cấu trúc và 5 nếu đó là loại lớp. Nếu một người muốn có thể sửa đổi một cách thuận tiện các thành viên của loại, người ta nên chọn 'lớp' hoặc 'struct' dựa trên việc người ta muốn thay đổi qcó ảnh hưởng hay không p.
supercat

Có tôi đồng ý biến tham chiếu sẽ nằm trên ngăn xếp nhưng đối tượng mà nó đề cập sẽ tồn tại trên heap. Mặc dù cấu trúc và các lớp hoạt động khác nhau khi được gán cho một biến khác nhau, nhưng tôi không nghĩ đó là một yếu tố quyết định mạnh mẽ.
Usman Zafar

Các cấu trúc đột biến và các lớp đột biến hành xử hoàn toàn khác nhau; nếu một cái đúng, cái kia rất có thể sẽ sai. Tôi không chắc hành vi sẽ không phải là yếu tố quyết định trong việc xác định nên sử dụng một cấu trúc hay một lớp.
supercat

Tôi đã nói rằng đó không phải là một yếu tố quyết định mạnh mẽ bởi vì thông thường khi bạn tạo một lớp hoặc cấu trúc, bạn không chắc nó sẽ được sử dụng như thế nào. Vì vậy, bạn tập trung vào cách mọi thứ có ý nghĩa hơn từ quan điểm thiết kế. Dù sao tôi chưa bao giờ thấy ở một nơi duy nhất trong thư viện .NET nơi cấu trúc chứa một biến tham chiếu.
Usman Zafar

1
Kiểu cấu trúc ArraySegment<T>gói gọn a T[], luôn là kiểu lớp. Kiểu cấu trúc KeyValuePair<TKey,TValue>thường được sử dụng với các kiểu lớp làm tham số chung.
supercat

19

Đầu tiên: Kịch bản xen kẽ hoặc khi bạn cần chỉ định bố cục bộ nhớ

Thứ hai: Khi dữ liệu có kích thước gần giống với con trỏ tham chiếu.


17

Bạn cần sử dụng "struct" trong các tình huống mà bạn muốn chỉ định rõ ràng bố cục bộ nhớ bằng StructLayoutAttribution - thường dành cho PInvoke.

Chỉnh sửa: Nhận xét chỉ ra rằng bạn có thể sử dụng lớp hoặc struct với StructLayoutAttribution và điều đó chắc chắn là đúng. Trong thực tế, bạn thường sử dụng một cấu trúc - nó được phân bổ trên ngăn xếp so với heap, điều này có ý nghĩa nếu bạn chỉ chuyển một đối số cho một cuộc gọi phương thức không được quản lý.


5
StructLayoutAttribution có thể được áp dụng cho các cấu trúc hoặc các lớp vì vậy đây không phải là lý do để sử dụng các cấu trúc.
Stephen Martin

Tại sao nó có ý nghĩa nếu bạn chỉ chuyển một đối số cho một cuộc gọi phương thức không được quản lý?
David Klempfner

16

Tôi sử dụng các cấu trúc để đóng gói hoặc giải nén bất kỳ loại định dạng giao tiếp nhị phân. Điều đó bao gồm đọc hoặc ghi vào đĩa, danh sách đỉnh DirectX, giao thức mạng hoặc xử lý dữ liệu được mã hóa / nén.

Ba nguyên tắc bạn liệt kê không hữu ích cho tôi trong bối cảnh này. Khi tôi cần viết ra bốn trăm byte nội dung trong một Thứ tự cụ thể, tôi sẽ xác định cấu trúc bốn trăm byte và tôi sẽ lấp đầy nó bằng bất kỳ giá trị không liên quan nào mà nó phải có, và tôi sẽ để thiết lập nó bất cứ cách nào có ý nghĩa nhất tại thời điểm đó. (Được rồi, bốn trăm byte sẽ khá kỳ lạ-- nhưng khi tôi đang viết các tệp Excel để kiếm sống, tôi đã xử lý các cấu trúc lên tới khoảng bốn mươi byte, bởi vì đó là một số bản ghi BIFF lớn như thế nào.)


Bạn không thể dễ dàng sử dụng một loại tham chiếu cho điều đó mặc dù?
David Klempfner

15

Ngoại trừ các loại giá trị được sử dụng trực tiếp bởi bộ thực thi và nhiều loại khác cho các mục đích PInvoke, bạn chỉ nên sử dụng các loại giá trị trong 2 trường hợp.

  1. Khi bạn cần sao chép ngữ nghĩa.
  2. Khi bạn cần khởi tạo tự động, thông thường trong các mảng của các loại này.

# 2 dường như là một phần lý do cho sự phổ biến cấu trúc trong các lớp sưu tập .Net ..
I Ab.

Nếu việc đầu tiên người ta sẽ làm khi tạo một vị trí lưu trữ của một loại lớp là tạo một thể hiện mới của loại đó, lưu trữ một tham chiếu đến nó ở vị trí đó và không bao giờ sao chép tham chiếu ở bất kỳ nơi nào khác và không ghi đè lên nó, sau đó là một cấu trúc và lớp học sẽ hành xử giống hệt nhau. Các cấu trúc có một cách tiêu chuẩn thuận tiện để sao chép tất cả các trường từ trường này sang trường khác và thường sẽ cung cấp hiệu suất tốt hơn trong trường hợp người ta không bao giờ sao chép tham chiếu đến một lớp (ngoại trừ thistham số phù du được sử dụng để gọi phương thức của nó); các lớp cho phép một bản sao tham chiếu.
supercat

13

.NET hỗ trợ value typesreference types(trong Java, bạn chỉ có thể định nghĩa các loại tham chiếu). Các trường hợp reference typesđược phân bổ trong heap được quản lý và là rác được thu thập khi không có tài liệu tham khảo nổi bật nào về chúng. Trường hợp value types, mặt khác, được phân bổ trong stack, và bộ nhớ do đó phân bổ được khai hoang càng sớm càng phạm vi của họ kết thúc. Và tất nhiên, value typesđược thông qua bởi giá trị, và reference typesbằng cách tham khảo. Tất cả các loại dữ liệu nguyên thủy C #, ngoại trừ System.String, là các loại giá trị.

Khi nào nên sử dụng struct trên lớp,

Trong C #, structsvalue types, các lớp là reference types. Bạn có thể tạo các loại giá trị, trong C #, sử dụng enumtừ khóa và structtừ khóa. Việc sử dụng value typethay vì reference typesẽ dẫn đến ít đối tượng hơn trong heap được quản lý, điều này dẫn đến tải ít hơn trên bộ thu gom rác (GC), chu kỳ GC ít thường xuyên hơn và do đó hiệu suất tốt hơn. Tuy nhiên, value typescó những nhược điểm của họ quá. Vượt qua một cái lớn structchắc chắn là tốn kém hơn so với việc chuyển một tài liệu tham khảo, đó là một vấn đề rõ ràng. Vấn đề khác là chi phí liên quan đến boxing/unboxing. Trong trường hợp bạn đang tự hỏi điều đó boxing/unboxingcó nghĩa là gì , hãy theo các liên kết sau để được giải thích tốt vềboxingunboxing. Ngoài hiệu suất, có những lúc bạn chỉ cần các loại để có ngữ nghĩa giá trị, sẽ rất khó (hoặc xấu) để thực hiện nếu đóreference typeslà tất cả những gì bạn có. Bạn chỉ nên sử dụng value types, Khi bạn cần sao chép ngữ nghĩa hoặc cần khởi tạo tự động, thông thường trong arrayscác loại này.


Sao chép các cấu trúc nhỏ hoặc truyền theo giá trị cũng rẻ như sao chép hoặc chuyển tham chiếu lớp hoặc chuyển các cấu trúc theo ref. Vượt qua bất kỳ cấu trúc kích thước nào bằng refchi phí giống như chuyển tham chiếu lớp theo giá trị. Sao chép bất kỳ cấu trúc kích thước hoặc chuyển qua giá trị nào rẻ hơn so với thực hiện một bản sao phòng thủ của một đối tượng lớp và lưu trữ hoặc chuyển một tham chiếu đến đó. Các lớp thời gian lớn tốt hơn các cấu trúc để lưu trữ giá trị là (1) khi các lớp không thay đổi (để tránh sao chép phòng thủ) và mỗi trường hợp được tạo sẽ được truyền qua rất nhiều, hoặc ...
supercat

... (2) khi vì nhiều lý do, một cấu trúc đơn giản là không thể sử dụng được [ví dụ vì người ta cần sử dụng các tham chiếu lồng nhau cho một cái gì đó giống như một cái cây, hoặc vì một người cần đa hình]. Lưu ý rằng khi sử dụng các loại giá trị, người ta thường nên phơi bày các trường trực tiếp vắng mặt một lý do cụ thể không (trong khi với hầu hết các loại lớp nên được bọc trong các thuộc tính). Nhiều người trong số những cái gọi là "tệ nạn" của các loại giá trị có thể thay đổi xuất phát từ gói không cần thiết của các trường trong tài sản (ví dụ như trong khi một số trình biên dịch sẽ cho phép một để gọi một setter tài sản trên một cấu trúc chỉ đọc vì nó sẽ đôi khi ...
supercat

... làm điều đúng đắn, tất cả các trình biên dịch sẽ từ chối các nỗ lực để đặt trực tiếp các trường trên các cấu trúc đó; cách tốt nhất để đảm bảo trình biên dịch từ chối readOnlyStruct.someMember = 5;không phải là tạo someMembermột thuộc tính chỉ đọc mà thay vào đó làm cho nó trở thành một trường.
supercat

12

Một cấu trúc là một loại giá trị. Nếu bạn gán một cấu trúc cho một biến mới, biến mới sẽ chứa một bản sao của bản gốc.

public struct IntStruct {
    public int Value {get; set;}
}

Thực hiện các kết quả sau trong 5 trường hợp của cấu trúc được lưu trữ trong bộ nhớ:

var struct1 = new IntStruct() { Value = 0 }; // original
var struct2 = struct1;  // A copy is made
var struct3 = struct2;  // A copy is made
var struct4 = struct3;  // A copy is made
var struct5 = struct4;  // A copy is made

// NOTE: A "copy" will occur when you pass a struct into a method parameter.
// To avoid the "copy", use the ref keyword.

// Although structs are designed to use less system resources
// than classes.  If used incorrectly, they could use significantly more.

Một lớp là một loại tài liệu tham khảo. Khi bạn gán một lớp cho một biến mới, biến đó chứa một tham chiếu đến đối tượng lớp gốc.

public class IntClass {
    public int Value {get; set;}
}

Thực hiện các kết quả sau chỉ trong một phiên bản của đối tượng lớp trong bộ nhớ.

var class1 = new IntClass() { Value = 0 };
var class2 = class1;  // A reference is made to class1
var class3 = class2;  // A reference is made to class1
var class4 = class3;  // A reference is made to class1
var class5 = class4;  // A reference is made to class1  

Struct s có thể làm tăng khả năng xảy ra lỗi mã. Nếu một đối tượng giá trị được đối xử như một đối tượng tham chiếu có thể thay đổi, nhà phát triển có thể ngạc nhiên khi những thay đổi được thực hiện bị mất bất ngờ.

var struct1 = new IntStruct() { Value = 0 };
var struct2 = struct1;
struct2.Value = 1;
// At this point, a developer may be surprised when 
// struct1.Value is 0 and not 1

12

Tôi đã tạo một điểm chuẩn nhỏ với BenchmarkDotNet để hiểu rõ hơn về lợi ích "struct" về số lượng. Tôi đang kiểm tra vòng lặp thông qua mảng (hoặc danh sách) các cấu trúc (hoặc lớp). Tạo các mảng hoặc danh sách đó nằm ngoài phạm vi của điểm chuẩn - rõ ràng "lớp" nặng hơn sẽ sử dụng nhiều bộ nhớ hơn và sẽ liên quan đến GC.

Vì vậy, kết luận là: hãy cẩn thận với LINQ và các cấu trúc đấm bốc / bỏ hộp ẩn và sử dụng các cấu trúc cho các vi mô tối thiểu tuân thủ nghiêm ngặt các mảng.

PS Một điểm chuẩn khác về việc chuyển struct / class qua ngăn xếp cuộc gọi là https://stackoverflow.com/a/47864451/506147

BenchmarkDotNet=v0.10.8, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Core   : .NET Core 4.6.25211.01, 64bit RyuJIT


          Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max |    Median | Rank |  Gen 0 | Allocated |
---------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|----------:|-----:|-------:|----------:|
   TestListClass |  Clr |     Clr |  5.599 us | 0.0408 us | 0.0382 us |  5.561 us |  5.689 us |  5.583 us |    3 |      - |       0 B |
  TestArrayClass |  Clr |     Clr |  2.024 us | 0.0102 us | 0.0096 us |  2.011 us |  2.043 us |  2.022 us |    2 |      - |       0 B |
  TestListStruct |  Clr |     Clr |  8.427 us | 0.1983 us | 0.2204 us |  8.101 us |  9.007 us |  8.374 us |    5 |      - |       0 B |
 TestArrayStruct |  Clr |     Clr |  1.539 us | 0.0295 us | 0.0276 us |  1.502 us |  1.577 us |  1.537 us |    1 |      - |       0 B |
   TestLinqClass |  Clr |     Clr | 13.117 us | 0.1007 us | 0.0892 us | 13.007 us | 13.301 us | 13.089 us |    7 | 0.0153 |      80 B |
  TestLinqStruct |  Clr |     Clr | 28.676 us | 0.1837 us | 0.1534 us | 28.441 us | 28.957 us | 28.660 us |    9 |      - |      96 B |
   TestListClass | Core |    Core |  5.747 us | 0.1147 us | 0.1275 us |  5.567 us |  5.945 us |  5.756 us |    4 |      - |       0 B |
  TestArrayClass | Core |    Core |  2.023 us | 0.0299 us | 0.0279 us |  1.990 us |  2.069 us |  2.013 us |    2 |      - |       0 B |
  TestListStruct | Core |    Core |  8.753 us | 0.1659 us | 0.1910 us |  8.498 us |  9.110 us |  8.670 us |    6 |      - |       0 B |
 TestArrayStruct | Core |    Core |  1.552 us | 0.0307 us | 0.0377 us |  1.496 us |  1.618 us |  1.552 us |    1 |      - |       0 B |
   TestLinqClass | Core |    Core | 14.286 us | 0.2430 us | 0.2273 us | 13.956 us | 14.678 us | 14.313 us |    8 | 0.0153 |      72 B |
  TestLinqStruct | Core |    Core | 30.121 us | 0.5941 us | 0.5835 us | 28.928 us | 30.909 us | 30.153 us |   10 |      - |      88 B |

Mã số:

[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
    [ClrJob, CoreJob]
    [HtmlExporter, MarkdownExporter]
    [MemoryDiagnoser]
    public class BenchmarkRef
    {
        public class C1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        public struct S1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        List<C1> testListClass = new List<C1>();
        List<S1> testListStruct = new List<S1>();
        C1[] testArrayClass;
        S1[] testArrayStruct;
        public BenchmarkRef()
        {
            for(int i=0;i<1000;i++)
            {
                testListClass.Add(new C1  { Text1= i.ToString(), Text2=null, Text3= i.ToString() });
                testListStruct.Add(new S1 { Text1 = i.ToString(), Text2 = null, Text3 = i.ToString() });
            }
            testArrayClass = testListClass.ToArray();
            testArrayStruct = testListStruct.ToArray();
        }

        [Benchmark]
        public int TestListClass()
        {
            var x = 0;
            foreach(var i in testListClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayClass()
        {
            var x = 0;
            foreach (var i in testArrayClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestListStruct()
        {
            var x = 0;
            foreach (var i in testListStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayStruct()
        {
            var x = 0;
            foreach (var i in testArrayStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestLinqClass()
        {
            var x = testListClass.Select(i=> i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }

        [Benchmark]
        public int TestLinqStruct()
        {
            var x = testListStruct.Select(i => i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }
    }

Bạn đã tìm ra lý do tại sao các cấu trúc chậm hơn rất nhiều khi được sử dụng trong danh sách và như vậy? Có phải vì quyền anh ẩn và unboxing mà bạn đã đề cập? Nếu vậy tại sao nó xảy ra?
Marko Grdinic

Truy cập struct trong mảng nên nhanh hơn chỉ vì không cần thêm tham chiếu. Boxing / Unboxing là trường hợp cho linq.
La Mã Pokrovskij

10

Các loại cấu trúc trong C # hoặc các ngôn ngữ .net khác thường được sử dụng để giữ những thứ nên hoạt động như các nhóm giá trị có kích thước cố định. Một khía cạnh hữu ích của các kiểu cấu trúc là các trường của một thể hiện kiểu cấu trúc có thể được sửa đổi bằng cách sửa đổi vị trí lưu trữ mà nó được giữ và không có cách nào khác. Có thể mã hóa một cấu trúc theo cách mà cách duy nhất để thay đổi bất kỳ trường nào là xây dựng một thể hiện hoàn toàn mới và sau đó sử dụng một phép gán cấu trúc để thay đổi tất cả các trường của mục tiêu bằng cách ghi đè chúng bằng các giá trị từ thể hiện mới, nhưng trừ khi một struct cung cấp không có phương tiện tạo một thể hiện trong đó các trường của nó có các giá trị không mặc định, tất cả các trường của nó sẽ có thể thay đổi nếu và nếu chính cấu trúc đó được lưu trữ ở một vị trí có thể thay đổi.

Lưu ý rằng có thể thiết kế một loại cấu trúc để về cơ bản nó sẽ hoạt động giống như một loại lớp, nếu cấu trúc đó chứa trường loại lớp riêng và chuyển hướng các thành viên của chính nó đến đối tượng của lớp được bọc. Ví dụ, một PersonCollectionthuộc tính có thể cung cấp các thuộc tính SortedByNameSortedById, cả hai đều có tham chiếu "bất biến" đến một PersonCollection(được đặt trong hàm tạo của chúng) và thực hiện GetEnumeratorbằng cách gọi một trong hai creator.GetNameSortedEnumeratorhoặc creator.GetIdSortedEnumerator. Các cấu trúc như vậy sẽ hoạt động giống như một tham chiếu đến a PersonCollection, ngoại trừ việc các GetEnumeratorphương thức của chúng sẽ bị ràng buộc với các phương thức khác nhau trong PersonCollection. Người ta cũng có thể có một cấu trúc bao bọc một phần của một mảng (ví dụ: người ta có thể định nghĩa một ArrayRange<T>cấu trúc sẽ giữ một cuộc T[]gọi Arr, một intOffset và intLength, với thuộc tính được lập chỉ mục, đối với chỉ mục idxtrong phạm vi 0 đến Length-1, sẽ truy cập Arr[idx+Offset]). Thật không may, nếu foolà một phiên bản chỉ đọc của cấu trúc như vậy, các phiên bản trình biên dịch hiện tại sẽ không cho phép các hoạt động như thế foo[3]+=4;bởi vì chúng không có cách nào để xác định liệu các hoạt động đó có cố ghi vào các trường không foo.

Bạn cũng có thể thiết kế một cấu trúc để hành xử giống như một loại giá trị chứa một bộ sưu tập có kích thước thay đổi (sẽ xuất hiện để sao chép bất cứ khi nào cấu trúc đó) nhưng cách duy nhất để thực hiện công việc đó là đảm bảo rằng không có đối tượng nào struct giữ một tài liệu tham khảo sẽ được tiếp xúc với bất cứ điều gì có thể làm thay đổi nó. Ví dụ, người ta có thể có một cấu trúc giống như mảng chứa một mảng riêng và phương thức "put" được lập chỉ mục của nó tạo ra một mảng mới có nội dung giống như của bản gốc ngoại trừ một phần tử đã thay đổi. Thật không may, có thể hơi khó để làm cho các cấu trúc như vậy hoạt động hiệu quả. Mặc dù đôi khi các ngữ nghĩa cấu trúc có thể thuận tiện (ví dụ: có thể chuyển một bộ sưu tập giống như mảng sang một thói quen, với người gọi và callee đều biết rằng mã bên ngoài sẽ không sửa đổi bộ sưu tập,


10

Không - tôi không hoàn toàn đồng ý với các quy tắc. Chúng là những hướng dẫn tốt để xem xét với hiệu suất và tiêu chuẩn hóa, nhưng không xem xét các khả năng.

Như bạn có thể thấy trong các câu trả lời, có rất nhiều cách sáng tạo để sử dụng chúng. Vì vậy, những hướng dẫn này cần phải luôn luôn như vậy, luôn luôn vì lợi ích của hiệu suất và hiệu quả.

Trong trường hợp này, tôi sử dụng các lớp để biểu diễn các đối tượng trong thế giới thực ở dạng lớn hơn của chúng, tôi sử dụng các cấu trúc để biểu diễn các đối tượng nhỏ hơn có cách sử dụng chính xác hơn. Cách bạn nói, "một tổng thể gắn kết hơn." Các từ khóa được gắn kết. Các lớp sẽ là các phần tử hướng đối tượng hơn, trong khi các cấu trúc có thể có một số đặc điểm đó, mặc dù ở quy mô nhỏ hơn. IMO.

Tôi sử dụng chúng rất nhiều trong các thẻ Treeview và Listview nơi các thuộc tính tĩnh phổ biến có thể được truy cập rất nhanh. Tôi đã luôn đấu tranh để có được thông tin này theo cách khác. Ví dụ: trong các ứng dụng cơ sở dữ liệu của mình, tôi sử dụng Treeview nơi tôi có Bảng, SP, Hàm hoặc bất kỳ đối tượng nào khác. Tôi tạo và điền vào struct của mình, đặt nó vào thẻ, kéo nó ra, lấy dữ liệu của vùng chọn và cứ thế. Tôi sẽ không làm điều này với một lớp học!

Tôi cố gắng và giữ chúng nhỏ, sử dụng chúng trong các tình huống đơn lẻ và giữ cho chúng không thay đổi. Thật thận trọng khi nhận thức được bộ nhớ, phân bổ và hiệu suất. Và thử nghiệm là rất cần thiết.


Các cấu trúc có thể được sử dụng hợp lý để biểu diễn các đối tượng bất biến nhẹ hoặc chúng có thể được sử dụng hợp lý để biểu diễn các tập hợp cố định của các biến liên quan nhưng độc lập (ví dụ: tọa độ của một điểm). Lời khuyên trên trang đó là tốt cho các cấu trúc được thiết kế để phục vụ cho mục đích trước, nhưng sai cho các cấu trúc được thiết kế để phục vụ cho mục đích sau. Suy nghĩ hiện tại của tôi là các cấu trúc có bất kỳ trường riêng nào thường đáp ứng mô tả được chỉ định, nhưng nhiều cấu trúc sẽ hiển thị toàn bộ trạng thái của chúng thông qua các trường công khai.
supercat

Nếu thông số kỹ thuật cho loại "điểm 3d" chỉ ra rằng toàn bộ trạng thái của nó được hiển thị thông qua các thành viên có thể đọc được x, y và z và có thể tạo một thể hiện với bất kỳ kết hợp doublegiá trị nào cho các tọa độ đó, thì thông số đó sẽ buộc nó phải hành xử theo ngữ nghĩa giống hệt với cấu trúc trường tiếp xúc ngoại trừ một số chi tiết về hành vi đa luồng (lớp bất biến sẽ tốt hơn trong một số trường hợp, trong khi cấu trúc trường tiếp xúc sẽ tốt hơn trong các trường hợp khác, cấu trúc được gọi là "bất biến" sẽ tồi tệ hơn trong mọi trường hợp).
supercat

8

Quy tắc của tôi là

1, Luôn luôn sử dụng lớp học;

2, Nếu có bất kỳ vấn đề về hiệu năng, tôi cố gắng thay đổi một số lớp thành struct tùy thuộc vào các quy tắc mà @IAb trích đã đề cập, sau đó thực hiện kiểm tra để xem liệu những thay đổi này có thể cải thiện hiệu suất hay không.


Một trường hợp sử dụng đáng kể mà Microsoft bỏ qua là khi người ta muốn một biến loại Foođể đóng gói một tập hợp các giá trị độc lập cố định (ví dụ tọa độ của một điểm) mà đôi khi người ta sẽ muốn chuyển qua thành một nhóm và đôi khi muốn thay đổi độc lập. Tôi đã không tìm thấy bất kỳ mẫu nào để sử dụng các lớp kết hợp cả hai mục đích gần như độc đáo như một cấu trúc trường tiếp xúc đơn giản (là một tập hợp cố định của các biến độc lập, hoàn toàn phù hợp với dự luật).
supercat

1
@supercat: Tôi nghĩ không hoàn toàn công bằng khi đổ lỗi cho Microsoft về điều đó. Vấn đề thực sự ở đây là C # như một ngôn ngữ hướng đối tượng đơn giản là không tập trung vào các loại bản ghi đơn giản mà chỉ phơi bày dữ liệu mà không có nhiều hành vi. C # không phải là ngôn ngữ đa mô hình ở cùng mức độ, ví dụ như C ++. Điều đó đang được nói, tôi cũng tin rằng rất ít người lập trình OOP thuần túy, vì vậy có lẽ C # là ngôn ngữ quá lý tưởng. (Tôi cũng gần đây đã bắt đầu phơi bày public readonlycác trường trong các loại của mình, vì việc tạo các thuộc tính chỉ đọc đơn giản là quá nhiều công việc thực tế không có lợi.)
stakx - không còn đóng góp vào

1
@stakx: Không cần thiết phải "tập trung" vào các loại đó; công nhận họ cho những gì họ sẽ đủ. Điểm yếu lớn nhất của C # liên quan đến cấu trúc là vấn đề lớn nhất của nó trong nhiều lĩnh vực khác: ngôn ngữ cung cấp phương tiện không đầy đủ để chỉ ra khi nào các biến đổi nhất định là hoặc không phù hợp, và việc thiếu các cơ sở đó dẫn đến các quyết định thiết kế không may. Ví dụ, 99% "cấu trúc có thể thay đổi là xấu xa" bắt nguồn từ việc nhà soạn nhạc biến MyListOfPoint[3].Offset(2,3);thành var temp=MyListOfPoint[3]; temp.Offset(2,3);, một biến đổi không có thật khi được áp dụng ...
supercat

... đến Offsetphương pháp. Cách thích hợp để ngăn chặn mã giả như vậy không nên tạo ra các cấu trúc không cần thiết, mà thay vào đó, để cho phép các phương thức như Offsetđược gắn thẻ với một thuộc tính cấm biến đổi nói trên. Chuyển đổi số ngầm định cũng có thể tốt hơn nhiều nếu chúng có thể được gắn thẻ để chỉ áp dụng trong trường hợp yêu cầu của chúng là rõ ràng. Nếu quá tải tồn tại cho foo(float,float)foo(double,double), tôi sẽ khẳng định rằng cố gắng sử dụng một floatdoublethường không nên áp dụng một chuyển đổi ngầm định, nhưng thay vào đó nên là một lỗi.
supercat

Việc gán trực tiếp một doublegiá trị cho a floathoặc chuyển nó sang một phương thức có thể đưa ra một floatđối số nhưng không double, hầu như luôn luôn làm những gì lập trình viên dự định. Ngược lại, việc gán floatbiểu thức doublemà không có một kiểu chữ rõ ràng thường là một sai lầm. Thời gian duy nhất cho phép double->floatchuyển đổi ngầm sẽ gây ra vấn đề là khi nó sẽ gây ra tình trạng quá tải không lý tưởng được chọn. Tôi muốn khẳng định rằng cách đúng đắn để ngăn chặn điều đó không nên ngăn cấm việc nhân đôi -> thả nổi, nhưng gắn thẻ quá tải với các thuộc tính để không cho phép chuyển đổi.
supercat

8

Một lớp là một loại tài liệu tham khảo. Khi một đối tượng của lớp được tạo, biến mà đối tượng được gán chỉ giữ một tham chiếu đến bộ nhớ đó. Khi tham chiếu đối tượng được gán cho một biến mới, biến mới tham chiếu đến đối tượng ban đầu. Các thay đổi được thực hiện thông qua một biến được phản ánh trong biến khác vì cả hai đều tham chiếu đến cùng một dữ liệu. Một cấu trúc là một loại giá trị. Khi một cấu trúc được tạo, biến mà cấu trúc được gán giữ dữ liệu thực tế của cấu trúc. Khi struct được gán cho một biến mới, nó được sao chép. Do đó, biến mới và biến ban đầu chứa hai bản sao riêng biệt của cùng một dữ liệu. Thay đổi được thực hiện cho một bản sao không ảnh hưởng đến bản sao khác. Nói chung, các lớp được sử dụng để mô hình hóa hành vi phức tạp hơn hoặc dữ liệu dự định được sửa đổi sau khi một đối tượng lớp được tạo.

Các lớp và cấu trúc (Hướng dẫn lập trình C #)


Các cấu trúc cũng rất tốt trong trường hợp cần thiết phải buộc chặt một vài biến liên quan nhưng độc lập cùng với băng keo (ví dụ: tọa độ của một điểm). Các hướng dẫn MSDN là hợp lý nếu một người đang cố gắng tạo ra các cấu trúc hoạt động giống như các đối tượng, nhưng ít phù hợp hơn khi thiết kế các cốt liệu; một số trong số họ gần như chính xác sai trong tình huống sau. Ví dụ, mức độ độc lập của các biến được gói gọn trong một loại càng lớn, lợi thế của việc sử dụng cấu trúc trường tiếp xúc hơn là một lớp không thay đổi.
supercat

6

MYTH # 1: CHIẾN LƯỢC LÀ LỚP HỌC

Huyền thoại này có nhiều dạng khác nhau. Một số người tin rằng các loại giá trị không thể hoặc không nên có các phương thức hoặc hành vi quan trọng khác, chúng nên được sử dụng làm các kiểu truyền dữ liệu đơn giản, chỉ với các trường công khai hoặc các thuộc tính đơn giản. Loại DateTime là một ví dụ tốt cho điều này: nó có ý nghĩa đối với nó là một loại giá trị, về mặt là một đơn vị cơ bản như một số hoặc một ký tự, và nó cũng có ý nghĩa để nó có thể thực hiện các phép tính dựa trên Giá trị của nó. Nhìn mọi thứ từ hướng khác, các kiểu truyền dữ liệu thường phải là kiểu tham chiếu dù sao thì quyết định nên dựa trên giá trị mong muốn hoặc ngữ nghĩa của kiểu tham chiếu, chứ không phải sự đơn giản của kiểu. Những người khác tin rằng các loại giá trị là nhẹ hơn so với các loại tham chiếu về mặt hiệu suất. Sự thật là trong một số trường hợp, các loại giá trị có hiệu suất cao hơn, họ không yêu cầu thu gom rác trừ khi chúng được đóng hộp, không có chi phí nhận dạng loại và ví dụ như không yêu cầu hội thảo. Nhưng theo những cách khác, các kiểu tham chiếu được truyền tham số nhiều hiệu suất hơn, gán giá trị cho biến, trả về giá trị và các hoạt động tương tự chỉ cần 4 hoặc 8 byte để được hỗ trợ (tùy thuộc vào việc bạn đang chạy CLR 32 bit hay 64 bit ) thay vì sao chép tất cả dữ liệu. Hãy tưởng tượng nếu ArrayList bằng cách nào đó là một loại giá trị tinh khiết của Viking và chuyển một biểu thức ArrayList cho một phương thức liên quan đến việc sao chép tất cả dữ liệu của nó! Trong hầu hết các trường hợp, hiệu suất không thực sự được quyết định bởi loại quyết định này. Nút cổ chai hầu như không bao giờ là nơi bạn nghĩ rằng chúng sẽ ở đó và trước khi bạn đưa ra quyết định thiết kế dựa trên hiệu suất, bạn nên đo các tùy chọn khác nhau. Điều đáng chú ý là sự kết hợp của hai niềm tin cũng không hoạt động. Không quan trọng có bao nhiêu phương thức mà một loại có (dù đó là một lớp hay một cấu trúc) Bộ nhớ được thực hiện cho mỗi phiên bản không bị ảnh hưởng. (Có một chi phí về bộ nhớ chiếm cho chính mã, nhưng điều đó phát sinh một lần thay vì cho từng trường hợp.)

MYTH # 2: CÁC LOẠI TÀI LIỆU THAM KHẢO TRỰC TUYẾN; CÁC LOẠI GIÁ TRỊ TRỰC TUYẾN

Điều này thường được gây ra bởi sự lười biếng trên một phần của người lặp lại nó. Phần đầu tiên là chính xác, một ví dụ về kiểu tham chiếu luôn được tạo trên heap. Đây là phần thứ hai gây ra vấn đề. Như tôi đã lưu ý, giá trị của một biến tồn tại ở bất cứ nơi nào nó được khai báo, vì vậy nếu bạn có một lớp với biến thể hiện của kiểu int, giá trị của biến đó cho bất kỳ đối tượng cụ thể nào sẽ luôn là nơi phần còn lại của dữ liệu cho đối tượng là Lít trên đống. Chỉ các biến cục bộ (biến được khai báo trong các phương thức) và các tham số phương thức sống trên ngăn xếp. Trong C # 2 trở lên, thậm chí một số biến cục bộ không thực sự tồn tại trên ngăn xếp, như bạn sẽ thấy khi chúng ta xem các phương thức ẩn danh trong chương 5. CÁC KHÁI NIỆM NÀY CÓ LIÊN QUAN ĐẾN KHÔNG? Có thể cho rằng nếu bạn đang viết mã được quản lý, bạn nên để bộ thực thi lo lắng về cách sử dụng bộ nhớ tốt nhất. Thật, đặc tả ngôn ngữ không đảm bảo về cuộc sống ở đâu; một thời gian chạy trong tương lai có thể có thể tạo một số đối tượng trên ngăn xếp nếu nó biết nó có thể thoát khỏi nó hoặc trình biên dịch C # có thể tạo mã mà hầu như không sử dụng ngăn xếp. Huyền thoại tiếp theo thường chỉ là một vấn đề thuật ngữ.

MYTH # 3: ĐỐI TƯỢNG ĐƯỢC THAM KHẢO THAM KHẢO TRONG C # B DENG DEFAULT

Đây có lẽ là huyền thoại được truyền bá rộng rãi nhất. Một lần nữa, những người đưa ra yêu cầu này thường xuyên (mặc dù không phải lúc nào) cũng biết C # thực sự hành xử như thế nào, nhưng họ không biết những gì mà Pass vượt qua bởi tham chiếu thực sự có nghĩa là gì. Thật không may, điều này gây nhầm lẫn cho những người biết ý nghĩa của nó. Định nghĩa chính thức của truyền bằng tham chiếu tương đối phức tạp, liên quan đến giá trị l và thuật ngữ khoa học máy tính tương tự, nhưng điều quan trọng là nếu bạn truyền một biến bằng tham chiếu, phương thức bạn gọi có thể thay đổi giá trị của biến của trình gọi bằng cách thay đổi giá trị tham số của nó. Bây giờ, hãy nhớ rằng giá trị của biến loại tham chiếu là tham chiếu, không phải chính đối tượng. Bạn có thể thay đổi nội dung của đối tượng mà tham số tham chiếu mà không tham số được truyền bởi tham chiếu. Ví dụ,

void AppendHello(StringBuilder builder)
{
    builder.Append("hello");
}

Khi phương thức này được gọi, giá trị tham số (tham chiếu đến StringBuilder) được truyền theo giá trị. Ví dụ, nếu bạn thay đổi giá trị của biến trình xây dựng trong phương thức, thì với trình tạo câu lệnh = null; Thay đổi sẽ không được người gọi nhìn thấy, trái với huyền thoại. Thật thú vị khi lưu ý rằng không chỉ các bit bằng cách tham khảo bit bit của huyền thoại không chính xác, mà các đối tượng trên mạng cũng được thông qua bit bit. Bản thân các đối tượng không bao giờ được thông qua, bằng cách tham chiếu hoặc theo giá trị. Khi một loại tham chiếu có liên quan, hoặc biến được truyền bởi tham chiếu hoặc giá trị của đối số (tham chiếu) được truyền theo giá trị. Ngoài bất cứ điều gì khác, điều này trả lời câu hỏi điều gì sẽ xảy ra khi null được sử dụng làm đối số giá trị phụ nếu các đối tượng được truyền xung quanh, điều đó sẽ gây ra vấn đề, vì sẽ không có đối tượng nào vượt qua! Thay thế, tham chiếu null được truyền theo giá trị theo cùng một cách như mọi tham chiếu khác. Nếu lời giải thích nhanh chóng này khiến bạn hoang mang, bạn có thể muốn xem bài viết của tôi, Thông số kỹ thuật chuyển qua trong C #, Rằng (http://mng.bz/otVt ), đi sâu vào chi tiết hơn nhiều. Những huyền thoại không phải là những người duy nhất xung quanh. Quyền anh và unboxing đến trong phần hiểu lầm công bằng của họ, mà tôi sẽ cố gắng làm rõ tiếp theo.

Tham khảo: C # trong phiên bản sâu thứ 3 của Jon Skeet


1
Rất tốt giả sử bạn là chính xác. Cũng rất tốt để thêm một tài liệu tham khảo.
NoChance

5

Tôi nghĩ rằng một xấp xỉ đầu tiên tốt là "không bao giờ".

Tôi nghĩ rằng một xấp xỉ thứ hai tốt là "không bao giờ".

Nếu bạn đang tuyệt vọng cho sự hoàn hảo, hãy xem xét chúng, nhưng sau đó luôn luôn đo lường.


24
Tôi sẽ không đồng ý với câu trả lời đó. Structs có một sử dụng hợp pháp trong nhiều tình huống. Dưới đây là một ví dụ - xử lý dữ liệu chéo theo quy trình nguyên tử.
Franci Penov

25
Bạn nên chỉnh sửa bài đăng của mình và giải thích về quan điểm của bạn - bạn đã đưa ra ý kiến ​​của mình, nhưng bạn nên sao lưu nó với lý do tại sao bạn đưa ra ý kiến ​​này.
Erik Forbes

4
Tôi nghĩ rằng họ cần một thẻ tương đương với thẻ Chip của Totin ( en.wikipedia.org/wiki/Totin%27_Chip ) để sử dụng các cấu trúc. Nghiêm túc.
Greg

4
Làm thế nào để một người 87,5K đăng một câu trả lời như thế này? Anh ấy đã làm điều đó khi còn nhỏ?
Rohit Vipin Mathews

3
@Rohit - đó là sáu năm trước; tiêu chuẩn trang web rất khác nhau sau đó. Tuy nhiên, đây vẫn là một câu trả lời tồi.
Andrew Arnold

5

Tôi vừa mới giao dịch với Windows Communication Foundation [WCF] Named pipe và tôi đã nhận thấy rằng việc sử dụng Structs để đảm bảo rằng việc trao đổi dữ liệu là loại giá trị thay vì loại tham chiếu .


1
Đây là đầu mối tốt nhất của tất cả, IMHO.
Ivan

4

Cấu trúc C # là một thay thế nhẹ cho một lớp. Nó có thể làm gần giống như một lớp, nhưng nó ít "tốn kém" hơn khi sử dụng một cấu trúc hơn là một lớp. Lý do cho điều này là một chút kỹ thuật, nhưng để tóm tắt, các phiên bản mới của một lớp được đặt trên heap, nơi các cấu trúc mới được khởi tạo được đặt trên ngăn xếp. Hơn nữa, bạn không xử lý các tham chiếu đến các cấu trúc, như với các lớp, mà thay vào đó bạn đang làm việc trực tiếp với thể hiện cấu trúc. Điều này cũng có nghĩa là khi bạn truyền một cấu trúc cho một hàm, nó là theo giá trị, thay vì như một tham chiếu. Có nhiều hơn về điều này trong chương về các tham số chức năng.

Vì vậy, bạn nên sử dụng các cấu trúc khi bạn muốn biểu diễn các cấu trúc dữ liệu đơn giản hơn và đặc biệt nếu bạn biết rằng bạn sẽ khởi tạo rất nhiều trong số chúng. Có rất nhiều ví dụ trong .NET framework, trong đó Microsoft đã sử dụng các cấu trúc thay vì các lớp, ví dụ như cấu trúc Point, Hình chữ nhật và Màu.


3

Struct có thể được sử dụng để cải thiện hiệu suất thu gom rác. Mặc dù bạn thường không phải lo lắng về hiệu suất của GC, có những tình huống có thể là kẻ giết người. Giống như bộ nhớ cache lớn trong các ứng dụng có độ trễ thấp. Xem bài đăng này cho một ví dụ:

http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/


3

Các loại cấu trúc hoặc giá trị có thể được sử dụng trong các tình huống sau -

  1. Nếu bạn muốn ngăn chặn đối tượng được thu thập bằng cách thu gom rác.
  2. Nếu nó là một loại đơn giản và không có hàm thành viên nào sửa đổi các trường đối tượng của nó
  3. Nếu không có nhu cầu xuất phát từ các loại khác hoặc được dẫn xuất sang các loại khác.

Bạn có thể biết thêm về các loại giá trị và các loại giá trị ở đây trên liên kết này


3

Tóm lại, sử dụng struct nếu:

1- thuộc tính / trường đối tượng của bạn không cần phải thay đổi. Ý tôi là bạn chỉ muốn cho họ một giá trị ban đầu và sau đó đọc chúng.

2- thuộc tính và trường trong đối tượng của bạn là loại giá trị và chúng không quá lớn.

Nếu đó là trường hợp bạn có thể tận dụng các cấu trúc để có hiệu suất tốt hơn và phân bổ bộ nhớ được tối ưu hóa vì chúng chỉ sử dụng ngăn xếp chứ không phải cả ngăn xếp và đống (trong các lớp)


2

Tôi hiếm khi sử dụng một cấu trúc cho mọi thứ. Nhưng đó chỉ là tôi. Nó phụ thuộc vào việc tôi có cần đối tượng là nullable hay không.

Như đã nêu trong các câu trả lời khác, tôi sử dụng các lớp cho các đối tượng trong thế giới thực. Tôi cũng có suy nghĩ về các cấu trúc được sử dụng để lưu trữ một lượng nhỏ dữ liệu.


-11

Các cấu trúc theo hầu hết các cách như các lớp / đối tượng. Cấu trúc có thể chứa các chức năng, các thành viên và có thể được kế thừa. Nhưng cấu trúc trong C # được sử dụng chỉ để giữ dữ liệu . Các cấu trúc không tốn ít RAM hơn các lớp và dễ dàng hơn cho trình thu gom rác . Nhưng khi bạn sử dụng các hàm trong cấu trúc của mình, thì trình biên dịch thực sự có cấu trúc đó rất giống với lớp / đối tượng, vì vậy nếu bạn muốn một cái gì đó có hàm, thì hãy sử dụng lớp / đối tượng .


2
Các cấu trúc KHÔNG thể được kế thừa, xem msdn.microsoft.com/en-us/l
Library / 0taef578.aspx
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.