Используйте byte [] в качестве ключа в словаре - PullRequest
38 голосов
/ 17 сентября 2009

Мне нужно использовать byte[] в качестве ключа в Dictionary. Поскольку 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[] и переопределяет GetHashCode на основе содержимого byte[], но это кажется подверженным ошибкам.

Ответы [ 7 ]

66 голосов
/ 17 сентября 2009

По умолчанию 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;
  }
}
8 голосов
/ 20 мая 2015

Итак, ответ 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;
}

Если вам известно, что байтовые массивы [], которые вы используете в качестве ключа, сами по себе являются криптографическими хешами, то вы можете использовать это предположение в своих интересах и просто вернуть первые 4 байта, преобразованные в int. Это, вероятно, тоже хорошо работает для байтовых массивов общего назначения:

// 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;
}
4 голосов
/ 17 сентября 2009

Не могли бы вы преобразовать байт [] в строку и использовать ее в качестве ключа?

Что-то вроде:

        ASCIIEncoding enc = new ASCIIEncoding();
        byte[] input;
        string demo = new string(enc.GetChars(input));
        byte[] decode = enc.GetBytes(demo.ToCharArray());
3 голосов
/ 22 февраля 2013
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);
    }
}
3 голосов
/ 17 сентября 2009

Твоя мысль была моей первой мыслью. Я не думаю, что это будет подвержено ошибкам. Но если вам не нравится эта опция, вы можете создать класс, реализующий IEqualityComparer, и передать его экземпляр конструктору Dictionary.

1 голос
/ 23 ноября 2017

Просто сделал 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;
    }
}
0 голосов
/ 17 сентября 2009

При извлечении элементов из словаря вы используете новый оператор для байта []. Это будет искать другой (новый) экземпляр 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...