Khi nào tôi nên sử dụng struct thay vì một lớp?


302

MSDN nói rằng bạn nên sử dụng cấu trúc khi bạn cần các vật nhẹ. Có kịch bản nào khác khi một cấu trúc được ưa thích hơn một lớp không?

Một số người có thể đã quên rằng:

  1. cấu trúc có thể có phương pháp.
  2. cấu trúc không thể được thừa kế.

Tôi hiểu sự khác biệt về kỹ thuật giữa các cấu trúc và các lớp, tôi chỉ không cảm thấy tốt khi sử dụng một cấu trúc.


Chỉ cần một lời nhắc nhở - điều mà hầu hết mọi người có xu hướng quên trong bối cảnh này là trong các cấu trúc C # cũng có thể có các phương thức.
petr k.

Câu trả lời:


295

MSDN có câu trả lời: Lựa chọn giữa các lớp và cấu trúc .

Về cơ bản, trang đó cung cấp cho bạn một danh sách kiểm tra gồm 4 mục và nói sẽ sử dụng một lớp trừ khi loại của bạn đáp ứng tất cả các tiêu chí.

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

  • 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.).
  • Nó có kích thước cá thể nhỏ hơn 16 byte.
  • Nó là bất biến.
  • Nó sẽ không phải được đóng hộp thường xuyên.

1
Có thể tôi đang thiếu một cái gì đó rõ ràng nhưng tôi không hiểu lý do đằng sau phần "bất biến". Tại sao điều này là cần thiết? Ai đó có thể giải thích nó?
Tamas Czinege

3
Có lẽ họ đã khuyến nghị điều này bởi vì nếu cấu trúc là bất biến, thì sẽ không có vấn đề gì nếu nó có giá trị ngữ nghĩa hơn là ngữ nghĩa tham chiếu. Sự khác biệt chỉ quan trọng nếu bạn thay đổi đối tượng / struct sau khi tạo một bản sao.
Stephen C. Steel

@DrJokepu: Trong một số trường hợp, hệ thống sẽ tạo một bản sao tạm thời của một cấu trúc và sau đó cho phép bản sao đó được chuyển qua tham chiếu đến mã thay đổi nó; vì bản sao tạm thời sẽ bị loại bỏ, thay đổi sẽ bị mất. Vấn đề này đặc biệt nghiêm trọng nếu một cấu trúc có các phương thức làm thay đổi nó. Tôi hoàn toàn không đồng ý với khái niệm rằng tính đột biến là một lý do để tạo ra một thứ gì đó, vì - mặc dù có một số thiếu sót trong c # và vb.net, các cấu trúc có thể thay đổi cung cấp ngữ nghĩa hữu ích không thể đạt được theo bất kỳ cách nào khác; không có lý do ngữ nghĩa để thích một cấu trúc bất biến cho một lớp.
supercat

4
@Chuu: Khi thiết kế trình biên dịch JIT, Microsoft đã quyết định tối ưu hóa mã để sao chép các cấu trúc có 16 byte hoặc nhỏ hơn; điều này có nghĩa là sao chép cấu trúc 17 byte có thể chậm hơn đáng kể so với sao chép cấu trúc 16 byte. Tôi thấy không có lý do cụ thể nào để mong đợi Microsoft mở rộng tối ưu hóa như vậy sang các cấu trúc lớn hơn, nhưng điều quan trọng cần lưu ý là trong khi các cấu trúc 17 byte có thể sao chép chậm hơn các cấu trúc 16 byte, có nhiều trường hợp các cấu trúc lớn có thể hiệu quả hơn các đối tượng lớp lớn và nơi lợi thế tương đối của các cấu trúc phát triển với kích thước của cấu trúc.
supercat

3
.... đáng chú ý nhất, người ta nên tránh vượt qua hoặc trả lại các cấu trúc theo giá trị. Vượt qua chúng như refcác tham số bất cứ khi nào hợp lý để làm như vậy. Truyền một cấu trúc với 4.000 trường làm tham số ref cho một phương thức thay đổi một giá trị sẽ rẻ hơn so với chuyển một cấu trúc có 4 trường theo giá trị cho một phương thức trả về phiên bản đã sửa đổi.
supercat

53

Tôi ngạc nhiên khi tôi chưa đọc bất kỳ câu trả lời nào trước đây, điều mà tôi cho là khía cạnh quan trọng nhất:

Tôi sử dụng structs khi tôi muốn một loại không có danh tính. Ví dụ: điểm 3D:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Nếu bạn có hai phiên bản của cấu trúc này, bạn không quan tâm nếu chúng là một phần dữ liệu duy nhất trong bộ nhớ hoặc hai. Bạn chỉ cần quan tâm đến (các) giá trị họ nắm giữ.


