字典以类作为键

12

我正在学习电子工程,同时也是C#的初学者。我测量了数据,并希望以二维方式存储它。我认为我可以像这样制作一个字典

Dictionary<Key, string> dic = new Dictionary<Key, string>(); 

"Key"是我自己创建的一个包含两个int变量的类。现在我想把数据存储到这个Dictionary中,但目前还没有成功。如果我想使用特定的Key读取数据,则会出现错误报告,该Key在Dictionary中不存在。

下面是Key类的代码:

public partial class Key
{
    public Key(int Bahn, int Zeile) {
    myBahn = Bahn;
    myZeile = Zeile;

}
    public int getBahn()
    {
        return myBahn;
    }
    public int getZeile()
    {
        return myZeile;
    }
    private  int myBahn;
    private int myZeile;
}

为了测试它,我做了这样的事情:

获取元素:

Key KE = new Key(1,1);
dic.Add(KE, "hans");
...

获取元素:

Key KE = new Key(1,1);
monitor.Text = dic[KE];

有人有想法吗?


为什么不使用字符串作为键?你到底想在字典中存储什么?你能解释一下使用案例吗? - Chetan
是的。我有来自3D房间扫描仪的数据。扫描仪有两个轴。我想根据两个轴的位置来存储测量数据。因此,如果轴1位于位置15,轴2位于位置30,我希望得到键:15,30。 - Quirin Kraus
你将什么数据存储在字符串值中? - Chetan
这只是为了测试。我有另一个类,其中存储了4个双精度值。这将是我想要作为字典值进行存储的数据。 - Quirin Kraus
不确定是否可能,但有许多简单的解决方法(例如:使用类名作为键)。 - The_Black_Smurf
好的,让我更具体地解释一下:字典类型是一个集合。一个键必须是唯一的,然后可以随意添加“值”。每个 KE 的实例都必须是唯一的。即使是接受的解决方案有时也会失败。如果您创建了两个相同的实例,即使它们的“json”字符串相同,它们的哈希值也将相同。您可以在实例上设置一个 ID,例如工厂模式?或者根据您的需求使系统无法创建相同的实例。但是修改字典的工作方式来解决这个问题是不好的。 - Morten Bork
3个回答

26

如果您想将自己的类用作键,那么您需要覆盖GetHashCodeEquals方法。

class Foo 
{ 
    public string Name { get; set;} 
    public int FooID {get; set;}
    public override int GetHashCode()             
    {  
           return FooID; 
    }
     public override bool Equals(object obj) 
    { 
             return Equals(obj as Foo); 
    }

    public bool Equals(Foo obj)
     { 
          return obj != null && obj.FooID == this.FooID; 
     }
}

