Atry
百度空间 | 百度首页 
 
文章列表
 
2009年11月15日 星期日 22:30
我们团队一直在用 GNU Make ,我们老大云风尤其把 make 用得十分高级。但使用过程中,碰到一些问题确实比较讨厌:
  1. 斜杠的处理
  2. 参见 http://blog.codingnow.com/2009/03/gnu_make_backslash.html
  3. 递归目录
  4. 参见 http://blog.codingnow.com/2009/06/make_recursion_directory.html,这个用到了神技(eval)
  5. 自动创建目录
  6. 参见 http://blog.codingnow.com/2009/07/gnu_make_mkdir.html,这个也用到了神技。
  7. 包含空格的文件名的处理
  8. 据高人说,可以在调用 foreach 等内置函数以前先把空格替换成别的字符,再在作为文件名使用时把空格替换为 。但这个做法等于是自己定义一种转义规则了。
  9. shell的依赖性
  10. make 的命令行都是调用操作系统的 shell ,这样会带来移植性问题。就连基本的文件操作都需要定义很多环境变量来处理 cp / copy cat / type 之类的差异。不同操作系统的命令行转义规则也各不相同,如果命令行参数中出现特殊字符,要想在不同的 shell 下都正确转义难度极大。
  11. 算术运算
  12. 比如我想要从 1 循环到 10,声明 frame_1.png 依赖于 frame_1.tga,frame_2.png 依赖于 frame_2.tga,frame_3.png 依赖于 frame_3.tga …… frame_10.png 依赖于 frame_10.tga。这样的事情在 C 里面就是 for (i = 0; i < 10; ++i) 即可。在 make 中, for 倒是可以用递归来模拟,但 make 里面除非求助于 shell 就没有办法能做 ++i 这个事情。
  13. 子模块依赖关系
  14. 这个是 make 的死穴。如果一个项目中有多个模块,模块 a 依赖于模块 b 的输出文件 libb.a,那应该怎么写依赖关系呢?一般有两种做法
    • 不同的模块使用独立的 Makefile ,a 模块依赖于 libb.a,如果 libb.a 不存在,就调用 b 模块的 make 来创建 libb.a,大概代码如下:
      ../b/libb.a:
      cd ../b && $(MAKE)

      a.exe: ../b/libb.a xxx.c yyy.c xxx.h
      gcc ……
    • 这样做的问题在于如果模块 b 中的某个源文件修改了,在 a 模块下 make 时, libb.a 不能自动重新编译,因而 a 模块也不会重新编译
    • 用 include 之类的办法把所有模块的 Makefile 统一起来,这是目前我们做的方式。缺点在于:
      • 编写子模块的 Makefile 时需要知道别的模块的信息,破坏了 Makefile 的局部性
      • 为了解决这个问题,云风想的办法是自己定义了一套语法,在子模块 make 时,通通调用根一级的 Makefile ,只是把子模块的信息通过环境变量传进去。但这样带来了另一个缺点,即自定义的语法有学习成本,和直接的 make 语法相比,维护起来要稍微困难一些。
      • 即使只构建一个子模块,也需要生成整个项目的依赖图
      • 尤其当文件很多的时候,生成整个依赖图会比较慢。这个问题在 bjam 中也存在。因为我们现在的做法就相当于用 make 实现了一套 bjam ,所以我们也会面临 bjam 的一些问题。

当一个项目构建过程比较复杂的时候,可以考虑用通用语言来管理构建过程。对于通用语言来说,处理字符串转义、算术运算、循环遍历等事情易如反掌,无 需使用 make 那些神奇而又晦涩的神技。而跨平台的通用语言的文件系统库往往也比 shell 有更好的移植性。至于子模块依赖问题,对于支持 closure 、 coroutine 的语言来说,很容易就能实现依赖关系的惰性计算,只需要生成整个依赖图中于构建目标相关的那部分,速度就快多了。

我在做的一个项目的构建过程就十分复杂,需要把一些配置文件从一种格式转成另一种格式,然后再嵌入主程序中;用到的美术素材也需要转格式。这些过程 需要用到好几种转换工具,有一些转换工具就直接要用 lua 编写。我原先是用 GNU Make 来做这件事情的,把各种神奇的特性都用上了,最后碰到空格文件名的时候,还是放弃了,决心自己用 lua 实现一套构建系统,我把它叫做 lua-make

因为编译器、转换工具都是命令行工具,所以这个构建系统碰到的第一个问题是要能启动进程。lua 标准库中启动进程有两个函数,io.popen 和 os.execute 。其中 io.popen 不支持 Windows。而 os.execute 要依赖于操作系统的 shell ,移植性不佳比如,在 bash 中,我们可以写这样的代码:

cat 111.txt | a/b/foo 'xxxxx' | bar 'yyyyy' 'aaa' 2> out/err > out/log

如果要用 lua 来启动,就是

os.execute[[cat 111.txt | a/b/foo 'xxxxx' | bar 'yyyyy' 'aaa' 2> out/err > out/log]]

可是上述代码在 Windows 中就跑不通了。这是因为 Windows 的 shell 是 cmd ,语法不同,字符串转义规则不同,目录分隔符也不同。

