How do I correctly constrain to related class type when using a generic method?

Viewed 62

I have two base classes BaseObject and BaseObjectSettings. The first defines the object behaviour and the second defines the state of the class (useful for serialisation).

If I want to create a derived BaseObject class with specific settings then I can use a method with a generic type constraint.

public void CreateBaseObjectInstance<T>(BaseObjectSettings baseObjectSettings) where T : BaseObject
{
    var instance = pool.GetInstance<T>();
    instance.Settings = baseObjectSettings;
    scene.Add(instance);
}

The problem I am facing is that while I can constrain the generic type to BaseClass I can't constrain the BaseClassSettings to the relevant derived BaseClass. This means that I can do things like

CreateBaseObjectInstance<Banana>(new AppleSettings());

which seems a bit terrible.

What are my options given that I am currently constrained to both creating and initialising the object in the same method before adding it to the scene?

3 Answers

One way is to have all your settings classes inherit from a generic base class. The generic base class could then inherit from BaseObjectSettings. The generic type parameter indicates what kind of object this settings class is for.

For example, for your AppleSettings,

class AppleSettings: ObjectSettings<Apple> {
    ...
}

abstract class ObjectSettings<T>: BaseObjectSettings where T: BaseObject {}

Now, you can change CreateBaseObjectInstance to accept an instance of ObjectSettings<T> instead:

public void CreateBaseObjectInstance<T>(ObjectSettings<T> objectSettings) where T : BaseObject
{
    var instance = pool.GetInstance<T>();
    instance.Settings = objectSettings;
    scene.Add(instance);
}

If you pass Banana as T, it would expect ObjectSettings<Banana>, preventing you from giving it AppleSettings, which is ObjectSettings<Apple>.

I don't really understand the logic here as things are missing, but from the code provided you can probably write:

public void CreateBaseObjectInstance<TBase, TSettings>(TSettings baseObjectSettings)
where TBase : BaseObject 
where TSettings : BaseObjectSettings 

Used like that:

CreateBaseObjectInstance<Banana, AppleSettings>(new AppleSettings());

Can be improved to:

public void CreateBaseObjectInstance<TBase, TSettings>(TSettings baseObjectSettings)
where TBase : BaseObject 
where TSettings : BaseObjectSettings, new()
{
  if ( baseObjectSettings == null ) baseObjectSettings = new TSettings();
  ...
}

CreateBaseObjectInstance<Banana, AppleSettings>();

But if there is a strong coupling between entity and settings, you should redesign to define dependency with an association using a thing that can also be similar to @Sweeper's and @Moho's answers:

Association, Composition and Aggregation in C#

Understanding the Aggregation, Association, Composition

Generics in .NET

Generic classes and methods

You need to create a generic interface or base class that where you define the settings type:

public class BaseObject<TSettings>
{
    public TSettings Settings { get; set; }
}

Then your method will require two generic arguments - one for the actual object to create TObject and one for method's argument for the settings TSettings. You then constrain TObject to an implementation of the implemented interface or base class/derivation thereof, using generic argument TSettings as the constraint's type's generic argument

public void CreateBaseObjectInstance<TObject, TSettings>(
    TSettings settings
)
    where TObject : BaseObject<TSettings>
{
    ...
}

Example (using above BaseObject implementation):

public class MyObjectSettings
{
    ...
}

public class MyObject : BaseObject<MyObjectSettigns>
{
}

Method call:

var settings = new MyObjectSettings(){ ... };

CreateBaseObjectInstance<MyObject>( settings );  // second generic argument should be inferred
Related