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

302

MSDN говорит, что вы должны использовать структуры, когда вам нужны легкие объекты. Существуют ли другие сценарии, когда структура предпочтительнее класса?

Некоторые люди могли забыть, что:

  1. Структуры могут иметь методы.
  2. структуры не могут быть унаследованы.

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

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

Ответы:

295

У MSDN есть ответ: Выбор между классами и структурами .

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

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

  • Логически представляет одно значение, похожее на примитивные типы (целое, двойное и т. Д.).
  • Размер экземпляра меньше 16 байт.
  • Это неизменно.
  • Это не должно быть упаковано часто.
OwenP
источник
1
Может быть, я упускаю что-то очевидное, но я не совсем понимаю причины «неизменной» части. Почему это необходимо? Может кто-нибудь объяснить это?
Тамас Чинеге
3
Вероятно, они рекомендовали это, потому что если структура неизменна, то не имеет значения, что она имеет семантику значений, а не ссылочную семантику. Различие имеет значение только в том случае, если вы изменили объект / структуру после создания копии.
Стивен С. Сталь
@DrJokepu: В некоторых ситуациях система создает временную копию структуры, а затем разрешает эту копию передавать по ссылке на код, который ее изменяет; поскольку временная копия будет отброшена, изменения будут потеряны. Эта проблема особенно серьезна, если структура имеет методы, которые изменяют ее. Я категорически не согласен с тем, что изменчивость - это причина сделать что-то классом, поскольку - несмотря на некоторые недостатки в c # и vb.net, изменяемые структуры предоставляют полезную семантику, которую невозможно достичь другим способом; нет семантической причины предпочитать неизменную структуру классу.
суперкат
4
@Chuu: При разработке JIT-компилятора Microsoft решила оптимизировать код для копирования структур размером 16 байт или меньше; это означает, что копирование 17-байтовой структуры может быть значительно медленнее, чем копирование 16-байтовой структуры. Я не вижу особой причины ожидать от Microsoft распространения таких оптимизаций на более крупные структуры, но важно отметить, что хотя копирование 17-байтовых структур может выполняться медленнее, чем 16-байтовых структур, во многих случаях большие структуры могут быть более эффективными, чем большие объекты класса, и где относительное преимущество структур растет с размером структуры.
суперкат
3
@Chuu: применение тех же шаблонов использования к большим структурам, что и к классам, может привести к неэффективному коду, но правильное решение часто состоит не в том, чтобы заменить структуры классами, а в том, чтобы использовать структуры более эффективно; особенно следует избегать прохождения или возврата структур по значению. Передайте их как refпараметры всякий раз, когда это целесообразно. Передача структуры с 4000 полями в качестве параметра ref методу, который меняет его, будет дешевле, чем передача структуры с 4 полями по значению методу, который возвращает измененную версию.
суперкат
53

Я удивлен, что я не читал ни в одном из предыдущих ответов, что я считаю наиболее важным аспектом:

Я использую структуры, когда хочу тип без идентификатора. Например, 3D-точка:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Если у вас есть два экземпляра этой структуры, вам все равно, являются ли они одним фрагментом данных в памяти или двумя. Вы просто заботитесь о ценности, которую они имеют.

Андрей Ринея
источник
4
Немного не по теме, но почему вы бросаете ArgumentException, когда объект не является ThreeDimensionalPoint? Разве вы не должны просто вернуть false в этом случае?
Свиш
4
Это верно, я был слишком нетерпеливым. return falseэто то, что должно было быть там, исправляя сейчас.
Андрей Ринея
Интересная причина использовать структуру. Я сделал классы с GetHashCode и Equals, определенными так, как показано здесь, но затем я всегда должен был быть осторожен, чтобы не изменять эти экземпляры, если я использовал их в качестве словарных ключей. Вероятно, было бы безопаснее, если бы я определил их как структуры. (Потому что тогда ключ будет копией полей в тот момент, когда структура стала ключом словаря , поэтому ключ останется неизменным, если позже я изменю оригинал.)
ToolmakerSteve
В вашем примере это нормально, потому что у вас есть только 12 байтов, но имейте в виду, что если в этой структуре много полей, превышающих 16 байтов, вы должны рассмотреть возможность использования класса и переопределения методов GetHashCode и Equals.
Даниэль Ботеро Корреа
Тип значения в DDD не означает, что вы обязательно должны использовать тип значения в C #
Дарра
26

