Коли ви використовуєте структуру замість класу? [зачинено]


174

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

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



7
@Frustrated Неможливо позначити питання як дублікат чогось іншого сайту, хоча це, безумовно, пов’язано.
Адам Лір

@Anna Lear: Я знаю, я проголосував за те, щоб не переходити до "копій". Після міграції він може бути належним чином закритий як дублікат .
FrustratedWithFormsDesigner

5
@ Розчарований, я не думаю, що це потрібно мігрувати.
Адам Лір

15
Це здається питанням, більш підходящим для P.SE проти SO. Ось чому я запитав це тут.
RationalGeek

Відповіді:


169

Загальне правило, яке слід дотримуватися, полягає в тому, що структури повинні бути невеликими, простими (однорівневими) колекціями споріднених властивостей, які незмінні після створення; будь-що інше, використовуйте клас.

C # приємно тим, що структури та класи не мають явних відмінностей у декларації, окрім визначального ключового слова; тож, якщо вам здається, що вам потрібно "модернізувати" структуру до класу, або, навпаки, "понизити" клас до структури, це здебільшого проста проблема зміни ключового слова (є декілька інших gotchas; структури не можуть отримати від будь-якого іншого типу класу або структури, і вони не можуть чітко визначити конструктор без параметрів за замовчуванням).

Я кажу "здебільшого", тому що, що важливіше знати про структури, це те, що вони є ціннісними типами, поводження з ними, як класи (референтні типи), може спричинити біль у півтора. Зокрема, внесення змін у властивості структури може спричинити несподівану поведінку.

Наприклад, скажіть, що у вас клас SimpleClass з двома властивостями, A і B. Ви створюєте копію цього класу, ініціалізуєте A і B, а потім передаєте екземпляр іншому методу. Цей спосіб додатково змінює A і B. Назад у функції виклику (тієї, що створила екземпляр), A і B вашого примірника матимуть значення, задані їм методом, що викликається.

Тепер ви зробите це структурою. Властивості все ще змінюються. Ви виконуєте ті самі операції з тим же синтаксисом, що і раніше, але тепер нові значення A і B не є в екземплярі після виклику методу. Що трапилось? Ну, ваш клас зараз є структура, тобто це тип цінності. Якщо ви передаєте тип значення методу, за замовчуванням (без ключового слова out або ref) є передача "за значенням"; неглибока копія екземпляра створюється для використання методом, а потім знищується, коли метод виконується, залишаючи початковий екземпляр неушкодженим.

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

З цієї причини практично кожен авторитет на C # каже завжди робити ваші структури непорушними; дозволяють споживачеві вказувати значення властивостей лише під час створення об'єкта і ніколи не надавати жодних засобів для зміни значень цього примірника. Поля для читання лише правила або властивості лише для отримання - це правило. Якщо споживач хоче змінити значення, він може створити новий об'єкт на основі значень старого зі змінами, які вони хочуть, або може викликати метод, який буде робити те саме. Це змушує їх ставитися до одного екземпляра вашої структури як до однієї концептуальної "цінності", неподільної та відмінної від (але можливо прирівняної до) всіх інших. Якщо вони виконують операцію зі збереженим вашим типом "значенням", вони отримують нове "значення", яке відрізняється від їх початкового значення,

Для хорошого прикладу подивіться тип DateTime. Ви не можете призначити жодне з полів екземпляра DateTime безпосередньо; ви повинні або створити новий, або викликати метод на існуючому, який створить новий екземпляр. Це тому, що дата і час - це "значення", як число 5, і зміна числа 5 призводить до нового значення, яке не є 5. Просто тому, що 5 + 1 = 6 не означає, що 5 зараз 6 тому що ви додали до нього 1 DateTimes працюють однаково; 12:00 не "стає" 12:01, якщо додати хвилину, ви отримаєте нове значення 12:01, яке відрізняється від 12:00. Якщо це логічний стан справ для вашого типу (хороші концептуальні приклади, які не вбудовані в .NET, - це гроші, відстань, вага та інші кількості UOM, де операції повинні враховувати всі частини значення), потім використовуйте структуру і відповідно проектуйте її. У більшості інших випадків, коли підпункти об'єкта повинні бути самостійно зміненими, використовуйте клас.


1
Хороша відповідь! Я б наголосив, що прочитав це методологією та книгою "Розробка домену, що здається", яка дещо пов'язана з вашою думкою про те, чому слід використовувати класи або структури, засновані на тому, яку концепцію ви насправді намагаєтесь реалізувати
Максим Поправко

