​原文​

可以传递​​常​​,这编译:

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;
}

只在​​类型​​有​​可变间接​​时,发出错误.因为此时可通过更改​​数组/指针​​后面的内容来违反​​常​​.

​我​​一般标记​​局部变量​​为​​常​​,因此此时​​不变​​无意义.


​序号​

必须用​​常​​时

​1​

泛型代码中,不知道变量是​​可变|不变​​的

​2​

如果从​​常函数参数/常变量​​初化变量

​3​

不想​​共享​​​变量,因为​​不变​​隐含共享

​4​

​为啥C++常很烂​


我写个这样的八哥:

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; }

结合​​尾调用优化​​​,​​附加函数调用​​的开销非常小.