💎

【Crystalと○○】Crystalと定数

2021/04/19に公開

📖【Crystalと○○】コンテンツ一覧

Crytalでは値を保持する枠組みとして、ローカル変数、インスタンス変数、クラス変数と定数を利用できます、今回はこの中から定数の使い方や特徴を説明することにしましょう。

定数はトップレベルか、もしくは何らかの型定義の直下で初期化することで使用可能になります。

変数と同じように情報を保持できますが、変数とは異なり、例え現在の値と同じ型であっても初期化後に別の値を再代入することはできません。

定数の命名規則

定数名は慣習的に全て大文字のスネークケースで記載します。

コンパイラ的には、大文字で始まってさえいれば定数として認識してくれますが、大文字始まりのキャメルケースは型名で使用することが多く、これらと明確に区別するためにも原則として定数名には小文字を使用しないことをお勧めします。

定数の型と初期化

再代入が不可能な定数には定義時に必ず値を代入して初期化する必要があり、定数の型はその際代入された値の型になります。

定数はコンパイル時点で型を特定しなければならないため、初期化時に代入した値(式)から方が識別できる必要があります。どのような初期化が型を特定可能かについては、インスタンス変数の代入による型の推定を参考にしてください。例えば、クラスメソッドによる定数の初期化は可能ですが、インスタンスメソッドによる初期化はコンパイルエラーになりますので注意が必要です。

クラスメソッドで初期化
class SomeClass
  def self.c_method : String
    "String"
  end
end

CONST = SomeClass.c_method
typeof(CONST) #=> String
インスタンスメソッドで初期化はできない
class SomeClass
  def i_method : String
    "String"
  end
end

some_class = SomeClass.new
CONST2 = some_class.i_method
#=> Error: undefined local variable or method 'some_class' for top-level

なお、定数には : 型名 による型の制約も指定できません。

CONST : String = "定数"
#=> Error: unexpected token: :

定数のスコープ

トップレベルで初期化された定数は、いわゆるグローバルスコープを持っており、たとえ別のソースファイルからであってもプログラム全体からアクセス可能です。一方、型定義内で初期化された定数のスコープは基本的にその型の内部となり、その型の外側から直接アクセスするためには、定数を定義した型を名前空間として使用する必要があります。

class SomeClass
  SOME_CLASS_CONST = "a"
end

SOME_CLASS_CONST
# Error: undefined constant SOME_CLASS_CONST

SomeClass::SOME_CLASS_CONST
#=> "a"

定数の更新

一度初期化された定数は、別の値を再代入することはできません。

CONST = 1

CONST = 2
#=> Error: already initialized constant CONST

ただし、定数の値が mutable な(状態変更可能な)オブジェクトだった場合、再代入しない形での値の更新はできてしまいます。

配列やハッシュなどのコレクションは、こうした mutable なオブジェクトの代表例です。

ARRAY = [0, 1, 2]

ARRAY << 3
#=> [0, 1, 2, 3]

ぱっと見は便利そうですが、定数は名前空間越しに外部からアクセスできてしまうため、このような定数は作成者の意図しない値で更新される可能性があります。

再代入なのか状態変更なのかに関わらず、型に固有な状態に外部からの更新を許す場合はクラス変数にセッタメソッドを被せて提供することをお勧めします。

今回のまとめ

  • 定数名は大文字のスネークケースを使用する
  • 再代入はできないが、再代入を伴わない値の更新は可能
  • とはいえ、状態変更を前提にするなら定数ではない実装を検討しては?

Discussion