美文网首页iOS 的那些事儿
五分钟看懂 Smalltalk 语言

五分钟看懂 Smalltalk 语言

作者: 玛卡Maca | 来源:发表于2020-08-06 23:09 被阅读0次

    前言

    为什么是 Smalltalk?

    Smalltalk 是一门历史悠久的语言。虽然如今我们几乎没有机会再去使用它,但它的不少开创性理念,仍影响了许多现代的编程语言和思想。尤其如今主流的面向对象语言 C++ 和 Java,它们本身并不是纯粹的、适合教学的「面向对象」语言,往往使得初学者陷入一些细枝末节之中,导致理解上的困难。

    而学习 Smalltalk,我们能从编程语言的演进中更深刻地理解一些重要概念的沿革,也可以打破固化思维,在不同语言的比较中得到新的编程灵感。

    如今中文互联网上关于 Smalltalk 的资料相对较少,且多以讲述理念/机翻为主,缺少实际的教程。前些年我出于个人爱好了解了一些 Smalltalk 入门知识,并和小伙伴们做了线下分享。这回把当时的一些笔记整理后发到网上,也算是添砖加瓦吧。

    本文脉络主要参考了 I Can Read C++ and Java But I Can't Read Smalltalk 以及 Why I love smalltalk 。不过并不是照搬,而是加入了一些个人的理解夹带私货。希望我的引申能够使读者更容易地理解这一有趣的语言。

    基础语法

    现代编程语言的语法往往大同小异,但 Smalltalk 和如今的语言语法差距不小,这也是大多数人第一眼看到代码觉得难以理解的原因之一(可以在这里看到真实的代码例子)。

    不过,虽然写法不同,编程语言的常规要素还是大差不差的,我们先浮光掠影地看一下这些基础语法,便能够读懂大部分的代码内容。

    • 注释:用双引号包围。"这是注释"

    • 字符串:用单引号包围。'这是一个字符串'

    • 单个字符:$c

    • 符号(Symbol):#thisIsASymbol

      大家也许对符号这个概念比较陌生。简单来说,只要两个符号的值一样,那么它们在内存中也是相同的对象。后面 Ruby 等语言也继承了这一设计。

    • 变量声明:| a | ,也可以一次声明多个:| a b c |

    • 赋值语句:a := 5

    • 相等性与同一性:相等性使用 = ,相当于 Java 中的 equals() 。同一性使用 == ,即判断内存地址相同。

    • 返回语句:^ a 即其他语言中的 return a

    • 级联:不同于大多数语言,Smalltalk 使用 . 表示语句的末尾,而 ; 用于实现级联的功能。所谓级联,是指可以连续在同一个对象上调用方法,而不必每次都重复写出这个对象。举个例子:

      | p | 
      p := Client new    
        name: 'Jack';     
        age: 32;     
        address: 'Earth'.
      

      现代语言中 Dart 就延续了类似的设定。对于其他语言,用 Builder 模式也可以实现类似的功能。

    方法与类

    来看一个 Smalltalk 中方法定义和调用的例子:

    "定义一个方法:使物体围绕某个轴旋转一定角度"
    rotateBy: angle around: vector
      | result |
      result := [...省略具体内容].
      ^result
    
    "调用这个方法"
    t rotateBy: a around: v.
    

    相信除了 Objective-C 程序员,大家初看到都会觉得这样的方法签名有点奇怪。不过我们可以基于主流语言的语法,来看看一个方法是以何种逻辑演化成 Smalltalk 的样子的。

    先从我们熟悉的形式开始:

    t->rotate(a, v);  // In C++
    t.rotate(a, v);   // In Java
    

    省略掉一些不影响语义的符号:

    t rotate(a, v);    // 省略掉 .
    t rotate a, v;     // 省略括号
    

    然后我们发现参数的含义不太清晰,可以给每个参数前面加一个前缀来使含义更加明确:

    t rotate by a around v;
    

    但是这样的话,哪个词语是前缀,哪个是参数就不太能分得清。因此我们给前缀加个冒号来区分:

    t rotate by: a around: v;
    

    最后,都这样了,方法名字本身也没必要单列出来,可以直接和第一个参数前缀合并起来,就成了 Smalltalk 现在的样子:

    t rotateBy: a around: v; 
    



    让我们再来看一个复杂点的例子:

    a negative | (b between: c and: d)
      ifTrue: [a := c negated]
    

    它相当于其他语言中的:

    if (a < 0 || (c < b && b < d)) {
        a = -c;
    }
    

    细品这个例子,我们可以看到很多有意思的地方。

    首先来看这个 c negated 。有人会说,直接写个 -c 不就完了,为什么要搞这么臃肿。表面看上去,只是写法的不同,但其实它代表着理念的不同。-c 代表对 c 这个数字做了一次数学运算,而 c negated 代表的是在 c 这个对象上调用 negated 方法,然后用这个方法的返回值去做下一步的操作。

    这里体现了 Smalltalk 的核心概念之一:一切皆是对象。对比于另一门同样号称「一切皆是对象」的语言 Java,你是不能写出 5.negated() 这种代码的。而所谓的「基本类型」、「包装类型」让多少初学者陷入困惑,又让多少面试官洋洋得意。但在 Smalltalk 里,对象的概念真正贯穿始终,保持同一性,也避免了这种困惑。

    其次我们来讨论 Smalltalk 中另一个重要概念:消息。在前文中我们称这种写法为「调用方法」。但实际上 Smalltalk 中并没有调用方法的概念,而是叫做给对象发送消息。例如 c negated 用 Smalltalk 的话说,就是给对象 c 发送了一个名为 negated 的消息。

    在 Smalltalk 中有三种消息,它们在上面的例子中都能体现:

    • 一元(unary)消息:可以理解为一个无参方法,例如上面的 negated 消息。
    • 二元(binary)消息:就是常用的一组计算符号,比如四则运算符还有上面的 | 等逻辑运算符。不过需要注意的是,它的本质还是发送消息而不是直接运算,因此没有运算符优先级的概念。+* 在 Smalltalk 中的优先级是相等的。
    • Keyword 消息:可认为是普通的有参方法,在 Smalltalk 中就是每个参数以冒号连接来表示,比如上面的 rotateBy:around: 还有 between:and: 。这样的消息在 Smalltalk 中被称为 keyword

    有人可能会说,把方法调用说成是发送消息,但运行的效果是一样的,这不是换汤不换药吗?话是这么说,但描述词汇的不同也会带来思维方式的不同。相比于「方法调用」,「发送消息」这个说法会显得更加泛用一些,也可以带来一些更接近本质的思考。

    举个例子,如果我们调用一个不存在的方法,那自然会报错。但如果给一个对象发送一个它不认识的消息,那非得报错吗?我大不了不处理不就行了。甚至,能不能在遇到这种情况时,有个兜底的逻辑统一处理下?—— Ruby 的 method_missing 了解一下。

    再比如说当我们学习面向对象时提到的所谓「三大特征」:继承、封装、多态。它们真的能定义什么是「面向对象」吗?

    继承实质是面向对象语言中实现代码重用的一种手段,但现在大部分情况下「组合优于继承」早已成为共识。

    封装是为了隐藏底层或内部的实现细节,也并不是面向对象独有的概念。

    多态在传统面向对象教学中的解释颇为复杂。我们定义一个父类,并让两个不同的子类 override 父类的同一个方法,并编写不同的实现,来说明多态性。但是如果用发送消息的思路来解释就很简单了。给两个不同的对象发送同一个消息,这两个对象会做出不同的反应,不是理所当然的事情吗?压根不需要牵扯到类、继承之类的概念。

    对面向对象这一概念的误读影响了很多人。记得几年前见过一个问题,问的是「JavaScript 是一门面向对象的语言吗?」,下面的回答也是众说纷纭。而之所以大家会有争议,是因为 JavaScript 中并没有类的概念,取而代之的是原型的概念,搞的很多人一下子就混乱了。后来 ES6 的标准里增加了 class 语法糖,我就再也没见过有人提这个问题了。

    希望读者通过学习 Smalltalk 这门面向对象的鼻祖级语言,理解一切皆是对象以及对象间通过消息来通信这两个核心理念,减少一些对面向对象概念的误读。

    在理解了 Smalltalk 的核心概念之后,我们再来看一下怎么声明一个类:

    Object subclass: #MyClass      
      instanceVariableNames: ''      
      classVariableNames: ''      
      poolDictionaries: ''      
      category: 'Pupeno'
    

    大部分编程语言都会使用 class 关键字声明一个类,并用 extends 或类似的关键字来声明继承关系。而 Smalltalk 则另辟蹊径。让我们来看看它的逻辑:首先,由于一切皆是对象,因此 Object 类也是一个对象;其次,Object 类可以接收名为 subclass: 的消息,创建一个新的对象作为它的子类。无需再引入任何特殊的语法,仅用语言本身的两个核心理念,就十分自洽地把类的概念创建了出来。

    不仅仅是定义类,还有我们常用的分支、循环语句等,在 Smalltalk 中都可以通过发送消息的形式来实现,而无需定义额外的诸如 ifwhile 等关键字。这或许就是 Smalltalk 把类似 subclass: 之类的带参消息都称作 keyword 的原因。它们完全可以替代其他语言中各种关键字的职能。

    代码块

    继续回到上一节的例子:

    a negative | (b between: c and: d)
      ifTrue: [a := c negated]
    

    关注这个 [a := c negated] 。这里由中括号包围的部分,大家或许会以为就像是 C++/Java 里跟在 if 语句后面的大括号——对,但不全对。在 Smalltalk 中它被称为代码块,是语言中的一等公民,可以放在一个变量里面,自由地控制它的调用时机,也可以把它像参数一样传递。如果你熟悉 JavaScript 就一定不会感到陌生。做个简单对比:

    "in Smalltalk"
    a := [:x | x + 1]       "将一个代码块赋值给 a"
    (a value: 10)           "运行这个代码块并传入参数 10"
    
    // in Javascript
    let a = function(x) { return x + 1; };
    a(10);
    

    其实这就是匿名函数的概念。函数式编程的理念近年来也重回大众视野,即使是 Java 也在 1.8 中加入了相关的特性。可以看到虽说 Smalltalk 是面向对象语言的代表,但论函数式编程的能力也完全不输现代的主流语言们。

    控制流

    结合 Smalltalk 对象间发送消息的思想和代码块的能力,我们将变得无所不能。上文中提到 Smalltalk 无需额外的关键字就能实现 ifwhile 等的功能。在最后一节中就让我们具体地看一下如何利用这些能力来实现 if 的功能吧。这也是我当初学习 Smalltalk 时最喜欢的一个例子。

    首先,我们需要定义出 truefalse 这两个常量:

    先定义出 PBoolean 类,然后将 PTruePFalse 作为它的子类。PTrue 类的实例就可以认为是 truefalse 也是同理。

    Object subclass: #PBoolean      
      instanceVariableNames: ''      
      classVariableNames: ''      
      poolDictionaries: ''      
      category: 'Pupeno'
    PBoolean subclass: #PTrue      
      instanceVariableNames: ''      
      classVariableNames: ''      
      poolDictionaries: ''      
      category: 'Pupeno'
    PBoolean subclass: #PFalse      
      instanceVariableNames: ''      
      classVariableNames: ''      
      poolDictionaries: ''      
      category: 'Pupeno'
    

    其次,我们定义 ifTrue:else: 消息。它接受两个代码块作为参数。当 PTrue 类的实例接收到消息时,它执行 ifTrue: 携带的代码块,忽略 else: 携带的代码块。反之亦然。

    "PTrue 中定义"
    ifTrue: do else: notdo  
      ^ do value
      
    "PFalse 中定义"
    ifTrue: notdo else: do  
      ^ do value
    

    再次,我们定义一个 MyClass 类用作测试,它可以接受 equals: 消息判断两个对象是否相等。为了方便起见,我们永远返回 true 。实际中可以自行实现其业务逻辑。

    equals: other  
      ^ PTrue new
    

    最后,我们就可以愉快地使用 if-else 了:

    m1 := MyClass new.
    m2 := MyClass new.
    (m1 equals: m2) ifTrue: [  
      Transcript show: 'They are equal'
    ] else: [  
      Transcript show: 'They are not equal'
    ]
    

    写在最后

    本文介绍了 Smalltalk 中最为基础的语言规则与设计思想。在它出现的时候,它引领了面向对象、IDE 等等前沿的概念,而即使放到现在,它的语言特性也毫不过时。愿大家都能在了解 Smalltalk 的过程中有所收获。

    相关文章

      网友评论

        本文标题:五分钟看懂 Smalltalk 语言

        本文链接:https://www.haomeiwen.com/subject/yremlktx.html