Итак, в какой момент класс становится слишком сложным, чтобы быть неизменным?
На мой взгляд, не стоит беспокоиться о том, чтобы сделать небольшие классы неизменяемыми в языках, подобных тому, который вы показываете. Я использую маленький здесь и не сложный , потому что даже если вы добавите десять полей к этому классу, и он действительно над ними работает, я сомневаюсь, что это займет килобайты, не говоря уже о мегабайтах, не говоря уже о гигабайтах, так что любая функция, использующая экземпляры вашего Класс может просто сделать дешевую копию всего объекта, чтобы избежать изменения оригинала, если он хочет избежать внешних побочных эффектов.
Постоянные структуры данных
Где я нахожу личное использование для неизменности, так это для больших центральных структур данных, которые объединяют кучу маленьких данных, таких как экземпляры класса, который вы показываете, например, тот, который хранит миллион NamedThings. Принадлежность к постоянной структуре данных, которая является неизменной, и находящаяся за интерфейсом, который разрешает только доступ только для чтения, элементы, которые принадлежат контейнеру, становятся неизменяемыми, и элементу class ( NamedThing) не приходится иметь дело с ним.
Дешевые копии
Постоянная структура данных позволяет преобразовывать ее области и делать их уникальными, избегая модификаций в оригинале без необходимости полностью копировать структуру данных. Это настоящая красота этого. Если вы хотите наивно писать функции, которые избегают побочных эффектов, которые вводят структуру данных, которая занимает гигабайты памяти и изменяет только объем памяти в мегабайтах, то вам нужно было бы скопировать всю чертову вещь, чтобы не касаться ввода, и вернуть новый выход. Это либо копирование гигабайт, чтобы избежать побочных эффектов, либо побочные эффекты в этом сценарии, поэтому вам придется выбирать между двумя неприятными вариантами.
Благодаря постоянной структуре данных это позволяет вам написать такую функцию и избежать копирования всей структуры данных, требуя только около мегабайта дополнительной памяти для вывода, если ваша функция только преобразовала объем памяти в мегабайтах.
обременять
Что касается бремени, то, по крайней мере, в моем случае, есть непосредственное. Мне нужны те конструкторы, о которых говорят люди, или «переходные процессы», как я их называю, чтобы они могли эффективно выражать преобразования в эту массивную структуру данных, не касаясь ее. Код как это:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... тогда должно быть написано так:
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
Но в обмен на эти две дополнительные строки кода функцию теперь можно безопасно вызывать через потоки с одним и тем же исходным списком, она не вызывает побочных эффектов и т. Д. Кроме того, это действительно позволяет легко сделать эту операцию отменяемым действием пользователя, поскольку отменить можно просто хранить дешевую мелкую копию старого списка.
Исключение-Безопасность или Восстановление после ошибок
Не все могут получить такую же выгоду, как я, от постоянных структур данных в подобных контекстах (я нашел их столь полезными в системах отмены и неразрушающем редактировании, которые являются центральными понятиями в моей области VFX), но одна вещь применима только к каждый должен учитывать исключительную безопасность или исправление ошибок .
Если вы хотите сделать исходную функцию мутации исключительной, то для нее требуется логика отката, для которой простейшая реализация требует копирования всего списка:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
На этом этапе изменяемая на исключение изменяемая версия еще более затратна в вычислительном отношении и, возможно, даже сложнее написать правильно, чем неизменяемая версия, использующая «конструктор». И многие разработчики на C ++ просто пренебрегают безопасностью исключений и, возможно, это хорошо для их домена, но в моем случае я хотел бы убедиться, что мой код работает правильно даже в случае исключения (даже при написании тестов, которые намеренно генерируют исключения для проверки исключений). безопасность), и это делает меня таким, чтобы я был в состоянии откатить любые побочные эффекты, которые функция вызывает на полпути в функцию, если что-то выбрасывает.
Если вы хотите быть безопасными для исключений и корректно восстанавливаться после ошибок без сбоев и прожогов вашего приложения, тогда вам нужно отменить / отменить любые побочные эффекты, которые может вызвать функция в случае ошибки / исключения. И там конструктор может фактически сэкономить больше времени программиста, чем это стоит вместе с вычислительным временем, потому что: ...
Вам не нужно беспокоиться об откате побочных эффектов в функции, которая не вызывает их!
Итак, вернемся к основному вопросу:
В какой момент неизменные классы становятся бременем?
Они всегда обременительны для языков, которые вращаются вокруг изменчивости, а не неизменности, поэтому я думаю, что вы должны использовать их там, где выгоды значительно перевешивают затраты. Но на достаточно широком уровне для достаточно больших структур данных, я верю, что во многих случаях это достойный компромисс.
Кроме того, у меня есть только несколько неизменных типов данных, и все они представляют собой огромные структуры данных, предназначенные для хранения огромного количества элементов (пикселей изображения / текстуры, объектов и компонентов ECS, а также вершин / ребер / многоугольников). сетка).