变量如何引入状态?


11

我正在阅读“ C ++编码标准”,并且此行在其中:

变量会引入状态,因此您必须处理尽可能少的状态,并且生存期应尽可能短。

最终变异的所有东西都不会操纵状态吗?什么是你应该有应对尽可能少的状态尽可能的意思吗?

在不纯正的语言(例如C ++)中,状态管理不是您真正在做什么吗?除了限制可变寿命之外,还有什么其他方法可以处理尽可能少的状态

Answers:


16

难道没有任何变数真正地操纵状态吗?

是。

“您应该处理很少的状态”是什么意思?

这意味着少状态胜于多状态。更多的状态往往会引入更多的复杂性。

在像C ++这样的不纯语言中,状态管理不是您真正在做什么吗?

是。

除了限制变量生存期外,还有什么其他方法可以“以很少的状态进行交易”?

最小化变量数量。将操纵某些状态的代码隔离到一个独立的单元中,以便其他代码段可以忽略它。


9

难道没有任何变数真正地操纵状态吗?

是。在C ++中,唯一可变的东西是(非const)变量。

“您应该处理很少的状态”是什么意思?

程序的状态越少,则越容易理解其功能。因此,您不应该引入不需要的状态,并且一旦不再需要它就不要保留它。

在像C ++这样的不纯语言中,状态管理不是您真正在做什么吗?

在像C ++这样的多范式语言中,通常可以在“纯”功能或状态驱动的方法之间进行选择,或在某种混合形式之间进行选择。从历史上看,与某些语言相比,对函数式编程的语言支持一直很薄弱,但是正在改善。

除了限制变量生存期外,还有什么其他方法可以“以很少的状态进行交易”?

限制范围和寿命,以减少对象之间的耦合;偏爱局部变量而不是全局变量,并且偏爱私有对象而不是公共对象成员。


5

状态表示某些内容存储在某处,以便您以后可以参考。

创建一个变量,为您创建一些空间来存储一些数据。此数据是程序的状态

您可以用它来做事,修改,计算等等。

这是状态,而您所做的事情不是状态。

在函数式语言中,您通常只处理函数并将函数作为对象传递给周围。尽管这些函数没有状态,并且将函数传递给周围,却不引入任何状态(可能在函数本身内部)。

在C ++中,您可以创建函数对象,它们structclass类型已operator()()超载。这些功能对象可以具有局部状态,尽管不一定要在程序中的其他代码之间共享局部状态。函子(即函数对象)很容易传递。这几乎可以模仿C ++中的功能范例。(据我所知)

几乎没有状态或没有状态意味着您可以轻松地优化程序以并行执行,因为线程或CPU之间无法共享任何内容,因此无法创建争用,也无需防止数据争用等。


2

其他人为前三个问题提供了很好的答案。

除了限制可变寿命之外,还有什么其他方法可以“以尽可能少的状态进行交易”?

问题1的关键答案是肯定的,任何变异最终都会影响状态。关键是不要改变事物。不可变类型,使用一种函数式编程风格,其中一个函数的结果直接传递给另一个函数而不存储,直接传递消息或事件而不是存储状态,计算值而不是存储和更新它们...

否则,您将只能限制状态的影响。通过可见性或生命周期。


1

“您应该处理很少的状态”是什么意思?

这意味着您的类应尽可能小,以最佳方式表示单个抽象。如果在类中放置10个变量,则很可能是您做错了什么,应该了解如何重构您的类。


1

要了解程序的工作方式,必须了解其状态变化。您所拥有的状态越少,对于使用该状态的代码的本地性就越高,这将越容易。

如果您曾经使用过具有大量全局变量的程序,那么您将隐式理解。


1

状态只是存储的数据。每个变量实际上都是某种状态,但是我们通常使用“状态”来指代在操作之间保持不变的数据。作为一个简单,毫无意义的示例,您可能有一个内部存储an int和has increment()decrement()member函数的类。在这里,内部值是状态,因为它在此类实例的生存期内一直存在。换句话说,该值是对象的状态。

