“ Set”是否应具有Get方法?


22

让我们拥有这个C#类(在Java中几乎相同)

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}

   public override bool Equals(object obj) {
        var item = obj as MyClass;

        if (item == null || this.A == null || item.A == null)
        {
            return false;
        }
        return this.A.equals(item.A);
   }

   public override int GetHashCode() {
        return A != null ? A.GetHashCode() : 0;
   }
}

如您所见,两个实例的相等性仅MyClass取决于A。因此,可以存在两个相等的实例,但它们的B属性持有不同的信息。

在多种语言(当然包括C#和Java)的标准集合库中,有一个SetHashSet在C#中),该集合可以容纳每组相等的实例中的最多一项。

可以添加项目,删除项目并检查集合中是否包含项目。但是,为什么不可能从集合中获得特定的物品呢?

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

//I can do this
if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) {
    //something
}

//But I cannot do this, because Get does not exist!!!
MyClass item = mset.Get(new MyClass {A = "Hello", B = "See you"});
Console.WriteLine(item.B); //should print Bye

检索我的物品的唯一方法是遍历整个集合并检查所有物品是否相等。但是,这需要O(n)时间而不是O(1)

到目前为止,我还没有发现任何支持get的语言。我所知道的所有“通用”语言(Java,C#,Python,Scala,Haskell ...)似乎都是以相同的方式设计的:可以添加项目,但不能检索它们。是否有充分的理由为什么所有这些语言都不支持某些简单且显然有用的语言?他们不可能全都错,对吗?是否有任何语言支持它?也许从集合中取出特定项目是错误的,但是为什么呢?


有一些相关的SO问题:

/programming/7283338/getting-an-element-from-a-set

/programming/7760364/how-to-retrieve-actual-item-from-hashsett


12
C ++ std::set支持对象检索,因此并非所有“通用”语言都像您描述的那样。
恢复莫妮卡

17
如果您声称(和编码)“ MyClass的两个实例的相等性仅取决于A”,则另一个具有相同A值和不同B的实例就是 “那个特定实例”,因为您自己定义了它们相等,并且B中的差异无关紧要;因为它是相等的,所以“允许”容器返回另一个实例。
彼得斯(Peteris)

7
真实的故事:在Java中,许多Set<E>实现都Map<E,Boolean>在内部。
corsiKa

10
对A人讲话:“嗨,您能带A人过来吗?”
Brad Thomas

7
a == b万一破坏了反射性(始终为真)this.A == null。该if (item == null || this.A == null || item.A == null)测试是“过头”,并检查多,可能为了创建人为“高质量”的代码。我看到这种“过度检查”,并且在代码审查中一直过分正确。
usr

Answers:


66

这里的问题不是HashSet缺少一种Get方法,而是从HashSet类型的角度来看您的代码没有意义。

Get方法实际上是“请给我这个值,<confused face />”。

如果要存储项目,然后根据与另一个稍有不同的值进行匹配来检索它们,请使用Dictionary<String, MyClass>,然后执行以下操作:

var mset = new Dictionary<String, MyClass>();
mset.Add("Hello", new MyClass {A = "Hello", B = "Bye"});

var item = mset["Hello"];
Console.WriteLine(item.B); // will print Bye

相等信息从封装的类中泄漏。如果要更改涉及的属性集,则Equals必须在外部更改代码MyClass

好吧,是的,但这是因为MyClass以最小惊讶原则(POLA)横行。封装了该相等性功能后,完全有理由假定以下代码有效:

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) 
{
    // this code is unreachable.
}

为避免这种情况,MyClass需要清楚记录其奇特的平等形式。这样做之后,它不再被封装,改变平等的工作方式将破坏开放/封闭的原则。因此,它不应该更改,因此Dictionary<String, MyClass>对于这种奇怪的要求是一个很好的解决方案。


2
@vojta,在这种情况下,请使用,Dictionary<MyClass, MyClass>因为它将基于使用的键来获取值MyClass.Equals
David Arno

8
我将使用Dictionary<MyClass, MyClass>带有适当值的提供者IEqualityComparer<MyClass>,并从MyClass为什么MyClass必须在实例上了解该关系来退出等价关系?
卡莱斯(Caleth)'16

