Using the new() constraint is a symptom of the XY problem
Contents
C# allows you to write generic code with a few constraints that can’t be expressed through the type hierarchy alone. For example, you can require that a generic type parameter only accepts types that are structs, or reference types, or non-abstract types that have a public default constructor:
|
|
That last case is what we’re interested in for this post.
When a generic type parameter has the new()
constraint, it can be used to call the default constructor and create new instances of that type:
|
|
Many C# programmers, including myself, have wondered at one point or another why the language doesn’t allow us to declare a requirement for arbitrary non-default constructors as well. This question usually comes up because you may stumble upon a situation where you’re writing generic code that needs to construct instances of certain type, but those types either don’t provide public default constructors, or they do but they don’t have a common interface that provides the ability to subsequently pass a value to the constructed object.
In this post, I’m not going to examine why C# doesn’t support richer variations of the new()
constraint. In this post, I’m going to argue that running into this issue altogether is a potential symptom of the infamous “XY problem”. To be precise, the XY problem is described as “asking about your attempted solution rather than your actual problem”. I’m going to use a slightly more general description and pretend that the XY problem is about “focusing on your attempted solution rather than your actual problem” (without necessarily asking on the Internet). Phrased differently, I would say that the XY problem is about focusing on a certain concrete solution (a solution with many specific characteristics) even though your problem is not equally specific. Thus, many of the attempted solution’s specific qualities are irrelevant to the problem at hand and they become limitations that you impose on yourself unnecessarily.
Examination of new()
Whenever you use the new()
constraint, or when you stumble upon the need for a richer new()
constraint, you clearly have a problem to which your attempted solution is “I want to ensure I can get new objects of type T
on demand by invoking a public constructor”. That solution is a special case of the solution “I want to ensure I can get new objects of type T
on demand by invoking a constructor” (not necessarily a public one). That solution is a special case of the solution ”I want to ensure I can get new objects of type T
on demand” (even if I don’t directly invoke a constructor). And that, in turn, is a special case of the solution “I want to ensure I can get objects of type T
on demand” (even if they are not guaranteed to have been constructed on my demand).
And there is a language feature that represents exactly that: delegates.
Func<T>
is a type for any operation that can be invoked without any arguments from its caller and will return a T
when invoked. By requiring that a Func<T>
be supplied to your generic code, you don’t need to specify the new()
constraint anymore. You only need to store the Func<T>
(in a local variable or in a field) and then invoke it anywhere you would have otherwise used a new T()
expression:
|
|
Of course, the caller will now need to supply a delegate, probably in the form of a lambda expression:
|
|
Advantages of Func<T>
Forcing the client code to provide a lambda expression as an additional formal argument to your generic method or to the constructor of your generic type seems more cumbersome at first. But it’s a very small price to pay for the flexibility you get as a result.
One example of this flexibility is that the client code isn’t forced to provide types with a public default constructor anymore. It can now keep its constructors non-public (e.g. private, internal, or protected) and give you access to them through the delegate.
Another advantage is that the client code can pass you any kind of expression, like a factory method:
|
|
Alternatively, the type T
can now expose any constructor it wants and the client code gets to select which constructor it wants you to invoke. If a non-default constructor is selected, the lambda can carry the required arguments as captured variables:
|
|
The delegate remains parameterless, so your generic code only has the responsibility of invoking it without having to pass anything.
When you need richer forms of the new()
constraint, delegates are still the general solution. For example, if you feel that you need a new(string)
constraint for your generic type argument T
, you can simply require that the client code provides you with a Func<string, T>
. Even further, you can choose to take a Func<TArg, T>
, where TArg
is another generic type parameter of your generic method or type.
Conclusion
I would say that using the new()
constraint or thinking that you need a richer new()
constraint is an indication that you’re focusing on a specific solution (“I need a specific public constructor”) instead of your problem (“I need some way to get instances of a type”). More likely than not, you should step back and reassess what your actual requirements are.
Remember that the new()
constraint is a remnant from an age when C# didn’t have lambdas. By now it might as well be considered obsolete, since lambdas supersede it elegantly.
Author Theodoros Chatzigiannakis
LastMod 2016-12-26