Como personalizar a igualdade de objetos para o conjunto JavaScript


167

O novo ES 6 (Harmony) apresenta o novo objeto Set . O algoritmo de identidade usado pelo Set é semelhante ao ===operador e, portanto, não é muito adequado para comparar objetos:

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

Como personalizar a igualdade dos objetos Set para fazer uma comparação profunda dos objetos? Existe algo como Java equals(Object)?


3
O que você quer dizer com "personalizar a igualdade"? Javascript não permite sobrecarga do operador, portanto não há como sobrecarregar o ===operador. O objeto do conjunto ES6 não possui nenhum método de comparação. O .has()método e o .add()método funcionam apenas com o mesmo objeto real ou o mesmo valor para uma primitiva.
usar o seguinte código

12
Por "customizar a igualdade", quero dizer como o desenvolvedor pode definir certos objetos para serem considerados iguais ou não.
Czerny

Respostas:


107

O Setobjeto ES6 não possui nenhum método de comparação ou extensibilidade de comparação personalizada.

Os métodos .has(), .add()e .delete()funcionam apenas com o mesmo objeto real ou o mesmo valor de uma primitiva e não têm como conectar ou substituir apenas essa lógica.

Presumivelmente, você poderia derivar seu próprio objeto de Setae replace .has(), .add()e .delete()métodos por algo que fez uma comparação profunda de objetos primeiro para descobrir se o item já está no Conjunto, mas o desempenho provavelmente não seria bom, pois o Setobjeto subjacente não ajudaria em absoluto. Você provavelmente precisaria fazer uma iteração de força bruta em todos os objetos existentes para encontrar uma correspondência usando sua própria comparação personalizada antes de chamar o original .add().

Aqui estão algumas informações deste artigo e discussão dos recursos do ES6:

5.2 Por que não consigo configurar como mapas e conjuntos comparam chaves e valores?

Pergunta: Seria bom se houvesse uma maneira de configurar quais chaves de mapa e quais elementos de conjunto são considerados iguais. Por que não existe?

Resposta: Esse recurso foi adiado, pois é difícil de implementar de maneira adequada e eficiente. Uma opção é entregar retornos de chamada para coleções que especificam igualdade.

Outra opção, disponível em Java, é especificar a igualdade por meio de um método que o objeto implementa (equals () em Java). No entanto, essa abordagem é problemática para objetos mutáveis: em geral, se um objeto é alterado, sua “localização” dentro de uma coleção também precisa ser alterada. Mas não é isso que acontece em Java. O JavaScript provavelmente seguirá a rota mais segura, permitindo apenas a comparação por valor de objetos imutáveis ​​especiais (os chamados objetos de valor). Comparação por valor significa que dois valores são considerados iguais se seu conteúdo for igual. Valores primitivos são comparados por valor em JavaScript.


4
Referência de artigo adicionada sobre esse problema específico. Parece que o desafio é como lidar com um objeto que era exatamente igual a outro no momento em que foi adicionado ao conjunto, mas agora foi alterado e não é mais o mesmo que esse objeto. Está dentro Setou não?
usar o seguinte comando

3
Por que não implementar um GetHashCode simples ou similar?
Jamby

@ Jamby - Esse seria um projeto interessante para criar um hash que lida com todos os tipos de propriedades e propriedades de hashes na ordem correta e lida com referências circulares e assim por diante.
precisa saber é o seguinte

1
@ Jamby Mesmo com uma função de hash, você ainda precisa lidar com colisões. Você está apenas adiando o problema de igualdade.
MPEN

5
@mpen Isso não está certo, estou permitindo que o desenvolvedor gerencie sua própria função de hash para suas classes específicas, o que em quase todos os casos evita o problema de colisão, pois o desenvolvedor conhece a natureza dos objetos e pode obter uma boa chave. Em qualquer outro caso, use o método de comparação atual. Lote de línguas fazem isso, js não.
Jamby

28

Como mencionado na resposta de jfriend00, a personalização da relação de igualdade provavelmente não é possível .

O código a seguir apresenta um esboço da solução alternativa computacionalmente eficiente (mas com memória dispendiosa) :

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());
    }

    // ...
}

Cada elemento inserido deve implementar o toIdString()método que retorna string. Dois objetos são considerados iguais se e somente se seus toIdStringmétodos retornarem o mesmo valor.


Você também pode fazer com que o construtor assuma uma função que compara itens para igualdade. Isso é bom se você deseja que essa igualdade seja um recurso do conjunto, e não dos objetos usados ​​nele.
Ben J

1
@BenJ O objetivo de gerar uma string e colocá-la em um mapa é que, dessa maneira, seu mecanismo Javascript usará uma pesquisa ~ O (1) no código nativo para pesquisar o valor de hash do seu objeto, enquanto aceitar uma função de igualdade forçaria para fazer uma varredura linear do conjunto e verificar todos os elementos.
Jamby

3
Um desafio com esse método é que eu acho que assume que o valor de item.toIdString()é invariável e não pode mudar. Porque se puder, GeneralSetpode facilmente tornar-se inválido com itens "duplicados". Portanto, uma solução como essa seria restrita a apenas certas situações prováveis ​​em que os objetos em si não são alterados durante o uso do conjunto ou onde um conjunto que se torna inválido não tem conseqüência. Todos esses problemas provavelmente explicam melhor por que o ES6 Set não expõe essa funcionalidade porque realmente funciona apenas em determinadas circunstâncias.
precisa saber é o seguinte