16
@vojta以及那里的注释:“ 嗯。重写equals的实现,使非相等对象成为“ equal”是这里的问题。要求一种方法,说“让我获得与此对象相同的对象”,然后期望返回一个不相同的对象似乎很疯狂,并且容易引起维护问题 ”。这通常是SO的问题:严重的错误答案被那些没有考虑过他们想要快速修复其破损代码的隐含想法的人推崇……
David Arno

6
@DavidArno:这是不可避免的,尽管只要我们坚持使用区分相等和同一性的语言即可;-)如果您想规范化相等但不相同的对象,则需要一种方法,该方法说“不能让我得到相同的对象”),但“让我获得与此对象相等的规范对象”。任何认为使用这些语言的HashSet.Get的人都必然意味着“给我相同的对象”已经严重错误。
Steve Jessop

4
这个答案有很多笼统的陈述,例如...reasonable to assume...。在99%的情况下,所有这些可能都是正确的,但仍然可以从集中检索项目的功能变得很方便。现实世界的代码不能总是遵守POLA等原则。例如,如果不区分大小写地对字符串进行重复数据删除,则可能需要获取“ master”项。Dictionary<string, string>是一种解决方法,但它需要perf。
usr

24

您已经有“位于”集合中的项目了-您将其作为密钥传递了。

“但是,这不是我叫Add with的实例”-是的,但是您明确声称它们是相等的。

A Set也是Map| 的特例。Dictionary,以void作为值类型(虽然没有定义无用的方法,但这无关紧要)。

您正在查找的数据结构是Dictionary<X, MyClass>其中X某种程度上得到作为出MyClasses的。

在这方面,C#字典类型很不错,因为它允许您为密钥提供IEqualityComparer。

对于给出的示例,我将具有以下内容:

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}
}

public class MyClassEquivalentAs : IEqualityComparer<MyClass>{
   public override bool Equals(MyClass left, MyClass right) {
        if (Object.ReferenceEquals(left, null) && Object.ReferenceEquals(right, null))
        {
            return true;
        }
        else if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
        {
            return false;
        }
        return left.A == right.A;
   }

   public override int GetHashCode(MyClass obj) {
        return obj?.A != null ? obj.A.GetHashCode() : 0;
   }
}

因此使用:

var mset = new Dictionary<MyClass, MyClass>(new MyClassEquivalentAs());
var bye = new MyClass {A = "Hello", B = "Bye"};
var seeyou = new MyClass {A = "Hello", B = "See you"};
mset.Add(bye);

if (mset.Contains(seeyou)) {
    //something
}

MyClass item = mset[seeyou];
Console.WriteLine(item.B); // prints Bye

在许多情况下,具有与密钥匹配的对象的代码将其替换为对用作密钥的对象的引用可能是有利的。例如,如果已知许多字符串与哈希集合中的某个字符串匹配,那么将对所有这些字符串的引用替换为对集合中一个字符串的引用可能是性能上的胜利。
超级猫

@supercat今天通过可以实现Dictionary<String, String>
MikeFHay

@MikeFHay:是的,但是必须将每个字符串引用存储两次似乎有点不雅致。
超级猫

2
@supercat如果您指的是相同的字符串,那只是字符串内部。使用内置的东西。如果您指的是某种“规范”表示(使用简单的大小写更改技术等无法实现的表示),这听起来像您基本上需要一个索引(在某种意义上,DB使用该术语)。我没有将每个“非规范形式”存储为映射到规范形式的键的问题。(如果“规范”形式不是字符串,我认为这同样适用。)如果这不是您在说的,那么您就完全迷失了我。
jpmc26 2016年

1
自定义ComparerDictionary<MyClass, MyClass>是一个务实的解决方案。在Java中,可以通过custom TreeSetTreeMapplus 实现相同的功能Comparator
马库斯·库尔

19

您的问题是您有两个矛盾的平等概念:

  • 所有字段均相等的实际相等
  • 设置成员资格相等,其中只有A等于

如果您要在集合中使用实际的相等关系,则不会出现从集合中检索特定项目的问题-检查对象是否在集合中,您已经有了该对象。因此,假设您使用的是正确的相等关系,则永远不必从集中检索特定实例。

我们还可以争辩说,集合是一种纯粹由or 关系(“特征函数”)定义的抽象数据类型。如果要执行其他操作,则实际上并不需要查找集合。S contains xx is-element-of S

