Коли я повинен використовувати структуру замість класу?


302

MSDN каже, що ви повинні використовувати структури, коли вам потрібні легкі предмети. Чи є інші сценарії, коли структура є кращою перед класом?

Деякі люди, можливо, забули це:

  1. структури можуть мати методи.
  2. структури не можуть бути успадковані.

Я розумію технічні відмінності між структурами та класами, просто не відчуваю, коли використовувати структуру.


Лише нагадування - те, що більшість людей, як правило, забувають у цьому контексті, - це те, що в C # структурах можуть бути і методи.
Петр к.

Відповіді:


295

MSDN має відповідь: Вибір між класами та структурами .

По суті, на цій сторінці ви знайдете контрольний список із 4-ма елементами і говорить про використання класу, якщо ваш тип не відповідає всім критеріям.

Не визначайте структуру, якщо тип не має всіх наведених нижче характеристик:

  • Воно логічно представляє єдине значення, схоже на примітивні типи (цілі, подвійні тощо).
  • Він має розмір екземпляра менше 16 байт.
  • Це незмінне.
  • Боксувати не доведеться часто.

1
Можливо, я пропускаю щось очевидне, але я не зовсім розумію міркування за "незмінною" частиною. Чому це потрібно? Хтось може це пояснити?
Тамас Цінеге

3
Вони, напевно, рекомендували це, бо якщо структура незмінна, то не має значення, що вона має значення значення семантики, а не референтну семантику. Розрізнення має значення лише якщо ви мутуєте об'єкт / структуру після створення копії.
Стівен К. Сталь

@DrJokepu: У деяких ситуаціях система зробить тимчасову копію структури, а потім дозволить передати цю копію шляхом посилання на код, який її змінює; оскільки тимчасова копія буде видалена, зміни будуть втрачені. Ця проблема особливо гостра, якщо структура має методи, які її мутують. Я категорично не погоджуюся з думкою, що змінність є причиною зробити щось класом, оскільки - незважаючи на деякі недоліки в c # і vb.net, змінні структури надають корисну семантику, яку неможливо досягти іншим способом; немає семантичних причин віддавати перевагу незмінній структурі класу.
supercat

4
@Chuu: розробляючи компілятор JIT, Microsoft вирішила оптимізувати код для копіювання структур, що мають 16 байт або менше; це означає, що копіювання 17-байтної структури може бути значно повільніше, ніж копіювання 16-байтної структури. Я не бачу особливих причин очікувати, що Microsoft поширить такі оптимізації на більші структури, але важливо зазначити, що хоча 17-байтові структури можуть копіювати повільніше, ніж 16-байтові структури, є багато випадків, коли великі структури можуть бути ефективнішими, ніж об'єкти великого класу та де відносна перевага конструкцій зростає з розміром структури.
supercat

3
@Chuu: Застосування тих же шаблонів використання до великих структур, що і до класів, може спричинити неефективний код, але правильне рішення часто не замінювати структури класами, а натомість використовувати структури більш ефективно; найголовніше, слід уникати передачі та повернення структур за значенням. Передавайте їх як refпараметри, коли це розумно робити. Передати структуру з 4000 полями як параметр ref методу, який змінює одне, буде дешевше, ніж передавати структуру з 4 полями за значенням методу, який повертає модифіковану версію.
supercat

53

Я здивований, що не прочитав жодної попередньої відповіді на це, що вважаю найважливішим аспектом:

Я використовую структури, коли хочу тип без ідентичності. Наприклад, точка 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);
    }
}

Якщо у вас є два екземпляри цієї структури, то вам не байдуже, чи є вони єдиними фрагментами даних у пам'яті або двома. Ви просто дбаєте про значення, які вони мають.


4
Трохи поза темою, але чому ви кидаєте ArgumentException, коли obj не ThreeDimensionsPoint? Ви не повинні просто повернути помилкове в такому випадку?
Свиш

4
Це правильно, я був надто нетерплячий. return falseце те, що мало там бути, виправляючи зараз.
Андрій Ронеа

Цікава причина використання структури. Я робив класи з визначеними GetHashCode та Equals аналогічно тому, що ви показуєте тут, але тоді я завжди повинен бути обережним, щоб не мутувати ці екземпляри, якщо я використовував їх як словникові ключі. Напевно, було б безпечніше, якби я визначив їх як структури. (Оскільки тоді ключ був би копією полів у той момент, коли структура стала ключем словника , то ключ залишився б незмінним, якщо я пізніше змінив оригінал.)
ToolmakerSteve

У вашому прикладі це нормально, тому що у вас є лише 12 байт, але майте на увазі, що якщо у вас є багато полів у цій структурі, які перевершують 16 байт, ви повинні розглянути можливість використання класу та замінити методи GetHashCode та Equals.
Даніель Ботеро

