Когда вы используете структуру вместо класса? [закрыто]

174

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

Я склонен использовать классы практически для всего и использовать структуры только тогда, когда что-то очень упрощено и должно иметь тип значения, например PhoneNumber или что-то в этом роде. Но это кажется относительно незначительным использованием, и я надеюсь, что есть более интересные варианты использования.

RationalGeek
источник
4
Возможный дубликат: stackoverflow.com/questions/6267957/struct-vs-class
FrustratedWithFormsDesigner
7
@Frustrated Невозможно пометить вопрос как дубликат чего-либо на другом сайте, хотя это, безусловно, связано.
Адам Лир
@ Анна Лир: я знаю, я проголосовала за переход, а не за дублирование. После переноса его можно правильно закрыть как дубликат .
FrustratedWithFormsDesigner
5
@ Разочарованный Я не думаю, что это нужно перенести.
Адам Лир
15
Этот вопрос кажется гораздо более подходящим для P.SE против SO. Вот почему я спросил это здесь.
RationalGeek

Ответы:

169

Общее правило, которое следует соблюдать, состоит в том, что структуры должны быть небольшими, простыми (одноуровневыми) коллекциями связанных свойств, которые являются неизменяемыми после создания; для всего остального используйте класс.

C # хорош тем, что структуры и классы не имеют явных различий в объявлении, кроме ключевого слова определения; поэтому, если вы чувствуете, что вам необходимо «обновить» структуру до класса или, наоборот, «понизить» класс до структуры, это в основном простой вопрос изменения ключевого слова (есть несколько других ошибок; структуры не могут быть получены из любого другого класса или типа структуры, и они не могут явно определить конструктор по умолчанию без параметров).

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

Например, скажем, у вас есть класс 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, где операции должны учитывать все части значения), затем используйте структуру и спроектируйте ее соответственно. В большинстве других случаев, когда подпункты объекта должны быть независимо изменяемыми, используйте класс.

Keiths
источник
1
Хороший ответ! Я бы сказал, читая один из них, методологию и книгу «Разработка через домен», которая, по-видимому, в некоторой степени связана с вашим мнением о том, почему следует использовать классы или структуры, основанные на том, какую концепцию вы на самом деле пытаетесь реализовать
Максим Поправко,
1
Стоит упомянуть лишь небольшую деталь: вы не можете определить свой собственный конструктор по умолчанию для структуры. Вы должны обходиться с тем, который инициализирует все к его значению по умолчанию. Обычно это довольно незначительно, особенно в классе, который является хорошим кандидатом на роль структуры. Но это будет означать, что изменение класса на структуру может подразумевать нечто большее, чем просто изменение ключевого слова.
Лоран Бурго-Рой
1
@ LaurentBourgault-Roy - правда. Есть и другие ошибки, а также; Например, структуры не могут иметь иерархию наследования. Они могут реализовывать интерфейсы, но не могут быть производными от чего-то другого, кроме System.ValueType (неявно их структурой).
KeithS
Как разработчик JavaScript, я должен задать две вещи. Чем литералы объектов отличаются от типичных форм использования структур и почему важно, чтобы структуры были неизменными?
Эрик Реппен
1
@ErikReppen: В ответ на оба ваших вопроса: когда структура передается в функцию, она передается по значению. С классом вы передаете ссылку на класс (все еще по значению). Эрик Липперт обсуждает, почему это проблема: blogs.msdn.com/b/ericlippert/archive/2008/05/14/… . Последний параграф KeithS также охватывает эти вопросы.
Брайан
14

Ответ: «Использовать структуру для чистых конструкций данных и класс для объектов с операциями» - это определенно неверный IMO. Если структура содержит большое количество свойств, то класс почти всегда более уместен. Microsoft часто говорит, с точки зрения эффективности, если ваш тип больше 16 байт, это должен быть класс.

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

bytedev
источник
1
Абсолютно. Я бы проголосовал, если бы мог. Я не понимаю, откуда пришла идея, что структуры для данных, а объекты - нет. Структура в C # - это просто механизм размещения в стеке. Копии всей структуры передаются вместо простой копии указателя объекта.
mike30
2
Структуры @mike - C # не обязательно размещаются в стеке.
Ли
1
@mike Идея «структуры для данных», вероятно, исходит из привычки, приобретенной многими годами программирования на Си. C не имеет классов и его структуры являются контейнерами только для данных.
Конамиман
Очевидно, что по крайней мере 3 программиста на Си (получили голосование) прочитали ответ Майкла К. ;-)
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. Следует использовать структуру в случаях, когда первая семантика имела бы больше смысла, и следует использовать класс в случаях, когда последняя семантика имела бы больше смысла.

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

