在具有多个键的HashTable/Dictionary对象中查找项
Dictionary对象以单个键作为查找键。此类简化了在您具有多个键(例如两个字符串和一个int等)时使用Dictionary。
引言
Dictionary
对象以单个键作为查找键。此类简化了在您具有多个键(例如两个string
s和一个int
等)时使用Dictionary
。当您将所有键组合成单个字符串并将其用作键时,如果让您感到不适,请使用此类。
下面是一个关于如何使用此类及其示例类TestClass
的示例。为键定义一个新类,并在其中实现抽象方法GetKeyValues
,该方法将返回用于用作Dictionary
中查找对象的键的值数组。在该方法中,只需返回您希望用作查找Dictionary
中对象的键的属性即可。
/// <summary>
/// Define test class to use key for
/// </summary>
public class TestClass
{
public string Column1 = null;
public string Column2 = null;
public TestClass(string Column1, string Column2)
{
this.Column1 = Column1;
this.Column2 = Column2;
}
}
//define key to use for test class
public class TestClassKey : ClassKey<TestClass>
{
//Init with object
public TestClassKey(TestClass ClassReference) : base(ClassReference) { }
//return list of column values we need to use as a key
public override object[] GetKeyValues()
{
return new object[] {
ClassReference.Column1,
ClassReference.Column2
};
}
}
下面是一个单元测试示例,用于确认它确实有效
TestClass model1 = new TestClass("abc", "def");
TestClass model2 = new TestClass("abc", "def");
Assert.AreEqual(new TestClassKey(model1), new TestClassKey(model2));
Assert.IsTrue(new TestClassKey(model1) == new TestClassKey(model2));
//change side of one and make sure not equal
model1.Column1 = "xyz";
model2.Column2 = "123";
Assert.AreNotEqual(new TestClassKey(model1), new TestClassKey(model2));
Assert.IsTrue(new TestClassKey(model1) != new TestClassKey(model2));
使用代码
由于此主要用于Dictionary
对象,因此我必须非常熟悉相等性重写和GetHashCode
方法。这是您一次完成并放在一个地方的事情,因为它非常非常容易出错。我已经在生产系统中使用此代码几个月了,并添加了针对我遇到过的各种错误的测试,因此我相当确信此代码运行良好。我在这里发布它是因为我对其他人的看法感到好奇,或者是否存在更简单的方法来做到这一点。这是ClassKey.cs的全部代码
/// <summary>
/// Defines a common set of operations and functionality for creating concrete
/// key classes which allow us to lookup items in a collection
/// using one or more of the properties in that collection.
/// </summary>
public abstract class ClassKey<T> where T : class
{
/// <summary>
/// The collection item referenced by this key
/// </summary>
public T ClassReference
{
get { return _CollectionItem; }
set { _CollectionItem = value; }
}
private T _CollectionItem = null;
/// <summary>
/// Init empty if needed
/// </summary>
public ClassKey() { }
/// <summary>
/// Init with specific collection item
/// </summary>
/// <param name="CollectionItem"></param>
public ClassKey(T CollectionItem)
{
this.ClassReference = CollectionItem;
}
/// <summary>
/// Compare based on hash code
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj is ClassKey<T>)
{
return (obj as ClassKey<T>).GetHashCode() == this.GetHashCode();
}
else
return false; //definitely not equal
}
public static bool operator ==(ClassKey<T> p1, ClassKey<T> p2)
{
//if both null, then equal
if ((object)p1 == null && (object)p2 == null) return true;
//if one or other null, then not since above
//we guaranteed if here one is not null
if ((object)p1 == null || (object)p2 == null) return false;
//compare on fields
return (p1.Equals(p2));
}
public static bool operator !=(ClassKey<T> p1, ClassKey<T> p2)
{
return !(p1 == p2);
}
//must override to get list of key values
public abstract object[] GetKeyValues();
/// <summary>
/// Implement hash code function to specify
/// which columns will be used for the key
/// without using reflection which may be a bit slow.
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
object[] keyValues = GetKeyValues();
//use co-prime numbers to salt the hashcode
//so same values in different order will
//not return as equal - see TestClassKeyXOROrderProblem
// to reproduce problem
//http://www.msnewsgroups.net/group/microsoft
// .public.dotnet.languages.csharp/topic36405.aspx
//http://directxinfo.blogspot.com/2007/06/gethashcode-in-net.html
//first co-prime number
int FinalHashCode = 17;
//other co-prime number - ask me if I know what
//co-prime means, go ahead, ask me.
int OtherCoPrimeNumber = 37;
//get total hashcode to return
if(keyValues != null)
foreach (object keyValue in keyValues)
{
//can't get hash code if null
if (keyValue != null)
{
FinalHashCode = FinalHashCode *
OtherCoPrimeNumber + keyValue.GetHashCode();
}
}
return FinalHashCode;
}
}
关注点
如果您不想为比较使用单独的类,也可以直接继承自ClassKey
。如果您想重写==
或GetHashCode
方法,那么它也是一个方便的类,这样您就不必记住在每次修改任何一个方法时都需要完成的所有细节。如果可以以不同或更好的方式处理涉及素数和互素数的那些部分,我很想听听大家的反馈。
结构体实现
在阅读了读者的一条评论后,我添加了另一个下载文件,其中包含键的结构体实现。我不喜欢这种实现,因为键的内容必须多次指定,也就是说,每次使用时都要指定,因为结构体不支持继承。尽管如此,它在查找速度上似乎快了15-20%,但在一百万行查找中,这相当于1200毫秒对1000毫秒,所以我仍然偏爱类/继承方法。
各种方法的性能测量
我通过运行不同方法的各种单元测试来完成以下操作。我尽量使它们大致相同,以保持公平。为了测量内存,我简单地关闭了ProcessInvocation.exe(TestDriven.NET测试运行程序),然后运行了性能测试,该测试进行一百万次迭代10次,并对初始化和查找结果进行平均。无论您选择哪种方式,都会有权衡。我仍然偏爱ClassKey方法。虽然它需要更长的时间(一百万行需要200毫秒),但我认为它可以大大降低出错的可能性,并且更直观。结构体实现需要更长的初始化时间,但查找速度更快,但可维护性较差。嵌套字典对于查找速度最快,但初始化时间更长,内存使用量是ClassKey方法的两倍,这可能是因为它为列表中的每个项创建了另一个字典对象。我还认为它的语法和可维护性最差。串联字符串键的性能还可以,所以如果您想偷懒不想实现这样的东西,那么我认为这就是最好的方法,只要您有一个通用的方法来构建可重用的键(而不是每次使用时都指定)。它还占用了更多的内存。这些数字不能保证或完美,只是我系统上的一些粗略测量,以作为比较的基础。
Class key
Initialization: 3,018ms
Lookups: 1,144ms
Memory - Never above 313MB
Struct
Initialization: 3,210ms
Lookups: 1,064ms
Memory - Never above 354MB
Dictionary of Dictionaries
Initialization: 4,313ms
Lookups: 919ms
Memory - Never above 555MB
Concatenated string key dictionary
Initialization: 3,305ms
Lookups: 1,039ms
Memory - Never above 460MB
Tuple method
Initialization: 3,810ms
Lookups: 3,241ms
Memory - Never above 316MB
历史
- 2008年4月22日
- 初始版本。
- 为格式化而奋斗,厌倦了处理CodeProject编辑器。
- 2008年4月23日
- 添加了新的zip文件,包含结构体实现和运行一百万次的单元测试。
- 添加了新的zip文件,包含直接的字典实现和以上所有内容。
- 2008年4月30日
- 添加了用于未选中/元组讨论的代码下载。