1
Лише невелика деталь, яку варто згадати: ви не можете визначити власний конструктор за замовчуванням на структурі. Ви повинні зробити те, що ініціалізує все до його значення за замовчуванням. Зазвичай, це зовсім незначно, особливо на уроці, які є хорошим кандидатом на те, щоб бути структуром. Але це означатиме, що зміна класу на структуру може означати більше, ніж просто зміна ключового слова.
Лоран Буро-Рой

1
@ LaurentBourgault-Roy - Правда. Є й інші готчі; Наприклад, структури не можуть мати ієрархію спадкування. Вони можуть реалізовувати інтерфейси, але не можуть виходити з нічого іншого, крім System.ValueType (неявно через їх будову).
KeithS

Як розробник JavaScript, я повинен запитати дві речі. Чим об'єктні літерали відрізняються від типових звичаїв структів і чому важливо, щоб структури були незмінні?
Ерік Реппен

1
@ErikReppen: Відповідаючи на обидва ваші запитання: Коли структура передається у функцію, вона передається за значенням. З класом ви обходите посилання на клас (все ще за значенням). Ерік Ліпперт обговорює, чому це проблема: blogs.msdn.com/b/ericlippert/archive/2008/05/14/… . Кінцевий параграф КітС також охоплює ці питання.
Брайан

14

Відповідь: "Використовуйте структуру для чистих конструкцій даних, а клас для об'єктів з операціями", безумовно, неправильна ІМО. Якщо структура має велику кількість властивостей, то клас майже завжди є більш підходящим. Microsoft часто каже, з точки зору ефективності, якщо ваш тип перевищує 16 байт, це повинен бути клас.

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


1
Абсолютно. Я б подав пропозицію, якби міг. Я не розумію, звідки виникла ідея, що структури є для даних, а об'єкти - ні. Структура в C # - це лише механізм розподілу в стеку. Копії всієї структури передаються навколо, а не просто копія вказівника об'єкта.
mike30

2
@mike - C # структури не обов'язково виділяються в стеку.
Лі

1
@mike Ідея "структури для даних", ймовірно, походить від звички, набутої багаторічним програмуванням на С. C не має класів, а його структури є контейнерами, що відповідають лише даним.
Конаміман

Очевидно, що принаймні 3 програмісти на C (отримали схвалення), прочитавши відповідь Майкла К ;-)
bytedev

10

Найважливішою відмінністю між a classі a structє те, що відбувається в наступній ситуації:

  Річ річ1 = нова річ ();
  thing1.somePropertyOrField = 5;
  Річ річ2 = річ1;
  thing2.somePropertyOrField = 9;

На що має вплинути остання заява thing1.somePropertyOrField? Якщо Thingструктура, і somePropertyOrFieldє відкритим публічним полем, об'єкти thing1і thing2будуть "відриватися" один від одного, тому останнє твердження не вплине thing1. Якщо Thingце клас, то thing1і thing2вони будуть приєднані один до одного, і тому остання заява буде писати thing1.somePropertyOrField. Слід використовувати структуру у випадках, коли колишня семантика мала б більше сенсу, а клас слід використовувати у випадках, коли друга семантика мала б більше сенсу.

Зауважте, що хоча деякі люди радять, що бажання зробити щось змінне - це аргумент на користь того, щоб це був клас, я б припустив, що це зворотне: якщо щось, що існує для того, щоб зберігати деякі дані, стане змінним, і якщо не буде очевидно, чи приєднані екземпляри до чого-небудь, річ повинна бути структурою (швидше за все, з відкритими полями), щоб зрозуміти, що екземпляри не прикріплені ні до чого іншого.

Наприклад, розглянемо твердження:

  Особа somePerson = myPeople.GetPerson ("123-45-6789");
  somePerson.Name = "Мері Джонсон"; // Була "Мері Сміт"

Чи змінить друге твердження інформацію, що зберігається myPeople? Якщо Personструктура відкритого поля, вона не буде, і той факт, що вона не буде, буде очевидним наслідком того, що вона буде структурою відкритого поля ; якщо Personце структура та бажає оновити myPeople, явно потрібно було б зробити щось на кшталт myPeople.UpdatePerson("123-45-6789", somePerson). Якщо Personце клас, однак, може бути набагато складніше визначити, чи не буде вищезазначений код ніколи не оновлювати вміст MyPeople, завжди оновлювати його, а іноді й оновлювати.

