変わらない値は定数にしよう

学習の目標

本章では、以下の内容を学習します。

  • 定数の概念と基本的な使い方を理解する
  • 定数の命名規則とベストプラクティスを習得する
  • 定数とクラスの関係性について学ぶ
  • 定数の変更に関する警告と制限を理解する
  • freezeメソッドを使って値を不変にする方法を習得する

はじめに

これまでプログラムを書く中で、変数を使って値を保存し、必要に応じて変更する方法を学んできました。しかし、プログラミングをしていると「一度設定したら変更しない値」を扱いたい場合があります。

例えば、消費税率は10%と決まっていて頻繁に変わるものではありません。また、円周率(3.14...)や重力加速度(9.8m/s²)のような自然界の定数も変わりません。このような「変わらない値」をプログラム内で管理するために、Rubyでは定数という機能が用意されています。

定数を使うことで、プログラムの意図が明確になり、バグの発生を減らすことができます。さらに、もし値を変更する必要が生じた場合も、一箇所を修正するだけで済むため、メンテナンス性が向上します。

それでは、実際にRubyでの定数の使い方を見ていきましょう。

定数の基本

定数の定義と使い方

まずは、定数の基本的な使い方を学びましょう。実際にコードを書きながら理解を深めていきます。VS Codeでconst.rbというファイルを作成し、商品の価格を計算するクラスを実装してみましょう。

初めて定数を使う方のために、一つの具体例として消費税計算のプログラムを作ります。商品の価格に消費税を加算して、最終的な支払金額を計算する例です。

class Product
TAX_RATE = 0.1
def total_price(price)
price * (1 + TAX_RATE)
end
end
product = Product.new
puts product.total_price(1000) # 1100.0を表示(1000 * 1.1)

上記のコードでは、TAX_RATEという名前で消費税率を表す定数を定義しています。この定数の値は0.1であり、10%の消費税率を表しています。

total_priceメソッドは引数として商品の税抜価格(price)を受け取り、定数TAX_RATEを使って税込み価格を計算しています。計算式は「価格 × (1 + 税率)」となっており、例えば1000円の商品であれば、1000 × (1 + 0.1) = 1100円となります。

このプログラムを実行すると、1000円の商品に対して消費税10%が加算された1100円が表示されます。このように、定数を使うことでプログラム内で一貫した値を使用できます。

定数の命名規則

Rubyでの定数の命名には、特定のルールと慣習があります。これらのルールを守ることで、他の開発者があなたのコードを読んだときに、どの変数が定数なのかを一目で理解できるようになります。

Rubyにおける定数の命名規則は以下の通りです。

  • 定数は必ず大文字で始まる必要があります
  • 慣習的にすべて大文字で記述します
  • 複数の単語からなる場合は、アンダースコア(_)でつなぐのが一般的です

例えば、次のような定数名が適切です。

TAX_RATE = 0.1 # 消費税率
MAX_LOGIN_ATTEMPTS = 3 # 最大ログイン試行回数
DEFAULT_TIMEOUT = 30 # デフォルトのタイムアウト時間
DATABASE_CONNECTION_STRING = "..." # データベース接続文字列

このような命名規則を守ることで、コードを読む人に「これは変更されない値」だということが一目でわかるようになります。また、コード内での検索も容易になります。

定数を使うメリット

定数を使うことにはいくつかの重要なメリットがあります。具体的に見ていきましょう。

  1. 値の意味が明確になる

単に0.1という数値をコード内で直接使用した場合、それが何を表しているのかが分かりにくいです。しかし、TAX_RATEという名前を付けることで「これは消費税率を表している」と一目で理解できます。

  1. 一箇所で管理できる

例えば、消費税率が8%から10%に変更になった場合を考えてみましょう。定数を使わずにプログラム内のあちこちで0.08という値を直接使っていた場合、すべての箇所を見つけて変更する必要があります。これは非常に手間がかかり、変更漏れの危険性もあります。

しかし、定数を使っていれば、定数の定義部分を一箇所変更するだけで、プログラム全体に変更が反映されます。

# 消費税率が変更された場合、この一箇所を変更するだけでよい
TAX_RATE = 0.1 # 8%から10%に変更
  1. 誤った変更を防ぐ

定数は「変更すべきでない値」であることが名前から分かるため、誤って変更してしまうリスクが減ります。また、Rubyでは定数を変更しようとすると警告が表示されるため、意図しない変更に気づきやすくなります。

  1. コードの可読性と保守性の向上

