Expose a way to bridge generic constraints #6308
Replies: 24 comments 75 replies
-
CC. @stephentoub, @jaredpar This has been raised a few times and does particularly impact the ability to use things like the new This is a rough draft starting point on the topic and would be great if we could flesh it out and get championed. |
Beta Was this translation helpful? Give feedback.
-
Related previous discussion: #905. |
Beta Was this translation helpful? Give feedback.
-
I don't think that would be possible without changing the meaning of existing code, e.g. SharpLab |
Beta Was this translation helpful? Give feedback.
-
What if instead of using
We use template specialization like :
or even |
Beta Was this translation helpful? Give feedback.
-
The combination with static interface method is also very useful:
|
Beta Was this translation helpful? Give feedback.
-
This would be a very important missing link to make static interface members really useful. I proposed something very similar: public static bool TryParse<T>(this string? s, IFormatProvider? provider, out T? value)
{
// New code: Shortcut if T is IParseable<T> (now illegal because T is a type, not an instance):
if (T is IParseable<T> TParseable) // TParseable is a constrained type argument now
return TParseable.TryParse(s, provider, out value);
// Original code: trying to use TypeConverters as fallback etc...
} I went a bit further by using |
Beta Was this translation helpful? Give feedback.
-
I think new c# people are just doing what they want to do until they can't, instead of looking for new things to do in the documentations, so when they try to convince themselves therefore hoping the compiler would infer a type, they provide hints / evidence which usually is a cast operation, the pattern matching works beautifully in this case because when the newbies realizes their cast is going to throw an exception and they need to check for it somehow, they are home |
Beta Was this translation helpful? Give feedback.
-
I've also proposed (#3050) a similar idea which would incorporate it (in my opinion quite nicely) into the existing pattern-matching facilities in the language. This specific issue could be solved: if(default(T) is new TUnmanaged where TUnmanaged : unmanaged)
{
// here TUnmanaged exists
} I can imagine it being possible to write |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
Trying to implement math operators over collection of numbers is a problem if the I have a signature public static TProbability Expectation<T, TProbability>(this Distribution<T, TProbability> source, int samplesCount)
where TProbability : IFloatingPoint<TProbability>
where T : INumber<T>,IDivisionOperators<T, TProbability, TProbability> to handle a Distrubiton over a numeric domain T and a signature public static TProbability Expectation<T, TProbability>(this Distribution<T, TProbability> source, int samplesCount)
where TProbability : IFloatingPoint<TProbability> that would handle any other case using a probability density function that maps T to TProbability. This is not possible as the generic constraint is not helping. Same problem comes up when using matching expression since is not possible to use generic constraints in defining the list of case. Being able to create ad hoc code for System.Single and System.Double (taking into account the Same would expand even more in multidimensional structures like vector and matrix. |
Beta Was this translation helpful? Give feedback.
-
I was recently experimenting with the new abstract statics in interfaces and came across this limitation. The imposibility to check if a type Also, even if I could test it, I can't access the members of |
Beta Was this translation helpful? Give feedback.
-
I'm bumping into this limitation as well. |
Beta Was this translation helpful? Give feedback.
-
I would love for this to be possible. |
Beta Was this translation helpful? Give feedback.
-
+1 for this feature. The pattern matching on type parameter can also contribute to redundant branch elision optimization in JIT, which can improve the performance significantly. For example, T GetSmallestChange<T>() where T : INumber<T>
{
if (T is IFloatingPoint<T> TF)
{
return TF.Epsilon;
}
else return T.One;
} In JIT time the concrete |
Beta Was this translation helpful? Give feedback.
-
+1, I've run into this a million times. |
Beta Was this translation helpful? Give feedback.
-
I need this as well as explained here: #6838 |
Beta Was this translation helpful? Give feedback.
-
In F# generics are inferred... |
Beta Was this translation helpful? Give feedback.
-
Would this be applicable to whole methods? public class Program
{
public static void Main()
{
new Vector2<float>(3, 4).Normalise(); // method exists
new Vector2<int>(3, 4).Normalise() // method shouldn't exist
}
}
public struct Vector2<T>
where T : INumber<T>, IConvertible
{
public T X;
public T Y;
public float Length => MathF.Sqrt(LengthSquared.ToSingle(null));
public T LengthSquared => X * X + Y * Y;
public Vector2(T x, T y)
{
X = x;
Y = y;
}
public void Normalise()
where T : IFloatingPoint<T> // not currently allowed
{
if (LengthSquared == T.Zero)
return;
var scale = T.CreateChecked(1 / Length);
X *= scale;
Y *= scale;
}
} |
Beta Was this translation helpful? Give feedback.
-
Using the if ((T is not IFoo && T is IFoo2) || x == y)
{
} Would this overwhelm the compiler/runtime with complexity? If not, ignore the rest of this comment :) If so, maybe allowing the void M<T>()
{
where T : unmanaged
{
N<T>(); // This is now valid
}
N<T>(); // This remains invalid and errors
}
void N<T>()
where T : unmanaged
{
} Theoretically, you would be able to use the same combinations we already have (multiple constraints on a single generic type, multiple generic types being constrained). The only difference is that the |
Beta Was this translation helpful? Give feedback.
-
A further consideration about this feature: if we can allow generic constrained members on T, why couldn't we allow treating variables as concrete types under a similar check? Consider the following snippet: if (T is int)
{
return 42;
} instead of: if (typeof(T) == typeof(int))
{
return (T)(object)42;
} |
Beta Was this translation helpful? Give feedback.
-
Not having this is a particularly big problem in NativeAOT where you can't rely on reflection to force the transition. https://github.com/dotnet/runtime/issues/95953 |
Beta Was this translation helpful? Give feedback.
-
Going to add a few changes to the core sample to help reveal some of the design points I think are interesting:
void M<T>(T p) {
if (T is I1) {
// Now valid
N<T>(p);
}
// Error
N<T>(p);
}
void N<U>(U p)
where U : I1 {
p.M();
}
interface I1 {
void M();
} It's important to establish why Establishing why void G(object o) {
if (o is string) {
// Error!
string str = o;
} The if (o is string s) {
// s is a valid identifier here of type string
if (T is T1 where I1) {
// T1 is a valid type identifier here which has the `I1` constraint
N<T1>();
} Inside the void M<T>(T p) {
if (T is T1 where I1) {
T1 local = p;
p = local;
}
} That system also seemingly works well if you extend this feature set to testing for values. For example: void M<T>(T p) {
if (p is T1 where I1) {
T1 local = p;
p.M(); // works
}
if (p is T2 where I1 p2) {
p2.M(); // works
}
} One part I'm curious about is what are some concrete samples where we're dealing with type only as the original sample does? I can definitely see where this would be valuable when dealing with values but struggling to see the value for types only. Asking because if the feature is limited to values only, the Issues that are still on my mind:
|
Beta Was this translation helpful? Give feedback.
-
IndexOf and other methods that needs a comparer in MemoryExtensions does not have corresponding lEqualityComparer<> overrides, instead they all forces the equality interface by a generic constraint, now I need the capability discussed here to force the call like this:
|
Beta Was this translation helpful? Give feedback.
-
This can also be used for HKT purpose: void Foo<T>(T x)
{
if (T is IEnumerable<var U where INumber<U>> V)
{
var enumerable = x as V;
var sum = U.Zero;
foreach (var i in enumerable) sum += i;
}
} |
Beta Was this translation helpful? Give feedback.
-
Bridging Generic Constraints
Summary
C# should expose a way to "bridge" generic constraints such that a method
void M<T>() { }
can successfully call a methodvoid N<T>() where T : struct { }
given an appropriate check.Motivation
Generics were introduced in .NET Framework 2.0 and since then there has been no way to go from a less constrained to a more constrained environment. Additionally, since the initial release there have been several new constraints introduced (such as
unmanaged
) and several new "core interfaces" (such asINumber<T>
) exposed through which users may want to take advantage of functionality. However, given that changing an existing generic constraint (especially to be more restrictive) is most often a binary breaking change, there is no way for these existing generic APIs to utilize this newer functionality and they are effectively "left behind".Design
C# should expose a syntax that allows a developer to perform a check against a generic type and within the scope of that check they then have ability to call other generic methods that fit the constraint. As an example:
This would require selecting a syntax, performing the relevant code flow analysis (ideally utilizing some of the flow analysis introduced for nullable annotations to reduce the work required), and ensuring that relevant "well known" APIs exist in the runtime such that the check can be done and the runtime will treat the call as "legal".
For syntax, utilizing the
is
keyword seems somewhat natural given that is already how certain type checks are done by the language. However, there are other alternative syntaxes that might be possible or better suited instead. For the checks there should at least be a way "bridge" the following constraints:where T : class
where T : struct
where T : unmanaged
where T : Type
Bridging other constraints, such as
where T : new()
may also be desirable but might also be significantly more complex to get support for/around. The ones immediately listed above can at least be "trivially" handled using existing APIs expose bytypeof(T)
.Open Questions
Should this only work against generic types (given
T
, you can doif (T is unmanaged)
) or should it also work against generic values (givenT x
, you can doif (x is unmanaged)
)?T x
andT y
,if (x is INumber<T>)
doesn't imply the same fory
).What is required in the runtime to make these checks performant and valid?
constrained
prefix before the call sufficient, similarly to what we do forstatic abstract
calls?How simple or complex should the flow analysis for these types be?
NRT
does with regards tonull checks
would be a good starting point.How do such checks play with NRTs given that
where T : class
andwhere T : class?
are both valid?Beta Was this translation helpful? Give feedback.
All reactions