Щодо уявлення про те, що структури повинні бути "непорушними", я взагалі не згоден. Існують дійсні випадки використання для "незмінних" структур (де інваріанти примусово виконуються в конструкторі), але вимагають, щоб вся структура повинна бути переписана будь-коли, коли будь-яка її зміна буде незручною, марною і більш схильною, щоб викликати помилки, ніж просто викриття поля безпосередньо. Наприклад, розглянемо PhoneNumberструктуру, поля якої включають серед інших, AreaCodeі Exchange, припустимо, одна має List<PhoneNumber>. Ефект наступного повинен бути досить чітким:

  for (int i = 0; i <myList.Count; i ++)
  {
    PhoneNumber theNumber = myList [i];
    if (theNumber.AreaCode == "312")
    {
      рядок newExchange = "";
      якщо (new312to708Exchanges.TryGetValue (theNumber.Exchange), вийдіть newExchange)
      {
        theNumber.AreaCode = "708";
        theNumber.Exchange = newExchange;
        myList [i] = число;
      }
    }
  }

Зауважте, що ніщо у наведеному вище коді не знає і не піклується про будь-які поля, PhoneNumberкрім AreaCodeта Exchange. Якби PhoneNumberтак звана "незмінна" структура, потрібно було б або забезпечити withXXметод для кожного поля, який би повертав новий екземпляр структури, який утримував передане значення у вказаному полі, або ж це було б необхідно для коду, як описано вище, щоб знати про кожне поле структури. Не зовсім привабливий.

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

  1. Семантика структури вказує на те, що вона зберігає ідентичність відповідного об'єкта, а не як ярлик для зберігання властивостей об'єкта. Наприклад, "KeyValuePair" міститиме ідентичність певних кнопок, але, як очікується, він не буде зберігати стійку інформацію про положення цих кнопок, виділяти стани тощо.
  2. Структура знає, що вона містить єдине посилання на цей об’єкт, ніхто більше не отримає посилання, і будь-які мутації, які коли-небудь будуть виконуватись на цей об’єкт, будуть зроблені до того, як посилання на нього зберігається в будь-якому місці.

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


7

Я схильний використовувати структури лише тоді, коли потрібен один метод, тому зробити клас здається занадто важким. Тому іноді я розглядаю структури як легкі предмети. ПОПЕРЕДЖАЙТЕ: це не завжди працює і це не завжди найкраще робити, тому це дійсно залежить від ситуації!

ДОСЛІДНІ РЕСУРСИ

Для отримання більш офіційного пояснення MSDN говорить: http://msdn.microsoft.com/en-us/library/ms229017.aspx Це посилання підтверджує те, що я згадував вище, конструкції слід використовувати лише для розміщення невеликої кількості даних.


5
Питання на іншому веб-сайті (навіть якщо цей сайт є переповненням стека ) не вважаються дублікатами. Розгорніть свою відповідь, щоб включити фактичну відповідь, а не лише посилання на інші місця. Якщо посилання колись змінюватимуться, ваша відповідь, як вона написана зараз, буде марною, і ми любимо зберігати інформацію та робити програмістів максимально автономними. Дякую!
Адам Лір

2
@Anna Lear Спасибі за те, що даєте мені знати, відтепер матиму це на увазі!
rrazd

2
-1 "Я схильний використовувати структури лише тоді, коли потрібен один метод, тому зробити клас здається занадто важким". ... важкий? що це насправді означає? ... а ви справді говорите, що просто у тому, що існує один метод, це, мабуть, має бути структура?
bytedev

2

Використовуйте структуру для чистих конструкцій даних, а клас - для об'єктів з операціями.

Якщо вашій структурі даних не потрібен контроль доступу і не має спеціальних операцій, крім отримання / встановлення, використовуйте структуру. Це робить очевидним, що вся ця структура є контейнером для даних.

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

Подумайте про це як про різницю між цеглою та літаком. Цегла - це структура; має довжину, ширину і висоту. Це не може зробити багато. У літака може бути кілька операцій, які його якось мутують, як переміщення керуючих поверхонь та зміна тяги двигуна. Було б краще як клас.


2
"Використовуйте структуру для чистих конструкцій даних, а клас для об'єктів з операціями" в C # це нісенітниця. Дивіться мою відповідь нижче.
bytedev

1
"Використовуйте структуру для чистих конструкцій даних та клас для об'єктів з операціями." Це стосується C / C ++, а не C #
taktak004
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.