语法手册

Last updated: March 20, 2018

介绍

概念

WurstScript是一种命令式的,面向对象的,静态类型的,对初学者友好的编程语言。

WurstScript提供了一个舒适的工作流程来生成可读和可维护的代码。 易于使用和无压力地图开发优先于最终产品的执行速度。 WurstScript易于使用和学习,特别是对于Jass或任何其他编程语言的先前知识,同时仍然对非程序员保持简单和易读性。

虽然我们知道WurstScript不会替换WC3映射场景中的vJass(原因之一是vJass脚本无法轻松移植),但我们仍然希望它能成为一个非常好的选择,特别是对于那些试图学习Jass的人。

请注意,本手册不是新手入门教程,需要读者具有编程知识。 点此查看入门教程

值与类型

WurstScript是一种静态类型语言。这意味着变量只能保存相同类型的值。 此外,由于所有类型都是在编译时确定的,不正确的赋值会抛出错误。

Wurst有与Jass相同的5种基本变量类型:null, boolean, int, real, string

在这个例子中,你可以看到左侧变量的静态类型。如果右侧的赋值与左侧不属于同一类型,则会引发错误。

boolean b = true // true or false
int i = 1337 // integer, i.e. value without decimals
real r = 3.14 // floating point value
string s = "wurst is great" // text

// Examples
int i = 0 // OK
int j = "no" // Error
string r = "yes" // OK
string s = i // Error

语法

WurstScript使用基于缩进的语法来定义块。您可以使用空格或制表符进行缩进,但将两者混合会引发警告。 在下面我们使用单词“tab”来指代制表符或4个空格字符。

// Wurst example. 'ifStatements' need to be indented
if condition
	ifStatements
nextStatements

// Other example using curly brackets (e.g. Java or Zinc)
if condition {
	ifStatements
}
nextStatements
// Other example using keywords (e.g. Jass)
if condition
	ifStatements
endif
nextStatements

一般而言,除了以下情况,换行符会在语句末出现:

  • 当一行语句以 ([ 结束时。
  • 当下一行以 ), ],., .., 或 begin 开始时。
  • 当行尾以下方符号结束时: ,, +, *, -, div, /, mod, %, and, or, ?, :

您可以使用它们来打破一些过长的表达式或长参数列表,或链式方法调用:

someFunction(param1, param2,
	param3, param4)

someUnit..setX(...)
	..setY(...)
	..setName(...)

new Object()
	..setName("Foo")
	..doSomething()

WurstScript试图避免过度冗长的语句和符号,以保持代码的简洁和可读性。

基础

以下是Wurst的核心功能的简要概述,阅读这些概述以便你可以立即开始编程。

关于每个主题的更详细的文档,请参阅页面下方的专用章节。

Wurst代码被组织成。 任何一段代码都必须放在包中才能被识别。 包可以导入其他包,以访问导入包中定义的变量,函数和类。 包有一个可选的_init_块,在地图运行时执行。

我们来看一个经典的 Hello World 示例。

package HelloWurst
// to use resources from other packages we have to import them at the top
import Printing

// the init block of each package is called when the map starts
init
	// calling the print function from the Printing package
	print("Hello Wurst!")

如果你运行这个例子,地图加载后将会显示 “Hello Wurst!” 。

正如你可能已经注意到的那样,import Printing会引发警告,因为它已经被自动导入。 您可以删除导入,它只是用来说明如何导入包。

有关包的更多信息,请参阅章节。

你仍然可以在包之外使用普通的 Jass语法/代码 - Wurst World Editor 将解析Jass(如果启用了JassHelper,则为vJass),并且编译你的Wurst代码。

有关使用具有混合 Wurst/Jass 代码的地图的更多信息,请参阅“在传统地图中使用Wurst”部分。

函数

function 定义由名称,形式参数列表和返回值类型组成。 返回类型是在使用 returns 关键字的参数之后声明的。 如果该函数没有返回值,则该部分将被忽略。

// this function returns the maximum of two integers
function max(int a, int b) returns int
	if a > b
		return a
	else
		return b

// this function prints the maximum of two integers
function printMax(int a, int b)
	print(max(a,b).toString())

function foo() // parentheses instead of "takes", "returns nothing" obsolete.
	...

function foo2(unit u) // parameters
	RemoveUnit(u)

function bar(int i) returns int // "returns" [type]
	return i + 4

function blub() returns int // without parameters
	return someArray[5]

function foobar()
	int i // local variable
	i = i + 1 // variable assignment
	int i2 = i // support for locals anywhere inside a function

变量

全局变量可以在包内的任何顶级层声明。 局部变量可以在函数内的任何地方声明。 常量可以使用 constantlet 关键字来声明。 变量通过使用 var 关键字或其类型的名称来声明。

// declaring a constant - the type is inferred from the initial expression
constant x = 5
let x2 = 25
// The same but with explicit type
constant real x = 5
// declaring a variable - the inferring works the same as 'let', but the value can be changed
var y = 5
// declaring a variable with explicit type
int z = 7
// declaring an array
int array a
// arrays can be initialized as well.
// however this is just a shorthand assignment
int array b = [1, 2, 3]
// but it allows having constant arrays
constant c = ["A", "B", "C"]
// Initialized arrays provide a length attribute which is equal to the initial amount of items in the array
// It is not updated when you modify the array and mostly intended for interation
constant blen = b.length

// inside a function
function getUnitInfo(unit u)
	let p = u.getOwner()
	var name = u.getName()
	print(name)
	var x = u.getX()
	var y = u.getY()
	let sum = x + y

有了这些基本概念,你应该可以做任何你在Jass学到的事情。 我们将在接下来的章节中谈谈更深入的话题。

基本概念

表达式

Wurst 表达式的一些基本语法:

Expr ::=
      Expr + Expr       // 加法
    | Expr - Expr       // 减法
    | Expr / Expr       //实数 除法
    | Expr div Expr     // 整数 除法
    | Expr % Expr       // 实数 取余(取模)
    | Expr mod Expr     // 整数 取余(取模)
    | Expr and Expr     //布尔值 和
    | Expr or Expr      // 布尔值 或
    | Expr ? Expr : Expr // 条件表达式  / 三元操作符
    | Expr < Expr       //小于
    | Expr <= Expr      //小于等于
    | Expr > Expr       // 大于
    | Expr >= Expr      // 大于等于
    | Expr == Expr      // 等于
    | Expr != Expr      // 不等于
    | - Expr            // 一元表达式 (负数)
    | not Expr          // 否定表达式
    | IDENTIFIER                    // 变量
    | IDENTIFIER(Expr, Expr, ...)   // 函数调用
    | Expr . IDENTIFIER             //成员变量
    | Expr . IDENTIFIER(Expr, Expr, ...)    // 成员函数
    | Expr .. IDENTIFIER(Expr, Expr, ...)   // 成员函数,和上面一样 但是会返回当前对象(见下文)

    | new IDENTIFIER(Expr, Expr, ...)       // 构造函数调用
    | destroy Expr                  // 析构函数
    | Expr castTo Type              // 类型转换
    | Expr instanceof Type          // 实例类型检测
    | begin                         // 多行 lambda 表达式
        Statements
        end // 多行 lambda 表达式
    | (param1, param2, ...) -> Expr  //匿名函数
    | (Expr)                        // 括号