Тип значення в DDD не означає , що ви повинні обов'язково використовувати тип значення в C #
Даррагі

26

Про це Білл Вагнер має книгу у своїй книзі "Ефективний c #" ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Він робить висновок, використовуючи наступний принцип:

  1. Чи є основна відповідальність зберігання даних типу?
  2. Чи загальнодоступний його інтерфейс визначений повністю властивостями, які отримують доступ або змінюють його учасників даних?
  3. Ви впевнені, що у вашого типу ніколи не буде підкласів?
  4. Ви впевнені, що ваш тип ніколи не буде лікуватися поліморфно?

Якщо ви відповіли "так" на всі 4 запитання, використовуйте структуру. В іншому випадку використовуйте клас.


1
так ... об'єкти передачі даних (DTO) повинні бути структурами?
крейсер

1
Я б сказав так, якщо він відповідає 4 описаним вище критеріям. Чому до об'єкта передачі даних потрібно поводитись певним чином?
Барт Гійсенс

2
@cruizer залежить від вашої ситуації. В одному проекті ми мали спільні аудиторські поля в наших DTO, і тому написали базовий DTO, від якого успадкували інші.
Ендрю Гроте

1
Усі, крім (2), здаються відмінними принципами. Потрібно було б побачити його міркування, щоб знати, що саме він означає під (2), і чому.
ToolmakerSteve

1
@ToolmakerSteve: для цього вам доведеться прочитати книгу. Не думайте, що справедливо копіювати / вставляти великі частини книги.
Барт Гійсенс


12

Я буду використовувати структури, коли:

  1. об'єкт повинен бути прочитаний (кожен раз, коли ви передаєте / присвоюєте структуру, вона копіюється). Тільки об'єкти для читання чудові, коли мова йде про багатопотоковій обробці, оскільки вони не вимагають блокування в більшості випадків.

  2. об’єкт невеликий і недовговічний. У такому випадку є хороший шанс, що об’єкт буде виділений на стек, що набагато ефективніше, ніж розміщення його на керованій купі. Більше того, пам'ять, виділена об'єктом, буде звільнена, як тільки вона вийде за межі своєї області. Іншими словами, це менше роботи для збирача сміття, і пам'ять використовується більш ефективно.


10

Використовуйте клас, якщо:

  • Її ідентичність важлива. Структури копіюються неявно, коли передаються значенням методу.
  • Він матиме великий слід пам’яті.
  • Її поля потребують ініціалізаторів.
  • Вам потрібно успадкувати від базового класу.
  • Вам потрібна поліморфна поведінка;

Використовуйте структуру, якщо:

  • Він буде діяти як примітивний тип (int, long, byte тощо).
  • Він повинен мати невеликий слід пам’яті.
  • Ви викликаєте метод P / Invoke, який вимагає передачі структури за значенням.
  • Потрібно зменшити вплив збору сміття на продуктивність програми.
  • Її поля потрібно ініціалізувати лише до значень за замовчуванням. Це значення буде нульовим для числових типів, помилковим для булевих типів та нульовим для типів посилань.
    • Зауважте, що в C # 6.0 структури можуть мати конструктор за замовчуванням, який може бути використаний для ініціалізації полів структури до невідповідних значень.
  • Вам не потрібно успадковувати з базового класу (крім ValueType, з якого успадковуються всі структури).
  • Вам не потрібна поліморфна поведінка.

5

Я завжди використовував структуру, коли хотів згрупувати кілька значень для передачі речей з виклику методу, але мені не потрібно буде використовувати його ні для чого після того, як я прочитав ці значення. Так само як спосіб зберегти чистоту. Я схильний розглядати речі в структурі як "викидні", а речі в класі як більш корисні та "функціональні"


4

Якщо суб'єкт господарювання буде непорушним, питання про те, чи використовувати структуру або клас, як правило, буде швидкістю продуктивності, а не семантикою. У 32/64-бітній системі для посилань на класи потрібно зберігати 4/8 байт, незалежно від кількості інформації в класі; копіювання довідок до класу вимагатиме копіювання 4/8 байт. З іншого боку, кожен виразнийЕкземпляр класу матиме 8/16 байт накладних витрат на додаток до інформації, яку він містить, та вартості пам'яті посилань на нього. Припустимо, що потрібно масив з 500 сутностей, кожен з яких має чотири 32-бітні цілі числа. Якщо сутність є структурним типом, для масиву буде потрібно 8000 байт незалежно від того, чи всі 500 сутності однакові, всі різні або десь між ними. Якщо сутність типу класу, масив з 500 посилань займе 4000 байт. Якщо всі ці посилання вказують на різні об'єкти, об'єкти потребують додаткових 24 байтів кожен (12 000 байт на всі 500), загалом 16 000 байт - вдвічі більше, ніж вартість зберігання структури типу. З іншого боку, з коду створено один екземпляр об'єкта, а потім скопійовано посилання на всі 500 слотів масиву, загальна вартість складе 24 байти для цього примірника та 4, 000 для масиву - загалом 4024 байтів. Основна економія. Мало ситуацій спрацювало б так само, як і останній, але в деяких випадках можливо скопіювати деякі посилання на достатньо слотів масиву, щоб зробити такий обмін вартим.

