38.14. 创建自己的对象

如前方所述,ELite是一种披着命令式语法外也的函数式语言,同时,ELite也是一种面向对象的语言。在ELite中,您不仅可以无缝的引用Java中的类,也完全可以在ELite中定义自己的对象。

以下从面向对象语言的几个基本特征简单展开介绍一下。

38.14.1. 封装

在ELite中声明一个对象是非常简单的,其关键词为 class,在对象内部,可以拥有数据(变量)与函数(方法),以下是一个简单示例。

例 38.2. employee.xel

class Employee {
   define name;
   define salary;      
}

上述代码定义了一个对象 Employee,它拥有两个属性,分别是 name 与 salary。

对象定义好以后,我们可以声明上述对象的一个实例,使用关键词:new

> require "../sample/employee.xel";
> define emp = new Employee();
> emp.name = "Kevin";
Kevin
> emp.salary = 5000;
5000

例 38.3. 改进后的employee.xel

class Employee {
    define name;
    define salary;      
   
    Employee(name, salary) {
        this.name = name;
        this.salary = salary;
    }
}

上述代码中,我们使用了另一个关键词“this”,此处的this同Java语言非常类似。

> require "../sample/employee.xel";
> define emp = new Employee("Kevin", 5000);
> emp.name;
Kevin
> emp.salary;
5000
>

例 38.4. 另一种写法的employee.xel

class Employee {
    define name;
    define salary;      
   
    define Employee = {name, salary => 
        this.name = name;
        this.salary = salary;
    }
}

只是上述写法,与我们传统意义上理解的对象的构造函数不太形象,因此,我们并不推荐您这样做,在这里,笔者只是想向你重复:Lambda表达式是ELite的理论支撑。

与Java语法同样类似的还有:如果我们没有指定构造函数,则对象默认拥有一个空参数的构造函数。

例 38.5. 声明函数的employee.xel

class Employee {
    define name;
    define salary;      
   
    Employee(name, salary) {
        this.name = name;
        this.salary = salary;
    }
    
    void adjust(percent) {
        salary = salary * (1 + percent);
    }
}

> require "../sample/employee.xel";
> define emp = new Employee("Kevin", 5000);
> emp.adjust(0.1);
> emp.salary
5500.0
>

在ELite中,针对类成员变量、类函数方法,可以通过“public”、“protected”、“private”等关键词修饰,其含义与Java语法保持一致。在ELite中,没有package的概念,因此,如果变量或函数没有修饰符,则默认为“public”。

38.14.2. 继承

在ELite中定义的对象,可以继承某个基类,与Java类似的是:ELite只支持单根继承。在下例中,我们实现了三个拥有继承关系的类:

例 38.6. inheritance.xel

class Art {
  Art() {
    print("Art constructor");
  }
}

class Drawing extends Art {
  Drawing() {
    print("Drawing constructor");
  }
}

class Cartoon extends Drawing {
  Cartoon() {
    print("Cartoon constructor");
  }  
}

我们在交互式窗口中测试一下:

> require "../sample/inheritance.xel";
> define c = new Cartoon();
Cartoon constructor
Drawing constructor
Art constructor
> c.getClass();
class org.operamasks.el.eval.closure.DefaultClosureObject
> c instanceof Object
true
> c is Cartoon
true
> c is Drawing
true
> c is Art
true
>

ELite中的对象,也可以继承自Java类。下例中,MyThread继承自java.lang.Thread:

例 38.7. thread.xel

class MyThread extends Thread {
    run() {
        print("MyThread is running...");
    }
}

我们现在可以执行此线程:

> require "../sample/thread.xel";
> new MyThread().start();
> MyThread is running...

同样的,ELite中的对象,可以实现一个或多个接口,但需要提醒您的是:在ELite中,您无法定义自己的接口,更确切的说法是:您根本无需定义自己的接口(在多态章节中会给您剖析原因),但您可以实现某个或多个Java中的interface。将上述线程的例子改写一下:

例 38.8. interface.xel

class MyRunnable implements Runnable {

  void run() {
      print("MyRunnable is runing");
  }
}

调用代码如下:

> require "../sample/interface.xel";
> define r = new MyRunnable();
> new Thread(r).start();
> MyRunnable is runing

