Чому змінні структури «злі»?


485

Після обговорень тут на SO я вже кілька разів читав зауваження, що змінні структури є «злими» (як у відповіді на це запитання ).

Яка актуальна проблема з мутабельністю та структурами в C #?


12
Претендувати на змінні структури є злом, як заявляти на змінні ints, bools, а всі інші типи цінностей є злими. Існують випадки незмінність та незмінність. Ці випадки залежать від ролі, яку відіграють дані, а не типу розподілу / обміну пам'яттю.
Сліп Д. Томпсон,

41
@slipp intі boolє НЕ змінювані ..
Blorgbeard виходить

2
.-Синтаксис, завдяки чому операції з повторно набраними даними та типовими даними значеннями виглядають однаково, навіть якщо вони чітко відрізняються. Це помилка властивостей C #, а не структури - деякі мови пропонують альтернативний a[V][X] = 3.14синтаксис для мутації на місці. У C # вам краще запропонувати такі мутаторні структури, як "MutateV (Action <ref Vector2> mutator)", і використовувати його як a.MutateV((v) => { v.X = 3; }) (приклад занадто спрощений через обмеження, яке має C # щодо refключового слова, але з деякими обхідні шляхи повинні бути можливими) .
Сліпп Д. Томпсон

2
@Slipp Добре, я думаю, навпаки, щодо подібних структур. Чому ви вважаєте, що структури, які вже реалізовані в бібліотеці .NET, як DateTime або TimeSpan (такі подібні), є незмінними? Можливо, може бути корисним змінити лише одного члена варунку такої структури, але це занадто незручно, призводить до занадто багатьох проблем. Насправді ви помиляєтесь у тому, що робить процесор calc, оскільки C # не компілює асемблер, він компілюється в IL. У IL (якщо у нас вже є названа змінна x) ця одна операція - це 4 вказівки: ldloc.0(завантажує змінну 0-індексу на ...
Sushi271

1
... тип. Tє тип. Ref - це лише ключове слово, яке робить змінну переданою самому методу, а не його копію. Це також має сенс для посилальних типів, оскільки ми можемо змінити змінну , тобто посилання поза методом буде вказувати на інший об'єкт після зміни в методі. Оскільки ref Tце не тип, але спосіб передачі параметра методу, ви не <>можете його ввести, тому що там можуть бути поміщені лише типи. Так що це просто неправильно. Можливо, це було б зручно зробити, можливо, команда C # могла б зробити це для якоїсь нової версії, але зараз вони працюють над деякими ...
Sushi271

Відповіді:


290

Структури - це типи значень, тобто вони копіюються, коли передаються навколо.

Отже, якщо ви змінюєте копію, ви змінюєте лише цю копію, а не оригінал, а не інші копії, які можуть бути навколо.

Якщо ваша структура незмінна, то всі автоматичні копії, отримані в результаті передачі за значенням, будуть однаковими.

Якщо ви хочете змінити це, ви повинні свідомо зробити це, створивши новий екземпляр структури зі зміненими даними. (не копія)


82
"Якщо ваша структура незмінна, всі копії будуть однаковими." Ні, це означає, що вам доведеться свідомо робити копію, якщо ви хочете іншого значення. Це означає, що ви не потрапите на зміну копії, думаючи, що ви змінюєте оригінал.
Лукас

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

3
Ваша редакція (16 місяців потому) робить це трохи зрозумілішим. Я до сих пір стояв ", (незмінна структура) означає, що ви не потрапите на зміну копії, думаючи, що ви змінюєте оригінал".
Лукас

6
@Lucas: небезпека зробити копію структури, змінити її і якось подумати - це змінити оригінал (коли факт, що пишеться структура поля, робить самоочевидним факт, що пишеться лише копія) досить мала порівняно з небезпекою того, що той, хто тримає об’єкт класу як засіб зберігання інформації, що міститься в ньому, буде мутувати об'єкт для оновлення власної інформації та в процесі пошкодження інформації, що міститься в якомусь іншому об'єкті.
supercat

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