Якщо сутність має бути змінною, питання про те, чи використовувати клас чи структуру, певним чином простіше. Припустимо, "Thing" - це або структура, або клас, який має ціле поле під назвою x, і робить такий код:

  Річ t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Чи хочеться, щоб останнє твердження впливало на t1.x?

Якщо Thing - це тип класу, t1 і t2 будуть еквівалентними, тобто t1.x і t2.x також будуть еквівалентними. Таким чином, друге твердження вплине на t1.x. Якщо Thing - тип структури, t1 і t2 будуть різними екземплярами, тобто t1.x і t2.x будуть посилатися на різні цілі числа. Таким чином, друге твердження не вплине на t1.x.

Змінні структури та мутаційні класи мають принципово різну поведінку, хоча .net має деякі химерності в обробці мутацій структури. Якщо ви хочете, щоб поведінка типу типу (тобто "t2 = t1" копіювала дані з t1 в t2, залишаючи t1 і t2 як окремі екземпляри), і якщо ви можете жити з химерністю в обробці типів значення .net, використовуйте будова. Якщо ви хочете, щоб семантика типу значень, але хитрості .net призведе до порушення семантики типу значень у додатку, використовуйте клас та перемовляйте.


3

Крім того, відмінні відповіді вище:

Структури - цінні типи.

Їх ніколи не можна встановити на Ніщо .

Встановлення структури = Нічого, встановить всі типи її значень за замовчуванням.


2

коли вам не дуже потрібна поведінка, але вам потрібна більше структури, ніж простий масив чи словник.

Слідкуйте за тим, як я думаю про структури взагалі. Я знаю, що вони можуть мати методи, але мені подобається зберігати цю загальну психічну відмінність.


Чому ти це кажеш? Структури можуть мати методи.
Естебан Арая

2

Як сказав @Simon, структури надають семантику "типу типу", тому якщо вам потрібна подібна поведінка з вбудованим типом даних, використовуйте структуру. Оскільки структури передаються копією, ви хочете переконатися, що вони невеликого розміру, приблизно 16 байт.


2

Це стара тема, але хотіла дати простий тест на тест.

Я створив два .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; }
}

Виконання еталону:

  • Створіть 1 тестовий клас
  • Створіть 1 TestStruct
  • Створіть 100 TestClass
  • Створіть 100 TestStruct
  • Створіть 10000 TestClass
  • Створіть 10000 TestStruct

Результати:

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

Хм ...

Я б не використовував збирання сміття як аргумент за / проти використання конструкцій проти класів. Керована купа працює так само, як стек - створення об’єкта просто ставить його у верхній частині купи, що майже так само швидко, як і виділення на стеку. Крім того, якщо об'єкт недовговічний і не переживає циклу GC, розселення є безкоштовним, оскільки GC працює лише з доступною пам'яттю. (Шукайте MSDN, є низка статей про керування пам’яттю .NET, я просто лінивий шукати їх).

Більшу частину часу, коли я використовую структуру, я забиваю, що б'є себе за це, тому що пізніше я виявляю, що наявність еталонної семантики зробила б щось простіше.

У будь-якому випадку, ці чотири пункти статті MSDN, розміщені вище, здаються гарною настановою.


1
Якщо вам іноді потрібна еталонна семантика із структурою, просто задекларуйте class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, і тоді a MutableHolder<T>буде об’єктом із семантикою змінного класу (це працює так само добре, якщо Tце структура або тип непорушного класу).
supercat

1

Структури в стеці не є Heap, тому вони є безпечними для потоків, і їх слід використовувати під час реалізації шаблону об'єкта передачі, ви ніколи не хочете використовувати об'єкти в Heap, вони є мінливими, ви хочете в цьому випадку використовувати стек викликів, це основний випадок використання структури. Мене дивують усі відповіді, які тут


-3

Я вважаю, що найкраща відповідь - просто використовувати структура, коли вам потрібно колекція властивостей, клас, коли це колекція властивостей ТА поведінки.


у структур можуть бути і методи
Нітін Чандран

Звичайно, але якщо вам потрібні методи, шанси 99%, ви неправильно використовуєте структуру замість класу. Єдині винятки, які я знайшов, коли нормально мати методи в структурі, - це зворотні виклики
Габріель Попеску,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.