1

I have this class:

public class Transform<PositionType, RotationType, ScaleType>
    where PositionType : Position
    where RotationType : Rotation
    where ScaleType : Scale
{
    public Transform<PositionType, RotationType, ScaleType> Parent;

    public PositionType GlobalPosition;
    // The next line has a compile error: Cannot implicitly convert type 'Position' 
    // to 'PositionType'.  An explicit conversion exists (are you missing a cast?)
    public PositionType LocalPosition => Parent.GlobalPosition - GlobalPosition;

    public RotationType GlobalRotation;
    // The next line has a compile error: Cannot implicitly convert type 'Rotation' 
    // to 'RotationType'. An explicit conversion exists (are you missing a cast?)
    public RotationType LocalRotation => Parent.GlobalRotation - GlobalRotation; 

    public ScaleType GlobalScale;
    // The next line has a compile error: Cannot implicitly convert type 'Scale' 
    // to 'ScaleType'. An explicit conversion exists (are you missing a cast?)
    public ScaleType LocalScale => Parent.GlobalScale - GlobalScale; 
}

Position: (Scale and Rotation are defined in the same way)

public class Position
{
    public Position(int axis)
    {
        Axis = new float[axis];
    }

    public Position(float[] axis)
    {
        Axis = axis;
    }

    public static Position operator -(Position a, Position b)
    {
        if (a.Axis.Length != b.Axis.Length)
        {
            throw new System.Exception("The axis of the two Positions are not comparable.");
        }

        Position difference = new Position(a.Axis);

        for (int i = 0; i < difference.Axis.Length; i++)
        {
            difference.Axis[i] = a.Axis[i] - b.Axis[i];
        }

        return difference;
    }

    public float[] Axis;
}

To me this looks totally valid, so I am confused why it generates a compile time error. What should I do to solve this issue while retaining this functionality?

PixxlMan
  • 69
  • 1
  • 3
  • 11
  • 3
    The error message is pretty specific, and tells you what to do. What part are not understanding? – Rufus L Dec 09 '19 at 18:37
  • keep in mind that just because all `PositionType` instances are also `Position` instances, the reverse may not be true. Hence the error. – Brandon Dec 09 '19 at 18:38
  • 5
    As an aside, these type parameter names would be more idiomatic as `TPosition`, `TRotation`, `TScale`. – Jon Skeet Dec 09 '19 at 18:41
  • @RufusL I don't understand what I am supposed to cast which type to. I have tried to cast Parent.Scale and Scale to ScaleType, but that was considered an unnecessary cast. Should I cast to scale instead? – PixxlMan Dec 09 '19 at 18:50
  • 1
    My guess is that your linear algebra operators (e.g. `Parent.Rotation - Rotation`) aren't constrained to produce the same generic subtype as the incoming types. I.e. `Parent.Rotation - Rotation` isn't guaranteed to be of type `RotationType` even when `Parent.Rotation` and `Rotation` are; it's just guaranteed to be of type `Rotation`. Need to see a [mcve] to be sure. – dbc Dec 09 '19 at 18:52
  • 2
    What's the definition of `Scale`, `Position` and `Rotation`? – Trevor Dec 09 '19 at 18:55
  • The code is confusing since you have named your local fields the same as existing types (you should change that), but presumably `Parent.Position` is not certainly a `PositionType`. Think of classes like `Mammal` and `Dog` and `Cat`, where `Mammal` is similar to `Position` and `Dog` and `Cat` are similar to `PositionType`. If I instantiate an instance of `Transform` with a `Cat`, and it's parent with a `Dog`, you cannot implicitly convert a `Cat` to a `Dog`. When you do `Parent.Position - Position`, both left and right sides of the operator need to convert to the same type. – Rufus L Dec 09 '19 at 18:58
  • And unfortunately c# doesn't support generic operators, see [C# Generic Operators](https://stackoverflow.com/questions/5905563/c-sharp-generic-operators). So there's no way to constrain your `+` operator to always return two rotations (or positions) of the same type. – dbc Dec 09 '19 at 19:01
  • Presumably you'd have to cast them to their common base (or interface): `public PositionType LocalPosition => (Position)Parent.Position - (Position)this.Position;` – Rufus L Dec 09 '19 at 19:02
  • @Çöđěxěŕ I have added the definition – PixxlMan Dec 09 '19 at 19:07
  • 1
    Right, so `Position operator -(Position a, Position b)` is only guaranteed to produce a `Position` not its subclass `PositionType` even when `a` and `b` are of subtype `PositionType`. And in fact there's no way to create such a generic operator `+` straightforwardly in c#, see [C# Generic Operators](https://stackoverflow.com/q/5905563/3744182). – dbc Dec 09 '19 at 19:09
  • @dbc Ouch, how could I solve this? A subtract method instead? A workaround? – PixxlMan Dec 09 '19 at 19:21
  • It's not obvious how to do this because c# doesn't have constraints for parameterized constructors. In order for `TPosition Subtract(this TPosition a, TPosition b) where TPosition : Position` to return a `TPosition` you need to be able to construct the new `TPosition` using the specified `a.Axis`, but there's no way to add a constraint like `TPosition : Position, new(float[])`; see [Is there a generic constructor with parameter constraint in C#?](https://stackoverflow.com/q/1852837). – dbc Dec 09 '19 at 19:27
  • 1
    And is it even true that subtracting two vectors or matrices of the same subtype will produce a new vector or matrix of the same subtype? It's not clear mathematically what you are trying to accomplish here. (incidentally, your code may have a bug: `new Position(a.Axis)` doesn't actually clone the `float[] Axis` array, but maybe it should.) – dbc Dec 09 '19 at 19:29
  • @dbc I've got to admit, I'm not great with this maths, but what I am trying to achieve is (supposed to be) identical to what can be seen in the Vector3 reference source code, https://referencesource.microsoft.com/#System.Numerics/System/Numerics/Vector3.cs,ebfe71bcfc917160 but supporting any amount of axis. How do you mean Position(a.Axis); doesn't clone the array? Looking at the code I'm almost certain it would? – PixxlMan Dec 10 '19 at 20:03