167

З чого почати ;-p

Блог Еріка Ліпперта завжди корисний для цитати:

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

По-перше, ви, як правило, втрачаєте зміни досить легко ... наприклад, вилучаючи речі зі списку:

Foo foo = list[0];
foo.Name = "abc";

що це змінило? Нічого корисного ...

Те саме з властивостями:

myObj.SomeProperty.Size = 22; // the compiler spots this one

змушує вас робити:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

менш критично, існує проблема розміру; об'єкти, що змінюються, мають тенденцію до декількох властивостей; але якщо у вас є структура з двома ints, a string, a DateTimeі a bool, ви можете дуже швидко записати багато пам'яті. З класом декілька абонентів можуть ділитися посиланням на один і той же екземпляр (посилання невеликі).


5
Ну так, але компілятор так просто дурний. Не допустити присвоєння членам властивість-STRUCT було ИМХО глупо дизайнерське рішення, тому що це дозволило ++оператору. У цьому випадку компілятор просто записує явне призначення замість того, щоб поспішати програмісту.
Конрад Рудольф

12
@Konrad: myObj.SomeProperty.Size = 22 змінить КОПІЮ myObj.SomeProperty. Компілятор рятує вас від явної помилки. І це не дозволено для ++.
Лукас

@Lucas: Ну, компілятор Mono C #, безумовно, дозволяє - оскільки у мене немає Windows, я не можу перевірити компілятор Microsoft.
Конрад Рудольф

6
@Konrad - з однією менш непрямою вона повинна працювати; це "мутація значення чогось, що існує лише як перехідне значення на стеку і яке збирається випаровуватися в небуття", що є випадком, який блокується.
Марк Гравелл

2
@Marc Gravell: У колишньому фрагменті коду ви закінчуєте "Foo", ім'я якого - "abc" та інші атрибути якого є списком List [0], не порушуючи List [0]. Якщо Фоо був класом, потрібно було б його клонувати, а потім змінити копію. На мій погляд, великою проблемою з відмінністю значення-типу від класу є використання знака "." оператор для двох цілей. Якби я мав свої барабани, класи могли б підтримувати обидва "". і "->" для методів і властивостей, але нормальна семантика для ". Властивості полягають у створенні нового примірника з відповідним зміненим полем.
supercat

71

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

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

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


1
Ерік Ліпперт каже, що вони ... дивіться моєї відповіді.
Марк Гравелл

42
Наскільки я поважаю Еріка Ліпперта, він не Бог (або, принаймні, ще не). Повідомлення в блозі, на яке ви посилаєтесь, і ваша публікація вище, є розумними аргументами для того, щоб зробити структури незмінними, звичайно, але вони насправді дуже слабкі, як аргументи за те, що ніколи не використовувати мутаційні структури. Однак ця публікація є +1.
Стівен Мартін

2
Розвиваючись в C #, зазвичай вам потрібна можливість змінити коли-небудь час - особливо з вашою бізнес-моделлю, де ви хочете, щоб потокове обладнання тощо працювало з існуючими рішеннями. Я написав статтю про те, як працювати з змінними І незмінними даними, вирішуючи більшість питань навколо змінності (сподіваюся): rickyhelgesson.wordpress.com/2012/07/17/…
Ricky Helgesson

3
@StephenMartin: Структури, які інкапсулюють одне значення, часто повинні бути незмінними, але структури, безумовно, є найкращим середовищем для інкапсуляції фіксованих наборів незалежних, але споріднених змінних (наприклад, координати X і Y точки), які не мають "тотожності" як групи. Структури, які використовуються для цієї мети, як правило, повинні відкривати свої змінні як публічні поля. Я вважаю, що для таких цілей більш доцільно використовувати клас, ніж структуру, - просто неправильне. Незмінні класи часто менш ефективні, а класи, що змінюються, часто мають жахливу семантику.
supercat