У Билла Вагнера есть глава об этом в его книге «Эффективный с #» ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). В заключение он использует следующий принцип:

  1. Является ли основная ответственность за тип хранения данных?
  2. Его открытый интерфейс полностью определяется свойствами, которые обращаются к его элементам данных или изменяют их?
  3. Вы уверены, что у вашего типа никогда не будет подклассов?
  4. Вы уверены, что ваш тип никогда не будет лечиться полиморфно?

Если вы ответите «да» на все 4 вопроса: используйте структуру. В противном случае используйте класс.

Барт Гийссенс
источник
1
так что ... объекты передачи данных (DTO) должны быть структурированы?
крейсер
1
Я бы сказал, да, если он соответствует 4 критериям, описанным выше. Почему объект передачи данных должен обрабатываться определенным образом?
Барт Гийссенс
2
@cruizer зависит от вашей ситуации. В одном проекте мы имели общие поля аудита в наших DTO и, следовательно, написали базовый DTO, от которого унаследовали другие.
Эндрю Грот
1
Все, кроме (2) кажутся отличными принципами. Нужно было бы увидеть его рассуждения, чтобы понять, что именно он подразумевает под (2) и почему.
ToolmakerSteve
1
@ToolmakerSteve: Вам придется прочитать книгу для этого. Не думайте, что честно копировать / вставлять большие части книги.
Барт Гийссенс
12

Я бы использовал структуры, когда:

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

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

Павел Пабич
источник
10

Используйте класс, если:

  • Его личность важна. Структуры неявно копируются при передаче по значению в метод.
  • Это будет иметь большой объем памяти.
  • Его поля нуждаются в инициализаторах.
  • Вам нужно наследовать от базового класса.
  • Вам нужно полиморфное поведение;

Используйте структуру, если:

  • Он будет действовать как примитивный тип (int, long, byte и т. Д.).
  • Он должен иметь небольшой объем памяти.
  • Вы вызываете метод P / Invoke, который требует, чтобы структура передавалась по значению.
  • Вам необходимо уменьшить влияние сборки мусора на производительность приложений.
  • Его поля должны быть инициализированы только к их значениям по умолчанию. Это значение будет нулевым для числовых типов, ложным для логических типов и нулевым для ссылочных типов.
    • Обратите внимание, что в C # 6.0 структуры могут иметь конструктор по умолчанию, который можно использовать для инициализации полей структуры значениями не по умолчанию.
  • Вам не нужно наследовать от базового класса (кроме ValueType, от которого наследуются все структуры).
  • Вам не нужно полиморфное поведение.
Яшвант Чоудары Ката
источник
5

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

Райан Скарин
источник
4

Если сущность будет неизменной, вопрос о том, использовать ли структуру или класс, обычно будет связан с производительностью, а не с семантикой. В 32/64-битной системе для ссылок на классы требуется 4/8 байтов для хранения, независимо от объема информации в классе; копирование ссылки на класс потребует копирования 4/8 байтов. С другой стороны, каждый отдельныйЭкземпляр класса будет иметь 8/16 байтов служебной информации в дополнение к информации, которую он содержит, и стоимости памяти для ссылок на него. Предположим, кто-то хочет массив из 500 сущностей, каждая из которых содержит четыре 32-битных целых числа. Если объект является структурным типом, массиву потребуется 8000 байтов независимо от того, все ли 500 объектов идентичны, различны или где-то между ними. Если сущность является типом класса, массив из 500 ссылок займет 4000 байтов. Если все эти ссылки указывают на разные объекты, объектам потребуются дополнительные 24 байта каждый (12 000 байтов на все 500), всего 16 000 байтов - вдвое больше, чем стоимость хранения типа структуры. С другой стороны, из кода, который создал один экземпляр объекта, а затем скопировал ссылку на все 500 слотов массива, общая стоимость составила бы 24 байта для этого экземпляра и 4, 000 для массива - всего 4024 байта. Основная экономия. Немногие ситуации сработают так же хорошо, как и последняя, ​​но в некоторых случаях может оказаться возможным скопировать некоторые ссылки на достаточное количество слотов массива, чтобы сделать такое совместное использование полезным.

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

  Вещь t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Желает ли последнее утверждение повлиять на t1.x?

