Processingで学ぶ 実践的プログラミング専門課程

第8回変数の使える範囲を限定する スコープ

導入

構造化プログラミングの最終回として、スコープについて話します。

かつて、スコープという概念が一般的でなかった頃、変数の取り扱いは大変面倒でした。ある場所で作った変数と同じ名前の変数が別のところで使われると、意図しない場合に値が変更されるのです。ソースコードが大きくなるにつれ、ソフトウエア開発に参加する人数が増すにつれ、変数の管理問題は指数関数的に難易度が高くなります。変数の管理のために帳簿やデータベース、管理の担当部署が必要になるほどでした。

この問題を上手に解決するためにプログラミング言語側に設けられた仕組みがスコープです。Processingなどのオブジェクト指向言語では、クラスやメソッドもスコープを適用できます。スコープを意識し、上手に区別してコードを作成するようにしましょう。

展開

スコープとは

スコープとは、変数やメソッド、クラスなどが使える範囲のことです(⁠⁠見える範囲」とも言います⁠⁠。コードはできる限りシンプルな方が良いため、ある箇所だけで必要なものは他所から使えないようにするべきなのです。スコープの仕組みの無い言語では、プログラマが神経を使って注意深くコードを書く必要があります。スコープの仕組みは、プログラマをストレスから解放してくれるのです。

ここでは変数のスコープに話を絞ります。メソッドやクラスについても同様に考え、極力狭いスコープに収まるようにしましょう。

スコープを限定する方法

スコープを限定する最も基本的な方法は、ブレース(波括弧)で囲むことです。

BasicScope.pde。スコープを限定する方法の基本
int x = 5;

{
  int y = 3;
  println("(x,y) = ("+x+","+y+")");
}

//スコープ外なのでエラーになります
//println("(x,y) = ("+x+","+y+")");

コメントアウトされているprintln文をアンコメントアウトすると、スコープ外で変数yを使用するのでエラーになります。

次のようにメッセージエリアへyが見えません」と表示されます。

The field Component.y is not visible

[作業] sketch BasicScope.pdeを自分の手で入力して実行しましょう。コメントアウトしている行をアンコメントアウトして、エラーを起こしてみましょう。変数xもブレースの中に移動してエラーを発生させましょう。

ブレースで囲まれたコードブロック内で宣言された変数は、そのコードブロックの中でしか参照できません。スコープの仕組みはこのように変数の有効範囲を限定できます。

コードブロックを切り分けていれば、同じ名前の変数を別の型や値で使用することができます。スコープを切り分けなければ、同じ名前の変数名を使わずに、無理に説明的な名前で区別をすることになります。不必要に長い名前付けは避けるべきです。コードを読みにくくします。

明確にコードブロックを切り分けていれば、また、双方のブロックがネストしていない状況ならば、それぞれのコードブロック内で同名の変数を宣言すれば良いでしょう。

SplitScope.pde。コードブロックが別ならば同じ名前の変数が宣言できる
int x = 5;

{
  int y = 3;
  println("(x,y) = ("+x+","+y+")");
}

{
  float y = 6;
  println("(x,y) = ("+x+","+y+")");
}

次のようにネストしたコードブロックでは、同名の変数を宣言できません。

NestedScope.pde。内側のスコープで、外側のスコープと同じ変数は使えない
int x = 5;

{
  int y = 3;
  println("(x,y) = ("+x+","+y+")");
  {
    float y = 6;
    println("(x,y) = ("+x+","+y+")");
  }
}

スコープを限定して変数を使っている例の代表は、forループのカウンタ変数です。次のサンプルコード(GSWPのExample 04-10)をご覧ください。

GSWPのExample 04-10
// Example 04-10 from "Getting Started with Processing" 
// by Reas & Fry. O'Reilly / Make 2010

size(480, 120);
background(0);
smooth();
noStroke();
for (int y = 0; y <= height; y += 40) {
  for (int x = 0; x <= width; x += 40) {
    fill(255, 140);
    ellipse(x, y, 40, 40);
  }
}

2重forループを使ってディスプレイウインドウを円で埋め尽くします。xyは円の中心座標のためだけの変数ですので他所で必要ありません。このコードのようにxyを宣言すると、外側のforのコードブロック内ではyのみ有効となり、内側のforのコードブロックの中ではxy両方が有効となります。

つまり、次のように変数を参照するコードを追加するとエラーになります。

EX04_10_2.pde。エラーを起こすGSWPのExample 04-10
size(480, 120);
background(0);
smooth();
noStroke();
for (int y = 0; y <= height; y += 40) {
  
  print("(x,y) = ("+x+","+y+")"); // エラー!xが使えません。

  for (int x = 0; x <= width; x += 40) {
    fill(255, 140);
    ellipse(x, y, 40, 40);
  }
}

print("(x,y) = ("+x+","+y+")"); // エラー!xもyも使えません。

これをwhile文で書き換えると状況が少し変わります。

EX04_10_WithWhile.pde。Example 04-10をwhile文で書き換えた
size(480, 120);
background(0);
smooth();
noStroke();
// 外側のループ
int y = 0;
while(y <= height){
  // 内側のループ
  int x = 0;
  while(x <= width){
    fill(255, 140);
    ellipse(x, y, 40, 40);
    x += 40;
  }
  y += 40;
}

Example 04-10と同じ動作をさせるためには、変数xのスコープを外側のwhile文のコードブロック内にしなければなりません。同様に、変数yのスコープはこのスケッチ全体となります。for文の利点は、ループの中だけで必要なカウンタ変数のスコープを閉じ込めることができることでしょう。

[作業] EX04_10_WithWhile.pdeを変更して、変数xのスコープも変数yと同じにしましょう。それでsketchの動きに変化があるでしょうか。エラーや問題が発生するでしょうか。エラーや問題が起きないとしたら、この変更の問題点や利点を検討してみましょう。

スコープを限定しない方法

ある範囲のコードブロック全体で参照できる変数を、そのスコープの範囲のグローバル変数といいます。

CalcHookesLaw.pde。グローバル変数の例
double k = 5; 

void setup(){
  double x = 3;
  println("k      = " + k );
  println("x      = " + x );
  println("F      = " + hookesLaw(x) );
}

double hookesLaw(double l){
   return k * l;
}

sketch CalcHookesLaw.pdeのコードの中で、ばね定数を表すdouble型の変数kは、このソースコードのすべての場所から参照できるグローバル変数です。

関数に引数として渡さずに使えますから、関数の呼出しがシンプルになります。ただし、注意が必要です。

注意その1

物理定数など変更の可能性も必要性もない値で、なおかつ変数名が他とダブりようがないのであればグローバル変数として許容できます。しかし、finalで宣言されていませんから、ソースコードのどこででも値の変更が可能なのです。すると、思いも寄らぬところで値を変更してしまい、それに気付かず誤った値で計算をしてしまうという危険性があるのです。

グローバル変数はなるべく使わない、という勧めがされるのはこのためです。どうしても必要ならば、final宣言し値が変更できないようにすべきです。

注意その2

また、例えば、先ほどのsketchのばね定数は輪ゴムを使った場合のばね定数だったとします。しかし、ゴムを強力な結束ゴムバンドに変更したとします。すると、先ほどのコードでは困ってしまいます。輪ゴムの場合と結束ゴムバンドの場合と別のバネ定数を用意した上で、結束ゴムバンドのための別のメソッドを作らなくてはなりません。

CalcHookesLaw2.pde。バネ定数が異なる場合のメソッドを別に作る
double k  = 5; //輪ゴム
double k2 = 2; //結束ゴムバンド

void setup(){
  double x = 3;
  println("k      = " + k );
  println("k2     = " + k2 );
  println("x      = " + x );
  println("F1     = " + hookesLaw(x) );
  println("F2     = " + hookesLaw2(x) );
}

double hookesLaw(double l){
   return k * l;
}

double hookesLaw2(double l){
   return k2 * l;
}

こんなコードは悪夢です。

こうなることは容易に推測がつきますから、最初からメソッドhookesLawの引数はばね定数とばねの伸びを2つとも持つべきだったのです。

CalcHookesLaw3.pde。関数呼出しがシンプルになった
double k1 = 5; //輪ゴム
double k2 = 2; //結束ゴムバンド

void setup(){
  double x = 3;
  println("k1     = " + k1 );
  println("k2     = " + k2 );
  println("x      = " + x );
  println("F1     = " + hookesLaw(k1,x) );
  println("F2     = " + hookesLaw(k2,x) );
}

double hookesLaw(double k, double l){
   return k * l;
}

こうしてばね定数を引数で渡す仕組みにするならば、もはやばね定数をグローバル変数にする必要がなくなりました。次のように書き換えるべきでしょう。

CalcHookesLaw4.pde。さらにコードを整理
void setup(){
  double k1 = 5; //輪ゴム
  double k2 = 2; //結束ゴムバンド
  double x  = 3;
  println("k1     = " + k1 );
  println("k2     = " + k2 );
  println("x      = " + x );
  println("F1     = " + hookesLaw(k1,x) );
  println("F2     = " + hookesLaw(k2,x) );
}

double hookesLaw(double k, double l){
   return k * l;
}

グローバル変数はなるべく使わずに済ませたいものです。

スコープを意識せず参照したい場合

変数は基本的に使用するクラスのオブジェクト内部で閉じたスコープにすべきです。これをインスタンス・スコープと呼びます。例外的に、物理定数など一般的に良く知られており不変な定数値を持つ変数は、クラス外に公開してあると便利です。

MathPI.pde。スコープを意識せずに使えるPI定数
//Java風に書くとこう。もちろんProcessingで動きます。
println("PI = " + Math.PI);

Processingでは、次のようにMathクラスを省略して書けます。

PI.pde。スコープを意識せずに使えるPI定数、Processingではこう書ける
println("PI = " + PI);

Java言語ではMathクラスのスタティックフィールドPIをクラス名から指定して使用するのが一般的ですが、Processingではその手間を省いています。この定数フィールドPIがグローバルなスコープを持つ変数(あるいはフィールド)の代表例です。

自分で作成したコードにおいても、考えうる限り変更の可能性の無い値を持つフィールドを、このようにスコープを限定せず公開する手があります。ただし、よほどの理由が無い限りやめましょう。万が一、フィールドの値が変更になった場合、そのフィールドを利用するすべてのコードに影響を与えるからです。

私はソフトの名称やバージョン番号などを定数クラスのstaticフィールドに持たせておき使っています。こうすると、同一パッケージ内ではグローバル定数として参照できますから、マジックナンバーをコード内にばらまかずに済みますし、変更は定数クラスの一カ所だけで済みます。コンパイルすればすべて完了、便利です。現在のコードのバージョンナンバーは、現在使用しているすべてのコードで統一しておきたい、つまり影響を及ぼしてほしい場合だからです。

AppConstantsTest.pde。グローバル変数ではなく定数クラスを使おう
void setup(){
  println("This application is <" + AppConstants.APP_NAME + ">");
  println("Version " + AppConstants.VERSION);
}

class AppConstants {
  static final String APP_NAME = "Macky!";
  static final String VERSION = "1.0.2";
}

演習

演習1(難易度:middle)

GSWPのサンプルコードEx_05_09.pdeを読んで、変数x, y, px, pydrawメソッドのローカル変数にしなかった理由を説明してください。

GSWPのサンプルコードEx_05_09.pde
// Example 05-09 from "Getting Started with Processing" 
// by Reas & Fry. O'Reilly / Make 2010

float x;
float y;
float px;
float py;
float easing = 0.05;

void setup() {
  size(480, 120);
  smooth();
  stroke(0, 102);
}

void draw() {
  float targetX = mouseX;
  x += (targetX - x) * easing;
  float targetY = mouseY;
  y += (targetY - y) * easing;
  float weight = dist(x, y, px, py);
  strokeWeight(weight);
  line(x, y, px, py);
  py = y;
  px = x;
}

演習2(難易度:middle)

sketch Ex_08_08.pdeは地上での体重を火星での体重に換算するものです。地球上での体重を火星での体重に換算する定数0.38は、火星の重力加速度(3.71[m/s2])を地球の重力加速度(9.8[m/s2])で割ったものです。この定数は後で意味が分からなくて困ってしまう可能性のあるマジックナンバーですので、定数クラスを作成して定数として参照できるようにすると良いでしょう。この定数クラスGravitationalAccelerationValuesを作成して、定数クラスのフィールドを参照して計算するコードに変更してください。ファイル名はEx_08_08_WithConstClass.pdeとしてください。

地球と火星の重力の違いを計算するsketch Ex_08_08.pde
// Example 08-08 from "Getting Started with Processing" 
// by Reas & Fry. O'Reilly / Make 2010

void setup() {
  float yourWeight = 132;
  float marsWeight = calculateMars(yourWeight);
  println(marsWeight);
}

float calculateMars(float w) {
  float newWeight = w * 0.38;
  return newWeight;
}

まとめ

  • スコープの意味と活用方法を学習しました。

学習の確認

それぞれの項目で、Aを選択できなければ、本文や演習にもう一度取り組みましょう。

  1. スコープの意味が理解できましたか?
    1. 理解できた。自分のコーディングに活かす決心をした。
    2. 理解できた。しかし、自分のコーディングに活かす必要を感じない。
    3. 理解できない。
  2. グローバル変数を避けるべき理由を理解できましたか?
    1. 理解できた。
    2. 理解はできるが必要を感じない。
    3. 理解できない。

参考文献

  • 『Javaルールブック ~読みやすく効率的なコードの原則』⁠電通国際情報サービス 監修、大谷晋平、米林正明、片山暁雄、横田健彦 著、技術評論社
    • Javaのコードを書く人は必携。ほぼ見開きでコンパクトながら意を尽くした解説がある。
  • 『イラストで読むプログラミング入門』⁠ダニエル・アップルマン著, インプレス社
    • プログラミングという技術をイラストレーションで分かりやすく解説している。例をふんだんに用いておりなるほどと思わされる。

演習解答

  1. x, y, px, pydrawメソッドのローカル変数にすると、マウスポインタの位置で各変数の値を更新するのですが、その回のdrawメソッドの呼出しが完了すると変数の値は破棄されます。前回drawメソッドを実行した結果を保持するためには、変数のスコープをdrawメソッドの外にする必要があるのです。

    高速で繰り返し実行されるdrawメソッド内でたくさんの変数を宣言すると、その「変数を宣言するための仕事にかかる時間」がコンピュータの負担になることが考えられます。実行速度をわずかでも向上したい場合には、ローカル変数を外に出すことも検討しましょう。しかし、そのようなチューニングはコードの読みやすさが低下しますから、なるべく選択するべきではありません。値の保持と言う問題が無ければ、変数の数はわずか4つ5つですから、すべてローカル変数にしても良いでしょう。

  2. Ex_08_08_WithConstClass.pde

おすすめ記事

記事・ニュース一覧