Haxe笔记:HaXe语言和其它编程语言的区别

* haxe是亦动亦静的语言。
说它是静态语言,因为它基本是强类型的,声明变量时"var a: Int;",或者带初始化而省略类型的"var a = 5;",无论哪种方式,编译器都能确定a的类型,如果省略类型也不赋初值,那么也会在运行时首次赋值确定类型(或编译时?)
说它是动态语言,因为你可以使用对象Dynamic对象,或实现Dynamic接口,在Dynamic对象上,你可以添加新属性,甚至新方法,而haxe是不做类型检查的,而且Dynamic也可以用来和宿主平台传递数据。另外可以用类似json的语法定义匿名Dynamic对象,这点很像js,很方便。

* switch语句的区别
无论是c, c++, java, 还是js,switch的语法都差不多,每个分支后面需要用break来跳出,否则就会继续向下一分支执行,但haxe不需要break。一方面避免了有些新手常犯的低级错误(忘写break,估计所有程序员都犯过吧?),不过另一方面,也使得fall through这种技巧无法使用了,本人恰恰就是爱用这个的,呜呜~~

* 语句块 { ... } 的区别
haxe的语句块 ,即两个大括号括起来的多行语句,是返回一个值的,也就是最后一个表达式的值,比如: var a = { trace("hello"); "world"; },执行完之后a的值为"world"。
另外也可以这样用:var max = if (a > b) { a; } else { b; }
以及:var word = switch (input) { case 1: "first"; case 2: "second"; case 3: "third"; }
这种写法可以增加不少灵活性。

* for语句的区别
不支持java/js/c/c++里for (exp1; exp2; exp3) ...的语法,而只有一种,即for (i in Iterator),可以用...操作符来创建一个Iterator,比如:
for (i in 0...myArray.length) { ... }
这里"0...myArray.length"就可以创建一个从0到myArray.length - 1的迭代器。
另外,任何对象只要定义一个返回Iterator实例的iterator()方法,就能在for语句中用了。比如for (a in myObj) ...相当于for (a in myObj.iterator()) ...
个人感觉for (;;)语法很灵活,应该保留,可惜人家不听我的。。。

* 构造函数的区别
haxe里面只要在类中定义一个叫new的实例方法,即为其构造函数,这点和java区别不大,好处是改类名少改一处,不过现在大家都用IDE的重构功能改名了。。。
至于javascript,基于原型的派生那是另一套思想了,这里不比较了。

* 方法的重载
遗憾,haxe不支持真正的方法重载,即同名方法不同参数,包括构造方法,也就是你只能有一个new方法。不过好在haxe的默认参数很强大,可以用其来近似代替方法重载。不过个人感觉还是不太方便的说。

* 判断实例类型,以及强制类型转换
在haxe里面可以用Std.is(a, A)来判断o是否是A或A的子类的实例,相当于java和javascript里面的的instanceof关键字。
类型转换方面,因为Int派生于Float,所以整数可以直接当成浮点数用,但要把浮点数转为整数,就要用Std.int(number)来转换,这个转换是直接舍去小数部分,而不是四舍五入,要四舍五入请用Math.round()。
如果是对象的转换,就要用cast(a, A)了,这是个函数,意思是把a对象转换成A类的实例来使用。
java里面就简单了,直接用int a = (int) number,以及String s = (String) strObj,即可进行转换。
以上说的类型判断和类型转换都是强类型语言的范畴,检测也在编译时发生的,而javascript是动态类型的,基本没什么转换一说,一个对象拿来随便用就是了,当然访问不存在的属性会出运行时的错误。

* 类的定义及类成员的访问限制
haxe里面类定义的方式倒是和java差别不大,比如:
class A {
public var a: Int;
var b: Float;
public function new() {
a = 0;
b = 1.0;
}
public function foo() { }
}
值得注意的是haxe里面只有public和private两种访问级别,而不是java中的四种(protected和默认的包级),默认情况下只要不使用public修饰的所有成员都是private的。特别注意,haXe中的private级别相当于java的protected级别,也就是可被子类使用和覆盖的。

* 类成员的初始化
还是前面的例子,和java不同,类的实例属性是不能在定义的时候初始化的,而是必须在构造函数new里面初始化
但静态成员则是可以直接在定义时初始化的,如果静态成员的初始化逻辑比较复杂,还可以写一个静态的__init__()函数来进行复杂初始化。这点就相当于java的static { ... }语法,其实java的static { } 最后在类文件中也是保存为一个叫<clinit>的普通静态方法,呵呵。
至于javascript,其实没有类这种东西,有的只是构造函数,在构造函数里(或在其他地方)给对象增加属性就相当于添加类成员了,不是一个思想,没法比。

