语法手册

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代码被组织成包(packages)。任何代码片段都必须放在包中才能被识别。 包可以导入(import)其他包,以访问被导入包中定义的变量、函数和类。 包有一个可选的 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 会引发一个警告,因为它已经被自动导入了。 您可以删除此导入语句,它在这里仅用于演示如何导入包。

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

您仍然可以在包之外使用普通的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_ 是一个函数或变量的名称。它可以由字母、数字和下划线组成。 (译者注:以单个下划线开头或结尾的函数名可能会导致编译成功但运行时出错的情况)

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

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

级联操作符 .. 类似于普通的 .,可以用来调用函数,但它不返回函数的结果,而是返回调用者对象。这使得对同一个对象进行链式调用变得容易。 请看以下示例:

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 应该具有兼容的类型才能在同一个三元表达式中使用。

条件表达式通常不常用。 一般用于在两个备选结果中进行选择,如下所示:

// 处理单数和复数形式的 "enemy"
let enemyCount = n.toString() + " " + (n == 1 ? "enemy" : "enemies")

语句

Wurst 为 Jass 添加了一些现代化的语句,例如 switchfor 循环。

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 语句

// i 是一个整型变量
switch i
    case 1
        print("1")
    case 3
        print("3")
    case 88
        print("88")
    default
        print("默认情况")

switch 语句用于处理有许多 if-else if 条件的场景。

循环

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

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 循环内的任何 return 语句之前被调用。

如果你已经有了一个迭代器,或者想要访问它的每个函数,你可以使用 for-from 循环。其转换也非常简单:

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

// 等效于:
let iterator = myList.iterator()
while iterator.hasNext()
    segment = iterator.next()
    //Statements
iterator.close() 

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

Skip

最基本的语句是 _skip_ 语句,它不做任何事情。在某些极少数情况下它可能有用。

迭代器

你可以通过为你的类型提供一组函数来使其支持 Wurst 的迭代器。通过提供迭代器,该类型就可以用于 for 循环:

  • 函数 hasNext() 返回 boolean(如果还有下一个元素,则返回 true
  • 函数 next() 返回 TYPE(返回下一个元素)

有了这两个函数,你就得到了一个可以用于 for-from 循环的迭代器。

如果你想让一个类型能用于 for-in 循环,你需要提供:

  • 函数 iterator() 返回 Iterator

对于你的类型,它返回一个用于迭代的对象。 这个对象可以是一个句柄(就像 group-iterator),也可以是一个类实例(就像 List-iterator)。 你的迭代器还应该提供一个 close 函数,用于清理它分配的所有资源。 通常,迭代器只是在 close 函数中销毁它自己。

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

Group-Iterator

public function group.iterator() returns 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)

如你所见,这个迭代器就是一个单位组,因此 group 类型需要提供上述函数。标准库通过扩展函数来实现这一点。

LinkedList-Iterator

public class LinkedList<T>
    ...

    // 获取此列表的迭代器
    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 迭代器有点不同,因为它是一个类。 它仍然提供了所需的功能,因此可以作为迭代器使用。 它还包含一些用于辅助迭代的成员变量。 当从 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。 我们可以通过在 C 中也导入 A 来解决这个问题,但有时你希望避免这种额外的导入。 使用公开导入(import public)可以解决这个问题,因为它会传递所有导入的内容。因此,以下代码将正常工作:

package A
public constant x = 5

package B
import public A

package C
import B
constant z = x

特殊的wurst包

默认情况下,每个包都会隐式导入一个名为 Wurst.wurst 的包,该包在标准库中定义。 这个包导入了一些最常用的标准库包。

如果你不想要这个标准的隐式导入,可以通过导入 NoWurst 包来禁用它。 该指令主要用于解决标准库中的一些循环依赖问题。

公开声明

默认情况下,包的所有成员都是私有的。如果想在导入该包的其他包中使用它们,必须添加 public 关键字。

常量

你可以将一个变量声明为常量(constant),以禁止在初始化后对其进行修改。这对生成的代码没有影响,但会在编译时抛出错误。

例子

package First
int i // (默认为私有)只在此包中可用
public int j // 公开的,导入此包后即可使用

package Second
import First

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


package Foo
constant int myImportantNumber = 21364 // 常量必须在声明时初始化

