2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Crystal言語の変化する型/ is_a? nil?の使い方

Last updated at Posted at 2021-05-08

Crystal 言語の型と制御構文

Rubyに似た構文で記述可能で、実行速度と安全性を兼ね備えたCrystal言語。
この言語には、安全性故にRubyではあまり意識しない"型"という概念が重要になります。
ここでは、そんなCrystal言語の制御構文による型の変化について記載します。

型の変化とは

型の変化とはCrystal言語内でユニオン型※と呼ばれる変数の型に発生します。
ユニオン型がかかわる制御構文によって、変数の型が変化していくことを指しています。
※ここではバーチャル型も併せてユニオン型と記載しています。

ユニオン型とは

ユニオン型について具体的な例を見てみましょう。
以下の例は受け取った文字が文字列の何文字目に存在しているかを返すString.indexメソッドの公式リファレンスの使用例です。

"Hello, World".index('o')    # => 4
"Hello, World".index('Z')    # => nil

このメソッドは文字列の中に引数の文字が含まれていればインデックスを返し、
存在しない場合はnilを返します。

この時、このメソッドが返す値はInt32かNil型の値となります。
ではこのメソッドからの返り値を入れる変数の型はどうなるでしょうか。
ここで登場するのがユニオン型です。

index : Int32 | Nil # ※

※これはInt32?とも書くことができます。型名 + ? は 型 | Nil の糖衣構文です。

ユニオン型は複数の型を表現することができるため非常に便利ですが、
その反面変数の中に実際に入っている値の型がわかりません。

index = str.index('/')
index #この時点で変数indexは変数strの内容によってInt32かNil型のどちらかの値が入っている。

上記の例では2行目の時点で変数indexにはInt32型の整数値かnilのどちらかが入っています。

では変数indexを実際に使用しようとしたとき、どのように扱うべきでしょうか。
例えば、文字列strのなかの'/'の次の文字を確認したいときは以下のようなコードになると思います。

index = str.index('/')
p str[index + 1] # => コンパイルエラー

もちろん上記はコンパイルエラーとなります。
なぜなら変数indexは足し算ができるInt32型ではなくInt32とNilのユニオン型だからです。
この時点で変数indexはnilの可能性もあるのです。

ではこの変数indexは今後のコードで計算やメソッド呼び出しが行えないのでしょうか。
そんなことはありません。ここで登場するのが型の変化です。
crystalコンパイラはif文の条件内に型を制限する記述があるとき、そのif文内では変数の型を条件に沿った型に変化させてくれます。

if(<変数に対して型を制限する条件式>)
  #条件式の変数は制限された型となる
else
  #条件式の変数は制限された型以外となる
end

nil?

実際に型の変化を見てみましょう。

p typeof(index) # => (Int32 | Nil)
if(index.nil?)
  p typeof(index) #1 => Nil
else
  p typeof(index) #2 => Int32
end

上記のif文の条件分岐にnil?メソッドを使用しました。
このメソッドはインスタンスがnilの時にtrueを返します。

すなわち、この条件式を評価したときifブロック内#1へ到達するのは変数indexがnilの時のみとなります。
よってコンパイラはこのブロック内では変数indexをユニオン型ではなくNil型として扱います。

また、elseブロックに到達するのは変数indexがnilでないときのみです。
そのため、#2の時には変数indexはInt32型として扱われます。

上記の例は以下のように書きなおすこともできます。

p typeof(index)
if(index)
  p typeof(index) #2 => Int32
else
  p typeof(index) #1 => Nil
end

crystal言語ではnilを評価するとfalseとして評価されるため、
#2に到達できるのはnil以外となるためです。

is_a?

上記の例ではNilかそれ以外を分類できましたが、逆にNil以外の型が複数ある場合には型の制限ができません。
もし以下のような例の場合、変数valueの型を制限するためにはどうするべきでしょうか。

def return_int_or_str
  #Int32かString型の値を返す関数
  value = Random.rand(100)
  return value % 3 == 0 ? "fizz" : value
end
value : Int32 | String
value = return_int_or_str

この時のベストプラクティスは引数の型のインスタンスかどうかを調べるis_a?メソッドを使用することでしょう。

p typeof(value) # => (Int32 | String)
if value.is_a?(Int32)
  p typeof(value) #1 => Int32
else
  p typeof(value) #2 => String
end

is_a?メソッドはインスタンスの型が引数で受け取った型、もしくはサブクラスの型のときにtrueを返します。
#1に到達するのは変数valueがInt32型の時のみになるため、#1ではvalueはInt32型として利用することができます。
また、#2に到達するのはvalueがInt32型以外の為、上記の例ではString型となります。

型変化の例外

ここまで記載してきた型変化についてですが、クラス変数とインスタンス変数については例外です。
クラス変数とインスタンス変数については条件式を通過しても型は一定のままとなります。
クラス変数やインスタンス変数の型を変化させるためには一度メソッド内のローカル変数へ代入を行う必要があります。

class Sample
  @value : Int32?
  def value
    if(@value)
      typeof(@value) # => (Int32 | Nil)
      #インスタンス変数、クラス変数は型制限が機能しないため、ここでもユニオン型として扱われる。
    end
    value = @value
    if(value)
      typeof(value) # => Int32
    end
  end
end
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?