2
@StephenMartin: Розглянемо, наприклад, метод або властивість, яка повинна повертати шість floatкомпонентів графічної трансформації. Якщо такий метод повертає структуру відкритого поля з шістьма компонентами, очевидно, що зміна полів структури не змінить графічний об'єкт, з якого він був отриманий. Якщо такий метод повертає об'єкт класу, що змінюється, можливо, зміна його властивостей змінить основний графічний об'єкт, а може, і не буде - ніхто насправді не знає.
supercat

48

Змінні структури не є злими.

Вони абсолютно необхідні в умовах високої продуктивності. Наприклад, коли лінії кеш-пам'яті та / або збирання сміття стають вузьким місцем.

Я б не назвав використання незмінної структури в цих цілком справедливих випадках використання "злом".

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

Однак, замість того, щоб просто позначати непорушні структури як «злі», я б радив обійняти мову та відстоювати більш корисне та конструктивне правило великих пальців.

Наприклад: "структури - це типи значень, які скопіюються за замовчуванням. Вам потрібна посилання, якщо ви не хочете їх копіювати" або "спробуйте спочатку працювати з структурами для читання" .


8
Я також вважаю, що якщо хочеться скріпити фіксований набір змінних разом із клейкою стрічкою, щоб їх значення можна було обробити або зберігати окремо або як одиницю, має набагато більше сенсу попросити компілятора закріпити фіксований набір змінні разом (тобто оголосити a structз загальнодоступними полями), ніж визначати клас, який можна використовувати незграбно для досягнення тих же цілей або додати купу сміття до структури, щоб змусити наслідувати такий клас (а не мати його поводитись так, як набір змінних, що скріплені з клейкою стрічкою, а саме цього хочеться насамперед)
supercat

41

Структури з загальнодоступними зміненими полями або властивостями не є злими.

Методи структури (на відміну від установників властивостей), які мутують "це", є дещо злими, лише тому, що .net не забезпечує способів відрізнити їх від методів, яких немає. Методи структури, які не мутують "це", повинні бути використані навіть на структурах, які мають лише читання, без необхідності захисного копіювання. Методи, які мутують "це", взагалі не повинні використовуватися в структурах лише для читання. Оскільки .net не хоче забороняти методам структури, які не змінюють "це", не викликати в структурах лише для читання, але не хоче дозволити мутувати структури лише для читання, він захисно копіює структури в " лише контексти, мабуть, отримуючи найгірше з обох світів.

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

struct PointyStruct {public int x, y, z;};
клас PointyClass {public int x, y, z;};

недійсний метод1 (Foot PointyStruct);
недійсний метод2 (ref PointyyStruct foo);
недійсний метод3 (PointyClass foo);

Для кожного методу дайте відповідь на наступні питання:

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

Відповіді:

Запитання 1::
Method1()ні (чіткий намір)
Method2() : так (чіткий намір)
Method3() : так (невизначений намір)
Питання 2
Method1():: ні
Method2(): ні (якщо не небезпечно)
Method3() : так

Метод1 не може змінювати foo і ніколи не отримує посилання. Метод2 отримує короткочасне посилання на foo, який може використовувати модифікувати поля foo будь-яку кількість разів у будь-якому порядку, поки він не повернеться, але він не може зберегти це посилання. Перед тим, як Method2 повернеться, якщо він не використовує небезпечний код, будь-яка та всі копії, які могли бути зроблені з його "foo", зникнуть. Method3, на відміну від Method2, отримує незрозумілу посилання на foo, і немає жодної інформації про те, що це може зробити з цим. Він може взагалі не змінювати foo, він може змінювати foo, а потім повертатися, або він може давати посилання на foo на інший потік, який може мутувати його якось довільним чином у якийсь довільний майбутній час.

