0

I made a dictionary-like container that supports key-wise arithmetic operations. I want it to "act" like a value type in the sense that it can be added, subtracted, and multiplied by other instances of the same container.

I'm making a set of stats containers for a game. The aforementioned arithmetic dictionary is to be the base class for all of the different stats containers (i.e. stats, stat multipliers, equipment stat multipliers, level up bonuses, etc.) using a common enum for their keys and a variety of data types for their values (i.e. integers, doubles, and number-like custom classes).

public class ArithmeticDictionary<TKey,TValue> : 
    IEnumerable<KeyValuePair<TKey,TValue>>,
    System.Numerics.IAdditionOperators<
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>>,
    System.Numerics.ISubtractionOperators<
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>>,
    System.Numerics.IMultiplyOperators<
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>>,
    System.Numerics.IComparisonOperators<
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>,
        bool>,
    System.Numerics.IMinMaxValue<
        ArithmeticDictionary<TKey,TValue>>,
    System.Numerics.IAdditiveIdentity<
        ArithmeticDictionary<TKey,TValue>,
        ArithmeticDictionary<TKey,TValue>>
where TKey : notnull
where TValue : 
    System.Numerics.IAdditionOperators<TValue,TValue,TValue>,
    System.Numerics.ISubtractionOperators<TValue,TValue,TValue>,
    System.Numerics.IMultiplyOperators<TValue,TValue,TValue>,
    System.Numerics.IComparisonOperators<TValue,TValue,bool>,
    System.Numerics.IMinMaxValue<TValue>,
    System.Numerics.IAdditiveIdentity<TValue,TValue>
{}

The functionality is all there, but every time I subclass the generic base class, I get a whole bunch of the same warning.

Here, Stat is an enum.

public class Stats : 
    ArithmeticDictionary<Stat, uint>,
    IEnumerable<KeyValuePair<Stat,uint>>,
    System.Numerics.IAdditionOperators<Stats,Stats,Stats>,
    System.Numerics.ISubtractionOperators<Stats,Stats,Stats>,
    System.Numerics.IMultiplyOperators<Stats,Stats,Stats>,
    System.Numerics.IComparisonOperators<Stats,Stats,bool>,
    System.Numerics.IMinMaxValue<Stats>,
    System.Numerics.IAdditiveIdentity<Stats,Stats>
{}

warning CA2260: The 'ArithmeticDictionary<TKey, TValue>' requires the 'TKey' type parameter to be filled with the derived type 'Stats'

From the warning, it seems to be due to ArithmeticDictionary implementing a number of interfaces that have a TSelf type parameter, but child classes are no longer of the same type.

My code still works, but 5 subclasses with 6 warnings each is a bit much.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
Ryan Chou
  • 23
  • 4
  • 1
    So you have declared that `Stats` implements both `IAdditionOperators` (which you havent) and `IAdditionOperators` (which the base type does) ? – Jeremy Lakeman May 16 '23 at 14:11
  • 2
    I think you're trying to implement a "curiously recursive" template, but you don't have a placeholder for the descendant type eg `Stats : ArithmeticDictionary` – Jeremy Lakeman May 16 '23 at 14:13
  • @JeremyLakeman I came to the same conclusion, and posted my answer before I saw your comment. – Menno van Lavieren May 16 '23 at 14:40

1 Answers1

2

Add a TSelf type parameter to IArithmeticDictionary. Inline with the answer to this question. Adding operator support to interfaces (Preview Feature in .NET 6)

I put some example code below that compiles, but ommits simmilar cases for brevity.

public interface IArithmeticDictionary<TSelf, TKey, TValue> :
    IEnumerable<KeyValuePair<TKey, TValue>>,
    System.Numerics.IAdditionOperators<TSelf,
        TSelf,
        TSelf>
where TSelf : IArithmeticDictionary<TSelf, TKey, TValue>
where TKey : notnull
where TValue :
    System.Numerics.IAdditionOperators<TValue, TValue, TValue>
{
   // Optional
    public static TSelf operator +(IArithmeticDictionary<TSelf, TKey, TValue> left, TSelf right)
    {
        throw new NotImplementedException();
    }
    // Optional
    static TSelf operator checked +(IArithmeticDictionary<TSelf, TKey, TValue> left, TSelf right)
    {
        throw new NotImplementedException();
    }
}

public class Stats :
    IArithmeticDictionary<Stats, Stat, uint>
{
    public IEnumerator<KeyValuePair<Stat, uint>> GetEnumerator()
    {
        throw new NotImplementedException();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }

    public static Stats operator +(Stats left, Stats right)
    {
        throw new NotImplementedException();
    }
}

public enum Stat
{
    A,
    B
}
  • Interesting, so the numerics interfaces should be implemented by other interfaces not classes? If I hypothetically I wanted to make a class, would I simply inherit the interface? `ArithmeticDictionary : IArithmeticDictionary, TKey, TValue>` Like how there's an interface `IDictionary` and also a type `Dictionary` – Ryan Chou May 16 '23 at 23:58
  • You don't need the interface, you can just implement the one type. But the generic constraints are the same. – Jeremy Lakeman May 17 '23 at 00:40
  • Why can't the child class inherit the interface's default operators if TSelf inherits the interface? i.e. why do I have to implement the addition operator on every child if the parent already has a seemingly compatible implementation? – Ryan Chou May 17 '23 at 00:58
  • Maybe I was presuming to much, in a sense that it would be difficult for ArithmeticDictionary to provide a sensible implementation. And therefore made it into an interface. If you keep it as a class you can implement it there. But after some trying myself I still need to defer the creating of the return value of the operator to the implementation classes. It still doesn't hurt to also make an interface. – Menno van Lavieren May 17 '23 at 09:37
  • Making it an interface fixed all the errors so thank you! However, implementing the interface feels like I'm breaking every rule to not repeat myself because each type that implements the interface is copied and pasted with a few find-and-replace fixes to type declarations. i.e. `StatMultipliers` was just a copy of `Stats` but with `Stats` replaced with `StatMultipliers` and `uint` replaced with `double` (plus a couple operator overloads). I'm coming from a python background where subclassing can inherit literally any method, property, field, operator, etc. so this feels extremely redundant. – Ryan Chou May 18 '23 at 02:38