Когда использовать структуру? - PullRequest
1307 голосов
/ 06 февраля 2009

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

Я сталкивался с этими правилами здесь :

  • Структура должна представлять один значение.
  • Структура должна иметь память занимаемая площадь менее 16 байт.
  • Структура не должна изменяться после создание.

Эти правила работают? Что означает семантически структура?

Ответы [ 28 ]

579 голосов
/ 07 августа 2011

Источник, на который ссылается OP, заслуживает доверия ... но как насчет Microsoft - какова позиция по использованию структуры? Я искал дополнительное обучение от Microsoft , и вот что я нашел:

Рассмотрим определение структуры вместо класса, если экземпляры типа являются маленькими и обычно недолговечными или обычно встраиваются в другие объекты.

Не определяйте структуру, если тип не имеет всех следующих характеристик:

  1. Логически представляет одно значение, аналогичное примитивным типам (целое, двойное и т. Д.).
  2. Размер экземпляра меньше 16 байт.
  3. Он неизменен.
  4. Это не должно быть часто в штучной упаковке.

Microsoft последовательно нарушает эти правила

Хорошо, № 2 и № 3 в любом случае. Наш любимый словарь имеет 2 внутренних структуры:

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //<Tkey, TValue>
{
    //  View code at *Reference Source
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

* Справочный источник

Источник 'JonnyCantCode.com' получил 3 из 4 - вполне простительно, так как # 4, вероятно, не будет проблемой. Если вы обнаружите, что занимаетесь структурой, переосмыслите свою архитектуру.

Давайте посмотрим, почему Microsoft будет использовать эти структуры:

  1. Каждая структура, Entry и Enumerator, представляет отдельные значения.
  2. Скорость
  3. Entry никогда не передается как параметр вне класса Dictionary. Дальнейшие исследования показывают, что для удовлетворения реализации IEnumerable словарь использует структуру Enumerator, которую он копирует каждый раз при запросе перечислителя ... имеет смысл.
  4. Внутренний для словаря класса. Enumerator является общедоступным, поскольку Dictionary является перечислимым и должен иметь равный доступ к реализации интерфейса IEnumerator, например IEnumerator getter.

Обновление - Кроме того, следует понимать, что когда структура реализует интерфейс - как это делает Enumerator - и приводится к этому реализованному типу, структура становится ссылочным типом и перемещается в кучу. Внутренний по отношению к классу Dictionary, перечислитель является по-прежнему типом значения. Однако, как только метод вызывает GetEnumerator(), возвращается ссылочный тип IEnumerator.

Здесь мы не видим никаких попыток или доказательств необходимости сохранять неизменяемыми структуры или поддерживать размер экземпляра не более 16 байт:

  1. Ничто в вышеприведенных структурах не объявляется readonly - не неизменным
  2. Размер этой структуры может быть больше 16 байт.
  3. Entry имеет неопределенный срок службы (от Add() до Remove(), Clear() или сборки мусора);

И ... 4. Обе структуры хранят TKey и TValue, которые, как мы все знаем, вполне могут быть ссылочными типами (дополнительная информация о бонусе)

Несмотря на хеширование ключей, словари работают быстро, потому что создание структуры происходит быстрее, чем ссылочный тип. Здесь у меня есть Dictionary<int, int>, в котором хранится 300 000 случайных целых чисел с последовательно увеличенными ключами.

Вместимость: 312874
MemSize: 2660827 байт
Завершено Изменение размера: 5 мс
Общее время заполнения: 889мс

Capacity : количество элементов, доступных до изменения размера внутреннего массива.

MemSize : определяется путем сериализации словаря в MemoryStream и получения длины в байтах (достаточно точной для наших целей).

Completed Resize : время, необходимое для изменения размера внутреннего массива с 150862 элементов до 312874 элементов. Когда вы понимаете, что каждый элемент последовательно копируется через Array.CopyTo(), это не так уж и плохо.

Общее время для заполнения : по общему признанию, искажено из-за регистрации и события OnResize, которое я добавил к источнику; Тем не менее, по-прежнему впечатляет заполнить 300 тысяч целых чисел при изменении размера в 15 раз во время операции. Просто из любопытства, сколько будет общего времени, чтобы заполнить, если бы я уже знал емкость? 13мс

Итак, что если Entry был классом? Будут ли эти времена или показатели действительно сильно отличаться?

Емкость: 312874
MemSize: 2660827 байт
Завершено Изменение размера: 26 мс
Общее время заполнения: 964мс

Очевидно, большая разница в изменении размера. Есть ли разница, если словарь инициализируется с Capacity? Недостаточно, чтобы беспокоиться о ... 12 мс .

Что происходит, потому что Entry является структурой, она не требует инициализации, как ссылочный тип. Это и красота, и проклятие типа значения. Чтобы использовать Entry в качестве ссылочного типа, мне пришлось вставить следующий код:

/*
 *  Added to satisfy initialization of entry elements --
 *  this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i < prime ; i++)
{
    destinationArray[i] = new Entry( );
}
/*  *********************************************** */  

Причину, по которой я должен был инициализировать каждый элемент массива Entry в качестве ссылочного типа, можно найти по адресу MSDN: Структура структуры . Короче говоря:

Не предоставлять конструктор по умолчанию для структуры.

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

Некоторые компиляторы, такие как компилятор C #, не позволяют структурам есть конструкторы по умолчанию.

Это на самом деле довольно просто, и мы заимствуем у Трима Роботизма Азимова :

  1. Структура должна быть безопасной для использования
  2. Структура должна эффективно выполнять свою функцию, если только это не нарушит правило # 1
  3. Структура должна оставаться неизменной во время ее использования, если только ее уничтожение не требуется для удовлетворения правила # 1

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

153 голосов
/ 06 февраля 2009

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

142 голосов
/ 28 февраля 2009

Я не согласен с правилами, приведенными в оригинальном посте. Вот мои правила:

1) Вы используете структуры для производительности при хранении в массивах. (см. также Когда составляется ответ? )

