|
JavaScript 2.0
根拠
その他 |
06/04/2003
この節では草案作成中に考えられていたその他の代替案を幾つか示す。
Object型階層の根底は JavaScript 1.5 にあった Object
型が採られた。数値、文字列、論理値の各単純値 (プリミティブ値) は Object
の派生クラスのインスタンスとなり、以前からあったラッパクラスは削除された。これは JavaScript 1.5
における単純値とオブジェクトの混同を無くすためである。
他に考えられていたものとしては新しい根底型に any を定義するものがあった。この型は単純な数値、文字列、論理値と同様
Object の祖先で、それぞれ自身の型 number 、string 、boolean を持つことができた。この筋書きの下では
JavaScript 1.5 のクラス Number 、String 、Boolean は Object の派生クラスのままだが、その単純値はそれらのクラスのメンバではなくなる
— 例えば false は boolean のメンバだが Boolean のメンバではない。false
をボクシングして得られたオブジェクトは Boolean クラスのメンバだが boolean のメンバではなく、if
文では真と (!) 評価されてしまう。
any 案は JavaScript 1.5 と互換性の高いものだが、新しく持ち込まれる複雑さと混乱は必要以上の互換性に見合ったものではない。代わりに
JavaScript 2.0 はこの部分において JavaScript 1.5 の挙動よりも簡単で整理されたものになっている。
Never と VoidNever 型と Void
型は異なる目的で使う。(関数の) 戻り値の型として使う場合、Never は関数が処理を返さないことを示し、Void
は関数は値を返すが返される値は無効である (常に undefined) であることを示す。
以下の例は Never と Void の使用法を示すものである:
// この関数は無効な値を返す。
// This function returns no useful value.
function display(message:String):Void {
document.write("<\_P>" + message + "<\/P>\n");
}
// この関数は処理を返さない。
// This function cannot return.
function abort(message:String):Never {
display(message);
throw AbortException;
}
function chickenCount(myChickens:Array[Chicken]):Integer {
if (notHatched(myChickens))
abort("Can’t count the chickens yet");
else
return myChickens.length;
}
仮に関数 abort が戻り値の型を明示していなかったり Never 以外の型を指定している場合は、コンパイラは関数
chickenCount の内で警告を出すということに注意していただきたい。これは chickenCount が
Integer を返すように宣言されているにも関わらず、値を返さずに関数から抜けるコード経路 (if 文が偽になる場合)
を含むからである。関数 abort の戻り値の型 Never は chickenCount
内にそのようなコード経路が存在しないということをコンパイラに知らせるものである。
コンパイラの警告の問題を解決するのに、その関数が処理を返さないことをコンパイラに検証させないためにということで、戻り値の型を Never にするアイデアは良いものだったかもしれない。
草案には元々他にも配列型が含まれていた (簡潔さのために除外された)。以下は言語の将来のバージョンで採用される可能性のあるものである:
| 型 | 値の集合 |
|---|---|
Array |
Array[Object] と同じ |
Array[t] |
型 t の要素を格納する全ての (連続、非連続) 配列、及び null |
List[t] |
型 t の要素を格納しする可変長連続配列、及び null |
t[] |
型 t の要素を格納する固定長連続配列、及び null |
ConstArray |
ConstArray[Object] と同じ |
ConstArray[t] |
型 t の要素を格納する全ての定数配列、及び null |
非連続配列は空洞を持つ。連続配列は必ず0から始まる添字を持つ連続した要素を持つ。可変長配列は length
プロパティを設定することで長さが変化する。
A が Array[t] 、List[t]
、t[] 、ConstArray 、及び ConstArray[t]
のいずれかの型式である場合、以下の3つの操作で A 型の配列の作成、或いは変換が可能になる:
new A(elt1, elt2, ...)
は与えられた要素 (要素数は1でもよい) を使って相当する配列型のインスタンスを作成する。new A(length: n) は相当する配列型のインスタンスを長さ
n で作成する。各要素の初期値は配列が非連続であれば空洞であり、連続していれば A
の要素型のデフォルト値になる。この形式を使うには名前付き引数拡張が必要である。(B) は相当する配列型のインスタンスを作成する。新しいインスタンスの各要素は
B から複製されたものであり、B は何らかの配列でなければならない。A が連続配列で B
が空洞を持つ場合の結果は未解決である。ConstArray 以外の配列型インスタンスが等しいのはそれらが同一のオブジェクトである場合だけである。ConstArray
配列型インスタンスが等しいのは要素が同じで要素の型 t も同じ場合である。
他に以下の表のような型演算子が定義できた。s と t は型式である。
| 型 | 値 | 値 v の暗黙の強制型変換 |
|---|---|---|
t[] |
型 t の固定長配列及び null |
undefined null |
const t |
型 t (配列型でなければならない) を読み取り専用の配列型にする | 無し |
- t |
型が t である null を除くあらゆる値 |
|
~ t |
型 t のあらゆる値及び undefined |
undefined undefined。型
t の他の暗黙の強制型変換には既に定義がある |
+t |
型 t のあらゆる値及び null |
null null。undefined
null (undefined が t
のメンバでない場合)。型 t の他の暗黙の強制型変換には既に定義がある |
s + t |
型 s と型 t のいずれか、或いは両方の全ての値 | vs+t
であれば v を使う。それ以外の場合で v as s が定義されていれば
v as s を使う。これら以外の場合で v as t
が定義されていれば v as t を使う |
s * t |
型 s であり、型 t でもある全ての値 | v as s as t
が定義されていて、且つ s*t のメンバであれば
v as s as t を使う |
s / t |
型 s であるが型 t ではない全ての値 | v as s が定義されていて、且つ s/t
のメンバであれば v as s を使う。 |
添字演算子 [] は配列型を作成するのに使用できる。Array
とは異なり、これらの配列には空洞が無く、整数値でのみインデクシング可能、固定長、そして派生不能である —
t[] はクラス修飾属性 final を持つ。
新しい単項演算子として const が追加できた。この演算子は
PostfixExpressionOrSuper x
をオペランドにとり、x が ConstArray 配列以外のインスタンスであれば const x
は x のコピーを ConstArray[T] で返す。ここに T は、x
の各要素が T のメンバであることを最もよく表す型である。x が配列型であれば const x
は対応する ConstArray 型となる。
[] は t の書き込み可能配列型である。const t[] は t の定数配列型である。[][] は t の書き込み可能配列の書き込み可能配列型である。[][] is the type of writable arrays of writable arrays of t.const t[][] は t の書き込み可能配列の定数配列型である —
const は [] よりも結合が弱いので const (t[])[] としても同じである。(const t[])[] は t の定数配列の書き込み可能配列型である。以前の JavaScript 2.0 草案では、1つのクラスが複数のコンストラクタを異なる名前で持ち、静的関数と同じ構文で呼び出すことができた。これらは言語の単純さを保つために除外された。現在では各クラスは最高でも1つしかコンストラクタを持てず、同じクラスから別のコンストラクタを呼び出す
this(args) 形式も除外された。コンストラクタは以下で述べるように定義されていた。
コンストラクタはクラス C の新しいインスタンスを作成する関数で、constructor
属性を使って定義する。コンストラクタは普通、クラスと同じ名前で与えられるが、その場合は constructor
属性は省略可能であり、コンストラクタはいわゆるデフォルトコンストラクタとして new C
で呼び出し可能になる。コンストラクタの名前がクラス名と異なる場合はそのクラスの static 関数として呼ぶ:
class C {
var a:String;
constructor function C(p:String) {this.a = "New "+p} // この constructor 属性は省略可能
constructor function make(p:String) {this.a = "Make "+p}
static function obtain(p:String):C {return new C(p)}
}
var c:C = new C("one");
var d:C = C.C("two");
var e:C = C.make("three");
var f:C = C.obtain("four");
c.a; // "New one" を返す
d.a; // "New two" を返す
e.a; // "Make three" を返す
f.a; // "New four" を返す
コンストラクタ内では this によりそのクラスのインスタンス変数を参照することができる。 クラス C をクラス
B の派生クラスとすると C のインスタンス作成中に B のコンストラクタが呼ばれるわけだが、その際
B のコンストラクタは部分的に構築されているインスタンスでクラス C
の仮想メソッドを呼び出すことができるだろう。同様に B のコンストラクタは this をグローバル変数 v
に格納することができ、他の関数は部分的に構築されているオブジェクト v で C
のメソッドを呼び出すことが可能である。クラス C とその祖先クラスのいずれも上記のどちらかの動作をするコンストラクタを持たない場合に限ってクラス
C のメソッドは C の完全に初期化されたインスタンスで呼ばれるだけということが保証され得る。
コンストラクタは自動的に新しく作成されたオブジェクトを返すので return
文で値を返すべきではない。コンストラクタの戻り値の型も記述してはいけない。
constructor 関数は常に新しいインスタンスを返す。一方、static
関数はそのクラスの既存のインスタンスを返すことができる。クラス C と同名の static
関数を定義することは可能で、そのような関数はクラスの外側からはコンストラクタのように見える (new C
として呼び出すことができる) が、そのクラスの既存のインスタンスを呼び出し側に与えることができる。しかしながら、派生クラスには
C がそのクラスのコンストラクタでないことが分かってしまうだろう。これは派生クラスのコンストラクタから C
の擬似コンストラクタを呼び出すことができないためである。
クラス C がデフォルトコンストラクタ C.C を定義しない場合は
C.C は自動的に定義される。この自動的に定義されるコンストラクタは C
の基底クラスのデフォルトコンストラクタ B.B と同じ引数をとる。C.C
は B.B を呼び出した後、C の新しいインスタンスメンバをそのデフォルト値で初期化する。
クラス C の基底クラスを B とする。コンストラクタ C.n は this
や super にアクセスしたり呼び出し側に処理を戻す前にコンストラクタ B.m か
C.m を呼び出さなければならない。この呼び出しは明示的なものでも暗黙のものでもよい。仮に
C.n が実行パス上でコンストラクタ B.m
、C.m いずれの呼び出しも含んでいなければ、自動的に C.n
の文の先頭に B.B の引数無しの呼び出し文が挿入される。コンストラクタ C.n
が例外を投げて処理を抜ける場合は他のコンストラクタを呼び出す必要は無い。C.n は2回以上コンストラクタ呼び出しを行ってはならない。
コンストラクタ C.n は以下の文を使って他のコンストラクタを呼び出すことができる:
super(args) |
B のデフォルトコンストラクタ B.B を呼ぶ。 |
super.m(args) |
B のコンストラクタ B.m を呼ぶ。m は B
のコンストラクタの1つを名前付ける QualifiedIdentifier でなければならない。 |
this(args) |
C のデフォルトコンストラクタ C.C を呼ぶ。 |
this.m(args) |
C のコンストラクタ C.m を呼ぶ。m は C
のコンストラクタの1つを名前付ける QualifiedIdentifier でなければならない。 |
上記の文は部分式などでない完全な文でなければならない。上の4つの形式の内、最初のものは確実に
SuperStatement となるが、残りの3つは
ExpressionStatement
としてパースされる。以下の規則により、これら3つの形式 (super.m(args)
、this(args) 、this.m(args))
が式、コンストラクタ呼び出しのいずれとして扱われるか決まる:
this(3,4)+5) 式である。super.m(args) の m
が基底クラスのコンストラクタの名前に無ければ式である (この場合、this 上で基底クラスのプロパティ m
が探索され、args を引数にしてメソッドとして呼び出す) this.m(args) の m
が現在のクラスのコンストラクタの名前に無ければ式である (この場合、this 上で基底クラスのプロパティ m
が探索され、args を引数にしてメソッドとして呼び出す) 。オブジェクト作成中にクラス階層レベルを飛び越えたりすることはできない — 例えば C の基底クラスが B で、B の基底クラスが A のときは、C のコンストラクタは直接 A のコンストラクタを呼び出すことはできない。
クラス拡張はどこか他の場所で定義されたクラスにメソッドを追加する機能である。クラス拡張は JavaScript 2.0 草案に含まれていたが、言語の単純さを保つために除外された。クラス拡張は有用なものだが、クラス設計は著しく複雑になり、厄介な問題 — パッケージ P がまずパッケージ Q から基底クラス C をインポートし、次に C の派生クラス D を定義、最後に D のメソッドと同じ名前、同じ名前空間で C を拡張した場合どうするのか — が発生する。
クラス拡張は以下で述べる extend 属性を使って行う。
extend 属性extend 属性は1つの引数 C をとり、定義をクラス C
に新メンバとして追加する。この引数はクラスとして評価されるコンパイル時定数式でなければならない。これによりクラス
C が既にパッケージ P で定義されていても、既存の C にメソッドを追加できるようになる。これには幾つか制限がある:
extend 属性が使用できるのは function 、const 、class
、namespace 及び static var 定義のみである。例えばクラス C
のオブジェクトに新インスタンス変数を追加するのにクラス拡張は使えない。ただしゲッタとセッタは可能である。演算子の定義にも使えない。static か final でなければならず、既存のメンバをオーバーライドすることはできない。新メンバが
function であればデフォルトで final になる。public
で定義できないことを意味している。それは public 名前空間はシステムにより定義済みであり他のあらゆるパッケージにより定義されるものではないからである。デフォルトの名前空間は
internal で C の新メンバを現在のパッケージ内でのみ可視にする。private メンバは不可視であり、P が現在のパッケージでなければ P の internal
メンバも不可視である。一方、現在のパッケージの internal メンバは可視である。以下は新しく作成した名前空間 StringExtension を使ってシステムクラス String にメソッドを追加する例である:
namespace StringExtension;
StringExtension extend(String) function scramble():String {...}
StringExtension extend(String) function unscramble():String {...}
use namespace(StringExtension);
var x:String = "abc".scramble();
クラス拡張が評価されるとメソッド scramble と unscramble は use namespace(StringExtension)
のスコープ内のコード中にある全ての文字列で使用可能になる。名前空間 StringExtension で限定すると名前 scramble
、unscramble は特別な意味を帯びるため、クラス拡張により他の無関係のパッケージとの間に名前の衝突が起こる可能性は無い。
クラス拡張を他のパッケージにエクスポートするつもりが無ければ、デフォルト名前空間 internal
の動作は大抵の場合素晴らしいものであり、上の例は簡単になる:
extend(String) {
function scramble():String {...}
function unscramble():String {...}
}
var x:String = "abc".scramble();
インターフェイスは JavaScript 2.0 で検討されたが、言語の単純さを保つために除外された — 動的な型付けが起こる言語では静的な型付けが起こる言語ほどの必要性は無い。
インターフェイスは以下の構文とセマンティクスの追加により定義されただろう。
インターフェイス I が I を実装するクラス C
の基底型ではないという点を除けば、インターフェイスはクラスのようなものである。その代わり C のインスタンス c
は型 I に暗黙に強制型変換可能で、I のインスタンス
i が得られ、i を型 C に暗黙の強制型変換すると元のインスタンス
c が得られる。これは c == i が成立するかどうかを示すものではない。
インターフェイスは具象メンバ、抽象メンバの両方を持つことができるが、コンストラクタを持つことはできない。
名前の衝突が無ければ、インターフェイス I のメンバは I を実装するクラス C のあらゆるインスタンス
c のプロパティとしてアクセスできる。しかしながらインターフェイス I のメンバ m をクラス C
のメンバと同じ名前で定義し、なお2つのメンバを区別することは正しいことである。また1つのクラスが両方とも名前 m
のメンバを持つ2つのインターフェイス I と J を実装し、2つの m
を区別するのも正しいことである。メンバ探索 c.m を処理するときどちらのメンバが得られるかは、c
が最後にどちらのインターフェイス、或いはオブジェクト型に変換されたかによる。
クラス定義の Inheritance 節はインターフェイスを許容するために
implements 節にインターフェイスのリストを指定するものに修正される:
新しく定義したクラスは、基底クラスと基底インターフェイスから (もしあれば) インスタンスメンバと静的メンバを継承する。基底クラスとインターフェイスで衝突がある場合は、基底クラスを優先する。2つの基底インターフェイスで衝突がある場合はどちらかを優先させること無く、探索ではインスタンスメンバについては基底インターフェイス名で限定し、静的メンバについては基底インターフェイス名で直接限定しなければならない。
primitive以前の草案では primitive クラス修飾属性が考えられていた。この属性を使用するとそのクラス型の変数が格納できる値の集合から
null を排除することができた。例えばクラス C を primitive 属性を使って定義すると null
は型 C のメンバではなくなる (型 C の未初期化の変数にデフォルト値を指定する方法が無ければならない)。この属性によりユーザ定義クラスに
Number などの定義済みクラスのような振る舞いを持たせることが可能になる。
この属性が現在の草案が除外されたのは、未定義クラスの循環問題のためである。仮にクラス C が未定義でも型 C
の変数を作成することは可能なのだ (この変数は null で初期化される)。C が単純 (プリミティブ)
クラスであることが分かると、この値を遡って変更しなければならない。
[訳注: この節の内容は「コア言語」の「パッケージ - パッケージの参照」とほとんど同じである]
以下に述べるように import
ディレクティブにより多くのオプションを使って、より良い識別子、名前空間の管理が可能なように拡張できる:
パッケージ P は import ディレクティブを使うことで異なるパッケージ Q を参照できる:
ParenListExpression
が与えられた場合、それはパッケージから提供される名前空間のリストであるべきである。これらの名前空間は import 文により
use される。パッケージ間の名前の衝突を解決するために、IncludesExcludes
はどの名前をインポートするかを決めるためのきめ細かい制御を可能にする。include 節、exclude
節はどの名前の集合が最上位変数として共有されるかを決定する。include を使った場合はリストに挙げられた名前だけがアクセス可能になる。逆に
exclude の場合はリストに含まれていない全ての名前がアクセス可能になる。例えば:
package My.P1 {
explicit namespace N;
N const a = "global a";
N const b = "global b";
N class C {
static var x = 2;
}
N const c = new C(i:5); // c.i を 5 で初期化する
const x = "global x";
}
package My.P2 {
import P = My.P1, namespace(N), exclude(N::b, x); // My.P1 をインポートし、名前空間 N を use する。ただし N::b
と x は除外する
c; // OK。クラス C のインスタンスとして評価される
N; // エラー: N は explicit なので見つからない
P.N; // OK。パッケージ My.P1 内の名前空間 N として評価される
a; // OK。"global a" として評価される
b; // エラー: N::b は除外されているので見つからない
P.b; // OK。"global b" として評価される
(P.N)::b; // エラー: N::b は除外されているので見つからない
x; // エラー: グローバルな x は除外されているので見つからない
C.x; // OK。2 として評価される
}
include 節も exclude 節も無ければ、exclude() としたことと同じになる。
import ディレクティブは以下の処理を行う:
const バインドする。explicit でない各最上位定義 N::n (n は名前空間
N にある) について、与えられた IncludesExcludes で
N::n が除外されていればその定義はスキップする。除外されていなければ、N::n
がグローバルスコープで定義されていない限り別名 N::n をグローバルスコープで P の
N::n にバインドする。. が付いているものとして)
制限の無い識別子を探す。これらの各式 E は名前空間 S として評価されるべきであり、与えられた
IncludesExcludes を使って
use namespace(S) を評価する。パッケージ P が public な最上位定義 n を持っており、パッケージ Q が
import PkgP = P で P をインポートすると、パッケージ Q
は n 或いは PkgP.n のいずれでも n を参照することができる。短い形式の n
は他の n と衝突する場合は使用できない。その最上位スコープの汚染を避けるためにパッケージ Q は
import PkgP = P, include() 或いは
import PkgP = P, exclude(n) を使ってパッケージ
P をインポートすることができる。この場合、PkgP.n を使う以外にパッケージ Q は
n を参照する方法は無い。
パッケージ P が explicit な最上位定義 n を持ち、パッケージ Q が P
をインポートした場合、PkgP.n を使う以外にパッケージ Q が n を参照する方法は無い。
パッケージ P が名前空間 N 内に最上位定義 n を持ち、パッケージ Q が
import PkgP = P で P をインポートすると、パッケージ Q は
PkgP.N::n 或いは N::n のいずれでも n
を参照することができる (いずれの場合も名前 N も同様にアクセス可能でなければならない。N のアクセス性が
public でない場合は名前の限定を、N のアクセス性が explicit である場合は n ではなく
(PkgP.N) の使用を必要とする可能性がある)。パッケージ Q は代わりに
import PkgP = P, namespace(N) を使ってパッケージ
P をインポートすることで、名前の衝突が無ければただ n と書くだけで n を参照可能になる。
他にも、パッケージ Q は後ろに use namespace(N) (或いは
use namespace(PkgP.N)) が続く import PkgP = P
を実行することで同様の効果を得ることができる。
この草案の初期のドラフトには、(型の) 範囲を超えた整数に対してエラーを生成する代わりに、その整数を暗黙の内に整数マシン型に変換する
wrap プラグマが含まれていた。このプラグマの有効範囲はプラグマの字句的な有効範囲と同じである。このプラグマの基本的な機能は結果が左辺値に収まらないときにエラーを生成する代わりに、sbyte
、byte 、short 、ushort 、int 、uint 、long
、及び ulong において算術的な折り返しを行うことである。
このプラグマは効率重視のコードでは有用なものだが、言語の単純さを保持するために除外された。折り返しは整数マシン型のいずれかに明示的にキャストすることでも実現できる。
| Waldemar Horwat 最終更新: 2003年6月4日 (水) |