TL;DR
- ECMASCriptのMapオブジェクトのgetによる比較操作は、「SameValueZero」で行われる
- 故に、TypeScriptでMapオブジェクトをkeyにクラスを指定して使用する時、Javaのようにequalsメソッドをオーバーライドをしても、Javaのようにvalueを取得することはできない
- keyにはクラスから得られる文字列などを指定して、valueを取得するなどの代替手段を取る必要がある
Map.prototype.get の仕様を正しく理解していなかった
テスト駆動開発を学ぶべく、『テスト駆動開発』の第一部 多国通貨をなぞってました。
すると、Mapを使った為替レートの管理をしているロジックがあったんですね。本書はJavaで書かれていたので、TypeScriptで実装し直してみたら、一点本書と挙動が違う点がありました。
class Bank {
private rates: Map<Pair, number> = new Map();
constructor() {}
public addRate = (from: string, to: string, rate: number): void => {
this.rates.set(new Pair(from, to), rate);
};
public rate = (from: string, to: string): number => {
if (from === to) return 1;
return this.rates.get(new Pair(from, to)) ?? 1;
};
}
class Pair {
constructor(private from: string, private to: string) {
this.from = from;
this.to = to;
}
equals(object: object) {
const pair: Pair = object as Pair;
return this.from === pair.from && this.to === pair.to;
}
}
const bank: Bank = new Bank();
bank.addRate('CHF', 'USD', 2);
console.log(bank.rates('CHF', USD))
書籍では、最後の行のconsole.logで出力される値の期待値が、 2
になる想定でしたが、実際は 1
でした。Bankクラスのrateメソッドでの挙動を確認すると、
return this.rates.get(new Pair(from, to)) ?? 1;
の this.rates.get(new Pair(from, to))
が undefined
でした。
bank.addRate('CHF', 'USD', 2);
でMapに登録して
console.log
Map(1) {
Pair { from: 'CHF', to: 'USD', generateKey: [Function (anonymous)] } => 2
}
上記のようにMapにもちゃんとsetできていることが確認できました。
Map.prototype.get の仕様
Mapにちゃんとkey, valueがsetできているのに、なぜgetできないのか調べると以下の記事を見つけました。
The JavaScript specification says Map uses the SameValueZero operation to determine whether keys are equal to each other
とあるように、Map は SameValueZero という操作で、キーが互いに等しいかどうかを判断します。
SameValueZero とは何かわからなかったので、そのままstack overflowに貼ってあったリンクを踏むとECMAScriptの仕様書にたどり着きました。
上記を読むと、
- Typeが違うとfalseを返す
- Typeがnumberの場合は Number:sameValueZero の比較結果を返す
- Typeがnumber以外の場合は SameValueNonNumberの比較結果を返す
です。であれば、別々のインスタンスを渡す今のロジックでは、当然のようにvalueを取得することはできませんね。
実際に Map.prototype.get にもSameValueZeroを利用すると書いてありました。ちゃんと読めばすぐにわかりましたね。
代替手段を考える
このままだと『テスト駆動開発』の多国通貨開発が進みません。今回の場合は、通貨の単位をPairクラスのインスタンス生成時に受け取るので、その文字列をもとにkeyとなり得る値を生成することで、実装していきたいと思います。
class Bank {
private rates: Map<string, number> = new Map();
constructor() {}
public addRate = (from: string, to: string, rate: number): void => {
this.rates.set(new Pair(from, to).generateKey(), rate);
};
public rate = (from: string, to: string): number => {
if (from === to) return 1;
return this.rates.get(new Pair(from, to).generateKey()) ?? 1;
};
}
class Pair {
constructor(private from: string, private to: string) {
this.from = from;
this.to = to;
}
public generateKey = () => {
return `${this.from}-${this.to}`;
};
}
const bank: Bank = new Bank();
bank.addRate('CHF', 'USD', 2);
console.log(bank.rates('CHF', USD))
Mapのkeyとなり得る型をstringにして、無事テストを通すことができました(equalsメソッドを定義しても意味ないことがわかったので消しちゃいました)