function blub()
    myImportantNumber = 123 // 错误

public constant int myPrivateNumber2 = 123 // 注意:包级变量和常量必须在调用它们的函数之前定义。

Init 初始化块

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

init 块开始执行时,你可以假定当前包中的所有全局变量都已初始化。

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

提示: 由于魔兽争霸3的字节码限制,主函数(main function)内的操作过多会导致其无法完全执行。 为了避免这个问题,Wurst 使用 TriggerEvaluate 在一个单独的“线程”中执行每个包的初始化,以确保包能够成功初始化。 如果在单个包的初始化过程中计算量过大,仍然可能会出现问题,此时编译器会显示警告。

初始化和延迟初始化

Wurst 的初始化规则很简单:

  1. 包内的初始化是自上而下完成的。一个包的初始化内容包括所有全局变量的初始化(包括静态类变量)和所有的 init 块。
  2. 如果包 A 导入了包 B,并且该导入不是 initlater 导入,那么包 B 的初始化总是在包 A 之前运行。不允许出现不带 initlater 的循环导入。

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

package A
import initlater B
import public initlater C
import D

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

配置包

全局变量和函数可以被配置。配置是通过配置包(Configuration Packages)完成的。 每个包最多只能有一个配置包,并且每个配置包只配置一个包。 包与其配置包之间的关系通过命名约定来表示:对于一个名为 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”,可以将变量和函数声明为“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]

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

数组成员

Wurst通过将定长数组成员转换为多个独立变量,并通过二叉树搜索在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
    // 我们用火球特效来构造它
    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个接口的类的所有实例特别有用

防御者方法

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

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

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.

泛型

泛型使得从特定类型中抽象出来,只用类型的占位符进行编程成为可能。这对于容器类型(例如列表)尤其有用,因为我们不想为每个需要列表的类 A、B、C 都编写一个 ListOfA、ListOfB、ListOfC。可以把它想象成创建一个通用的列表,它拥有所有功能,但是针对一个未知的类型,这个类型在创建实例时才被定义。

有了泛型,我们就可以只为列表编写一个实现,并将其用于所有类型。泛型类型参数和实参写在标识符后面的尖括号(< 和 >)中。

// 一个带有类型参数 T 的泛型 Set 接口
interface Set<T>
    // 向集合中添加一个元素
    function add(T t)

    // 从集合中移除一个元素
    function remove(T t)

    // 返回集合中元素的数量
    function size() returns int

    // 检查某个元素是否在集合中
    function contains(T t) returns boolean

class SetImpl<T> implements Set<T>
    // [...] 接口的实现

如果我们这样定义了一个类,我们就可以将它与一个具体的类型(例如 Missile)一起使用:

// 创建一个导弹集合
Set<Missile> missiles = new SetImpl<Missile>();
// 添加一个导弹 m
missiles.add(m);

Wurst 中的泛型参数可以直接绑定到整数、类类型和接口类型。为了将泛型参数绑定到原始类型、句柄类型和元组类型,您必须提供以下函数

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

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

原始类型和句柄类型的类型转换函数在 Typecasting.wurst 中使用 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)

泛型函数

函数可以使用泛型类型。类型参数写在函数名之后。在下面的例子中,函数 forall 测试一个断言是否对列表中的所有元素都为真。该函数必须是泛型的,因为它必须适用于所有类型的列表。

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

// 用法:
    LinkedList<int> l = ...
    // 检查列表中的所有元素是否都是偶数
    if forall<int>(l, (int x) -> x mod 2 == 0)
        print("true")

调用泛型函数时,如果类型实参可以从函数的实参中推断出来,则可以省略它们:

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

扩展函数也可以是泛型的,如下例所示:

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

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

模块

模块_是一个为类提供某些功能的小包。类可以_使用(use)模块来继承模块的功能。

你可以像在类中声明一样使用所用模块中的函数。你也可以_重写_(override)模块中定义的函数来调整其行为。

如果你了解像 Java 或 C# 这样的面向对象语言:模块就像抽象类,使用模块就像从抽象类继承,但没有子类型化。模块封装了你可能想添加到多个类中的行为,而没有开销和层次结构。

示例 1

在这个例子中,我们只有一个使用模块 A 的类。最终的程序行为就好像模块 A 的代码被粘贴到类 C 中一样。

module A
    public function foo()
        ...