4
Hơi lạc đề, nhưng tại sao bạn lại ném ArgumentException khi obj không phải là ThreeDimensionalPoint? Bạn có nên trả lại sai trong trường hợp đó không?
Svish

4
Điều đó đúng, tôi đã quá háo hức. return falselà những gì nên có ở đó, sửa chữa ngay bây giờ.
Andrei Rînea

Một lý do thú vị để sử dụng một cấu trúc. Tôi đã tạo các lớp với GetHashCode và Equals được định nghĩa tương tự như những gì bạn hiển thị ở đây, nhưng sau đó tôi luôn phải cẩn thận để không làm biến đổi các trường hợp đó nếu tôi sử dụng chúng làm khóa từ điển. Có lẽ sẽ an toàn hơn nếu tôi định nghĩa chúng là cấu trúc. (Bởi vì sau đó khóa sẽ là bản sao của các trường tại thời điểm cấu trúc trở thành khóa từ điển , vì vậy khóa sẽ không thay đổi nếu sau này tôi thay đổi bản gốc.)
ToolmakerSteve

Trong ví dụ của bạn, bạn chỉ có 12 Byte nhưng hãy nhớ rằng nếu bạn có nhiều trường trong cấu trúc đó vượt qua 16 Byte, bạn phải xem xét sử dụng một lớp và ghi đè các phương thức GetHashCode và Equals.
Daniel Botero Correa

Loại giá trị trong DDD không có nghĩa là bạn nhất thiết phải sử dụng loại giá trị trong C #
Darragh

26

Bill Wagner có một chương về điều này trong cuốn sách "hiệu quả c #" của mình ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Ông kết luận bằng cách sử dụng nguyên tắc sau:

  1. Là khả năng đáp ứng chính của lưu trữ dữ liệu loại?
  2. Là giao diện công cộng của nó được xác định hoàn toàn bởi các thuộc tính truy cập hoặc sửa đổi các thành viên dữ liệu của nó?
  3. Bạn có chắc chắn loại của bạn sẽ không bao giờ có các lớp con?
  4. Bạn có chắc chắn loại của bạn sẽ không bao giờ được điều trị đa hình?

Nếu bạn trả lời 'có' cho cả 4 câu hỏi: hãy sử dụng cấu trúc. Nếu không, sử dụng một lớp.


1
vậy ... đối tượng truyền dữ liệu (DTOs) nên được cấu trúc?
dương

1
Tôi muốn nói có, nếu nó đáp ứng 4 tiêu chí được mô tả ở trên. Tại sao một đối tượng chuyển dữ liệu cần phải được xử lý theo một cách cụ thể?
Bart Gijssens

2
@cruizer tùy thuộc vào tình hình của bạn. Trong một dự án, chúng tôi đã có các lĩnh vực kiểm toán chung trong các DTO của mình và do đó đã viết một DTO cơ sở mà những người khác được thừa hưởng từ đó.
Andrew Grothe

1
Tất cả nhưng (2) dường như là nguyên tắc tuyệt vời. Sẽ cần phải xem lý lẽ của anh ta để biết chính xác anh ta có ý gì bởi (2), và tại sao.
ToolmakerSteve

1
@ToolmakerSteve: Bạn sẽ phải đọc cuốn sách đó. Đừng nghĩ rằng việc sao chép / dán các phần lớn của một cuốn sách là công bằng.
Bart Gijssens


12

Tôi sẽ sử dụng cấu trúc khi:

  1. một đối tượng được cho là chỉ đọc (mỗi khi bạn vượt qua / gán một cấu trúc thì nó sẽ được sao chép). Các đối tượng chỉ đọc là tuyệt vời khi xử lý đa luồng vì chúng không yêu cầu khóa trong hầu hết các trường hợp.

  2. một đối tượng là nhỏ và ngắn hạn. Trong trường hợp như vậy, rất có khả năng đối tượng sẽ được phân bổ trên stack, hiệu quả hơn nhiều so với việc đặt nó vào heap được quản lý. Hơn nữa bộ nhớ được phân bổ bởi đối tượng sẽ được giải phóng ngay khi nó vượt ra ngoài phạm vi của nó. Nói cách khác, nó ít hoạt động hơn đối với Garbage Collector và bộ nhớ được sử dụng hiệu quả hơn.


10

Sử dụng một lớp nếu:

  • Bản sắc của nó rất quan trọng. Các cấu trúc được sao chép ngầm khi được truyền bởi giá trị vào một phương thức.
  • Nó sẽ có một dấu chân bộ nhớ lớn.
  • Các lĩnh vực của nó cần khởi tạo.
  • Bạn cần kế thừa từ một lớp cơ sở.
  • Bạn cần hành vi đa hình;

