Как добавить числа общего типа

У меня есть коллекция объектов с уникальным ключом. Этот ключ может быть числом или строкой, он общий, и большую часть класса это не волнует, потому что он хранится в Dictionary<TKey, TItem>.

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

Я ищу что-то вроде метода GetUniqueKey ниже:

// Restrict TKey to numbers or strings: https://stackoverflow.com/a/30660880
class MyCollection<TKey, TObject>
    where TObject : class
    where TKey : notnull, IComparable, IConvertible, IEquatable<TKey>
{
    private Dictionary<TKey, TObject> items;

    public TKey GetUniqueKey()
    {
        if (TKey is INumber)
            return items.Keys.Max() + 1;
        if (TKey is string)
            return Guid.NewGuid().ToString();
        throw new NotSupportedException("Key type not supported.");
    }
}

Можно ли это вообще сделать?

Пока нет, но это в процессе...

György Kőszeg 21.07.2024 19:17

MAX+1 — не лучший способ генерации уникальных ключей. Это гарантирует дубликаты, если элементы будут удалены. Гораздо лучшим способом было бы сохранить последний сгенерированный идентификатор и каждый раз увеличивать его.

Panagiotis Kanavos 22.07.2024 13:23

Хм, я не понимаю, как это гарантирует дубликаты, если генерирует только значение, которого на данный момент никогда не существует. Я знаю, что это нельзя сравнивать с последовательностями SQL. Это только локальное хранилище данных.

ygoe 23.07.2024 17:50
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
3
119
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы действительно можете использовать здесь общие математические API, но вам все равно понадобится какой-то способ убедить компилятор, что TKey — это число. В случае string вы можете просто проверить typeof(TKey) == typeof(string) и выполнить несколько преобразований.

Вот пример того, как это делается путем dynamicпривязки к вспомогательному методу:

частный словарь<TKey, TObject> items = ...;

public TKey GetUniqueKey()
{
    if (typeof(TKey).GetInterface("System.Numerics.INumber`1") != null)
        return GetNumericUniqueKey((dynamic)items.Keys);
    if (typeof(TKey) == typeof(string))
        return (TKey)(object)Guid.NewGuid().ToString();
    throw new NotSupportedException("Key type not supported.");
}

private static T GetNumericUniqueKey<T>(ICollection<T> existingKeys) where T: INumber<T> {
    if (existingKeys.Any()) {
        return (existingKeys.Max() ?? T.Zero) + T.One;
    }
    return T.Zero;
}

Другой подход — абстрагироваться от идеи, что ключи должны быть строками или числами. Создайте интерфейс IKey, который требует, чтобы его реализации имели необходимое вам поведение:

interface IKey<TSelf> where TSelf: IKey<TSelf> {
    static abstract TSelf GetUniqueKey(ICollection<TSelf> existingKeys);
    
    // declare other useful things you might want a key to have here
}

Тогда MyCollection может быть таким простым, как:

class MyCollection<TKey, TObject>
    where TObject : class
    where TKey : IKey<TKey>
{
    private Dictionary<TKey, TObject> items = ...;

    public TKey GetUniqueKey()
    {
        return TKey.GetUniqueKey(items.Keys);
    }
}

Теперь вам просто нужно реализовать числовые и строковые клавиши как простые оболочки числовых типов и string. Опять же, вы можете использовать общие математические методы для реализации числовых ключей.

record struct NumericKey<T>(T number): IKey<NumericKey<T>> where T: INumber<T> {
    
    public static NumericKey<T> GetUniqueKey(ICollection<NumericKey<T>> existingKeys) {
        if (existingKeys.Any()) {
            var newKey = (existingKeys.Max(x => x.number) ?? T.Zero) + T.One;
            return new NumericKey<T>(newKey);
        }
        return new NumericKey<T>(T.Zero);
    }
    
    // add implicit conversions perhaps...
}

record struct StringKey(string str): IKey<StringKey> {
    public static StringKey GetUniqueKey(ICollection<StringKey> existingKeys) {
        return new StringKey(Guid.NewGuid().ToString());
    }
    
