ELENA编程语言简介






4.54/5 (62投票s)
ELENA是一种通用、面向对象、多态的语言,具有后期绑定
引言
编程语言是一项艰巨的开发和学习任务。所以遇到一门新语言,你可能会问:为什么还要有另一门编程语言?简短的回答是为了尝试一些不同的东西。为什么不把消息看作是普通的语言元素,可以进行不同的操作:加载、传递、修改、分派?我们能在不使用反射的情况下做到这一点吗?这就是我的动机。ELENA旨在促进动态、多态代码的编写,融合了动态和静态语言的元素。
ELENA是一种具有后期绑定的通用语言。它是多范式的,结合了函数式和面向对象编程的特性。它同时支持强类型和弱类型,运行时转换,原始类型的装箱和拆箱,直接使用外部库。提供了丰富的工具集来处理消息分派:多方法、消息限定、通用消息处理器。可以通过混入(mixins)和类型接口来模拟多重继承。内置脚本引擎允许将自定义脚本集成到您的应用程序中。支持独立应用程序和虚拟机客户端。
总而言之,让我们列举一些ELENA的功能
- 免费开源(MIT许可)
- 完整的源代码
- GUI IDE 和调试器
- 可选类型
- 多重分派/多方法
- 支持可变参数方法
- 支持可yield方法
- 闭包
- Mixins
- 类型接口/转换
- 类/代码模板
- 脚本引擎
支持的平台
ELENA **6.0** 支持 *x86 (Windows / Linux)*、*x86-64(Windows / Linux)*、*Aarch64(Linux)* 和 *PPC64le(Linux)*。
与其他语言的比较
主要问题是和什么语言比较?外面有成百上千种语言,其中几十种相当流行。而且大多数都是图灵完备的。所以这种比较总是相当主观的。但有一个专门的网站对此——Rosetta Code。你可以比较ELENA实现的近两百个任务与你可能想到的任何语言——http://rosettacode.org/wiki/Category:Elena。
我们来举一个——闭包/值捕获
在C#中
var captor = (Func<int, Func<int>>)(number => () => number * number);
var functions = Enumerable.Range(0, 10).Select(captor);
foreach (var function in functions.Take(9))
{
Console.WriteLine(function());
}
在Clojure中
(def funcs (map #(fn [] (* % %)) (range 11)))
(printf "%d\n%d\n" ((nth funcs 3)) ((nth funcs 4)))
在Smalltalk中
funcs := (1 to: 10) collect: [ :i | [ i * i ] ] .
(funcs at: 3) value displayNl .
在ELENA中
var functions := Array.allocate(10).populate::(int i => {^ i * i} );
functions.forEach::(func) { console.printLine(func()) }
那么,哪一个更好?最快的那个?这取决于编译器。最短的那个?不知道。这就像在问哪种语言更好,法语还是英语?
可移植性
它用C++和汇编编写。所以支持新的CPU相当容易,只需要移植汇编部分。
可写性和可读性
当前的语法类似于C风格的语法(尽管不完全兼容)。所以它具有相同的优点/缺点。一般来说,ELENA使用LL(1)语法。可以通过模板扩展语言的语法结构。例如,if,while,for——是用户定义的代码模板模式。该语言没有内置关键字。它们也是用户定义的。(因此,它可以相当容易地本地化)。
该语言默认使用UTF-8源文件。
它的优点和缺点是什么?
缺点:编译器可能被认为是半工业级的(与业余编译器相比相当先进,但仍需大量努力才能获得良好的代码性能),用户基数非常小(实际上不存在),代码库很小。
优点:混入,消息分派,多方法,集成到编译器脚本引擎,一个调试器,完整的Unicode支持(包括关键字/属性),模板(类/运算符)。
在哪里可以找到编译器和源代码的可执行文件?
- ELENA主页:https://elena-lang.github.io/
- 您可以下载最新版本于
https://github.com/ELENA-LANG/elena-lang/releases
ELENA开发社区
- Twitter / X:https://twitter.com/elena_language/
- GitHub:https://github.com/orgs/ELENA-LANG/discussions
构建ELENA编译器
ELENA编译器完全用C++实现。为了编译,编译器不需要外部依赖,如Flex、Bison……您只需要Visual Studio 2017或更高版本。
单元测试需要googletest
Alex实现了自己的语法生成器、汇编器以及他需要的工具。
要构建编译器,您可以打开CMD,转到elena的根文件夹并键入
- recompile60.bat
或者,要生成发行版文件
- build\create_package_x86.bat elena-lang-6.2.0-win32
入门
在开始用100%面向对象语言编程之前,了解一门真正面向对象语言的基本概念是极其重要的。
基本OOP概念和术语
ELENA的基本概念是什么?
- 对象
- 类
- 字段
- 参考文献
- 方法
- 消息
- 继承
- 接收者
- 动态绑定
类
在OOP中,类定义了一组对该类型的所有对象都通用的属性和方法。它可能包含创建其实例的特殊方法,称为构造函数。
对象
对象是类的实例。通常,对象存储在动态内存中。对象有一个特殊情况——单例,它只存在一个实例。*请注意,在ELENA中,类本身就是一个对象,可以像任何其他对象一样使用。*
字段
字段,也称为实例变量,是在类中声明的变量。这些变量只能通过方法来读取,以分配或读取字段的内容。支持*静态*字段。
参考文献
在C++中,我们谈论对象的“指针”,而在C#中,我们谈论对象的“引用”。这两个概念基本相同,引用是通过直接指针实现的,而指针就是内存地址。在ELENA中,我们使用引用。
方法
方法就像函数,只是它应用于特定的对象。我们也说方法在对象上“调用”或“发送到”对象。相关的对象被称为“接收者”。每个方法都在接收对象上调用。在C++或C#中,接收者称为“this”,在ELENA中——称为“self”。
ELENA中的方法类似于C#和C++中的方法,在这些语言中它们被称为“成员函数”。方法可以接受参数,并始终返回一个结果(如果未提供结果,则返回“self”引用)。方法体是一系列可执行语句。方法就像其他语言一样,从表达式中调用。
消息
“方法”和“消息”之间有一个重要的区别。方法是一段代码,而消息是发送出去的东西。方法类似于函数。在这个类比中,发送消息类似于调用函数。调用方法的表达式称为“消息发送表达式”。
ELENA的术语清楚地区分了“消息”和“方法”。消息发送表达式将消息发送到对象。对象如何响应消息取决于对象的类。不同类的对象将以不同的方式响应同一消息,因为它们将调用不同的方法。通用方法可以接受具有指定签名(参数类型)的任何消息。
“Hello World!”应用程序
当我们学习或只是了解任何新的编程语言时,一个很好的起点总是著名的程序“Hello world!
”。
为了创建我们的第一个程序,我们必须获取ELENA编译器,为此,您只需在此链接下载编译器:此。
下载并提取所有内容到您选择的文件夹后……
重要提示:您需要将“bin”文件夹添加到系统路径中。
将bin文件夹添加到系统路径中后,这是生成可执行文件以访问elena运行时所必需的,例如,当异常发生时,该运行时会显示异常发生的类和方法。
让我们开始吧,首先,我们需要打开ELENA IDE,为此,您可以键入CMD“elide”或直接打开名为“elide”的可执行文件。
打开elide IDE后,我们将创建一个新项目,为此,只需转到菜单“文件->新建->项目”,当您单击项目菜单时,它将打开一个如下所示的对话框。
每个字段的含义
- 类型:您将创建的应用程序的类型,例如GUI、控制台、在VM上运行的控制台应用程序……
- 应用程序的命名空间
- 关于未解析引用的警告复选框
- 编译器的其他选项
- 目标文件名:输出文件名
- 输出路径:ELENA对象的输出
- 启用或禁用调试器
- 用于测试应用程序的命令行参数
对于本文,我们将使用这些特定的设置
单击“确定”后,我们需要创建一个新的源文件,为此,只需转到菜单“文件->源文件”或使用快捷键“Ctrl+N”。
现在,我们可以开始编码了……这是Hello World
的代码。
public program()
{
system'console.writeLine("Hello World!");
}
编写完此代码后,您需要将项目和源代码保存在某个目录中,然后只需构建它。
理解代码
public program()
在这里,我们声明了一个public
函数(一个包含function
消息的static class
)。默认情况下,“program
”是程序的入口点,但可以更改。
system'console.writeLine("Hello World!");
在这一行代码中,我们有一些非常有趣的信息需要关注
- “
system
”:这是一个命名空间,命名空间本身可以包含由撇号分隔的子元素 - “
console
”:这是一个类 - “
writeLine
”:这是一个消息 - “
Hello World!
”:这是消息的参数 - “
;
”:终止符
这对我们意味着什么?
这意味着我们向系统命名空间中控制台类的writeLine方法发送了一个消息,参数为“Hello World!”。
与其他语言(如C#、C++、Python)一样,我们可以添加一个特殊的关键字来“导入”一些命名空间,这样我们可以“缩减”代码。
因此,“import”关键字的hello world示例将如下所示:
import system;
public program()
{
console.writeLine("Hello World!");
}
我们用这个程序遇到的唯一问题是我们无法读取输出……为了解决这个问题,我们只需要再使用一个方法,并且我们可以用两种方式使用这个方法。
First
import system;
public program()
{
console.writeLine("Hello World!");
console.readChar();
}
第二种
import system;
public program()
{
console.writeLine("Hello World!").readChar()
}
最后一个分号可以省略。
第二种编写相同程序的方式有效,因为在ELENA中,每个方法都返回一个对象。
声明变量
变量是程序内存中(例如在函数堆栈中)的一个命名位置,其中包含一个值。在最简单的情况下,它是对象或null指针的引用——**nil**值。变量名可以包含任何UTF-8字符(特殊字符、空格除外;首字符不能是数字),并且区分大小写。没有保留关键字。不允许使用重复名称的变量(在同一作用域中声明)。
public program()
{
var myFirstVariable := "Hello";
var Δ := 1;
}
新的变量声明以*属性* **var**开头,绑定的值放在赋值运算符之后(*注意:ELENA中的赋值运算符和等价运算符不同*)。值也可以是表达式的结果。
声明和赋值可以分开
public program()
{
var myFirstVariable; // declaring a new variable myFirstVariable
myFirstVariable := "Hello"; // assigning a value
var Δ := 1; // declaring and assigning in the same statement
Δ := Δ + 1;
}
如果变量在没有赋值的情况下声明,其值为**nil**。
变量类型在ELENA中是可选的。如果未显式声明类型,则变量是无类型的(或更准确地说,其类型为**system'Object**,即超类)。这意味着不进行类型检查,并且可以分配任何值。
var v := "Hello";
v := 3;
实际上,在许多情况下我们确实关心变量类型。因此,让我们创建一个强类型变量。必需的类型(类名或别名)应写在变量名前面。
var string v := "Hello";
其中**string**是**system'String**类的别名。
**var**属性在这种情况下是可选的,我们可以简化代码。
string v := "Hello";
变量类型限制了变量的可能值。与静态类型语言的最大区别在于,类型检查发生在**运行时**(尽管编译器会警告如果两个变量都是强类型的,则找不到转换例程)。因此,例如,以下代码可以编译,但会生成运行时错误。
public program()
{
var o := 2;
string v := o;
}
输出是:
system'IntNumber : Method #cast[0] not found
Call stack:
system'Exception#class.new[1]:exceptions.l(96)
system'MethodNotFoundException#class.new[1]:exceptions.l(1)
system'MethodNotFoundException#class.new[2]:exceptions.l(190)
system'$private'entry.#invoke[0]:win32_app.l(42)
mytest'program.#invoke[0]:test.l(6)
system'$private'entry.#invoke[0]:win32_app.l(37)
system'#startUp:win32_app.l(55)
如果声明了适当的转换方法,则值可以成功转换。
var o := 2;
real r := o;
在这种情况下,**system'IntNumber**类支持转换为**system'RealNumber**类型。
可以使用**auto**属性从其初始值设定项自动推断变量类型。
auto r := 1.2r;
变量类型通过赋值值——数字文字——**1.2r**来定义,即**system'RealNumber**(或**real**)。
控制流
ELENA支持丰富的控制流结构:分支、循环、异常处理。它们实现为代码模板,并且可以轻松扩展。分支和循环语句由布尔表达式控制。尽管分支运算符也可以处理非布尔结果。异常用于优雅地处理关键错误。异常可以包含堆栈跟踪以标识错误源。
布尔类型
**BoolValue**(别名**bool**)是一种布尔类型,只有两个可能的值:**true**和**false**。它是比较操作的典型结果。控制流语句需要布尔值(尽管也可以使用非布尔结果,前提是存在转换操作)。它支持典型的比较和逻辑操作。
console
.printLine("true==false : ",true == false)
.printLine("true!=false : ",true != false)
.printLine("true and false : ",true && false)
.printLine("true or false : ",true || false)
.printLine("true xor false : ",true ^^ false);
结果是:
true==false : false
true!=false : true
true and false : false
true or false : true
true xor false : true
布尔反演通过**Inverted**属性实现。
console.printLine("not ",true,"=",true.Inverted)
结果是:
not true=false
布尔值可以直接用于分支操作。可以向其发送带有两个无参数函数的**if**消息。**true**值将执行第一个函数,**false**——第二个函数。
import extensions;
public program()
{
var n := 2;
(n == 2).if({ console.printLine("true!") },{ console.printLine("false!") });
}
结果是:
true!
还支持**iif**消息:true值将返回第一个参数,false——第二个参数。
var n := 2;
console.printLine((n != 2).iif("true", "false") )
输出如预期:
false
分支运算符
ELENA支持内置分支运算符**?**。它要求左操作数是布尔值。真部分参数紧跟在运算符后面。可选的假部分由感叹号引入。
import extensions;
public program()
{
var n := 2;
(n == 2) ? { console.printLine("n==2") };
(n == 3) ? { console.printLine("n==3") } ! { console.printLine("n!=3") }
}
将打印以下结果:
n==2
n!=3
如果右操作数不是函数,运算符将返回真部分(如果左操作数为true)和假部分(否则)。
var n := 2;
console.printLine(n == 2 ? "n==2" : "n!=2");
结果类似:
n==2
分支语句
ELENA不为控制语句保留关键字。但有一组代码模板有助于使代码更具可读性。程序员可以自行扩展此列表。
让我们从许多人都熟悉的**if-else**开始。
var n := 2;
if (n == 2)
{
console.printLine("n==2")
}
else
{
console.printLine("n!=2")
}
结果是:
n==2
如果我们只需要真部分,可以省略else块。
if (n == 2)
{
console.printLine("n==2")
}
或者,可以只写else部分。
ifnot (n == 2)
{
console.printLine("n!=2")
}
如果代码块只包含单个语句,我们可以直接编写该语句——如果它是最后一个块。
if(n == 2)
console.printLine("n==2");
条件类型转换
可以使用特殊的**if-is**语句来尝试类型转换对象。
add(o) { if (o; is int n) { ^ self.add(n); }; console.printLine("unsupported") }
如果变量**o**可以转换为整数,并且新变量n包含转换后的值,则将执行花括号内的代码。
循环语句
ELENA提供了几种类型的循环结构:for,while,until,do-until,do-while。
**FOR**循环用于指定迭代次数来执行循环体。它由初始化、条件和迭代表达式(以分号分隔)以及主循环体组成。
import extensions;
public program()
{
for (var i := 0; i < 5; i++)
{
console.printLine(i)
}
}
第一个表达式声明并初始化循环变量,第二个表达式在变量处于迭代范围之内时进行检查,第三个表达式在主循环体执行后增加循环变量。
结果将是。
0
1
2
3
4
类似地,如果循环体只包含单个语句,我们可以省略括号。
import extensions;
public program()
{
for (var i := 0; i < 5; i++)
console.printLine(i)
}
如果我们需要在检查条件之前执行迭代步骤,我们可以跳过最后一个表达式。在这种情况下,第一个表达式将在每一步同时初始化和迭代循环变量。条件(第二个表达式)的工作方式类似,循环将一直继续,直到遇到假值。结果是,迭代步骤保证至少执行一次。
import extensions;
public program()
{
var sum := 0;
for(var line := console.readLine(); line != emptyString)
{
sum += line.toInt()
};
console.printLine("The sum is ", sum)
}
在此示例中,我们从控制台读取数字,直到遇到空行,并打印总和。**readLine**方法从控制台读取下一行字符。**emptyString**是空字符串字面量常量。扩展方法**toInt**将字符串转换为整数。
输出如下:
1
3
5
The sum is 9
**WHILE**是一个经典的循环结构,它在条件为真时执行代码。让我们编写一个枚举循环。**枚举器**是特殊的*对象*,它们枚举集合,逐个返回成员。我们将枚举一个字符串。
import extensions;
public program()
{
auto s := "♥♦♣♠";
auto e := s.enumerator();
while (e.next()) {
console.printLine(e.get())
}
}
结果将是。
♥
♦
♣
♠
相反的结构是**UNTIL**。它重复代码,直到条件为真。让我们重复代码,直到数字不为零。
import extensions;
public program()
{
var n := 23;
until(n == 0)
{
console.printLine(n);
n /= 3
}
}
输出将是:
23
7
2
很明显,这两种循环结构都是可互换的。选择可能取决于语义含义:是为了强调一个条件还是另一个条件。
如果我们想至少执行一次循环,无论条件如何,我们必须使用**DO-WHILE**或**DO-UNTIL**。让我们计算5的阶乘。
import extensions;
public program()
{
int counter := 5;
int factorial := 1;
do {
factorial *= counter;
counter -= 1
}
while(counter > 0);
console.printLine(factorial)
}
请注意,我们必须在**while**令牌后加上冒号。
结果如预期:
120
可以使用特殊的**foreach**循环来轻松遍历枚举值。
import extensions;
import algorithms;
public program()
{
var array := new int[] { 2,3,4 };
foreach(auto item; in array.quickSort()) {
console.writeLine(item);
}
}
?. 和 \. 运算符
ELENA支持以下模板运算符:doIfNotNil
(?., 类似于C#) 和 tryOrReturn
(.\)。
doIfNotNil
仅当**左操作数**不为nil时才发送消息。
tryOrReturn
尝试向**左操作数**发送消息并返回操作的结果。如果对象不处理它——则返回nil
。
import system;
import extensions;
B
{
test(string command)
{
console.printLine(command);
}
}
C;
public program()
{
var a := nil;
var b := new B();
var c := new C();
a?.test("Fired a?.test");
b?.test("Fired b?.test");
a\.test("Fired a\.test");
b\.test("Fired b\.test");
c\.test("Fired c\.test");
console.readLine();
}
输出是:
Fired b?.test
Fired a\.test
它也可以与函数调用一起使用。
public program()
{
// declaring a nested function
Func f1 := { console.writeLine("Fired 'f1'") };
Func f2 := nil;
f1?.();
f2?.();
console.readLine();
}
输出是:
Fired 'f1'
符号
符号是命名的表达式,可以用来使代码更具可读性。典型的符号是常量声明。尽管它们可以更多。符号用于声明全局单例实例。模块初始化代码是通过它们实现的。它们可以是私有的或公共的。
值/常量
符号的典型用例是可以在代码中进一步重用的值。
import extensions;
import extensions'math;
real PiOver2 = RealNumber.Pi / 2;
public program()
{
console.printLine("sin(",PiOver2,")=", sin(PiOver2))
}
在这里,我们声明了一个私有的强类型符号PiOver2。可以省略类型。在这种情况下,它将是一个弱类型值。结果是:
sin(1.570796326795)=1.0
如果符号应该是公共的(可以在模块外部访问),则应在类型和名称之前加上**public**属性。
如果符号的值可以在编译时计算,我们可以声明一个常量符号。
import extensions;
import extensions'math;
public const int N = 3;
public program()
{
for (int i := 0; i < N; i++)
{
console.printLine(i,"^2=", sqr(i))
}
}
输出是:
0^2=0
1^2=1
2^2=4
静态符号
普通符号在每次使用时都会被计算。静态符号是一种特殊情况,它只在第一次调用时初始化(所谓的延迟加载),并且其值在每次后续调用时都会被重复使用。这是实现单例(如果它不是无状态的)的首选方式。
import extensions;
import extensions'math;
static inputedNumber = console.print("Enter the number:").readLine().toInt();
public program()
{
console.printLine(inputedNumber,"^3 = ", power(inputedNumber, 3))
}
在此示例中,该符号仅被计算一次,并且输入的(值)被保留,因此数字应该只输入一次。
Enter the number:4
4^3 = 64
预加载符号
预加载符号是一种特殊符号,如果程序中使用命名空间成员,则在程序启动时会自动对其进行求值。因此,它可以用于模块初始化。
import extensions;
preloaded onModuleUse = console.printLine("Starting");
public program()
{
console.printLine("Hello World")
}
输出是:
Starting
Hello World
Types
在编程中,数据类型(或简称类型)定义了数据的角色:数字、文本等。在面向对象编程中,这个概念已经扩展到了类,类封装了数据及其操作。数据存储在字段中,操作通过方法完成。在ELENA中,类型和类在大多数情况下含义相同,并且可以互换使用而不丢失含义(除了原始类型)。类基于继承形成层次结构。这意味着每个类(除了超类——**system'Object**)都有一个父类和一个父类。
编程语言可以具有静态或动态类型系统。在OOP中,类可以被制成多态的,这解决了静态类型问题的一部分。动态语言在运行时解析类型。这会影响它们的性能。现代编程语言因此试图结合这些方法。在ELENA中,类型是动态的,并在运行时解析。从这个意义上说,您可以编写程序而无需显式指定类型。但在大多数情况下,应指定类型以使代码更具可读性并提高其性能。
在动态语言中,调用方法的*操作*通常称为发送消息。类通过调用适当的方法(具有相同的名称和签名)来响应消息。在正常情况下,这是通过在方法表中搜索适当的处理程序来完成的。这需要时间。为了解决这个问题,ELENA允许声明**sealed**或**closed**类。密封类的*方法*可以在编译时解析并直接调用。关闭的类(或接口)的方法可以通过方法表在编译时解析。但密封类不能被继承。接口可以被继承,但不能声明新方法(私有方法除外)。
在ELENA中,类型转换是通过发送特殊方法——转换处理程序——来实现的。任何类都可以转换为另一个类,只要它处理该消息。否则,将引发异常。
原始类型
在大多数情况下,类和类型意味着相同。唯一的例外是原始类型。原始类型是内置的,只支持预定义的*操作*。它们是经典数据类型,因为它们是纯数据。ELENA支持以下类型:
类型 | 大小 | 描述 |
---|---|---|
__float | 8 | 64位浮点数 |
__int | 1 | 8位整数 |
__int | 2 | 16位整数 |
__int | 4 | 32位整数 |
__int | 8 | 64位整数 |
__raw | 原始数据 | |
__ptr | 4 | 32位指针 |
__mssg | 4 | 消息引用 |
__mssg | 8 | 扩展引用 |
__subj | 4 | 消息名称引用 |
__symbol | 4 | 符号引用 |
__string | 数组 |
原始类型可以通过适当的包装器在程序中使用。每次在代码中使用原始类型(原始操作除外)时,它都会被装箱到其包装器中。操作后,它会再次被拆箱。由于性能问题,不对原始操作进行任何验证,因此由程序员(或包装器类)正确处理。
类
撇开原始类型不谈,每个程序对象都是类的实例。类是封装数据(字段)及其操作(方法)的结构。类可以指定构造函数来创建对象,并指定转换例程来将其从另一个对象转换。
声明和使用类对于熟悉C风格面向对象语言的任何人来说都很简单。
import extensions;
// declaring a class named A
class A
{
// declaring a field named a
field a;
// declaring a default constructor
constructor()
{
a := "Some value"
}
// declaring a method named printMe
method printMe()
{
console.printLine(a)
}
}
public program()
{
// creating an instance of A class
var o := new A();
// calling a method
o.printMe()
}
输出是:
Some value
关键字**class**指定一个普通类。类体包含在花括号中。类名紧跟在类体之前声明。字段可以在类体内的任何位置声明。它可以以**field**属性开头。隐式构造函数名为**constructor**。方法可以用**method**关键字声明。代码可以放在花括号内。
类形成层次结构。除了超类之外,每个类都有一个父类。如果未指定父类,则该类继承**system'Object**(超类)。子类可以覆盖其父类的任何非密封方法。子类实例同时也是父类型的实例(所谓的is-a关系)。因此,我们可以将子类分配给父类型变量。但将被覆盖的方法将被使用(多态代码)。在弱类型(或无类型)的情况下,这也是如此(我们只假设所有变量都是超类型的)。
import extensions;
class Parent
{
field f;
constructor()
{
f := "some value"
}
printMe()
{
console.printLine("parent:",f)
}
}
class Child : Parent
{
printMe()
{
console.printLine("child:",f)
}
}
public program()
{
Parent p := new Parent();
Child c := new Child();
p.printMe();
c.printMe()
}
输出将是:
parent:some value
child:some value
父子句应跟在类名之后,并用冒号引入。要覆盖方法,我们只需使用相同的名称和签名再次声明它。
在ELENA中,类型(或类)可以直接在代码中像任何其他符号一样使用。唯一的区别是我们不能调用默认构造函数。应该使用命名构造函数代替。*注意:默认构造函数将自动调用。*
import extensions;
// declaring a class named A
class A
{
field a;
// default constructor
constructor()
{
a := "a"
}
// explicit constructor named new
constructor new()
{
// default constructor is called automatically
}
}
// declaring a class named B
class B
{
field b;
// default constructor
constructor()
{
b := 2
}
// explicit constructor named new
constructor new()
{
// default constructor is called automatically
}
}
// factory function
factory(class)
// sending a message - name and returning a result of the operation
= class.new();
public program()
{
// creating objects using a class directly
var a := factory(A);
var b := factory(B);
// printing the object types
console
.printLine(a)
.printLine(b);
}
输出是:
mylib'$private'A
mylib'$private'B
默认情况下,类被声明为私有。这意味着它不能在命名空间外部访问。对于库,我们希望重用它。所以我们必须提供一个公共属性。
public class B
{
}
现在,类可以通过引用在库外部或内部访问。
public program()
{
var b := new mylib'B()
}
引用(或完整名称)由一个命名空间(其本身可以包含由撇号分隔的子命名空间)和一个由撇号分隔的实际名称组成。
抽象类
抽象类是一种特殊类,用作创建符合其协议或其支持的操作集的特定类的基础。抽象类不能直接实例化。它与接口不同,子类可以扩展其功能(声明新方法)。抽象类可以包含抽象方法和普通方法。抽象方法必须有一个空体(分号)。
import extensions;
abstract class Bike
{
abstract run();
}
class Honda4 : Bike
{
// overriding an abstract method
run()
{
console.printLine("running safely")
}
}
public program()
{
Bike obj := new Honda4();
obj.run()
}
输出将是:
running safely
基于抽象类的类必须实现所有父抽象方法。如果子类添加了新的抽象方法或仅实现了部分父抽象方法,它也应被声明为抽象。
abstract class BikeWithTrolley : Bike
{
abstract pack();
}
接口
接口是抽象类的一种特殊情况。出于性能原因,不能为接口子类声明新方法(私有方法除外)。因此,接口方法可以半直接调用(通过方法表)。接口方法可以是抽象的和普通的(就像在抽象类中一样)。
interface IObserver
{
// declaring an interface abstract method
abstract notify();
}
虽然我们可以直接继承接口。
class Oberver : IObserver
{
notify()
{
console.printLine("I'm notified")
}
}
但这是不切实际的,因为我们无法扩展类功能。相反,我们可以使用一个名为**interface**的模板。
import extensions;
// declaring an interface
interface IObserver
{
// declaring an interface method to be implemented
abstract notify();
}
// creating a class supporting an interface
class MyClass : interface<IObserver>
{
// implementing an interface method
notify()
{
console.printLine("I'm notified")
}
// implementing some other functionality
doSomework()
{
console.printLine("I'm doing some work")
}
}
// simple routine to invoke the interface
sendNotification(IObserver observer)
{
observer.notify()
}
public program()
{
// creating an intance of MyClass
auto myObject := new MyClass();
// do some work
myObject.doSomework();
// passing the interface implementation to the function by
// explicit typecasting the object
sendNotification(myObject :as IObserver);
}
输出是:
I'm doing some work
I'm notified
类可以实现多个接口(因此我们可以绕过单重继承问题)。
import extensions;
// declaring an interface
interface IObserver
{
// declaring an interface method to be implemented
abstract notify();
}
// declaring the second interface
interface IWork
{
abstract doSomework();
}
// creating a class supporting both interfaces
class MyClass : interface<IObserver>, interface<IWork>
{
// implementing an interface method
notify()
{
console.printLine("I'm notified")
}
// implementing the second interface
doSomework()
{
console.printLine("I'm doing some work")
}
}
// simple routine to invoke the interface
sendNotification(IObserver observer)
{
observer.notify()
}
public program()
{
// creating an intance of MyClass
auto myObject := new MyClass();
// implicitly typecast it to the second interface
IWork work := myObject;
// use the second interface
work.doSomework();
// passing the interface implementation to the function by
// explicit typecasting the object
sendNotification(myObject :as IObserver);
}
结果相同:
I'm doing some work
I'm notified
单例
单例是一种特殊的类,无法初始化(不允许构造函数),并且只有一个实例。单例大多是无状态的(尽管它们可以有静态字段)。它们总是被密封的。
单例类用**singleton**属性声明。它可以通过类引用直接引用。
import extensions;
// declaring a singleton
public singleton StringHelpers
{
// Gets the first character of a string.
char first(string str)
{
^ str[0]
}
}
public program()
{
var str := "My string";
// calling a singleton method
console.printLine("Calling StringHelpers.First(""",str,""")=",
StringHelpers.first(str))
}
输出将是:
Calling StringHelpers.First("My string")=M
结构体
结构体是一种特殊的类,它们存储在原地,而不是存储在内存堆中的引用。结构体是密封的(意味着它们不能被继承)。它们主要包含小数据值(如数字、处理程序、指针)。所有原始数据处理程序都是结构体。
结构体字段可以是原始类型,也可以是另一个结构体。不允许引用类型。因此,所有字段都应该是强类型的。
在大多数情况下,结构体的使用都相当直接。
import extensions;
// declaring a struct
struct Record
{
// declaring struct fields
int x;
int y;
// declaring struct constructor
constructor(int x, int y)
{
// using this prefix to distinguish a class member from the local one
this x := x;
this y := y
}
printMe()
{
console.printLine("Record(",x,",",y,")");
}
}
public program()
{
// creating a struct
auto r := new Record(2, 4);
// invoking struct method
r.printMe()
}
结果将是。
Record(2,4)
请注意,结构体是就地存储的。这意味着在我们的示例中,对象是在方法堆栈中声明的。每次它被用作弱类型时,它都会被装箱,并在操作后被拆箱。为了提高性能,我们可以将结构体声明为常量,以避免拆箱操作。例如,所有数字都是常量结构体。
// declaring a constant struct
public const struct IntNumber : IntBaseNumber
{
// a field is a primitive 32-bit integer type
embeddable __int theValue[4];
...
字符串
字符串是一种特殊类型的类(结构体和非结构体类),用于包含数组。因此,类的长度是可变的。不允许使用默认构造函数(无参数)。
public const struct String : BaseValue
{
__string byte[] theArray;
constructor allocate(int size)
= new byte[](size + 1);
它只能包含一个标记为**__string**的字段。
扩展
扩展是特殊的无状态类,用于声明扩展方法。扩展方法允许您在不创建新派生类型、重新编译或修改原始类型的情况下扩展原始类功能。在扩展类中声明的每个方法都是一个扩展方法。
在正常情况下,扩展类从不直接使用(混入除外)。要在其他模块中使用,它们应声明为公共的。要开始使用扩展,只需在与其将使用的代码相同的命名空间中声明它即可。如果扩展在另一个模块中声明,则应使用**import**语句将该模块包含到代码中。
import extensions;
// declaring an extension
public extension MyExtension
{
// every member of the extension is an extension method
printMe()
{
console.printLine("I'm printing ", self);
}
}
public program()
{
// invoking an extension method for various types
2.printMe();
"abc".printMe();
2.3r.printMe()
}
结果是:
I'm printing 2
I'm printing abc
I'm printing 2.3
扩展方法在ELENA的许多地方都有使用。例如,**print**和**printLine**是扩展的可变参数方法。如果类已经有一个同名的方法,它将被使用而不是扩展方法。例如,如果我们用一个包含**printMe**方法的新类来扩展我们之前的示例。
MyClass
{
printMe()
{
console.printLine("I'm printing myself");
}
}
public program()
{
auto o := new MyClass();
o.printMe();
}
将调用正确的方法。
I'm printing myself
但是,如果对象是弱类型的,将调用扩展。
public program()
{
var o := new MyClass();
o.printMe();
}
输出将是:
I'm printing mytest'$private'MyClass
因此,不混合使用扩展和普通方法名称是一个好习惯。
扩展可以是弱的,这意味着它们可以扩展任何对象(或**system'Object**类的实例)。但我们总是可以指定确切的扩展目标。
// declaring an extension of MyClass
public extension MyStrongExtension : MyClass
{
printMe()
{
console.printLine("I'm printing MyClass");
}
}
MyClass
{
}
public program()
{
auto o := new MyClass();
o.printMe();
}
输出将是:
I'm printing MyClass
如果扩展目标不同,则可以具有多个同名的扩展方法。
扩展可以在编译和运行时进行解析。编译器尝试直接解析所有扩展。但如果存在多个相似的扩展,它将生成一个运行时分派器。
// declaring classes
A;
B;
// declaring several strong-typed extensions with the same name
extension AOp : A
{
// extending an instance of A
whoAmI()
{
console.printLine("I'm instance of A")
}
}
extension BOp : B
{
// extending an instance of B
whoAmI()
{
console.printLine("I'm instance of B")
}
}
public program()
{
// declaring weak-typed variables
var a := new A();
var b := new B();
// resolving an extension in compile-time is not possible
// so the run-time dispatcher will be used
a.whoAmI();
b.whoAmI();
}
输出将是:
I'm instance of A
I'm instance of B
密封/关闭类
**Sealed**和**closed**属性用于提高与类的*操作*的性能。密封类不能被继承。因此,编译器可以为该类生成直接方法调用(当然,当类型已知时)。所有结构体和单例默认都是密封的。声明一个密封类相当简单。
sealed class MySealedClass
{
}
关闭的类可以被继承,但不能声明新方法(私有方法或新字段)。所有接口都是关闭的。当类型被关闭时,编译器可以使用方法表来解析方法调用。
closed class MyBaseClass
{
// declaring a "virtual" method
myMethod() {}
}
class MyChileClass : MyBaseClass
{
// overriding a method
myMethod() {}
}
基本类型转换
让我们学习如何将基本数据类型(int
、long
、real
、string
)相互转换。我们将使用extensions模块中声明的扩展方法。
让我们将基本类型的数字转换为**string**。
import extensions;
public program()
{
int n := 20;
real r := 3.4r;
long l := 5000000000000l;
console.writeLine("n=" + n.toString());
console.writeLine("r=" + r.toString());
console.writeLine("l=" + l.toString());
}
输出是:
n=20
r=3.4
l=5000000000000
我们可以为整数使用不同的基数。
import extensions;
public program()
{
int n := 20;
long l := 5000000000000l;
console.writeLine("n=0o" + n.toString(8));
console.writeLine("l=0x" + l.toString(16));
}
输出是:
n=0o24
l=0x48C27395000
反过来也是可能的——使用toInt
/ toLong
/ toReal
扩展方法。
import extensions;
public program()
{
int n := "20".toInt(); // = 20
long l := "5000000000000".toLong(); // = 5000000000000l
real r := "2.3".toReal(); // = 2.3r
}
也可以使用自定义基数。
import extensions;
public program()
{
int n := "24".toInt(8); // = 20
long l := "48C27395000".toLong(16); // = 5000000000000l
}
数值类型之间的*操作*也是可能的(只要它们兼容)。
import extensions;
public program()
{
int n := 200;
long l := n.toLong(); // = 200l
real r1 := n.toReal(); // = 200.0r
real r2 := l.toReal(); // = 200.0r
}
我们可以跳过显式转换。
import extensions; :
public program()
{
int n := 200;
long l := n; // = 200l
real r1 := n; // = 200.0r
real r2 := l; // = 200.0r
}
当实数没有小数部分时,可以进行从实数到整数的转换。
import extensions;
public program()
{
real r := 200.0r;
long l := r.toLong(); // = 200l
int n := r.toInt(); // = 200
}
我们可以通过使用Integer扩展方法忽略小数部分。
import extensions;
public program()
{
real r := 200.2r;
real r2 := r.Integer; // = 200.0r
long l := r.Integer.toLong(); // = 200l
int n := r.Integer.toInt(); // = 200
}
异常处理
异常处理旨在处理程序执行期间的意外情况。对于动态语言,典型的意外情况是向不处理该消息的对象发送消息(未声明适当的方法)。在这种情况下,将引发**MethodNotFound**异常。其他值得注意的例子是:除零、nil引用和内存不足异常。该语言提供了几种代码模板来处理这种情况。
ELENA中的典型异常继承自**system'Exception**基类。名为**raise()**的方法被声明并用于在代码中引发异常。**system**库中有几种异常:OutOfRangeException、InvalidArgumentException、InvalidOperationException、MethodNotFoundException、NotSupportedException、AbortException、CriticalException等。典型异常包含错误消息和指向异常创建时刻的调用堆栈。
在ELENA中引发异常非常简单。
divide(l,r)
{
if(r == 0)
{
InvalidArgumentException.raise()
}
else
{
^ l / r
}
}
为了处理它,我们必须编写**TRY-CATCH**或**TRY-FINALLY**结构。
public program()
{
console.print("Enter the first number:");
var l := console.readLine().toInt();
console.print("Enter the second number:");
var r := console.readLine().toInt();
try
{
console.printLine("The result is ", divide(l,r))
}
catch(InvalidArgumentException e)
{
console.printLine("The second argument cannot be a zero")
};
}
输出将是:
Enter the first number:2
Enter the second number:0
The second argument cannot be a zero
**catch**令牌后声明的匿名函数将在引发的异常与函数参数列表匹配时调用。如果我们想处理任何标准异常,我们可以使用基类异常。
catch(Exception e)
{
console.printLine("An operation error")
};
可以在嵌套类中声明多个异常处理程序。
catch::
{
function(InvalidArgumentException e)
{
console.printLine("The second argument cannot be a zero")
}
function(Exception e)
{
console.printLine("An operation error")
}
}
在我们的例子中,两个处理程序都匹配InvalidArgumentException异常(因为InvalidArgumentException是Exception的子类),但将引发适当的异常处理程序,因为它描述得更早。
如果我们想在引发异常或代码正确工作时始终执行代码,我们必须使用**TRY-FINALLY-CATCH**结构。典型用例是资源释放。
import extensions;
public program()
{
console.printLine("Try");
try
{
var o := new Object();
o.fail();
}
finally
{
console.printLine("Finally");
}
catch(Exception e)
{
console.printLine("Error!");
}
}
输出将是:
Try
Finally
Error!
如果注释掉第二行,输出将是:
Try
Finally
如果我们想在操作后执行代码,无论是否引发异常,都可以跳过catch部分。
console.printLine("Try");
try
{
var o := new Object();
o.fail();
}
finally
{
console.printLine("Finally");
}
结果是:
Try
Finally
system'Object : Method fail[0] not found
Call stack:
system'Exception#class.new[1]:exceptions.l(96)
system'MethodNotFoundException#class.new[1]:exceptions.l(1)
system'MethodNotFoundException#class.new[2]:exceptions.l(190)
mytest'program.#invoke[0]:test.l(14)
mytest'program.#invoke[0]:test.l(9)
system'$private'entry.#invoke[0]:win32_app.l(37)
system'#startUp:win32_app.l(55)
using语句
当我们要确保对象始终正确关闭时,可以使用using模式。
import extensions;
class A
{
constructor()
{
console.printLine("opening");
}
do()
{
console.printLine("doing");
}
close()
{
console.printLine("closing");
}
}
public program()
{
using(var a := new A())
{
a.do();
}
}
结果是:
opening
doing
closing
文件操作
让我们逐行读取文本文件。
import system'io;
import extensions;
public program()
{
// getting a file name from the command line argument
var fileName := program_arguments[1];
// opening a text file reader
using(auto reader := File.assign(fileName).textreader())
{
// repeating until all the lines are read
while (reader.Available)
{
// read the line
string line := reader.readLine();
// print the line
console.printLine(line);
};
}
}
让我们写入文件。
import system'io;
import extensions;
public program()
{
// getting a file name from the command line argument
var fileName := program_arguments[1];
// opening a text file writer
using(auto writer := File.assign(fileName).textwriter())
{
// repeating until the empty line is entered
for(string line := console.readLine(), line != emptyString, line := console.readLine())
{
// save to the file
writer.writeLine(line)
}
}
}
如果指定名称的文件存在,将被覆盖。
如果我们要追加到现有文件,那么我们应该使用logger()
方法,例如:
using(auto writer := File.assign(fileName).logger())
让我们读取二进制文件。
import system'io;
import extensions;
import system'collections;
public program()
{
// getting a file name from the command line argument
var fileName := program_arguments[1];
// creating a list to hold the read data
List<byte> dump := new List<byte>();
// opening a binary file reader
using(auto reader := BinaryFileReader.new(fileName))
{
// repeating until all the file is read
while (reader.Available)
{
// read the current byte into a variable b
// note the variable is declared just-in-place
reader.read(ref byte b);
// add the read byte to the dump
dump.append(b);
};
};
// print the file content as a list of bytes
console.printLine(dump.asEnumerable());
}
最后,让我们搜索文件和目录。
import system'io;
import extensions;
public program()
{
// specifying the target directory
var dir := Directory.assign(".");
var files := dir.getFiles();
var dirs := dir.getDirectories();
console.printLine("Directory ", dir);
console.printLine("files:", files.asEnumerable());
console.printLine("directories:", dirs.asEnumerable());
}
惰性表达式
惰性表达式是在访问时才计算的表达式。
import extensions;
public program()
{
var expr := (lazy:console.write("Reading a number:").readLine().toInt());
// the expression is invoked only when it is used
var sum := expr + expr;
console.printLine("the result is ", sum);
}
输出是:
Reading a number:2
Reading a number:3
the result is 5
使用可枚举对象
ELENA支持几种不同类型的可枚举对象:数组、列表、字典、范围等。
为了被用作可枚举对象,对象应该处理enumerator消息。
让我们看一个简单的例子。
import extensions;
public program()
{
var list := new{1,2.3r,"String",3l};
var it := list.enumerator();
while (it.next())
{
console.printLine(it.get())
}
}
在输出中,将打印集合的每个成员。
1
2.3
String
3
在此代码中,我们声明了一个static
数组——list
。我们向它发送enumerator
消息,并将返回的enumerator
分配给变量it
。然后,我们重复代码,直到next
消息返回false
值。get
消息返回当前的枚举成员。
相同的模式可以应用于任何可枚举对象。
import extensions;
import system'routines;
public program()
{
var range := new Range(1, 9);
var it := range.enumerator();
while (it.next())
{
console.printLine(it.get())
}
}
在这里,我们生成并打印一个从1
到10
的自然数范围。结果是:
1
2
3
4
5
6
7
8
9
与C#类似,有许多扩展方法可用于搜索、计数、过滤等。
让我们从一个简单的开始——forEach
——为每个枚举成员执行代码。上面的代码可以使用forEach
扩展方法(在system'routines模块中声明)和闭包重写。
import extensions;
import system'routines;
public program()
{
new Range(1, 9).forEach:(item){ console.printLine(item) }
}
其中(item){ <...> }
是一个带有单个参数的通用闭包。
我们可以使用现有的闭包extensions'routines'printingLn
进一步简化我们的代码。
import extensions;
import system'routines;
import extensions'routines;
public program()
{
new object[]{1,2.3r,"String",3l}.forEach:printingLn
}
在这两种情况下,输出都将与我们的前两个示例类似。
我们可以将多个扩展方法串联起来。例如,filterBy
用于根据参数输出来过滤枚举。只有当filter
函数返回true
时,成员才会被传递。它可能与其他扩展方法(如forEach
)结合使用。
import system'routines;
import extensions'routines;
import system'math;
public program()
{
new Range(1, 9).filterBy:(i => i.mod:2 == 0).forEach:printingLn
}
filterBy
将只返回偶数。输出将是:
2
4
6
8
请注意,(item => <...> )
是lambda闭包,是(item){ ^<...> }
的简写形式。
summarize
扩展用于总结集合的所有成员。
import extensions;
import system'routines;
import extensions'routines;
import system'math;
public program()
{
console.printLine(new Range(1, 9).filterBy:(i => i.mod:2 == 0).summarize())
}
结果将是前10个偶数自然数的总和。
20
使用toArray
扩展,我们可以将枚举保存为数组。
import extensions;
import system'routines;
import extensions'routines;
import system'math;
public program()
{
var evens := new Range(1, 9).filterBy:(i => i.mod:2 == 0).toArray();
console.printLine("sum(",evens,")=", evens.summarize())
}
toArray
会将枚举成员收集到数组中。输出是:
sum(2,4,6,8)=20
我们可以使用top
扩展来限制我们的输出。
import system'routines;
import extensions'routines;
import system'math;
public program()
{
new Range(1,100).filterBy:(i => i.mod:2 == 0).top:10.forEach:printingLn
}
结果将是前10个偶数。
2
4
6
8
10
12
14
16
18
20
我们可以使用*zip:by*扩展将多个可枚举对象组合成一个集合。
import extensions;
import system'routines;
import extensions'routines;
symbol list = new string[]{"a","b","c","d"};
public program()
{
list
.zipBy(new Range(1, list.Length), (ch,i => i.toPrintable() + " - " + ch.toPrintable()))
.forEach:printingLn
}
输出是:
1 - a
2 - b
3 - c
4 - d
其中(ch,i => <...>)
是一个带有两个参数的lambda闭包。
selectBy
扩展可用于基于前一个集合生成新集合。orderBy
扩展将对集合进行排序。
import extensions;
import system'routines;
import extensions'routines;
public program()
{
var list := new Range(1,5).selectBy::(n => randomGenerator.nextInt(100)).toArray();
console.printLine("sort(",list,")=",list.orderBy::(p,n => p < n))
}
结果将是随机生成的数字列表,按升序排列。
sort(50,94,40,78,93)=40,50,78,93,94
groupBy
可用于将枚举成员分组为子集合。
import extensions;
import system'routines;
import extensions'routines;
public program()
{
var list := new Range(1,20).selectBy::(n => randomGenerator.nextInt(10)).toArray();
list
.groupBy::(x => x)
.selectBy::(sub_list =>
sub_list.Key.toPrintable() + ":"
+ sub_list.countMembers().toPrintable() + " times")
.orderBy(ifOrdered)
.forEach(printingLn)
}
代码将计算随机数字遇到的次数。
0:2 times
1:4 times
2:6 times
3:2 times
4:2 times
5:4 times
6:10 times
7:6 times
8:2 times
9:2 times
枚举类型
枚举类型是一种特殊类型的对象,它只能包含枚举值。
虽然ELENA不直接支持此类型(作为一个纯粹的面向对象语言,它只支持类)。但从ELENA 6.2.1开始,将可以使用特殊的后缀模板:enum。
声明非常简单,我们声明类型并在括号中列出可能的值。
public const struct Color : enum<int>(Red = 1,Green = 2,Blue = 3);
在代码中使用它非常简单。
Color red := Color.Red;
Color green := Color.Green;
可以使用**toPrintable**方法转换为字符串。
console.printLine(red.toPrintable());
console.printLine(green.toPrintable());
输出将是:
Red
Green
可以使用**enumerator**方法或**foreach**操作列出所有可能的值。
foreach(Color c; in Color)
{
console.writeLine(c);
}
结果将是。
Red
Green
Blue
动态分派、通用处理程序和混入
让我们从多方法开始。我们可以声明多个具有相同名称但签名不同的方法。也可以声明显式多方法分派器。
class MyClass
{
// accepts the integer
testMe(int n)
{
console.writeLine("It is a number")
}
// accepts the string
testMe(string s)
{
console.writeLine("It is a string")
}
// default handler
testMe(o)
{
console.writeLine("Unsupported parameter")
}
}
public program()
{
object o := new MyClass();
o.testMe(2);
o.testMe("s");
o.testMe(3l)
}
输出是:
It is a number
It is a literal
Unsupported parameter
在某些情况下,也可以做相反的事情,我们可以声明通用处理程序,它将接受任何传入的消息。
import extensions;
class Example
{
generic()
{
// __received is an built-in variable containing the incoming message name
console.printLine(__received," was invoked")
}
generic(x)
{
console.printLine(__received,"(",x,") was invoked")
}
generic(x,y)
{
console.printLine(__received,"(",x,",",y,") was invoked")
}
}
public program()
{
var o := new Example();
o.foo();
o.bar(1);
o.someMethod(1,2)
}
输出
foo was invoked
bar(1) was invoked
someMethod(1,2) was invoked
我们可以声明一个自定义分派器,它将所有未映射的传入消息重定向到另一个对象,从而有效地覆盖它(某种动态变异/代码注入)。
import extensions;
class Extender
{
object theObject;
// the injected property
prop object Foo;
constructor extend(o)
{
theObject := o
}
// redirect Object.Printable method to foo one
string toPrintable() => theObject;
// custom dispatcher
dispatch() => theObject;
}
public program()
{
var o := 234;
// adding a field
o := Extender.extend(o);
// setting a field value
o.Foo := "bar";
console.printLine(o,".foo=",o.Foo)
}
输出是:
234.foo=bar
消息可以动态分派。
class MyClass
{
eval()
{
console.writeLine("eval method")
}
state0()
{
console.writeLine("state0 method")
}
}
public program()
{
var o := new MyClass();
var mssg := mssgconst state0[1]; // a message literal
mssg(o) // dynamically dispatching the message
}
输出是:
eval method
state0 method
虽然ELENA不支持多重继承,但使用自定义分派器,我们可以模拟它。
singleton CameraFeature
{
cameraMsg
= "camera";
}
class MobilePhone
{
mobileMsg
= "phone";
}
class CameraPhone : MobilePhone
{
dispatch() => CameraFeature;
}
public program()
{
var cp := new CameraPhone();
console.writeLine(cp.cameraMsg);
console.writeLine(cp.mobileMsg)
}
输出是:
camera
phone
从外部程序使用ELENA代码
语言编译器支持两种类型的输出:独立和VM客户端。后者将虚拟机加载到其内存中并启动程序。但虚拟机可以在没有任何客户端的情况下使用。它提供了一个特殊的接口,允许直接与ELENA代码进行通信。因此,它可以用于从外部应用程序执行ELENA代码。
让我们从一个简单的ELENA代码开始。我们将声明一个打印消息的函数。
public printingOut(arg)
{
console.writeLine("Hello from " + arg);
}
代码相当直接,参数与文字字符串结合并在控制台中打印。为简单起见,未提供参数类型。
因为我们的代码不会独立使用,所以我们必须创建一个库——embedded1.prj并进行编译。
>elena-cli.exe embedded1.prj
此编译结果是一个编译后的库文件——embedded1.nl。我们可以从另一种语言调用其代码。我们将使用C#控制台应用程序来完成。
为此,我们还需要一个辅助类。
public class ELENAVMWrapper
{
[DllImport(@"elenavm60.dll")]
public static extern int ExecuteVMLA(string target, string arg, IntPtr output, int maxLength);
[DllImport(@"elenavm60.dll")]
public static extern int PrepareVMLA(string configName, string ns, string path, string exceptionHandler);
public const string CONFIG_NAME = "vm_client";
public const string EXCEPTION_HANDLER = "system'core_routines'critical_exception_handler";
public static void Prepare(string ns, string path)
{
if (PrepareVMLA(CONFIG_NAME, ns, path, EXCEPTION_HANDLER) == -1)
throw new Exception("The operation is failed");
}
public static void Execute(string target, string arg)
{
if (ExecuteVMLA(target, arg, , 0) == -1)
throw new Exception("The operation is failed");
}
public static string ExecuteAndReturn(string target, string arg, int maxLength = 1024)
{
var ptr = Marshal.AllocHGlobal(maxLength);
try
{
int copied = ExecuteVMLA(target, arg, ptr, maxLength);
return Marshal.PtrToStringUTF8(ptr, copied);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
}
在主程序中,我们编写一个简单的代码。
try
{
ELENAVMWrapper.Prepare("embedded1", ".");
ELENAVMWrapper.Execute("embedded1'printingOut", "C#");
}
catch (Exception e)
{
Console.WriteLine(e);
}
让我们也编译并运行它。
ELENA VM 6.0.36 (C)2022-2024 by Aleksey Rakov
Initializing...
Hello from C#
在下一个示例中,我们将对从ELENA程序外部传递的数组进行排序并将其返回。
我们的ELENA代码将再次包含一个带单个参数的函数。但这次我们将它从Json反序列化,对数组进行排序,然后再次以Json的形式返回。
import extensions'dynamic;
import algorithms;
public string sorting(string json)
{
var array := json.fromJson();
var sortedArr := array.quickSort();
^ sortedArr.toJson();
}
现在让我们编写C#代码。为了将数据解析为Json,我们将使用System.Text.Json。
using System;
using System.Text.Json;
Console.WriteLine("Calling ELENA library from C#, Sample 3");
try
{
ELENAVMWrapper.Prepare("embedded3", ".");
var array = new int[] { 1, 5, -2, 56, -23 };
var json = JsonSerializer.Serialize(array);
string result = ELENAVMWrapper.ExecuteAndReturn("embedded3'sorting", json);
Console.WriteLine($"{json} >> {result}");
}
catch (Exception e)
{
Console.WriteLine(e);
}
输出将类似:
Calling ELENA library from C#, Sample 3
ELENA VM 6.0.36 (C)2022-2024 by Aleksey Rakov
Initializing...
[1,5,-2,56,-23] >> [-23,-2,1,5,56]
语法
ELENA使用LL(1)语法。它是一种分析语法,意味着终结符*令牌*的角色由其在语句中的位置定义。因此,语法缺少关键字(而是使用用户定义的*属性*)。例如,可以完全在没有属性的情况下编写代码。
class
{
field;
method(param)
{
}
}
其中声明了一个名为class
的类。它有一个名为field
的字段和一个方法——method[1]
。
但在大多数情况下,需要提供额外信息。因此,令牌的角色由其位置决定。
class class;
singleton Tester
{
do(var class class)
{
}
}
public program()
{
var class class := new class();
Tester.do(class);
}
其中class
用作声明新class
的属性,用作标识符类型,也用作标识符。
更多示例
您可以在Rosseta Code中获取更多示例并了解更多关于ELENA语言的信息。
有任何问题吗?
请随时提问!
谁使用ELENA语言以及用于什么?
这是当人们看到关于新语言的任何内容时,一直萦绕在每个人心中的问题。
嗯,我使用该语言,并用ELENA语言开发业务程序。
我在ELENA中开发的程序控制着像福特、通用、(梅赛德斯)Fundo Estrela、(雪铁龙和标致)PSA Bank、(菲亚特、克莱斯勒和菲亚特二手车)FIDIS Bank这样的汽车金融银行收到的所有付款,这意味着如果您住在巴西并在这些制造商之一购买了汽车,那么他们的付款就是由ELENA编写的程序处理的。
我还为Bradesco银行和汇丰银行编写了一些程序来处理和生成付款数据。
ELENA中的开源项目
- CPU模拟器(用旧版语言编写):https://github.com/bencz/cpu-simulator
- DotNetProject(开发中):https://github.com/bencz/DotNetProject
历史
- 2024年7月10日
- 更新以兼容ELENA 6.2
- 添加了一个新部分,用于从外部代码调用代码。
- 新的模板语句:if::is & foreach::in
- 2024年2月11日
- 更新以兼容ELENA 6.0
- 2022年9月25日
- 使用当前ELENA状态更新文章。
- 2020年3月28日
- 更新以兼容ELENA 5.0
- 2019年12月27日
- 文章更新。
- 2019年6月24日
- 新版本 - 4.1.0
- 一些文章编辑。
- 2019年4月4日
- 添加新主题:文件操作、转换、.? 和 .\ 运算符。
- 2019年1月9日
- 更新以兼容ELENA 4.0
- 2018年8月6日
- 添加新主题:可枚举对象操作、混入。
- 2018年6月13日
- 更新以兼容ELENA 3.4
- 修复拼写错误。
- 2017年6月26日
- 添加社区群组。
- 2017年6月19日
- 添加新版本编译器供下载。
- 2017年6月9日
- 将所有文章更新为新版本Elena语言。
- 添加Elena语言的新版本供下载。
- 添加“
if
-else
模板”部分。 - 添加“Elena中的开源项目”部分。
- 2017年1月17日
- 添加Code Project上新Elena文章的链接。
- 2016年12月12日
- 将Elena编译器更新到最新版本。
- 2016年9月22日
- 添加Rosetta Code链接。
- 2016年9月12日
- 文章创建。