理想情况下,类定义的状态应尽可能小,并且冗余最少。这有助于您的班级满足单一责任原则,改善封装并降低复杂性。一个对象的状态应该由该对象的接口完全封装。这意味着,根据对象的语义,对该对象进行任何操作的结果都是可以预测的。您可以通过最小化可访问状态的函数的数量来进一步改善封装。

这是避免出现全球状态的主要原因之一。全局状态可能会在没有接口表示它的情况下为对象引入依赖关系,从而使该状态对对象的用户隐藏。对具有全局依赖性的对象调用操作可能会产生变化且不可预测的结果。


1

最终变异的所有东西都不会操纵状态吗?

是的,但是如果它在小类的成员函数后面,而该小类是整个系统中唯一可以操纵其私有状态的实体,则该状态的范围非常狭窄。

您应该处理尽可能少的状态是什么意思?

从变量的角度来看:应尽可能少地访问几行代码。将变量的范围缩小到最小。

从代码角度来看:应该从该代码行访问尽可能少的变量。缩小代码行可能访问的变量数量(是否访问它并不重要,重要的是是否可以访问)。

全局变量之所以糟糕,是因为它们具有最大范围。即使从代码库的两行代码(从代码的POV行)访问它们,也始终可以访问全局变量。从变量的POV中,具有整个外部链接的全局变量可用于整个代码库中的每一行代码(或无论如何包括头文件的每一行代码)。尽管实际上只有2行代码可以访问,但如果全局变量对400,000行代码可见,那么当您将其设置为无效状态时,您的可疑立即列表将具有400,000个条目(也许很快减少为2条带有工具的条目,但是尽管如此,直接列表将有40万名嫌疑犯,这不是一个令人鼓舞的起点)。

同样,即使全局变量仅在整个代码库中仅被两行代码修改才有可能,但是不幸的是,代码库向后发展的趋势也倾向于使该数字急剧增加,这仅仅是因为它可以增加尽可能多的数目。急于按时完成任务的开发人员,看到了这个全局变量,并意识到他们可以通过它获得捷径。

在不纯正的语言(例如C ++)中,状态管理不是您真正在做什么吗?

在很大程度上,是的,除非您以一种非常奇特的方式使用C ++,从而使您始终处理定制的不可变数据结构和纯函数式编程-当状态管理变得复杂且复杂度高时,它通常也是大多数错误的源头通常取决于该状态的可见性/曝光度。

除了限制可变寿命之外,还有什么其他方法可以处理尽可能少的状态?

所有这些都是限制变量范围的领域,但是有很多方法可以做到这一点:

  • 避免像瘟疫这样的原始全局变量。甚至某些愚蠢的全局设置器/获取器函数都极大地缩小了该变量的可见性,并且至少允许以某种方式维护不变式(例如:如果永远不应该将全局变量设置为负值,则设置器可以保持该不变式)。当然,即使是在全局变量之上的二传手/ getter设计也很糟糕的设计,我的观点是,它仍然要好得多。
  • 尽可能减少您的班级。具有数百个成员函数,20个成员变量和30,000行代码的类的类将具有相当的“全局”私有变量,因为其所有成员函数都可以访问所有这些变量,这些成员函数由3万行代码组成。在这种情况下,您可能会说“状态复杂度”是,而每个成员函数中的局部变量都没有折扣30,000*20=600,000。如果有10个全局变量可访问,则状态复杂度可能类似于30,000*(20+10)=900,000。一个健康的“状态复杂度”(我个人发明的度量标准)应该在几千或以下,而不是数万,绝对不要成千上万。对于免费功能,请说几百个或更少,然后我们才能开始严重维护头痛。
  • 按照与上述相同的方式,不要将某些东西实现为成员函数或朋友函数,否则它们可以成为非成员,仅使用类的公共接口成为非朋友。此类函数无法访问类的私有变量,因此通过减小这些私有变量的范围来减少发生错误的可能性。
  • 避免在函数实际需要它们之前就声明变量(即避免使用传统的C样式,该样式在函数顶部声明所有变量,即使它们仅在下面需要很多行也是如此)。如果您仍然使用此样式,则至少要争取使用较短的功能。