IDENTIFIER is 只一个函数或者一个变量的名字.他可以以字母数字和下划线开头 (译著:函数以下划线开头或结尾可能产生能编译但是无法运行的情况)

提示: 关于泛型函数的调用,可以查看泛型章节

级联操作符 (点点操作符)

级联操作符 ..类似于普通的. 可以用来调用函数, 但是他不会返回函数的结果而是返回调用者的对象 . 这使得可以轻松的对同一个变量进行链式的调用. 来看看这个例子:

CreateTrigger()
    ..registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER)
    ..addCondition(Condition(function cond))
    ..addAction(function action)

以上代码等效于:

let temp = CreateTrigger()
temp.registerAnyUnitEvent(EVENT_PLAYER_UNIT_ISSUED_ORDER)
temp.addCondition(Condition(function cond))
temp.addAction(function action)

条件操作符

条件操作符 (也叫三元表达式) 具有这样的格式 条件 ? A : B. 首先条件会先执行. 如果条件成立了,为 true, 那么 A 表达式会执行并作为整个语句的结果. 否则 B 表达式会执行并作为结果.

这个表达式结果的类型为两个子表达式结果的共同类型, 也就是说表达式 AB 应该具有同样的类型才能在一个三元表达式中使用.

条件表达式一般用的不多. 一般用于结果在两个备选结果中选择,如下:

// 处理一个敌人和多个敌人的情况:
let enemyCount = n.toString() + " " + (n == 1 ? "enemy" : "enemies")

语句

Wurst 为 Jass 添加了一些现代化的语句 , 如 switch 和 for 循环

If语句

if 语句用于在满足某些条件的时候才执行一些动作. 在else 块中你可以处理条件不满足的情况.

// 简单的例子
if u.getHP() < 100
    print("unit s hurt") //if中的动作必须缩进

// if 和else
if x > y
    print("x 比 y 大")
else if x < y // 关闭if块 开启 else-if块
    print("x 比 y 小")
else // 处理剩余的情况
    print("x 等于 y")

//通过缩进来编写if语句

// 更多例子:

if caster.getOwner() == BOT_PLAYER
    print("caster owned by BOT_PLAYER")

if GetSpellAbilityId() == SPELL_ID
    flashEffect(getSpellTargetPos(), FX_PATH)

if x > 0 and x < 100 and y > 0 and y < 100
    print("inside box")

Switch 语句

// 是一个整数变量
switch i
    case 1
        print("1")
    case 3
        print("3")
    case 88
        print("88")
    default
        print("都不满足的情况")

switch用于处理if ,else-if 条件很多的场景.

循环

while a > b // 条件循环
    ...

for i = 0 to 10 // for-循环 i每次+1
    ...

for i = 0 to 10 step 2 // for-循环 每次+2
    ...

for i = 10 downto 0 // 也可以每次减少
    ...

for u in someGroup // 循环单位组中的单位
    ...

for u from someGroup //循环单位组中的单位,并将单位移除
    ...

for i in myList // 用于标准库中的集合类型
    ...

For 循环

for-in 可以用于循环任何一个可迭代的对象. for-in 可以简单的转换为while循环:

for A a in b
    Statements

// 等效于:
let iterator = b.iterator()
while iterator.hasNext()
    A a = iterator.next()
    Statements*
iterator.close()

提示 iterator.close() 会在while类任何一个返回语句之前被调用 .

如果你已经拥有了迭代器,或者想要访问其中的每一个函数,那么你可以使用for-from循环, 转换也十分简单

let iterator = myList.iterator()
for segment from iterator
    //Statements
iterator.close()

// is equivalent to:
let iterator = myList.iterator()
while iterator.hasNext()
    segment = iterator.next()
    //Statements
iterator.close() 

提示 你需要手动的关闭迭代器 ( iterator.close() ).

Skip

最基本的语句就是 skip 语句了,他没有任何作用, 在某些很少的情况下才有用.

迭代器

您可以通过提供一组函数为Wurst提供所需类型的迭代器。通过提供迭代器,该类型可用于for循环:

  • function hasNext() returns boolean (如果还有下一个元素,就返回true)
  • function next() returns TYPE (返回下一个元素)

使用这两个函数你也会获取一个可以用于for-from循环的迭代器

如果你想使某个类型能用for-in循环,你需要提供

  • function iterator() returns Iterator

对于您的类型,它返回迭代的对象。 这可以很像一个句柄,就像在group-iterator中一样,或者像List-iterator这样的对象。 您的迭代器还应提供一个close函数,用于清除它分配的所有资源。 通常,迭代器只是在close函数中摧毁它自己。

可以看标准库中的两个例子:

Group-Iterator

public function group.iterator() returns group
    // return a copy of the group:
    bj_groupAddGroupDest = CreateGroup()
    ForGroup(this, function GroupAddGroupEnum)
    return bj_groupAddGroupDest

public function group.hasNext() returns boolean
    return FirstOfGroup(this) != null

public function group.next() returns unit
    let u = FirstOfGroup(this)
    GroupRemoveUnit(this, u)
    return u

public function group.close()
    DestroyGroup(this)

如您所见,这个迭代器是一个单位组,因此该单位组需要提供上述函数。标准库的做法是通过扩展函数来完成的

LinkedList-Iterator

public class LinkedList<T>
    ...

    // get an iterator for this list
    function iterator() returns LLIterator<T>
        return new LLIterator(dummy)

class LLIterator<Q>
    LLEntry<Q> dummy
    LLEntry<Q> current

    construct(LLEntry<Q> dummy)
        this.dummy = dummy
        this.current = dummy

    function hasNext() returns boolean
        return current.next != dummy

    function next() returns Q
        current = current.next
        return current.elem

    function close()
        destroy this

LinkedList Iterator有点不同,因为它是一个类。 它仍然提供所需的功能,因此可以作为迭代器使用。 它还包含一些帮助迭代的成员。 如果从LinkedList的.iterator()函数返回LLIterator,则为新实例。

运算简写

WurstScript 支持这些运算简写:

i++         // i = i + 1
i--         // i = i - 1
x += y      // x = x + y
x -= y      // x = x - y
x *= y      // x = x * y
x /= y      // x = x / y

因为这些简称只是简单地转化为它们的等价物,所以它们可以与重载运算符一起使用。

如上所述,用Wurst编写的每个代码段都必须在_package_中,包定义代码组织和单独的名称空间。

包也可以有全局变量 - 每个不在函数/类/模块的变量就是全局变量.被叫做包的全局性

当在vscode中创建一个.wurst文件时,包的顶部会自动的添加:

package SomeVSCodePackage // 文件的名字,也是包的名字
...

Imports 导入 / 依赖

包可以导入( 或者叫 依赖)另一个包,从而访问另一个包中的被定义为公开(public)的函数,变量.

// 声明
package SomePackage

// 导入
package Blub
import SomePackage
import AnotherPackge //导入多个包
import public PackageX // 公开导入 (看下面)

import指令在项目的wurst文件夹中搜索包,并从wurst.dependencies文件中搜索所有依赖的项目。

import public 公开导入

默认情况下,程序包不会传递自己导入的包, 如以下的代码是错误的:

package A
public constant x = 5

package B
import A

package C
import B
constant z = x