É possível adicionar a implementação correta de .delete()para esta resposta?
precisa saber é o seguinte

1
@JLewkovich sure
czerny 14/01/19

6

Como a resposta principal menciona, personalizar a igualdade é problemático para objetos mutáveis. A boa notícia é (e estou surpreso que ninguém tenha mencionado isso ainda) que existe uma biblioteca muito popular chamada immutable-js que fornece um rico conjunto de tipos imutáveis ​​que fornecem a profunda semântica de igualdade de valor que você está procurando.

Aqui está o seu exemplo usando 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
Como o desempenho do conjunto / mapa immutable-js se compara ao conjunto / mapa nativo?
frankster

5

Para adicionar as respostas aqui, fui adiante e implementei um invólucro de mapa que usa uma função de hash personalizada, uma função de igualdade personalizada e armazena valores distintos que possuem hashes (personalizados) equivalentes em buckets.

Previsivelmente, acabou sendo mais lento que o método de concatenação de strings do czerny .

Fonte completa aqui: https://github.com/makoConstruct/ValueMap


"Concatenação de strings"? O método dele não é mais como "substituto das cordas" (se você vai dar um nome a ele)? Ou existe um motivo para você usar a palavra "concatenação"? Estou curioso ;-)
binki

@binki Esta é uma boa pergunta e acho que a resposta traz um bom ponto que demorei um pouco para entender. Normalmente, ao calcular um código de hash, é feito algo como HashCodeBuilder, que multiplica os códigos de hash de campos individuais e não é garantido que seja exclusivo (daí a necessidade de uma função de igualdade personalizada). No entanto, ao gerar uma string id você concatenar as cordas ID de campos individuais que é garantido para ser único (e, portanto, nenhuma função igualdade necessários)
Pace

Então, se você tem um Pointdefinido como o { x: number, y: number }seu id stringprovavelmente é x.toString() + ',' + y.toString().
Pace

Fazer sua comparação de igualdade criar algum valor que é garantido variar apenas quando as coisas devem ser consideradas diferentes é uma estratégia que eu já usei antes. Às vezes é mais fácil pensar sobre as coisas dessa maneira. Nesse caso, você está gerando chaves em vez de hashes . Contanto que você tenha um derivador de chave que emita uma chave de uma forma que as ferramentas existentes suportem com igualdade de estilo de valor, que quase sempre acaba sendo String, então você pode pular toda a etapa de hash e balde como você disse e usar apenas diretamente um Mapou mesmo objeto simples de estilo antigo em termos da chave derivada.
binki

1
Uma coisa a ter cuidado se você realmente usar concatenação de cadeia na implementação de um derivador de chave é que as propriedades da cadeia talvez precisem ser tratadas de maneira especial se tiverem permissão para assumir qualquer valor. Por exemplo, se você tiver {x: '1,2', y: '3'}e {x: '1', y: '2,3'}, em seguida String(x) + ',' + String(y), produzirá o mesmo valor para os dois objetos. Uma opção mais segura, assumindo que você pode JSON.stringify()ser determinista, é tirar proveito do escape de sua string e usá-la JSON.stringify([x, y]).
binki

3

Compará-los diretamente parece não ser possível, mas o JSON.stringify funciona se as chaves foram classificadas. Como apontei em um comentário

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

Mas podemos contornar isso com um método stringify personalizado. Primeiro, escrevemos o método

Stringify personalizado

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);
}

O conjunto

Agora usamos um conjunto. Mas usamos um conjunto de seqüências de caracteres em vez de objetos

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

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

Obter todos os valores

Depois de criarmos o conjunto e adicionarmos os valores, podemos obter todos os valores por

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

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

Aqui está um link com tudo em um arquivo http://tpcg.io/FnJg2i


"se as chaves são classificadas" é um grande se, especialmente para objetos complexos
Alexander Mills

é exatamente por isso que eu escolhi esta abordagem;)
relief.melone

2

Talvez você possa tentar usar JSON.stringify() para fazer uma comparação profunda de objetos.

por exemplo :

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
Isso pode funcionar, mas não precisa ser JSON.stringify ({a: 1, b: 2})! == JSON.stringify ({b: 2, a: 1}) Se todos os objetos forem criados pelo seu programa no mesmo ordem você está seguro. Mas não é uma solução realmente seguro em geral
relief.melone

1
Ah sim, "converta-o em uma string". Resposta do Javascript para tudo.
Timmmm

2

Para usuários do Typecript, as respostas de outras pessoas (especialmente do czerny ) podem ser generalizadas para uma boa classe base segura e reutilizável:

/**
 * 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;
}

A implementação de exemplo é assim simples: basta substituir o stringifyKeymétodo. No meu caso, eu especifico alguma uripropriedade.

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

O uso de exemplo é como se isso fosse regular 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

Crie um novo conjunto a partir da combinação dos dois conjuntos e compare o comprimento.

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 é igual a set2 = true

set1 é igual a set4 = false


2
Esta resposta está relacionada à pergunta? A questão é sobre igualdade de itens em relação a uma instância da classe Set. Esta pergunta parece discutir a igualdade de duas instâncias de conjunto.
Czerny

@czerny você está correto - i foi originalmente vendo esta pergunta stackoverflow, onde o método acima poderia ser usado: stackoverflow.com/questions/6229197/...
Stefan Musarra

-2

Para alguém que encontrou essa pergunta no Google (como eu) que deseja obter o valor de um mapa usando um objeto como Chave:

Atenção: esta resposta não funcionará com todos os objetos

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");

Resultado:

Trabalhou

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.