Pattern Matching on Generic Type Parameters #1225
Replies: 15 comments 5 replies
-
What are the use cases for wanting to do this? I can't think of any that don't involve reflection of some type. I've proposed additional APIs to the BCL team to help reason about generic types. This scenario could pretty easily be covered by one of those APIs: var Samples = new object[] {
new List<String>(), 32, null, new List<Int>(), new List<Person>(), "",
};
foreach(var item in Samples){
if(item != null && item.GetType().Is(typeof(List<>), out Type elementType)) {
Console.WriteLine($@"Item is a list of {elementType}");
}
} |
Beta Was this translation helpful? Give feedback.
-
with recursive patterns it would get a lot less verbose: if (item is (typeof(List<>), {var elementType})) {
Console.WriteLine($"item is a list of {elementType}");
} which is equivalent to: item.Deconstruct(out var type, out var args);
if (type == typeof(List<>) && args.Length == 1) {
var elementType = args[0]
Console.WriteLine($"item is a list of {typeArgument}");
} using a Deconstruct method: static void Deconstruct(this object obj, out Type openType, out Type[] typeArguments) {
var t = obj.GetType();
openType = t.GetGenericTypeDefinition();
typeArguments = t.GenericTypeArguments;
} PS: probably better to define the extension on Assert.True(typeof(A<B<int>>) is (typeof(A<>), {(typeof(B<>), {typeof(int)})})); |
Beta Was this translation helpful? Give feedback.
-
Just brainstorming, is this suggesting that the following somehow work, effectively extending the concept of generic type parameters to local variables? public void AddIfCompatible(object list, object obj)
{
if (list is List<var T> listT && objToInsert is T objT)
{
listT.Add(objT);
}
} And if that works then would type constraints be allowed? if (list is List<var T> listT where T : int)
{ ... } I'm having a hard time justifying this over simply adding a generic type parameter to the method. Keeping the API backwards compatible without introducing an overload (e.g. the method also works on non-generic lists)? |
Beta Was this translation helpful? Give feedback.
-
@HaloFour -
|
Beta Was this translation helpful? Give feedback.
-
Matching specific parameters is certainly possible with my API: if(item != null
&& item.GetType().Is(typeof(Dictionary<,>), out Type keyType, out Type valueType)
&& keyType.Equals(typeof(int)) { ... } That was a fairly common use case in my project. You frequently move generic type parameters around? Even so, I can't imagine that there are enough use cases to justify a special syntax for pattern matching generic type arguments. Given that whatever this pattern would be used for likely involves reflection odds are that would still break during said refactoring anyway. Between recursive patterns (and hopefully parameterized active patterns) you should be able to mix type matching in with pattern matching anyway, and hopefully with fairly decent syntax. |
Beta Was this translation helpful? Give feedback.
-
@TonyValenti How would this work? For example, how do you imagine this would be translated into existing C#? Or if that's not possible, into IL? Or if that's not possible, how would it be translated into machine language? |
Beta Was this translation helpful? Give feedback.
-
@HaloFour wondering why use a method on GetType() instead of extending the 'is' keyword as originally proposed by @TonyValenti ? Syntax seems nicer with the 'is' keyword. I have often wanted this feature. When a generic type is defined one often wants to know whether or not a given object is "one of those" in order to provide special treatment such as invoking a certain method on the object (the method does not necessarily have the type parameter in its signature). The only other obvious approach to this is to factor out a base class of the generic type. There are many situations in which it is not very convenient to do that or where one is working with a pre-existing type. |
Beta Was this translation helpful? Give feedback.
-
FWIW, and for the benefit of anyone else finding this question, you can obtain the runtime type of a generic object (within certain limitations) at runtime and without using reflection. The key is to remember that dynamic defers method overload resolution until runtime. Taking the original example, and expanding it slightly to make something that compiles today: using System;
using System.Collections.Generic;
namespace DynamicDemo
{
public static class Program
{
public static void Main()
{
var Samples = new object[]
{
new List<string>(), 32, null, new List<int>(), new List<Person>(), "",
};
foreach(var item in Samples)
{
dynamic d = item;
PrintIt(d);
}
}
private static void PrintIt(object obj)
{
Console.WriteLine($"Have object: {obj}");
}
private static void PrintIt<T>(List<T> list)
{
Console.WriteLine($"Have list of type: {typeof(T).Name}");
}
}
public class Person
{
// ... elided ...
}
} Output is this:
I've used this technique to good effect a handful of times. As mentioned above, this technique does have a number of limitations - but it's faster than reflection, arguably easier to read than using reflection, and achieves many of the goals as the original request. |
Beta Was this translation helpful? Give feedback.
-
@theunrepentantgeek pretty cool! Comment, the type T in what you show has to be known in advance. In the original posting, I think the type is not known. I like the idea of a syntax identifying the reflected type, for example: if (item is IList<Type t>) { ... } @HaloFour I think your APIs are worthy of having language syntax for them. Obviously a few other people have thought that having syntax for this would be a good idea, and you also are saying it is a common use case. I feel it would make for much more intuitive and streamlined coding. I don't see anything wrong with the idea of "is" being extended to have reflection-based uses. The pattern-matched variable ("list" in the following example) could be dynamic and then depending on the amount of analysis done by the compiler one could generate static errors in some cases.
In the above one can also imagine writing "x is t xx" and then having xx be dynamic but again restricted to invoking members of type t. In this case the errors would be run-time only since obviously there is no compile-time information about t. Not saying all this is easy to do :-). |
Beta Was this translation helpful? Give feedback.
-
This would be great feature to have. Take, for example, following situation: I have bunch of interfaces that entity class can implement, like public static void IsTimestamped<T>(this EntityTypeBuilder<T> builder)
where T : class, ITimestampedEntity
{
builder.HasIndex(x => x.DateCreated);
builder.Property(x => x.DateCreated).IsRequired();
}
public static void IsSoftDeletable<T>(this EntityTypeBuilder<T> builder)
where T : class, ISoftDeletableEntity
{
builder.HasIndex(x => x.IsDeleted);
builder.Property(x => x.IsDeleted).IsRequired();
} etc. Now I would love to write public static void ApplyAllPossible<T>(this EntityTypeBuilder<T> builder)
where T : class
{
if (T is ITimestampedEntity)
{
IsTimestamped(builder);
}
if (T is ISoftDeletableEntity)
{
IsSoftDeletable(builder);
}
// etc...
} Or, slightly different: public static void ApplyAllPossible<T>(this EntityTypeBuilder<T> builder)
where T : class
{
if (builder is EntityTypeBuilder<T2> castedBuilder where T2 : ITimestampedEntity)
{
IsTimestamped(casteBuilder);
}
if (T is EntityTypeBuilder<T2> castedBuilder where T2 : ISoftDeletableEntity)
{
IsSoftDeletable(castedBuilder);
}
// etc...
} |
Beta Was this translation helpful? Give feedback.
-
If pattern matching on generic types happens, I'd like to see it be capable of something more or less like: public static bool HasValue<T>(T value)
{
if (T : class)
{
return value is not null;
}
else if (T is Nullable<var U>)
{
return value.HasValue; // this compiles because the compiler knows that value is of type T and that T is Nullable<something>
}
else if (T : struct)
{
return true;
}
else
{
throw new InvalidOperationException();
}
} |
Beta Was this translation helpful? Give feedback.
-
Pattern matching on generic types would be seriously useful for scenarios where you don't have an instance, which could be covered by the dynamic binder already, but are working directly on a type. I have some code here which needs to create a dynamic property accessor that can either be pointing to an In an ideal world, I'd be able to: public class HandlerFactory<TRoot>
{
public static bool TryCreateHandler<TValue>(string propertyName, out IHandler<TRoot, TValue> handler)
=> TValue is IEnumerable<var TItem>
? new MultiItemHandler<TItem>()
: new SingleItemHandler<TValue>();
private sealed class MultiItemHandler<TItem> : IHandler<TRoot, IEnumerable<TItem>>
{
// ...
}
private sealed class SingleItemHandler<TValue> : IHandler<TRoot, TValue>
{
// ...
}
} Alas that doesn't exist, so either I have to take on a heap of reflection magic creating generic delegates and recasting their type parameters. (Libraries exist for this. But I still hate using it. Feels like black magic.) Or, I have to use type constraints and a second generic parameter, i.e. public class HandlerFactory<TRoot>
{
public static bool TryCreateHandler<TValue>(string propertyName, out IHandler<TRoot, TValue> handler)
=> new SingleItemHandler<TValue>();
public static bool TryCreateHandler<TCollection, TValue>(string propertyName, out IHandler<TRoot, TValue> handler)
where TCollection : IEnumerable<TValue>
=> new MultiItemHandler<TValue>();
private sealed class MultiItemHandler<TItem> : IHandler<TRoot, IEnumerable<TItem>>
{
// ...
}
private sealed class SingleItemHandler<TValue> : IHandler<TRoot, TValue>
{
// ...
}
} But then, any callers also have to supply both arguments - which is really redundant and an awful API surface. |
Beta Was this translation helpful? Give feedback.
-
This feature would be a handy improvement for generic math. I have an Avalonia value converter for turning numeric types into hexadecimal numbers. It would be convenient if this worked: class HexConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is IBinaryInteger<> val && targetType.IsAssignableTo(typeof(string)) && parameter is string precision)
{
return val.ToString("X" + precision, null);
}
return new BindingNotification(new InvalidCastException(), BindingErrorType.Error);
}
} But it doesn't, so at the moment I need to test for necessary integer types by hand instead of merely testing for |
Beta Was this translation helpful? Give feedback.
-
After seeing this pop back up again, and thinking about it again - I now realize: If a type implements both if (foo is IEnumerable<var T>)
{
// ...
} use as the type Getting this to work correctly would require C# to have support for TypeScript-like type unions and type intersections, because then |
Beta Was this translation helpful? Give feedback.
-
@rjgotten good point! I’ve often thought that type intersections and unions would be a very useful thing to have. Having said that, it would be quite the departure for c#. A simpler approach would be an array output |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I'd like to be able to write code like this:
Basically I'd like to be able to easily detect and extract type parameters.
Beta Was this translation helpful? Give feedback.
All reactions