* 调试输出
在haxe里面是调用trace()方法,相当于java的System.out.println(),javascript么,我承认我一般是用alert()的...
在flash里面,trace默认是输出在播放器里面的,简单使用还好,但要查看复杂点的日志就不合适了。
在neko, windows等目标里,trace输出在控制台里,这点没什么可说的,不过在用nme开发android的时候,即使是直接传输到设备上运行,也是直接输出在PC的控制台里,这点实在是太方便了,以前做塞班和J2ME时可没这待遇!

* 类和类文件
在haxe里面,一个类文件里可以定义多个类,其中主类名必须和文件名相同,其它的类都被看做是主类的Helper类,比如:
// filename: Main.hx
class HelperA { ... }
private class HelperB { ... }
class Main { ... }
Helper类可以是public的,也可以是private的,比如上面的HelperA,那么可以在此类文件外部用Main.HelperA来调用,而HelperB则仅在此文件中可见。
所以这个工具类应该是对应于java中的静态内部类,从用法到意义都十分类似,而java中的实例内部类,以及源文件内声明的和文件名不同命的其它顶级类,在haxe中则没有对应物。当然,实例内部类这种东西,其大部分功能可以被函数对象代替了。

* 包和导入
haxe里面package的概念及用法和java非常类似,语法也一样,倒是和as的有些差别,当然我作为java爱好者,对as的那种方式自然是表示嗤之以鼻,哈哈。
haxe里面的import语法也和java基本一致,不过最大的不同就是haxe里面没法用通配符*来导入一个包中的所有类,这要是没有IDE支持,可真要累死个人哟!

* 使用using导入
这个是我看文档的,没亲自使用过,但是这特性很有意思,就写出来了。
在haxe里面如果用using导入一个类,那么这个类中所有的静态方法,都可以当做其第一个参数的类型的类成员方法来使用,比如:
class StringUtils {
public static function double( string:String ):String {
return string + string;
}
}
使用using导入StringUtils,就相当于给String类加了个叫double的成员方法,很有意思又有用的特色:
using my.pack.StringUtils;
"Hello!".double(); // returns "Hello!Hello!"

* 函数对象
跟javascript, php等动态语言类似,haxe的函数是一个对象,是可以作为参数传递的,也就是回调函数。这个特性java是没有的,java里你只能在参数中使用接口,然后在接口的实现中写接口方法的具体逻辑。
当然在haxe中,和javascript有区别的是,haxe可是强类型的,编译器得确保形参和实参的函数原型一致,这点要求更像C语言中的函数指针的使用,因为C语言也是强类型的,当年这里可是快绕死我了。
haxe里面函数原型在形参中的声明语法,虽然有些独特,但是还是很好理解的。比如:
function foo(callback: String -> Int -> Bool) : Bool {
return foo("value", 3);
}
这就表示callback这个函数必须是接受两个参数,一个String型,一个Int型,并返回Bool的。
另外在类属性中也可以有函数的引用,比如:
class A {
public parseString: String-> Int;
}
这样你可以给A的实例的parseString赋值,让它有不同的行为,这感觉可就有点像javascript里面的多态机制了。