38.14.3. 多态

多态(Polymorphism)是面向对象语言的核心特征之一,同时,也是函数式编程的重要特征。但在面向对象语言中的多态,与函数式编程中的多态是有不同色彩不同含义的。我们一般称面向对象语言中的多态为“包容性多态”(Inclusion Polymorphism),而把函数式编程中的多态称为“参数型多态”(Parametric polymorphism)。那么,这两者有什么差异呢?

在面向对象的语言如Java中,假设有一个方法,它接受的参数类型为A 对象(此处是指A Class),那么,我们传递给此方法的参数必须是A对象本身(此处是指A Instance)或者它的子类;如果参数类型是B接口,那么,传递的参数必须是实现B接口的对象,换言之,形参的类型必须能够包容实参的类型(形参是实参的超集),而在方法体内,是把实参当成形参类型来对待的,因此,我们把这种多态称之为“包容性多态”。

但在函数式编程中,则不一样了,它在函数的形参声明中,并不需要指定任何类型,换言之,传递进来的实参可以是任意类型,唯一的前提是:在函数体内,解释器会把实参当成指定的形参来用,只要实参能够“满足”这种调用即可。

说起来有些困惑。在Ruby中,有一个戏谑化的术语:duck type,可以加深您的理解。所谓duck type是这样定义的:如果对象能够像鸭子那样行走,像鸭子那样呱呱叫,那么解释器会很高兴的把它当作鸭子来对待。

举一个简单的例子,假设我们现在要绘制不同类型的图形,在Java中,我们可能会这样定义:

例 38.9. Java示例代码

package demo;

public interface Figure {
  public void draw();
}
package demo;

public class Rectangle implements Figure {

  public void draw() {
    System.out.println("Rectangle drawing...");
  }

}
package demo;

public class Circle implements Figure {

  public void draw() {
    System.out.println("Circle drawing...");
  }

}
public class Painter {

  public static void main(String[] args) {
    Painter painter = new Painter();
    painter.paint(new Rectangle());
    painter.paint(new Circle());
  }

  public void paint(Figure f) {
    f.draw();
  }
}

我们可以看到,在Java中,为了对不同的形状进行绘制,我们首先会抽象出来一个接口:Figure,它有一个方法draw,然后,针对此接口,我们有不同的实现,分别是Rectangle和Circle。并且,在 paint 方法中,它只能接受实现了 Figure 接口的对象实例。

如果我们把上述例子用ELite改写一下,我们可能会这样做:

例 38.10. polymorphism.xel

class Circle  {
  public void draw() {
    print("Circle drawing...");
  }
}

class Rectangle   {
  public void draw() {
    print("Rectangle drawing...");
  }
}

define paint(f) { f.draw() };

paint(new Rectangle());

paint(new Circle());

运行效果如下:

> require "../sample/polymorphism.xel";
Rectangle drawing...
Circle drawing...
>

我们可以看到,在ELite中,我们并没有为 Rectangle与Circle这两个对象声明一个通用的接口,相反,在paint函数中,我们假定传递进来的参数都有 draw 方法,因此,只要传递进来的参数符合这个条件,解释器就能够正常运行。这就是所谓的 duke type:传递进来的对象,看起来象鸭子(拥有draw方法),我们就假定它是鸭子。

事实上,ELite的这种参数型多态,较面向对象的包容性多态,拥有较多的便利。

再举一个简单的例子:在Java中的java.lang.Math对象中,有取绝对值的abs函数,在Math中是这样定义的:

例 38.11. java.lang.Math中的部分代码

public static int abs(int a) {
    return (a < 0) ? -a : a;
}

public static long abs(long a) {
    return (a < 0) ? -a : a;
}

public static float abs(float a) {
    return (a <= 0.0F) ? 0.0F - a : a;
}

public static double abs(double a) {
    return (a <= 0.0D) ? 0.0D - a : a;
}

为了能够对int、long、float、double等不同类型的参数进行取绝对值操作,我们需要定义四个方法,以处理不同的参数类型。但在ELite中,只需要定义一个方法即可:

> define abs(x) { x < 0 ? -x : x};
> abs(-1);
1
> abs(-1.0);
1.0