class C
    use A

示例 2

模块不仅仅是复制粘贴代码的机制。类和模块可以重写在所使用的模块中定义的函数:

// 一个存储整型变量的模块
module IntContainer
    int x

    public function getX() returns int
        return int

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

// 一个只存储正整数的容器
module PositiveIntContainer
    use IntContainer

    // 重写 setter 以只存储正整数
    override function setX(int x)
        if x >= 0
            IntContainer.setX(x)

可见性与使用规则

  • 模块的变量总是私有的
  • 私有函数只能从模块本身使用
  • 模块的每个函数都必须声明为 public 或 private
  • 如果一个类使用一个模块,它只继承该模块的公共函数
    • 你可以私下(private)使用一个模块(尚未实现)。这将让你能够使用模块的功能,而无需将其函数暴露给外部。

重写函数

  • 你可以通过在函数前写上 override 关键字来重写在所使用的模块中定义的函数。
  • 如果使用了两个具有相同函数的模块,那么它必须被底层的类或模块重写,以避免歧义(当然,这只有在函数签名匹配时才可能。我们正在考虑这个问题的解决方案)
  • 私有函数不能被重写

抽象函数

模块可以声明抽象函数:即没有给定实现的函数。抽象函数必须由底层的类来实现。

Thistype

你可以在模块内部使用 thistype 来引用使用该模块的类的类型。当需要将类转换为整数再转换回来时,这会很有用。

高级概念

枚举

在 Wurst 中,枚举 (Enums) 是一组命名整数常量的简写包装器。 枚举与类无关,并直接转换为它们所代表的整数。 其主要目的是在通常会使用整数的地方添加安全、舒适的 API。

用法类似于通过枚举名称访问静态类成员:

enum MyUnitState
	FLYING
	GROUND
	WATER

let currentState = MyUnitState.GROUND

枚举可以用作类成员

class C
	State currentState

	construct(State state)
		currentState = state

要检查枚举的当前值,你可以使用 switch 语句。 请注意,必须检查所有枚举成员(或使用 default)。

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

请注意,在 switch 语句和变量赋值中,限定符 MyUnitState 可以省略。

要获取枚举成员所代表的整数,只需将其转换为 int 类型:

print((MyUnitState.GROUND castTo int).toString()) // 将打印 "1"

合并的整数值从 0 开始,每个后续的枚举成员递增。因此,对于 MyUnitStateFLYING 将是 0,GROUND 是 1,WATER 是 2。

元组类型

通过元组类型,你可以将多个变量组合成一个包。这可以用于从函数返回多个值、创建自定义类型,当然也可以提高可读性。

请注意,元组不像类。它们有一些重要的区别:

  • 你不需要销毁元组值。
  • 当你将元组赋给另一个变量或将其传递给函数时,你会创建该元组的副本。对此副本的更改不会影响原始元组。
  • 元组类型不能绑定到类型参数,因此如果 vec 是元组类型,你就不能拥有 List{vec}
  • 由于元组类型不是在堆上创建的,与使用单个变量相比,没有性能开销。
// 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()

因为元组本身没有任何函数,你可以为现有的元组类型添加扩展函数,以实现类似类的功能。 请记住,你不能在元组的扩展函数中修改其值——因此,如果你想更改某些内容,每次都必须返回一个新的元组。 可以查看标准库中的 Vector 包以获取一些元组使用示例。(Math/Vectors.wurst)

拓展函数

扩展函数使你能够向现有类型“添加”函数,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。 扩展函数是一种特殊的静态函数,但它们的调用方式就像它们是扩展类型的实例函数一样。

声明

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

示例

// 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

不定参函数

可变参数函数可以传递可变数量的同类型参数。它们最常用于防止样板代码并提供更好的 API。 在函数内部,可以通过 for .. in 循环访问可变参数。

示例:

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)

在此示例中,所有对 asNumberList 的调用都是有效的,其好处显而易见。我们可以向函数传递任意数量的整数(最多 31 个参数),但我们只需要实现一次。

限制

当前的实现会创建一个具有正确参数数量的专用函数。 由于 Jass 最多允许 31 个参数,因此函数调用总共不能使用超过 31 个参数。

Lambdas和闭包

