【オブジェクト指向っぽく無く理解する】 new演算子がやってることを噛み砕く

はじめに

 見方を変えれば理解度も違うと思っての一言アドバイスです。わたしなりにこう解釈したらピンと来た!という事を述べていくシリーズです。


 今回は、なんとなくnew演算子はインスタンス化で使うもの、程度の理解でいたため そこそこハマる目にあったので真面目に整理したいと思います。

 尚、かなりこじつけ的な解釈をしていますのでオブジェクト指向色がちょっと多めです。


今更なインスタンス化の説明

 まずは引用。

 インスタンスは『オブジェクト指向言語においては、多くの場合クラスと呼ばれるものを元に作成したオブジェクトの実体を指す。』


 『コンストラクタは、オブジェクト指向のプログラミング言語で新たなオブジェクトを生成する際に呼び出されて内容の初期化などを行なう関数あるいはメソッドのことである。』『初期化用のコードのみを記述するのが普通であり、戻り値指定のない関数であるかのような記法となっている。』


 『newまたはNewは、C++を始めとしたオブジェクト指向プログラミング言語において、インスタンスを作成する演算子である。』


 さて、これらのコンストラクタとインスタンスの説明は入門書でよく見かける記述です。初学者には決まり事として丸暗記を強いられる部分ですが、言われる通りにやっていればいい部分なのでここまでは問題ありません。


 ところが、new演算子についての解説はこう続きます。

new のキーワードは以下のことを行います。(-MDN- より)

1.空の JavaScript オブジェクトを生成する。
2.このオブジェクトを他のオブジェクト (コンストラクター) へリンクする。
3.ステップ 1 で新しく生成されたオブジェクトを this コンテキストとして渡す。
4.関数が自分自身を返さない場合は this を返す。

 よくある事ですが、英語の原文直訳のままに こそあど言葉が使われているので、私には何を指しているのか全くわかりませんでした。
 とりあえず、使えなくはないので深く気にしなかったのですが、この部分の理解不足でハマる羽目になったので一旦整理したいと思いました。

 ※)尚、失敗例は備忘録として記事最後に掲載しますが、今後も同様の原因で失敗しそうなので随時追加の予定です。


『new演算子』が作るもの

 再びwikiから引用します。

 New演算子とは『newまたはNewは、C++を始めとしたオブジェクト指向プログラミング言語において、インスタンスを作成する演算子である。多くの場合、ヒープ領域からの動的メモリ確保(動的記憶域確保)を伴う。new演算子によるインスタンスの作成は、大きく分けて、記憶域を確保することと初期化を行うことに分けられる。』とのこと。

 プログラミング言語が勝手にやってくれるので見逃しがちですが、ここで注目したいのは、『動的メモリ確保(動的記憶域確保)を伴う』部分です。

 『動的メモリ確保は、メモリ管理のひとつである、プログラムを実行しながら、並行して必要なメモリ領域の確保と解放を行う仕組みである。メモリの利用状況は、自身の実行状況や他のプログラムの実行状況に応じて常に変動するため、それらの動作に支障を来さぬよう必要なメモリ領域を適切なアドレスに対して臨機応変に確保・解放を行う必要がある。』


 まぁ、今更なコンピュータのしくみの基本的な解説なのですが、多々ある入門サイトのインスタンス化の説明ではオブジェクトの初期化の部分は取り上げられるのに、動的記憶域確保については軽く流されている気がします。

 でもこれこそがインスタンス化で作成されるオブジェクトの「実体」部分なわけで、『動的メモリアロケーションを使うことで、プログラムの実行時に必要な分だけ「メモリの分け前」 (記憶領域) を確保(allocate)し、また、記憶領域が不要になった時には、他のデータに再利用できるよう、解放 (release, free, deallocate) する』わけですね。

 これを読むと、忘れがちなメモリ解放も非常に重要なことだと認識できます。


var = new T

 で、コード例として上記解説があるのですが、『varは、作成されたインスタンスへの参照を保持するポインタ型もしくは参照型の変数である。Tは、作成されるインスタンスのデータ型を指定する。』とあります。

 あれぇ?今まで大きな勘違いをしていました。 new ってオブジェクトの実体を作るものなのだから 代入される変数に実体が入るんじゃないの? って思ってました。なんとオブジェクトのアドレスが入っていたんですね。

 よくある入門サイトの例題で、一つのインスタンスを複数の変数に代入すると同期してしまうことや、同じコードでインスタンスを二つ作って変更を加えた時に同期しない理由がこれだったわけです。

 new演算子でインスタンス化されたオブジェクトを代入した変数はアドレス参照されていたからなんですね。目から鱗が落ちました。納得納得。