2) Они нужны вам при передаче кода структурированных данных в / из C / C ++

3) Не используйте структуры, если они вам не нужны:

  • Они ведут себя не так, как «обычные объекты» ( ссылочные типы ) при назначении и при передаче в качестве аргументов, которые могут привести к неожиданному поведению; это особенно опасно, если человек, смотрящий на код не знаю, что они имеют дело со структурой.
  • Они не могут наследоваться.
  • Передача структур в качестве аргументов дороже, чем классов.
83 голосов
/ 06 февраля 2009

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

Редактировать

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

Если вам нужна ссылочная семантика, вам нужен класс, а не структура.

58 голосов
/ 22 октября 2011

В дополнение к ответу "это значение" существует один конкретный сценарий использования структур, когда вы знаете , что у вас есть набор данных, который вызывает проблемы со сборкой мусора и у вас есть много объектов. Например, большой список / массив экземпляров Person. Естественной метафорой здесь является класс, но если у вас есть большое количество долгоживущих экземпляров Person, они могут в итоге засорить GEN-2 и вызвать остановку GC. Если сценарий этого требует, один из возможных подходов заключается в использовании массива (а не списка) Person structs , т.е. Person[]. Теперь, вместо миллионов объектов в GEN-2, у вас есть отдельный кусок в LOH (я предполагаю, что здесь нет строк и т. Д., То есть чистое значение без каких-либо ссылок). Это имеет очень небольшое влияние ГХ.

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

int index = ...
int id = peopleArray[index].Id;

Обратите внимание, что поддержание неизменности самих значений поможет вам. Для более сложной логики используйте метод с параметром by-ref:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

Опять же, это на месте - мы не скопировали значение.

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

39 голосов
/ 17 сентября 2012

Из спецификации языка C # :

1,7 Структуры

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

Структуры особенно полезны для небольших структур данных, которые имеют семантика значения. Комплексные числа, точки в системе координат или Все пары ключ-значение в словаре являются хорошими примерами структур. использование структур, а не классов для небольших структур данных может сделать большая разница в количестве выделенных памяти приложения выполняет. Например, следующая программа создает и инициализирует массив из 100 точек. С Точкой, реализованной как класс, 101 создаются отдельные объекты - по одному для массива и по одному для каждого 100 элементов.

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

Альтернатива - сделать Point структурой.

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

Теперь создается только один объект - один для массива - и экземпляры Point хранятся в виде строки в массиве.

Конструкторы Struct вызываются оператором new, но это не означает, что память выделяется. Вместо того, чтобы динамически размещать объект и возвращать ссылку на него, конструктор структуры просто возвращает само значение структуры (обычно во временном месте в стеке), и это значение затем копируется по мере необходимости.

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

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

Если Point - это класс, выходное значение равно 20, поскольку a и b ссылаются на один и тот же объект. Если Point является структурой, выходное значение равно 10, поскольку при присваивании a-b создается копия значения, и на эту копию не влияет последующее присваивание a.x.

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

33 голосов
/ 06 февраля 2009

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

25 голосов
/ 22 января 2014

Вот основное правило.

  • Если все поля-члены являются типами значений, создайте struct .

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

Exmaples

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}
18 голосов
/ 06 февраля 2009

Первое: взаимодействие сценариев или когда вам нужно указать расположение памяти

Второе: если данные в любом случае имеют почти такой же размер, что и ссылочный указатель.

17 голосов
/ 06 февраля 2009

Вам необходимо использовать "struct" в ситуациях, когда вы хотите явно указать макет памяти, используя StructLayoutAttribute - обычно для PInvoke.

Редактировать: Комментарий указывает, что вы можете использовать класс или структуру со StructLayoutAttribute, и это, безусловно, верно. На практике вы обычно используете struct - она ​​распределяется в стеке, а не в куче, что имеет смысл, если вы просто передаете аргумент в вызов неуправляемого метода.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...