Lambda 表达式(也称为匿名函数)是一种轻量级的方式,用于提供函数式接口或抽象类的实现(为简化文本,以下解释均指接口,但抽象类可以以相同的方式使用)。

函数式接口是只有一个方法的接口。 这是一个例子:

// 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

使用 lambda 表达式时,无需定义一个实现该函数式接口的新类。相反,该函数式接口的唯一函数可以在其使用的地方实现,如下所示:

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

重要的部分是:

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

这是一个 lambda 表达式。它由两部分和一个箭头符号 -> 组成。箭头的左侧是形式参数列表,就像你从函数定义中了解的那样。右侧是一个表达式,即实现部分。实现通常只包含单个表达式,因为 lambda 表达式通常很小并且在一行中使用。但如果一个表达式不够,可以使用 begin-end 表达式。

请记住,因为闭包就像普通对象一样,你也必须像销毁普通对象一样销毁它们。你还可以对它们执行所有可以对其他对象执行的操作,比如将它们放入列表或表中。

类型推断

如果可以从上下文中推断出 Lambda 参数的类型,则无需提供它们。此外,当只有一个无类型参数或没有参数时,括号是可选的。

因此,以下定义是等效的:

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 表达式

有时一个表达式对于闭包来说是不够的。在这种情况下,可以使用 begin-end 表达式。它允许在表达式内部包含语句。begin 关键字后必须跟一个换行符和增加的缩进。可以在其中包含多行语句:

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

在 begin-end 表达式内部也可以有 return 语句,但只有最后一条语句可以是 return。

Lambda 块

通常,带有 begin-end 块的 lambda 表达式作为一行中的最后一个参数。Wurst 为这种情况提供了一种特殊的语法,它更符合 Wurst 中使用的基于缩进的通用语法。

lambda 表达式可以在赋值、局部变量定义、返回语句或函数调用之后使用。lambda 表达式的箭头 -> 后面必须跟一个换行符和一个缩进的语句块。

例如,上面的 begin-end 块可以替换如下:

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

以下示例使用带参数 u 的 lambda 遍历玩家拥有的所有单位:

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)

变量捕获

lambda 表达式真正酷的特性是它们会创建一个闭包。 这意味着它们可以封闭其作用域之外的局部变量并捕获它们。 这是一个非常简单的例子:

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

在这个例子中,lambda 表达式捕获了局部变量 min 和 max。

重要的是要知道,变量是按值捕获的。当创建闭包时,值被复制到闭包中,闭包只在该副本上工作。变量仍然可以在环境或闭包中更改,但这不会对变量的另一个副本产生任何影响。

当变量在闭包创建后被更改时,可以观察到这一点:

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!"

幕后原理

编译器会为代码中的每个 lambda 表达式创建一个新类。 这个类实现了由 lambda 表达式使用上下文给出的接口。 生成的类为所有捕获的局部变量设有字段。 每当 lambda 表达式被求值时,就会创建该类的一个新对象并设置其字段。

所以上面的“Hello!”示例大致等同于以下代码:

// (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!"

函数类型

lambda 表达式有一种特殊的类型,它捕获参数的类型和返回类型。这种类型称为函数类型。以下是一些示例及其类型:

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

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

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

虽然函数类型是类型系统的一部分,但 Wurst 没有办法写下函数类型。 没有类型为 (int,string) -> string 的变量。 因此,lambda 表达式只能在已知具体接口或类类型的地方使用。 这可以是一个赋值语句,其中变量的类型是给定的。

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

但是,如果变量的类型只是被推断出来,则无法使用 lambda 表达式:

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

作为 code 类型的 Lambda 表达式

Lambda 表达式也可以在需要 code 类型表达式的地方使用。 这样做的前提是,lambda 表达式没有任何参数,也不捕获任何变量。例如,以下代码是无效的,因为它捕获了局部变量 x

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

这可以通过手动将数据附加到计时器来解决:

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

如果 lambda 表达式用作 code,则不会创建新对象,因此也没有需要销毁的对象。lambda 表达式将被直接翻译成一个普通的 Jass 函数,因此以这种方式使用 lambda 表达式没有性能开销。

函数重载

函数重载允许你拥有多个同名函数。 编译器将根据参数的静态类型来决定调用哪个函数。

Wurst 使用一种非常简单的重载形式。如果在作用域中只有一个函数对于给定的参数是可行的,那么将使用该函数。 如果存在多个可行的函数,编译器将报错。

