D中的纯
原文
D
的纯及同其他特征的交互
纯是程序员和编译器
帮助理解代码
的利器.
pure
代表不访问全局可变状态
的函数属性
.全局
指除了(不能在线程间引用共享数据的)函数参数外
的东西.访问就是读写,未标记纯
则为不纯
.
即给定参数集
,纯函数总是具有相同效果和或返回相同结果
.因而纯
不能调用不纯
,且不能处理(经典意义的)io
.
透明引用
D
中的纯
可改变参数.下面完全有效:
int readAndIncrement(ref int x) pure {
return x++;
}
可能会让人感到惊讶,因为理论中纯度
应是引用透明.即可用结果
替换,而不改变语义(无副作用).但D
不是这样的.如下:
int val = 1;
auto result = readAndIncrement(val) * readAndIncrement(val);
// assert(val == 3 && result == 2);
不能这样替代:
int val = 1;
auto tmp = readAndIncrement(val);
auto result = tmp * tmp;
// assert(val == 2 && result == 1);
如果,想达到经典的强纯
呢?用const/immutable
来标记数据:
int a(int[] val) pure;
int b(const int[] val) pure;
int c(immutable int[] val) pure;
a
与上面的readAndIncrement
一致.而b和c
,因为纯
我们知道他们不更改
任何全局状态,且参数
不可变.所以b/c
是经典意思的纯
,即强纯
,无副作用,调用他们是透明引用
的.
常/不变
在此区别不大,但有个微妙但重要的影响调用
区别:取决于调用参数是否为const/常/不变
.常
则可变/不变
均可转为它.如果是不变数组
调用,b/c
都可应用.
如,实现缓存或消除公共子表达式机制
,遇见带不变
的纯
函数,只需要检查参数的唯一性
,以便将多次调用优化为一次
.如按比较运行时实现的内存地址
,或按在优化编译器的几个非常简单检查
.
另一方面,如参数类型有间接
且为常
.则可在两次调用
间修改数据.深度比较,对大数据结构
或大量分析数据流
就不可行,
同样分析也适用于并行化,如果纯
无或仅有不变
间接,则可保证安全并行化(因为不会有副作用
),且参数中无数据竞争
.但常
则很难推断.因为可变
可能会修改
他们.
返回类型中的间接
上例,a,b,c
因可变性(间接)
而不同.但都返回整
.如果返回
引用,是否要考虑
更多?
第1个要点是地址
.在函数式语言
中,值所在的实际内存地址
一般不重要.而D
暴露该概念,考虑:
ulong[] primes(uint count) pure
返回分配count
个素数
的数组.用相同count
多次调用primes
函数,返回相同数字,但地址不一样.
当考虑返回值
中有带间接
的函数透明引用
时,重要的是逻辑相等(==)
而非按位相等(is)
.
对透明引用,另外重要的是返回类型
中的可变间接引用
.
auto p = primes(42);
auto q = primes(42);
p[] *= 2;
显然,重写第2个为auto q = p
是无效的.这样的话,q
指向相同内存切片.执行后,q
与p
一样的了.
一般,返回类型
中有可变间接
的纯函数的调用,不能立即认为是透明引用
,但仍可优化许多调用,可能取决于调用代码
如何使用返回值.
弱纯允许强保证
D
最初为纯/强纯
,但讨论后,分成:弱纯/强纯
,弱纯
有像上面的readAndIncrement和a
的可变
参数.而强纯则类似b/c
一样的无副作用
.
纯度根据参数/返回类型的不同
,而得到不同保证.
放松规则,允许更多的强纯
函数.
如每次画三角形
都复制帧缓冲区
肯定不好.而在D
中可实现纯
三角形绘画函数,而不担心性能:
alias ubyte[4] Color;
struct Vertex { float[3] position; /* ... */ }
alias Vertex[3] Triangle;
void drawTriangle(Color[] framebuffer, const ref Triangle tri) pure;//画三角.
这里,画三角
不可能是透明引用,因为它要写入帧缓冲区
.但纯
仍保证它不会更改任何隐藏/全局状态
.同时,作为纯
,别的纯
可调用你.
如果可以每帧分配
新缓冲区,这可能是渲染三角形组成的整个场景
的函数.
Color[] renderScene(//渲染场景
const Triangle[] triangles,
ushort width = 640,
ushort height = 480
) pure {
auto image = new Color[width * height];
foreach (ref triangle; triangles) {
drawTriangle(image, triangle);
}
return image;
}
注意
,renderScene
无可变间接.虽然内部调用可变参的drawTriangle
,但renderScene
作为整体是透明引用
的!放松纯
增加了纯
函数.
现代编程,不鼓励使用
全局状态,应将不处理I/O
的大多数函数标记为纯
.因为纯
添加晚
了,所以默认不纯
.
模板和纯度
函数模板是否是纯
可能由被实例化
类型决定.如写接受区间
,返回数组
函数:
auto array(R)(R r) if (isInputRange!R) {
ElementType!R[] result;
while (!r.empty) {
result ~= r.front;
r.popFront();
}
return result;
}
问题是,能为纯
吗?如果是map或filter
,则可纯,但从标准输入
读呢?这里没有r.empty,r.front和r.popFront()
,如果标记为纯
,则不能操作他们了.
D
最后为了实例化模板,其源必须可用,编译器自动推导纯度(及如不抛等其他类似属性)
.
即,如果区间允许纯
,则从纯函数调用
.
如果纯
不依赖于模板参数,显示指定纯
也是好的.
纯成员函数
构/类
成员函数,也可为纯
,与自由函数
一样.只是对纯
语义,增加了隐式的this
参数.可以说纯函数
可访问和修改
成员变量了.
class Foo {
int getBar() const pure {
return bar;
}
void setBar(int bar) pure {
this.bar = bar;
}//纯
private int bar;
}
允许纯函数
访问成员变量.标记const或immutable
同样应用至this
参数.
对类继承而言,子类成员函数
假设更少,保证更多.纯函数
可覆盖不纯
函数,但反之则不然.
覆盖纯基类
方法,隐式标记为纯
.
纯与不变
由于与类型系统集成
,纯函数返回值
有时可安全转换
为不变
.考虑上面的ulong[] primes(uint n) pure
.第1眼,下面代码为何编译,不明显:
immutable ulong[] p = primes(5);
这是在假设
不存在其他可变引用primes
的返回值,因而当然为纯
(只用了一次嘛).它不带可变
间接参数,不访问全局可变状态
.
这实际上很有用.因为它允许在(函数式
)不变数据上下文
中无缝使用函数.
但仍要调用不纯函数
如处理遗留代码,通过用cast
来处理,取函数指针,通过转换(cast)
加pure
属性.在@safe
中是禁止
的.
可引入assumePure
模板来封装它.
为了调试,加不纯
语句,因而,有debug
开关时,允许在纯函数
中使用不纯
代码.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现