Reputation: 147
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
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
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
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:
TView
to be inferred from an argumentTView
as a type argumentT0
and T1
as type argumentsYou 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
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