* haxe的集合类
数组、列表、哈希表等集合类恐怕在任何语言里都是很重要的。java中有固定长度的数组,另外java.util包中有很多数据结构,如可变长度的有序列表类ArrayList, LinkedList,Vector,Stack等,无序集合类HashSet,映射类(关联数组)Hashtable, HashMap等等。
面对这么多东西,除了映射类之外的,不管是数组、列表、堆栈、集合,haxe只有一招鲜,就是Array<T>类,而映射的对应则是Hash<T>类,集合类都有类型参数,方便编译器进行类型检查。
haxe的Array长度是可变的,你可以像用java中的List那样,用push方法向数组末尾添加元素。
你也可以用'[]'操作符来给定索引随机访问一个元素,注意,haXe中的Array是个稀疏数组,你可以跳过若干索引直接读写后面的索引,而它不会检查数组是否越界。比如:
var a : Array<Int> = [];
a[55] = 20;
trace(a.length); // 输出56
用Array.length属性可以获取数组长度,但有点需要注意,在javascript和actionscript里面,length属性是可读写的,也就是说你可以设定length来截断数组,但在haxe里却不行,haxe里Array.length是只读的,即使你的编译目标是flash,这样你就只能用Array.splice()来截短数组了。
在haxe里面有个ArrayAccess<T>接口,据官方API参考说只要实现它就可以使用'[]'操作符来随机访问元素了,看上去很酷是不是?然而非常坑爹的是,文档里面没说具体实现方法,也没有例子!你要是认为像文档里说的那样,只要implements ArrayAccess<T>,你的对象就像变魔术似的能当数组用了,那你未免太天真了。目前看来(据我看了一些NME的源码后的认知),你要写两个方法__get(Int): T和__set(Int, T): Void,才能真正实现这一点,从这两个方法的名字可以看出,和__init__类似,又属于haxe的“潜规则”……
值得一提的是flash里面还有个非稀疏的Vector<T>集合类,我猜它的性能大概比数组高一些?它的长度是可写的,可惜在非flash的目标中,Vector<T>只是一个Array<T>的别名而已,有点坑爹了。
haxe中有个看上去很美的List<T>类,它是一个链表的实现,不过目前我感觉它的优势仅在于一种情形,也就是要在列表头部和尾部频繁增删元素的情况,比如一个双向的队列,但你无法删除链表的中间元素,因为遍历List只能用迭代器Iterator<T>,而haxe的Iterator没有删除当前元素的功能,而且List也没有在给定位置添加元素的方法。因此List的适用场合比较狭窄,和java中的同名数据结构不是一个概念。
haxe的Hash<T>类和java的HashMap<K, V>不同,它的键总是字符串类型的,当然这也覆盖了90%以上的需求了。
haxe中还有IntHash<T>类,它的键是Int类型。

* 类型参数
haxe支持类型参数,目前我还没太深入研究,不过使用还是很频繁的,因为haxe的集合类都是需要类型参数的嘛,如Array<T>, Hash<T>等。
简单使用的感觉和java差不多,不过更方便点,因为在haxe里,你如果忽略类型声明,编译器会自己“猜”真正的类型,比如:
var a = new Array<Float>();
看到了吧,我不用声明b的类型,因为通过这个赋值haxe编译器就能确定b的类型了,要是在java中,你就不得不写:
ArrayList<Integer> a = new ArrayList<Integer>();
啰嗦多了,尤其是类型参数复杂的时候比如Map<String, List<String>>

* 可选参数,及可选参数的默认值
在haxe里面可选参数很强大,不像别的语言,只能把可选参数放在参数列表的最后,在haxe里面可以放在任意位置,而且只要不会引起歧义,调用的时候就可以任意忽略可选参数。比如:
public function foo(?s: String = "default", ?a = 1, ?b: Array<Int>) { ... }
上面这个例子表示,s的默认值是"default",a的类型是Int,默认值则是1, b的默认值是null。注意到了吧,我没声明a的类型,编译器一样可以替我“猜”,如果你指定了默认赋值,那么前面的问号也可以省略的。
在上面的例子中,你调用foo(), foo("hello"), foo(5), foo([ 3, 4, 5])都可以。
有意思的一点是,在其它语言中,基本类型比如整形、布尔型、浮点数型等,是不能为null值的,但在haxe里面,可以用一个技巧允许基本类型的参数的默认值为null,如下:
public function foo(?a: Null<Float>, ?b: Null<Bool>) { }
这样,如果你调用foo(),那么在foo中,你得到的a和b都是null。这在有些情况中还是有用的,比如你想用null来表示一种特殊情况来处理。
另外,似乎在flash目标中,如果你不这么写,就无法在调用时忽略基本类型的参数,我目前还没深入研究,反正用上面的这种写法就能有效规避这个问题了。

* 枚举类型
haxe的枚举也是个很有意思的实现,从我目前的理解,枚举是一个类,而每个枚举值,都是这个类的一个构造函数。比如:
enum Sex {
male;
female;
}
然后你就可以用Sex.male和Sex.female来饮用枚举值了。
下面,最有意思的地方来了,既然是构造函数,那么就可以有参数,比如:
enum Align {
left;
right;
center;
custom(position: Float);
}
也就是说Align.custom这个枚举值可以有无穷多个可能值,这在很多时候还真是方便。就比如我这个例子,你就可以使用switch来根据枚举值确定对齐方式:
var x = switch (align) { case Align.left: 0; case Align.right: width; case Align.center: width / 2; case custom(position): position; }
而且这种写法好像也是唯一一种能够访问带参数的枚举值的方式。在这种针对枚举的switch里面,条件分支必须包含所有的枚举值,否则编译器会报错。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注