Sử dụng cấu trúc nếu:

  • Nó sẽ hoạt động như một kiểu nguyên thủy (int, long, byte, v.v.).
  • Nó phải có một dấu chân bộ nhớ nhỏ.
  • Bạn đang gọi một phương thức P / Gọi yêu cầu một cấu trúc được truyền vào bằng giá trị.
  • Bạn cần giảm tác động của việc thu gom rác đến hiệu suất của ứng dụng.
  • Các trường của nó chỉ cần được khởi tạo với các giá trị mặc định của chúng. Giá trị này sẽ bằng 0 đối với các loại số, sai đối với các loại Boolean và null đối với các loại tham chiếu.
    • Lưu ý rằng trong cấu trúc C # 6.0 có thể có một hàm tạo mặc định có thể được sử dụng để khởi tạo các trường của cấu trúc thành các giá trị không phá hủy.
  • Bạn không cần phải kế thừa từ một lớp cơ sở (ngoài ValueType, từ đó tất cả các cấu trúc kế thừa).
  • Bạn không cần hành vi đa hình.

5

Tôi đã luôn sử dụng một cấu trúc khi tôi muốn nhóm lại một vài giá trị để chuyển mọi thứ trở lại từ một cuộc gọi phương thức, nhưng tôi sẽ không cần sử dụng nó cho bất cứ điều gì sau khi tôi đã đọc các giá trị đó. Chỉ là một cách để giữ cho mọi thứ sạch sẽ. Tôi có xu hướng xem mọi thứ trong một cấu trúc là "vứt bỏ" và những thứ trong một lớp là hữu ích và "chức năng" hơn


4

Nếu một thực thể sẽ không thay đổi, câu hỏi về việc nên sử dụng một cấu trúc hay một lớp nói chung sẽ là một trong những hiệu suất hơn là ngữ nghĩa. Trên hệ thống 32/64 bit, các tham chiếu lớp yêu cầu 4/8 byte để lưu trữ, bất kể lượng thông tin trong lớp; sao chép một tham chiếu lớp sẽ yêu cầu sao chép 4/8 byte. Mặt khác, mỗi khác biệtthể hiện của lớp sẽ có 8/16 byte chi phí ngoài thông tin chứa và chi phí bộ nhớ của các tham chiếu đến nó. Giả sử một người muốn một mảng gồm 500 thực thể, mỗi thực thể chứa bốn số nguyên 32 bit. Nếu thực thể là một kiểu cấu trúc, mảng sẽ yêu cầu 8.000 byte bất kể tất cả 500 thực thể đều giống hệt nhau, tất cả đều khác nhau hoặc ở đâu đó giữa. Nếu thực thể là một loại lớp, mảng 500 tham chiếu sẽ mất 4.000 byte. Nếu các tham chiếu đó đều trỏ đến các đối tượng khác nhau, thì các đối tượng sẽ yêu cầu thêm 24 byte mỗi đối tượng (12.000 byte cho tất cả 500), tổng cộng 16.000 byte - gấp đôi chi phí lưu trữ của loại cấu trúc. Mặt khác, trong mã đã tạo một thể hiện đối tượng và sau đó sao chép một tham chiếu đến tất cả 500 vị trí mảng, tổng chi phí sẽ là 24 byte cho thể hiện đó và 4, 000 cho mảng - tổng cộng 4.024 byte. Một khoản tiết kiệm lớn. Rất ít tình huống sẽ diễn ra tốt như tình huống cuối cùng, nhưng trong một số trường hợp, có thể sao chép một số tham chiếu đến đủ các vị trí mảng để làm cho việc chia sẻ đó trở nên đáng giá.

Nếu thực thể được coi là có thể thay đổi, câu hỏi về việc sử dụng một lớp hoặc cấu trúc là một cách dễ dàng hơn. Giả sử "Điều" là một cấu trúc hoặc lớp có trường số nguyên gọi là x và người ta thực hiện mã sau:

  Điều t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Có ai muốn câu lệnh sau ảnh hưởng đến t1.x không?

Nếu Thing là một loại lớp, t1 và t2 sẽ tương đương, có nghĩa là t1.x và t2.x cũng sẽ tương đương. Do đó, câu lệnh thứ hai sẽ ảnh hưởng đến t1.x. Nếu Thing là một loại cấu trúc, t1 và t2 sẽ là các trường hợp khác nhau, có nghĩa là t1.x và t2.x sẽ đề cập đến các số nguyên khác nhau. Do đó, câu lệnh thứ hai sẽ không ảnh hưởng đến t1.x.

