LoginSignup
175
80

More than 1 year has passed since last update.

TypeScript 3.8の新機能「ハードプライベート」と従来の「ソフトプライベート」を比べてみた

Last updated at Posted at 2020-04-28

この投稿は、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と共有する仕様となっています。

TypeScriptにおいては、3.8で導入された仕様です。

TypeScriptコンパイラは、このECMAScript Private Fields構文を、ES2015に対応している環境なら動くようなJSコードにトランスパイルしてくれるので、最新じゃないブラウザでも動かすことができます。ちなみに、JavaScriptとして、Private Fieldsを動かせる環境は、まだ出揃っておらず、FirefoxやSafariでは動きません:

Class_fields_-_JavaScript___MDN.png

なぜ「ソフトプライベート」なんていう新しい用語が必要な事態になったか?

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つに、クラスを継承したときのプロパティ名の重複問題があります。

ソフトプライベートは、親クラスが持っているプライベートフィールドと同じ名前のフィールドを子クラスで定義することができません:

TypeScript__Playground_-_An_online_editor_for_exploring_TypeScript_and_JavaScript.png

一方のハードプライベートは、親クラスと同じフィールド名を使うことができます:

class Parent {
    #a: any
}

class Child extends Parent {
    #a: any // OK!
}

プライベートメソッド

ソフトプライベートは、メソッド定義の構文を用いて、プライベートメソッドが生やせます:

class Soft {
    private doSomething() {}
}

しかし、ハードプライベートは、メソッド定義の構文が使えません:

TypeScript__Playground_-_An_online_editor_for_exploring_TypeScript_and_JavaScript.png

ハードプライベートでも、フィールドに関数を代入することで、プライベートなメソッドを持たせることは可能です:

class Soft {
    private doSomething = function() {}
}

class Hard {
    #doSomething = function() {}
}

ただし、この書き方はメソッド定義構文とは意味が異なり、prototypeのメンバーにはなりません:

esnextでコンパイルした結果
"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__Playground_-_An_online_editor_for_exploring_TypeScript_and_JavaScript.png

コンストラクタのパラメータプロパティ宣言

TypeScriptのコンストラクタの引数には、オブジェクトプロパティを宣言し、引数の値で初期化する「パラメータプロパティ宣言」という機能があります。

どういうものかというと、下記のコードのように引数を受け取り、それをそのままフィールドに代入するようなコードは、

class Soft {
    private foo: any
    constructor(foo: any) {
        this.foo = foo
    }
}

次のように短縮して書くことができるというものです:

class Soft {
    constructor(private foo: any) {}
}

ご覧のとおり、ソフトプライベートはこの「パラメータプロパティ宣言」が可能です。

一方、ハードプライベートのほうは、この短縮形を使うことができません:

TypeScript__Playground_-_An_online_editor_for_exploring_TypeScript_and_JavaScript.png

ハードプライベートは遅い?

「ハードプライベートは処理速度が遅くなる」という説があります。その説を確かめるために、検証してみたいと思います。

ハードプライベート遅い説の論拠

なぜハードプライベートが遅いと言われるのか、その論拠を考えてみたいと思います。

まず、ソフトプライベートとハードプライベートが、どのようにJavaScriptにコンパイルされるか見てみましょう。

class Soft {
    private prop = 'foo'
}

class Hard {
    #prop = 'bar'
}

上のSoftクラスがソフトプライベートで、Hardがハードプライベートです。それぞれコンパイルすると次のようなJavaScriptが生成されます。(ちなみにコンパイルオプションのtargetはES2020です)

JavaScript
"use strict";
class Soft {
    constructor() {
        this.prop = 'foo';
    }
}
JavaScript
"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を見て下さい。)

ここで肝心なことは、ハードプライベートは、自身のプライベートフィールドにアクセスするたびに、WeakMapsetメソッドやgetメソッドを呼び出すということです。this.prop = ...のようなコードと比べたら、「オーバーヘッドがあるだろう」という想像はできるんじゃないかと思います。

要するに、このWeakMap化が「ハードプライベートは遅い」の論拠になるわけです。

※上の例では、ゲッターがないハードプライベートを示しましたが、ゲッターを作るとJavaScriptコードはもっと複雑になります:

TypeScript__Playground_-_An_online_editor_for_exploring_TypeScript_and_JavaScript.png

ハードプライベートは遅いのか実測してみた

WeakMap化が「ハードプライベートを遅くする」というのはあくまで仮説なので、実際に検証してみました。

結論としては、

  • ハードプライベートは、ソフトプライベートに比べて、書き込みアクセス(set)は遅い
  • ハードプライベートは、ソフトプライベートに比べて、読み込みアクセス(get)はむしろ速い

という不思議な結果になりました。

Pasted_Image_2020_04_28_11_08.png

この検証の実行環境はNode.js 13なので、ブラウザでどうなるかは調べてません。検証に使ったコードはGitHubで公開しているので、試したい方はどうぞ。

なお、Node.js 12からは、ハードプライベートもそのまま(WeakMap化することなく)実行できるので、TypeScriptのJavaScriptターゲットをesnextにして、WeakMap化ありのes2020コードと、WeakMap化なしのesnextコードの速度比較をしたところ、ハードプライベートのほうが読み込み性能が格段に上がるという結果も出ました:

Pasted_Image_2020_04_28_11_15.png

以上の結果を踏まえると、少なくとも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コードは、処理速度が早かったり遅かったりして、パフォーマンスが読みにくい。
      • 運用時の不確定要素は減らしておきたい。
  • TypeScriptでもハードプライベートのメソッドがまだ書けない。

というものがあります。

しかし、将来的にはTypeScript固有のソフトプライベートよりも、ECMAScript準拠のハードプライベートのほうにシフトしていったほうがいいとも考えています。それは、

  • ハードプライベートは「完全なカプセル化」であり、実行時もプライベートなのが約束される安心感がある。
  • ECMAScriptの延長としての言語である「TypeScript」としての思想と一致している。

からです。

いつからハードプライベートを使い始めるかは、なかなか決めにくいですが、おおまかなタイミングとしては

  • Firefox、SafariがClass Fieldsに対応する
  • TypeScriptがハードプライベートメソッドに対応する

という2つのマイルストーンが達成されてからかと思います。

以上は、執筆時点の見解なので、状況が変わってきたら意思決定も変わってくると思います。


最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローしてもらえると嬉しいです:relieved:Twitter@suin

175
80
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
175
80