Почему структуры и классы являются отдельными понятиями в C #?

44

При программировании на C # я наткнулся на странное решение о дизайне языка, которое просто не могу понять.

Таким образом, C # (и CLR) имеют два совокупных типа данных: struct(тип значения, хранящийся в стеке, без наследования) и class(тип ссылки, хранящийся в куче, имеет наследование).

Вначале эта настройка звучит неплохо, но затем вы наткнулись на метод, принимающий параметр агрегатного типа, и чтобы выяснить, действительно ли он имеет тип значения или ссылочный тип, вы должны найти объявление его типа. Это может стать действительно запутанным время от времени.

Похоже, что общепринятым решением проблемы является объявление всех structs как «неизменных» (установка их полей в readonly), чтобы предотвратить возможные ошибки, ограничивая structполезность s.

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

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

Mints97
источник
34
«Общепринятое решение проблемы, по-видимому, объявляет все структуры« неизменяемыми »... ограничивая полезность структур» Многие люди утверждают, что создание чего-либо неизменного обычно делает его более полезным, когда это не является причиной узкого места производительности. Кроме того, structs не всегда хранятся в стеке; Рассмотрим объект с structполем. Кроме того, как отметил Мейсон Уилер, проблема с нарезкой, вероятно, является главной причиной.
Довал
7
Это не правда, что C # был вдохновлен C ++; Скорее, C # был вдохновлен всеми ошибками (хорошо продуманными и хорошо звучащими в то время) в дизайне C ++ и Java.
Питер Гиркенс
18
Примечание: стек и куча - это детали реализации. Ничто не говорит о том, что экземпляры структуры должны размещаться в стеке, а экземпляры классов должны размещаться в куче. И это на самом деле даже не правда. Например, вполне возможно, что компилятор может определить с помощью Escape Analysis, что переменная не может выйти за пределы локальной области видимости и, таким образом, разместить ее в стеке, даже если это экземпляр класса. Это даже не говорит о том, что должен быть стек или куча вообще. Вы можете выделить кадры среды в виде связанного списка в куче и даже не иметь стека вообще.
Йорг Миттаг
3
"но затем вы наткнулись на метод, принимающий параметр агрегатного типа, и чтобы выяснить, действительно ли он имеет тип значения или ссылочный тип, вы должны найти объявление его типа" Умм, почему это имеет значение, точно ? Метод просит вас передать значение. Вы передаете значение. В какой момент вам нужно заботиться о том, является ли это ссылочным типом или типом значения?
Луаан

Ответы:

58

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

Вы правильно определили соответствующие пункты выше: structтип значения, без наследования. class: ссылочный тип, имеет наследование. Типы наследования и значения (или, более конкретно, полиморфизм и передача по значению) не смешиваются; если вы передаете объект типа Derivedаргументу метода типа Base, а затем вызываете для него виртуальный метод, единственный способ получить правильное поведение - убедиться, что полученное значение является ссылкой.

Между этим и всеми другими беспорядками, с которыми вы сталкиваетесь в C ++, имея наследуемые объекты в качестве типов значений ( приходят на ум конструкторы копирования и нарезка объектов !), Лучшим решением является Just Say No.

Хороший языковой дизайн - это не только реализация функций, но и знание того, какие функции не следует реализовывать, и один из лучших способов сделать это - учиться на ошибках тех, кто был до вас.

Мейсон Уилер
источник
29
Это просто еще одна бессмысленная субъективная рутина на C ++. Не могу понизить голос, но был бы, если бы мог.
Бартек Банахевич
17
@MasonWheeler: « это ужасный беспорядок » звучит достаточно субъективно. Это уже обсуждалось в длинной ветке комментариев к другому вашему ответу ; нить была обстреляна (к сожалению, потому что она содержала полезные комментарии, хотя в огненном соусе войны). Я не думаю, что здесь стоит повторять все это, но «C # понял это правильно, а C ++ понял это неправильно» (что, по-видимому, является сообщением, которое вы пытаетесь передать) - действительно субъективное утверждение.
Энди Проул
15
@MasonWheeler: Я сделал это в ветке, которая получила ядерное оружие, и несколько других людей - поэтому я думаю, что это неудачно, что он был удален. Я не думаю, что это хорошая идея, чтобы скопировать этот поток здесь, но короткая версия: в C ++ пользователь типа, а не его конструктор , должен решить, с какой семантикой следует использовать тип (ссылочная семантика или значение семантика). В этом есть свои преимущества и недостатки: вы гневаетесь на минусы, не рассматривая (или не зная?) Плюсы. Вот почему анализ субъективен.
Энди Проул
7
вздох Я считаю, что обсуждение нарушения LSP уже состоялось. И я вроде как полагаю, что большинство людей согласились с тем, что упоминание LSP довольно странно и не связано, но не может проверить, потому что мод взорвал ветку комментариев .
Бартек Банахевич
9
Если вы переместите свой последний абзац в верх и удалите текущий первый абзац, я думаю, у вас есть идеальный аргумент. Но текущий первый абзац просто субъективен.
Мартин Йорк,
19

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

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

