У меня есть коллекция объектов с уникальным ключом. Этот ключ может быть числом или строкой, он общий, и большую часть класса это не волнует, потому что он хранится в 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.");
}
}
Можно ли это вообще сделать?
MAX+1 — не лучший способ генерации уникальных ключей. Это гарантирует дубликаты, если элементы будут удалены. Гораздо лучшим способом было бы сохранить последний сгенерированный идентификатор и каждый раз увеличивать его.
Хм, я не понимаю, как это гарантирует дубликаты, если генерирует только значение, которого на данный момент никогда не существует. Я знаю, что это нельзя сравнивать с последовательностями SQL. Это только локальное хранилище данных.
Вы действительно можете использовать здесь общие математические 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 Я не понимаю, почему вы не можете использовать собственные типы. Какое отношение к этому имеет фраза «ключ — это одно из свойств объектов в моей коллекции»? Добавление оператора неявного преобразования тоже не поможет? Суть в том, что вы проверяете каждый числовой тип вручную. например if (typeof(TKey) == typeof(int))
плюс много кастингов. Вы всегда можете выполнить трансляцию в/из TKey
— сначала выполните трансляцию в object
, если не можете сделать это напрямую. Конечно, в отличие от решений в моем ответе, здесь используется бокс.
Вы хотите MyCollection<TKey, TObject>
, но вам может понадобиться MyCollection<TKey, TFactory, TObject> where TFactory : ISomething<TKey>
. Тогда вам не нужно TKey
что-либо реализовывать, так что это может быть int
или string
.
@JeremyLakeman Да, на самом деле моей первой мыслью было использование трех общих параметров, но это требует, чтобы пользователи постоянно записывали все три параметра типа, и я подумал, что это не очень удобно. Тогда я подумал, что абстрагирование конкретного ключа и неявных операторов преобразования будет хорошим компромиссом.
Или просто добавьте аргумент конструктора ISomething<TKey>
для явной передачи фабрике. Я думаю, что эти ответы/комментарии суммировали ваши доступные варианты.
@JeremyLakeman Ты отвечаешь на ygoe? Вероятно, вам следует упомянуть их с помощью «@».
Мне кажется, что весь фокус в том, чтобы использовать фабрику 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;
}
Похоже на перекладывание проблемы на вызывающего класса. Кроме того, эти сгенерированные ключи не являются уникальными после добавления некоторых элементов с существующими ключами.
Ну, кто-то где-то должен решить, какую функцию вызывать. Возможно, вы могли бы сохранить делегаты в статическом словаре и динамически извлекать их. Что касается существующих ключей: у вас всегда будет эта проблема, в вопросе нет никаких указаний на то, что вы вообще об этом думали или что вы хотели бы с этим сделать.
Вот что я придумал:
// 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()
.
Вас интересует словарь, который может сочетать в одном экземпляре как числа, так и строки? Или конкретный экземпляр всегда имеет фиксированный тип ключа?
TKey
фиксируется в экземпляре. Но я могу хранить объекты с целочисленными и строковыми ключами в отдельных коллекциях. Это далекая замена EF'ам DbSet
.
Пока нет, но это в процессе...