变量之外:副作用

我上面列出的许多指导原则都是针对直接访问原始的可变状态(变量)。但是,在足够复杂的代码库中,仅缩小原始变量的范围不足以轻松推断正确性。

例如,您可能有一个中央数据结构,它位于完全SOLID抽象的接口后面,完全有能力完美地维护不变式,并且由于这种中央状态的广泛暴露而最终仍会遇到很多麻烦。不一定全局可访问但只能广泛访问的中央状态的一个示例是游戏引擎的中央场景图或Photoshop的中央层数据结构。

在这种情况下,“状态”的概念超出了原始变量的范围,而仅涉及数据结构和此类事物。同样,它有助于减小它们的范围(减少可以调用间接使它们变异的函数的行数)。

在此处输入图片说明

请注意,在这里我是如何故意将接口标记为红色的,因为从广泛的,缩小的体系结构级别来看,访问该接口仍然是突变状态,尽管是间接的。该类可以由于接口而保持不变,但这仅就我们推理正确性的能力而言如此。

在这种情况下,中央数据结构位于抽象接口的后面,该接口甚至可能无法全局访问。它可能只是被注入,然后(通过成员函数)从复杂代码库中的大量函数中间接地突变。

在这种情况下,即使数据结构完美地保持了自己的不变性,也可能在更广泛的层面上发生奇怪的事情(例如:音频播放器可能会维护各种不变性,例如音量水平永远不会超出0%到0%的范围)。 100%,但这不能防止用户按下播放按钮并拥有随机音频剪辑,而该音频剪辑不是他最近加载的音频剪辑,而是触发事件触发播放列表以一种有效的方式重新播放,从广泛的用户角度来看仍然不希望出现的小故障行为)。

在这些复杂的情况下保护自己的方法是“瓶颈”在代码库中可以调用最终导致外部副作用的功能的位置,即使从原始状态和接口之外的这种更广泛的系统角度来看也是如此。

在此处输入图片说明

看起来很奇怪,您可以看到许多地方都没有访问“状态”(以红色显示,并不表示“原始变量”,仅表示“对象”,甚至可能位于抽象接口的后面) 。每个功能都可以访问本地状态,该状态也可以由中央更新器访问,并且中央状态仅可由中央更新器访问(使其本质上不再是中央的,而是本地的)。

这仅适用于真正复杂的代码库,例如跨越1000万行代码的游戏,但是当您显着限制/瓶颈数目时,它可以极大地帮助您推理软件的正确性,并发现您的更改会产生可预测的结果。可以改变整个体系结构正常运行的关键状态的位置。

除了原始变量之外,还存在外部副作用,即使将外部副作用限制在少数成员函数中,它们也是错误的根源。如果一系列功能可以直接调用那些少数成员函数,那么系统中就有一系列功能可以间接导致外部副作用,从而增加复杂性。如果代码库中只有一个地方可以访问这些成员函数,并且一条执行路径不是由各地的偶发事件触发的,而是以一种非常可控的,可预测的方式执行的,那么它将降低复杂性。

国家复杂性

即使是国家的复杂性,也是要考虑的一个相当重要的因素。一个简单的结构,在抽象接口的后面可以广泛访问,并不是那么容易搞砸。

表示复杂体系结构的核心逻辑表示形式的复杂图形数据结构非常容易弄乱,并且甚至不会违反图形的不变式。图比简单的结构复杂很多倍,因此在这种情况下,降低代码库的感知复杂度,将访问这种图结构的位置的数量减少到绝对最小值变得尤为重要,在那种转变为拉式范式以避免零星发生的“中央更新器”策略的情况下,从各地到图数据结构的直接推送确实可以带来回报。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.