Чому структури та класи є окремими поняттями в C #?


44

Програмуючи в C #, я натрапив на дивне рішення дизайну мови, яке я просто не можу зрозуміти.

Отже, C # (і CLR) має два сукупні типи даних: struct(тип значення, що зберігається в стеку, без успадкування) і class(тип посилання, зберігається в купі, має спадщину).

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

Загальноприйняте рішення проблеми, схоже, оголошує всі structs "непорушними" (встановлюючи їх поля readonly) для запобігання можливих помилок, обмеження structкорисності s.

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

Питання в тому, чому стали classі structставали окремими поняттями в C # та CLR замість одного типу агрегату з двома варіантами розподілу?


34
"Загальноприйняте рішення проблеми, схоже, оголошує всі структури" непорушними "... обмеження корисності конструкцій" Багато людей стверджують, що зробити що- небудь незмінне в цілому робить його більш корисним, коли це не є причиною вузького місця. Крім того, structs не завжди зберігаються у стеку; розглянути об’єкт із structполем. Це вбік, як Мейсон Уілер згадував проблему нарізки, мабуть, найбільшу причину.
Довал

7
Неправда, що C # надихнувся C ++; скоріше, C # був натхненний усіма (надуманими та гарними звучаннями на той час) помилками в дизайні як C ++, так і Java.
Пітер Геркенс

18
Примітка: стек та купа - це деталі реалізації. Ніщо не говорить про те, що екземпляри структури повинні бути розподілені на стек, а екземпляри класів мають бути розподілені на купі. І це насправді навіть не так. Наприклад, цілком можливо, що компілятор може визначити за допомогою програми Escape Analysis, що змінна не може вийти з локальної області і таким чином виділити її на стеці, навіть якщо це екземпляр класу. Це навіть не говорить про те, що взагалі має бути стек чи купа. Ви можете виділити оточуючі кадри як пов'язаний список на купі і навіть не мати стека взагалі.
Йорг W Міттаг


3
"але потім ви натрапляєте на метод, що приймає параметр сукупного типу, і щоб зрозуміти, чи він насправді типу значення або еталонного типу, ви повинні знайти декларацію його типу" Umm, чому це має значення саме так ? Метод просить передати значення. Ви передаєте значення. У який момент вам потрібно подбати про те, чи це тип посилання чи тип значення?
Луань

Відповіді:


58

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

Ви правильно визначили відповідні пункти вище: structтип значення, відсутність успадкування. class: тип довідки, має спадщину. Спадкові та ціннісні типи (а точніше, поліморфізм та прохідні значення) не змішуються; якщо ви передаєте об’єкт типу Derivedаргументу методу типу Base, а потім викликаєте на ньому віртуальний метод, єдиний спосіб отримати належну поведінку - переконатися, що те, що було передано, було посиланням.

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

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


29
Це лише черговий безглуздий суб'єктивний злодій на C ++. Не можу підкреслити, але якби я міг.
Bartek Banachewicz

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

15
@MasonWheeler: Я робив тему, яка отримала нук, і так само зробили кілька інших людей - саме тому я вважаю, що прикро, що її видалили. Я не думаю , що це гарна ідея , щоб повторити цю нитку тут, але коротка версія: в C ++ з користувачем типу, а не його дизайнер , вирішує , з якою семантикою слід використовувати тип (посилання семантикою або значенням семантика). Це має і переваги, і недоліки: ви лютуєтесь проти мінусів, не враховуючи (чи знаючи?) Плюси. Ось чому аналіз є суб’єктивним.
Енді Проул

7
зітхаю Я вважаю, що дискусія про порушення ЛСП вже відбулася. І я якось вважаю, що більшість людей погодилися з тим, що згадка про LSP є досить дивною і не пов'язаною, але не можу перевірити, тому що мод нанизав нитку коментарів .
Bartek Banachewicz

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

19

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

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

Це відображає основну різницю у філософії: C ++ намагається надати вам всі інструменти, які можуть знадобитися для будь-якого дизайну, який ви хочете. Це майже не робить спроби контролювати, як ви використовуєте ці інструменти, тому також легко їх використовувати для створення конструкцій, які добре працюють лише в рідкісних ситуаціях, а також конструкцій, які, ймовірно, просто хитра ідея, і ніхто не знає про ситуацію, в якій вони, ймовірно, працюють взагалі добре. Зокрема, велика частина цього робиться шляхом розв’язування дизайнерських рішень - навіть тих, що на практиці майже завжди поєднуються. Як результат, існує величезна різниця між тим, як просто писати C ++ і добре писати C ++. Щоб добре писати C ++, ви повинні знати безліч ідіом і правил великого пальця (включаючи правила великого пальця про те, як серйозно переглянути, перш ніж порушувати інші великі правила). Як результат, C ++ орієнтована набагато більше на простоту використання (експертами), ніж на простоту навчання. Існують також (занадто багато) обставини, в яких також не дуже просто користуватися.

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

