杂项
§ 运行代码方式
Crystal 是一门编译型、静态类型的语言,因此你需要先编译,再运行。
假设你有一个 foo.cr:
1
2 # Crystal
3 puts "Hello world"
4
你需要首先 build 它
1
2 $ crystal build foo.cr
3
它在当前文件夹下,创建了一个可执行文件叫做:foo,然后你可以运行它。
1
2 $ ./foo
3 Hello world
4
当作为最终发布版分发时,记得添加 --release, 这开启了最高级别的 llvm 优化,它带来了好得多的性能,但需要更长的 build 时间。
当执行基准测试时,请总是记得开启它。
1
2 $ crystal build --release foo.cr
3
crystal build --help 来获取更多的帮助。
§ 迭代器(Iterator)
Crystal 额外引入了 Iterator 类型(等价于 Ruby 中的 Enumerator::Lazy)
通常来说,当调用一个 Enumerable 方法,例如:#each, #map 等,但是没有代码块时, 会返回一个 lazy 的 Iterator 对象。
举个例子:我们希望取出一千万以内的前三个偶数,并将其 ✖️ 3。
1
2 (1..10_000_000).select(&.even?).map { |x| x * 3 }.first(3) # => [6, 12, 18]
3
上面的写法是工作的,但是建立了数个不必要的中间数组,我们将其分解如下:
1
2 (1..10_000_000).select(&.even?) # => 返回一个大小为 500 万,元素全是偶数的数组
3
4 map { |x| x * 3 } # => 将上面的的所有元素✖️3
5
6 first(3) # => 最后取出前三个
7
这带来额外的计算以及不必要的巨大内存消耗,因为我们只对前三个元素感兴趣, 但是却返回了 500 万个,并计算,但最后只取了前三个,其他被抛弃。
更高效的做法,首先通过 each 返回一个 lazy 的 Iterator,因为 Iterator 重新定义了很多 Enumerable 的方法,例如:上面的 #map, #select, #first, 调用这些方法,返回了一个 包裹调用者 Iterator 对象的新的 Iterator 对象,在调用链的最后,我们仍然得到的是一个新的 lazy 的 Iterator 对象,没有任何实际计算,
1
2 (1..10_000_000).each.select(&.even?).map { |x| x * 3 }.first(3) # => #<Iterator::FirstIterator ... >
3
分解如下:
1
2 (1..10_000_000).each # => <Range::ItemIterator ...>
3 select(&.even?) # => Iterator::SelectIterator(<Range::ItemIterator ...> ... >>
4 ...
5 first(3) # => #<Iterator::FirstIterator ... >
6
当我们希望取出值时,我们可以再次调用 #each(&) 或 to_a
1
2 first_three_iter = (1..10_000_000).each.select(&.even?).map { |x| x * 3 }.first(3)
3
4 first_three_iter.each do |x|
5 p x # => 直到这里才实际执行计算。
6 end # => 打印 6, 12, 18
7
8 # 因为 Iterator 是单向的,上面已经通过 each(&) 将所有的三个结果都取出了
9
10 first_three_iter.to_a # => [],因此 to_a 返回空数组。
11
正如你猜想的那样,你也可以通过 Iterator#next 方法,一个一个的将元素取出。
1
2 iter = (1..5).each
3
4 iter.next # => 1
5 iter.next # => 2
6 typeof(iter.next) # => Int32 | Iterator::Stop
7
创建一个新的 Iterator (Ruby 里面叫做 Enumerator) 也是可以的, 需要下面的几步:
- 创建一个类,并且混入 Iterator(T) 模块
- 定义一个 #next 方法,返回下一个元素
- 在达到结尾时,调用 #stop 方法返回一个 Iterator::Stop::INSTANCE
例如,下面是一个 Zeros 类,返回指定数量的 0
1
2 class Zeros
3 include Iterator(Int32)
4
5 def initialize(@size : Int32)
6 @produced = 0
7 end
8
9 def next
10 if @produced < @size
11 @produced += 1
12 0
13 else
14 stop
15 end
16 end
17 end
18
19 zeros = Zeros.new(5)
20
21 zeros.each {|e| print e } # => 00000
22
§ require 用法不同
§ Crystal 移除了 require_relative 方法
代之,可以直接使用 require 来引用相对路径的文件。
1
2 require "./foo"
3
并且支持和 File.match? 一样的增强版的 shell filename globbing
- * 支持任意数量的除了目录分隔符之外的任意字符。
例如:require "./foo/*",匹配 foo 目录下的所有 cr 文件, 但不含子目录。
- /** 递归的匹配所有子目录中的 cr 文件
例如:require "./foo/**",匹配 foo 目录以及所有子目录下的所有 cr 文件。
§ $CRYSTAL_PATH
类似于 Ruby 在 $RUBYLIB 从前往后中查找 lib 文件夹来确定引用的 gem, Crystal 等价的环境变量 叫做 $CRYSTAL_PATH, 可以通过 crystal env CRYSTAL_PATH 来取得这个变量的默认值
1
2 ╰──➤ $ crystal env CRYSTAL_PATH
3 lib:/home/zw963/Crystal/bin/../share/crystal/src
4
可以看到,$CRYSTAL_PATH 默认仅仅包含 当前目录下的 ./lib 以及本地安装的编译器的 标准库相对路径. ~/Crystal/share/crystal/src,我们将指定的文件夹加入到到 $CRYSTAL_PATH中,使用冒号(:) 分隔即可。
例如:
1
2 ╰──➤ $ export CRYSTAL_PATH=new_folder:$(crystal env CRYSTAL_PATH)
3 lib:/home/zw963/Crystal/bin/../share/crystal/src
4 ╰──➤ $ crystal env CRYSTAL_PATH
5 new_folder:lib:/home/zw963/Crystal/bin/../share/crystal/src
6
§ require 查找策略
我们假设这个加入 $CRYSTAL_PATH 的文件夹叫做 CPATH, 当我们 require "foo" 时,会按照如下顺序查找:
- CPATH/foo.cr (1) 简单的 foo.cr
- CPATH/foo/foo.cr (2) foo 替换为 foo/foo.cr
- CPATH/foo/src/foo.cr (3) 和 (2) 类似,只不过 第一级 文件夹后面加了一个 src 文件夹。
上面的 foo 可以扩展成 a/b/c 这种形式,例如,require "foo/bar/baz", 会查找:
- CPATH/foo/bar/baz.cr
- CPATH/foo/bar/baz/baz.cr
- CPATH/foo/src/bar/baz.cr
可见仍旧满足上面的策略,只不过将 foo 替换为 foo/bar/baz 而已。
注意:require 绝对路径是不支持的。例如:require "/some/aboslote/path" Crystal 没有类似于 Ruby 中 load 方法的等价物,但是可以通过下面的 read_file 宏(macro) 来达到同样的目的
1
2 # 这个 macro 相当于把文件 path.cr 里面的内容物理粘贴到宏调用位置
3 {{ read_file("/some/alsolute/path.cr").id }}
4