可变状态带来的问题
1 |
|
计算某个电影的平均值,看似没有问题,但是由于 scores 容器没有大小限制,会出现如下两个问题:
- 当你在求平均值的时候,可能有新的数据添加,它可能被计入总和,也可能没有被计入总和
- 你不知道
scores.size()
是不是会先求值,导致 后面新添加的元素没有被视为 size 大小的一部分
那我们可以添加一个成员变量 scores_size 来记录链表中到底有多少数据(要加锁),到时候遍历的时候按照这个数量去遍历和求平均数。但是作者认为我们可能会忘记更新 scores_size 大小,那这属于代码逻辑层面的问题了,不意味着我前面这个想法有问题。
作者的意思是如果 scores 和 scores_size 变量都不可变,即声明为 const,还会遇到这些问题吗?
- 第一个问题在于计算得分的平均值时,有人可能改变得分列表。如果列表是不可改变的,那么在使用它时,就不会有人改变它,这个问题就不复存在
- 第二个问题在于如果这些变量都是不可变的,那么就需要在创建 movie_t 类时初始化它们。变量 scores_size 可能初始化为一个错误的值,但这种错误应该比较明显。如果某人忘记了更新它的值–那一定是计算代码出了问题。另外,设置了错误的值以后,它就一直是错的,但这并不难调试出来
我完全没有想到作者居然要把 scores 和 scores_size 变量都不可变,这样做的意义何在?难道我的这个 movie_t 类不能添加元素?作者究竟在想什么?那就视为作者只是想表达可变状态存在的问题吧。
纯函数和引用透明
所有这些问题都源自一个设计缺陷:软件系统中多个组件负责相同的数据,而不知道另外的组件何时更改数据。修改这些问题最简单的办法就是禁止修改任何数据,所有问题都会迎刃而解。但事实是不可能没有数据被修改。
现在通过称为引用透明的概念,定义更加纯洁的函数。引用透明是表达式的一个特征,而不仅仅是函数。表达式是定义了一种计算并返回结果的任何东西。如果用表达式的结果替换整个表达式,而程序不会出现不同的行为,那么就说这个表达式是引用透明的。如果表达式是引用透明的,那它就没有任何可见的副作用,因此表达式中的所有函数都是纯函数。
max 是 引用透明的吗?如果用 max 返回值替换 max 调用,发现程序的行为没有变化,即计算出结果依旧是 6。
尽管依旧有些不同,因为 max 里面会通过 cerr 输出计算的结果,但是我们可以宽松引用透明的概念。如果非要严苛一点,下面的这个就完全符合了。
无副作用编程
在纯函数式编程中,不是去改变一个值,而是创建一个新的(值)。如果要改变对象的一个属性,就创建这个对象的副本,只不过属性的值要改变为新值。如果这样设计软件,就不会出现前面例子中的问题–当处理电影得分时,其他人改变了它。这时没人能够改变电影得分的列表,只能创建一个新的列表,插入新值。
大型系统有许多可改变的部分。需要创建一个巨大的、包罗万象的结构,并在每次需要改变时,重新创建它。这将产生巨大的性能开销(即使使用针对函数式编程优化的数据结构,这将在第8章介绍),并大幅增加软件的复杂性。
通常会有一些可变状态,如果经常复制和传递,将影响程序的效率。可以将函数设计为返回如何改变的语句,而不是返回改变后状态的复制。这种方式带来的是系统可变部分与 pure 部分进行了明确分离。
1 |
|
代码解释:
- State 类:包含两个可变的成员变量
value1
和value2
。apply
函数接受一个std::function<void(State&)>
类型的参数,表示对状态的某种操作。 - 纯函数
incrementValue1
和incrementValue2
:这些函数不直接改变状态,而是返回一个 lambda 表达式,该表达式描述了如何修改状态。这些函数是纯函数,因为它们不改变任何外部状态,只生成描述状态变化的操作。 - 应用状态变化:在
main
函数中,我们创建了一个State
对象,并通过apply
函数来应用状态变化。apply
函数调用传入的 lambda 表达式,从而修改State
对象的状态。
并发环境中的可变状态和不可变状态
并发问题只有在并行的进程间共享可变数据才会出现。因此,一种解决方案就是不使用并行,另一种方案就是不使用可变数据。但还有第三种选择:使用可变数据,但不共享它。如果不共享数据,则不会发生在不知情的情况下数据被修改的事情。
const 的重要性
如果希望某个对象不被修改,可以使用 const 关键字。
逻辑 const 与内部 const
如果不希望修改内部类成员的数据,可以把成员变量加上 const ,甚至不需要创建访问函数,只需要加上 public 即可。
1 |
|
这种方法有个缺陷:有些编译器的优化功能将停止工作。一旦声明成员常量,就会丢失移动构造函数和移动赋值操作符。
1 |
|
虽然这种方式中,数据成员没有声明为 const,但类的用户却不能修改它们,因为在实现的所有成员函数中,this 都指向一个 Person 的 const 实例。这不但提供了逻辑上的 const(不能修改对象中的用户可见数据),还实现了内部const(不会修改对象的内部数据)。同时,还不会丢失任何优化机会,因为编译器会产生任何必需的移动操作。
如果需要修改,但又想保证数据的不可变性(至少对外部调用者是这样的),那就用并发编程相关的技术来做了。
对于临时值优化成员函数
如果类设计为不可变的,则创建 setter 函数时,需要创建一个返回对象副本的函数,以保存特定变量修改后的值。创建对象副本保存修改后的对象是十分低效的(尽管编译器在某些场合下可以对此进行优化)。对于原对象不再需要的情况,这一点也是适用的。
现在声明了两个 with_name 的重载函数。正如 const 修饰符一样,给成员函数指定引用类型修饰符(&)只影响this 指针的类型。第二个重载函数只适用于借用数据的对象的调用–临时对象或其他右值引用。
当第一个重载函数被调用时,创建一个新的 persont 对象的副本,并由 this 指向它,设置新的 name,并返回新创建的对象实例。在第二个重载函数中,创建一个新的 persont 对象实例,并把 this 所指对象的数据移动(不是复制)到新对象实例中。
注意第二个重载函数没有声明为 const。如果是,就不能把当前对象中的数据移动到新的对象中,就需要像第一个重载函数一样复制数据。
const缺陷
- const 禁止对象移动
- const 可以被破坏
1 |
|
const_cast 可以移除 const 性质,实际上还有很多方式可以移除 const 性质,就不逐一列举了,只是想说有 const 关键字并不一定始终跟随定义的对象。