经常发生(但不是集合)的情况是,我们将所有对象归为不同的对等类。每个此类或子集中的对象仅是等效的,而不是相等的。我们可以通过该子集的任何成员表示每个等价类,然后变得需要检索该表示元素。这将是从等价类到代表性元素的映射

我认为,在C#中,字典可以使用显式的相等关系。否则,可以通过编写快速包装器类来实现这种关系。伪代码:

// The type you actually want to store
class MyClass { ... }

// A equivalence class of MyClass objects,
// with regards to a particular equivalence relation.
// This relation is implemented in EquivalenceClass.Equals()
class EquivalenceClass {
  public MyClass instance { get; }
  public override bool Equals(object o) { ... } // compare instance.A
  public override int GetHashCode() { ... } // hash instance.A
  public static EquivalenceClass of(MyClass o) { return new EquivalenceClass { instance = o }; }
}

// The set-like object mapping equivalence classes
// to a particular representing element.
class EquivalenceHashSet {
  private Dictionary<EquivalenceClass, MyClass> dict = ...;
  public void Add(MyClass o) { dict.Add(EquivalenceClass.of(o), o)}
  public bool Contains(MyClass o) { return dict.Contains(EquivalenceClass.of(o)); }
  public MyClass Get(MyClass o) { return dict.Get(EquivalenceClass.of(o)); }
}

“从集合中检索特定实例”,如果您将“实例”更改为“成员”,我认为这将更直接地传达您的意思。只是一个小建议。=)+1
jpmc26 2016年

7

但是,为什么不可能从集合中获得特定的物品呢?

因为那不是集合的目的。

让我改一下例子。

“我有一个HashSet,我想将MyClass对象存储在其中,并且我希望能够通过使用等于对象的属性A的属性A来获取它们。”

如果将“ HashSet”替换为“ Collection”,将“ objects”替换为“ Values”,将“ Property A”替换为“ Key”,则该句子将变为:

“我有一个要存储MyClass值的Collection,并且我希望能够通过使用与对象的Key相等的Key来获取它们。”

正在描述的是字典。被问到的实际问题是“为什么不能将HashSet视为字典?”

答案是它们不用于同一件事。使用集合的原因是为了保证其单个内容的唯一性,否则您可以只使用列表或数组。问题中描述的行为是字典的作用。所有的语言设计师并没有搞砸。它们不提供get方法,因为如果您有对象并且它在集合中,则它们是等效的,这意味着您将“获取”等效的对象。当语言提供其他允许您执行此操作的数据结构时,争论说HashSet应该以可以“获取”已定义为相等的非等效对象的方式实现,这是一个不起眼的事情。

关于OOP和平等评论/答案的注释。可以将映射的键设置为Dictionary中的属性/存储值的成员是可以的。例如:将Guid作为键,以及用于equals方法的属性是完全合理的。对于其余属性,使用不同的值是不合理的。我发现如果我朝着这个方向前进,我可能需要重新考虑我的班级结构。


6

只要覆盖等于,就更好地覆盖哈希码。完成此操作后,您的“实例”将永远不会再更改内部状态。

如果不覆盖等于,则使用哈希码VM对象标识来确定相等。如果将此对象放入Set中,则可以再次找到它。

更改用于确定相等性的对象的值将导致该对象在基于哈希的结构中不可追溯。

因此,对A的二传手很危险。

现在您没有不参与平等的B。这里的问题在语义上不是技术上的。因为技术上改变B对平等的事实是中立的。从语义上讲,B必须类似于“版本”标志。

重点是:

如果您有两个等于A但不等于B的对象,则假定这些对象中的一个比另一个更新。如果B没有版本信息,则当您决定“覆盖/更新”集合中的该对象时,该假设将隐藏在算法中。发生这种情况的源代码位置可能并不明显,因此开发人员将很难识别对象X和对象Y之间的关系,该关系不同于B中的X。

如果B具有版本信息,则可以暴露以前只能从代码隐式派生的假设。现在您可以看到,对象Y是X的较新版本。

想想你自己:你的身份将贯穿你的一生,也许某些特性会发生变化(例如,头发的颜色;-)。当然,您可以假设如果您有两张照片,一张是棕色的头发,另一张是灰色的头发,则可能是年轻的棕色头发的照片。但是,也许你染了头发?问题是:您可能知道自己染过头发。可以吗?要将其置于有效上下文中,您必须引入属性年龄(版本)。然后,您在语义上是明确的且毫不含糊的。