归根到底, os.execute 是不可移植的。所以我用了 Lua-Ex 。Lua-Ex 提供了创建管道的函数(ex.pipe),启动进程并重定向标准输入输出的函数(ex.spawn),刚好适合做构建系统。不过,pipe 和 spawn 是比较底层的函数,要想简单的完成一串管道的命令,还需要一些语法糖。

我做了一个 path 模块和一个 shell 模块来做实现这个语法糖。先前的 bash 表达式,用我提供的语法糖来写的话就是这样:

"111.txt" / path["a/b/foo"]("xxxxx") / path["bar"]('yyyyy', 'aaa') % path["out/err"] / path["out/log"]

上述表达式中,运算符/用来重定向标准输出,相当于 bash 中的|<>;运算符%用来重定向标准错误,相当于 bash 中的2>

path["out/log"] 是一个路径对象。我重载了路径对象metatable中的 __div ,所以也可写成 path["out"] / path["log"] 或者 path.out / path.log 又或者 path.out / "log" 。如果用 setfenv 把当前执行环境设成 path ,甚至可以直接写成 out/log 。

路径的比较也十分简单,我能保证同一个路径在内存中只有一份,所以 path["x/b"] 、 path["x"] / b 、 path["x/a/../b"] 、 path["x\\b"] 都是同一个对象。

我也重载了路径对象metatable中的 __call ,当它被调用的时候就会被当成可执行文件来执行。这就是为什么上述代码可以直接写path["bar"]('yyyyy', 'aaa') 的缘故。

对于一个构建系统来说,核心部分是目标的依赖关系管理。这方面,我主要是造搬 GNU Make 的模型,只是语法用 lua 的表来描述。

这 里我碰到一个问题,就是多任务的并行执行。make 有一个 -j 选项,可以并行执行多个任务。用 lua 怎么实现类似的功能呢?用 ex.spawn 可以创建进程,用 ex.wait 可以等待一个进程退出,但是如果同时启动了多个进程,怎样才能等待其中任意一个退出呢?Windows 中有一个 WaitForMultiObjects 可以做这件事情,但是一方面这不可移植,另一方面我也不想为了这一个功能而多写一个 lua C库。我用的办法是在主进程调用 ex.pipe 创建一个 pipe,为需要监视的进程创建一个辅助进程来监视,辅助进程等待被监视的进程退出,退出时向 pipe 发一个消息。所以主进程之需要读这个 pipe 就可以知道任意一个进程退出了。我把这个监视模块叫做 shell.watchdog 。

最终,要使用这个构建系统,大概是这样来写Makefile:

require "shell"
require "path"
require "shell.watchdog"
require "make"
require "make.filetarget"

local watchdog = shell.watchdog()

make(10, make.filetarget {
path = "xxxx.exe",
dependencies = {
{
run = function ()
print("这是一个伪目标")
end
},
make.filetarget {
path = "xxxx.c"
},
make.filetarget {
path = "xxxx.h"
}
}
run = watchdog + path.gcc("-o", "xxxx.exe", "xxxx.c")
})

watchdog:wait()

make 函数的第一个参数指最多允许多少个并行任务的意思,这里传 10,就相当于GNU Make 的 -j 10

make.filetarget 是说这是一个文件目标,而不是一个伪目标(即 GNU Make 的 .PHONY)。文件目标和伪目标相比,要增加判断文件修改时间、自动创建父级目录等功能。在我实现的构建系统中,一个目标默认是没有这些功能的,如果需 要这些功能就得写 make.filetarget {......}

这里的watchdog +是一个语法糖,意思是要把接下来的这一段 shell 表达式加入到 watchdog 这个监视对象中去监视。

大体上我就是这样实现 Lua 构建系统的,欢迎回帖讨论

 
2009年10月16日 星期五 09:19

用ActionScript的时间长了,实在是不能容忍里面还有我不认识的API。昨天终于搞清楚trackAsMenu的用处了,于是AS里面我不认识的API又少了一个。

在 Flash创作工具中文版的界面上,这个trackAsMenu被翻译为“音轨作为菜单”,看到这几个字,我眼睛湿润了。trackAsMenu的意思应 该是“以菜单的方式追踪鼠标”。这个选项在ActionScript3里面是SimpleButton和MovieClip上的属性,如果打开这个选项, 有两个效果:

  1. 如果在别的对象上按下鼠标,然后不松开鼠标,移到一个trackAsMenu为true的对象上,再松开鼠标的话。这个trackAsMenu为 true的对象就会收到MouseEvent.CLICK事件。而如果把trackAsMenu设为false的话,这个click事件是收不到的。
  2. 如 果在一个trackAsMenu为true的对象上按下鼠标,然后不松开鼠标,移到另一个trackAsMenu为true的按钮上,进入的这个按钮会显 示overState的图像。但如果是移到另一个trackAsMenu为flase的按钮上,则那个按钮还是显示upState的图像。

所以,这个功能用来制作菜单时会有用。只要把各个菜单项都设上trackAsMenu属性,原先按下鼠标时的那个对象以外的对象也可以触发click事件了。

 
2009年10月15日 星期四 21:48