变量x可用于包B,但不从B传递。因此,在包C中,我们不能使用变量x。 我们可以通过简单地将A导入C来解决这个问题,但有时您希望避免这些导入。 使用公开导入解决了这个问题,因为它们将传递导入的所有内容。因此,以下代码将起作用:

package A
public constant x = 5

package B
import public A

package C
import B
constant z = x

特殊的wurst包

默认情况下,每个包都会导入一个名为Wurst.wurst的包,该包在标准库中定义。 这个包拥有一些经常被使用的包

如果您不想要此标准导入,可以通过导入包NoWurst来禁用它。 该指令主要用于解决标准库中的一些循环依赖关系

公开声明

默认情况下,程序包的所有成员都是私有的。如果要在导入该包的包中使用它们,则必须添加关键字“public”。

常量

您可以声明一个变量常量来禁止这个变量初始化后被更改。这对生成的代码没有影响,但在尝试编译时会抛出错误。

例子

package First
int i // (private by default)只在这个包中可用
public int j // 公开的,导入这个类就可以使用

package Second
import First

int k = i // 错误
int m = j // 正确, 因为是公开的


package Foo
constant int myImportantNumber = 21364 // 常量必须初始化

function blub()
    myImportantNumber = 123 // Error

public constant int myPrivateNumber2 = 123 //这是错误的 包中的变量和常量需要定义在调用它们的函数的上面

Init 初始化块

包的另一个功能是init块。 每个包都可以在其中的任何位置具有一个或多个init块。 包的init块内的所有操作都在地图载入中执行。

在init块的开头,您可以假设所有全局变量都在 当前包也已初始化。

package MyPackage
init
    print("this is the initblock")

提示: 由于魔兽3具有字节码限制,因此主函数内的操作过多会使其无法完全执行。 为了避免这个问题,Wurst使用TriggerEvaluate在单独的线程中执行包init,以确定包是否已成功初始化。 在包内部过度计算时仍会出现问题,在这种情况下会显示警告。

初始化和延迟初始化

Wurst的初始化规则很简单:

  1. 包内部初始化从上到下完成。 包的初始化程序是只所有全局变量静态初始化程序(包括静态类变量)和所有init块。
  2. 如果包A导入包B并且导入不是’initlater`导入, 然后包A的初始化程序在B之前运行。 不允许使用不带“initlater”的循环导入。

通常,您应该通过移动受影响的代码来避免包之间的循环依赖关系。 但是,如果有必要,您可以手动通过将关键字initlater定义将使用的包稍后初始化:

package A
import initlater B
import public initlater C
import D

这里只有’D包保证在包'A之前被初始化。 包’BC`允许稍后初始化。

配置包

可以配置全局变量和函数。配置通过配置包完成。 每个包最多只有一个配置包,每个配置包只配置一个包。 包及其配置包之间的关系通过以下命名约定表示: 对于名为Blub的包,配置包必须命名为Blub_config

在配置中,全局变量可以使用@config注释进行注解。 这样配置包中的变量可以覆盖原始包中具有相同名称的变量。 在原始包中,变量应该用@configurable注释,来标志这个变量是可以被配置覆盖的。

例子:

package Example
@configurable int x = 5

package Example_config
@config int x = 42

配置功能基本相同:

package Example
@configurable public function math(int x, int y) returns int
    return x + y


package Example_config
@config public function math(int x, int y) returns int
    return x*y

类是一种简单、强大和非常有用的结构。一个类定义了数据和处理这些数据的相关函数。看看这个小例子:

let dummyCaster = new Caster(200.0, 400.0)
dummyCaster.castFlameStrike(500.0, 30.0)
destroy dummyCaster

在这个例子中,我们在坐标x:200 y:400创建了一个名为“dummyCaster”的施法马甲单位

译注:马甲单位指的是帮助模拟某些功能用的辅助单位。

然后我们命令施法马甲向另一个位置施放烈焰风暴,最后我们摧毁了施法马甲这个类的实例。

这个例子向您展示了如何创建一个新对象(第1行)、调用对象上的方法(第2行)以及如何销毁对象(第3行)。 但是如何定义一个新的对象类型,比如“Caster”?这就是类的功能。类定义了一种新的对象。 类定义变量和方法,该类的每个实例都应该调用这些变量和方法。

译注:在类中的非静态函数一般称为方法。

类还可以定义如何构造新对象(构造函数)以及当它被破坏时应该发生什么(析构函数)。

可以像这样定义一个施法者会类

class Caster // 定义一个'类'结构. "Caster" 是这个类的名字
    unit u // 类内部的变量一般叫做成员变量,可以定义在类的任何地方

    construct(real x, real y) // 构造函数
        u = createUnit(...)

    function castFlameStrike(real x, real y) //一个方法
        u.issueOrder(...)

    ondestroy //析构函数
        u.kill()

构造函数

WurstScript允许您为每个类定义自己的构造函数。构造函数是用来构造类的新实例的函数。

在通过new关键字创建类时调用构造函数,并在返回类实例之前对其执行操作。

class Pair
    int a
    int b

    construct(int pA, int pB)
        a = pA
        b = pB

    function add() returns int
        return a + b


function test()
    let pair = new Pair(2,4) //构造函数
    let sum = p.add() //调用方法
    print(sum) // 打印6

在这个例子中,构造函数将两个整数a和b作为参数,并将类变量设置为它们。

只要参数不同,就可以定义多个构造函数。

class Pair
    int a
    int b

    construct(int pA, int pB)
        a = pA
        b = pB

    construct(int pA, int pB, int pC)
        a = pA
        b = pB
        a += pC
        b += pC

function test()
    let p = new Pair(2, 4)
    let p2 = new Pair(3, 4, 5)

在本例中,类对有两个构造函数-一个取2,另一个取3个参数。

根据参数类型和参数数量,Wurst自动决定在使用“new”时使用哪个构造函数。

This

关键字this是指调用函数的类的当前实例。这还允许我们将参数命名为与类变量相同的名称。

但是在类函数中可以忽略它,如例子所示。

class Pair
    int a
    int b

    // 使用这个关键字来访问类的成员变量
    construct(int a, int b)
        this.a = a
        this.b = b

析构函数

每个类可以有一个ondestroy块。此块在实例被销毁之前执行。

Ondestroy块的定义如下

class UnitWrapper
    unit u
    ...

    ondestroy
        u.remove()

静态元素

通过在声明前面写入关键字“static”,可以将变量和函数声明为“u static”。 静态变量和方法/函数属于类,而其他元素属于类的实例。 因此,您可以调用静态方法,而无需类的实例。

class Terrain
    static var someVal = 12.

    static int array someArr

    static function getZ(real x, real y) returns real
        ...

function foo()
    let z = Terrain.getZ( 0, 0 ) // 使用 $类名$.$静态方法名字$()来访问静态方法
    let r = Terrain.someVal // 静态变量也是一样的 
    let s = Terrain.someArr[0]

译注:非私有的静态变量认为是全局变量.和全局变量相比只是访问时需要使用 类名.静态变量名

数组成员

香肠通过将大小数组转换为大小乘以数组,然后通过二叉树搜索在get/set函数中解析数组,从而支持将大小数组作为类成员。

译注:这一点不用太在意,只需要知道成员数组变量需要标明数组大小即可(数组的大小不应过大)

例子:

class Rectangle
    Point array[4] points

    function getPoint(int index)
        return points[index]

    function setPoint(int index, Point nP)
        points[index] = nP

可见性规则

默认情况下,类元素在任何地方都可见。可以在变量或函数定义前面添加修饰符private(私有)或protected(受保护)来限制其可见性。

私有的元素只能从类中看到。在封闭包和子类中可以看到受保护的元素。

译注:封闭包的概念极少用到,实际上或protected这个修饰符也比较少用

继承

一个类可以扩展另一个类。

译注:被继承的类称为这个类的父类. 比如A继承B,那么B是A的父类,A是B的子类

这个类继承父类的所有非私有函数和变量并且可以在任何可以使用父类的地方使用。

类的构造函数必须指定如何构造父类。通过用super()来调用,

它定义了父类构造函数的参数。在此调用之前不能有任何语句。

如果父类的构造函数没有参数,可以省略这句话.

从父类继承的函数可以在子类中重写。这些函数必须用“override”关键字进行重写。

例子

//弹幕
class Missile 
    construct(string fx, real x, real y)
        // ...
    //碰撞
    function onCollide(unit u)


// 定义一种特殊的弹幕 火球弹幕 它继承于弹幕
class FireballMissile extends Missile
    // we construct it using a fireball fx
    construct(real x, real y)
        //译注:调用父类的构造函数
        super("Abilities\\Weapons\\RedDragonBreath\\RedDragonMissile.mdl", x, y)
    // 火球弹幕可能会和普通弹幕有一些不同的作用,通过重写来实现
    override function onCollide(unit u)
        // 这里就可以写出火球弹幕的额外效果 普通弹幕的效果不再执行

译注:可以在火球弹幕中用过super.onCollide(u)来执行普通弹幕的效果

类型转换

为了进行类型转换,您可以使用 castTo关键字.

译注:类型转换可以把一个类的实例转换为一个整数,也有一些其他用法。

作用: 一种是保存类实例,例如将它们附加到计时器上,就像在TimerUtils.wurst中那样

这个过程也可以反转(从整数转换为类的实例)。

class Test
    int val

init
    let t = new Test()
    let i = t castTo int

使用子类型时,类型转换有时很有用。如果你有一个类型A的实例a但是如果你知道对象的动态类型是B,则可以将对象强制转换为B以更改类型。

class Shape

class Triangle extends Shape
    function calculate()
        ...

init
    A a = new B()
    // 我们知道a其实是B类型的,所以可以转换成B来执行B中定义的额外方法
    B b = a castTo B
    b.special()

提示: 你别瞎鸡儿转换,只有在你知道他是什么类型的时候才转换.在运行时不会检查强制类型转换,因此它们可能会出现可怕的错误. 使用高级库和面向对象编程可以避免强制转换.

多态(动态调度)

Wurst在访问类实例时具有多态功能。

这意味着很简单:如果你有一个B类的实例,它被转换成父类A调用具有该引用的函数时,将从依然执行B中的操作.

举个例子更容易理解:

例子 1

class Recipe
    function printOut()
        print("I'm a recipe")

class SwordRecipe extends Recipe
    override function printOut()
        print("I'm a sword recipe")

init
    A a = new B()
    a.printOut()//  会输出"I'm B"

例子 2

class A
    string name

    construct(string name)
        this.name = name

    function printName()
        print("实例 A 的名字是: " + name )


class B extends A

    construct(string name)
        super(name)

    override function printName()
        print("实例 B 的名字是: " + name )

init
    A a = new B("first")
    a.printName() // 会输出 "Instance of B named: first", 因为他真的是B

这在遍历同一父型的类实例时特别有用,这意味着您不必将实例强制转换为适当的子类型。

父类调用

当重写方法时,仍然可以使用super关键字调用原始实现。这很有用,因为

子类通常只是向其父类添加一些功能.

例子,设计一个火球技能,我们想为它创建一个更强大的版本:

class FireBall
    ...
    function hitUnit(unit u, real damage)
        ...

class PowerFireBall extends FireBall
    ...
    // 这里我们重写hitUnit方法
    override function hitUnit(unit u, real damage)
        // 第一步,我们创建一个更大的火球特效
        createSomeBigExplosionEffect(u)
        // 然后我们造成2倍火球的伤害
        super.hitUnit(u, damage*2)

在定义自定义构造函数时,还应使用super关键字。必须在子类的构造函数中调用父类的构造函数。当没有给出父类构造函数调用时,编译器将尝试调用没有参数的构造函数。如果不存在这样的构造函数,则会出现如下错误:“对父类构造函数的调用不正确:缺少参数。”

class Missile
    ...
    construct(vec2 position)
        ...

class TimedMissile extends Missile
    ...
    construct(vec2 position, real duration)
        // 这里调用父类的构造函数
        // 而且必须写在第一句
        super(position)
        ...

instanceof

在Wurst中,您可以使用关键字instanceof检查一个类实例的类型。

注意:您应该尽可能避免instanceof检查,而使用面向对象的编程。

如果对象o是C类型的子类型,则instanceof表达式“o instanceof C”返回true。

不能将instanceof与来自不同类型分区的类型一起使用,因为instanceof 基于类型ID(见下一章)。这也是为什么不能使用instanceof检查整数的类型。 编译器将尝试拒绝instanceof表达式,该表达式将始终产生true或始终产生false。

例子1

class A

class B extends A

init
    A a = new B()

    if a instanceof B
        print("It's a B")

typeId(类型ID)

提示: typeId是一个实验特性。尽量避免使用它们。

有时需要检查对象的确切类型。为此,如果a是一个对象,可以写“a.typeId”;如果A是一个类,可以写“A.typeId”。

// 检查a是否是A的实例
if a.typeId == A.typeId
    print("It's an A")

typeId是一个整数,对于类型分区内的每个类都是唯一的。

类型分区

A的类型分区是包含A的最小集合,对于所有类或接口T1和T2,它包含:

如果T1在集合中,并且T1是T2的子类型或父类型,则T2也在集合中。

或者用不同的方式表示:如果且仅当它们的类型层次结构以某种方式连接时,A和B在同一个分区中。

抽象类

抽象类是一个被声明为抽象的类,它可以包括抽象函数。不能创建抽象类的实例,

但是您可以为它创建非抽象的子类。

用关键字“abstract”声明抽象函数,并省略实现。

abstract function onHit()

抽象类类似于接口,但它们可以拥有自己的、实现的函数和变量,就像普通类一样。

抽象类的优点是,您可以引用并调用抽象类中的抽象函数。

以碰撞响应为例。你的地图上有好几个单位,你希望每一个单位都有不同的表现。

可以通过集中更新函数或将他抽象交给子类去处理

像这样:

abstract class CollidableObject

    abstract function onHit()

    function checkCollision(CollidableObject o)
        if this.inRange(o)
            onHit()
            o.onHit()

class Ball extends CollidableObject

    override function onHit()
        print("I'm a ball")

class Rect extends CollidableObject

    override function onHit()
        print("I'm a Rect")

因为CollidableObject需要它的子类来实现函数onHit;它可以在抽象类中调用,而不知道他的类型。

如果抽象类的子类没有实现所有抽象函数,那么它也必须抽象

接口

interface Listener
    function onClick()

    function onAttack(real x, real y) returns boolean