new演算子がやってることを噛み砕く

 再びnew演算子がやってることを引用します。

1.空の JavaScript オブジェクトを生成する。
2.このオブジェクトを他のオブジェクト (コンストラクター) へリンクする。
3.ステップ 1 で新しく生成されたオブジェクトを this コンテキストとして渡す。
4.関数が自分自身を返さない場合は this を返す。

 では、こそあど言葉の対象がはっきりしないnew演算子の解説文をこれまでの解説を踏まえて読み替えてみましょう。


インスタンス作成手順1:メモリ割り当てフェーズ
1.メンバを持たない空のオブジェクトをメモリ領域に確保(作成)します。
(ここで新しく作成した空のオブジェクトには "this" という名前のポインタが付いています。)

インスタンス作成手順2:領域初期化フェーズ
2.コンストラクター関数が呼び出された時、新しく作成された初期化対象のオブジェクトのポインタ"this" がコンストラクター関数へ渡されます。
3.コンストラクター関数は 受け取った引数に従って、ポインタ"this" で指定されたアドレスに展開されたオブジェクトへプロパティやメソッドを追加します。
(根拠ないけど、ひょっとしたらプロパティ名もメソッド名もポインタなのかも??)
4.(未だ意味不明。thisも勉強しなきゃ?)

※尚、代入された変数は作成されたインスタンスへの参照を保持する、ポインタ型もしくは参照型の変数となる。

 領域確保には初期化して実際のサイズ決定する必要がありそうなので、手順1ではアドレスを決めているだけかもしれません。だいたいこんな感じでしょうか?



失敗例)String オブジェクトの作成方法(リテラル文字列、String 関数、String コンストラクタ)の違い

 厳密には失敗例ではありませんが、文字列配列のソートについて調べていたら違いに気が付いたので整理したことをメモしました。

 リファレンスからの転記を最後に載せました。実際に実行しての確認はしていないので間違っている部分があるかも知れません。矛盾しているように見える記述もそのまま転記。

【stringオブジェクトの作成構文とオブジェクトの型】
構文: string1 = "文字列プリミティブ";   // プリミティブ文字列
構文:String(thing)           // string型(String関数で呼び出し)
構文:new String(thing)   // object型(String コンストラクタで作成)

プリミティブの文字列になる場合:
 ・文字列リテラル (二重引用符または単一引用符で示される)
 ・String 型 (String 関数で 呼び出した場合)

String オブジェクトのプロパティ読み取り、メソッドの呼び出し可能:
 ・文字列リテラル
 ・String 型
 ・String オブジェクト(newでインスタンス化した場合のみ)

新しいプロパティの作成やメソッドの追加:
 ・String オブジェクト


以下根拠となる引用(かまわず物故抜いてきたので長いです。)

『String オブジェクトは、リテラル文字列を使用して自動的に作成できます。この方法で作成された String オブジェクト ("プリミティブ" 文字列) は、new 演算子で作成された String オブジェクトとは使い方が異なります。プリミティブ文字列では、プロパティの読み取りやメソッドの呼び出しは実行できますが、新しいプロパティの作成やメソッドの追加は実行できません。

リテラル文字列にエスケープ シーケンスを使用すると、改行文字や Unicode 文字など、文字列で直接使用できない特殊文字を表すことができます。スクリプトのコンパイル時に、リテラル文字列の各エスケープ シーケンスは、それぞれが表している文字に変換されます。詳細については、「文字列データ」を参照してください。

JScript では、String 型も定義されています。このデータ型は、String オブジェクトとは異なるプロパティとメソッドを提供します。String 型の変数にプロパティを作成したり、メソッドを追加したりすることはできません。String オブジェクトのインスタンスでは、これらの処理が可能です。

String オブジェクトは、String 型 (System.String 型と同じ) と相互運用されます。つまり、String オブジェクトは String 型のメソッドとプロパティを呼び出すことができ、String 型は String オブジェクトのメソッドとプロパティを呼び出すことができます。詳細については、「String」を参照してください。また、String オブジェクトは String 型を受け取る関数で使用でき、その逆も可能です。』