Windows命令行参数的转义分为两个步骤,第一个步骤是cmd解析你输入的字符串决定要启动哪些进程,进程之间是否要标准输入输出重定向等等;第二个步骤是CRT把传入进程的命令行字符串切成多个参数,填好argc和argv传入main。

如果一个程序不是被cmd启动的,而是通过快捷方式或者bash等别的shell启动的,或者干脆直接调用API CreateProcess,那么第一个步骤可以无视;如果一个程序并不使用argc、argv,而自行调用GetCommandLine等Windows API,则第二个步骤可以无视。

第一个步骤中,| ^ " &等字符是特殊字符。如果特殊字符出现在双引号外,需要被转义。但如果出现在双引号中,就不需要也不能被转义。转义时,^^代表^,^|代表|,^"代表"等等

第二个步骤的具体实现取决于CRT,在Windows XP中用得最广泛的CRT是msvcrt.dll。Windows XP附带的系统程序大都使用了它,用VC6或mingw的gcc编译的程序也使用这个CRT。我看了一下VC6附带的源码,切参数这件事情的规则相当复杂。

首先是空格和tab如果不在双引号,就会被当作参数的分隔符,而双引号引住的空格和tab代表空格tab字符本身。

其次是连写双引号的特殊处理。如果想要把双引号作为参数内的字符,那么至少要连写两个双引号:""。当他遇见连续的两个双引号时,他会把其中一个双引号作为双引号字符,另一个双引号作为引用范围的开始或结束。也就是说,如果你已经在引号中了,连写2个双引号的话,就会产生一个"字符,并且关闭引用。要想产生一个"字符但不改变引用状态,就需要连写三个双引号"""。

最变态的是用反斜杠(即\)转义的规则。对于n个反斜杠外加一个双引号会被转义。如果n为奇数,那么会被转义成(n-1)/2个\字符外加一个"字符。如果n为偶数,那么会被转义成n/2个\字符,然后再进入或结束引用范围。而如果反斜杠后面并不跟随双引号,反斜杠就不转义。

值得注意的是,因为cmd和CRT对双引号字符的转义规则并不一致,所以cmd和CRT对于某个字符是否被引用的判断也是不一致的。比如说,如果你想进行一次Subversion提交,注释中写上

hello " | " |   " | "  world
,那你得写:
svn commit -m "hello """ ^| """ |   """ ^| """  world"

注意,其中某些|必须要转义,而另一些则必须不转义。

上面说的还仅仅是msvcrt.dll切参数的规则,这个规则和cygwin的CRT是不一致的,也就是说,如果你在cmd中要调用cygwin编译的程序,那么转义规则又不一样。幸好这种情况并不是很多,用cygwin的话,就用bash好了,至少全世界的bash解析参数的方式都一样。

 
2009年09月02日 星期三 12:56

第一个原因是如果新写的代码改用haXe,新写的代码依赖于部分AS代码,而另一部分AS代码又会依赖于haXe,那么就会导致循环依赖。虽然有办 法可以解决,但是每种解决方案都有代价,要么是把现有AS代码拆成两部分来编译,要么是用 ApplicationDomain.getDefinition之类的字符串解耦合办法。这些办法都不是我想要的。

第二个原因在于haXe用的全局变量flash.Lib.current. 把一段haXe代码编译成SWC或者SWF,分别作为静态库和动态库使用时,会有微妙的差异(参见http://haxe.org/doc/advanced/swc)。 而我如果想要在开发期间缩短编译时间,必须要把haXe部分编译成动态链接的SWF。这个问题体现了“全局变量的邪恶性”。全局变量之所以邪恶在于其“全 局”概念的不可控,在模块角度看是全局的东西,到了装载器的角度来看就有时候是而有时候不是了,就会导致行为不一致。

 
2009年08月30日 星期日 21:44

主流的flash开发工具当然是Flex Builder/Flex SDK/Flash创作工具。一般用Flash创作工具生成素材,用Flex SDK来编译代码,喜欢IDE环境的就用用Flex Builder。但除此之外,还有一些非主流的开发工具选择。

现在能生成flash的编译器已经有很多了,真的不必拘泥于龟一般的mxmlc. 前一篇文章,我介绍了haXe。除此之外,另一个选择是swftools中的as3compile,这个编译器号称是要兼容mxmlc,不过就我目前测试的情况来看,它有一个缺陷,即不支持闭包中访问upvalue。

当然我不认为mxmlc慢是因为它用了Java,javac也是Java写的,快得很呢。

由于AVM开源了,还可以用AVM开源后提供的编译器,AVM开源后的项目名叫做Tamarin,源码在这里下载。 这是一个C++编写的编译器,现在想尝尝鲜的话,可以找到Tamarin里面的utils/avmc,里面就是这个编译器。不过avmc编译出来的目标文 件是.abc,还需要包装上swf的文件头才能用,这件事情可以用前面提到的as3compile来做。比较头疼的问题在于如何引用 flashplayer的API,这些API在flex_sdk_3/frameworks/libs/player/playerglobal.swc 里面是有,要把这个playerglobal.swc导出成.abc我没试过,但应该不难。

以上是一些编译器,至于素材的制作,可以考虑swftoolsswfc。和官方的Flash创作工具相比,它的优势在于源文件.sc格式是一个文本格式,工具又是命令行工具,因此易于用脚本生成代码。缺点有两个,首先是缺乏“所见即所得”编辑器,其次是一些功能缺失,比如向ActionScript导出元件。用swfc来打包素材已经足用了,我就用swfc来把一些公司自定格式的动画素材批量转换成swf格式。

 
2009年08月30日 星期日 21:14

其实我早就听说过haXe。那是大概两年前,mtasc停止开发,转而开发haXe. 当时我没怎么注意,直到现在对mxmlc的编译速度忍无可忍的时候,才偶然发现改用haXe也许是个解决办法。

haXe主页上强调这是一种“multiplatform language”的语言,可以编译成JavaScript、Flash、NeckoVM(这个没听说过)、PHP、C++。不过这些我不关心,我只关心它能编译成Flash.

从haXe的语言特性来看,这是一种大路货的面向对象语言,跟Java、C#、ActionScript3都差不多。对我而言,haXe比ActionScript3好的地方有:

  1. haXe编译速度奇快,比mxmlc快得多,这应该是选用它的最大动机。
  2. 更完备的静态类型系统。比如支持泛型,此外,其函数类型也是强类型。这解决了ActionScript3语言陷阱和缺陷一文提到的第三个缺陷
  3. 支持类型推断,不用每个变量都写类型。
  4. 局部变量的范围是某个区块而不是整个函数。这解决了ActionScript3语言陷阱和缺陷一文提到的第四个缺陷
  5. 支持枚举

haXe比ActionScript3差的地方有:

  1. 不支持自由函数,因而函数必须放在类中。flash.utils.setTimeout等自由函数也就不能直接调用了。
  2. 没有const关键字。

从ActionScript3迁移到haXe时还需要考虑的事情包括:

  1. 调试。根据我测试,在haXe 2.x中只需要加上 -debug -D fdb 参数就可以使用Flex SDK中的fdb来调试了。
  2. 动 态加载。这个说来话长,得另写一篇文章来说这个问题了。简单的说一下要点吧。要使用 haxe --gen-hx-classes 来生成头文件以便在编译时找到符号,然后在运行时加载动态库。运行时加载要指定传一个ApplicationDomain.currentDomain进 去。
  3. 如果想要和现有的swf编译到一起,需要用-swf-lib把已有的swf捆进来。此外,调用其中已有的AS代码则还需要 --gen-hx-classes生成的头文件。AS要调用haXe代码的话,则需要haxe -swf9 xxx.swc来生成一个swc文件让AS能通过这个文件找到符号
  4. 要编译一般在网上用的swf,需要加入-D network-sandbox参数。
  5. haXe的学习成本。对我而言,学习成本很低,花费一天时间足够搞懂。再次强调,这是一种大路货语言。语法和ActionScript区别不大,就算有些犄角旮旯之处不同,遇到时也很容易解决。比较讨厌的一个语法在于每行代码都必须加上分号。
 
2009年08月14日 星期五 11:50
  1. 闭包会产生多余的引用

  2. ActionScirpt3的闭包会引用外层函数的所有局部变量,而不仅仅只是用到的变量。这些多余的引用是不必要的。它们会使垃圾收集器认为这些数据仍然被引用着而不去释放,哪怕这些数据本来已经不可能再被用到。例如,以下代码会导致内存泄露:
    package {
    import flash.display.*
    public final class C extends Sprite {
    private static function ref(f:Function):Function {
    return function():void {
    }
    }
    private var f:Function = function():void{}
    public function C() {
    for(var i:uint = 0; i < 1000000; i++) {
    f = ref(f)
    }
    }
    }
    }
  3. 与JavaScript的异同

  4. ActionScript2和JavaScript一致的地方更多些,而ActionScript3则少一些,这样会迷惑很多用惯JavaScript的人,例如:
    http://www.ac.net.blog.163.com/blog/static/1364905620060274329791/
    http://www.ac.net.blog.163.com/blog/static/1364905620060297425813/
    还有一些区别在ASDoc中也提到了,比如=== 和 == 用在Number、uint、String上时,AS2和AS3行为是不同的。
  5. 编译器不检查函数返回值类型

  6. 例如,以下代码可以通过编译。
    function xxx():uint {
    return 0
    }
    var a:Array = xxx()
  7. 局部变量的范围是整个函数而不是某个区块

  8. 例如以下代码会出错:
    function f(b:Boolean):void {
    if(b) {
    var a:String = "a"
    } else {
    var a:Array = []
    }
    }
    这一点对于闭包的使用来说,很麻烦,例如
    var a:Array = []
    for(var i:uint = 0; i < 10; i++) {
    a[i] = function():uint {
    return i
    }
    }
    trace(a[3]()) // 输出10而不是3
    上述代码会trace出 10 而不是 3
  9. const并不真的不可改变

  10. 更有甚者,即使你使用 const 关键字声明一个常量,实际上它还是会被改变:
    var a:Array = []
    for(var i:uint = 0; i < 10; i++) {
      const c:uint = i
    a[i] = function():uint {
    return c
    }
    }
    trace(a[3]()) // 还是输出10而不是3
  11. setTimeout不自动释放对闭包的引用

  12. ASDoc中的的文档有下面这句话:
    If you intend to use the clearTimeout() method to cancel the setTimeout() call, be sure to assign the setTimeout() call to a variable (which the clearTimeout() function will later reference). If you do not call the clearTimeout() function to cancel the setTimeout() call, the object containing the set timeout closure function will not be garbage collected.
    意思是说,如果是说可以用clearTimeout来取消setTimeout设置的定时器。让人觉得言下之意是,如果不想取消定时器,不调用 clearTimeout也可以。实际上,如果不调用clearTimeout,就会导致内存泄露。setTimeout并不自动释放对闭包的引用。
 
2009年08月12日 星期三 09:47
翻译后记 2007年的时候,我关注过D语言,甚至还用它来写过一个东西。当时D社区里面讨论得比较激烈的特性就是final/const/invariant这三种数据类型。那时候我就觉得D 2.0的这三个东西会很有用。
过了两年,突然发现这些东西已经改得面目全非,invariant已经改名叫做immutable了,而final数据类型竟然被砍掉了。我在D的论坛上泡了半天也没搞清楚前因后果。所以我翻译了这篇文章,可能会对被D语言这些常量所迷惑的同学有帮助。原文地址:http://www.digitalmars.com/d/2.0/const3.html

对数据结构或接口来说,如果能轻易地分辨出哪些数据不会改变,哪些数据可能会改变,以及谁可能改变这些数据,就会非常有用。以上可借助语言的类型系统做到。数据可以被标记为const或immutable,而其默认是可变的(即mutable ) 。

不 可改变的数据可以用immutable表示。immutable数据构建之后,在程序整个运行期间都会保持原值。immutable数据可以放在 ROM(只读存储器)中或者由硬件设为只读的内存中。由于immutable数据不会改变,所以可以得到许多程序优化的机会,还可便于采用函数式编程。

const适用于不能通过引用本身改变数据的引用。不过,这块数据还时可能被其他引用所改变。若向接口传入数据时需要保证该接口中不修改该数据,则应采用const。

immutable和const都是transtive(具有可传递性),这意味着通过immutable引用取得的数据同样也是immutable的,const亦是如此。

immutable存储类

immutable最简单的用法是用作存储类。可以用它来声明明确的常量。

immutable int x = 3; // 将x设为3
x = 4; // 错误,x不可改变
char[x] s; // s 是一个包含三个char的数组

数据类型也可以由初始化表达式推断:

immutable y = 4; // y的类型是int
y = 5; // 错误,y不可改变

如果不写初始化表达式,相应的构造函数也可以初始化immutable数据:

immutable int z;
void test()
{
z = 3; // 错误,z不可改变
}
static this()
{
z = 3; // 正确,没有静态初始化表达式的immutable数据可以赋值
}

除了函数类的immutable数据外,其他immutable声明的初始化表达式都必须能在编译时求值:

int foo(int f) { return f * 3; }
int i = 5;
immutable x = 3 * 4; // 正确,12
immutable y = i + 1; // 错误,无法在编译时求值
immutable z = foo(2) + 1; // 正确,foo(2)能在编译时求得值——7

非静态的局部immutable数据的初始化表达式是在运行时计算的:

int foo(int f)
{
immutable x = f + 1; // 运行时求值
x = 3; // 错误,x不可改变
}

由于immutable数据具有可传递性,由immutable数据引用的数据也是不变的:

immutable char[] s = "foo";
s[0] = 'a'; // 错误,s指向一块不可改变的数据
s = "bar"; // 错误,s不可改变

immutable声明可以作为左值出现,比如它们的地址都是可以获取到的,再如它们会占据存储空间。

const存储类

除了以下区别之外,以const声明的数据和immutable的完全相同:

  • 不能通过const声明来修改它所引用的数据,但这块数据还是可能被指向该数据的其他引用所修改。
  • const声明自身也是const的。

immutable类型

一定不会被改变的数据可以标记为immutable。immutable关键字可以被用作type constructor:

immutable(char)[] s = "hello";

括号中的类型被标记为immutable。因此,一方面,s能被设为新的值,另一方面,s[]的内容却不能被改变:

s[0] = 'b';  // 错误,s[]不可改变
s = null; // 正确,s自身并不是immutable的

immutable具有可传递性,这意味着被immutable类型引用的数据也是immutable的:

immutable(char*)** p = ...;
p = ...; // 正确,p可以改变
*p = ...; // 正确,*p可以改变
**p = ...; // 错误,**p不可改变
***p = ...; // 错误,***p不可改变

把immutable用作存储类相当于把immutable用作整个声明类型的type constructor:

immutable int x = 3;   // x的类型是immutable(int)
immutable(int) y = 3; // y不可改变

创建不变数据

第一种方式:使用本来就不变的字面量(literal),比如字符串字面量。字符串字面量始终都是immutable的。

auto s = "hello";   // s的类型是immutable(char)[5]
char[] p = "world"; // 错误,immutable类型不能隐式转换为非immutable类型。

第二种方式是把数据转换为immutable。这样做时,需要由程序员确保不存在其他的对该块数据的可变引用。

char[] s = ...;
immutable(char)[] p = cast(immutable)s; // 未定义行为
immutable(char)[] p = cast(immutable)s.dup; // 正确,只存在一个引用

使用.idup属性,可以便利的创建某个数组的不变副本。

auto p = s.idup;
p[0] = ...; // 错误,p[]不可改变

通过转换移除immutable

immutable类型可通过强制转换而被移除。

immutable int* p = ...;
int* q = cast(int*)p;

不过,这并不意味着可以修改这块数据。

*q = 3; // 编译通过,但结果会导致未定义行为

在有些情况下,强制抹除不变性是必要的。比如涉及某个不能更改的库,而库中的静态类型并不正确。有史以来,强制转换一直很直接很有效。使用强制转换抹除不变性时,必须要承担保证数据不变的责任,因为它已经不再由编译器静态保证了。

immutable成员函数

immutable成员函数表示this以及被this引用的任何数据都是不变的。可以这样声明:

struct S
{ int x;

immutable void foo()
{
x = 4; // 错误,x不可改变
this.x = 4; // 错误,x不可改变
}

函数的constimmutable属性也可以写在参数列表的右括号之后:

struct S
{
void bar() immutable
{
}
}

const类型

const类型和immutable类型类似,唯一的区别在于const建立的是数据的只读视图。数据本身随时可能被其他引用了该数据的变量所修改。

const成员函数

在const成员函数中,不允许通过该函数的this指针改变对象任何部分的数据。

隐式转换

immutable和非immutable类型都可以隐式转换为const类型。非immutable类型不能隐式转换为immutable类型,反之亦然。

对比D和C++的immutable/const

Const, Immutable Comparison
特性 D C++98
const关键字
immutable关键字
const表示方式 函数式:
//指向指向const int的const指针的指针
const(int*)* p;
后缀:
//指向指向const int的const指针的指针
const int *const *p;
有传递性的const 有:
//指向指向const int的const指针的const指针
const int** p;
**p = 3; // 错误
无:
//指向指向int的指针的const指针
int** const p;
**p = 3; // 通过
强制抹除const 有:
// 指向const int的指针
const(int)* p;
int* q = cast(int*)p; // ok
有:
// 指向const int的指针

const int* p;
int* q = const_cast<int*>p; //ok
强制抹除const后进行修改 无:
// 指向const int的指针
const(int)* p;
int* q = cast(int*)p;
*q = 3; // 未定义行为
有:
// 指向const int的指针
const int* p;
int* q = const_cast<int*>p;
*q = 3; // 通过
以顶级const区分重载的函数 有:
void foo(int x);
void foo(const int x); //正确
无:
void foo(int x);
void foo(const int x); //错误
变量的const别名 有:
void foo(const int* x, int* y)
{
bar(*x); // bar(3)
*y = 4;
bar(*x); // bar(4)
}
...
int i = 3;
foo(&i, &i);
有:
void foo(const int* x, int* y)
{
bar(*x); // bar(3)
*y = 4;
bar(*x); // bar(4)
}
...
int i = 3;
foo(&i, &i);
变量的immutable别名 无:
void foo(immutable int* x, int* y)
{
bar(*x); // bar(3)
*y = 4; // 未定义行为
bar(*x); // bar(??)
}
...
int i = 3;
foo(cast(immutable)&i, &i);
没有immutable
字符串字面量的类型 immutable(char)[] const char*
将字符串字面量转换为非常量 不允许 允许但不推荐
 
2009年08月05日 星期三 16:06
本来在Windows里面用ACDSee批量转格式的,结果发现用ACDSee把TGA转到PNG之后,透明通道都丢失了。
没办法,只好到 Linux里面用gimp脚本来转。gimp的脚本得用Scheme写,gimp里面就能找到gimp的API。不过我没用过Scheme,不知道 Scheme有些啥库函数能遍历文件系统的,所以绕了个圈子,用Shell脚本遍历文件,再把文件名硬编码到Scheme脚本中:
#!/bin/bash
(
find . -iname '*.TGA' -exec echo '
(let ((image (car (file-tga-load 1 "{}" "{}"))))
(file-png-save-defaults 1 image (car (gimp-image-get-active-drawable image)) "{}.png" "{}.png")
(gimp-image-delete image)
)
' \;
echo '(gimp-quit 1)'
) | gimp-console -d -i -b -
 
2009年07月22日 星期三 15:14
我日志上写的东西都是些自说自话,有点想法就记下来,没有怎么考虑读者。不过我在Google Reader上也看到有30个人订阅了我的日志,不由得产生好奇,到底上这儿来的朋友都是做什么的,怎么找到这儿来的,对哪方面的文章感兴趣?以后也可以 多交流交流嘛。
 
2009年07月21日 星期二 18:32
英文原文由 rompf 发表于 2009年6月5日。

Scala 2.8将支持一种强大的逻辑流程抽象功能——continuation。它最牛逼之处在于允许你直接以逐语句风格使用基于回调、事件驱动的API。

这将能简化许多编程任务:

  • 使用Java NIO处理异步I/O
  • 使用Executor和线程池
  • 在Web应用中处理跨越多个请求的逻辑流程

在Scala 2.8中并不会专门为以上用例引入新的API。相反,我们期待,当底层的 Continuation 机制加入之时,你会自然而然的用上它。

概览Shift和Reset

Continuation 可在某一时刻对程序进行“捕获”得到,它包含了这一时刻该程序的“其余部分”。Continuation有一点很有意思,它可以被视为普通的函数变量,比 如你可以把它保存在数据结构中,然后再进行一次或多次调用(也可以根本不调用它)。Scala中的Continuation和 Schememe或ML中的不同,Scala中的Continuation具有隔离范围(即composable或delimited)。这意味着,它们 并不包含程序的全部剩余部分,而只包含由程序员定义的范围内的剩余部分。

可以使用shiftreset两个基本方法操作Continuation。调用shift能捕获当前Continuation,而reset界定了Continuation能到达的边界。下面是一段示例:

reset {
...
shift { k: (Int=>Int) => // the continuation k will be the '_ + 1' below
k(7)
} + 1
}
// result: 8

当然,shiftreset是库函数,而不是新的关键字。从语法角度看,这二者亦无需同时出现。事实上,对shiftreset的使用中可以出现在完全无关的方法中。类型系统能确保不出差错,并要求每一个包含shift方法都在其返回类型标记了@cps类型注释(type annotation)。许多情况下,该类型注释会被自动推断,无须程序员过问。

def foo(): Int @cps[Int,Int] = { // could leave out return type
shift { k: (Int=>Int) =>
k(7)
} + 1
}
reset(2 * foo()) // result: 16

以下是另一示例,示范了如何在基于回调的API使用Continuation.

import java.util.{Timer,TimerTask}

val timer = new Timer()

def sleep(delay: Int) = shift { k: (Unit => Unit) =>
timer.schedule(new TimerTask {
def run() = k() // in a real program, we'd execute k on a thread pool
}, delay)
}

println("look, Ma ...")
sleep(1000)
println(" no threads!")

如果您想先睹为快,自己动手试试Scala编译器的Continuation插件预发布版吧(可从源代码版本库中获取)。若要了解Scala 2.8实现Continuation的相关细节,请看相应的文件 (ICFP'09中加入) 。

 
2009年06月12日 星期五 10:16
租房时可能和两类人打交道,中介和房东。房东的心态和做任何生意的人一样,都希望尽量高价成交,但中介不是。
事实上,中介会尽力促成成交,而根本 不在乎价格,也不在乎房屋质量。原因有很多,首先,中介做的是无本生意,中介费多一点少一点关系不大,但是,能不能收到中介费则关系很大;其次,中介之间 是有竞争的,一套房源可能登记在多家中介公司,就算同一家中介公司,也会有多个中介人,对中介来说,把一套房子以低于市场价的价格租掉对他来说没有任何损失 照收中介费,反而,如果被其他中介租掉了,他一分钱都拿不到。
既然知道了中介关注什么,就不难理解中介租房时采用的策略了。中介带人看房时是非常 简单粗暴的,他不管房源质量如何,价格如何,始终就带着房客一家家去看,当房客看累了时,房客自然会选择看过的房子中性价比最高的一处来成交。不论房屋装 修情况、租金价格如何,中介总是会说同样的话劝房客成交,根本对房屋状况毫不在乎。
对于房客来说,想要租到价廉物美的房子,和中介打交道时要注意以下几点:
第 一、在中介带你看房以前,先问清楚房屋状况,虽然很多信息不看房是不会知道的,但也有很多信息问问房东就可以知道,比如房屋配备的家具家电、面积、装修时 间、床的大小,是否有地板、墙纸。如果这些状况不如你曾经看过的某套房,那么就告诉中介,不去看房。一定要中介找性价比最高的房子给你看,如果某个中介公 司没有比你曾经看过的房更好的,那么就换家中介公司。不要在没有可能成交的房子上浪费时间,那样会让你变累,而倾向于马上在看过的房子中性价比最高的一处 来成交,而不是潜在可能的性价比最高的房子。
第二、如果看中了一套房子想租,要求中介帮你讲价。不要直接找房东讲价,让中介去谈价格。你只需威胁中介如果价格不降下来就不成交,中介自然会努力帮你讲价的。
第三、有些房屋是托管房,是中介低价租来,改造一下再高价租出去赚取差价的,这类房屋不用交中介费,而且可以要求中介配备缺少的设施或电器。
第 四、在网上找房时只找房东房,不要去找中介房,网上的中介房源几乎都是虚假的。如果你打电话过去,中介会告诉你你要的那套房已经租出去了,但是他有更合适 的房源可以租给你。这是因为中介只是把租房网站当成了一个免费的广告发布地,并不真正在上面提供信息。某些公司甚至要求每个中介人每天要在每个租房网站发 布10条租房信息,实际上每个中介人新增房源都没有10条,这些信息只能是虚假的。所以,对于房客来说,就算要去看中介房,也应该直接去中介公司的门店, 不必在网上找。

有空我再写一篇《三亚旅游心得》,一年多以前就想要写的。
 
2009年04月24日 星期五 21:28
有时候想要把lua中的表的内容作为log打出来,对于数组,可以这样写:
print(unpack(t))
不过,如果是字符串的键名,则需要写这样的代码:
for key, value  in pairs(t) do
    print(key, value)
end
有没有办法像unpack一样,对枚举器进行unpack,返回多个返回值呢?

这也可以用递归实现:
local function _iterator_unpack_i(i, f, s, var1, ...)
    if var1 == nil then
        return
    else
        return select(i, var1, ...), _iterator_unpack_i(i, f, s,f(s, var1))
    end
end
local function iterator_unpack_i(i, f, s, var)
    return _iterator_unpack_i(i, f, s, f(s, var))
end


local function _iterator_unpack_f(formatstring, f, s, var1, ...)
    if var1 == nil then
        return
    else
        return format(formatstring, var1, ...), _iterator_unpack_f(formatstring, f, s,f(s, var1))
    end
end
local function iterator_unpack_f(formatstring, f, s, var)
    return _iterator_unpack_f(formatstring, f, s, f(s, var))
end


local function keys(t)
    return iterator_unpack_i(1, pairs(t))
end

local function values(t)
    return iterator_unpack_i(2, pairs(t))
end
然后你就可以这样用:
print(keys(t))
或者
print(values(t))
或者
print(iterator_unpack_f("%s=%s", pairs(t)))

类似昨天的例子,用递归的实现在参数较少时比用临时表快,而且没有堆内存分配
 
2009年04月24日 星期五 03:47
从lua 5.1开始,就不鼓励用隐含的arg来处理可变变量了,推荐做法是使用递归来处理...

在lua中,不论多返回值还是函数调用,...都只能用于最后剩下的参数,例如下列代码中
function f(...)
    print("a", ...)
    print(..., "a")
end
f("1", "2")
输出为:
a 1 2
1 a
这是因为第一个print调用是把...作为剩余的所有参数传进去,而第二次调用,...则被调整为1个参数。如果要想把参数添加到...后面,有两种做法。

第一种做法是使用临时表:
local tinsert = table.insert
local function append(x, ...)
local t = {...}
tinsert(t, x)
return unpack(t)
end
第二种做法则是使用递归:
local function append(x, arg1, ...)
if arg1 == nil then
return x
else
return arg1, append(x, ...)
end
end
当参数较少时(在我的机器上大概10个以下参数时),第二种做法更快。不过,第二种做法的另一个优势是没有分配堆内存,因而可以减少垃圾收集的次数。
 
2009年01月23日 星期五 19:56

首 先,要对ActionScript 3中的XML提出批评,这个XML搞了些全局设置,比如 ignoreComments、 prettyPrinting啥的。其实这些设置不应该是设置,而应该是构造函数和toXMLString的参数。为了进行某一次序列化或者反序列化的操 作就得改变全局设置,这是很傻的设定。

以下代码证明,Flex中的RemoteObject会猥琐的修改全局设置:

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="main()">
<mx:Script><![CDATA[
import flash.xml.*;
import flash.utils.*;

import mx.messaging.*;
import mx.messaging.channels.*;

import mx.rpc.*;
import mx.rpc.xml.*;
import mx.rpc.remoting.*;
import mx.rpc.events.*;

private function main():void {

var channels:ChannelSet = new ChannelSet();
channels.addChannel(new HTTPChannel(null, "http://xxx.xxx.xxx.xxx/xxx"));

var userService:RemoteObject = new RemoteObject();
userService.destination = "XXX";
userService.channelSet = channels;

trace("XML.prettyPrinting before RPC invoking:", XML.prettyPrinting);
userService.xxx().addResponder(new mx.rpc.Responder(
function (event:Object):void {
trace("XML.prettyPrinting after RPC invoking:", XML.prettyPrinting);
},
function (event:Object):void {
trace("XML.prettyPrinting after RPC invoking:", XML.prettyPrinting);
}
));
}
]]></mx:Script>
</mx:Application>

ASDoc 中提到,XML.prettyPrinting的默认设置为true。但是,RemoteObject猥琐的修改了全局设置,导致ASDoc所说只是在忽悠我。事实上,XML.prettyPrinting有时为true,XML.prettyPrinting有时为false,取决于与你代码一起工作的 未知代码干了啥。我的结论是,在一个库里面使用全局变量是很蠢的事情,这一点和云风说的不同。云风说可执行程序中可以使用一些永远不销毁的全局变量,他的 前提是,你确切的知道这些东西的生命周期,而对库、框架之类的东西来说,你永远都不知道别人会怎么使用你,所以全局变量就很猥琐了。

 
     
 
 
个人档案
 
pop_atry
男, 23岁
上次登录:
20小时前
加为好友
 
   
 
最新评论
 
文章评论|照片评论


mark
 

“音轨作为菜单” 很有创意的翻译
 

今天这不是更新了吗?
 
 
 
     
 
文章存档
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
     
 
订阅我的空间
 
已有人次访问本空间
 
订阅RSS  什么是RSS?

您也想拥有这样的空间?请点此申请。
     


©2009 Baidu