「マジックナンバー」(説明のない数値リテラル)を避け、意味のある名前の定数を使うことで、コードの可読性が向上します。また、将来的な変更や拡張にも対応しやすくなります。

例えば、以下の2つのコードを比較してみましょう。

# 定数を使わない場合
def total_price(price)
price * 1.1 # この1.1が何を意味するのか分かりにくい
end
# 定数を使う場合
TAX_RATE = 0.1
def total_price(price)
price * (1 + TAX_RATE) # 消費税を加算していることが明確
end

定数を使ったコードの方が、何をしているのかが明確に伝わります。

定数のスコープとアクセス方法

定数は、どこで定義され、どこからアクセスできるのかという「スコープ」の概念が重要です。ここでは、クラス内での定数の定義と、クラス外からのアクセス方法について詳しく見ていきましょう。

クラス内での定数

先ほどの例では、クラス内で定数を定義しました。クラス内で定義された定数は、そのクラス内のメソッドから直接参照できます。これは非常に便利で、クラスに関連する固定値を管理するのに適しています。

次のコード例を見てみましょう。

class Product
TAX_RATE = 0.1
def total_price(price)
# クラス内のメソッドから直接TAX_RATEにアクセスできる
price * (1 + TAX_RATE)
end
def discount_price(price, discount_rate)
# 同じクラス内の別のメソッドからも同じようにアクセス可能
discounted = price * (1 - discount_rate)
total_price(discounted) # 割引後の価格に税率を適用
end
end

この例では、Productクラス内で定義されたTAX_RATE定数が、同じクラス内の複数のメソッドから参照されています。クラス内の定数は、そのクラスの「属性」のような役割を果たし、クラス全体で共有される値として機能します。

クラス外からの定数へのアクセス

クラス内で定義された定数にクラス外からアクセスするには、::演算子(スコープ解決演算子)を使います。この演算子を使うことで、特定のクラスやモジュール内で定義された定数を参照できます。

具体的な例を見てみましょう。

class Settings
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
end
# クラス名::定数名 の形式でアクセス
puts Settings::DEFAULT_TIMEOUT # 30を表示
puts Settings::MAX_RETRIES # 3を表示
# クラスのインスタンス経由ではアクセスできない(注意)
settings = Settings.new
# puts settings.DEFAULT_TIMEOUT # これはエラーになる

この例では、Settingsクラス内で定義されたDEFAULT_TIMEOUTMAX_RETRIESという2つの定数に、クラス外からアクセスしています。Settings::DEFAULT_TIMEOUTのように、クラス名の後に::を続け、さらに定数名を指定することでアクセスできます。

注意点として、定数はクラスのインスタンス経由ではアクセスできません。つまり、settings.DEFAULT_TIMEOUTのようなアクセス方法は無効です。定数はクラスに属するものであり、インスタンスに属するものではないためです。

定数の変更と警告

Rubyの定数には少し特殊な性質があります。それは「技術的には変更可能である」ということです。しかし、定数の値を変更しようとすると警告が表示されます。この警告は、定数を変更することは本来の定数の概念に反するため、開発者に注意を促すものです。

実際にどのように警告が表示されるか、例を通して見てみましょう。

class Settings
DEFAULT_TIMEOUT = 30
end
puts Settings::DEFAULT_TIMEOUT # 30を表示
# 定数に新しい値を代入してみる
Settings::DEFAULT_TIMEOUT = 60 # 警告が表示される
puts Settings::DEFAULT_TIMEOUT # それでも値は変更され、60が表示される

このプログラムを実行すると、以下のような警告メッセージが表示されます。

warning: already initialized constant Settings::DEFAULT_TIMEOUT

この警告は「Settings::DEFAULT_TIMEOUTという定数は既に初期化されています」という意味で、定数を再定義することが通常は避けるべき操作であることを教えてくれています。

しかし、警告は表示されるものの、プログラムは停止せずに実行が続けられ、定数の値は実際に変更されます。つまり、Rubyにおける「定数」は厳密には不変ではなく、変更する際に警告が出るだけなのです。

この性質は他のプログラミング言語と異なる点なので、注意が必要です。一般的なプログラミングの慣習として、定数は変更すべきではありません。もし値を変更する必要がある場合は、代わりに変数を使用することを検討しましょう。

定数の凍結(freeze)

