Должен ли я использовать структуру или класс? - PullRequest
9 голосов
/ 06 октября 2010

Я нахожусь в классической дилемме дизайна. Я пишу структуру данных C # для содержания кортежа значения и единицы измерения (например, 7,0 миллиметра), и мне интересно, должен ли я использовать ссылочный тип или тип значения.

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

Кортеж является частью довольно общей структуры результатов анализа, в которой результаты представлены в приложении WPF по-разному в зависимости от типа значения результата. Этот тип слабой типизации отлично обрабатывается WPF со всеми его шаблонами данных, преобразователями значений и селекторами шаблонов. Подразумевается, что значение подвергнется большому количеству упаковки / распаковки, если мой кортеж будет представлен как структура. На самом деле использование кортежа в выражениях будет второстепенным по сравнению с использованием в сценариях бокса. Чтобы избежать всего бокса, я считаю объявление своего типа классом. Еще одна проблема, связанная со структурой, заключается в том, что в WPF могут быть подводные камни с двусторонним связыванием, поскольку в конечном итоге было бы проще получить копии кортежей где-то в коде, а не ссылочные копии.

У меня также есть несколько удобных перегрузок операторов. Я могу сравнить, скажем, миллиметры с сантиметрами без проблем, используя перегруженные операторы сравнения. Однако мне не нравится идея перегрузки == и! =, Если мой кортеж является классом, так как соглашение таково, что == и! = Это ReferenceEquals для ссылочных типов (в отличие от System.String, которая является еще одним классическим обсуждением). Если == и! = Перегружены, кто-то напишет if (myValue == null) и получит неприятное исключение времени выполнения, когда myValue в один прекрасный день окажется нулевым.

Еще один аспект заключается в том, что в C # нет ясного способа (в отличие, например, в C ++) различать ссылочные типы и типы значений при использовании кода, однако семантика сильно отличается. Я беспокоюсь о том, что пользователи моего кортежа (если объявлена ​​структура) предполагают, что тип является классом, поскольку большинство пользовательских структур данных являются и предполагают ссылочную семантику. Это еще один аргумент, почему следует отдавать предпочтение классам просто потому, что это то, что ожидает пользователь, и нет "." / "->" чтобы отличить их. В общем, я почти всегда использовал бы класс, если мой профилировщик не скажет мне использовать структуру, просто потому, что семантики класса наиболее вероятно ожидаются коллегами-программистами, а у C # есть только смутные намеки, одно это или другое.

Итак, мои вопросы:

Какие еще соображения следует учитывать при принятии решения о том, следует ли мне указывать значение или ссылку?

Может ли == /! = Перегрузка в классе быть оправдана в любых обстоятельствах?

Программисты принимают вещи. Большинство, вероятно, предположит, что то, что называется «Точка», является типом значения. Что бы вы предположили, если бы читали какой-то код с помощью «UnitValue»?

Что бы вы выбрали, учитывая мое описание использования?

Ответы [ 6 ]

9 голосов
/ 06 октября 2010

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

Учитывая без какого-либо контекста, это огромная - и опасная - чрезмерная генерализация. Структура не автоматически подходит для стека. Структура может быть помещена в стек, если (и только если) ее время жизни и экспозиция не выходят за пределы функции, которая ее объявляет, она не упаковывается в эту функцию и, вероятно, содержит множество других критериев, которые этого не делают. прийти на ум сразу. Это означает, что превращение его в лямбда-выражение или делегат означает, что оно все равно будет храниться в куче. Дело не в том, чтобы беспокоиться об этом, потому что есть вероятность 99,9%, что ваши узкие места находятся где-то еще .

Что касается перегрузки операторов, то ничто не мешает вам (ни технически, ни философски) перегрузить операторы вашего типа. Хотя вы технически правы в том, что сравнения на равенство между ссылочными типами по умолчанию семантически эквивалентны object.ReferenceEquals, это не правило «все и все». При перегрузке операторов следует учитывать две основные вещи:

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

Например, если я объявляю тип Foo, который определяет перегруженный оператор равенства, который всегда возвращает true, тогда я делаю это:

Foo foo1 = new Foo();
Foo foo2 = new Foo();
object obj1 = foo1;

bool compare1 = foo1 == foo2; // true
bool compare2 = foo1 == obj1; // false

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