class ExpertListener implements Listener
    function onClick()
        print("clicked")


    function onAttack(real x, real y) returns boolean
        flashEffect(EFFECT_STRING, x, y)

接口是一组具有空主体的相关方法。

如果一个类实现了某个接口,它必须实现接口中的所有空方法。

一个类可以实现多个接口,这意味着它同时满足更多的接口需求。

class ExampleClass implements Interface1, Interface2, ...
    // 实现接口中所有的空方法

有了接口(如果是隐式的,还有模块),现在可以上下转换任何实现它的类。 这对于保存来自仅在1个集合列表或者数组中继承1个接口的类的所有实例特别有用

Defender methods

接口可以具有带有实现的函数。当实现接口的类不提供方法本身的实现。 通常这是不需要的,但在某些情况下可能为了在不破坏接口实现类的情况下实现接口,必须这样做。

译注:这一块可能是试验性的特性,我并没有使用过.为了避免翻译出现错误,以下是原文

An interface can have functions with an implementation. This implementation is used, when a class implementing the interface does not provide an implementation of the method itself. Usually this is not needed but in some cases it might be necessary in order to evolve an interface without breaking its implementing classes.

泛型

Generics make it possible to abstract from specific types and only program with placeholders for types. This is especially useful for container types (e.g. Lists) where we do not want to code a ListOfA, ListOfB, ListOfC for every class A, B, C which we need a list for. Think of it as creating a general List with all it’s functionality, but for an unknown type, that gets defined when creating the instance.

With generics we can instead write only one implementation for lists and use it with all types. Generic type parameter and arguments are written in angled bracket s (< an d >) after the identifier.

// a generic interface for Sets with type parameter T
interface Set<T>
    // adds an element to the set
    function add(T t)

    // removes an element from the set
    function remove(T t)

    // returns the number of elements in the set
    function size() returns int

    // checks whether a certain element is in the set
    function contains(T t) returns boolean

class SetImpl<T> implements Set<T>
    // [...] implementation of the interface

If we have a class defined like this, we can then use it with a concrete type (e.g. Missile):

// create a set of missiles
Set<Missile> missiles = new SetImpl<Missile>();
// add a missile m
missiles.add(m);

Generic parameters in Wurst can be bound to integers, class types and interface types directly. In order to bind generic parameters to primitive-, handle- and tuple types you have to provide the functions

function [TYPE]ToIndex([TYPE] t) returns int

function [TYPE]FromIndex(int index) returns [TYPE]
    return ...

The typecasting functions for primitive- and handle types are provided in Typecasting.wurst using the fogstate bug.

function unitToIndex(unit u) returns int
    return u.getHandleId()

function unitFromIndex(int index) returns unit
    data.saveFogState(0,ConvertFogState(index))
    return data.loadUnit(0)

Generic Functions

Functions can use generic types. The type parameter is written after the name of the function. In the following example the function forall tests if a predicate is true for all elements in a list. The function has to be generic, because it has to work on all kinds of lists.

function forall<T>(LinkedList<T> l, LinkedListPredicate<T> pred) returns boolean
    for x in l
        if not pred.isTrueFor(x)
            return false
    return true

// usage:
    LinkedList<int> l = ...
    // check if all elements in the list are even
    if forall<int>(l, (int x) -> x mod 2 == 0)
        print("true")

When calling a generic function, the type arguments can be omitted if they can be inferred from the arguments to the function:

...
if forall(l, (int x) -> x mod 2 == 0)
    ...

Extension functions can also be generic, as shown by the following example:

function LinkedList<T>.forall<T>(LinkedListPredicate<T> pred) returns boolean
    for x in this
        if not pred.isTrueFor(x)
            return false
    return true

// usage:
    ...
    if l.forall((int x) -> x mod 2 == 0)
        ...

Modules

A module is a small package which provides some functionality for classes. Classes can use modules to inherit the functionality of the module.

You can use the functions from the used module as if they were declared in the class. You can also override functions defined in a module to adjust its behavior.

If you know object oriented languages like Java or C#: Modules are like abstract classes and using a module is like inheriting from an abstract class but without the sub-typing. Modules encapsulate behaviour that you might want to add to several classes, without overhead and hierarchical structure.

Example 1

In this example we just have a class which uses a module A. The resulting program behaves as if the code from module A would be pasted into Class C.

module A
    public function foo()
        ...

class C
    use A

Example 2

Modules are more than just a mechanism to copy code. Classes and modules can override functions defined in used modules:

// a module to store an integer variable
module IntContainer
    int x

    public function getX() returns int
        return int

    public function setX(int x)
        this.x = x

// a container which only stores positive ints
module PositiveIntContainer
    use IntContainer

    // override the setter to only store positive integers
    override function setX(int x)
        if x >= 0
            IntContainer.setX(x)

Visibility & Usage Rules

  • Variables of modules are always private
  • private functions are only usable from the module itself
  • each function of a module must be declared public or private
  • if a class uses a module it inherits only the public functions of the module
    • you can use a module with private (not implemented yet). This will let you use the functionality of the module without exposing its functions to the outside.

Overriding Functions

  • You can override functions which are defined in used modules by writing the override keyword in front of a function.
  • If two modules are used, which have the same function, it must be overridden by the underlying class or module in order to avoid ambiguousness (of course this is only possible if the function signatures match. We are thinking about a solution for this)
  • private functions cannot be overridden

Abstract Functions

Modules can declare abstract functions: Functions without a given implementation. Abstract functions have to be implemented by the underlying classes.

Thistype

You can use thistype inside a module to refer to the type of the class which uses the module. This can be useful if you need to cast the class to an integer and back.

高级概念

枚举

In Wurst, Enums are a shorthand wrapper for collections of named integer constants. Enums are not related to classes and are directly translated into the integers they represent. The main purpose is to add safe, comfortable API in places where otherwise ints would be used.

Usage is similar to static class members via the Enum’s name:

enum MyUnitState
	FLYING
	GROUND
	WATER

let currentState = MyUnitState.GROUND

Enums can be used as class members

class C
	State currentState

	construct(State state)
		currentState = state

To check the current value of an enum, you can use the switch statement. Note that all Enummembers have to be checked (or a defaut).

let currentState = MyUnitState.GROUND
switch currentState
	case FLYING
		print("flying")
	case GROUND
		print("ground")
	case WATER
		print("water")

Note that in switch statements and variable assignments the qualifier MyUnitState can be ommited.

To retrieve the int an enum member represents, simply cast it to the int type:

print((MyUnitState.GROUND castTo int).toString()) // Will print "1"

The coalescent integer value starts at 0, incrementing with each succeeding enum member. So for MyUnitState FLYING will be 0, GROUND 1 and WATER 2.

元组类型

With tuple types you can group several variables into one bundle. This can be used to return more than one value from a function, to create custom types and of course for better readability.

Note that tuples are not like classes. There are some important differences:

  • You do not destroy tuple values.
  • When you assign a tuple to a different variable or pass it to a function you create a copy of the tuple. Changes to this copy will not affect the original tuple.
  • Tuple types cannot be bound to type parameters, so you can not have a List{vec} if vec is a tuple type.
  • As tuple types are not created on the heap you have no performance overhead compared to using single variables.
// Example 1:

// define a tuple
tuple vec(real x, real y, real z)

