JavaScriptセットのオブジェクト等価性をカスタマイズする方法


167

新しいES 6(ハーモニー)は新しい Setオブジェクトがます。Setで使用されるIDアルゴリズムは===演算子に似ているため、オブジェクトの比較にはあまり適していません。

var set = new Set();
set.add({a:1});
set.add({a:1});
console.log([...set.values()]); // Array [ Object, Object ]

深いオブジェクト比較を行うためにSetオブジェクトの等価性をカスタマイズする方法は?Javaのようなものはありますかequals(Object)か?


3
「平等をカスタマイズする」とはどういう意味ですか?JavaScriptは演算子のオーバーロードを許可しないため、演算子をオーバーロードする方法はありません===。ES6セットオブジェクトには、compareメソッドはありません。.has()方法と.add()だけオフ方法作業がプリミティブに対して同じ実際のオブジェクトまたは同じ値です。
jfriend00

12
「同等性のカスタマイズ」とは、開発者が特定のいくつかのオブジェクトを同等と見なすかどうかを定義する方法を意味します。
czerny 2015

回答:


107

ES6 Setオブジェクトには、compareメソッドやカスタム比較拡張機能はありません。

.has().add()および.delete()方法は、プリミティブのために同じ実際のオブジェクトまたは同じ値であるだけでオフに動作し、ちょうどそのロジックを差し込むまたは交換する手段を持っていません。

おそらく、から独自のオブジェクトを派生させ、とメソッドを最初にディープオブジェクト比較を行ってアイテムがセット内にあるかどうかを確認する何かでメソッドをSet置き換えることができますが、基礎となるオブジェクトが役に立たないため、パフォーマンスはおそらく良くありませんまったく。オリジナルを呼び出す前に、独自のカスタム比較を使用して一致を見つけるために、既存のすべてのオブジェクトに対してブルートフォースの反復を実行する必要があるでしょう.has().add().delete()Set.add()

この記事の情報とES6機能の説明を以下に示します。

5.2マップとセットがキーと値を比較する方法を設定できないのはなぜですか?

質問:どのマップキーとどのセット要素が等しいと見なされるかを構成する方法があればいいでしょう。なぜそこにないのですか?

回答:この機能は、適切かつ効率的に実装することが難しいため、延期されました。1つのオプションは、同等性を指定するコレクションにコールバックを渡すことです。

Javaで利用可能なもう1つのオプションは、オブジェクトが実装するメソッド(Javaの場合はequals())を使用して等価性を指定することです。ただし、このアプローチは変更可能なオブジェクトには問題があります。一般に、オブジェクトが変更されると、コレクション内の「場所」も変更される必要があります。しかし、それはJavaでは起こりません。JavaScriptはおそらく、特別な不変オブジェクト(いわゆる値オブジェクト)の値による比較のみを有効にするというより安全な方法になります。値による比較とは、2つの値の内容が等しい場合、それらは等しいと見なされることを意味します。プリミティブ値は、JavaScriptの値によって比較されます。


4
この特定の問題に関する記事の参照を追加しました。問題は、セットに追加されたときに別のオブジェクトとまったく同じであったオブジェクトをどのように処理するかであるように見えますが、現在は変更されており、そのオブジェクトと同じではありません。にあるSetかどうか?
jfriend00

3
単純なGetHashCodeなどを実装しないのはなぜですか?
ジャンビー2016

@Jamby-これは、すべてのタイプのプロパティを処理し、正しい順序でプロパティをハッシュし、循環参照などを処理するハッシュを作成する興味深いプロジェクトです。
jfriend00 2016

1
@Jambyハッシュ関数を使用しても、衝突に対処する必要があります。平等の問題を先送りしているだけです。
mpen 2017

5
@mpen不正解です。開発者が特定のクラスの独自のハッシュ関数を管理できるようにしています。これにより、ほとんどすべての場合で、衝突の問題が回避されます。それ以外の場合は、現在の比較方法にフォールバックします。多く 言語は すでにそうしてますが、そうではありません。
ジャンビー2017

28

jfriend00の回答で述べたように、等式関係のカスタマイズはおそらく不可能です。

次のコードは、計算効率の高い(ただし、メモリを消費する)回避策の概要を示しています。

class GeneralSet {

    constructor() {
        this.map = new Map();
        this[Symbol.iterator] = this.values;
    }

    add(item) {
        this.map.set(item.toIdString(), item);
    }

    values() {
        return this.map.values();
    }

    delete(item) {
        return this.map.delete(item.toIdString());
    }

    // ...
}

挿入された各要素は、toIdString()文字列を返すメソッドを実装する必要があります。2つのオブジェクトが等しいと見なされるのは、それらのtoIdStringメソッドが同じ値を返す場合のみです。


コンストラクタに、アイテムが等しいかどうかを比較する関数を実行させることもできます。これは、この等式を、セットで使用されるオブジェクトではなく、セットの機能にしたい場合に適しています。
ベンJ