Масиви структур пропонують чудову семантику. З огляду на RectArray [500] типу Rectangle, зрозуміло і очевидно, як, наприклад, скопіювати елемент 123 в елемент 456, а потім через деякий час встановити ширину елемента 123 на 555, не порушуючи елемент 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123]. Ширина = 555; ". Знаючи, що Прямокутник - це структура з цілим полем під назвою Ширина, скаже все, що потрібно знати про вищезазначені твердження.

Тепер припустимо, що RectClass був класом з тими ж полями, що і Rectangle, і один хотів виконувати ті самі операції на RectClassArray [500] типу RectClass. Можливо, масив повинен містити 500 попередньо ініціалізованих незмінних посилань на змінні об'єкти RectClass. у цьому випадку правильний код буде чимось на зразок "RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;". Можливо, у масиві передбачається наявність примірників, які не збираються змінювати, тому власний код буде більше схожий на "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = Новий RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555; " Щоб знати, що потрібно робити, треба було б знати набагато більше і про RectClass (наприклад, чи підтримує він конструктор копій, метод копіювання з тощо. ) та призначене використання масиву. Ніде не так чисто, як використання структури.

Для переконання, на жаль, немає жодного приємного способу для будь-якого класу контейнерів, крім масиву, запропонувати чисту семантику структури масиву. Найкраще, що можна зробити, якби хотілося, щоб колекція індексувалася, наприклад, рядком, ймовірно, було б запропонувати загальний метод "ActOnItem", який би прийняв рядок для індексу, загального параметра та делегата, який буде переданий шляхом посилання як на загальний параметр, так і на предмет колекції. Це дозволило б отримати майже ту саму семантику, що і масиви stru, але якщо тільки vb.net та C # люди не можуть переслідувати запропонувати хороший синтаксис, код буде чітко виглядає, навіть якщо він має досить ефективні дії (передаючи загальний параметр дозволяють використовувати статичний делегат і уникають необхідності створювати будь-які екземпляри тимчасового класу).

Особисто я підглядаю ненависть до Еріка Ліпперта та ін. spew щодо змінних типів значень. Вони пропонують набагато більш чисту семантику, ніж розбещені типи посилань, які використовуються всюди. Незважаючи на деякі обмеження з підтримкою .net для типів значень, є багато випадків, коли типи змінних значень краще підходять, ніж будь-який інший тип сутності.


1
@ Ron Warholic: не очевидно, що SomeRect - це прямокутник. Це може бути якийсь інший тип, який можна неявно набрати від прямокутника. Хоча єдиний визначений системою тип, який можна неявно набрати з прямокутника, - це RectangleF, і компілятор би збився, якби спробувати передати поля RectangleF конструктору Rectangle (оскільки перші є Single, а другі Integer) , можуть бути визначені користувачем структури, які дозволяють мати такі неявні типи передач. До речі, перше твердження буде однаково добре працювати, чи був RealRectangle чи RectangleF.
supercat

Все, що ви показали, - це те, що у надуманому прикладі ви вважаєте, що один метод зрозуміліший. Якщо ми візьмемо ваш приклад, Rectangleя міг би легко створити загальну ситуацію, де ви маєте вкрай незрозумілу поведінку. Подумайте, що WinForms реалізує Rectangleтип, що змінюється, що використовується у Boundsвластивості форми . Якщо я хочу змінити межі, я б хотів використовувати ваш симпатичний синтаксис: form.Bounds.X = 10;Однак це абсолютно нічого не змінює у формі (і створює прекрасну помилку, повідомляючи вас про таке). Невідповідність є основою програмування, і тому потрібна незмінність.
Рон Уорхолік

3
@Ron Warholic: BTW, я б хотів сказати "form.Bounds.X = 10;" і нехай це просто працює, але система не забезпечує жодного чистого способу цього. Конвенція щодо викриття властивостей типу значень як методів, що приймають зворотні дзвінки, може запропонувати набагато більш чистий, ефективний та підтверджувально правильний код, ніж будь-який підхід із використанням класів.
supercat