Например, рассмотрим утверждения:

  Person 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")
    {
      string newExchange = "";
      if (new312to708Exchanges.TryGetValue (theNumber.Exchange), out newExchange)
      {
        theNumber.AreaCode = "708";
        theNumber.Exchange = newExchange;
        myList [i] = theNumber;
      }
    }
  }

Обратите внимание, что ничто в приведенном выше коде не знает и не заботится о каких-либо полях, PhoneNumberкроме AreaCodeи Exchange. Если бы PhoneNumberэто была так называемая «неизменяемая» структура, было бы необходимо либо предоставить withXXметод для каждого поля, который бы возвращал новый экземпляр структуры, который содержал переданное значение в указанном поле, либо это было бы необходимо чтобы код, подобный приведенному выше, знал о каждом поле в структуре. Не совсем привлекательный.

Кстати, есть как минимум два случая, когда структуры могут разумно содержать ссылки на изменяемые типы.

  1. Семантика структуры указывает на то, что она хранит идентичность рассматриваемого объекта, а не как ярлык для хранения свойств объекта. Например, KeyValuePair будет хранить идентификаторы определенных кнопок, но не ожидается, что они будут содержать постоянную информацию о позициях этих кнопок, состояниях выделения и т. Д.
  2. Структура знает, что она содержит единственную ссылку на этот объект, никто больше не получит ссылку, и любые мутации, которые когда-либо будут выполнены с этим объектом, будут выполнены до того, как ссылка на него будет сохранена где-либо.

В первом сценарии IDENTITY объекта будет неизменным; во-вторых, класс вложенного объекта может не обеспечивать неизменность, но структура, содержащая ссылку, будет.

Supercat
источник
7

Я склонен использовать структуры только тогда, когда нужен один метод, поэтому класс кажется слишком «тяжелым». Таким образом, иногда я отношу структуры к легким объектам. ОСТЕРЕГАЙТЕСЬ: это не всегда работает и не всегда является лучшим решением, так что это действительно зависит от ситуации!

ДОПОЛНИТЕЛЬНЫЕ РЕСУРСЫ

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

rrazd
источник
5
Вопросы на другом сайте (даже если этот сайт переполнен стеком ) не считаются дубликатами. Пожалуйста, расширьте свой ответ, включив в него фактический ответ, а не просто ссылки на другие места. Если ссылки когда-либо изменятся, ваш ответ в том виде, в котором он написан сейчас, будет бесполезным, и мы хотели бы сохранить информацию и сделать программистов максимально автономными. Спасибо!
Адам Лир
2
@ Анна Лир. Спасибо, что сообщили мне, теперь будем помнить об этом!
Разд
2
-1 «Я склонен использовать структуры только тогда, когда нужен один метод, поэтому класс кажется слишком« тяжелым ».» ... тяжелый? что это на самом деле означает? ... и вы действительно говорите, что если есть только один метод, то он, вероятно, должен быть структурой?
bytedev
2

Используйте структуру для чистых конструкций данных и класс для объектов с операциями.

Если ваша структура данных не нуждается в управлении доступом и не имеет специальных операций, кроме get / set, используйте struct. Это делает очевидным, что вся эта структура является контейнером для данных.

Если в вашей структуре есть операции, которые изменяют ее более сложным способом, используйте класс. Класс также может взаимодействовать с другими классами через аргументы методов.

Думайте об этом как о разнице между кирпичом и самолетом. Кирпич - это структура; имеет длину, ширину и высоту. Это не может сделать многое. Самолет может иметь несколько операций, которые каким-то образом изменяют его, например, перемещение поверхностей управления и изменение тяги двигателя. Это было бы лучше как класс.

Майкл К
источник
2
«Использовать структуру для чистых конструкций данных и класс для объектов с операциями» в C # это нонсенс. Смотрите мой ответ ниже.
bytedev
1
«Используйте структуру для чистых конструкций данных и класс для объектов с операциями». Это верно для C / C ++, а не для C #
taktak004