1
@BenJ文字列を生成してマップに配置するポイントは、JavaScriptエンジンがオブジェクトのハッシュ値を検索するためにネイティブコードで〜O(1)検索を使用する一方で、等式関数を受け入れると強制されることです。セットの線形スキャンを行い、すべての要素をチェックします。
ジャンビー2016

3
この方法の1つの課題は、の値item.toIdString()が不変であり、変更できないと想定していることです。可能であればGeneralSet、「重複」アイテムが含まれていると、簡単に無効になる可能性があるためです。したがって、そのような解決策は、セットの使用中にオブジェクト自体が変更されない、またはセットが無効になることが重要ではない特定の状況のみに制限されます。これらの問題はすべて、ES6セットが実際に特定の状況でのみ機能するためにこの機能が公開されない理由をさらに説明しています。
jfriend00

.delete()この回答に正しい実装を追加することは可能ですか?
jlewkovich

1
@JLewkovich確認
czerny

6

トップの答えは言及し、カスタマイズの平等は、変更可能なオブジェクトのための問題があります。朗報です(そして、まだ誰もこれについて言及していないことに驚いています)。探している深い値の等価のセマンティクスを提供する不変の型の豊富なセットを提供するimmutable-jsと呼ばれる非常に人気のあるライブラリがあります。

次に、immutable-jsを使用した例を示します。

const { Map, Set } = require('immutable');
var set = new Set();
set = set.add(Map({a:1}));
set = set.add(Map({a:1}));
console.log([...set.values()]); // [Map {"a" => 1}]

10
immutable-jsセット/マップのパフォーマンスは、ネイティブセット/マップとどのように比較されますか?
フランクスター

5

ここで答えを追加するために、カスタムハッシュ関数、カスタム等価関数を取り、バケットに同等の(カスタム)ハッシュを持つ個別の値を格納するMapラッパーを実装しました。

予想通り、czernyの文字列連結方式より遅いことわかりました

ここで完全なソース:https : //github.com/makoConstruct/ValueMap


「文字列連結」?彼の方法は「文字列代理」のようなものではありませんか(名前を付ける場合)。それとも「連結」という言葉を使う理由はありますか?気になる;-)
binki 2017年

@binkiこれはいい質問で、答えを理解するのに少し時間がかかったのは良い点だと思います。通常、ハッシュコードを計算するときは、個々のフィールドのハッシュコードを乗算するHashCodeBuilderのような処理を行い、一意であることが保証されていません(そのため、カスタムの等値関数が必要です)。ただし、id文字列を生成するときは、一意であることが保証されている個々のフィールドのid文字列を連結します(したがって、等価関数は必要ありません)
Pace

あなたがしているのであればPointと定義し{ x: number, y: number }、その後、あなたid stringはおそらくですx.toString() + ',' + y.toString()
2018年

等値比較を行うことで、不等であると考える必要がある場合にのみ変化することが保証された値を構築します。これは、以前に使用した戦略です。時々そのように物事を考える方が簡単です。その場合、ハッシュではなくキーを生成します。既存のツールが値スタイルの同等性でサポートする形式でキーを出力するキーデリバターがある限り、ほとんどの場合はString最終的にになるため、前述のようにハッシュとバケットの手順全体をスキップして、直接Mapまたは派生キーの観点から見た古いスタイルのプレーンオブジェクト。
binki 2018年

1
キーデリバターの実装で実際に文字列連結を使用する場合に注意すべきことの1つは、文字列プロパティが任意の値を取ることが許可されている場合、文字列プロパティを特別に扱う必要がある場合があることです。たとえば、{x: '1,2', y: '3'}およびがある{x: '1', y: '2,3'}場合、String(x) + ',' + String(y)は両方のオブジェクトに同じ値を出力します。JSON.stringify()確定的であると期待できる場合のより安全なオプションは、文字列のエスケープを利用してJSON.stringify([x, y])代わりに使用することです。
binki 2018年

3

それらを直接比較することは不可能のようですが、JSON.stringifyは、キーがソートされただけの場合に機能します。コメントで指摘したように

JSON.stringify({a:1、b:2})!== JSON.stringify({b:2、a:1});

しかし、カスタムの文字列化メソッドでそれを回避することができます。最初にメソッドを書きます

カスタム文字列化

Object.prototype.stringifySorted = function(){
    let oldObj = this;
    let obj = (oldObj.length || oldObj.length === 0) ? [] : {};
    for (let key of Object.keys(this).sort((a, b) => a.localeCompare(b))) {
        let type = typeof (oldObj[key])
        if (type === 'object') {
            obj[key] = oldObj[key].stringifySorted();
        } else {
            obj[key] = oldObj[key];
        }
    }
    return JSON.stringify(obj);
}

セット

次に、セットを使用します。しかし、オブジェクトの代わりに文字列のセットを使用します

let set = new Set()
set.add({a:1, b:2}.stringifySorted());

set.has({b:2, a:1}.stringifySorted());
// returns true

すべての値を取得する

セットを作成して値を追加したら、次の方法ですべての値を取得できます