4
Ця відповідь настільки проникливіша, ніж декілька відповідей, які беруть голоси. Це як би абсурдно, що аргумент проти змінних типів значень спирається на поняття "те, що ви очікуєте", що відбудеться, коли ви змішуєте псевдонім та мутацію. Це страшна річ , щоб зробити , так чи інакше !
Еймон Нербонна

2
@supercat: Хто знає, можливо, ця функція зворотного повернення, про яку вони говорять для C # 7, може покрити цю базу (я насправді не детально розглядав це, але це поверхово звучить схоже).
Еймон Нербонна

24

Ціннісні типи в основному представляють незмінні поняття. Fx, немає сенсу мати таке математичне значення, як ціле число, вектор тощо, а потім мати змогу змінювати його. Це було б як перевизначення значення цінності. Замість зміни типу значення має більше сенсу присвоїти інше унікальне значення. Подумайте про те, що типи значень порівнюються, порівнюючи всі значення його властивостей. Справа в тому, що якщо властивості однакові, то це те саме універсальне подання цієї величини.

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

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


Ну, принаймні , вони повинні представляти незмінні поняття
;-p

3
Ну, я вважаю, що вони могли зробити System.Drawing.Point непорушним, але це була б серйозна помилка дизайну IMHO. Я думаю, що точки фактично є типом архетипічного значення, і вони є змінними. І вони не створюють жодних проблем нікому, крім дійсно раннього програмування 101 початківців.
Стівен Мартін

3
В принципі, я думаю, що пункти також повинні бути незмінні, але якщо це робить тип складніше або менш елегантним у використанні, то, звичайно, це також слід враховувати. Немає сенсу створювати кодові конструкції, які підтримують найкращі принципи, якщо ніхто не хоче їх використовувати;)
Morten Christiansen

3
Типи значень корисні для представлення простих незмінних понять, але структури відкритого поля - найкращі типи, які можна використовувати для утримування або проходження навколо невеликих фіксованих наборів пов’язаних, але незалежних значень (таких як координати точки). Місце зберігання такого типу значень інкапсулює значення його полів та нічого іншого. На відміну від цього, місце зберігання змінного еталонного типу може використовуватися для утримання стану об'єкта, що змінюється, але також інкапсулює ідентичність всіх інших посилань у всьому Всесвіті, які існують до цього самого об'єкта.
supercat

4
"Типи значень в основному представляють незмінні поняття". Ні, вони ні. Одне з найдавніших і найкорисніших застосунків змінної типу типу - це intітератор, який був би абсолютно марним, якби він був незмінним. Думаю, ви поєднуєте компілятор / типи значень типових значень / виконання під час виконання з типом "змінних, набраних до типу значень" - остання, безумовно, може змінюватися на будь-яке з можливих значень.
Сліпп Д. Томпсон,

19

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

Типи змінних значень та поля, які читаються тільки

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this() 
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass 
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass 
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

Типи та масив змінних значень

Припустимо, у нас є масив нашої Mutableструктури, і ми викликаємо IncrementIметод для першого елемента цього масиву. Якої поведінки ви очікуєте від цього дзвінка? Чи слід змінювати значення масиву або лише копію?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

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


5
Що повинно відбутися, якщо б .net мови мали трохи кращу підтримку типу значень, методам структури було б заборонено мутувати "це", якщо вони прямо не оголошені так, а методи, які так оголошуються, повинні бути заборонені лише для читання контексти. Масиви змінних структур пропонують корисну семантику, яку неможливо досягти за допомогою інших засобів.
supercat

2
це хороші приклади дуже тонкої проблеми, яка виникла б із змінних структур. Я б не очікував жодної такої поведінки. Чому масив дасть вам посилання, а інтерфейс дає вам значення? Я б подумав, окрім цінностей, які постійно існували (на що я дійсно очікував), що це, принаймні, навпаки: інтерфейс, що дає посилання; масиви, що дають значення ...
Дейв Кузен

