|
JavaScript 2.0
根拠
メンバ探索
|
09/20/2002 (Fri)
メンバ探索の意味については TC39 サブグループにおいて多くの議論がなされている。ここではその内容を数多く紹介しよう。
一般的な非限定メンバ探索を a.b のように表す。ここに a は式、b
は識別子である。限定メンバ探索も考慮し、これを a.n::b と書く。ここに
n は名前空間として評価される式である。ほとんど全ての場合我々の興味は a の動的型 Td
にある。ある案においては式 a の 静的型 Ts も考慮する。言語が確実なものであれば常に
Td Ts が成り立つ。
最も単純なアプローチはオブジェクトをメンバの名前と値を格納したただの連想配列とみなすことである。このように解釈すると、オブジェクト
a の内部に b という名前のメンバがあるかどうかをチェックするだけでよい。存在すればそのメンバの値を返し、見つからなければ
undefined を返すかエラーを出すのである。
この単純なアプローチには数多くの困難が伴い、ほとんどのオブジェクト指向言語はこれを採用していない:
private や internal にできない。private や internal メンバを認めてしまうと、オブジェクト a が名前 b
のメンバを複数持つ可能性を許容しなければならなくなる — 抽象理論においてクラス C のユーザは C の非公開
(private) メンバの詳細について関知すべきでなく、特にユーザは C の派生クラス D を作成可能で、C
の非公開メンバの名前を知らなくても D にメンバを追加できなければならない。C++ と Java
は両方ともこれを許容しており、我々もオブジェクト a が名前 b
のメンバを持つことを許容する。しかしこのメンバへのアクセスは認めない。我々は近代的な言語の慣例に従って、アクセス制御はレキシカルスコープにより規定されるものとする。
メンバ探索モデルが満たすべき基準は以下のようなものである:
private
メンバへのアクセスを許可しない。また、メンバを定義しているパッケージ外部からの internal
メンバへのアクセスも許可しない。更にクラス C がその非公開メンバ m にアクセスする場合、C
の悪意のある派生クラス D は C のコード中で m の振りをするメンバ m' を代用することはできない。private メンバや internal
メンバはそれらのクラスやパッケージの外部からは不可視である。大規模プログラミングにおいてクラスはインポート側に public
なバージョンを複数提供でき、より新しいバージョンの public メンバは古いバージョンをインポートしている側からは不可視である。これは堅牢なライブラリを提供するのに必要なことである。private 、internal 、或いは public に変更し、新しく設定された可視性以外で使用できないようにする。a.b のような通常の非限定メンバ探索のための有力なモデルは3つある。S を式
a (以下、単に "オブジェクト a" という) を評価したときに得られるオブジェクトメンバの内、名前
b のものの集合としよう。このメンバは a.b
が評価されるレキシカルスコープで名前空間規則を適用することによりアクセス可能である。3つのモデルは全てこのメンバ
s S
のどれかを正しいメンバとして選ぶ。集合 S が空であればメンバ探索の失敗は明らかだが、「Spice」モデルと純粋な「静的」モデルは集合
S が空でなくても何らかの理由で失敗することがある。このような何らかの理由による失敗を除き、集合 S
がメンバとして s しか含まない場合は3つのモデルは全て同じ要素 s を返す。集合 S
がメンバを複数持つ場合は3つのモデルはおそらく異なるメンバを選ぶ。
もう1つの興味深い (そして有用な) 話は静的モデルと「動的」モデルが this.b
形式のメンバ探索において常に同じ解釈をするということである。this.b 形式のメンバ探索において3つのモデルが全て同じ解釈をするのは、b
が現在のクラスで定義されているメンバである場合である。
オーバーライドについて: クラス D がその基底クラス C のメンバ m をオーバーライドするとき、m は概念的には D の全てのインスタンスにおいて置換される。しかし3つのモデルはメンバ m が宣言された最上位のクラスしか見ようとしない。3つのモデルはいずれもオブジェクト指向言語として自然な方法でオーバーライドを処理するのである。3つのモデルが異なる解釈をするのは、クラス C が名前 m のメンバを持ち、C の派生クラス D が同名のメンバ m を持ちながら、C の m が D の内側から不可視であるためにオーバーライドにならない場合である (あまり知られていないが、このようなオーバーライドにならないケースは C++ や Java でも同様にある [隠しルールと呼ばれるものである。「あまり知られていない」こともないだろう])。
静的モデルにおいては式 a の静的な型 Ts に注目することになる。クラス Ts か Ts の祖先クラスを含む集合 S の部分集合を S1 とすると、S1 内の最派生クラスのメンバが選択される。
このような純粋な静的モデルは Java と C++ で実装されている。JavaScript ではほとんど全てでないものの多くの式が Any
型なのでこれはうまくいかない。Any 型はメンバを持たないため、ユーザは目的の型 T のメンバにアクセスする前に式
a を型 T にキャストしなければならない。このことから、部分集合 S1
が空の場合、すなわち静的探索が失敗するような場合を扱えるように静的モデルを拡張しなければならない。(このようなことをしなくても静的型
Ts が特別な型であるときに静的モデルを拡張することもできる。しかしそうするとどの型が特別でどの型が特別でないのか決めなければならない。Any
が特別であるのは明白だが Object はどうか? Array は? そのような境界線を引くのは難しい。)
静的モデルをどのように拡張してもメンバ選択の余地はそのままである。動的モデルを使うことにするか、S 中の最派生メンバを選択するか、或いはおそらく他のアプローチを使うことになるだろう。
制限:
| 安全 | 純粋な静的モデルは良好。拡張静的モデルの問題 (派生クラスが暗黙の内にメンバを隠蔽する) はおそらく警告が出されるだろう。 |
| 抽象 | 良好。 |
| 堅牢さ | 完全に不適。関数の戻り値やグローバル変数の型を変更すると、その関数やグローバル変数を使う全てのコードの意味も暗黙の内に変更されてしまう。大規模プロジェクトにおいてそのような変更は非常に困難であり、式を正しく部分式に分割することも困難である。 |
| 独立した名前空間 | 良好。 |
| 互換性 | 純粋な静的モデルは不適 (あらゆる箇所に型キャストが必要になる)。拡張静的モデルはその拡張の仕方によっては良好。 |
| その他 |
複合式における中間的な型をコンパイラが決定するのは困難であるため、このモデルにおけるコンパイルは難しいものになる。静的モデルに基づく言語は従来オフラインでコンパイルされ、プログラマがデータ構造を前以って宣言していなければオンラインでのコンパイルは難しいのが普通である (コンパイラが前方参照されているデータ構造を見つけても、それが型を持つべきかどうか分からない)。より動的な実行モデルはより多くの情報が明らかになるまでコンパイルを遅延するので、この困難を解決する助けになる。 |
Spice モデルではクラス C で定義された各メンバ m を、第1引数の型が C である (オーバーロード可能な) 関数の関数定義として考える。内側のレキシカルスコープの定義は外側のスコープの定義を隠蔽する。Spice モデルでは式 a の静的型 Ts は考慮しない。
メンバ探索式 a.b を囲っている最も内側のレキシカルスコープを L としよう
(名前 b のメンバがレキシカルスコープ L で定義されている)。そしてレキシカルスコープ L
で定義されている名前 b の全てのメンバの集合を Lb とし、S1 =
S Lb (S と
Lb の共通部分) としよう。S1 が空であれば探索は失敗である。S1 に含まれるのが
s 1つだけであれば s を選択する。S1 が複数のメンバを含む場合は失敗である (これは衝突がある場合にのみ起こる)。
制限:
| 安全 | 良好。 |
| 抽象 | 良好。 |
| 堅牢さ | 乏しい。internal
メンバの名前を変更すると、メンバを定義するクラス外部のコードがそのメンバにアクセスしているどうかに関係無く、そのコードは破壊される可能性がある。private
メンバを他の2つの可視性に変更した場合も他 (同名だが無関係のメンバを偶然持っていた同じパッケージ内に存在する無関係のクラス)
との衝突を引き起こす。幸いこれらの衝突は大抵の場合 (いつもではないが)
暗黙の内にプログラムの意味を変更される前にエラーになるため、変更を加えた後にプログラムを徹底的にテストすれば発見できる。 |
| 独立した名前空間 | 不適。無関係のクラスに同名のメンバがあれば衝突する。 |
| 互換性 | 乏しいと思われる。既存のプログラムの多くは名前空間の独立性に依存しており、再構成が必要になる。 |
| その他 |
独立した名前空間が得られないことで大半のオブジェクト指向プログラマは困惑するだろう。この想定無しにプログラミングを行うには大半のプログラマが持っているものとは別の観点で望まなければならない (Lisp や Self のプログラマのことを言っているのではない。連中はこのような考え方には慣れている)。 |
[Spice モデルはこの他に多くの変種が存在する。]
動的モデルにおいてメンバ s は、メンバ探索式 a.b を囲っている最も内側のレキシカルスコープ L
で定義されている S から選択される。このレキシカルスコープ L が S 内に複数のメンバを含む場合探索は失敗する
(これは衝突がある場合にのみ起こる)。
制限:
| 安全 | 言語レベルでは良好だが「その他」も見よ。 |
| 抽象 | 良好。 |
| 堅牢さ | 良好。Spice モデルの項で述べた変更は全て簡単にできる。 |
| 独立した名前空間 | 良好。 |
| 互換性 | 良好。 |
| その他 |
動的モデルを使ったパッケージは特定の侵入コードによる乗っ取り (作者が意図することとは別の行為を強制されること) に遇い易いかもしれない。コンパイラがこれを検出し、警告することは可能である。 |
幾つかのモデルでは、オブジェクトの可視メンバにアクセスする方法が無かったり、アクセスが安全でない場合
(「メンバの乗っ取り」を見よ) でも問題が起こらないようになっている。このような場合、可能性のある同じ名前の複数のメンバの中から的確に選択できるべきである。::
名前空間構文がこれを可能にする。:: の左オペランドはパッケージかクラスとして評価される式である。また、この式の代わりに
public 、internal 、private といった特別なキーワードを使うこともできる。或いは完全に省略してもよい。::
の右オペランドは名前であり、結果は名前空間で限定された名前になる。
これまで見てきたように、メンバアクセス式 a.b の名前 b は必ずしもオブジェクト
a の一意なアクセス可能メンバを参照している必要はない。限定メンバアクセス式 a.n::b
では、考えるメンバの集合が結果的に2つ以上のメンバを含む可能性があるにも関わらず、名前空間 n
はその集合を縮小する。その場合探索モデルは再度メンバを確定する。S をオブジェクト a の名前 b
のアクセス可能メンバの集合として、a.n::b が
n によってどのように S の部分集合を形成するかを以下の表に示す:
| n | 部分集合 |
|---|---|
| 無し | 名前 b の動的メンバのみ (あれば) |
| クラス C | C の名前 b の固定メンバ。存在しなければ C の基底クラス...とチェインを上がる |
| パッケージ P | P の全てのアクセス可能メンバを含む S の部分集合 |
private |
現在のクラスの名前が b である固定メンバ |
internal |
パッケージの可視性 (internal) を持つ全てのアクセス可能メンバを含む S の部分集合 |
public |
可視性 public を持つ全てのアクセス可能メンバを含む S の部分集合 |
:: 演算子の役目は . 演算子と同じではない。.
演算子が値を生成するのに対し、:: 演算子は限定名を生成する。限定名は
. の右オペランドに使用できるが値は使用できない。値を置くべき箇所に限定名を用いた場合、限定名はレキシカルスコープ規則に従って値
(大抵はグローバル変数) を得るために探索される。
上に挙げた3つのモデルは全てクラスの固定プロパティにのみアクセスする。JavaScript
では個々のインスタンスに動的にプロパティを追加できる。簡潔さのためにこのような動的プロパティにはアクセス制御とバージョン管理は提供されない
— これらは全て公開演算であり、どこからでもアクセスできる。安全基準のために、private
や internal メンバの探索では同じ名前の動的プロパティがある場合でも private 、internal
メンバを選択しなければならない。堅牢さの基準を満たすために、public メンバを可能な限り
private や internal メンバと同じように扱うべきである。このため同名の動的プロパティが存在する場合に、常に固定プロパティを優先させるのである。
固定プロパティにより隠蔽された動的プロパティにアクセスするには、メンバ名の前に :: を付けるか、間接プロパティアクセスを使う。
a[b] のような式の振る舞いはどのように定義すべきだろうか? (a
のクラスは配列や、[] 演算子の既定の意味をオーバーライドするクラスに型付けされていないとする) これには2、3の考えがある:
"s"
として評価し、a[b] を a.s
のように扱う。これは本来 JavaScript 1.5 の動作である。残念ながら JavaScript 1.5 プログラムの期待
(同名のメンバが2つ以上存在しない、など) を裏切らずにこの振る舞いを保つのは困難である。更にこの種の間接操作はメンバの乗っ取りにも遇い易い。メンバ乗っ取りの問題は
a.n::[b]
のように名前空間の節で示した規則に従った [] 演算子の制限付き異性体を考案することで解決できるかもしれない。"s"
として評価し、a[b] を a.::s
のように扱う。故にこの振る舞いは動的メンバを選択しようとするときに限られる。動的メンバの振る舞いは十分なものだが、JavaScript 1.5
のスクリプトが [] で JavaScript 2.0 のオブジェクトを使おうとしたとき互換性の基準に違反することになる。普通に考えると文字列 "s" の構文を文字列中に ::
を含められるように拡張する考えはまずいものである。そのような文字列の生成は非常に早い段階で行われ、メンバを指すことができないのである。
[セキュリティアタックの説明]
|
Waldemar Horwat 最終更新: 2002年9月20日(金) |