Question

How to add numbers of a generic type

I have a collection of objects that have a unique key. That key could be numbers or a string, it's generic and most of the class doesn't care because it's stored in a Dictionary<TKey, TItem>.

Now the class should provide a method to return a new unique key for an item to be added. That's where I can't find a solution. I've tried to read up on the new generic math feature of C# but that doesn't make any sense to me.

I'm looking for something like the GetUniqueKey method below:

// 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.");
    }
}

Can this be done at all?

 3  119  3
1 Jan 1970

Solution

 1

It seems to me that the trick is to use a Create factory, which passes in an appropriate GetUniqueKey method as a delegate

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);
}

If you want to do this entirely from a single function then you could store those delegates in a dictionary.

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()) },
};

And then

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

        _uniqueKeyFactory = (Func<TKey, TKey>)uniqueKeyFactory;
    }
2024-07-21
Charlieface

Solution

 0

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 dynamically 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();
    }
}
2024-07-21
Sweeper

Solution

 0

This is what I came up with:

// 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.");
        }
    }
}

I makes use of IConvertible for all numbers to convert the types, any also IComparable for Max().

2024-07-23
ygoe