为了避免隐藏操作“用新对象替换旧对象”,Set不应具有get-Method。如果您想要这样的行为,则必须通过删除旧对象并添加新对象使其明确。

顺便说一句:如果传递的对象等于要获取的对象,那意味着什么?那没有意义。保持语义干净,不要这样做,尽管从技术上讲,没有人会妨碍您。


7
“一旦覆盖等于,就更好地覆盖哈希码。完成此操作后,您的“实例”将永远不会再更改内部状态。” 该语句在那儿值+100。
David Arno

+1指出取决于可变状态的相等性和哈希码的危险
尔克(Hulk)

3

特别是在Java中,HashSet最初是使用HashMap反正实现的,只是忽略了该值。因此,最初的设计在提供get方法到时没有预期任何优势HashSet。如果要在相等的各个对象之间存储和检索规范值,则只需使用HashMap自己。

我还没有及时更新与这样的实施细节,所以我不能说这种推理是否还适用于全在Java中,更不用说在C#等,但即使HashSet是重新实现使用内存少HashMap,在任何情况下,向Set接口添加新方法将是一项重大突破。因此,获得收益并不是所有人都认为值得拥有的痛苦。


好吧,在Java中,可以提供一个-实现以一种default不间断的方式来实现此目的。这似乎不是一个非常有用的更改。
绿巨人

@Hulk:我可能是错的,但是我认为任何默认的实现方式都效率低下,因为发问者说:“检索我项目的唯一方法是遍历整个集合并检查所有项目是否相等”。很好,您可以以向后兼容的方式进行操作,但是要增加一个陷阱,O(n)即使得即使哈希函数给出了良好的分布,生成的get函数也只能保证在比较中运行。然后,Set该实现会覆盖接口中的默认实现,包括HashSet,可以提供更好的保证。
Steve Jessop

同意-我认为这不是一个好主意。尽管有这种行为的优先级-List.get(int index)或-选择最近添加的List.sort的默认实现。接口提供了最大的复杂度保证,但是某些实现可能比其他实现好得多。
绿巨人

2

有一种主要语言,其语言集具有所需的属性。

在C ++中,std::set是有序集合。它具有一种.find根据您提供的排序运算符<或二进制bool(T,T)函数查找元素的方法。您可以使用find来实现所需的get操作。

实际上,如果bool(T,T)您提供的函数上有特定标志(is_transparent),则可以传入该函数对其具有重载的其他类型的对象。这意味着您不必将“虚拟”数据粘贴到第二个字段,只需确保您使用的排序操作可以在查找类型和包含集合的类型之间进行排序。

这样可以有效:

std::set< std::string, my_string_compare > strings;
strings.find( 7 );

其中my_string_compare了解了如何对整数和字符串进行排序,而无需先将整数转换为字符串(潜在成本)。

对于unordered_set(C ++的哈希集),没有等效的透明标志(尚未)。您必须将传递Tunordered_set<T>.find方法。可以添加它,但是哈希需要==一个哈希器,这与仅需要排序的有序集不同。

一般模式是容器将进行查找,然后为您提供该容器中该元素的“迭代器”。此时,您可以将元素放入集合中,或将其删除等等。

简而言之,并非所有语言的标准容器都具有您描述的缺陷。C ++标准库的基于迭代器的容器不存在,并且至少有一些容器在您描述的任何其他语言之前存在,并且甚至添加了比您描述的方法更有效的获取功能。您的设计没有任何问题,也不需要这种操作。您正在使用的Set的设计者根本没有提供该界面。

C ++标准容器,用于干净包装等效的手摇C代码的低级操作,旨在匹配您在汇编中如何高效地编写它。它的迭代器是C样式指针的抽象。您提到的语言已完全脱离了指针这一概念,因此它们没有使用迭代器抽象。

C ++没有此缺陷的事实很可能是设计的意外。以迭代器为中心的路径意味着,要与关联容器中的项目进行交互,您首先要获得元素的迭代器,然后使用该迭代器来讨论容器中的条目。

代价是必须跟踪迭代无效规则,并且某些操作需要2个步骤而不是1个步骤(这会使客户端代码更嘈杂)。好处是,健壮的抽象允许比API设计人员最初考虑的更高级的使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.