Các cấu trúc có thể thay đổi và các lớp có thể thay đổi có các hành vi khác nhau về cơ bản, mặc dù .net có một số điểm kỳ quặc trong việc xử lý các đột biến cấu trúc. Nếu ai đó muốn hành vi loại giá trị (có nghĩa là "t2 = t1" sẽ sao chép dữ liệu từ t1 sang t2 trong khi để t1 và t2 làm các trường hợp riêng biệt) và nếu một người có thể sống với các quirks trong cách xử lý các loại giá trị của .net, hãy sử dụng một cấu trúc. Nếu ai đó muốn ngữ nghĩa loại giá trị nhưng các quirks của .net sẽ dẫn đến ngữ nghĩa loại giá trị bị hỏng trong ứng dụng của một người, hãy sử dụng một lớp và lẩm bẩm.


3

Ngoài ra các câu trả lời tuyệt vời ở trên:

Cấu trúc là các loại giá trị.

Chúng không bao giờ có thể được đặt thành Không có gì .

Đặt cấu trúc = Không có gì, sẽ đặt tất cả các loại giá trị của nó thành giá trị mặc định của chúng.


2

khi bạn không thực sự cần hành vi, nhưng bạn cần nhiều cấu trúc hơn một mảng hoặc từ điển đơn giản.

Theo dõi Đây là cách tôi nghĩ về các cấu trúc nói chung. Tôi biết họ có thể có phương pháp, nhưng tôi thích giữ sự phân biệt tinh thần chung đó.


tại sao bạn nói như vậy? Cấu trúc có thể có phương pháp.
Esteban Araya

2

Như @Simon đã nói, các cấu trúc cung cấp ngữ nghĩa "kiểu giá trị" vì vậy nếu bạn cần hành vi tương tự với kiểu dữ liệu tích hợp, hãy sử dụng một cấu trúc. Vì các cấu trúc được truyền qua bản sao, bạn muốn đảm bảo rằng chúng có kích thước nhỏ, khoảng 16 byte.


2

Đây là một chủ đề cũ, nhưng muốn cung cấp một bài kiểm tra điểm chuẩn đơn giản.

Tôi đã tạo hai tệp .cs:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Chạy điểm chuẩn:

  • Tạo 1 TestClass
  • Tạo 1 TestSturation
  • Tạo 100 TestClass
  • Tạo 100 TestSturation
  • Tạo 10000 TestClass
  • Tạo 10000 TestSturation

Các kết quả:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |

1

Hừm ...

Tôi sẽ không sử dụng bộ sưu tập rác làm đối số cho / chống lại việc sử dụng cấu trúc so với các lớp. Heap được quản lý hoạt động giống như một ngăn xếp - tạo ra một đối tượng chỉ cần đặt nó ở trên cùng của heap, gần như nhanh như phân bổ trên ngăn xếp. Ngoài ra, nếu một đối tượng tồn tại trong thời gian ngắn và không tồn tại trong chu trình GC, thì việc phân bổ là miễn phí vì GC chỉ hoạt động với bộ nhớ vẫn có thể truy cập được. (Tìm kiếm MSDN, có một loạt bài viết về quản lý bộ nhớ .NET, tôi quá lười để tìm kiếm chúng).

Hầu hết thời gian tôi sử dụng một cấu trúc, tôi tự đá mình vì đã làm như vậy, bởi vì sau đó tôi phát hiện ra rằng việc có ngữ nghĩa tham chiếu sẽ làm mọi thứ đơn giản hơn một chút.

Dù sao, bốn điểm trong bài viết MSDN được đăng ở trên có vẻ là một hướng dẫn tốt.


1
Nếu đôi khi bạn cần ngữ nghĩa tham chiếu với một cấu trúc, chỉ cần khai báo class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, và sau đó a MutableHolder<T>sẽ là một đối tượng với ngữ nghĩa lớp có thể thay đổi (điều này cũng hoạt động tốt nếu Tlà một cấu trúc hoặc một loại lớp bất biến).
supercat

1

Các cấu trúc nằm trên Stack chứ không phải Heap, do đó chúng là luồng an toàn và nên được sử dụng khi triển khai mẫu đối tượng chuyển, bạn không bao giờ muốn sử dụng các đối tượng trên Heap mà chúng không ổn định, trong trường hợp này bạn muốn sử dụng Call Stack, đây là một trường hợp cơ bản để sử dụng một cấu trúc Tôi ngạc nhiên bởi tất cả các cách trả lời ở đây,


-3

Tôi nghĩ rằng câu trả lời tốt nhất chỉ đơn giản là sử dụng struct khi thứ bạn cần là một tập hợp các thuộc tính, lớp khi nó là tập hợp các thuộc tính VÀ hành vi.


cấu trúc cũng có thể có phương pháp
Nithin Chandran

tất nhiên, nhưng nếu bạn cần phương thức thì có thể 99% là bạn sử dụng struct không đúng cách thay vì lớp. Các trường hợp ngoại lệ duy nhất tôi tìm thấy khi có các phương thức trong cấu trúc là các cuộc gọi lại
Lucian Gabriel Popescu
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.