• 2310d用d写自证明代码


    原文

    用D写自证明的代码

    你有没有看过五年前的代码,不得不研究它,才能弄清楚它在做什么?越久远,情况就越糟糕.可怜的我,仍在维护40多年前编写的代码.

    本文演示了许多简单方法,来使你的代码自证明且更易于理解和维护.
    请允许我介绍我在1987年写的该小宝石:

    #include 
    #define O1O printf
    #define OlO putchar
    #define O10 exit
    #define Ol0 strlen
    #define QLQ fopen
    #define OlQ fgetc
    #define O1Q abs
    #define QO0 for
    typedef char lOL;
    lOL*QI[] = {"Use:\012\011dump file\012","Unable to open file '\x25s'\012",
      "\012","   ",""};
    main(I,Il)
    lOL*Il[];
    {    FILE *L;
       unsigned lO;
       int Q,OL[' '^'0'],llO = EOF,
       O=1,l=0,lll=O+O+O+l,OQ=056;
       lOL*llL="%2x ";
       (I != 1<<1&&(O1O(QI[0]),O10(1011-1010))),
       ((L = QLQ(Il[O],"r"))==0&&(O1O(QI[O],Il[O]),O10(O)));
       lO = I-(O<<l<<O);
       while (L-l,1)
       {    QO0(Q = 0L;((Q &~(0x10-O))== l);
           OL[Q++] = OlQ(L));
         if (OL[0]==llO) break;
         O1O("\0454x: ",lO);
         if (I == (1<<1))
         {    QO0(Q=Ol0(QI[O<<O<<1]);Q<Ol0(QI[0]);
           Q++)O1O((OL[Q]!=llO)?llL:QI[lll],OL[Q]);/*"O10(QI[1O])*/
           O1O(QI[lll]);{}
         }
         QO0 (Q=0L;Q<1<<1<<1<<1<<1;Q+=Q<0100)
         {    (OL[Q]!=llO)? /* 0010 10lOQ 000LQL */
           ((D(OL[Q])==0&&(*(OL+O1Q(Q-l))=OQ)),
           OlO(OL[Q])):
           OlO(1<<(1<<1<<1)<<1);
         }
         O1O(QI[01^10^9]);
         lO+=Q+0+l;}
       }
       D(l) { return l>=' '&&l<='\~';
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    是的,这就是当时编写C代码的方式.我甚至为此获奖!
    现在就射我:

    #define BEGIN {
    #define END   }
    
    • 1
    • 2

    这是1980年代常见的C做法.它属于"不要让你的新语言像你以前语言"的分类.此问题以多种形式出现.我仍喜欢用Fortran风格命名旧时代的变量.
    转向D之前,我发现在C之上,使用C宏发明一个个人自定义语言令人憎恶的.删除它并用普通的C代码替换能大大改进清晰度.

    不要重新发明布尔值

    了解什么是布尔值,以下内容都是相同的:

    false|true
    0|1
    no|yes
    off|on
    0 volts|5 volts
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如下无疑使代码更糟:

    enum { No, Yes };
    
    • 1

    只需使用假和真.就完了.顺便,

    enum { Yes, No };
    
    • 1

    只是个自动的"不雇用"决定,因为if (Yes)彻底搞混所有人.如果已这样做了,请在有人诅咒你的整个祖先运行并修复它.

    D封锁了恐怖

    D的语法旨在避免某些类型的编码恐怖.

    C++有重载操作符的正则式

    我甚至不会链接它.可通过勤奋的搜索找到.作用是重载操作符来使普通的C++代码是个正则式!它违反了代码不应假装是另一个语言的原则.

    如果存在编码错误,编译器会告诉你错误消息.D通过只允许重载算术操作符完成,这禁止重载一元*等.
    在D中滥用重载操作符更难,但仍有可能.但幸好,让它更难九分阻止了它.

    使用宏元编程

    许多人要求给D添加宏.我们抵制这一点,因为宏不可避免地导致,人们在D上发明自定义,未记录的语言分层.使其他人很难利用此代码.

    在我不太谦虚的观点中,Lisp从未在主流中流行的原因.没有Lisper可读懂其他人的Lisp代码.

    C++依赖参数查找

    没有人知道会找到什么符号.添加了ADL,以便可在左侧操作数上重载操作符.而D只有个简单的语法来重载左或右操作数.

    替换失败不算错

    没人知道SFINAE是否在特定表达中发挥作用.

    地板蜡或美味的甜点浇头

    这是指结构类型或引用类型,或两者的某种嵌合体间的混淆.在D中,结构值类型,引用类型.公平地说,有人仍构建D嵌合体类型,但应该加钱.

    多继承

    为何要这样做?使用钻石继承时,事情非常糟糕.怜悯下个人,避免诱惑.D仅对接口多继承,已证明这是绰绰有余的.

    代码流

    代码应从左到右,从上到下排列.与这篇文章的阅读方式一样.

    f() + g() //`0010 10l0Q 000LQL`
    //哪个先执行
    
    • 1
    • 2

    幸好,D保证从左到右的排序(C不保证).但是呢:

    g(f(e(d(c(b(a))),3)));
    
    • 1

    由内而外执行!快速,3调用哪个函数?传递给D的通用函数调用语法以救援:

    a.b.c.d(3).e.f.g;
    
    • 1

    这是等效的,但执行显然是从左到右的.这是个极端示例,还是常态?

    import std.stdio;
    import std.array;
    import std.algorithm;
    void main() {
         stdin.byLine(KeepTerminator.yes).
         map!(a => a.idup).
         array.
         sort.
         copy(stdout.lockingTextWriter());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此代码逐行从stdin读取,并把行放入数组中,排序数组,然后把排序结果写入stdout.它不太符合"愚蠢简单"的标准,但它非常接近.
    所有这些都从左到右,从上到下都有很好的流动.

    该示例也很好地进入了下个观察结果.

    控制路径越多,越难理解

    Shaw:你很了解计算机,不是吗?
    斯波克先生:我对他们了如指掌.

    version (X)
         doX();
    doY();
    if (Z)
         doZ();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    比以下更难理解:

    doX();
    doY();
    doZ();
    
    • 1
    • 2
    • 3

    条件式有什么变化?把它们移动到doX()doZ()的内部.

    我知道你在想什么."但是沃尔特,你没有消除条件式,只是移动了它们!"完全正确,但这些条件式正确的属于函数,而不是将这些函数括起来.
    它们是函数提供的概念封装的一部分,因此调用者是干净的.

    否定

    if (!noWay)
    
    • 1

    不可避免地当作:

    if (noWay)
    
    • 1

    教训是尽量避免标识中使用否定.

    if (way)
    
    • 1

    不是更好吗?

    DMD源码耻辱堂

    我自己的代码几乎是反面教材.一些标识:

    tf.isnothrow
    IsTypeNoreturn
    Noaccesscheck
    Ignoresymbolvisibility
    Include.notComputed
    not nothrow
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    否定和版本

    D版本条件非常简单,这里:

    version ( Identifier )
    
    • 1

    一般由编译器或命令行预定义标识.只允许使用标识,禁止否定,AND,ORXOR.(统称为版本代数).用户经常对该限制感到恼火.
    可以搞版本代数:

    version (A) { } else {
        //!A
    }
    version (A) version (B) {
         //`A&&B`
    }
    version (A) version = AorB;
    version (B) version = AorB;
    version (AorB) {
         //`A||B`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    等等.它故意笨拙.为什么D会这样做?它鼓励积极方式思考版本.假设项目有个WindowsOSX版本:

    version (Windows) {
         ...
    }
    else version (OSX) {
         ...
    }
    else
         static assert(0, "不支持");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    是否比如下更好:

    ...
    version (!Windows){
    ...
    }
    
    • 1
    • 2
    • 3
    • 4

    我在C语言中看到了很多该风格.使得很难添加新操作系统的支持.毕竟,"不是窗口"操作系统到底是什么,这确实缩小了区间!
    前一个片段更容易.
    更进一步:

    if (A && B && C && D)
    if (A || B || C || D)
    
    • 1
    • 2

    很容易懂.而遇见:

    if (A && (!B || C))
    
    • 1

    呸.我一直在此结构上犯错误.不仅很难看到!而且仍很难满足自己是正确的.
    幸好,德摩根定理有时可派上用场:

    (!A && !B) => !(A || B)
    (!A || !B) => !(A && B)
    
    • 1
    • 2

    它摆脱了一个否定.重复应用一般可转换其为更容易理解的方程,同时同样正确.

    轶事:在设计数字逻辑电路时,NAND栅极比AND栅极效率更高,因为它少了一个晶体管.(AND表示(A&&B),NAND表示!(A&&B)).

    但人们在制作无错NAND逻辑方面很臭.当我在1980年代设计ABEL语言时,来编程可编程逻辑器件,ABEL会接受正逻辑输入.
    它使用德摩根定理自动转换它为有效的负逻辑.电子设计师喜欢它.

    总结本节,这里有一个来自Ubuntuunistd.h的可耻片段:

    #if defined __USE_BSD || (defined __USE_XOPEN && !defined __USE_UNIX98)
    
    • 1

    转换是错误

    转换颠覆了类型系统的保护.有时必须要有它们(如,要实现malloc,需要转换结果),但很多时候它们只是为了纠正草率误用类型.

    因此,在D中,使用cast关键字来完成转换,而不是用特殊语法,这样,很容易搜索它们.偶尔搜索cast,并检查是否可重新设计,来消除转换,并让类型系统为你工作而不是反对你,是值得的.

    拉取请求:删除一些dyncast调用

    自记录函数声明

    char* xyzzy(char* p)
    
    • 1

    1,p是否修改了指向内容?
    2,是否返回p?
    3,xyzzy是否释放p
    4,xyzzy是否如在全局中保存p?
    5,xyzzyp?

    函数文档很少注明这些关键信息.更差,文档经常出错!需要的是编译器强制的自记录代码.D涵盖此内容的属性:

    const char* xyzzy(return scope const char* p)
    
    • 1

    1,p不会修改指向内容
    2,返回p
    3,未释放p
    4,xyzzy不会丢弃P副本
    5,p不会在异常中

    现在不需要编写文档,编译器为你检查准确性.是的,叫"属性汤"是有充分理由的,且要花时间适应,但它仍比糟糕文档要好,且添加属性可选的.

    函数参数和返回

    函数声明中存在的函数输入和输出是"前门".不在函数声明中的输入和输出都是"侧门".侧门包括全局变量,环境变量,从操作系统取信息,读取/写入文件,抛异常等内容.

    很少在文档中说明侧门.调用函数时,必须仔细阅读实现,来辨别侧门是什么.

    自证明代码应努力通过前门运行所有内容.这不仅帮助理解,而且还可实现,如简单单元测试.

    分配内存

    实现需要分配内存的算法的函数面临的一个持续问题是,应用哪种分配内存方案.一般,给调用者强加可重用分配内存函数的方法.那是落后的.

    对由函数分配和释放的内存,方法是函数决定如何做.对函数返回已分配对象,调用者应通过传递指定分配方案参数来确定分配方案.

    此参数一般带"接收器"来发送输出.

    为输出传递抽象"接收器"

    方式(摘自DMD源码):

    import dmd.errors;
    void gendocfile(Module m) {
         ...
         if (!success)
             error("扩展极限");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    error()是发送错误消息函数.这是传统代码中的典型公式.从侧门发出错误消息.gendocfile()管不了如何处理错误消息,且文档一般会省略生成错误消息的事实.
    更差,发射错误消息,就很难单元测试函数.
    最好按参数传递"sink"抽象接口,并发送错误消息到接收器:

    import dmd.errorsink;
    void gendocfile(Module m, ErrorSink eSink) {
         ...
         if (!success)
             eSink.error("扩展极限");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    现在,调用者可完全控制错误消息,且隐式记录.单元测试器可提供特殊实现接口,来适应测试.

    这是真实世界的PR此改进:doc.d:使用errorSink.

    按缓冲而不是要读取的文件,传递文件

    传递文件名函数以读取和处理它们的典型代码:

    void gendocfile(Module m, const(char)*[] docfiles) {
         OutBuffer mbuf;
         foreach (file; ddocfiles) {
             auto buffer = readFile(file.toDString());
             mbuf.write(buffer.data);
         }
         ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    很难单元测试该类代码,因为给单元测试器添加文件I/O非常笨拙,因此没有编写单元测试.文件I/O,一般与函数无关.只需要操作的数据.
    修复按数组传递文件内容.

    void gendocfile(Module m, const char[] ddoctext) {
         ...
    }
    
    • 1
    • 2
    • 3

    PR:从doc.d中移出读取ddoc文件,这里.

    写入缓冲,调用者写入文件

    处理数据并把结果写入文件的典型函数:

    void gendocfile(Module m) {
         OutBuffer buf;
         ... 填充缓冲 ...
         writeFile(m.loc, m.docfile.toString(), buf[ ]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    现在,知道应该是调用者写入文件:

    void gendocfile(Module m, ref OutBuffer outbuf) {
         ... 填充缓冲 ...
    }
    
    • 1
    • 2
    • 3

    和公关:
    doc.d:把写入文件移动到调用者,这里

    移动调用环境到调用者

    下面是从环境中取输入函数:

    void gendocfile(Module m) {
         char* p = getenv("DDOCFILE");
         if (p)
             global.params.ddoc.files.shift(p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    调用者读取环境,然后通过前门传递信息的PR:
    DDOCFILEdoc.d移动到main.d,这里

    使用函数(或模板)指针

    最近在研究文本处理模块.需要识别标识串的开头.因为Unicode很复杂,它导入了处理Unicode的(相当重要的)模块.
    但,我只要求确定标识的开头;不需要进一步了解Unicode.
    终于想到,调用者只需按参数传递函数指针给不需要Unicode知识的文本处理器.

    import dmd.doc;
    bool expand(...) {
         if (isIDStart(p))
             ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    变成:

    alias fp_t = bool function(const(char)* p);
    bool expand(..., fp_t isIDStart) {
         if (isIDStart(p))
             ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意导入是如何消失的,改进了函数的封装可理解性.函数指针也可是对应用更方便者的模板参数.函数注解越多,就越容易理解.

    PR:删除dmacro.ddoc.d的依赖,这里

    两类函数

    1,更改程序状态
    函数名中提供线索,如doAction().
    2,提出问题
    同样,应该在名字有线索.如isSomething(),hasCharacteristic(),getInfo()等.考虑使函数为,以确保无副作用.

    尽量不要创建既提出问题又修改状态函数.

    识别视觉模式

    格式化源码的程序很好.但我不使用它们.哎呀!原因如下:

    final switch (of)
    {
         case elf:   lib = LibElf_factory();    break;
         case macho: lib = LibMach_factory();   break;
         case coff:  lib = LibMSCoff_factory(); break;
         case omf:   lib = LibOMF_factory();    break;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    表明,大脑非常擅长识别模式.通过排列,可创建模式.偏离该模式行为都可能是漏洞.
    我用模式检测到了很多漏洞,而格式化源码程序在检查模式方面做得不好.

    ref代替*

    引用受限指针.禁止使用算术,也禁止ref参数逃逸函数.这不仅通知用户,还通知编译器,确保函数带引用好孩子.

    用引用的混杂(mangleToBuffer())

    总结

    1,按期望使用语言功能,不要发明自己的语言
    2,避免否定
    3,从左到右,从上到下
    4,函数,通过前门完成所有操作
    5,不要把引擎与环境搞混
    6,降低圈复杂度
    7,拆分提出问题更改状态的函数.

    建议很容易遵循.少许重构就可实现.引用的PR表明使代码自证明是很简单的!

  • 相关阅读:
    LabVIEW下载 安装 和谐 与卸载教程
    IDEA软件使用步骤
    【c++】类和对象
    信号处理(频谱,能量谱,功率谱)--学习笔记
    Application.OpenForms
    力扣(88.53)补8.31
    容器启动报错
    k8s 集群使用 haproxy+keepalived+nginx 实现k8s集群负载均衡
    【217】#!/usr/bin/env 的意义
    jmeter-beanshell学习1-vars使用获取变量和设置变量
  • 原文地址:https://blog.csdn.net/fqbqrr/article/details/133522267