那么,什么时候一个类变得太复杂而无法改变呢?
在我看来,使小类在您所显示的语言中不可变是不值得的。我在这里使用的是小字,而不是复杂的字,因为即使您向该类添加了十个字段,并且确实在它们上进行了花哨的操作,我还是怀疑它会占用千字节,更不用说兆字节,更不用说千兆字节了,所以任何使用您实例的函数如果希望避免引起外部副作用,则class可以简单地对整个对象进行廉价复制,以避免修改原始对象。
持久数据结构
我发现个人用于不变性的地方是大型的中央数据结构,这些数据结构聚集了一堆小数据,例如您正在显示的类的实例,例如存储了一百万的类NamedThings
。通过属于一个不变的持久数据结构并位于仅允许只读访问的接口后面,属于该容器的元素变得不可变,而无需元素类(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);
}
但是,以这两条额外的代码行为交换,此函数现在可以安全地在具有相同原始列表的线程之间进行调用,不会引起任何副作用,等等。这也使得将操作变为不可撤消的用户操作非常容易,因为undo可以只存储旧列表的廉价浅表副本。
异常安全或错误恢复
在这种情况下,并不是每个人都能从持久数据结构中受益匪浅(我在VFX域中的核心概念undo系统和非破坏性编辑中发现了对它们的大量使用),但是有一点适用于每个人都要考虑的是异常安全或错误恢复。
如果要使原始变异函数具有异常安全性,则需要回滚逻辑,为此,最简单的实现需要复制整个列表:
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的实体和组件以及的顶点/边/多边形)。网格)。