    // add implicit conversions perhaps...
}

Если вы не против MyCollection иметь 3 параметра типа, вы можете сделать это, не меняя TKey на типы-оболочки.

class MyCollection<TKey, TFactory, TObject>
    where TObject : class
    where TKey : notnull, IComparable, IConvertible, IEquatable<TKey>
    where TFactory: IKeyFactory<TKey>
{
    private Dictionary<TKey, TObject> items = new();

    public TKey GetUniqueKey()
    {
        return TFactory.GetUniqueKey(items.Keys);
    }
}

interface IKeyFactory<TKey> {
    static abstract TKey GetUniqueKey(ICollection<TKey> existingKeys);
}

sealed class NaximumPlusOneFactory<T>: IKeyFactory<T> where T: INumber<T> {
    private NaximumPlusOneFactory() {}
    public static T GetUniqueKey(ICollection<T> existingKeys) {
        if (existingKeys.Any()) {
            var newKey = (existingKeys.Max() ?? T.Zero) + T.One;
            return newKey;
        }
        return T.Zero;
    }
}
sealed class GuidFactory: IKeyFactory<string> {
    private GuidFactory() {}
    public static string GetUniqueKey(ICollection<string> existingKeys) {
        return Guid.NewGuid().ToString();
    }
}

Хм. Первое решение выглядит многообещающе, но я не могу использовать dynamic в проекте AOT. Второе решение требует от меня использования этих пользовательских типов вместо int или string, чего я не могу сделать. Ключ — это одно из свойств объектов в моей коллекции, и эти простые типы используются повсюду.

ygoe 21.07.2024 16:38

@ygoe Я не понимаю, почему вы не можете использовать собственные типы. Какое отношение к этому имеет фраза «ключ — это одно из свойств объектов в моей коллекции»? Добавление оператора неявного преобразования тоже не поможет? Суть в том, что вы проверяете каждый числовой тип вручную. например if (typeof(TKey) == typeof(int)) плюс много кастингов. Вы всегда можете выполнить трансляцию в/из TKey — сначала выполните трансляцию в object, если не можете сделать это напрямую. Конечно, в отличие от решений в моем ответе, здесь используется бокс.

Sweeper 21.07.2024 16:49

Вы хотите MyCollection<TKey, TObject>, но вам может понадобиться MyCollection<TKey, TFactory, TObject> where TFactory : ISomething<TKey>. Тогда вам не нужно TKey что-либо реализовывать, так что это может быть int или string.

Jeremy Lakeman 22.07.2024 03:18

@JeremyLakeman Да, на самом деле моей первой мыслью было использование трех общих параметров, но это требует, чтобы пользователи постоянно записывали все три параметра типа, и я подумал, что это не очень удобно. Тогда я подумал, что абстрагирование конкретного ключа и неявных операторов преобразования будет хорошим компромиссом.

Sweeper 22.07.2024 03:34

Или просто добавьте аргумент конструктора ISomething<TKey> для явной передачи фабрике. Я думаю, что эти ответы/комментарии суммировали ваши доступные варианты.

Jeremy Lakeman 22.07.2024 03:35

@JeremyLakeman Ты отвечаешь на ygoe? Вероятно, вам следует упомянуть их с помощью «@».

Sweeper 22.07.2024 03:49

Мне кажется, что весь фокус в том, чтобы использовать фабрику Create, которая передает соответствующий метод GetUniqueKey в качестве делегата.

public MyCollection<TKey, TObject> CreateNumberCollection<TKey, TObject>()
        where TKey : INumberBase<TKey> =>
    new MyCollection<TKey, TObject>(lastKey => lastKey++);

public MyCollection<string, TObject> CreateStringCollection<TObject>() =>
    new MyCollection<string, TObject>(lastKey => Guid.NewGuid().ToString());

public MyCollection<Guid, TObject> CreateGuidCollection<TObject>() =>
    new MyCollection<Guid, TObject>(lastKey => Guid.NewGuid());