2.) Операции сравнения должны быть детерминированными. Не должно быть возможности сравнивать одни и те же два экземпляра, используя перегруженный оператор, и иметь возможность давать разные результаты. Практически, такого рода требования обычно приводят к тому, что типы являются неизменяемыми (поскольку возможность определить разницу между одним или несколькими значениями в классе, но получить true от оператора равенства довольно нелогично), но по сути это просто означает, что вы не сможете изменить значение состояния в экземпляре, который изменит результат операции сравнения . Если в вашем сценарии имеет смысл изменять некоторые информации о состоянии экземпляра, не влияя на результат сравнения, то нет причин, по которым вы не должны этого делать. Это просто редкий случай.

1 голос
/ 06 октября 2010

Однако мне не нравится идея перегрузки == и! =, Если мой кортеж является классом, поскольку существует соглашение == и! = Is ReferenceEquals для ссылочных типов

Нет, соглашение немного отличается:

(в отличие от System.String, которая является еще одним классическим обсуждением).

Нет, это то же самое обсуждение.

Суть не в том, является ли тип ссылочным типом. - Тип ведет себя как как значение. Это верно для String, и это должно быть верно для любого класса, для которого вы хотите перегрузить operator == и !=.

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

Если == и! = Перегружены, кто-то напишет if (myValue == null) и получит неприятное исключение времени выполнения, когда myValue в один прекрасный день окажется нулевым.

Не должно быть никаких исключений (в конце концов, (string)null == null тоже не дает исключения!), Это будет ошибкой в ​​реализации перегруженного оператора.

0 голосов
/ 06 октября 2010

структура данных для хранения кортежа значения и единицы измерения (например, 7,0 миллиметров)

Звучит так, как будто у него есть семантика значения. Фреймворк предоставляет механизм для создания типов с семантикой значений, а именно struct. Используйте это.

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

Одна вещь, которая не касается оптимизации:

Мне не нравится идея перегрузки == и! =, Если мой кортеж является классом, поскольку существует соглашение == и! = Is ReferenceEquals для ссылочных типов

Не совсем так. default заключается в том, что == и! = Имеют дело с равенством ссылок, но это так же, потому что это единственное значимое значение по умолчанию без дополнительных знаний о семантике класса. == и! = должны быть перегружены, когда это соответствует семантике классов, для этого необходимо использовать ReferenceEquals, когда единственное, что заботится о равенстве ссылок.

Если == и! = Перегружены, кто-то напишет if (myValue == null) и получит неприятное исключение времени выполнения, когда myValue в один прекрасный день окажется нулевым.

Только если перегрузка == имеет ошибку новичка. Нормальный подход был бы:

public static bool operator == (MyType x, MyType y)
{
  if(ReferenceEquals(x, null))
    return ReferenceEquls(y, null);
  if(ReferenceEquals(y, null))
    return false;
  return x.Equals(y);
}

И, конечно, перегрузка Equals должна также проверять, что параметр имеет значение null, и возвращать false, если это так, для людей, вызывающих его напрямую. Это даже не оказывает существенного влияния на производительность по сравнению с поведением по умолчанию ==, когда одно или оба значения равны нулю, так в чем же проблема?

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

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

Может ли перегрузка в классе == /! = Быть оправдана при любых обстоятельствах?

Я бы скорее спросил, нельзя ли перегрузить == и! = Оправданно, когда это разумно сделать для класса?

Что касается того, что я, как программист, мог бы предположить о «UnitValue», я бы, вероятно, предположил, что это структура, так как она звучит так, как и должно быть. Но на самом деле, я бы даже не предположил, что, поскольку мне, в основном, будет все равно, пока я не сделаю что-то с этим, где это важно, учитывая, что это также звучит так, как будто оно должно быть неизменным, это сокращенный набор (семантические различия между изменяемым ссылочные типы и изменяемые структуры более эффективны на практике, но этот не вызывает затруднений).

0 голосов
/ 06 октября 2010

На мой взгляд, ваш дизайн требует тип значения семантика для вашего кортежа. <7.0, мм> всегда должно быть равно <7.0, мм> с точки зрения программиста. <7.0, мм> является в точности суммой его частей и не имеет собственной идентификации. Все остальное я бы нашел очень запутанным. Этот вид также подразумевает неизменность.

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

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

0 голосов
/ 06 октября 2010

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

На самом деле вы могли бы сформулировать свой вопрос другим способом: хотите ли вы, чтобы ваш тип был изменчивым или неизменным?Я думаю, что неизменность была бы логичной с вашими спецификациями.Это значение, вы сказали это сами, назвав его UnitValue.Как разработчик, я был бы довольно удивлен, что UnitValue не является значением;) => Использовать неизменную структуру

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

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

0 голосов
/ 06 октября 2010

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

...