let iterator = set.values();
let done = false;
while (!done) {
  let val = iterator.next();

  if (!done) {
    console.log(val.value);
  }
  done = val.done;
}

これはすべてが1つのファイルにあるリンクです http://tpcg.io/FnJg2i


特にキーがソートされている場合、特に複雑なオブジェクトの場合は重要です
Alexander Mills

それこそが、私がこのアプローチを選択した理由です;)
安心

2

多分あなたはを使っJSON.stringify()て深いオブジェクト比較をすることを試みることができます。

例えば ​​:

const arr = [
  {name:'a', value:10},
  {name:'a', value:20},
  {name:'a', value:20},
  {name:'b', value:30},
  {name:'b', value:40},
  {name:'b', value:40}
];

const names = new Set();
const result = arr.filter(item => !names.has(JSON.stringify(item)) ? names.add(JSON.stringify(item)) : false);

console.log(result);


2
これは機能しますが、JSON.stringify({a:1、b:2})である必要はありません。あなたが安全だと注文します。一般的にではなく、本当に安全ソリューション
relief.melone

1
ああ、「文字列に変換する」。すべてに対するJavascriptの答え。
Timmmm

2

Typescriptユーザーの場合、他の人(特にczerny)の回答は、タイプセーフで再利用可能な基本クラスに一般化できます。

/**
 * Map that stringifies the key objects in order to leverage
 * the javascript native Map and preserve key uniqueness.
 */
abstract class StringifyingMap<K, V> {
    private map = new Map<string, V>();
    private keyMap = new Map<string, K>();

    has(key: K): boolean {
        let keyString = this.stringifyKey(key);
        return this.map.has(keyString);
    }
    get(key: K): V {
        let keyString = this.stringifyKey(key);
        return this.map.get(keyString);
    }
    set(key: K, value: V): StringifyingMap<K, V> {
        let keyString = this.stringifyKey(key);
        this.map.set(keyString, value);
        this.keyMap.set(keyString, key);
        return this;
    }

    /**
     * Puts new key/value if key is absent.
     * @param key key
     * @param defaultValue default value factory
     */
    putIfAbsent(key: K, defaultValue: () => V): boolean {
        if (!this.has(key)) {
            let value = defaultValue();
            this.set(key, value);
            return true;
        }
        return false;
    }

    keys(): IterableIterator<K> {
        return this.keyMap.values();
    }

    keyList(): K[] {
        return [...this.keys()];
    }

    delete(key: K): boolean {
        let keyString = this.stringifyKey(key);
        let flag = this.map.delete(keyString);
        this.keyMap.delete(keyString);
        return flag;
    }

    clear(): void {
        this.map.clear();
        this.keyMap.clear();
    }

    size(): number {
        return this.map.size;
    }

    /**
     * Turns the `key` object to a primitive `string` for the underlying `Map`
     * @param key key to be stringified
     */
    protected abstract stringifyKey(key: K): string;
}

実装例はこのように単純ですstringifyKey。メソッドをオーバーライドするだけです。私の場合、いくつかのuriプロパティを文字列化します。

class MyMap extends StringifyingMap<MyKey, MyValue> {
    protected stringifyKey(key: MyKey): string {
        return key.uri.toString();
    }
}

使用例は、これが通常の場合と同じですMap<K, V>

const key1 = new MyKey(1);
const value1 = new MyValue(1);
const value2 = new MyValue(2);

const myMap = new MyMap();
myMap.set(key1, value1);
myMap.set(key1, value2); // native Map would put another key/value pair

myMap.size(); // returns 1, not 2

-1

両方のセットの組み合わせから新しいセットを作成し、長さを比較します。

let set1 = new Set([1, 2, 'a', 'b'])
let set2 = new Set([1, 'a', 'a', 2, 'b'])
let set4 = new Set([1, 2, 'a'])

function areSetsEqual(set1, set2) {
  const set3 = new Set([...set1], [...set2])
  return set3.size === set1.size && set3.size === set2.size
}

console.log('set1 equals set2 =', areSetsEqual(set1, set2))
console.log('set1 equals set4 =', areSetsEqual(set1, set4))

set1はset2 = trueと等しい

set1はset4 = falseと等しい


2
この回答は質問に関連していますか?問題は、Setクラスのインスタンスに関するアイテムの同等性についてです。この質問では、2つのSetインスタンスの同等性について説明しているようです。
czerny

@czernyあなたは正しい-私は最初にこのスタックオーバーフローの質問を見ていたが、上記の方法を使用することができた: stackoverflow.com/questions/6229197/...
ステファンMusarra

-2

Googleでこの質問を見つけた人(私として)が、オブジェクトをキーとして使用してマップの値を取得したい場合:

警告:この回答はすべてのオブジェクトで機能するわけではありません

var map = new Map<string,string>();

map.set(JSON.stringify({"A":2} /*string of object as key*/), "Worked");

console.log(map.get(JSON.stringify({"A":2}))||"Not worked");

出力:

働いた

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.