imekon
imekon

Reputation: 1523

Generic constraints with C#

I wanted to create a generic (sic) solution to have a class with a bunch of properties. These properties should be simple types (bool, int, float etc) or complex types (vector, colour etc.). They should all have a means to parse them from text to their type.

The one complex type I'm going to use here is Vector:

public class Vector
{
    private float _x, _y, _z;

    public Vector(float x, float y, float z)
    {
        _x = x;
        _y = y;
        _z = z;
    }

    public Vector() : this(0.0f, 0.0f, 0.0f)
    {

    }

    public float X
    {
        get { return _x; }
        set { _x = value; }
    }

    public float Y
    {
        get { return _y; }
        set { _y = value; }
    }

    public float Z
    {
        get { return _z; }
        set { _z = value; }
    }
}

Here's my base class for parameter, which just gives it a name:

public class Parameter
{
    protected string _name;

    public Parameter() : this("untitled")
    {

    }

    public Parameter(string name)
    {
        _name = name;
    }

    public string Name => _name;
}

Here's the derived class which adds a value of generic type TType, which has a constraint:

public class Parameter<TType> : Parameter where TType : ParameterValue<TType>
{
    public Parameter(string name, TType value) : base(name)
    {
        Value = value;
    }

    public TType Value { get; set; }
}

Now here's the ParameterValue generic class which ensures all TType objects have a parse function

public abstract class ParameterValue<TType>
{
    protected TType _value;

    public ParameterValue(TType value)
    {
        _value = value;
    }

    public TType Value
    {
        get { return _value; }
        set { _value = value; }
    }

    public abstract void Parse(string text);
}

Here's a definition for string:

public class StringValue : ParameterValue<string>
{
    public StringValue(string value) : base(value)
    {

    }

    public override void Parse(string text)
    {
        _value = text;
    }
}

Here's a definition for Vector:

public class VectorValue : ParameterValue<Vector>
{
    public VectorValue(Vector value) : base(value)
    {

    }

    public override void Parse(string text)
    {
        var tokens = text.Split(',');

        var x = float.Parse(tokens[0]);
        var y = float.Parse(tokens[1]);
        var z = float.Parse(tokens[2]);

        _value = new Vector(x, y, z);
    }
}

Here's my manager class which contains all the parameters:

public class ParameterManager
{
    private Dictionary<string, Parameter> _parameters;

    public ParameterManager()
    {
        _parameters = new Dictionary<string, Parameter>();
    }

    public void AddParameter<TType>(string name, TType value) where TType : ParameterValue<TType>
    {
        _parameters[name] = new Parameter<TType>(name, value);
    }

    public TType FindParameterValue<TType>(string name) where TType : ParameterValue<TType>
    {
        var parameter = _parameters[name];
        var parameterTyped = parameter as Parameter<TType>;
        return parameterTyped?.Value;
    }
}

Now, if I create a class that uses ParamaterManager, I hit problems:

public class Thing
{
    private ParameterManager _parameters;

    public Thing()
    {
        _parameters = new ParameterManager();

        _parameters.AddParameter("name", new StringValue("untitled"));
        _parameters.AddParameter("position", new VectorValue(new Vector()));
    }
}

The two lines adding parameters "name" and "position" throw errors:

1>...\Thing.cs(11,13,11,37): error CS0311: The type 'ParameterProblem.StringValue' cannot be used as type parameter 'TType' in the generic type or method 'ParameterManager.AddParameter<TType>(string, TType)'. There is no implicit reference conversion from 'ParameterProblem.StringValue' to 'ParameterProblem.ParameterValue<ParameterProblem.StringValue>'.
1>...\Thing.cs(12,13,12,37): error CS0311: The type 'ParameterProblem.VectorValue' cannot be used as type parameter 'TType' in the generic type or method 'ParameterManager.AddParameter<TType>(string, TType)'. There is no implicit reference conversion from 'ParameterProblem.VectorValue' to 'ParameterProblem.ParameterValue<ParameterProblem.VectorValue>'.

How do I get this to do what I wanted?

Upvotes: 0

Views: 83

Answers (2)

EvilTak
EvilTak

Reputation: 7579

where TType : ParameterValue<TType>

This is a recursive generic constraint, which will simplify to TType : ParameterValue<XYZParameterValue> where XYZParameterValue : ParameterValue<TType> which is not what you want, because in your case the actual type (e.g. string) does not inherit its corresponding ParameterValue (ParameterValue<string>).

Your generic constraints would work when using a generic interface/base class which is implemented/inherited by the same type which it is generic over, like the IComparable<T> interface which is implemented by the type T (i.e. System.String : IComparable<System.String>).

Instead, I'd do the following:

public class Parameter<T> : Parameter
{
    public Parameter(string name, ParameterValue<T> value) : base(name)
    {
        Value = value;
    }

    public ParameterValue<T> Value { get; set; }
}

You'd have to change ParameterManager methods to a similar form too:

public void AddParameter<T>(string name, ParameterValue<T> value)
{
    _parameters[name] = new Parameter<TType>(name, value);
}

public ParameterValue<T> FindParameterValue<T>(string name) 
{
    var parameter = _parameters[name];
    var parameterTyped = parameter as Parameter<TType>;
    return parameterTyped?.Value;
}

Side Note: Naming the type constraint TType doesn't make any sense by general conventions since the T prefix in a type parameter means "type" and so T would be enough.

Upvotes: 1

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

Your constraint should be replaced with a change in the second parameter type:

public void AddParameter<TType>(string name, ParameterValue<TType> value)

The calls should be done like this:

_parameters.AddParameter<string>("name", new StringValue("untitled"));
_parameters.AddParameter<Vector>("position", new VectorValue(new Vector()));

Upvotes: 1

Related Questions