在字典中使用byte[]作为键

47

我需要在Dictionary中使用一个byte[]作为键。由于byte[]没有重写默认的GetHashCode方法,所以包含相同数据的两个不同的byte[]对象将在字典中使用两个单独的槽位。基本上我想要的是这样的:

Dictionary<byte[], string> dict = new Dictionary<byte[], string>();
dict[new byte[] {1,2,3}] = "my string";
string str = dict[new byte[] {1,2,3}];
// I'd like str to be set to "my string" at this point

有没有简单的方法来做到这一点?我能想到的唯一办法是构建一个包含 byte[] 的封装类,并根据 byte[] 的内容重写 GetHashCode, 但这似乎容易出错。


你自己回答了这个问题... - EricSchaefer
5
@Eric: 但如果他没有发这个问题,他就不会知道更好的选择了。 :) - Sam Harwell
7个回答

79

默认情况下,byte[] 将按引用进行比较,这在这种情况下不是你想要的。你需要做的是指定自定义的 IEqualityComparer<byte[]> 并进行所需的比较。

例如:

public class ByteArrayComparer : IEqualityComparer<byte[]> {
  public bool Equals(byte[] left, byte[] right) {
    if ( left == null || right == null ) {
      return left == right;
    }
    return left.SequenceEqual(right);
  }
  public int GetHashCode(byte[] key) {
    if (key == null)
      throw new ArgumentNullException("key");
    return key.Sum(b => b);
  }
}

然后您可以执行

var dict = new Dictionary<byte[], string>(new ByteArrayComparer());

2.0的解决方案

public class ByteArrayComparer : IEqualityComparer<byte[]> {
  public bool Equals(byte[] left, byte[] right) {
    if ( left == null || right == null ) {
      return left == right;
    }
    if ( left.Length != right.Length ) {
      return false;
    }
    for ( int i= 0; i < left.Length; i++) {
      if ( left[i] != right[i] ) {
        return false;
      }
    }
    return true;
  }
  public int GetHashCode(byte[] key) {
    if (key == null)
      throw new ArgumentNullException("key");
    int sum = 0;
    foreach ( byte cur in key ) {
      sum += cur;
    }
    return sum;
  }
}

3
将结果相加可能不是最佳的哈希码。也许可以这么做:sum = 33 * sum + cur; - user7116
1
@SerG 因为在这种情况下,EqualityComparer<byte[]>.Default.EqualsObject.Equals 是相同的,所以没有意义。 - JAB
16
据说.NET 4引入了一个等价物:StructuralComparisons.StructuralEqualityComparer,可以将解决方案简化为 var dict = new Dictionary<byte[], string>(StructuralComparisons.StructuralEqualityComparer);. (由于2.0限制,这对Jason没有帮助,但知道这一点很好。) - JAB
1
@JAB 但是我使用 StructuralComparisons.StructuralEqualityComparer 时出现了“无法将 'System.Collections.IEqualityComparer' 转换为 'System.Collections.Generic.IEqualityComparer<byte[]>'”的错误。 - SerG
4
好的,我会尽力进行翻译。内容如下:“@SerG 嗯,我没有注意到StructuralComparisons.StructuralEqualityComparer没有通用版本。” - JAB
显示剩余12条评论

15

所以,JaredPar的回答并不糟糕,但有几个方面可以改进。首先,IEqualityComparer页面中指出,“我们建议您从EqualityComparer类派生而来,而不是实现IEqualityComparer接口。”

其次,GetHashCode的实现应该尽可能地快速。它用于快速消除显然不同的对象,在这些对象上运行Equals显然是浪费时间的。因此,GetHashCode应该比实际运行Equals要快得多。

第三,像JaredPar所做的返回字节数组的总和非常容易产生冲突 - 如果字节顺序不同,或者相对差异互相抵消等。

因此,我建议采用以下解决方案:

public class ByteArrayComparer : EqualityComparer<byte[]>
{
    public override bool Equals(byte[] first, byte[] second)
    {
        if (first == null || second == null) {
            // null == null returns true.
            // non-null == null returns false.
            return first == second;
        }
        if (ReferenceEquals(first, second)) {
            return true;
        }
        if (first.Length != second.Length) {
            return false;
        }
        // Linq extension method is based on IEnumerable, must evaluate every item.
        return first.SequenceEqual(second);
    }
    public override int GetHashCode(byte[] obj)
    {
        if (obj == null) {
            throw new ArgumentNullException("obj");
        }
        // quick and dirty, instantly identifies obviously different
        // arrays as being different
        return obj.Length;
    }
}

以上,返回obj.Length确实很快也很简单粗暴,但也容易出现很多碰撞(即哈希冲突)。我认为我们可以做得更好。

如果你要检查所有字节,像JaredPar的答案中那样简单地将所有字节相加会产生较少的冲突。但同样地,这需要检查所有元素,所以它并不比实际运行Equals更优。你可能最好无条件地返回0,并始终强制使用Equals。

我强调:这比像JaredPar答案中返回总和要好。而总是返回0比这个更好。而返回obj.Length比返回0更好。

// This is not recommended. Performance is too horrible.
public override int GetHashCode(byte[] obj)
{
    // Inspired by fletcher checksum. Not fletcher.
    if (obj == null) {
        throw new ArgumentNullException("obj");
    }
    int sum = 0;
    int sumOfSum = 0;
    foreach (var val in obj) {
        sum += val; // by default, addition is unchecked. does not throw OverflowException.
        sumOfSum += sum;
    }
    return sum ^ sumOfSum;
}

如果您知道用作密钥的 byte[] 数组本身是加密哈希值,则可以利用此假设,简单地返回前四个字节转换为 int。这对于通用的 byte[] 数组也可能有效:

// This implementation works great if you assume the byte[] arrays
// are themselves cryptographic hashes. It probably works alright too,
// for general-purpose byte arrays.
public override int GetHashCode(byte[] obj)
{
    if (obj == null) {
        throw new ArgumentNullException("obj");
    }
    if (obj.Length >= 4) {
        return BitConverter.ToInt32(obj, 0);
    }
    // Length occupies at most 2 bits. Might as well store them in the high order byte
    int value = obj.Length;
    foreach (var b in obj) {
        value <<= 8;
        value += b;
    }
    return value;
}

我喜欢这种思路。我认为对于通用目的,从开头取2个字节和从结尾取2个字节可能会带来一些优势,比如在使用混合小端/大端字节数组时。当然,通过了解数据结构,始终可以找到更优化的解决方案 :) - Wayne Uroda
Moneyball:“如果您在GetHashCode中添加所有字节值,则最好无条件返回0,并始终强制使用Equals。”我真的很喜欢“从前四个字节创建一个int”的建议,两者都让我想起了GetHashCode应该做什么。太棒了。 - ruffin

4

您能把byte[]转换成字符串并将其用作密钥吗?

类似这样:

        ASCIIEncoding enc = new ASCIIEncoding();
        byte[] input;
        string demo = new string(enc.GetChars(input));
        byte[] decode = enc.GetBytes(demo.ToCharArray());

虽然这样做会在性能上付出更多的代价,因为需要复制数据,但由于字符串具有比迭代字节数组更好的哈希算法,所以检索速度可能会更快。 - Bryan Legend

4
using System;
using System.Collections;
using System.Collections.Generic;

[Serializable]
class StructuralEqualityComparer : IEqualityComparer, IEqualityComparer<object>
{
    public new bool Equals(object x, object y)
    {
        var s = x as IStructuralEquatable;
        return s == null ? object.Equals(x, y) : s.Equals(y, this);
    }

    public int GetHashCode(object obj)
    {
        var s = obj as IStructuralEquatable;
        return s == null ? EqualityComparer<object>.Default.GetHashCode(obj) : s.GetHashCode(this);
    }
}

这个与byte[]完美配合(目前为止,我还没有发现任何问题),看起来最好。谢谢! - beatcoder
更新:它可以工作,但是我正在使用一个byte[4]作为字典的键,递增其字节并且在{0,32,0,0}之后无法插入,因为存在重复的键。由于哈希算法的限制,它只能插入约270k个条目。不错,不过。 - beatcoder

3

我也认为您的想法是正确的。我不认为这会导致错误。但如果您不喜欢这个选项,您可以创建一个实现IEqualityComparer接口的类,并将其实例传递给Dictionary的构造函数。


我曾考虑过自己计算byte[]的哈希码可能存在错误,但看起来这是不可避免的... - Jason

1

将EqualityComparer变得更加通用,不再仅适用于数组,而是适用于IEnumerable<T>。

由于我们现在有了一个T,因此我们需要能够为元素指定可选的相等比较器。

最后,GetHashCode()不应该抛出异常,有时您需要快速计算哈希值,有时您需要在第一次运行时更准确。因此,您可以选择定义一个精度,从中考虑多少项(最大值)的哈希码用于我们自己的哈希。

public class EnumerableEqualityComparer<T> : IEqualityComparer<IEnumerable<T>>
{
    private static readonly Lazy<IEqualityComparer<IEnumerable<T>>> Lazy = new Lazy<IEqualityComparer<IEnumerable<T>>>(() => new EnumerableEqualityComparer<T>());
    private int accuracy;
    private IEqualityComparer<T> comparer;

    public EnumerableEqualityComparer()
        : this(-1)
    {
    }

    public EnumerableEqualityComparer(int accuracy)
        : this(accuracy, null)
    {
    }

    public EnumerableEqualityComparer(IEqualityComparer<T> elementEqualityComparer)
        : this(-1, elementEqualityComparer)
    {
    }

    public EnumerableEqualityComparer(int accuracy, IEqualityComparer<T> elementEqualityComparer)
    {
        if (accuracy < 0)
        {
            accuracy = 4;
        }

        this.accuracy = accuracy;
        comparer = elementEqualityComparer ?? EqualityComparer<T>.Default;
    }

    public static IEqualityComparer<IEnumerable<T>> Default { get; private set; } = Lazy.Value;

    public bool Equals(IEnumerable<T> x, IEnumerable<T> y)
    {
        if (ReferenceEquals(x, y))
        {
            return true;
        }

        if (ReferenceEquals(x, null)
            || ReferenceEquals(y, null))
        {
            return false;
        }

        return x.SequenceEqual(y, comparer);
    }

    public int GetHashCode(IEnumerable<T> obj)
    {
        if (ReferenceEquals(obj, null))
        {
            return -1;
        }

        var count = (obj as ICollection<T>)?.Count ?? 1;
        var hashCode = count * 49297;

        foreach (var item in obj.Take(accuracy))
        {
            hashCode += comparer.GetHashCode(item) * 17123;
        }

        return hashCode;
    }
}

-3

当您从字典中检索项目时,您正在使用byte[]的new运算符。这将在字典中查找不同(新)的byte[]实例,而该实例不存在。

以下是一个可行的解决方案:

 var dict = new Dictionary<byte[], string>();

            var b = new byte[] { 1,2,3};

            dict[b] = "my string";

            var value = dict[b]; 

            Console.WriteLine(value);

这不是解决方案。如果您使用另一个字节数组访问字典,它将抛出异常。var c = new byte[] { 1, 2, 3 };var value = dict[c];最后一行会失败。 - sahl04

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