Kai Rohmer
Kai Rohmer

Reputation: 147

C# advanced constrains on generic types

im looking for a nice way to a synthactic problem. I want to realize a navigation system that allows to switch between different Views. The next view should be specified by its type TView (not an instance of that type). The view then will be initialized with some generic arguments

public abstract class ViewBase { }

public abstract class ViewBase<T0, T1> : ViewBase 
{
    public abstract void Initialize(T0 arg0, T1 arg1);
}

public static class Navigator
{
    public static void Navigate<TView, T0, T1>(T0 arg0, T1 arg1) where TView : ViewBase<T0, T1>
    {
        var view = (ViewBase<T0, T1>)Activator.CreateInstance<TView>();
        view.Initialize(arg0, arg1);

        /* ... */
    }
}

The code above should work, but I don't like the long list of generic types each time I call Navigate:

Navigate<DerivedView, string, int>("Joe", 42);

Assuming a derived class:

public class DerivedView : ViewBase<string, int> 
{
    /* ... */
}

In genereal it could be possible to reduce the to:

Navigate<DerivedView>("Joe", 42);

because the type information is redundant.

Do you have any suggestions? I'm missing variadic templates ;)

Thanks

Upvotes: 0

Views: 128

Answers (4)

John Alexiou
John Alexiou

Reputation: 29274

Consider also using a generic factory. I cache the constructor call signatures for faster retrieval the second time.

public static class Factory
{
    //store constructors for type T
    static Dictionary<string, ConstructorInfo> ctors=new Dictionary<string, ConstructorInfo>();

    public static T New<T>(params object[] args)
    {
        var arg_types=args.Select((obj) => obj.GetType());
        var arg_types_names=args.Select((obj) => obj.GetType().Name);

        string key=string.Format("{0}({1})", typeof(T).Name, string.Join(",", arg_types_names);

        if(!ctors.ContainsKey(key))
        {
            // if constructor not found yet, assign it
            var ctor=typeof(T).GetConstructor(arg_types.ToArray());
            if(ctor==null)
            {
                throw new MissingMethodException("Could not find appropriate constructor");
            }
            ctors[key]=ctor;
        }
        // invoke constructor to create a new T
        return (T)ctors[key].Invoke(args);
    }                
}

Upvotes: 0

Kai Rohmer
Kai Rohmer

Reputation: 147

Besides the presented approaches I came up with creating the object from the ViewBase class:

public abstract class ViewBase<T0, T1>
{
    public abstract void Initialize(T0 arg0, T1 arg1);

    public static T Navigate<T>(T0 arg0, T1 arg1) where T : ViewBase<T0, T1>
    {
        var view = Activator.CreateInstance<T>();
        view.Initialize(arg0, arg1);
        return view;
    }
}

this allows the following call:

DerivedView.Navigate<DerivedView>("Joe", 42);

but this also comes with redundant code.. or:

ViewBase.Navigate<DerivedView>("Joe", 42); 

Upvotes: 0

user743382
user743382

Reputation:

Type inference is all or nothing. If any type arguments are specified, they must all be specified. Therefore, you need a way to either:

  • allow TView to be inferred from an argument
  • avoid having TView as a type argument
  • avoid having T0 and T1 as type arguments
  • find a way to split the type arguments

You can allow TView to be inferred by passing in a factory function to specify TView's type:

public static void Navigate<TView, T0, T1>(Func<TView> factory, T0 arg0, T1 arg1)
        where TView : ViewBase<T0, T1> {
    var view = factory();
    view.Initialize(arg0, arg1);
    // ...
}

Now, instead of Navigate<DerivedView>("Joe", 42), call Navigate(() => new DerivedView(), "Joe", 42).

Note: this requires the types of arg0 and arg1 to match exactly to what TView specifies. Implicit conversions will not work. If the view derives from ViewBase<int, object>, and arg1 is "Hello", then T1 will be inferred as type string, causing a compiler error message.


Since you avoid using TView's type other than to construct the instance, you can avoid having TView as a type argument by using a factory function too:

public static void Navigate<T0, T1>(Func<ViewBase<T0, T1>> factory, T0 arg0, T1 arg1) {
    var view = factory();
    view.Initialize(arg0, arg1);
    // ...
}

This also avoids the problem with implicit conversions, since both T0 and T1 can be deduced from factory.


As for avoiding T0 and T1 as type arguments: the name Initialize somewhat suggests that its functionality may belong in a constructor. If that's the case for you, you can trivially avoid them as type arguments by leaving the construction to the caller: the factory function could be () => new DerivedView("Joe", 42). However, if that's not the case for you, if you really need a separate Initialize method that gets called by Navigate, then I don't see any options that avoid T0 and T1 as type arguments.


Finally, splitting the type arguments:

public static class Navigator {
    public static WithViewHelper<TView> WithView<TView>()
            where TView : new() => new WithViewHelper<TView>();
    public struct WithViewHelper<TView> where TView : new() { }
}
public static class NavigatorExtensions {
    public static void Navigate<TView, T0, T1>(this Navigator.WithViewHelper<TView> withViewHelper, T0 arg0, T1 arg1)
            where TView : ViewBase<T0, T1>, new() {
        var view = new TView();
        view.Initialize(arg0, arg1);
    }
}

This allows you to call Navigator.WithView<DerivedView>().Navigate("Joe", 42). It needs Navigate to be an extension method because otherwise the generic type constraint cannot be expressed. It has problems again with implicit conversions: Navigator.WithView<DerivedView>().Navigate(null, 42) will fail to compile: even though null is convertible to string, the compiler will not figure out that T0 should be string.

Upvotes: 1

Adrian Zanescu
Adrian Zanescu

Reputation: 8008

I guess what you want is a constraint that boils down to:

class Generic<T> where T : Generic2<T1, T2> and have access to T1, T2 in your Generic member signatures.

Unfortunately this is not currently supported in C#.

Upvotes: 0

Related Questions