Если Thing является типом класса, t1 и t2 будут эквивалентны, то есть t1.x и t2.x также будут эквивалентны. Таким образом, второе утверждение повлияет на t1.x. Если Thing является структурным типом, t1 и t2 будут разными экземплярами, то есть t1.x и t2.x будут ссылаться на разные целые числа. Таким образом, второе утверждение не повлияет на t1.x.

Изменчивые структуры и изменяемые классы имеют принципиально различное поведение, хотя .net имеет некоторые особенности в обработке структурных мутаций. Если кто-то хочет поведение типа значения (имеется в виду, что «t2 = t1» будет копировать данные из t1 в t2, оставляя t1 и t2 как отдельные экземпляры), и если можно жить со странностями в обработке типов значений в .net, используйте структура. Если кто-то хочет семантику типа значения, но причуды .net могут привести к нарушению семантики типа значения в приложении, используйте класс и бормотайте.

Supercat
источник
3

Кроме того, превосходные ответы выше:

Структуры являются типами значений.

Они никогда не могут быть установлены в Ничто .

Установка структуры = Ничего, установит для всех типов значений значения по умолчанию.

Джордж Филиппакос
источник
2

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

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

Джим девиль
источник
Почему ты это сказал? Структуры могут иметь методы.
Эстебан Арайя
2

Как сказал @Simon, структуры предоставляют семантику «тип значения», поэтому, если вам нужно поведение, подобное встроенному типу данных, используйте struct. Поскольку структуры передаются посредством копирования, вы должны убедиться, что они имеют небольшой размер, около 16 байт.

Скотт Дорман
источник
2

Это старая тема, но мы хотели предоставить простой тест производительности.

Я создал два файла .cs:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

и

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Выполнить тест:

  • Создать 1 TestClass
  • Создать 1 TestStruct
  • Создать 100 TestClass
  • Создать 100 TestStruct
  • Создать 10000 TestClass
  • Создать 10000 TestStruct

Полученные результаты:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |
Эдуардас Шлутас
источник
1

Хм ...

Я бы не использовал сборку мусора в качестве аргумента за / против использования структур против классов. Управляемая куча работает во многом как стек - создание объекта просто помещает его на вершину кучи, что почти так же быстро, как и размещение в стеке. Кроме того, если объект недолговечен и не выдерживает цикл GC, освобождение происходит бесплатно, поскольку GC работает только с памятью, которая все еще доступна. (Поищите в MSDN, есть серия статей по управлению памятью .NET, мне просто лень копаться в них).

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

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


источник
1
Если вам иногда требуется ссылочная семантика со структурой, просто объявите class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, и тогда a MutableHolder<T>будет объектом с изменяемой семантикой класса (это работает так же хорошо, если Tэто структура или тип неизменяемого класса).
суперкат
1

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

разъем
источник
-3

Я думаю, что лучший ответ - просто использовать struct, когда вам нужен набор свойств, класс, когда это набор свойств и поведений.

Люциан Габриэль Попеску
источник
структуры также могут иметь методы
Нитин Чандран
конечно, но если вам нужны методы с вероятностью 99%, вы неправильно используете struct вместо class. Единственные исключения, которые я обнаружил, когда в структуре вполне нормально использовать методы, - это обратные вызовы
Люциан Габриэль Попеску