跳至内容

面向 Ruby 开发者的 Crystal

虽然 Crystal 拥有类似 Ruby 的语法,但 Crystal 是一种不同的语言,而不是另一个 Ruby 实现。出于这个原因,主要是因为它是一种编译的静态类型语言,与 Ruby 相比,该语言有一些重大的差异。

Crystal 作为一种编译语言

使用 crystal 命令

如果您有一个程序 foo.cr

# Crystal
puts "Hello world"

当您执行其中一个命令时,您将获得相同的输出

$ crystal foo.cr
Hello world
$ ruby foo.cr
Hello world

看起来 crystal 解释了文件,但实际上发生的是,文件 foo.cr 首先被编译成一个临时可执行文件,然后运行该可执行文件。这种行为在开发周期中非常有用,因为您通常会编译一个文件并希望立即执行它。

如果您只想编译它,可以使用 build 命令

$ crystal build foo.cr

这将创建一个 foo 可执行文件,您可以使用 ./foo 运行它。

请注意,这将创建一个未经优化的可执行文件。要对其进行优化,请传递 --release 标志

$ crystal build foo.cr --release

在编写基准测试或测试性能时,请务必在发布模式下进行编译。

您可以通过在没有参数的情况下调用 crystal 或在没有参数的情况下使用命令调用 crystal(例如 crystal build 将列出可与该命令一起使用的所有标志)来检查其他命令和标志。或者,您可以阅读 手册

类型

Bool

truefalse 的类型为 Bool,而不是类 TrueClassFalseClass 的实例。

整数

对于 Ruby 的 Fixnum 类型,请使用 Crystal 的其中一个整数类型 Int8Int16Int32Int64UInt8UInt16UInt32UInt64

如果对 Ruby Fixnum 的任何操作超过其范围,则该值将自动转换为 Bignum。Crystal 则会在溢出时引发 OverflowError。例如

x = 127_i8 # An Int8 type
x          # => 127
x += 1     # Unhandled exception: Arithmetic overflow (OverflowError)

Crystal 的标准库提供了具有任意大小和精度的数字类型:BigDecimalBigFloatBigIntBigRational

请参阅有关 整数 的语言参考。

正则表达式

