实践中的总结二:函数副作用

2024/04/08 总结 实践 共 1178 字,约 4 分钟

函数副作用

一句话总结:函数副作用能让我们在函数内部做一些影响到当前函数栈与生命周期外的数据的操作。请尽量避免这样做。

然而很多时候是无法避免的,在无法避免的前提下应对函数副作用的问题,请用函数名和注释标注清楚副作用。

简介

  • 数学意义上的函数
    • 函数(英语:Function)是数学描述对应关系的一种特殊集合
    • 每个输入值只能对应一个输出值,在给定相同的输入的情况下,它将始终返回相同的结果
    • 定义域值域
  • 程序意义上的函数
    • 一次调用过程
    • 有明确名称,入口,出口的代码段

函数

很明显,我们程序中有很多算不上数学意义的函数,如:

  • 依赖于外部环境
    • rpc 调用外部系统
    • 读文件,环境变量,全局变量
    • 写传入引用指向的数据,文件,环境变量,全局变量
  • 函数返回与返回值情况多
    • 存在错误,程序崩溃,函数自然无法返回
    • Java 可能抛出受检查异常,独立于返回值,需要特殊处理
    • 死循环不返回

这导致两个结果:

相同入参,会有不同的运行情况。

同时函数的作用,效应不仅有返回值,还会改变当前函数栈与生命周期外的数据。也就是我们这里要说的副作用

例子

不好的例子:

public void doIt(Param a){
	doOne(a);
	doTwo(a);
}

这样的实现可读性差,实际在逼迫看代码的人详细了解其实现。如果不了解,使用时担惊受怕,有踩坑的可能。

稍微好一点的实现:

public void doIt(Param a){
	useA(a);
	updateA(a);
	doTwo(a);
}

将副作用单独提取出来成为方法,并命名。这样的干扰会小很多(或者反而更多,如果 useA(a)仍然名不副实,看似命名标准清晰的函数们会带来更多麻烦)。

最后,较为推荐的实现:

public void doIt(Param a){
	Objectb b=getB(a);
	updateB(b);
	Objectc c=getC(b);
	...
}

相较更加清晰,而且如果不是刻意为之,getB(a)中做有副作用的操作的可能性很小,可以仅仅从函数接口判断函数逻辑,不需要知道具体实现细节。

总结

这一点经常在函数式及其教义中讨论。

这里也能体悟出 GO,RUST 等语言为什么将错误作为返回值返回,和 Java 的 exception 体系有何不同。

Java 的非受检查异常类似 Panic,直接终止程序的运行,这都是非函数的。

Java 的受检查异常,和错误返回值有所不同,这是对与返回值不同的另一种运行情况,明显是非函数的,所有调用方要么接受这一点,继续抛出,要么做特殊处理捕获。GO,RUST 在调用后需要对错误进行处理,错误作为一个值返回,被调用方从返回值中可以了解函数,并据此进行处理,这样的发生错误的情况仍然是函数的。

同时,这也能指导我们的编码风格,具体的说,如何提炼函数,如何将更大的一段逻辑处理为多个易读的函数。将大逻辑的函数切分为多个没有副作用的小函数与将副作用集合在一起的函数,这是较好的实践。

文档信息

Search

    Table of Contents