You can indeed use the generic math APIs here, but you will still need some way to convince the compiler that TKey
is a number. For the string
case, you can simply check typeof(TKey) == typeof(string)
and do some casts.
Here is an example doing this by dynamic
ally binding to a helper method:
private Dictionary<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;
}
Another approach is to abstract away the idea that keys must be strings or numbers. Create an IKey
interface that requires its implementations to have the behaviours you need:
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
}
Then MyCollection
can be as simple as:
class MyCollection<TKey, TObject>
where TObject : class
where TKey : IKey<TKey>
{
private Dictionary<TKey, TObject> items = ...;
public TKey GetUniqueKey()
{
return TKey.GetUniqueKey(items.Keys);
}
}
Now you just need to implement numeric keys and string keys as simple wrappers of numeric types and string
. Again, you can use generic math to implement numeric keys.
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...
}
If you don't mind MyCollection
having 3 type parameters, you can do this without changing TKey
to wrapper types.
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();
}
}