init
	// create a new tuple value
	let v = vec(1, 2, 3)
	// change parts of the tuple
	v.x = 4
	// create a copy of v and call it u
	let u = v
	u.y = 5
	if v.x == 4 and v.y == 2 and u.y == 5
		testSuccess()


// Example 2:

tuple pair(real x, real y)
init
	var p = pair(1, 2)
	// swap the values of p.x and p.y
	p = pair(p.y, p.x)
	if p.x == 2 and p.y == 1
		testSuccess()

Because tuples don’t have any functions themselves, you can add extension functions to an existing tuple type in order to achieve class-like functionality. Remember that you can’t modify the value of a tuple in it’s extension function

  • so you have to return a new tuple every time if you wan’t to change something. Look at the Vector package in the Standard Library for some tuple usage examples. (Math/Vectors.wurst)

拓展函数

Extension functions enable you to “add” functions to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension functions are a special kind of static function, but they are called as if they were instance functions of the extended type.

Declaration

public function TYPE.EXTFUNCTIONNAME(PARAMETERS) returns ...
	BODY
	// The keyword "this" inside the body refers to the instance of the extended type

Examples

// Declaration
public function unit.getX() returns real
	return GetUnitX(this)

// Works with any type
public function real.half() returns real
	return this/2

// Parameters
public function int.add( int value )
	return this + value

// Usage
let u = CreateUnit(...)
...
print(u.getX().half())

// Also classes, e.g. setter and getter for private vars
public function BlubClass.getPrivateMember() returns real
	return this.privateMember

// And tuples as mentioned above
public function vec2.lengthSquared returns real
	return this.x*this.x+this.y*this.y

不定参函数

Variable argument functions can be passed an variable amount of parameters of the same type. They are most commonly used to prevent boilerplate code and provide better API. Inside the function, the variable arguments can be accessed via a for .. in loop.

Example:

function asNumberList(vararg int numbers) returns LinkedList<int>
	let list = new LinkedList<int>()
	for number in numbers
		list.add(number)
	return list

init
	asNumberList(1, 2, 3)
	asNumberList(44, 14, 651, 23)
	asNumberList(10)

All the calls to asNumberList are valid in this example and the benefit is apparent. We can pass any amount of integers (up to 31 arguments) to the function, but we only need to implement it once.

Limitations

The current implementation creates a specialized function with the right number of parameters. Since Jass allows at most 31 parameters, function calls must not use more than 31 arguments in total.

Lambdas和闭包

A lambda expression (also called anonymous function) is a lightweight way to provide an implementation of a functional interface or abstract class (To keep the text simple, the following explanations are all referring to interfaces, but abstract classes can be used in the same way).

A functional interface is an interface which has only one method. Here is an example:

// the functional interface:
interface Predicate<T>
	function isTrueFor(T t) returns bool

// a simple implementation
class IsEven implements Predicate<int>
	function isTrueFor(int x) returns bool
		return x mod 2 == 0

// and then we can use it like so:
let x = 3
Predicate<int> pred = new IsEven()
if pred.isTrueFor(x)
	print("x is even")
else
	print("x is odd")
destroy pred

When using lambda expressions, it is not necessary to define a new class implementing the functional interface. Instead the only function of the functional interface can be implemented where it is used, like so:

let x = 3
// Predicate is defined here:
Predicate<int> pred = (int x) -> x mod 2 == 0
if pred.isTrueFor(x)
	print("x is even")
else
	print("x is odd")
destroy pred

The important part is:

(int x) -> x mod 2 == 0

This is a lambda expression. It consists of two parts and an arrow symbol -> between the two parts. The left hand side of the arrow is a list of formal parameters, as you know them from function definitions. On the right hand side there is an expression, which is the implementation. The implementation consists only of a single expressions, because lambda expressions are typically small and used in one line. But if one expression is not enough there is the begin-end expression.

Remember that, because closures are just like normal objects, you also have to destroy them like normal objects. And you can do all the other stuff you can do with other objects like putting them in a list or into a table.

Type inference

It is not necessary to provide the parameter types for Lambda-parameters, if they can be inferred from the context. Moreover, parenthesis are optional, when there is only one parameter without type or no parameter.

Therefore the following definitions are equivalent:

Predicate<int> pred = (int x) -> x mod 2 == 0
Predicate<int> pred = (x) -> x mod 2 == 0
Predicate<int> pred = x -> x mod 2 == 0

begin-end expression

Sometimes one expression is not enough for a closure. In this case, the begin-end expression can be used. It allows to have statements inside an expression. The begin keyword has to be followed by a newline and an increase in indentation. It is possible to have multiple lines of statements within:

doAfter(10.0, () -> begin
	u.kill()
	createNiceExplosion()
	doMoreStuff()
end)

It is also possible to have a return statement inside a begin-end expression but only the very last statement can be a return.

Lambda blocks

Often a lambda expression with begin-end-block is given as the last argument in a line. Wurst offers a special Syntax for this case, which fits better with the general indentation based Syntax used in Wurst.

A lambda expression can be used after an assignment, local variable definition, return statement or function call. The arrow -> of the lambda expression must then be followed by a newline and an indented block of statements.

For example, the begin-end-block above can be replaced as follows:

doAfter(10.0) ->
	u.kill()
	createNiceExplosion()
	doMoreStuff()

The following examples uses a lambda with parameter u to iterate over all units owned by a player:

forUnitsOfPlayer(lastRinger) u ->
	if u.getTypeId() == TOWER_ID
		let pos = u.getPos()
		let facing = u.getFacingAngle()
		addEffect(UI.goldCredit, pos).destr()
		u.remove()
		createUnit(players[8], DUMMY_ID, pos, facing)

Capturing of Variables

The really cool feature with lambda expressions is, that they create a closure. This means that they can close over local variables outside their scope and capture them. Here is a very simple example:

let min = 10
let max = 50
// remove all elements not between min and max:
myList.removeWhen((int x) ->  x < min or x > max)

In this example the lambda expression captured the local variables min and max.

It is important to know, that variables are captured by value. When a closure is created the value is copied into the closure and the closure only works on that copy. The variable can still be changed in the environment or in the closure, but this will have no effect on the respective other copy of the variable.

This can be observed when a variable is changed after the closure is created:

var s = "Hello!"
let f = () ->
	print(s)
	s = s + "!"

s = "Bye!"
f.run()  // will print "Hello!"
f.run()  // will print "Hello!!"
print(s) // will print "Bye!"

Behind the scenes

The compiler will just create a new class for every lambda expression in your code. This class implements the interface which is given by the context in which the lambda expression is used. The generated class has fields for all local variables which are captured. Whenever the lambda expression is evaluated, a new object of the class is created and the fields are set.

So the “Hello!” example above is roughly equivalent to the following code:

// (the interface was not shown in the above code, but it is the same):
interface CallbackFunc
	function run()

// compiler creates this closure class implementing the interface:
class Closure implements CallbackFunc
	// a field for each captured variable:
	string s

	function run()
		// body of the lambda expression == body of the function
		print(s)
		s = s + "!"

var s = "Hello!"
let f = new Closure()
// captured fields are set
f.s = s
s = "Bye!"
f.run()  // will print "Hello!"
f.run()  // will print "Hello!!"
print(s) // will print "Bye!"

Function types

A lambda expression has a special type which captures the type of the parameter and the return type. This type is called a function type. Here are some examples with their type:

() -> 1
	// type: () -> integer

(real r) -> 2 * r
	// type: (real) -> real

(int x, string s) -> s + I2S(x)
	// type: (int, string) -> string

While function types are part of the type system, Wurst has no way to write down a function type. There are no variables of type “(int,string) -> string”. Because of this, a lambda expression can only be used in places where a concrete interface or class type is known. This can be an assignment where the type of the variable is given.

Predicate<int> pred = (int x) -> x mod 2 == 0

However it is not possible to use lambda expressions if the type of the variable is only inferred:

// will not compile, error "Could not get super class for closure"
let pred = (int x) -> x mod 2 == 0

Lambda expressions as code-type

Lambda expressions can also be used where an expression of type code is expected. The prerequisite for this is, that the lambda expression does not have any parameters and does not capture any variables. For example the following code is not valid, because the local variable x is captured.

let t = getTimer()
let x = 3
t.start(3.0, () -> doSomething(x)) // error: Cannot capture local variable 'x'

This can be fixed by attaching the data to the timer manually:

let t = getTimer()
let x = 3
t.setData(x)
t.start(3.0, () -> doSomething(GetExpiredTimer().getData()))

If a lambda expression is used as code, there is no new object created and thus there is no object which has to be destroyed. The lambda expression will just be translated to a normal Jass function, so there is no performance overhead when using lambda expressions in this way.

函数重载

Function overloading allows you to have several functions with the same name. The compiler will then decide which function to call based on the static type of the arguments.

Wurst uses a very simple form of overloading. If there is exactly one function in scope which is feasible for the given arguments, then this function will be used. If there is more than one feasible function the compiler will give an error.

Note that this is different to many other languages like Java, where the function with the most specific feasible type is chosen instead of giving an error.

function unit.setPosition(vec2 pos)
	...

function unit.setPosition(vec3 pos)
	...

function real.add(real r)
	...

function real.add(real r1, real r2)
	...

This works because the parameters are of different types or have a different amount of paramaters and the correct function can therefore be determined at compiletime.

function real.add(real r1)
	...

function real.add(real r1) returns real

This does not work because only the returntype is different and the correct function cannot be determined.

class A
class B extends A

function foo(A c)
	...

function foo(B c)
	...

// somewhere else:
	foo(new B)

This does not work either, because B is a subtype of A. If you would call the function foo with a value of type B, both functions would be viable. Other languages just take the “most specific type” but Wurst does not allow this. If A and B are incomparable types, the overloading is allowed.

操作符重载

Operator Overloading allows you to change the behavior of internal operators +, -, * and / for custom arguments. A quick example from the standard library (Vectors.wurst):

// Defining the "+" operator for the tupletype vec3
public function vec3.op_plus( vec3 v ) returns vec3
	return vec3(this.x + v.x, this.y + v.y, this.z + v.z)

// Usage example
vec3 a = vec3(1., 1., 1.)
vec3 b = vec3(1., 1., 1.)
// Without Operator Overloading (the add function was replaced by it)
vec3 c = a.add(b)
// With operator Overloading
vec3 c = a + b

You can overload operators for existing types via Extension-Functions or via class-functions for the specific classtype. In order to define an overloading function it has to be named as following:

+  "op_plus"
-  "op_minus"
*  "op_mult"
/  "op_divReal"

注解

Almost any definition in wurst can be annotated with one or more optionally named annotations. Annotations are compiletime only metadata which can be used for compiletime function, tests and callFunctionsWithAnnotation. In most cases annotations are generally disregarded unless you use them yourself. As of build#1124 annotations must be defined as a top level, bodiless function using @annotation, in order to be valid.

@annotation public function my_annotation() // Definition

@my_annotation function someOtherFunc() // Usage

The wurst reserved annotations are defined in the Annotations package.

@annotation public function compiletime()
@annotation public function deprecated()
@annotation public function deprecated(string _message)
@annotation public function compiletimenative()
@annotation public function configurable()
@annotation public function inline()
@annotation public function config()
@annotation public function extern()

编译时执行

Wurst includes an interpreter and can execute code at compiletime, which can be useful for testing and for object editing.

Compiletime Functions

Compiletime Functions are functions, that are executed when compiling your script/map. They mainly offer the possibility to create Object-Editor Objects via code.

A compiletime function is just a normal Wurst function annotated with @compiletime.

@compiletime function foo()

Compiletime functions have no parameters and no return value.

Compiletime functions are run by default. You can change this with the cmdline arguments -runcompiletimefunctions and -injectobjects. When you use compiletime functions to generate objects, Wurst will generate the object files next to your map and you can import them into your map using the object editors normal import function. Compared to ObjectMerger this has the advantage, that you can directly see your new objects in the object editor. You can also enable an option to directly inject the objects into the map file, though the changes will not be visible in the object-editor directly.

You can use the same code during runtime and compiletime. The special constant compiletime can be used to distinguish the two. The constant is true when the function was called at compiletime and false otherwise. The following example shows how this could be useful:

init
	doInit()

@compiletime
function doInit()
	for i = 1 to 100
		if compiletime
			// create item object
		else
			// place item on map

Compiletime Expressions

Similar to compiletime functions, Wurst also has compiletime expressions. As the name suggests, these are expressions, which are evaluated at compiletime. The result of executing the expression is placed into the mapscript instead of the original expression.

The syntax for compiletime expressions is a simple function call to the compiletime function defined in package MagicFunctions in the standard library. This function takes one argument, which is the expression to evaluate.

For example the following defines a global variable blub and initializes it with the compiletime expression fac(5):

let blub = compiletime(fac(5))

function fac(int x) returns int
    if x <= 1
        return 1
    return x * fac(x - 1)

The factorial function is evaluated at compiletime and returns 120. The number 120 then replaces the compiletime expression in the generated mapscript.

Just like compiletime functions, it is also possible to use compiletime expressions with object editing natives (see below).

Compiletime expressions have the restriction, that it is not possible to compile the map without the -runcompiletimefunctions flag.

Execution order

Compiletime expressions and functions are executed from top to bottom inside a package. Imported packages are executed before the importing package if the import-relation is one directional. Otherwise the execution order is not specified and depends on implementation details.

Functions available at compiletime

Not all functions which can be used in the game can be used at compiletime. Only a few functions are implemented in the Wurst compiler and emulate the respective functions from common.j.

The currently implemented functions can be found in the compiler code in the class NativeFunctionsIO.

Object Editing Natives

The standard library provides some functions to edit objects in compiletime functions. You can find the corresponding natives and higher level libraries in the objediting folder of the standard library.

The package ObjEditingNatives contains natives to create and manipulate objects. If you are familiar with the object format of Wc3 and know similar tools like Lua Object Generation or the ObjectMerger from JNGP, you should have no problems in using them. If you run Wurst with compiletime functions enabled, it will generate the object creation code for all the objects in your map. This code is saved in files named similar to “WurstExportedObjects_w3a.wurst.txt” and can be found right next to your map file. You can use this code as a starting point if you want to use the natives.

Wurst also provides a higher level of abstraction. For example the package AbilityObjEditing provides many classes for the different base abilities of Wc3 with readable method names. That way you do not have to look up the IDs.

