Стратегия для реализации правильной функции, подобной GetHashCode - PullRequest
1 голос
/ 16 января 2020

Вопрос

Какой лучший способ реализовать функцию, которая дает объекту возможность вернуть ключ га sh?

Требования будут:

  • HashCodeFn(((bool?)false, "example")) != HashCodeFn(((bool?)null, "example"))
  • Сравнительно дешево рассчитать
  • Работает с любым типом без каких-либо особых требований c (например, атрибут [Serializable])

Что я пробовал

Я пробовал с .GetHashCode, но:

  • Это ненадежно для таких вещей, как null против 0 против false
  • Требуется реализовать для каждого типа

Я пробовал:

    private static int GetHashKey<T>(T input)
    {
        using var memoryStream = new MemoryStream();
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(memoryStream, input);
        memoryStream.Position = 0;
        using var reader = new StreamReader(memoryStream);
        return reader.ReadToEnd().GetHashCode();
    }

, но:

  • Требуется, чтобы все типы в дереве объектов реализуют [Serializable] (некоторые типы, которые я не контролирую и не реализую)

Я думаю о сериализации объекта до JSON в самом компактном форму, а затем получить GetHashCode этой строки, но я не уверен, насколько хорошо она работает с чем-то вроде NodaTime.Instant. Это самый быстрый способ выполнить sh это?


Specifi c сценарий использования

Это используется в качестве ключа загрузчика данных (см. Github.com/graphql/dataloader например) если это поможет понять пример использования.

В частности, для загрузки данных используется ключ загрузчика данных. Когда у вас много запросов с вводом (a, b, c) и вы хотите «включить», например, a (что означает, что (1, b, c), (2, b, c), (3, b, c) должен вызывать пакетную функцию fn([1, 2, 3], (b, c)), тогда вам нужно иметь возможность определить ключ это то же самое для тех же значений (b, c), которые будут использоваться в качестве ключа загрузчика данных.

С точки зрения ввода, например, указание или отсутствие bool для чего-то вроде b считается 2 разные вещи и должны быть объединены в две разные функции.

Если бы я использовал (b, c).GetHashCode(), то я бы посчитал ((bool?)false, "ok") и ((bool?)null, "ok") одной и той же вещью, поэтому объединял их в одну и ту же пакетная функция, дающая неожиданные результаты.

1 Ответ

1 голос
/ 16 января 2020

Я не думаю, что есть какой-то особенно эффективный способ сделать то, что вы хотите. Потребуется какая-то дополнительная обработка, чтобы убедиться, что вы получаете соответствующие коды ha sh. Кроме того, имейте в виду, что если классы, которыми вы не управляете, уже реализуют Equals и GetHashCode, а Equals возвращает true, например, которые отличаются только чем-то вроде обнуляемого логического значения, равного false или null, то для GetHashCode неверно возвращать разные значения.

Вы можете сериализовать до JSON, чтобы достичь того, что вы хотите. Это исключит любые поля, которые могут быть аннотированы для исключения. Предполагая, что ни одно из полей, относящихся к коду ha sh, не исключено, это сработает. В качестве альтернативы вы можете написать функции расширения для типов, которые будут вызывать конфликты, и настроить хеширование для этих полей. Затем используйте рефлексию (которая, вероятно, также будет использоваться для сериализации в JSON), чтобы перебрать членов класса и получить коды sh, используя ваши расширения, где это необходимо. Нечто похожее на приведенный ниже код.

class ThingToHash
{
    public bool? CouldBeFalseOrNullOrNull { get; }
    public int IncludesZero { get; }
    public string CanBeEmptyOrNull { get; }
    private string Hidden { get; }

    public ThingToHash(bool? couldBeFalseOrNull, int includesZero, string canBeEmptyOrNull)
    {
        CouldBeFalseOrNullOrNull = couldBeFalseOrNull;
        IncludesZero = includesZero;
        CanBeEmptyOrNull = canBeEmptyOrNull;
    }
}

static class StringExtensions
{
    public static int GetAltHashCode(this string toHash)
    {
        return toHash?.GetHashCode() ?? 17;
    }
}

static class NullableBoolExtensions
{
    public static int GetAltHashCode(this bool? toHash)
    {
        return toHash?.GetAltHashCode() ?? true.GetHashCode() * 19;
    }
}

static class BoolExtensions
{
    public static int GetAltHashCode(this bool toHash)
    {
        if (false == toHash)
        {
            return true.GetHashCode() * 17;
        }

        return toHash.GetHashCode();
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(false.GetHashCode());
        Console.WriteLine(((bool?)null).GetHashCode());
        Console.WriteLine(false == (bool?)null);

        Console.WriteLine(HashUnknownObject(new ThingToHash(null, 0, "")));
        Console.WriteLine(HashUnknownObject(new ThingToHash(false, 0, "")));

        Console.ReadKey();
    }

    static int HashUnknownObject(Object toHash)
    {
        PropertyInfo[] members = toHash.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        int hash = 17;

        foreach (PropertyInfo memberToHash in members)
        {
            object memberVal = memberToHash.GetValue(toHash);

            if (null == memberVal)
            {
                if (typeof(bool?) == memberToHash.PropertyType)
                {
                    hash += 31 * ((bool?)null).GetAltHashCode();
                }
                else if (typeof(string) == memberToHash.PropertyType)
                {
                    hash += 31 * ((string)null).GetAltHashCode();
                }
            }
            else
            {
                hash += 31 * memberToHash.GetValue(toHash).GetHashCode();
            }
        }

        return hash;
    }
}

Вам, очевидно, придется добавить другие проверки, чтобы использовать расширение bool, добавить другие расширения и т. Д., Чтобы покрыть случаи, которые вам нужны. И сделайте тестирование, чтобы проверить влияние использования отражения для сериализации. Вы можете уменьшить это для классов, которые уже реализуют GetHashCode, например, не генерируя ha sh кодов для каждого члена для них.

И этот код, очевидно, можно очистить. Здесь просто быстро и грязно.

...