『JavaScript では、プリミティブ値の文字列と String オブジェクトの文字列は区別されることに注意してください。 (Boolean や Numbers にも同じことが言えます。)

文字列リテラル (二重引用符または単一引用符で示されます)、および String 関数をコンストラクター以外の場面で (すなわち new キーワードを使わずに) 呼び出した場合はプリミティブの文字列になります。 JavaScript では、必要に応じてプリミティブの文字列が自動的に String オブジェクトに変換されるので、プリミティブの文字列に対して String オブジェクトのメソッドを使用することができます。プリミティブの文字列に対して、メソッドの呼び出しやプロパティの参照が行われようとした場合、 JavaScript は自動的にプリミティブの文字列をオブジェクトでラップし、メソッドを呼び出したりプロパティの参照を行ったりします。』


 文字列リテラルにプロパティ追加している例。MsDocsのフィルタで出なくなっているのは上記の説明と矛盾するため削除扱いされているのかも。後程確認する用に保存の意味で。


失敗例:Filesコレクションと作成したEnumeratorオブジェクトの関係

 あるフォルダ内にあるファイルの中から、更新されたファイルを別フォルダへ移動するスクリプトを組んでいてハマった事例です。

 newでインスタンス化した時点で元のフォルダオブジェクトのFilesコレクションとEnumeratorオブジェクトのfileオブジェクトの集合体は別物だったよということです。


 JScriptではフォルダ内のファイル一覧を取得する場合、一度FolderオブジェクトからFilesコレクションを取得し、Enumeratorオブジェクト化する必要があります。

Filesコレクションについて
・フォルダ内の全ファイルを一覧表示するにはFilesコレクションを使う。
 (FilesコレクションにはCountプロパティとitemプロパティのみ。メソッドはなし。)
・Filesコレクションにはフォルダ内の全てのファイルオブジェクトが含まれている。

※コレクションとは複数の要素を格納できる特殊なオブジェクト。
Enumeratorオブジェクトについて
・コレクション内の項目を列挙する方法はEnumeratorオブジェクトで提供される。
・Enumeratorオブジェクトはインスタンス化して作成する。


 Enumeratorオブジェクトから個々のFileオブジェクトのプロパティやメソッドを参照・更新するのには何の問題もないのですが、Moveメソッドには注意が必要です。

 失敗例として、ファイル移動と同時にコレクションからも削除されることを期待(ここが仕様バグ)した上での処理を作成しました。

 Enumeratorオブジェクトの全ての要素を処理するループの中で、1.moveメソッド実行、2.移動完了確認するまで待機ループする処理を組みんだとき、待機ループを抜け出す判定にEnumeratorオブジェクトのitemプロパティを直接使い、FileExistsメソッドで元のフォルダから消えたか確認する処理にしたのが以下の通り(該当部分のみ抜粋)。


var enuFiles = new Enumerator(this.objInFiles);
for (; !enuFiles.atEnd(); enuFiles.moveNext()){
	/* ---------------------------- */
	/* ファイルサイズをチェックする */
	/* ---------------------------- */
	if (enuFiles.item().Size !== 0){
		/* ----------------------------------------- */
		/* プロパティ変更があればファイルを移動する。*/
		/* ----------------------------------------- */
		enuFiles.item().Move( this.objOutFolder +"/");
		while ( ! this.objFS.FileExists( this.objFS.BuildPath( this.strOutFolderPath , enuFiles.item().Name ) ) ){
			i += 100;
/**/	    WScript.Echo( "*debug* ループ "+ /**/  "" +"" + ": FileExists.status="+ this.objFS.FileExists( enuFiles.item() ) +  ", SleepTime="+ i +"ms" + enuFiles.item() );
		    WScript.Sleep(100);
		};
		
/**/	WScript.Echo( enuFiles.item().Name + "  を移動しました。");	/*移動ファイル名(Nameを取るとパス名になる)*/
	}else{
		WScript.Echo(++k);	/*ループ回数*/
	};
};
delete enuFiles;


 実行すると、移動確認の待機ループで無限ループを起こします。

 確認してみると、実際のファイル移動は行われるのですが、Enumeratorオブジェクトの要素自体はそのままに、Pathプロパティの値が移動先のパスに変更されていました。

 なんか、オブジェクトってめんどくさい。




この記事が気に入ったらサポートをしてみませんか?