原文
可以传递常
,这编译:
struct S {
int x;
const(int)[] arr;
immutable(char)[] str;
}
void main() {
const S sc;
immutable S si;
f(sc); // 可变副本传递给f
f(si); // 可变副本传递给f
}
void f(S s) {
s.x = 3;
}
只在类型
有可变间接
时,发出错误.因为此时可通过更改数组/指针
后面的内容来违反常
.
我
一般标记局部变量
为常
,因此此时不变
无意义.
| 必须用 |
| 泛型代码中,不知道变量是 |
| 如果从 |
| 不想 |
| … |
我写个这样的八哥:
void alphabeta(ref Field field, /*...*/) {
// (...)
Field fieldCopy = field;
field.modify(); //<应为fieldCopy.modify()
alphabeta(fieldCopy);
// (...)
}
我可标记field
参数为常
,因为不应修改它.下次编译,立即找到
该处错误
.
我一般尽量严格const/scope/pure/@safe
标记变量/函数
.如果不这样,会生产多么八哥
.
现在当我几天或几周后
修改部分代码
时,不用担心意外
破坏我所依赖
的这些属性,这里人工
代替不了机器检查
.
标记
所有想不修改
的变量为常
,确实有点麻烦.
C++
还有程序员这样:因为不喜欢处理常
,用#define const 空格
来取消常
.
你认为不值得,可以不标记为常
.
常
在接口边界
处很实用,帮助维护.
希望
编译器帮助你防止错误时,可用常
.或让用常
的程序员调用你接口时,也尽量用常
,因为它最大化使用.常/不变
是编译时概念
.
你在说什么?如下编译:
int foo(const int);
int bar(int i)
{
return foo(i);
}
不变全局
数据放置在只读内存节
中.
永远不会标记只读内存节
为"脏",因而不必复制
这些页面,所以只读内存节
在按需分页
的虚拟系统中很好.
前两天,我犯错了:
foreach(i; 0..2)
// foo();
bar();
函数API
上的const
也用于通信:告诉调用者函数不应改变哪些参数
.但我已经成为提倡'in'
而不是'常'
的人之一,尤其是在使用-preview=in
编译时,参见,in
甚至允许按引用
传递右值
.
这样,比较简洁:
if (x == y)
doSomething();
else if (y == z)
doSomethingElse();
else
doYetAnotherThing();
D
禁止单个;
循环体来避免:
for (int i=0; i<l; i++);
错误,你必须用{}
.也不允许悬挂else
,函数体必须用{}
.
我用埃及牙套
,不是最漂亮的,但紧凑且安全
.
if (x == y) {
doSomething();
} else if (y == z) {
doSomethingElse();
} else {
doYetAnotherThing();
}
我曾是一名铁杆C程序员
.如此顽固,以至于我曾经在IOCCC
中获得过一次奖项(好吧,这不是值得骄傲的事情:-D)
.在面试技术
考试中正确回答了连我的面试官
都错了的C题.完全融入了"程序员更懂,编译器请靠边站,不要再限制我
“的哲学.相信我的代码是完美的,不可能有任何错误,因为我仔细考虑了每一行并打磨了每个字符.不相信测试套件,因为我在编写每个函数时都对它进行了手工测试,因此不会留下任何错误.此外,测试套件使用起来太麻烦了.曾经为我的程序从不崩溃而自豪.(他们这样做的次数我归咎于偶然因素),
然后我发现了D.特别是D的单元测试
块.起初非常抗拒(为什么我需要测试完美的代码),但它们非常方便(不像其他语言的单元测试框架),它们只是用小狗的眼睛盯着我,直到我为不使用它们而感到羞愧.然后单元测试
开始捕捉错误.大量的错误
.各种边界情况,粗心的错别字,逻辑缺陷
等等,都在我的"完美"代码中.每次我修改一个函数
时,另一个单元测试
开始在以前测试的案例上失败(我认为这与我的更改无关,因此不值得重新测试).
然后我开始意识到这个可怕的认识:我的代码并不完美.事实上,它一点也不完美.我的"完美"逻辑源于我对完美算法的"完美"构想,实际上充满了缺陷,逻辑错误,我没有想到的边界情况,错别字,以及纯粹的愚蠢错误.最糟糕的是,*我*
是犯这些粗心错误的人,几乎每一次我都写了任何代码.我认为完美的代码实际上几乎每一行都充满了隐藏的错误.通常在我整个职业生涯中写了数万次的台词中,我认为即使在梦中我也可以完美地写出来,我非常了解它们.但正是因为我的信心,这些"琐碎"的代码行是正确的,
然后我观察到我公司的顶级C程序员
犯了同样的错误,一遍又一遍.这些不是没有经验的C新手,他们不知道自己在做什么;这些是几十年来一直在努力的顶级C黑客.然而,他们一遍又一遍地重复同样的古老错误.我开始意识到,这些不仅仅是新手的错误,会随着经验和专业知识而消失.这些错误不断发生,因为人类犯了错误.
而且由于C的哲学是信任程序员
,这些错误会在未经检查的情况下潜入代码中,造成一场又一场的灾难.这里的缓冲区溢出
,那里的安全漏洞
,粗心的拼写错误
导致客户的生产服务器在关键时刻崩溃.内存泄漏和文件描述符泄漏使顶级服务器在"完美"运行数月后陷入瘫痪.花费在寻找和修复这些漏洞上的时间和金钱,导致技术债务堆积如山.
今天,我的”信任程序员
“理念被打破了.我*希望*
编译器告诉我,当我在做一些看起来可疑的错误的事情时.我*希望*
默认语言是安全的,我必须竭尽全力犯错.我希望编译器阻止我做我以前做过一百次但继续做的愚蠢事情,因为人类是容易犯错的.
当然,我不想像Java
让你做的那样穿着紧身衣
写作,当我*确实*
知道我在做什么时,必须有一个逃生口.但是*default*
应该是阻止我做愚蠢事情的编译器.如果我真的要转换该指针,我*希望*
必须编写一个冗长,丑陋的"cast(NewType*)ptr"
,而不是仅仅让一个void*
隐式转换为我手头碰巧
拥有的任何指针类型,写出这个冗长的结构,这迫使我停下来三思而后行,并希望在它滑入代码之前抓住任何错误的假设.我*想要*
编译器告诉我"嘿,你说数据是const
,现在你正在尝试修改它!”,这会让我记住"哦,是的",
正如沃尔特经常说的,按惯例编程
是行不通的.数十年的C代码灾难性故障已经证明了这一点.人类是容易犯错的
,不能依赖于程序的正确性.我们擅长某些事情,直觉飞跃和针对难题的开箱即用
的聪明解决方案.
但是对于其他事情,比如让我们的代码中出现错误
,我们需要帮助.我们需要编译器
可以静态验证事物,以证明我们的假设确实成立(并且某人,即我们在编写该代码3个月后,没有违反这个假设并在最后一刻的代码更改期间引入错误在最后一个发布截止日期之前).像C++
的const
这样的弱酱,可以在任何时候随意丢弃而不会产生任何后果,这是行不通
的.你*需要*
像D
这样强大的const
来控制人为错误.编译器
可以自动检查
并提供真正保证
的东西.
4个
空格的缩进比较合适.
常
编译器结构来避免你干傻事
.但编译器可不用有常
概念,用只读
内存就干得好,D1
没有常
,只是把串字面
放在只读
内存中.
新语言
倾向于默认使用常
.如Swift
默认按常
取参,现在你要定义常 变量
.声明var
而不变,编译器会说,你应该用let
(声明常量).
常
可以防止错误,多数人喜欢.
我知道你想干啥.
void mul_num(T)(T num) {
num *= 2;
}
如果只mul_num(number)
这样,会失败,因为编译器从参数推导类型
为const int
.但D
没有这里应正常工作的尾常
.
可合并不变数据
在一起,从而减少占用内存
.dmd
对串这样,相同串在链接时
合并,仅当串
为不变
时才发生.
也合并,不变的0
初化数据,0
就是0
,与类型没关系.
你可反汇编
目标文件,不变数据
在只读节
.
标记参数为常
时,阅读器可确定该函数
对该参数
无副作用.
我是极简主义程序员
,我尽量避免不必要的小括号和大括号
.
是的,但如写至D1串
的话,则是未定义行为.
带相同函数体的函数
,合并为一个,可以考虑添加该优化
.
D的模板
要强大得多,但却是以模板膨胀
为代价的:对每个T
,都有新的代码
副本.
两全其美的是,D编译器按某种方式选择性
地擦除模板
类型,可分解为T的通用代码
加实例特化版
,合并二进制相同的函数
,是一个好步骤.
应该用模板
来代替inout
,让编译器来推断.
合并具有相同实体
的函数在一起,有多可行?
主要障碍是函数指针
.如果两个具有相同主体
的不同函数
都取了它们地址,并比较结果指针,则结果
必须为假
.除非编译器(或链接器
,在LTO
的情况下)可证明不会比较,否则优化
是无效的.
更简单
优化是"分解
"函数主体
为新函数,并简单转发原始函数
给新函数.例如,当出现
int f(int x, int y) { return x^^2 - y + 42; }
int g(int x, int y) { return x^^2 - y + 42; }
编译器这样:
int f(int x, int y) { return __generated(x, y); }
int g(int x, int y) { return __generated(x, y); }
int __generated(int x, int y) { return x^^2 - y + 42; }
结合尾调用优化
,附加函数调用
的开销非常小.