Hello World
Spiga

编程语言的发展趋势及未来方向(3):函数式编程

2010-05-04 19:09 by 老赵, 21134 visits

这是Anders Hejlsberg(不用介绍这是谁了吧)在比利时TechDays 2010所做的开场演讲。由于最近我在博客上关于语言的讨论比较多,出于应景,也打算将Anders的演讲完整地听写出来。在上一部分中,Anders阐述了他眼中声明式编程的理念及DSL,并演示C#中一种内部DSL的形式:LINQ。在这一部分中,Anders谈及了声明式编程的另一个重要组成部分:函数式编程,并使用.NET平台上的函数式编程语言F#进行了演示。

如果没有特别说明,所有的文字都直接翻译自Anders的演讲,并使用我自己的口语习惯表达出来,对于Anders的口误及反复等情况,必要时在译文中自然也会进行忽略。为了方便理解,我也会将视频中关键部分进行截图,而某些代码演示则会直接作为文章内容发表。

(听写开始,接上篇

关于声明式编程的还有一部分重要的内容,那便是函数式编程。函数式编程已经有很长时间的历史了,当年LISP便是个函数式编程语言。除了LISP以外我们还有其他许多函数式编程语言,如APLHaskellSchemeML等等。关于函数式编程在学术界已经有过许多研究了,在大约5到10年前许多人开始吸收和整理这些研究内容,想要把它们融入更为通用的编程语言。现在的编程语言,如C#、Python、Ruby、Scala等等,它们都受到了函数式编程语言的影响。

我想在这里先花几分钟时间简单介绍一下我眼中的函数式编程语言。我发现很多人听说过函数式编程语言,但还不十分清楚它们和普通的命令式编程语言究竟有什么区别。如今我们在使用命令式编程语言写程序时,我们经常会写这样的语句,嗨,x等于x加一,此时我们大量依赖的是状态,可变的状态,或者说变量,它们的值可以随程序运行而改变。

可变状态非常强大,但随之而来的便是叫做“副作用”的问题。在使用可变状态时,你的程序则会包含副作用,比如你会写一个无需参数的void方法,然后它会根据你的调用次数或是在哪个线程上进行调用对程序产生影响,因为void方法会改变程序内部的状态,从而影响之后的运行效果。

而在函数式编程中则不会出现这个情况,因为所有的状态都是不可变的。你可以声明一个状态,但是不能改变这个状态。而且由于你无法改变它,所以在函数式编程中不需要变量。事实上对函数式编程的讨论更像是数学、公式,而不像是程序语句。如果你把x = x + 1这句话交给一个程序员看,他会说“啊,你在增加x的值”,而如果你把它交给一个数学家看,他会说“嗯,我知道这不是true”。

然而,如果你给他看这条语言,他会说“啊,y等于x加一,就是把x + 1的计算结果交给y,你是为这个计算指定了一个名字”。这时候在思考时就是另一种方式了,这里y不是一个变量,它只是x + 1的名称,它不会改变,永远代表了x + 1。

所以在函数式编程语言中,当你写了一个函数,接受一些参数,那么当你调用这个函数时,影响函数调用的只是你传进去的参数,而你得到的也只是计算结果。在一个纯函数式编程语言中,函数在计算时不会对进行一些神奇的改变,它只会使用你给它的参数,然后返回结果。在函数式编程语言中,一个void方法是没有意义的,它唯一的作用只是让你的CPU发热,而不能给你任何东西,也不会有副作用。当然现在你可能会说,这个CPU发多少热也是一个副作用,好吧,不过我们现在先不讨论这个问题。

这里的关键在于,你解决问题的方法和以前大不一样了。我这里还是用代码来说明问题。使用函数式语言写没有副作用的代码,就好比在Java或C#中使用final或是readonly的成员。

例如这里,我们有一个Point类,构造函数接受x和y,还有一个MoveBy方法,可以把一个点移动一些位置。 在传统的命令式编程中,我们会改变Point实例的状态,这么做在平时可能不会有什么问题。但是,如果我把一个Point对象同时交给3个API使用,然后我修改了Point,那么如何才能告诉它们状态改变了呢?可能我们可以使用事件,blablabla,如果我们没有事件,那么就会出现那些不愉快的副作用了。

那么使用函数式编程的形式写代码,你的Point类还是可以包含状态,例如x和y,不过它们是readonly的,一旦初始化以后就不能改变了。MoveBy方法不能改变Point对象,它只能创建一个新的Point对象并返回出来。这就是一个创建新Point对象的函数,不是吗?这样就可以让调用者来决定是使用新的还是旧的Point对象,但这里不会有产生副作用的情况出现。

在函数式编程里自然不会只有Point对象,例如我们会有集合,如Dictionary,Map,List等等,它们都是不可变的。在函数式编程中,当我们向一个List里添加元素时,我们会得到一个新的List,它包含了新增的元素,但之前的List依然存在。所以这些数据结构的实现方式是有根本性区别的,它们的内部结构会设法让这类操作变的尽可能高效。

在函数式编程中访问状态是十分安全的,因为状态不会改变,我可以把一个Point或List对象交给任意多的地方去访问,完全不用担心副作用。函数式编程的十分容易并行,因为我在运行时不会修改状态,因此无论多少线程在运行时都可以观察到正确的状态。两个函数完全无关,因此它们是并行还是顺序地执行便没有什么区别了。我们还可以有延迟计算,可以进行Memorization,这些都是函数式编程中十分有趣的方面。

你可能会说,那么我们为什么不都用这种方法来写程序呢?嗯,最终,就像我之前说的那样,我们不能只让CPU发热,我们必须要把计算结果表现出来。那么我们在屏幕上打印内容时,或者把数据写入文件或是Socket时,其实就产生了副作用。因此真实世界中的函数式编程,往往都是把纯粹的部分进行隔离,或是进行更细致的控制。事实上也不会有真正纯粹的函数式编程语言,它们都会带来一定的副作用或是命令式编程的能力。但是,它们默认是函数式的,例如在函数式编程语言中,所有东西默认都是不可变的,你必须做些额外的事情才能使用可变状态或是产生危险的副作用。此时你的编程观念便会有所不同了。

我们在自己的环境中开发出了这样一个函数式编程语言,F#,已经包含在VS 2010中了。F#诞生于微软剑桥研究院,由Don Syme提出,他在F#上已经工作了5到10年了。F#使用了另一个函数式编程语言OCaml的常见核心部分,因此它是一个强类型语言,并支持一些如模式匹配,类型推断等现代函数式编程语言的特性。在此之上,F#又增加了异步工作流,度量单位等较为前沿的语言功能。

而F#最为重要的一点可能是,在我看来,它是第一个和工业级的框架和工具集,如.NET和Visual Studio,有深入集成的函数式编程语言。F#允许你使用整个.NET框架,它和C#也有类似的执行期特征,例如强类型,而且都会生成高效的代码等等。我想,现在应该是展示一些F#代码的时候了。

首先我想先从F#中我最喜欢的特性讲起,这是个F#命令行……(打开命令行窗口以及一个F#源文件)……F#包含了一个交互式的命令行,这允许你直接输入代码并执行。例如输入5……x等于5……然后x……显示出x的值是5。然后让sqr x等于x乘以x,于是我这里定义了一个简单的函数,名为sqr。于是我们就可以计算sqr 5等于25,sqr 10等于100。

F#的使用方式十分动态,但事实上它是一个强类型的编程语言。我们再来看看这里。这里我定义了一个计算平方和的函数sumSquares,它会遍历每个列表中每个元素,平方后再把它们相加。让我先用命令式的方式编写这个函数,再使用函数式的方式,这样你可以看出其中的区别。

let sumSquaresI l = 
    let mutable acc = 0
    for x in l do
        acc <- acc + sqr x
    acc

这里先是命令式的代码,我们先创建一个累加器acc为0,然后遍历列表l,把平方加到acc中,然后最后我返回acc。有几件事情值得注意,首先为了创建一个可变的状态,我必须显式地使用mutable进行声明,在默认情况下这是不可变的。

还有一点,这段代码里我没有提供任何的类型信息。当我把鼠标停留在方法上时,就会显示sumSquaresI方法接受一个int序列作为参数并返回一个int。你可能会想int是哪里来的,嗯,它是由类型推断而来的。编译器从这里的0发现acc必须是一个int,于是它发现这里的加号表示两个int的相加,于是sqr函数返回的是个int,再接下来blablabla……最终它发现这里到处都是int。

如果我把这里修改为浮点数0.0,鼠标再停留一下,你就会发现这个函数接受和返回的类型都变成float了。所以这里的类型推断功能十分强大,也十分方便。

现在我可以选择这个函数,让它在命令行里执行,然后调用sumSquaresI,提供1到100的序列,就能得到结果了。

let rec sumSquaresF l = 
    match l with
    | [] -> 0
    | h :: t -> sqr h + sumSquaresF t

那么现在我们来换一种函数式的风格。这里是另一种写法,可以说是纯函数式的实现方式。如果你去理解这段代码,你会发现有不少数学的感觉。这里我定义了sumSqauresF函数,输入一个l列表,然后使用下面的模式去匹配l。如果它为空,则结果为0,否则把列表匹配为头部和尾部,然后便将头部的平方和尾部的平方和相加。

你会发现,在计算时我不会去改变任何一个变量的值,我只是创建新的值。我这里会使用递归,就像在数学里我们经常使用递归,把一个公式分解成几个变化的形式,以此进行递归的定义。在编程时我们也使用递归的做法,然后编译器会设法帮我们转化成尾递归或是循环等等。

于是我们便可以执行sumSquaresF函数,也可以得到相同的结果。当然实际上可能你并不会像之前这样写代码,你可能会使用高阶函数:

let sumSquares l = Seq.sum (Seq.map (fun x -> x * x) l )

例如这里,我只是把函数x乘以x映射到列表上,然后相加。这样也可以得到相同的结果,而且这可能是更典型的做法。我这里只是想说明,这个语言在编程时可能会给你带来完全不同的感受,虽然它的执行期特征和C#比较接近。

这便是关于F#的内容。

(未完待续)

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

49 条回复

  1. 老赵
    admin
    链接

    老赵 2010-05-04 19:10:34

    看看不发到博客园的话访问量如何,呵呵。

  2. Snake
    120.42.198.*
    链接

    Snake 2010-05-04 19:14:52

    占位!老赵竟然不留沙发!

  3. Snake
    120.42.198.*
    链接

    Snake 2010-05-04 19:15:30

    刚在想老赵怎么这么多天没有动静,过来一看就抓到宝了.哇哈哈!

  4. 老赵
    admin
    链接

    老赵 2010-05-04 19:17:19

    @Snake

    用RSS订阅吧。

  5. Snake
    120.42.198.*
    链接

    Snake 2010-05-04 19:20:15

    利用这么前排的位置问问老赵,曾经非常想与您联系,给您发了封email,貌似也在您推上和您打了下招呼(目前还不怎么会用twitter),您都没理睬我- - 最后问一下,该以什么样的方式联系您比较好:)

  6. 老赵
    admin
    链接

    老赵 2010-05-04 19:22:13

    @Snake

    打招呼我一般就不理睬了,有什么事情直接说比较好,呵呵。

  7. Snake
    120.42.198.*
    链接

    Snake 2010-05-04 19:22:47

    @老赵

    没..刚才真是碰运气进来的.嘿嘿

  8. Snake
    120.42.198.*
    链接

    Snake 2010-05-04 19:37:27

    我的大脑在革命,这篇帮到了我很多,让我知道函数式编程和命令式编程到底哪里不同.感谢老赵的翻译.

  9. JimLiu
    221.223.46.*
    链接

    JimLiu 2010-05-04 20:58:19

    函数式编程需要对编程思维进行一些转变啊……

  10. 链接

    孝园 2010-05-05 08:22:26

    正如我所说,我就把这个博客设为首页。哈哈。

  11. 链接

    景良 2010-05-05 09:04:26

    oyear,自打老趙有自己的博客開始,馬上訂閱.

  12. 老赵
    admin
    链接

    老赵 2010-05-05 09:10:10

    @孝园

    可惜访问量很低,不承诺放弃使用博客园引流方案,嘿嘿。

  13. _龙猫
    124.160.91.*
    链接

    _龙猫 2010-05-05 09:25:02

    看了这篇文章,更加确定JS的先进。同时对F#产生了很大的兴趣。

  14. 老赵
    admin
    链接

    老赵 2010-05-05 09:31:17

    @_龙猫

    在我看来JS不是函数式编程语言,只是有一定函数式编程特性吧,就像C#和Scala那样。而且就算它是函数式编程语言,也谈不上“先进”的,JS与大部分现代编程语言(Scala,F#,C#,Ruby,Python,Clojure等等随便举)相比起来那还是相当相当朴素的。

  15. 看看
    222.92.42.*
    链接

    看看 2010-05-05 09:50:17

    希望老赵不要受某些人的影响,因为主页是博客园,如果上面给个连接也好知道有新的解惑出来了。呵呵,只是麻烦老赵了。

  16. meback
    202.99.16.*
    链接

    meback 2010-05-05 10:51:36

    赵兄弟写篇F#入门吧, 看的云里雾里的。

  17. lajabs
    218.66.110.*
    链接

    lajabs 2010-05-05 11:29:03

    我们平台正在细化/灵活/敏捷,大包大揽的开发语言已经不适合现在的需求了,现在追求的是多语言协同开发,更多要求编程语言要有特性,而不是发挥共性. FP语言很好地补充了传统顺序语言的空白,而相反,类似java这样的综合语言的死期也就不久了.

    起码在WEB开发领域,我们可以看到最早摈弃oracle的庞大,再到mysql的盛行,再到NoSQL的崛起.

  18. 老赵
    admin
    链接

    老赵 2010-05-05 13:36:28

    @lajabs: FP语言很好地补充了传统顺序语言的空白

    这怎么说?在我看来Java,F#,C#,Scala,Haskell等等都是通用编程语言,都是大包大揽型的。不过有点没错,就是说要有特性(我的理解是“特长”)。Java这种没有特长的会很尴尬。

  19. feng
    202.106.180.*
    链接

    feng 2010-05-05 17:58:11

    非常好. 希望再讲一些 函数是编程 和微线程的关系

  20. 老赵
    admin
    链接

    老赵 2010-05-05 22:42:43

    @feng

    没关系的

  21. 链接

    Ivony 2010-05-06 01:03:09

    谈点儿不同的看法,尽管大多数人都会认为不可变的“变量”是函数式的重要“特点”,但在我看来,这只是我们被非函数式的范式毒害过久所造成的。

    我认为函数式的特点应该在于函数的结果不应受到任何非参数因素的影响,或者换一个朴素的说法,对于任何函数而言,相同的参数(对于内部函数而言,这里的参数包括外部函数的参数)总是得到相同的结果。

    所以函数式范式里面,应该是没有必要存在变量的,变量只是一个别名。

    如何理解?

    简单的说,我们写下如下语句:

    let x = y + 1;

    从传统的角度来理解,我们说是把y + 1这个结果的值赋给x。

    但从另一个角度来说,这句话实际上就是:
    令x = y + 1。

    作为赋值语句而言,我们强调的是把y的值加上1给x。但事实上如果y的值是所谓的“不可变”的,那么x和y+1就是等同的(也就是令x = y + 1所表达的意思),换言之,对于任何这样的表达式:
    f(y+1)
    都有:
    f(y+1) = f(x)

    现在是不是能更好的理解这个别名的意思?换言之,在任何表达式中,x和y+1都可以互换而不影响结果。

    回过头来,我们说y是什么,只有两种可能,参数或是其他别名。 我们假设当前上下文只有一个参数p,那么y只可能是:

    1. 就是p
    2. f(p)


    所以函数式里面只存在两种“值”:p和f(p),要什么变量呢?变量只是p或者f(p)的别名。

    换言之,我们写下如下语句:

    let y = f(p);

    只是为了,我们在写这样的代码的时候方便点:

    f(p) + g(f(p) + h(f(p)))

    因为有了上面那个所谓的赋值语句,我们可以写成这样:

    y + g( y + h(y) )

    所以,所谓的不可变“变量”,只是用传统的变量的观点来阐述函数式里面的别名而已。

  22. 老赵
    admin
    链接

    老赵 2010-05-06 09:21:02

    @Ivony

    你的意思是不是说,在函数式编程里应该不提“变量”,而是应该理解为“代换”呢?

    其实我觉得这要看怎么理解吧,因为“代换”的目的只是取个别名,而不是计算。在Haskell这种Lazy Evaluation的语言中,理解为代换是妥当的,因为它的确只是个别名,整个表达式直到最后一步才会计算。但是如F#这种非延迟的语言来说,可能叫做“变量”会更合适一点,因为它是立即计算的。

  23. 链接

    Ivony 2010-05-06 10:37:38

    @老赵

    我的切入点在用lambda演算来理解函数式范式。所以在我看来,事实上延迟计算并不是函数式范式的特点。这是一种实现,或者说是函数式的优越之处。任何代码只要能满足苛刻的条件,即函数结果永远只与函数参数有关,那么延迟计算/并行计算(因为函数计算结果与计算时机无关)或是单次计算(一旦计算了f(y)则之后可以直接用结果代换不必重复计算)就自然而然的可行,这取决于语言的实现。不然,延迟计算就可能会存在副作用(C#就很典型,需要自己避免栽到陷阱)。即上文中苛刻的条件是延迟/并行/单次计算的充分条件。

    函数式是一种用代码表达意图(一般是算法)的方式,当完全满足函数式的要求时,这段代码在计算的时候就可以延迟/并行计算。

    F#是为函数式而生的,但显然这不是一种纯粹的函数式语言。其实我很希望能够在C#或是其他什么语言中加上一种语法,指令某个函数不存在任何副作用,需要上述的限制。像这样:

    private int i = 0;
    
    public lambda int Function( int p, Random random )//编译错误,不允许引用类型参数
    {
      int j = p + i;//编译错误,不允许使用i。
      p++;//编译错误,不允许更改变量
      int r = random.Next( j );//编译错误,不允许使用非lambda修饰的函数。
      return r;
    }
    

    存在这样的语法的话,我们就可以很轻易的知道,任何用lambda修饰的函数,都可以并行执行或是延迟执行而毫无副作用。

    小修改一下,应该是不能有引用类型参数,而不是返回值。

  24. DeathKnight
    60.186.219.*
    链接

    DeathKnight 2010-05-06 11:48:44

    rss订阅了,博客园现在有深度的内容太少了,不看也罢

  25. 链接

    xiaotie 2010-05-06 14:31:43

    @Ivony: 我认为函数式的特点应该在于函数的结果不应受到任何非参数因素的影响,或者换一个朴素的说法,对于任何函数而言,相同的参数(对于内部函数而言,这里的参数包括外部函数的参数)总是得到相同的结果。

    不是很熟悉函数式语言。有一个疑问:这样说的话,随机数产生器咋办?

  26. 链接

    韦恩卑鄙 @waynebaby 2010-05-06 14:32:51

    我还是觉得 要是引流还是全文发表比较厚道。

    Ivony 的描述很有道理啊 学习中

  27. 老赵
    admin
    链接

    老赵 2010-05-06 14:53:26

    @xiaotie

    所以必须有副作用的,随机数产生器就不是无副作用的函数。

  28. 老赵
    admin
    链接

    老赵 2010-05-06 14:56:57

    韦恩卑鄙 @waynebaby: 我还是觉得要是引流还是全文发表比较厚道。

    唉,就怕内容太多就没人来袅……下次试试看发全文。

  29. Snake
    220.162.41.*
    链接

    Snake 2010-05-06 18:13:49

    @老赵: 唉,就怕内容太多就没人来袅……下次试试看发全文。

    其实我倒是觉得没关系,留言的人还是会来留言的,真正有看得懂您文章,并且思考了的一般都会留言的.而您又关闭了那边的留言...

  30. 链接

    Ivony 2010-05-06 21:32:54

    @xiaotie

    所以我的例子里特地拿随机函数来做例子。

    纯函数式代码里面就是不可能产生随机函数的,应该可以证明用lambda演算无法得到随机函数。

    也所以,几乎不可能用纯函数式的语言来写软件,除非你只是算个圆周率什么的。

  31. 链接

    xiaotie 2010-05-06 22:21:01

    @Ivony

    lambda演算不和那啥啥(忘了怎么称呼)能力一样吗?那啥啥能够产生伪随机函数,lambda演算也能产生伪随机函数。可能写出来太累赘。每一个用到随机函数的地方,都要给出一个初始值,一个function。

  32. 链接

    Ivony 2010-05-07 10:10:04

    没有外界状态,伪随机数应该是不可能的。

    那啥啥是图灵机吧。

  33. 链接

    biser007 2010-05-08 00:08:03

    还是习惯从cnblogs进入你的Blog。

  34. 老赵
    admin
    链接

    老赵 2010-05-08 01:21:16

    @biser007

    为什么不订阅?

  35. 链接

    熙智 2010-05-08 02:29:07

    使用F#時,每次創建出來新的“變量”或者是內存塊,這會不會對機器有較大的消耗?管理F#時回收內存會不會更頻繁?

  36. 老赵
    admin
    链接

    老赵 2010-05-08 12:54:28

    @熙智

    理论上会加快内存回收频率,但是并不一定会增加消耗,因为Gen0,Gen1的回收是相当快的。

  37. 1-2-3
    123.191.105.*
    链接

    1-2-3 2010-05-10 11:40:33

    嗯,希望以后大学里都能开函数式编程的课程。这样大家就都能写出不产生无谓的副作用的代码了。把副作用严格控制起来的话,代码会更清晰,也更加不容易写错,排错也会更容易吧。

  38. 老菜
    61.162.217.*
    链接

    老菜 2010-05-10 21:12:30

    谢谢老赵,写的东西对我来说很有价值。今后我要经常来这里看看。已经RSS订阅了。

  39. 三
    110.96.152.*
    链接

    2010-05-13 12:04:26

    上面Ivony说的和我想得差不多。函数式语言中不应该再谈论赋值这种操作,所以也没有只能赋值一次这种特点。

    我对随机数产生器需要副作用的说法表示怀疑,也许是在说,随机数的产生需要调用同一个函数而在不同的调用时机得到不同的结果。但是你如果把随机数产生器看做一个无穷递归的函数,返回值是一个无穷列表,然后用continuation来取得列表中的某一项。

    别忘了图林机和lambda是等价的。

  40. ffl
    220.160.174.*
    链接

    ffl 2010-05-15 00:32:48

    readonly,const, 如果能提供internal frient就好了,internal粒度稍微大了点,friend又太容易被滥用, 那可以提供internal friend岂不是很好。。

  41. 老赵
    admin
    链接

    老赵 2010-05-15 01:32:13

    @ffl

    friend是什么意思?internal不就是friend吗?

  42. George Wing
    114.80.140.*
    链接

    George Wing 2010-05-18 10:24:52

    学习了~~期待下一篇~

  43. albertlee
    222.130.185.*
    链接

    albertlee 2010-05-24 10:15:41

    随机数函数不是纯函数,因为每次调用的返回值不同。在Haskell中,它的函数类型中带有 IO 标记。

    像Haskell这样通过类型系统,把纯与非纯的代码隔离开,可以很好的避免很多错误。

  44. meback
    218.241.130.*
    链接

    meback 2010-05-28 09:58:32

    老赵来个F#入门系列吧, 网上的资料实在太少了,

  45. lsp
    125.35.6.*
    链接

    lsp 2010-07-29 10:10:48

    老赵,文中应该是Memoization而不是Memorization吧?

  46. 链接

    zhangzy95 2010-09-17 13:37:17

    在函数式编程里自然不会只有Point对象,例如我们会有集合,如Dictionary,Map,List等等,它们都是不可变的。在函数式编程中,当我们向一个List里添加元素时,我们会得到一个新的List,它包含了新增的元素,但之前的List依然存在。所以这些数据结构的实现方式是有根本性区别的,它们的内部结构会设法让这类操作变的尽可能高效。

    List添加个元素就得到一个新的List,底层会是怎么实现的,List 中元素数量为1000000,不会是直接复制1000000,然后加1个元素这么样吧,这样高效吗?

  47. 老赵
    admin
    链接

    老赵 2010-09-17 23:25:54

    @zhangzy95

    高效不高效看你怎么加,例如,一个不可变的单向列表,在头部加上元素,很高效。

  48. abc
    216.113.168.*
    链接

    abc 2011-04-02 15:24:52

    说实话几乎没看懂楼主想说什么,从文章的意思是不可变量是不同语言区别的本质?但是不可变量的副作用不是大量内存结构的反复建立和销毁吗?而且似乎没有人说java,c的问题是因为传入了引用变量吧。说一半的话跟没说一样

  49. 老赵
    admin
    链接

    老赵 2011-04-02 16:17:06

    @abc

    我怎么觉得他说的很清楚,你的问题反而没明白,怎么扯到Java和C的引用变量问题了。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我