The following example creates a new spell based on “Thunder Bolt”. The created spell has the ID “A005” for illustratory purpose. In proper code you should generate your IDs so you don’t have to deal with them directly. See this blog entry In the next line the name of the spell is changed to “Test Spell”. Level specific properties are changed using level closures, which calculate values for all levels.

package Objects
import AbilityObjEditing

@compiletime function myThunderBolt()
	// create new spell based on thunder bolt from mountain king
	new AbilityDefinitionMountainKingThunderBolt("A005")
	// change the name
	..setName("Wurst Bolt")
	..setTooltipLearn("The Wurstinator throws a Salami at the target.")
	// 400 damage, increase by 100 every level
	..presetDamage(lvl -> 400. + lvl * 100)
	// 10 seconds cooldown
	..presetCooldown(lvl -> 10.)
	// 0 mana, because no magic is needed to master Wurst
	..presetManaCost(lvl -> 0)
	// ... and so on

NOTE Packages exist for all object types.

自动化单元测试

You can add the annotation @Test to any function. Open the task runner F1 and type run test to see the available commands. You can either run the test under cursour, all in the package or all tests overall.

To perform assertions you need to import the Wurstunit package.

Example:

package Test
import Wurstunit

@Test public function a()
	12 .assertEquals(3 * 4)

@Test public function b()
	12 .assertEquals(5 + 8)

If you run this, you get the following output:

Running <PlayerData:114 - a>..
	OK!
Running <PlayerData:117 - b>..
	FAILED assertion:
	Test failed: Expected <13>, Actual <12>
... when calling b() in PlayerData.wurst:117
... when calling int_assertEquals(12, 13) in PlayerData.wurst:118

Tests succeeded: 1/2
>> 1 Tests have failed!

You can search the standard library for “@Test” to get some more examples.

代码规范

代码规范描述了一套规则,这些规则不会强制生成errors,但在开发者间作为建议被广泛接受,以生成一致并规范的代码.

源码的组织结构

文件结构

你的所有包应该丢进你工程的wurst/文件夹里。你可以在这个文件夹内创建任何形式的自定义文件夹结构,因为包不会受到文件夹结构的影响.

文件夹应该用来组织包和文件.

源文件名

如果一个 .wurst 文件只包含一个类或者元组(with potentially closely related declarations), 则该文件应该与该类或元组同名.如果一个文件包含多个类,元组,或者仅仅是一些顶部声明,那么选个能描述这个文件包含什么内容的名字.多个单词用驼峰命名法.(比如 ClosureEvents.wurst).

源文件的组织

我们鼓励把多个声明(类,元组,顶层的函数)放置在同一wurst源文件中的行为,只要这些声明在语义上高度关联.并且文件的长度足够合理(没有超过几百行)

类的布局

通常来说,类的内容以如下顺序保存.

  • 成员变量的声明
  • 构造函数
  • 一般函数声明

不要按字母序来排列函数声明,不要在扩展方法中分离常规函数.作为替代,将相关联的事物放在一起,这样的话如果有人自上而下阅读该类的时候,就能够搞清楚发生了什么.选择一个组织的顺序(要么更高层次的事物优先,或者与之相反),并长久的保持这样的习惯.包的API应该放在文件顶部,并配上一个文档说明,使得其他开发者能开门见山的看到。

将嵌套的类放在使用这些类的代码旁,如果这些类被设计为外部使用的,并且在类(文件?)内部没有被引用.则将他们放置在最后.

实现接口

实现接口的时候,保持接口的成员在实现中的顺序与在接口中声明的顺序一致.(如有需要的话,也可以跟该实现使用的额外私有方法一起散置开,)

重载布局

在一个类中,把相同的重载放在一起

命名规则

Wurst和Java的命名规则类似,特别要强调的是:

包名和类名开头大写,并用驼峰命名法 (MyExamplePackage).

类名开头大写,并用驼峰命名法

元组名开头小学,并用驼峰命名法

函数,属性,局部变量开头小写,用驼峰而非下划线

常量名使用全大写的下划线分割命名法.

选一个好名字

类名和元组名通常应该是名词或者名词组成的短句,以解释这个类或者元组是什么: LinkedList, InstantDummyCaster.

函数名通常是动词或者动词短语,以解释这些方法的作用: add, castImmediate. 该名字应该提示该方法是否修改了对象或返回了一个新对象

名字应该凸显出该实例的目的.所以最好不要用无意义的单词:(Manager, Wrapper etc.)

在用缩写命名的时候,如果缩写仅为两个字符,两个字符都大写 (IOTaskExecutor); 更长的话则单单大写首字符(LzwCompressor, Wc3String).

格式化

通常情况下,Wurst遵循JAVA的代码规范. In most cases, Wurst follows the Java coding conventions.

使用4个空格或者tap来做缩进.不要在同一个文件中混用两者

平行的空格

在二元操作符间添加空格 (a + b).

一元操作符不要添加空格(a++)

在流程控制的关键词间加一个单一的空格(if, switch, for and while) ,并跟着表达式,如果不是为了提升可读性,不要用括号.

函数的调用和声明,在开括号(就是())前不要有空格.

function foo(int x)

init
    foo(1)

不要在 (, [之前,或], )之后加空格.

不要在 ... 周围加空格

在注释后跟一个空格 //: // This is a comment

不要在用来指定参数类型的尖括号周围加空格 class HashMap<K, V>

作为通用的规则,避免任何形式的水平排列.将一个标识符重命名为不同的长度不应该影响声明和使用的格式.

Lambda 格式

In lambda expressions, spaces should be used around the begin and end keywords, as well as around the arrow which separates the parameters from the body. If a call takes a single lambda, it should be passed outside of parentheses whenever possible. Prefer putting lambda parameters as the last argument, so it can be written without begin and end.

list.filter(t -> t > 10)

execute() ->
	hash = hash()

Documentation comments (hot doc)

For documentation comments, also known as hot doc, that will show up in auto-completion, place the opening /** and close them with */. Short comments can be placed on a single line.

/** This is a short documentation comment. */

Wurst does not provide support @param and @return tags right now. Instead, you should incorporate the description of parameters and return values directly into the documentation comment, and add links to parameters wherever they are mentioned.

Avoiding warnings

Address any warnings the compiler shows for your code. If you need to ignore a warning for an intentionally unused variable, prefix it with an underscore _.

Idiomatic use of language features

Local variable declaration

Prefer declaring locals at the location where they are needed over declaring them all at the top like in Jass. Merge declarations and first assignments if sensible.

Type inference

Prefer using var and let over explicit types whenever possible.

Immutability

Prefer using immutable data to mutable. Always declare local variables and members as let rather than var if they are not modified after initialization.

Using loops

Prefer using higher-order functions (filter, map etc.) to loops. Exception: forEach (prefer using a regular for loop instead, unless the receiver of forEach is nullable or forEach is used as part of a longer call chain).

When making a choice between a complex expression using multiple higher-order functions and a loop, understand the cost of the operations being performed in each case and keep performance considerations in mind.

Unit Testing

Prefer test driven development if the feature is not too dependent on wc3 game mechanics. Create small, self-contained functions, annotate them with @Test individually and give them a descriptive name. Make sure to have at least one assertion inside your test to verify the behavior.

Tests should either be placed at the end of a package, or into a separate package suffixed Tests which will be ignored for autocomplete suggestions. Code and Tests should not be mixed.