この投稿は、TypeScriptの古くからあるソフトプライベートと、TypeScript 3.8で導入されたハードプライベートを対比し説明していくものです。
この投稿で学べること
- ソフトプライベートとハードプライベートとは何か?
- 両者の違い
- 両者の共通点
ソフトプライベートとハードプライベート
このセクションでは、ソフトプライベートとは何なのか? ハードプライベートとは何なのか? についての基本的なことを説明します。
ソフトプライベートとは?
ソフトプライベート(soft-private)とは、private
アクセス修飾子でオブジェクトのプロパティの可視性を表現する書き方のことです。
class Foo {
private name: string
}
このprivate
キーワードは、TypeScript固有の仕様で、ECMAScriptには無いものです。
ハードプライベートとは?
ハードプライベート(hard-private)とは、#
でオブジェクトのプロパティの可視性を表現する書き方のことです。
class Foo {
#name: string
}
これは、今後ECMAScriptに盛り込まれることが見込まれる「Class fields」(stage 3)の言語仕様の一部をTypeScriptに取り入れたものです。なので、TypeScript固有の仕様ではなく、JavaScriptと共有する仕様となっています。
- Class fields - JavaScript | MDN
- tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals
TypeScriptにおいては、3.8で導入された仕様です。
TypeScriptコンパイラは、このECMAScript Private Fields構文を、ES2015に対応している環境なら動くようなJSコードにトランスパイルしてくれるので、最新じゃないブラウザでも動かすことができます。ちなみに、JavaScriptとして、Private Fieldsを動かせる環境は、まだ出揃っておらず、FirefoxやSafariでは動きません:
なぜ「ソフトプライベート」なんていう新しい用語が必要な事態になったか?
private
アクセス修飾子は、TypeScript初期からあったのですが、「ソフトプライベート」は最近になって登場した新しい用語です。もともとあったprivate
を「ソフトプライベート」と呼ぼうとなったわけです。
なぜ、新しい呼び名ができたかというと、TypeScript 3.8にて「ECMAScript Private Fields」という仕様が追加されたためです。
これは従来のprivate
アクセス修飾子とは異なる方法で、プロパティの可視性を表現する文法です。このプライベートと区別する必要が出てきたため、在来のプライベートに「ソフトプライベート」という名前を冠したわけです。新機能である、ECMAScript Private Fieldsのことは「ハードプライベート」と呼びます。
ちなみに、soft-privateはsoft privacy、hard-privateはhard privacyと呼ばれることもあります。
どうして別の書き方のプライベートを導入したのか?
ひとつのプログラミング言語に、同じ目的に対して、いくつも書き方があるというのは望ましくないですよね。読むときに混乱するかもしれませんし、書くときはどれを使うべきか選択に頭をひねる必要が出てきます。
では、なぜTypeScriptは、すでにソフトプライベート(private
アクセス修飾子)があるのに、ハードプライベート(#
)を導入したのでしょうか? どうして、わざわざプログラマーの労力を増やすような機能追加をしたのでしょうか?
その理由は、TypeScriptがJavaScriptのスーパーセットであることを言語設計の基本に置いているからです。スーパーセットとは、JavaScriptコードはTypeScriptとしても解釈できる、ということです。別の言い方をすれば、JavaScriptの文法を拡張して、いろいろ追加したのがTypeScriptということでもあります。
なので、JavaScript(の仕様であるECMAScript)に、プライベートが追加された以上、TypeScriptもそれを解釈できるようになっていないといけないのです。これが、既にプライベートがあるTypeScriptに、別のプライベートが追加された理由です。
どう「ソフト」で、どう「ハード」なのか?
今のTypeScriptに2つのプライベートがあることは分かったと思います。それぞれ呼び分けるために別の名前が必要なのも理解できたと思います。
ところで、なぜprivate
アクセス修飾子のほうは「ソフト」と呼ばれ、ECMAScript Private Fieldsのほうは「ハード」と呼ばれるのでしょうか?
「ソフト」は「やわらかい」という意味ですが、「寛大だ」とか「甘い」という意味でもあります。プロパティの可視性に寛大も厳格もあるのか、という話ですが、TypeScirptの性質上private
は寛大な可視性になっています。
その根拠として、まずソフトプライベートのプロパティはJavaScriptにコンパイルしたコードではpublic同然になります。例えば、下のFoo
オブジェクトのb
プロパティはプライベートですが、コンパイル後はpublic
であるa
プロパティと同様の方法でアクセス可能です:
class Foo {
public a: string = 'a'
private b: string = 'b'
}
const foo = new Foo()
console.log(foo.a) //=> 'a'
console.log(foo.b) //=> 'b'
もちろん、foo.b
はTypeScript的にはコンパイルエラーになります。
TypeScript上でも、foo.b
へのアクセスをコンパイルエラーを起こさず、やってのける方法もあります:
console.log((foo as any).b) //=> 'b'
このように、ソフトプライベートは基本的にはプライベートな可視性を実現するものですが、絶対にアクセスできないことを保証し尽くすものではなく、その可視性にはある程度の寛容さがあるわけです。
「ハード」なほうのプライベートはどうかというと、こうした甘さが一切ないプライベートになっています。コンパイラのチェックをすり抜ける方法も、JavaScript実行時にアクセスできるようにする抜け道も一切ないものになっています。
ソフトプライベートとハードプライベートの違い
ここでは、ソフトプライベートとハードプライベートの違いを見ていきます。
どちらのプライベートも、「プロパティ」の可視性をプライベートにする、という点では共通していますが、細かいところで違いがあります。
継承とプロパティ名
ソフトプライベートとハードプライベートの細かい違いの1つに、クラスを継承したときのプロパティ名の重複問題があります。
ソフトプライベートは、親クラスが持っているプライベートフィールドと同じ名前のフィールドを子クラスで定義することができません:
一方のハードプライベートは、親クラスと同じフィールド名を使うことができます:
class Parent {
#a: any
}
class Child extends Parent {
#a: any // OK!
}
プライベートメソッド
ソフトプライベートは、メソッド定義の構文を用いて、プライベートメソッドが生やせます:
class Soft {
private doSomething() {}
}
しかし、ハードプライベートは、メソッド定義の構文が使えません:
ハードプライベートでも、フィールドに関数を代入することで、プライベートなメソッドを持たせることは可能です:
class Soft {
private doSomething = function() {}
}
class Hard {
#doSomething = function() {}
}
ただし、この書き方はメソッド定義構文とは意味が異なり、prototypeのメンバーにはなりません:
"use strict";
class Soft {
constructor() {
this.doSomething = function () { };
}
}
class Hard {
constructor() {
this.#doSomething = function () { };
}
#doSomething;
}
ハードプライベートのメソッドは、ECMAScript的にはstage 3の提案になっているので、
TypeScriptとしてもサポートされる可能性があります。そのためのIssueも立っています:
インデックスアクセス型(indexed access type)
TypeScriptでは、ClassName['propertyName']
のようにして、プロパティの型を参照する書き方が可能です。
たとえば、下記のSoft['prop']
はstring | boolean | number
と同じ意味になります:
class Soft {
private prop: string | boolean | number
constructor(prop: Soft['prop']) {
this.prop = prop
}
}
これと同じことをハードプライベートでもやろうとしてもできません:
コンストラクタのパラメータプロパティ宣言
TypeScriptのコンストラクタの引数には、オブジェクトプロパティを宣言し、引数の値で初期化する「パラメータプロパティ宣言」という機能があります。
どういうものかというと、下記のコードのように引数を受け取り、それをそのままフィールドに代入するようなコードは、
class Soft {
private foo: any
constructor(foo: any) {
this.foo = foo
}
}
次のように短縮して書くことができるというものです:
class Soft {
constructor(private foo: any) {}
}
ご覧のとおり、ソフトプライベートはこの「パラメータプロパティ宣言」が可能です。
一方、ハードプライベートのほうは、この短縮形を使うことができません:
ハードプライベートは遅い?
「ハードプライベートは処理速度が遅くなる」という説があります。その説を確かめるために、検証してみたいと思います。
ハードプライベート遅い説の論拠
なぜハードプライベートが遅いと言われるのか、その論拠を考えてみたいと思います。
まず、ソフトプライベートとハードプライベートが、どのようにJavaScriptにコンパイルされるか見てみましょう。
class Soft {
private prop = 'foo'
}
class Hard {
#prop = 'bar'
}
上のSoft
クラスがソフトプライベートで、Hard
がハードプライベートです。それぞれコンパイルすると次のようなJavaScriptが生成されます。(ちなみにコンパイルオプションのtargetはES2020です)
"use strict";
class Soft {
constructor() {
this.prop = 'foo';
}
}
"use strict";
var _prop;
class Hard {
constructor() {
_prop.set(this, 'bar');
}
}
_prop = new WeakMap();
コンパイル結果を見てみると、ソフトプライベートのほうはthis.prop
に代入しているだけのごく普通のコードです。一方、ハードプライベートのほうは、「ハードさ」を再現するために、クラス外に_prop
というレキシカル変数を定義した上で、値をそこに格納していくコードになります。
この_prop
変数は、WeakMap
オブジェクトで、キーがnew
したオブジェクト(this
)、値がそのオブジェクトのプロパティ、というマップになっています。(余談ですが、WeakMap
の性質上、this
をキーにしていてもメモリリークは起きないので安心してください。WeakMap
の詳細はMDNを見て下さい。)
ここで肝心なことは、ハードプライベートは、自身のプライベートフィールドにアクセスするたびに、WeakMap
のset
メソッドやget
メソッドを呼び出すということです。this.prop = ...
のようなコードと比べたら、「オーバーヘッドがあるだろう」という想像はできるんじゃないかと思います。
要するに、このWeakMap
化が「ハードプライベートは遅い」の論拠になるわけです。
※上の例では、ゲッターがないハードプライベートを示しましたが、ゲッターを作るとJavaScriptコードはもっと複雑になります:
ハードプライベートは遅いのか実測してみた
WeakMap
化が「ハードプライベートを遅くする」というのはあくまで仮説なので、実際に検証してみました。
結論としては、
- ハードプライベートは、ソフトプライベートに比べて、書き込みアクセス(
set
)は遅い - ハードプライベートは、ソフトプライベートに比べて、読み込みアクセス(
get
)はむしろ速い
という不思議な結果になりました。
この検証の実行環境はNode.js 13なので、ブラウザでどうなるかは調べてません。検証に使ったコードはGitHubで公開しているので、試したい方はどうぞ。
なお、Node.js 12からは、ハードプライベートもそのまま(WeakMap
化することなく)実行できるので、TypeScriptのJavaScriptターゲットをesnext
にして、WeakMap
化ありのes2020
コードと、WeakMap
化なしのesnext
コードの速度比較をしたところ、ハードプライベートのほうが読み込み性能が格段に上がるという結果も出ました:
以上の結果を踏まえると、少なくともNode.jsでは、一様に「ハードプライベートは遅い」ということは言えないようです。
ソフトプライベートとハードプライベートの共通点
最後に、ソフトプライベートとハードプライベートの共通点を確認しておきます。
自クラス他オブジェクトへのアクセス可否
ソフトプライベートでは、自分と同じクラスであれば、プライベートフィールドにアクセスできるようになっています:
class Soft {
private id: number
constructor(id: number) {
this.id = id
}
equals(other: Soft): boolean {
return this.id === other.id
// ^^^^^^^^ OK!
}
}
この性質は、ハードプライベートでも同じです:
class Hard {
#id: number
constructor(id: number) {
this.#id = id
}
equals(other: Hard): boolean {
return this.#id === other.#id
// ^^^^^^^^^ OK!
}
}
ソフトプライベートとハードプライベート、どちらを使うべきか?
ソフトプライベートとハードプライベートの違いを見てきましたが、「じゃあ、どっちを使って行ったらいいの?」という疑問があるかと思います。
僕個人の見解を言いたいと思います。
現状は「しばらくはソフトプライベートでいい」と考えています。
その理由としては、
- ソフトプライベートでもカプセル化が困らないレベルで十分に達成できる。
- ハードプライベートをそのまま使える実行環境が整っていない。
- かといって、WeakMap化されたJSコードは、処理速度が早かったり遅かったりして、パフォーマンスが読みにくい。
- 運用時の不確定要素は減らしておきたい。
- かといって、WeakMap化されたJSコードは、処理速度が早かったり遅かったりして、パフォーマンスが読みにくい。
- TypeScriptでもハードプライベートのメソッドがまだ書けない。
というものがあります。
しかし、将来的にはTypeScript固有のソフトプライベートよりも、ECMAScript準拠のハードプライベートのほうにシフトしていったほうがいいとも考えています。それは、
- ハードプライベートは「完全なカプセル化」であり、実行時もプライベートなのが約束される安心感がある。
- ECMAScriptの延長としての言語である「TypeScript」としての思想と一致している。
からです。
いつからハードプライベートを使い始めるかは、なかなか決めにくいですが、おおまかなタイミングとしては
- Firefox、SafariがClass Fieldsに対応する
- TypeScriptがハードプライベートメソッドに対応する
という2つのマイルストーンが達成されてからかと思います。
以上は、執筆時点の見解なので、状況が変わってきたら意思決定も変わってくると思います。
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローしてもらえると嬉しいです→Twitter@suin