public class MyCollection<TKey, TObject>
    where TObject : class
    where TKey : notnull, IComparable, IConvertible, IEquatable<TKey>
{
    private Dictionary<TKey, TObject> _items = new();

    private TKey _lastKey;

    private Func<TKey, TKey> _uniqueKeyFactory;

    public MyCollection(Func<TKey, TKey> uniqueKeyFactory)
    {
        _uniqueKeyFactory = uniqueKeyFactory;
    }

    public TKey GetUniqueKey() =>
        _lastKey = _uniqueKeyFactory(_lastKey);
}

Если вы хотите сделать это полностью с помощью одной функции, вы можете сохранить эти делегаты в словаре.

private static Dictionary<Type, Delegate> _factories = new(){
    { typeof(int), new Func<int, int>(lastKey => lastKey++) },
    { typeof(byte), new Func<int, byte>(lastKey => lastKey++) },
    { typeof(double), new Func<int, double>(lastKey => lastKey++) },
    { typeof(Guid), new Func<int, Guid>(lastKey => Guid.NewGuid().ToString()) },
    { typeof(string), new Func<int, string>(lastKey => Guid.NewGuid()) },
};

А потом

    public MyCollection()
    {
        if (!_factories.TryGetValue(typeof(TKey), out var uniqueKeyFactory))
            throw new Exception("Invaid type");

        _uniqueKeyFactory = (Func<TKey, TKey>)uniqueKeyFactory;
    }

Похоже на перекладывание проблемы на вызывающего класса. Кроме того, эти сгенерированные ключи не являются уникальными после добавления некоторых элементов с существующими ключами.

ygoe 22.07.2024 08:40

Ну, кто-то где-то должен решить, какую функцию вызывать. Возможно, вы могли бы сохранить делегаты в статическом словаре и динамически извлекать их. Что касается существующих ключей: у вас всегда будет эта проблема, в вопросе нет никаких указаний на то, что вы вообще об этом думали или что вы хотели бы с этим сделать.

Charlieface 22.07.2024 12:15
Ответ принят как подходящий

Вот что я придумал:

// Restrict TKey to numbers or strings: https://stackoverflow.com/a/30660880
public class MyCollection<TObject, TKey>
    where TObject : class
    where TKey : notnull, IComparable, IConvertible, IEquatable<TKey>
{
    private readonly Dictionary<TKey, TObject> objects = [];

    public TKey GetUniqueKey()
    {
        switch (Type.GetTypeCode(typeof(TKey)))
        {
            case TypeCode.SByte:
            case TypeCode.Byte:
            case TypeCode.Int16:
            case TypeCode.UInt16:
            case TypeCode.Int32:
            case TypeCode.UInt32:
            case TypeCode.Int64:
                if (Count == 0)
                    return (TKey)Convert.ChangeType(1, typeof(TKey));
                return (TKey)Convert.ChangeType(Convert.ToInt64(objects.Keys.Max()) + 1, typeof(TKey));
            case TypeCode.UInt64:
                if (Count == 0)
                    return (TKey)Convert.ChangeType(1, typeof(TKey));
                return (TKey)Convert.ChangeType(Convert.ToUInt64(objects.Keys.Max()) + 1, typeof(TKey));
            case TypeCode.String:
                return (TKey)(object)Guid.NewGuid().ToString();
            default:
                throw new NotSupportedException($"Key type {typeof(TKey).Name} not supported.");
        }
    }
}

Я использую IConvertible для всех чисел для преобразования типов, а также IComparable для Max().

Вас интересует словарь, который может сочетать в одном экземпляре как числа, так и строки? Или конкретный экземпляр всегда имеет фиксированный тип ключа?

silkfire 23.07.2024 19:24
TKey фиксируется в экземпляре. Но я могу хранить объекты с целочисленными и строковыми ключами в отдельных коллекциях. Это далекая замена EF'ам DbSet.
ygoe 24.07.2024 08:20

Другие вопросы по теме