我在Stack Overflow上看到了一些这样的提及,但是盯着维基百科(相关页面已被删除),并且在一个MFC动态对话框演示中没有任何启发我.有人可以解释一下吗?学习一个根本不同的概念听起来不错.
根据答案:我认为我对此有了更好的感受.我想我第一次没有仔细查看源代码.在这一点上,我对差异执行情有不同的看法.一方面,它可以使某些任务变得相当容易.另一方面,启动并运行(即,用您选择的语言设置)并不容易(我确信如果我更好地理解它)...虽然我猜它的工具箱只需要制作一次,然后根据需要进行扩展.我认为为了真正理解它,我可能需要尝试用另一种语言实现它.
吉,布莱恩,我希望我早点看到你的问题.因为它几乎是我的"发明"(无论好坏),我或许可以提供帮助.
插入:我可以做的最简单的解释是,如果正常执行就像在空中投球并抓住它,那么差异执行就像玩杂耍一样.
@ windfinder的解释与我的不同,那没关系.这种技术并不容易缠绕在一起,而且花了大约20年的时间(关闭和开启)才能找到有效的解释.让我再说一遍:
它是什么?
我们都理解计算机单步执行程序,根据输入数据获取条件分支以及执行操作的简单概念.(假设我们只处理简单的结构化goto-less,无返回代码.)该代码包含语句序列,基本结构化条件,简单循环和子例程调用.(忘记现在返回值的函数.)
现在想象两台计算机彼此锁定执行相同的代码,并能够比较笔记.计算机1运行输入数据A,计算机2运行输入数据B.它们一步一步地并行运行.如果他们来到像IF(测试)...... ENDIF这样的条件语句,并且如果他们对测试是否为真有不同意见,那么说测试的人如果错误跳到ENDIF并等待它的姐姐赶上了.(这就是代码结构化的原因,所以我们知道姐姐最终会到达ENDIF.)
由于两台计算机可以相互通信,因此它们可以比较注释并详细说明两组输入数据和执行历史是如何不同的.
当然,在差分执行(DE)中,它是用一台计算机完成的,模拟两台计算机.
现在,假设您只有一组输入数据,但是您想看看它从时间1到时间2的变化情况.假设您正在执行的程序是序列化器/解串器.在执行时,您既可以序列化(写出)当前数据,也可以反序列化(读入)过去的数据(上次执行此操作时写入).现在,您可以轻松查看上次数据与此次数据之间的差异.
您正在写入的文件和您正在读取的旧文件一起构成一个队列或FIFO(先进先出),但这不是一个非常深刻的概念.
到底有什么好处呢?
当我在一个图形项目上工作时,我想到了这一点,用户可以构建一个名为"符号"的小型显示处理器例程,这些例程可以组装成更大的例程来绘制管道,坦克,阀门等图形.我们希望图表是"动态的",因为他们可以逐步更新自己,而无需重绘整个图表.(按照今天的标准,硬件很慢.)我意识到(例如)绘制条形图条的例程可以记住它的旧高度并且只是逐步更新自身.
这听起来像OOP,不是吗?但是,我可以利用图表过程的执行顺序的可预测性,而不是"制造"一个"对象".我可以在一个连续的字节流中写出条形高度.然后,为了更新图像,我可以在一种模式下运行该过程,在该模式下,它在写入新参数时顺序读取其旧参数,以便为下一次更新传递做好准备.
这似乎是非常明显的,并且一旦过程包含条件,似乎就会中断,因为那时新流和旧流将不同步.但后来我突然意识到,如果他们也序列化条件测试的布尔值,他们可以恢复同步.花了一段时间才说服自己,然后证明,如果遵循一个简单的规则("擦除模式规则"),这将始终有效.
最终结果是用户可以设计这些"动态符号"并将它们组装成更大的图表,而无需担心它们如何动态更新,无论显示器的复杂性或结构变化如何.
在那些日子里,我确实不得不担心视觉对象之间的干扰,因此擦除它不会损害其他对象.但是,现在我使用Windows控件的技术,我让Windows负责渲染问题.
那么它实现了什么?这意味着我可以通过编写一个绘制控件的过程来构建一个对话框,我不必担心实际记住控件对象或处理逐步更新它们,或者让它们在条件允许时出现/消失/移动.结果是更小和更简单的对话框源代码,大约一个数量级,并且诸如动态布局或改变控件的数量或具有控件的阵列或网格之类的事情是微不足道的.此外,诸如编辑字段之类的控件可以简单地绑定到它正在编辑的应用程序数据,并且它总是可证明是正确的,并且我永远不必处理它的事件.将应用程序字符串变量放在编辑字段中是一行编辑.
为什么难以理解?
我发现最难解释的是,它需要对软件进行不同的思考.程序员如此坚定地坚持软件的对象 - 动作视图,他们想知道什么是对象,什么是类,他们如何"构建"显示,以及他们如何处理事件,它需要一个樱桃炸弹炸出它们.我试图传达的是,真正重要的是你需要说什么?想象一下,你正在构建一个特定于域的语言(DSL),你需要做的就是告诉它"我想在这里编辑变量A,变量B在那里,变量C在那里",它会神奇地为你处理它.例如,在Win32中有用于定义对话框的"资源语言".它是一个非常好的DSL,除了它还不够远.它不会"存在"主要的过程语言,或为您处理事件,或包含循环/条件/子例程.但这意味着很好,动态对话试图完成这项工作.
因此,不同的思维模式是:编写程序,首先找到(或发明)适当的DSL,并尽可能多地编写程序代码.让它处理所有的对象和动作只存在于执行的缘故.
如果你想真正了解差异执行并使用它,那么有一些棘手的问题可能会让你失望.我曾经用Lisp宏编写它,可以为你处理这些棘手的位,但在"普通"语言中,它需要一些程序员纪律来避免陷阱.
很抱歉这么啰嗦.如果我没有意义,如果你指出它并且我可以尝试修复它,我会很感激.
添加:
在Java Swing中,有一个名为TextInputDemo的示例程序.它是一个静态对话框,占用270行(不包括50个状态列表).在动态对话框中(在MFC中)它大约有60行:
#define NSTATE (sizeof(states)/sizeof(states[0])) CString sStreet; CString sCity; int iState; CString sZip; CString sWholeAddress; void SetAddress(){ CString sTemp = states[iState]; int len = sTemp.GetLength(); sWholeAddress.Format("%s\r\n%s %s %s", sStreet, sCity, sTemp.Mid(len-3, 2), sZip); } void ClearAddress(){ sWholeAddress = sStreet = sCity = sZip = ""; } void CDDDemoDlg::deContentsTextInputDemo(){ int gy0 = P(gy); P(www = Width()*2/3); deStartHorizontal(); deStatic(100, 20, "Street Address:"); deEdit(www - 100, 20, &sStreet); deEndHorizontal(20); deStartHorizontal(); deStatic(100, 20, "City:"); deEdit(www - 100, 20, &sCity); deEndHorizontal(20); deStartHorizontal(); deStatic(100, 20, "State:"); deStatic(www - 100 - 20 - 20, 20, states[iState]); if (deButton(20, 20, "<")){ iState = (iState+NSTATE - 1) % NSTATE; DD_THROW; } if (deButton(20, 20, ">")){ iState = (iState+NSTATE + 1) % NSTATE; DD_THROW; } deEndHorizontal(20); deStartHorizontal(); deStatic(100, 20, "Zip:"); deEdit(www - 100, 20, &sZip); deEndHorizontal(20); deStartHorizontal(); P(gx += 100); if (deButton((www-100)/2, 20, "Set Address")){ SetAddress(); DD_THROW; } if (deButton((www-100)/2, 20, "Clear Address")){ ClearAddress(); DD_THROW; } deEndHorizontal(20); P((gx = www, gy = gy0)); deStatic(P(Width() - gx), 20*5, (sWholeAddress != "" ? sWholeAddress : "No address set.")); }
添加:
这是用于编辑大约40行代码中的医院患者阵列的示例代码.第1-6行定义"数据库".第10-23行定义了UI的整体内容.第30-48行定义了用于编辑单个患者记录的控件.请注意,程序的形式几乎不会及时发现事件,就好像它只需要创建一次显示一样.然后,如果添加或删除主题或发生其他结构更改,则只需重新执行,就好像它是从头重新创建一样,除了DE导致增量更新发生.优点是程序员不必给予任何关注或编写任何代码来使UI的增量更新发生,并保证它们是正确的.看起来这种重新执行可能是一个性能问题,但事实并非如此,
1 class Patient {public: 2 String name; 3 double age; 4 bool smoker; // smoker only relevant if age >= 50 5 }; 6 vector< Patient* > patients; 10 void deContents(){ int i; 11 // First, have a label 12 deLabel(200, 20, “Patient name, age, smoker:”); 13 // For each patient, have a row of controls 14 FOR(i=0, iname)); 42 deEdit(w, 20, P(&p->age)); 43 // If age >= 50 have a checkbox for smoker boolean 44 IF(p->age >= 50) 45 deCheckBox(w, 20, “Smoker?”, P(&p->smoker)); 46 END 47 deEndHorizontal(20); 48 }
补充:Brian问了一个很好的问题,我认为答案属于正文:
@Mike:我不清楚"if(deButton(50,20,"Add")){"声明实际上在做什么.deButton函数有什么作用?你的FOR/END循环是使用某种宏还是什么? - 布莱恩
@Brian:是的,FOR/END和IF语句都是宏.SourceForge项目有一个完整的实现.deButton维护一个按钮控件.当发生任何用户输入操作时,代码以"控制事件"模式运行,其中deButton检测到它被按下并表示通过返回TRUE按下它.因此,"if(deButton(...)){...动作代码...}是一种将动作代码附加到按钮的方式,而不必创建闭包或编写事件处理程序.DD_THROW是一个终止通时所采取的行动,因为行动可能已经修改应用程序的数据,所以它是无效的继续"控制事件"的方式通过该程序.如果你比较这对编写事件处理程序,这样可以节省你写这些,它可以让你拥有任意数量的控件.
补充:对不起,我应该用"维护"这个词来解释我的意思.首次执行该过程时(在SHOW模式下),deButton会创建一个按钮控件并在FIFO中记住它的id.在后续传递中(在UPDATE模式下),deButton从FIFO获取id,必要时修改它,并将其放回FIFO中.在ERASE模式下,它从FIFO中读取它,销毁它,并且不会将其取回,从而"垃圾收集"它.所以deButton调用管理控件的整个生命周期,使其与应用程序数据保持一致,这就是为什么我说它"维护"它.
第四种模式是EVENT(或CONTROL).当用户键入字符或单击按钮时,将捕获并记录该事件,然后在EVENT模式下执行deContents过程.deButton从FIFO中获取其按钮控件的id,并询问这是否是单击的控件.如果是,则返回TRUE,以便执行操作代码.如果没有,它只返回FALSE.另一方面,deEdit(..., &myStringVar)
检测事件是否适合它,如果是,则将其传递给编辑控件,然后将编辑控件的内容复制到myStringVar.在此UPDATE处理和正常UPDATE处理之间,myStringVar始终等于编辑控件的内容.这就是"绑定"的方式.同样的想法适用于滚动条,列表框,组合框,任何允许您编辑应用程序数据的控件.
这是我维基百科编辑的链接:http://en.wikipedia.org/wiki/User : MikeDunlavey/Difex_Article
差异执行是一种基于外部事件更改代码流的策略.这通常通过操纵某种数据结构来记录变化来完成.这主要用于图形用户界面,但也用于序列化等内容,您可以将更改合并到现有的"状态"中.
基本流程如下:
Start loop: for each element in the datastructure: if element has changed from oldDatastructure: copy element from datastructure to oldDatastructure execute corresponding subroutine (display the new button in your GUI, for example) End loop: Allow the states of the datastructure to change (such as having the user do some input in the GUI)
这样做的好处很少.一,它是您的更改执行的分离,以及支持数据的实际操作.这对多处理器来说很好.二,它提供了一种低带宽的方法来传达程序中的变化.
想想监视器的工作原理:
它的更新频率为60 Hz - 每秒60次.闪烁闪烁闪烁60次,但你的眼睛很慢,不能真正告诉.监视器显示输出缓冲区中的内容; 无论你做什么,它每隔1/60秒就会拖拽这些数据.
现在为什么你希望你的程序每秒更新整个缓冲区60次,如果图像不经常改变?如果你只改变图像的一个像素,你应该重写整个缓冲区怎么办?
这是基本思想的抽象:您希望根据您希望在屏幕上显示的信息来更改输出缓冲区.您希望尽可能多地节省CPU时间和缓冲区写入时间,因此您不需要编辑缓冲区中不需要为下一次屏幕提取而更改的部分.
监视器与计算机和逻辑(程序)分开.它以任何更新屏幕的速率从输出缓冲区读取.我们希望我们的计算机不必要地停止同步和重绘.我们可以通过改变缓冲区的工作方式来解决这个问题,这可以通过各种方式完成.他的技术实现了一个延迟的FIFO队列 - 它保存了我们刚发送到缓冲区的内容.延迟的FIFO队列不保存像素数据,它保存"形状基元"(可能是应用程序中的像素,但它也可能是线条,矩形,易于绘制的东西,因为它们只是形状,没有不必要的数据是允许).
所以你想从屏幕上绘制/删除东西?没问题.根据FIFO队列的内容,我知道显示器目前的样子.我将我想要的输出(擦除或绘制新的基元)与FIFO队列进行比较,只更改需要更改/更新的值.这是给它起名称差异评估的步骤.
我欣赏这两种截然不同的方式:
第一个: Mike Dunlavey使用条件语句扩展.FIFO队列包含大量信息("先前状态"或监视器或基于时间的轮询设备上的当前内容).您需要添加的就是您希望在屏幕上显示的状态.
将条件位添加到可以在FIFO队列中保存基元的每个槽.
0 means erase 1 means draw
但是,我们有以前的状态:
Was 0, now 0: don't do anything; Was 0, now 1: add it to the buffer (draw it); Was 1, now 1: don't do anything; Was 1, now 0: erase it from the buffer (erase it from the screen);
这很优雅,因为当您更新某些内容时,您实际上只需要知道要绘制到屏幕的基元 - 这种比较将确定它是否应该擦除基元或将其添加/保留在缓冲区中.
第二个: 这只是一个例子,我认为迈克所做的事情应该是所有项目设计的基础:通过将计算量最大的操作编写为计算机脑食品,降低设计的复杂性(计算)或尽可能接近.尊重设备的自然时间.
绘制整个屏幕的重绘方法非常昂贵,而且还有其他应用程序,这种洞察力非常有价值.
我们永远不会在屏幕上"移动"物体.如果我们在设计像计算机显示器这样的代码时模仿"移动"的物理动作,"移动"是一项代价高昂的操作.相反,对象基本上只是用显示器闪烁.每当一个物体移动时,它现在是一组新的原语,旧的原语组闪烁.
每次监视器从缓冲区拉出时,我们都有类似的条目
Draw bit primitive_description 0 Rect(0,0,5,5); 1 Circ(0,0,2); 1 Line(0,1,2,5);
对象永远不会与屏幕(或时间敏感的轮询设备)交互.当它贪婪地要求更新整个屏幕只显示仅针对自身的更改时,我们可以比对象更智能地处理它.
假设我们有一个列表,列出了我们的程序能够生成的所有可能的图形基元,并且我们将每个基元绑定到一组条件语句
if (iWantGreenCircle && iWantBigCircle && iWantOutlineOnMyCircle) ...
当然,这是一个抽象,实际上,代表特定基元开启/关闭的条件集可能很大(可能有数百个标志必须全部评估为真).
如果我们运行程序,我们可以以与我们评估所有这些条件的速率基本相同的速率绘制到屏幕.(最坏情况:评估最大条件语句集需要多长时间.)
现在,对于程序中的任何状态,我们可以简单地评估所有条件并快速输出到屏幕!(我们知道我们的形状基元及其相关的if语句.)
这就像买一个图形密集的游戏.只有将其安装到您的硬盘驱动器并通过您的处理器运行它,您才会购买一个全新的电路板来保存整个游戏并将其作为输入:鼠标,键盘和输出:监视器.令人难以置信的浓缩条件评估(作为条件的最基本形式是电路板上的逻辑门).这自然会非常敏感,但它几乎不提供修复错误的支持,因为当你进行微小的设计更改时整个电路板设计会发生变化(因为"设计"与电路板的性质相差甚远) ).以我们在内部表示数据的灵活性和清晰度为代价,我们获得了显着的"响应能力",因为我们不再在计算机中"思考"; 用于基于输入的电路板.
根据我的理解,这一课是分工,以便你给系统的每个部分(不一定只是计算机和监视器),它可以做得很好."计算机思维"可以用对象这样的概念来完成......计算机大脑很乐意尝试为你考虑这一点,但是如果你能让计算机进行思考,你可以大大简化任务. data_update和conditional_evals的术语.我们将人类抽象的概念转化为代码是理想主义的,并且在内部程序绘制方法的情况下有点过于理想化.当您想要的只是一个结果(具有正确颜色值的像素数组)并且您拥有一台可以轻松实现的机器 吐出一个每1/60秒大的数组,尝试从计算机大脑中消除尽可能多的花哨思维,这样你就可以专注于你真正想要的东西:将你的图形更新与你的(快速)输入同步显示器的自然行为.
这如何映射到其他应用程序? 我想听听其他例子,但我确信有很多例子.我认为任何为您的信息状态提供实时"窗口"的东西(可变状态或类似数据库......监视器只是进入显示缓冲区的窗口)都可以从这些见解中受益.