不支持全局变量 $`$'(但 $~$1$2 等仍然存在)。使用 $~.pre_match$~.post_match了解更多

简化的实例方法

在 Ruby 中,有多种方法可以完成同一件事,而在 Crystal 中可能只有一种。具体而言

Ruby 方法 Crystal 方法
Enumerable#detect Enumerable#find
Enumerable#collect Enumerable#map
Object#respond_to? Object#responds_to?
lengthsizecount size

省略的语言结构

在 Ruby 中,有一些替代结构,而 Crystal 只有一个。

  • 缺少尾随 while/until。但请注意,if 作为后缀 仍然可用
  • andor:使用 &&|| 代替,并使用适当的括号来指示优先级
  • Ruby 有 Kernel#procKernel#lambdaProc#new->,而 Crystal 使用 Proc(*T, R).new->(请参阅 这里 以供参考)。
  • 对于 require_relative "foo",使用 require "./foo"

数组没有自动 splat 以及强制最大代码块参数数量

[[1, "A"], [2, "B"]].each do |a, b|
  pp a
  pp b
end

将生成类似以下的错误消息

    in line 1: too many block arguments (given 2, expected maximum 1)

但是,省略不需要的参数是可以的(就像在 Ruby 中一样),例如

[[1, "A"], [2, "B"]].each do # no arguments
  pp 3
end

或者

def many
  yield 1, 2, 3
end

many do |x, y| # ignoring value passed in for "z" is OK
  puts x + y
end

元组有自动 splat

[{1, "A"}, {2, "B"}].each do |a, b|
  pp a
  pp b
end

将返回您期望的结果。

您还可以显式解包以获得与 Ruby 的自动 splat 相同的结果

[[1, "A"], [2, "B"]].each do |(a, b)|
  pp a
  pp b
end

以下代码也适用,但更倾向于前者。

[[1, "A"], [2, "B"]].each do |e|
  pp e[0]
  pp e[1]
end

#each 返回 nil

在 Ruby 中,.each 为许多内置集合(如 ArrayHash)返回接收器,这允许对该接收器进行方法链,但这可能会在 Crystal 中导致一些性能和代码生成问题,因此不支持该功能。或者,可以使用 .tap

Ruby

[1, 2].each { "foo" } # => [1, 2]

Crystal

[1, 2].each { "foo" }       # => nil
[1, 2].tap &.each { "foo" } # => [1, 2]

参考

反射和动态评估

省略了 Kernel#eval() 和奇怪的 Kernel#autoload()。还省略了对象和类内省方法 Object#kind_of?()Object#methodsObject#instance_methodsClass#constants

在某些情况下,可以使用 进行反射。

语义差异

单引号和双引号字符串

在 Ruby 中,字符串字面量可以使用单引号或双引号分隔。在 Ruby 中,双引号字符串会受到字面量内变量插值的影響,而单引号字符串则不会。

在 Crystal 中,字符串字面量只用双引号分隔。单引号就像 C 类语言一样充当字符字面量。与 Ruby 一样,字符串字面量中存在变量插值。

总之

X = "ho"
puts '"cute"' # Not valid in crystal, use "\"cute\"", %{"cute"}, or %("cute")
puts "Interpolate #{X}"  # works the same in Ruby and Crystal.

不支持 Ruby 或 Python 的三引号字符串字面量,但字符串字面量可以包含换行符

"""Now,
what?""" # Invalid Crystal use:
"Now,
what?"  # Valid Crystal

不过,Crystal 支持许多 百分比字符串字面量

[][]? 方法

在 Ruby 中,[] 方法通常在找不到该索引/键的元素时返回 nil。例如

# Ruby
a = [1, 2, 3]
a[10] #=> nil

h = {a: 1}
h[1] #=> nil

在 Crystal 中,这些情况下会抛出异常

# Crystal
a = [1, 2, 3]
a[10] # => raises IndexError

h = {"a" => 1}
h[1] # => raises KeyError

此更改背后的原因是,如果每个 ArrayHash 访问都可能返回 nil 作为潜在值,那么以这种方式编程将非常令人讨厌。这将不起作用

# Crystal
a = [1, 2, 3]
a[0] + a[1] # => Error: undefined method `+` for Nil

如果确实想要在找不到索引/键时获取 nil,可以使用 []? 方法

# Crystal
a = [1, 2, 3]
value = a[4]? # => return a value of type Int32 | Nil
if value
  puts "The number at index 4 is : #{value}"
else
  puts "No number at index 4"
end

[]? 只是一个普通方法,可以(也应该)为类似容器的类定义。

需要了解的另一件事是,当执行以下操作时

# Crystal
h = {1 => 2}
h[3] ||= 4

程序实际上会被转换为以下内容

# Crystal
h = {1 => 2}
h[3]? || (h[3] = 4)

也就是说,[]? 方法用于检查索引/键是否存在。

正如 [] 不返回 nil 一样,一些 ArrayHash 方法也不返回 nil 并抛出异常(如果找不到元素):firstlastshiftpop 等。对于这些方法,也提供了疑问方法来获取 nil 行为:first?last?shift?pop? 等。


约定是 obj[key] 返回一个值,或者如果 key 丢失则抛出异常(“丢失”的定义取决于 obj 的类型),而 obj[key]? 返回一个值,或者如果 key 丢失则返回 nil。

对于其他方法,则取决于情况。如果存在名为 foo 的方法以及同一个类型的另一个 foo? 方法,则意味着 foo 在某些条件下会抛出异常,而 foo? 在相同条件下会返回 nil。如果只有 foo? 变体而没有 foo,则它返回一个真值或假值(不一定是 truefalse)。

以上所有内容的示例

  • Array#[](index) 在越界时抛出异常,Array#[]?(index) 在这种情况下返回 nil。
  • Hash#[](key) 如果键不在哈希表中则抛出异常,Hash#[]?(key) 在这种情况下返回 nil。
  • Array#first 如果数组为空则抛出异常(没有“第一个”,因此“第一个”丢失),而 Array#first? 在这种情况下返回 nil。pop/pop?、shift/shift?、last/last? 也一样。
  • String#includes?(obj)Enumerable#includes?(obj)Enumerable#all?,它们都没有非疑问变体。前面的方法确实返回 true 或 false,但这并不是必要条件。

for 循环

不支持 for 循环。建议使用 Enumerable#each。如果仍然想要使用 for,可以通过宏添加它们

macro for(expr)
  {{expr.args.first.args.first}}.each do |{{expr.name.id}}|
    {{expr.args.first.block.body}}
  end
end

for i  [1, 2, 3] do # You can replace ∈ with any other word or character, just not `in`
  puts i
end
# note the trailing 'do' as block-opener!

方法

在 ruby 中,以下操作将引发参数错误

def process_data(a, b)
  # do stuff...
end

process_data(b: 2, a: "one")

这是因为,在 ruby 中,process_data(b: 2, a: "one")process_data({b: 2, a: "one"}) 的语法糖。

在 Crystal 中,编译器将把 process_data(b: 2, a: "one") 视为使用命名参数 b: 2a: "one" 调用 process_data,这与 process_data("one", 2) 相同。

属性

ruby 的 attr_accessorattr_readerattr_writer 方法被使用不同名称的宏取代

Ruby 关键字 Crystal
attr_accessor property
attr_reader getter
attr_writer setter

示例

getter :name, :bday

此外,Crystal 为可空或布尔实例变量添加了访问器宏。它们在名称中包含问号 (?)

Crystal
property?
getter?

示例

class Person
  getter? happy = true
  property? sad = true
end

p = Person.new

p.sad = false

puts p.happy?
puts p.sad?

即使这是针对布尔值的,也可以指定任何类型

class Person
  getter? feeling : String = "happy"
end

puts Person.new.feeling?
# => happy

在文档中详细了解 getter? 和/或 property?

一致的点符号

例如,Ruby 中的 File::exists? 在 Crystal 中变为 File.exists?

Crystal 关键字

Crystal 添加了一些新的关键字,这些关键字仍然可以作为方法名使用,但需要使用点显式调用:例如 self.select { |x| x > "good" }

可用关键字

abstract   do       if                nil?           select          union
alias      else     in                of             self            unless
as         elsif    include           out            sizeof          until
as?        end      instance_sizeof   pointerof      struct          verbatim
asm        ensure   is_a?             private        super           when
begin      enum     lib               protected      then            while
break      extend   macro             require        true            with
case       false    module            rescue         type            yield
class      for      next              responds_to?   typeof
def        fun      nil               return         uninitialized

私有方法

Crystal 要求每个私有方法都以 private 关键字为前缀

private def method
  42
end

从 Ruby 到 Crystal 的哈希语法

Crystal 引入了一种 Ruby 中没有的数据类型,即 NamedTuple

通常在 Ruby 中,可以使用多种语法定义哈希表

# A valid ruby hash declaration
{ 
  key1: "some value",
  some_key2: "second value"
}

# This syntax in ruby is shorthand for the hash rocket => syntax
{
  :key1 => "some value",
  :some_key2 => "second value"
}

在 Crystal 中,情况并非如此。Hash 火箭 => 语法是在 Crystal 中声明哈希表的必要条件。

但是,Ruby 中的 Hash 简写语法在 Crystal 中创建了一个 NamedTuple

# Creates a valid `Hash(Symbol, String)` in Crystal
{
  :key1      => "some value",
  :some_key2 => "second value",
}

# Creates a `NamedTuple(key1: String, some_key2: String)` in Crystal
{
  key1:      "some value",
  some_key2: "second value",
}

NamedTuple 和常规 Tuple 的大小固定,因此它们最适合在编译时已知的那些数据结构。

伪常量

Crystal 提供了一些伪常量,它们提供有关正在执行的源代码的反射数据。

在 Crystal 文档中详细了解伪常量。

Crystal Ruby 描述
__FILE__ __FILE__ 正在执行的 Crystal 文件的完整路径。
__DIR__ __dir__ 正在执行的 Crystal 文件所在的目录的完整路径。
__LINE__ __LINE__ 正在执行的 Crystal 文件中的当前行号。
__END_LINE__ - 调用块结束的行号。只能用作方法参数的默认值。

用于 Ruby Gems 的 Crystal Shards

许多流行的 Ruby gems 已在 Crystal 中移植或重写。 以下是 Ruby Gems 的等效 Crystal Shards 列表


有关 Ruby 和 Crystal 之间差异的其他问题,请访问 常见问题解答