跳到內容

給 Rubyist 的 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 會列出可用於該指令的所有標誌)來檢查其他指令和標誌。或者,您可以閱讀手冊

型別

布林值

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"

陣列無自動展開且強制執行最大區塊引數數量

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

元組有自動展開

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

將會傳回您預期的結果。

您也可以明確解壓縮以獲得與 Ruby 的自動展開相同的結果

[[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 gem 已移植或在 Crystal 中重寫。 這裡有一些等效於 Ruby Gems 的 Crystal Shards


有關 Ruby 和 Crystal 之間差異的其他問題,請造訪 FAQ