定数の値を完全に不変にしたい場合は、Rubyのfreezeメソッドを使用することができます。freezeメソッドは、オブジェクトを「凍結」して変更できないようにするものです。

これを定数に適用することで、より厳密に値の変更を防ぐことができます。特に、配列やハッシュのような複合データ型の定数を使う場合に重要です。

基本的なfreezeの使い方

まずは基本的なfreezeの使い方を見てみましょう。

FIXED_ARRAY = [1, 2, 3].freeze
# 凍結された配列に要素を追加しようとするとエラーになる
begin
FIXED_ARRAY << 4 # 実行するとエラーになる
rescue => e
puts "エラーが発生しました: #{e.message}"
end

このコードでは、FIXED_ARRAYという定数に[1, 2, 3]という配列を代入する際に、その配列をfreezeメソッドで凍結しています。そのため、この配列に新しい要素を追加しようとすると、「can't modify frozen Array」(凍結された配列は変更できません)というエラーが発生します。

これにより、定数に代入されているオブジェクトが誤って変更されるのを防ぐことができます。

ハッシュの凍結

ハッシュを定数として使う場合も、同様にfreezeメソッドを使って変更を防止できます。設定情報のような構造化されたデータを定数として管理する場合に特に役立ちます。

class Configuration
SETTINGS = {
timeout: 30,
retries: 3,
host: 'localhost'
}.freeze
end
# 凍結されたハッシュの値を変更しようとするとエラーになる
begin
Configuration::SETTINGS[:timeout] = 60 # 実行するとエラーになる
rescue => e
puts "エラーが発生しました: #{e.message}"
end

この例では、Configurationクラス内でSETTINGSという定数にハッシュを代入し、そのハッシュをfreezeメソッドで凍結しています。そのため、このハッシュの値を変更しようとすると、「can't modify frozen Hash」(凍結されたハッシュは変更できません)というエラーが発生します。

このように、freezeメソッドを使うことで、定数の内容が誤って変更されるのを効果的に防ぐことができます。

ネストした構造の凍結に関する注意点

freezeメソッドには一つ重要な注意点があります。それは、freezeが「浅い凍結」しか行わないということです。つまり、複合データ型(配列やハッシュなど)の最上位のオブジェクトだけが凍結され、その中に含まれるオブジェクトは凍結されません。

これを理解するために、次の例を見てみましょう。

NESTED = {
outer: {
inner: 'value'
}
}.freeze
# 最上位のハッシュは変更できない
begin
NESTED[:new_key] = 'new value' # エラーになる
rescue => e
puts "最上位の変更: #{e.message}"
end
# 内部のハッシュは変更できる
begin
NESTED[:outer][:inner] = 'new value' # これは成功する
puts "内部の値が変更されました: #{NESTED[:outer][:inner]}"
rescue => e
puts "内部の変更: #{e.message}"
end

この例では、NESTEDという定数に、内部にハッシュを含むハッシュを代入し、freezeメソッドで凍結しています。その結果、最上位のハッシュに新しいキーを追加しようとするとエラーになりますが、内部のハッシュの値を変更することは可能です。

これは、freezeメソッドが浅い凍結しか行わないためです。内部のオブジェクトまで完全に凍結したい場合は、再帰的にfreezeメソッドを適用する必要があります。

まとめ

この章では、Rubyの定数について学びました。以下の内容を理解できたことと思います。

  • 定数は大文字で始まり、慣習的にすべて大文字で表される
  • 定数はプログラム内の固定値を分かりやすく管理するために使われる
  • クラス内で定義した定数は::演算子でアクセスできる
  • 定数は技術的には変更可能だが、変更すると警告が表示される
  • freezeメソッドを使うことで、値を不変にして意図しない変更を防げる

定数を適切に使うことで、コードの意図が明確になり、変更に強いプログラムを書くことができます。特に設定値やプログラム全体で共有する固定値は、定数として管理することをお勧めします。

次の章では、より実践的なRubyプログラミングの技術について学んでいきます。

このセクションは有料サブスクリプションへの登録、またはログインが必要です。完全なコンテンツにアクセスするには、料金ページ(/pricing)をご覧ください。購入済みの場合は、ログインしてください。

Starterプランでより詳しく学習

この先のコンテンツを読むにはStarterプラン以上が必要です。より詳細な解説、実践的なサンプルコード、演習問題にアクセスして学習を深めましょう。

作成者:とまだ
Previous
selfを使ってコードをスッキリさせよう