Результат полягає в тому, що, з одного боку, існує досить багато конструкцій, які можна виразити досить безпосередньо в C ++, які по суті є незграбними для вираження в C #. З іншого боку, це ціле набагато легше вивчити C #, і шанси отримання дійсно жахливий дизайн , який не працюватиме для вашої ситуації (або , можливо , будь-який інший) є різко знижується. У багатьох (напевно, навіть у більшості) випадків ви можете отримати міцний, працездатний дизайн, просто «ідучи з потоком», так би мовити. Або, як хтось із моїх друзів (принаймні, я люблю вважати його товаришем - не впевнений, чи дійсно він згоден) любить це ставити, C # дозволяє легко потрапити в яму успіху.

Отже, дивлячись конкретніше на питання про те, як classі structяк вони знаходяться на двох мовах: об’єкти, створені в ієрархії спадкування, де ви можете використовувати об'єкт похідного класу під виглядом його базового класу / інтерфейсу, ви майже застряг з тим, що вам зазвичай потрібно це робити через якийсь вказівник чи посилання - на конкретному рівні відбувається те, що об'єкт похідного класу містить щось пам'ять, що може розглядатися як екземпляр базового класу / інтерфейс, а похідним об'єктом управляється через адресу цієї частини пам'яті.

У програмі C ++ програміст повинен це зробити правильно - коли він використовує спадщину, він повинен переконатися, що (наприклад) функція, яка працює з поліморфними класами в ієрархії, робить це за допомогою вказівника або посилання на базу клас.

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


2
Як фанат C ++, я вважаю, що це чудовий підсумок відмінностей між C # та бензопилою Швейцарської армії.
Девід Торнлі

1
@DavidThornley: Я хоча б намагався написати те, що, на мою думку, було б дещо збалансованим порівнянням. Не вказувати пальцями, але щось із того, що я бачив, коли писав це, мене вразило як… дещо неточне (якщо сказати красиво).
Джеррі Труну

7

Це з "C #: навіщо нам потрібна інша мова?" - Гуннерсон, Ерік:

Простота була важливою метою дизайну для C #.

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

[...]

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

Довідкова семантика для об’єктів - це спосіб уникнути безлічі неприємностей (звичайно, і не тільки нарізання об’єктів), але проблеми з реальним світом іноді можуть вимагати об'єктів із значенням семантичного значення (наприклад, погляньте на Звуки, як я ніколи не повинен використовувати опорну семантику, правда? для іншої точки зору).

Отже, чим краще підходити, ніж відокремлювати ці брудні, потворні та погані об'єкти зі значенням-семантикою під тегом struct?


1
Я не знаю, можливо, не використовуючи ці брудні, потворні та погані предмети з посиланням-семантикою?
Bartek Banachewicz

Можливо ... Я втрачена справа.
manlio

2
IMHO, один з найбільших недоліків у дизайні Java - відсутність будь-яких засобів для визначення того, чи використовується змінна для інкапсуляції ідентичності чи права власності , а одним із найбільших дефектів C # є відсутність засобу для розрізнення операцій над змінна від операцій над об'єктом, на який змінна містить посилання. Навіть якщо Runtime не піклується про такі розрізнення, можливість вказувати мовою, чи int[]має бути змінна типу змінною або змінною (масиви можуть бути або різними , але, як правило, не обидва), допоможе зробити неправильний код невірним.
supercat

4

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

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

і для твердження типу: Console.WriteLine ("Значення дорівнює {0}", деякийPoint);

перекладається як: boxed_Point box1 = новий boxed_Point (деякийPoint); Console.WriteLine ("Значення {0}", поле1);

На практиці, оскільки типи розташування сховища та типи екземплярів купи існують в окремих всесвітах, не потрібно називати такі речі типу "heap", як подібні boxed_Int32; оскільки система знає, які контексти вимагають екземпляр об'єкта heap та які потрібні для зберігання.

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

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