Это отражает основное различие в философии: C ++ пытается предоставить вам все инструменты, которые вам могут понадобиться, по существу, для любого дизайна, который вам может понадобиться. Он практически не пытается контролировать использование этих инструментов, поэтому их легко использовать для создания проектов, которые хорошо работают только в редких ситуациях, а также проектов, которые, вероятно, являются просто паршивой идеей, и никто не знает о ситуации, в которой они, скорее всего, будут хорошо работать. В частности, многое из этого достигается путем разделения дизайнерских решений - даже тех, которые на практике действительно почти всегда связаны. В результате между написанием C ++ и написанием C ++ существует огромная разница. Чтобы хорошо писать на C ++, вам нужно знать много идиом и практических правил (включая практические правила о том, как серьезно пересмотреть, прежде чем нарушать другие эмпирические правила). В результате, C ++ больше ориентирован на простоту использования (экспертами), чем на простоту обучения. Есть также (слишком много) обстоятельства, при которых это не очень просто использовать.

C # делает гораздо больше, чтобы попытаться (или, по крайней мере, чрезвычайно убедительно) предложить то, что разработчики языка считают хорошими практиками проектирования. Многие вещи, которые в C ++ отделены (но обычно на практике встречаются вместе), напрямую связаны в C #. Это позволяет «небезопасному» коду немного расширять границы, но, честно говоря, не очень.

В результате, с одной стороны, существует довольно много дизайнов, которые можно выразить довольно прямо в C ++, которые существенно неуклюжи для выражения в C #. С другой стороны, это целое намного легче выучить C #, и шансы получения действительно ужасный дизайн , который не будет работать для вашей ситуации (или , возможно , любой другой) являются резко снижается. Во многих (возможно, даже в большинстве) случаях вы можете получить надежный, работоспособный дизайн, просто, так сказать, «плывя по течению». Или, поскольку один из моих друзей (по крайней мере, мне нравится думать о нем как о друге - не уверен, действительно ли он согласен) любит это выражать, C # позволяет легко попасть в пропасть успеха.

Итак, более подробно рассмотрев вопрос о том, как classи structкак они появились на двух языках: объектах, созданных в иерархии наследования, где вы можете использовать объект производного класса под видом его базового класса / интерфейса, вы в значительной степени застряв в том факте, что вам обычно нужно делать это с помощью какого-то указателя или ссылки - на конкретном уровне происходит то, что объект производного класса содержит что-то в памяти, что можно рассматривать как экземпляр базового класса / интерфейс, и производным объектом манипулируют через адрес этой части памяти.

В C ++ программист должен сделать это правильно - когда он использует наследование, он должен убедиться, что (например) функция, которая работает с полиморфными классами в иерархии, делает это с помощью указателя или ссылки на базу учебный класс.

В C # то, что по сути такое же разделение между типами, гораздо более явное и обеспечивается самим языком. Программисту не нужно предпринимать никаких шагов для передачи экземпляра класса по ссылке, потому что это произойдет по умолчанию.

Джерри Гроб
источник
2
Как фанат C ++, я думаю, что это отличная сводка различий между C # и бензопилой Swiss Army.
Дэвид Торнли
1
@DavidThornley: я, по крайней мере, попытался написать то, что, по моему мнению, было бы несколько сбалансированным сравнением. Не для того, чтобы показывать пальцем, но кое-что из того, что я увидел, когда писал это, показалось мне ... несколько неточным (мягко говоря).
Джерри Коффин
7

Это из "C #: зачем нам нужен другой язык?" - Ганнерсон, Эрик:

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

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

[...]

Типы значений , перегрузка операторов и определяемые пользователем преобразования - все это усложняет язык , но позволяет значительно упростить важный пользовательский сценарий.

Ссылочная семантика для объектов - это способ избежать многих неприятностей (конечно, и не только нарезки объектов), но проблемы реального мира могут иногда требовать объектов со смысловой семантикой (например, взглянуть на звуки, как будто я никогда не должен использовать ссылочную семантику, верно? для другой точки зрения).

Следовательно, какой лучший подход, чтобы отделить эти грязные, уродливые и плохие объекты со значением-семантикой под тегом struct?

Manlio
источник
1
Я не знаю, может быть, не используя эту грязную, уродливую и плохую семантику объектов со ссылками?
Бартек Банахевич
Может быть ... Я потерял дело.
Манлио
2
ИМХО, одним из самых больших недостатков в дизайне Java является отсутствие каких-либо средств для объявления того, используется ли переменная для инкапсуляции идентификатора или владения , а одним из самых больших недостатков в C # является отсутствие средств для различения операций над переменная из операций над объектом, на который переменная содержит ссылку. Даже если среда выполнения не заботилась о таких различиях, возможность указать на языке, int[]должна ли переменная типа быть разделяемой или изменяемой (массивы могут быть и другими, но обычно не обоими), поможет неправильному коду выглядеть неправильно.
суперкат
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}», somePoint);

переводится как: boxed_Point box1 = новый boxed_Point (somePoint); Console.WriteLine («Значение равно {0}», box1);

На практике, поскольку типы мест хранения и типы экземпляров кучи существуют в отдельных юниверсах, нет необходимости называть типы экземпляров кучи такими, как boxed_Int32; поскольку система будет знать, какие контексты требуют экземпляра объекта кучи, а какие - места хранения.

Некоторые люди думают, что любые типы значений, которые не ведут себя как объекты, следует считать «злыми». Я придерживаюсь противоположной точки зрения: поскольку места хранения типов значений не являются ни объектами, ни ссылками на объекты, ожидание того, что они должны вести себя как объекты, следует считать бесполезным. В тех случаях, когда структура может вести себя как объект, нет ничего плохого в том, чтобы один из них делал это, но в основе каждого structлежит не что иное, как совокупность открытых и закрытых полей, склеенных клейкой лентой.

Supercat
источник