3 Answers3

2

You could potentially do this with an interface, but you will have to implement your subtraction logic in each derived class.

public interface ISubtractable<T>
{
    T Subtract(T subtrahend);
}

public abstract class Position
{
    public float[] Axis;

    public Position(int axis)
    {
        Axis = new float[axis];
    }

    public Position(float[] axis)
    {
        Axis = axis;
    }
}

public class DerivedPosition : Position, ISubtractable<DerivedPosition>
{
    public DerivedPosition(int axis)
        : base(axis)
    { }

    public DerivedPosition(float[] axis)
        : base(axis)
    { }

    public DerivedPosition Subtract(DerivedPosition subtrahend)
    {
        if(Axis == subtrahend.Axis)
        {
            throw new System.Exception("The axis of the two Positions are not comparable.");
        }

        DerivedPosition difference = new DerivedPosition(Axis);

        for (int i = 0; i < difference.Axis.Length; i++)
        {
            difference.Axis[i] = Axis[i] - subtrahend.Axis[i];
        }

        return difference;
    }
}

public class Transform<TPosition, TRotation, TScale>
    where TPosition : ISubtractable<TPosition>
    where TRotation : ISubtractable<TRotation>
    where TScale : ISubtractable<TScale>
{
    public Transform<TPosition, TRotation, TScale> Parent;

    public TPosition GlobalPosition;

    public TPosition LocalPosition => Parent.GlobalPosition.Subtract(GlobalPosition);

    // etc.
}
Derrick Moeller
  • 4,808
  • 2
  • 22
  • 48
1

I could be wrong, but what happens if you change the local variable names to not be the same as the types --- for example

 public PositionType myPosition;
 public PositionType LocalPosition => Parent.myPosition - myPosition;
Hogan
  • 69,564
  • 10
  • 76
  • 117
  • Changing the naming did not solve the issue, but it did make the code more readable. I will update the question. – PixxlMan Dec 09 '19 at 19:03
1

What's your use case look like, it's not clear to me that you even needed to make this generic? Are Position, Rotation, and Scale base classes?

public class Transform
{
    public Transform Parent;

    public Position GlobalPosition;

    public Position LocalPosition => Parent.GlobalPosition - GlobalPosition;

    public Rotation GlobalRotation;

    public Rotation LocalRotation => Parent.GlobalRotation - GlobalRotation; 

    public Scale GlobalScale;

    public Scale LocalScale => Parent.GlobalScale - GlobalScale; 
}
Derrick Moeller
  • 4,808
  • 2
  • 22
  • 48
  • Yes they are base classes. My intention is that there can then be classes like "Point3D" where the size of "axis" is 3, and properties for X Y and Z just refer to Axis[0] and [1] Aso. – PixxlMan Dec 09 '19 at 19:44