请注意,这与许多其他语言(如 Java)不同,在这些语言中,会选择具有最具体可行类型的函数,而不是报错。

function unit.setPosition(vec2 pos)
	...

function unit.setPosition(vec3 pos)
	...

function real.add(real r)
	...

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

这是可行的,因为参数类型不同或参数数量不同,因此可以在编译时确定正确的函数。

function real.add(real r1)
	...

function real.add(real r1) returns real

这是不可行的,因为只有返回类型不同,无法确定正确的函数。

class A
class B extends A

function foo(A c)
	...

function foo(B c)
	...

// somewhere else:
	foo(new B)

这也不可行,因为 B 是 A 的子类型。如果你用 B 类型的值调用函数 foo,两个函数都将是可行的。其他语言会选择“最具体的类型”,但 Wurst 不允许这样做。如果 A 和 B 是不可比较的类型,则允许重载。

操作符重载

操作符重载允许你为自定义参数更改内部操作符 +、-、* 和 / 的行为。 一个来自标准库的快速示例 (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

你可以通过扩展函数为现有类型重载操作符,或通过类函数为特定类类型重载。 为了定义一个重载函数,它必须按以下方式命名:

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

注解

Wurst 中的几乎任何定义都可以用一个或多个可选的命名注解进行标注。 注解是仅在编译时存在的元数据,可用于编译时函数、测试和 callFunctionsWithAnnotation。 在大多数情况下,除非你自己使用,否则注解通常会被忽略。 从 build#1124 开始,注解必须使用 @annotation 定义为顶级的无函数体函数才有效。

@annotation public function my_annotation() // Definition

@my_annotation function someOtherFunc() // Usage

Wurst 的保留注解在 Annotations 包中定义。

@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 包含一个解释器,可以在编译时执行代码,这对于测试和对象编辑非常有用。

编译时函数

编译时函数是在编译你的脚本/地图时执行的函数。 它们主要提供了通过代码创建对象编辑器对象的能力。

编译时函数只是一个用 @compiletime 注解的普通 Wurst 函数。

@compiletime function foo()

编译时函数没有参数,也没有返回值。

编译时函数默认运行。你可以使用命令行参数 -runcompiletimefunctions-injectobjects 来更改此行为。 当你使用编译时函数生成对象时,Wurst 会在你的地图旁边生成对象文件,你可以使用对象编辑器的常规导入功能将它们导入到你的地图中。与 ObjectMerger 相比,这样做的好处是你可以直接在对象编辑器中看到你的新对象。 你还可以启用一个选项,将对象直接注入到地图文件中,不过这些更改不会直接在对象编辑器中显示。

你可以在运行时和编译时使用相同的代码。 特殊常量 compiletime 可用于区分两者。 当函数在编译时调用时,该常量为 true,否则为 false。 以下示例展示了这可能如何有用:

init
	doInit()

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

编译时表达式

与编译时函数类似,Wurst 也有编译时表达式。 顾名思义,这些是在编译时求值的表达式。 执行表达式的结果会取代原始表达式,被放入地图脚本中。

编译时表达式的语法是对标准库中 MagicFunctions 包内定义的 compiletime 函数的简单调用。 该函数接受一个参数,即要求值的表达式。

例如,以下代码定义了一个全局变量 blub,并用编译时表达式 fac(5) 对其进行初始化:

let blub = compiletime(fac(5))

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

阶乘函数在编译时被求值并返回 120。 然后,数字 120 会在生成的地图脚本中替换掉编译时表达式。

就像编译时函数一样,编译时表达式也可以与对象编辑原生函数一起使用(见下文)。

编译时表达式有一个限制,即如果不带 -runcompiletimefunctions 标志,就无法编译地图。

执行顺序

编译时表达式和函数在包内从上到下执行。 如果导入关系是单向的,则导入的包会在导入它的包之前执行。 否则,执行顺序未指定,并取决于实现细节。

编译时可用的函数

并非所有可以在游戏中使用的函数都可以在编译时使用。 只有少数函数在 Wurst 编译器中实现,并模拟了 common.j 中的相应函数。

当前实现的函数可以在编译器代码的 NativeFunctionsIO 类中找到。

对象编辑原生函数

标准库提供了一些在编译时函数中编辑对象的函数。 你可以在标准库的 objediting 文件夹中找到相应的原生函数和更高级别的库。

ObjEditingNatives 包包含用于创建和操作对象的原生函数。如果你熟悉 Wc3 的对象格式,并了解类似 Lua 对象生成 或 JNGP 的 ObjectMerger 等工具,那么使用它们应该没有问题。如果你在启用编译时函数的情况下运行 Wurst,它将为地图中的所有对象生成对象创建代码。此代码保存在类似于 “WurstExportedObjects_w3a.wurst.txt” 的文件中,可以在你的地图文件旁边找到。如果你想使用原生函数,可以将此代码作为起点。

Wurst 还提供了更高级别的抽象。例如,AbilityObjEditing 包为 Wc3 的不同基础技能提供了许多类,并带有可读的方法名。这样你就不必查找 ID。

以下示例基于“雷霆一击”创建一个新法术。为说明目的,创建的法术 ID 为“A005”。 在正确的代码中,你应该生成你的 ID,这样就不必直接处理它们。请参阅这篇博客文章 在下一行中,法术的名称被更改为“测试法术”。 特定等级的属性使用等级闭包进行更改,该闭包会计算所有等级的值。

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

注意 所有对象类型都存在相应的包。

自动化单元测试

你可以将 @Test 注解添加到任何函数。打开任务运行器 F1 并输入 run test 以查看可用命令。你可以运行光标下的测试、包中的所有测试或所有测试。

要执行断言,你需要导入 Wurstunit 包。

示例:

package Test
import Wurstunit

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

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

如果你运行此代码,将得到以下输出:

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!

你可以在标准库中搜索“@Test”以获取更多示例。

代码规范

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

源码的组织结构

文件结构

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

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

源文件名

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

源文件的组织

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

类的布局

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

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

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

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

实现接口

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

重载布局

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

命名规则

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

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

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

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

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

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

选一个好名字

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

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

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

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

格式化

通常情况下,Wurst遵循JAVA的代码规范.

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

平行的空格

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

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

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

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

function foo(int x)

init
    foo(1)

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

不要在 ... 周围加空格

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

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

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

Lambda 格式

在 lambda 表达式中,应在 beginend 关键字周围以及分隔参数和主体的箭头周围使用空格。如果一个调用只接受单个 lambda,应尽可能将其放在括号外传递。倾向于将 lambda 参数作为最后一个参数,这样就可以在不使用 beginend 的情况下编写。

list.filter(t -> t > 10)

execute() ->
	hash = hash()

文档注释 (hot doc)

对于将在自动补全中显示的文档注释(也称为 hot doc),请使用 /** 开头,并用 */ 结尾。简短的注释可以放在单行上。

/** 这是一个简短的文档注释。 */

Wurst 目前不支持 @param 和 @return 标签。相反,您应该将参数和返回值的描述直接整合到文档注释中,并在提到参数的地方添加链接。

避免警告

处理编译器为您的代码显示的所有警告。如果您需要忽略一个有意未使用的变量的警告,请在其名称前加上下划线 _

通顺地使用语言特性

局部变量声明

倾向于在需要局部变量的地方声明它们,而不是像在 Jass 中那样将它们全部声明在顶部。如果合理,请将声明与首次赋值合并。

类型推断

尽可能使用 varlet 进行类型推断,而不是显式指定类型。

不可变性

倾向于使用不可变数据而非可变数据。如果局部变量和成员在初始化后不再被修改,应始终将其声明为 let 而不是 var

循环的使用

倾向于使用高阶函数(filtermap 等)而非循环。例外:forEach(倾向于使用常规的 for 循环,除非 forEach 的接收者是可空的,或者 forEach 被用作更长调用链的一部分)。

在选择使用包含多个高阶函数的复杂表达式还是使用循环时,请了解每种情况下所执行操作的成本,并考虑性能问题。

单元测试

如果功能不太依赖于 wc3 游戏机制,则倾向于使用测试驱动开发。创建小型的、自包含的函数,用 @Test 单独注解它们,并给它们一个描述性的名称。 确保您的测试中至少包含一个断言以验证其行为。

测试应放置在包的末尾,或者放置在一个以 Tests 为后缀的独立包中,该包将被自动补全建议所忽略。代码和测试不应混合在一起。