1
谢谢。但我无法在我的类中实现它...你能解释一下如果我重写这些方法会发生什么吗?如果您的Foo类中有两个int变量,会怎样呢?例如: class Foo { public int Name { get; set;} public int FooID {get; set;} - Quirin Kraus
3
默认情况下,在字典中查找键与值时,会调用object类的GetHashCodeEquals方法进行比较。这不会给您想要的结果,因为比较不会根据您的自定义类进行。当您重写这些方法时,可以自定义上述比较的方式。只需从GetHashCode返回使对象唯一的值,并在Equals中检查您类的所有属性的相等性即可。 - Akshey Bhat
1
这里有一个实现 https://www.codeproject.com/Articles/23610/Dictionary-with-a-Custom-Key - user1908746

1
For the sake of an alternate opinion and not to be disparaging of Mr. Kuroi's solution (which is a good one) here is a simple class that can be used as a key in a map as well as other uses.  We use a complex class as a key because we want to know track other things.  In this example, we arrive at a vertex in a graph and we want to know if we have visited it before.
<code>
    public class Vertex : IComparable<Vertex>, IEquatable<Vertex>, IComparable
    {
        String m_strVertexName = String.Empty;
        bool m_bHasVisited = false;

        public Vertex()
        {
        }

        public Vertex(String strVertexName) : this()
        {
            m_strVertexName = strVertexName;
        }

        public override string ToString()
        {
            return m_strVertexName;
        }

        public string VertexName
        {
            get { return m_strVertexName; }
            set
            {
                if (!String.IsNullOrEmpty(value))
                    m_strVertexName = value; 
            }
        }

        public bool HasVisited
        {
            get { return m_bHasVisited; }
            set { m_bHasVisited = value; }
        }
        public override int GetHashCode()
        {
            return ToString().GetHashCode();
        }
        public int CompareTo(Vertex rhs)
        {
            if (Equals(rhs))
                return 0;
            return ToString().CompareTo(rhs.ToString());
        }
        int IComparable.CompareTo(object rhs)
        {
            if (!(rhs is Vertex))
                throw new InvalidOperationException("CompareTo: Not a Vertex");
            return CompareTo((Vertex)rhs);
        }
        public static bool operator < (Vertex lhs, Vertex rhs) => lhs.CompareTo(rhs) < 0;
        public static bool operator > (Vertex lhs, Vertex rhs) => lhs.CompareTo(rhs) > 0;
        public bool Equals (Vertex rhs) => ToString() == rhs.ToString();
        public override bool Equals(object rhs)
        {
            if (!(rhs is Vertex))
                return false;
            return Equals((Vertex)rhs);
        }
        public static bool operator == (Vertex lhs, Vertex rhs) => lhs.Equals(rhs);
        public static bool operator != (Vertex lhs, Vertex rhs) => !(lhs == rhs);
    }
</code>

0

虽然你可以通过实现自己的EqualsGetHashCode来使用类作为键,但如果你还不熟悉C#,我不建议这样做。

C#内部库会调用这些方法,并期望它们严格按照规范工作,优雅地处理所有边缘情况。如果你在其中放入一个错误,你可能需要经历一段不愉快的头痛时期。

在我看来,使用已经支持哈希和比较的经过尝试、验证和测试的现有类型来创建即时键无疑是效率更高、更简单的。

例如,从你的角度坐标开始:

int Bahn = 15;
int Zeile = 30;

你可以使用一个字符串(例如"15,30"):
String Key (int Bahn, int Zeile) { return $"{Bahn},{Zeile}"; }

var myDict = new Dictionary<string, string>();
myDict.Add (Key(Bahn,Zeile), myString);

或者如果你需要更高效的方式,可以返回一个包含两个元素的元组(例如<15,30>):

Tuple<int,int> Key (int Bahn, int Zeile) { return Tuple.Create(Bahn,Zeile); }

var myDict = new Dictionary<Tuple<int, int>, string>();
myDict.Add (Key(Bahn,Zeile), myString);

或者,如果范围足够小以适合 int(例如 15+30*360),则仅组合您的两个角度即可。如果需要更高效的方法:

int Key (int Bahn, int Zeile) { return Bahn+360*Zeile; }

var myDict = new Dictionary<int, string>();
myDict.Add (Key(Bahn,Zeile), myString);

这似乎比以下方式更简便:

   class Key {
      // truckloads of code to implement the class,
      // not to mention testing it thourougly, including edge cases
   }

    var myDict = new Dictionary<Key, string>();
    myDict.Add (new Key(Bahn,Zeile), myString);

键的可变性

另外,请注意,只要用于索引条目,您的键必须是不可变的。

如果在使用键添加元素后更改BahnZiel的值,则会严重破坏字典。

行为未定义,但您很可能会丢失随机条目,导致内存泄漏,并且如果内部库最终检测到不一致状态(例如由同一键索引的多个条目),则可能会崩溃并出现异常。

例如:

var myKey = new Key(15, 30);

for (String data in row_of_data_sampled_every_10_degrees)
{
   myDict.Add (myKey, data); // myKey must remain constant until the entry is removed
   myKey.Bahn += 10;         // changing it now spells the death of your dictionary
}

关于哈希角度坐标的一点说明

现在问题是,为整数、字符串和元组提供的通用哈希函数可能不会为您特定的数据集产生最佳结果。

我建议从简单的解决方案开始,只有在遇到实际性能问题时才使用专门的代码。在这种情况下,您可能最好使用更适合空间索引的数据结构(通常是极坐标的四叉树,或者如果您想从扫描仪数据重建3D模型,则使用八叉树)。


我非常鼓励使用之前的解决方案,因为它教授了正确的技术并且具有未来的可靠性。我不确定一次性的解决方案是否具有未来的可靠性。我的建议是使用经过验证的集合和技术,使得一个复杂的类可以像原始类型一样可比较和相等。 - user3820843
这个 OP 的例子对我来说看起来像是在寻找问题的解决方案,而且还是一个危险的解决方案。"使用类作为键"本来就是一个不好的想法,所以我宁愿挑战这个想法本身,而不是继续讨论通常的 IEquatable 重载。 - kuroi neko
使用类作为键非常健壮,并且有许多应用。此外,学习编写可以像基元一样用于排序、比较甚至其他运算符重载的复杂类是一个很好的计算机科学练习。当您想要创建可以与标准模板库无缝工作的类时,这种技术在C++中非常流行。就像你所说的,你的里程可能会有所不同。 - user3820843
在我的看法中,OP的例子是一场灾难的配方。无论如何... - kuroi neko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接