@Sahuagin: На жаль, не існує стандартного механізму, за допомогою якого інтерфейс може відкривати посилання. Існують способи .net може дозволити такі речі робити безпечно та корисно (наприклад, визначивши спеціальну структуру "ArrayRef <T>", що містить a T[]і цілий індекс, і забезпечивши, що доступ до властивості типу ArrayRef<T>буде інтерпретуватися як доступ до відповідного елемента масиву) [якщо клас хотів викрити показник ArrayRef<T>для будь-якої іншої мети, він може надати метод - на відміну від властивості - отримати його]. На жаль, таких положень не існує.
supercat

2
О мій ... це робить змінні структури проклятого зла!
nawfal

1
Мені подобається ця відповідь, оскільки вона містить дуже цінну інформацію, яка не є очевидною. Але насправді це не аргумент проти змінних структур, як дехто стверджує. Так, те, що ми бачимо тут, є "ямою відчаю", як сказав би Ерік, але джерелом цього відчаю не є незмінність. Джерелом відчаю є методи самозаймання структури . (Що стосується того, чому масиви та списки поводяться по-різному, це тому, що один - це в основному оператор, який обчислює адресу пам'яті, а інший - властивість. Загалом, все стає зрозумілим, як тільки ти зрозумієш, що "посилання" - це адресне значення .)
AnorZaken

18

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

Отже, змінні структури не є злими, C # зробив їх злими. Я весь час використовую змінні структури в C ++, і вони дуже зручні та інтуїтивно зрозумілі. На відміну від цього, C # змусив мене повністю відмовитися від структур як членів класів через те, як вони обробляють об'єкти. Їх зручність обійшлося нам.


Наявність класових полів типів структури часто може бути дуже корисною схемою, хоча, правда, є деякі обмеження. Продуктивність буде знижена, якщо користуватися властивостями, а не полями, або використовувати readonly, але якщо уникнути виконання цих речей, поля класів типів структури є просто чудовими. Єдине дійсно фундаментальне обмеження структур полягає в тому, що структове поле змінного типу класу, як-от, int[]може інкапсулювати ідентичність або незмінний набір значень, але не може бути використане для інкапсуляції змінних значень без також інкапсуляції небажаної ідентичності.
supercat

13

Якщо ви будете дотримуватися того, для чого призначені структури (у C #, Visual Basic 6, Pascal / Delphi, C ++ тип структури (або класи), коли вони не використовуються в якості покажчиків), ви виявите, що структура не є більше, ніж складова змінна. . Це означає: ви будете трактувати їх як запакований набір змінних під загальним іменем (змінною запису, на яку ви посилаєтесь членів).

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

Так: структури включають багато пам'яті, але це не буде точно більше пам'яті, виконуючи:

point.x = point.x + 1

у порівнянні з:

point = Point(point.x + 1, point.y)

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

Але, нарешті, структури - це структури , а не об’єкти. У POO головна властивість об’єкта - це їх ідентичність , яка в більшості випадків не більше його адреси пам'яті. Struct означає структуру даних (не належний об'єкт, і тому вони так і не мають ідентичності), і дані можна змінювати. В інших мовах запис (замість структури , як це стосується Паскаля) - це слово і має ту саму мету: просто змінна запис даних, призначена для читання з файлів, модифікація та скидання у файли (що є основним використання, і багатьма мовами ви навіть можете визначити вирівнювання даних у записі, хоча це не обов'язково стосується правильно названих об'єктів).

Хочете гарного прикладу? Структури використовуються для легкого читання файлів. У Python є ця бібліотека, оскільки, оскільки вона орієнтована на об'єкти і не підтримує структури, їй довелося реалізувати її іншим способом, який є дещо потворним. Мови, що реалізують структури, мають таку особливість ... вбудована. Спробуйте прочитати заголовок растрової карти з відповідною структурою на таких мовах, як Pascal або C. Це буде легко (якщо структура будується правильно і вирівнюється; в Pascal ви б не використовували доступ на основі запису, а функції для читання довільних бінарних даних). Отже, для файлів та прямого (локального) доступу до пам'яті структури краще, ніж об’єкти. Що стосується сьогодні, ми звикли до JSON та XML, і тому ми забуваємо про використання бінарних файлів (і як побічний ефект - використання структур). Але так: вони існують і мають мету.

Вони не злі. Просто використовуйте їх з правильною метою.

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


12

Уявіть, що у вас є масив з 1 000 000 струків. Кожна структура, що представляє власний капітал з такими речами, як bid_price, offer_price (можливо, десяткові дроби) тощо, це створюється C # / VB.

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

Уявіть, що код C # / VB прослуховує ринковий канал зміни цін, тому, можливо, коду доведеться отримати доступ до якогось елемента масиву (для забезпечення безпеки), а потім змінити деякі цінові поля.

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

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

Гюго


3
Щоправда, але в цьому випадку причина використання структури полягає в тому, що це робиться накладенням на дизайн програми за допомогою зовнішніх обмежень (тих, які використовуються нативним кодом). Все інше, що ви описуєте про ці об'єкти, говорить про те, що вони повинні бути чітко класами в C # або VB.NET.
Джон Ханна

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

9

Коли щось можна мутувати, воно набуває почуття ідентичності.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Оскільки Personце мінливо, більш природно думати про зміну позиції Еріка, ніж клонування Еріка, переміщення клону та знищення оригіналу . Обидві операції могли б змінити вміст eric.position, але одна більш інтуїтивна, ніж інша. Крім того, інтуїтивніше передавати Еріку навколо (як орієнтир) методи його модифікації. Надання методу клону Еріка майже завжди буде дивовижним. Кожен, хто хоче мутувати, Personповинен пам’ятати, щоб попросити посилання на те, Personабо вони будуть робити неправильно.

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

  • непорушний
  • еталонні типи
  • безпечно пройти за значенням

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

Інтуїтивність незмінного Personзалежить від того, що ви намагаєтесь зробити. Якщо Personпросто представлений набір даних про людину, в цьому немає нічого неінтуїтивного; Personзмінні справді представляють абстрактні значення , а не об'єкти. (У такому випадку, ймовірно, буде доцільніше перейменувати його PersonData.) ЯкщоPerson насправді моделює сама людина, ідея постійно створювати та переміщувати клонів є дурною, навіть якщо ви уникали гніту думки, що ви змінюєте Оригінальний. У цьому випадку, мабуть, більш природно було б просто зробити Personтип посилання (тобто клас.)

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


3
Ваша думка про особистість - хороша; можливо, варто відзначити, що особистість є актуальною лише тоді, коли існує чимало посилань на щось. Якщо fooутримується єдине посилання на його ціль у будь-якій точці Всесвіту, і ніщо не захопило значення хеша ідентичності цього об'єкта, то поле, що мутує, foo.Xє семантично еквівалентним вказуванню fooна новий об'єкт, подібний до того, про який він раніше згадувався, але з Xутримуючи потрібне значення. З типами класів, як правило, важко зрозуміти, чи існує кілька посилань на щось, але зі структурами це легко: вони не мають.
supercat

1
Якщо Thingтип мутаційного класу, що змінюється, заголовок Thing[] буде інкапсулювати ідентифікатори об'єктів - хочуть вони цього чи ні - якщо тільки не можна гарантувати, що жодне Thingв масиві, на який існують будь-які зовнішні посилання, ніколи не буде змінено. Якщо хтось не хоче, щоб елементи масиву інкапсулювали ідентичність, потрібно, як правило, переконатися, що жодні елементи, на які він містить посилання, ніколи не будуть мутовані, або що жодні зовнішні посилання ніколи не існуватимуть до будь-яких предметів, які він містить [гібридні підходи також можуть працювати ]. Жоден підхід не є надзвичайно зручним. Якщо Thingце структура, Thing[]інкапсулює лише значення.
supercat

Для об’єктів їхня особа походить від їх місцезнаходження. Екземпляри посилальних типів мають свою ідентичність завдяки їх розташуванню в пам'яті, і ви передаєте лише їх ідентичність (посилання), а не їх дані, тоді як типи значень мають свою ідентичність у зовнішньому місці, де вони зберігаються. Ідентифікатор вашого типу значень Еріка походить лише від змінної, де він зберігається. Якщо ти передаєш його навколо, він втратить свою ідентичність.
IllidanS4 хоче, щоб Моніка повернулася

6

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


3
Це вірно, якщо ви також використовуєте клас як ключ на карті.
Марк Гравелл

6

Особисто, коли я дивлюсь на код, мені здається досить незграбним:

data.value.set (data.value.get () + 1);

а не просто

data.value ++; або data.value = data.value + 1;

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

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

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


1
Прямо до точки! Структури - це організаційні підрозділи без обмежень контролю доступу! На жаль, C # зробив їх марними для цієї мети!
ThunderGr

це повністю пропускає суть, оскільки обидва ваші приклади показують змінні структури.
vidstige

C # зробив їх марними для цієї мети, оскільки це не мета структур
Луїз Феліпе

6

У прикладі містера Еріка Ліпперта є кілька питань. Надумано проілюструвати те, що структури копіюються, і як це може бути проблемою, якщо ви не обережні. Дивлячись на приклад, я бачу це як погану звичку програмування, а насправді не є проблемою ні для структури, ні для класу.

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

  2. Якщо у вас клас закриває структуру, ви б викликали метод у класі мутувати структуру члена. Це те, що я б робив як гарну звичку програмування.

Правильна реалізація полягала б у наступному.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

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

Результат змін вуаля веде себе як очікувалося:

1 2 3 Для продовження натисніть будь-яку клавішу. . .


4
Немає нічого поганого в тому, щоб сконструювати невеликі непрозорі структури, що поводяться як об'єкти незмінного класу; рекомендації MSDN є розумними, коли людина намагається зробити щось, що поводиться як об'єкт . Структури доцільні в деяких випадках, коли потрібні легкі речі, які ведуть себе як предмети, а також у випадках, коли потрібна купа змінних, скріплених із клейкою стрічкою. Однак чомусь багато людей не усвідомлюють, що структури мають дві чіткі звички, і що керівництво, відповідне для однієї, є невідповідним для іншого.
supercat

5

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

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

Мистецтво перекладача детально описує ці компроміси та наводить приклади.


структури не залишаються в псевдонімі c #. Кожне призначення структури - це копія.
рекурсивний

@recursive: У деяких випадках це головна перевага змінних структур, і те, що змушує мене ставити під сумнів поняття, що структури не повинні змінюватися. Той факт, що компілятори іноді неявно копіюють структури, не зменшує корисність змінних структур.
supercat

1

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

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

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


Як я бачу, в основі лежить структура - це струпка змінних, скріплених із клейкою стрічкою. Можливо, що в .NET структура може претендувати на щось інше, ніж купу змінних, скріплених з клейкою стрічкою, і я б припустив, що при практичному застосуванні тип, який буде робити вигляд, що є чимось іншим, ніж купа змінних, що тримаються разом з клейкою стрічкою повинен вести себе як об'єднаний об'єкт (що для структури означало б незмінність), але іноді корисно приклеювати купу змінних разом із клейкою стрічкою. Навіть у виробничому коді я вважаю, що краще мати тип ...
supercat

... що, очевидно, не має семантики за межами "кожне поле містить останнє написане до нього", підштовхуючи всю семантику до коду, який використовує структуру, ніж намагатися, щоб структура робила більше. Враховуючи, наприклад, Range<T>тип із членами Minimumта Maximumполями типу Tта кодом Range<double> myRange = foo.getRange();, будь-які гарантії щодо того, що Minimumі що Maximumпотрібно містити foo.GetRange();. Будучи Rangeструктурою відкритого поля, було б зрозуміло, що вона не збирається додавати жодної власної поведінки.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.