Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Generic Argument Wildcards #1992

Open
mikernet opened this issue Nov 10, 2018 · 46 comments
Open

Proposal: Generic Argument Wildcards #1992

mikernet opened this issue Nov 10, 2018 · 46 comments

Comments

@mikernet
Copy link

@mikernet mikernet commented Nov 10, 2018

IMPORTANT: This proposal is very different than Java wildcards in terms of its implementation and behavior. It looks similar on the surface but it's a much more elegant solution than extending variance. It follows all existing variance rules and takes advantage of the CLR generic type system to transform the block of code using the unknown type into a generic method that is JITed to use the ACTUAL types of the generic parameters. "Extracted" types can therefore be safely used just like any other generic parameter and everything is compile-time verified so you can't do something wacky like try to pass an object into a method that takes a string without getting a compiler error.

Details:

I've run into many situations where this would be extremely useful but I'll give a couple to start things off:

A) Eliminate the need for a non-generic interface in addition to a suitable generic interface

It would help avoid the giant mess often created when generic types need to inherit non-generic versions of the same type in order to be usable when the generic type argument is unknown, and if implemented in an ideal manner where the JIT would emit different versions of the method as needed then it would also eliminate boxing in lots of situations.

Hypothetical example - non-generic IEnumerable interface would no longer be needed:

void IterateIfPossible(object obj)
{
    if (obj is IEnumerable<?> items) 
    {
        foreach (var item in items)
            Console.WriteLine(item);
    }
}

This would remove the need to implement a non-generic interface with a bunch of methods that just cast the generic argument typed return value/parameter to object.

In this case var would be treated as an anonymous generic parameter. The next example shows how to extract that parameter so it can be used like any other generic parameter.

B) Allow extraction of the unknown type:

void Type GetItemType(IEnumerable obj)
{
    if (obj is IEnumerable<?T>) 
    {
        return typeof(T);
    }
    return typeof(object);
}

C) Test if an object inherits a generic type or implements a generic interface and if so use the value in its strongly typed form:

I don't believe there is currently any way to avoid very messy and slow reflection code to test if an object implements, say, IList<T>, and if so then pass it to a method that requires an IList<T>. In order to do this:

private IItemsProcessor GetItemsProcessor(object items)
{
    // Optimize for List<T>
    if (items is List<?T> list)                
        return new ListProcessor<T>(list);

    if (items is IEnumerable<?T> enumerable)
        return new EnumerableProcessor<T>(enumerable);
}

I currently have to do this:

private IItemsProcessor GetItemsProcessor(object items)
{
    var itemsType = items.GetType();
    // GetBaseTypes() is an extension method
    var listType = itemsType.GetBaseTypes().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(List<>));

    // Optimize for indexable collections
    if (listType != null) 
    {
        var itemType = listType.GetGenericArguments()[0];
        var processorType = typeof(ListProcessor<>).MakeGenericType(itemType);

        return (IItemsProcessor)Activator.CreateInstance(processorType, items);
    }

    var enumerableType = itemsType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));

    if (enumerableType != null)
    {
        var itemType = listType.GetGenericArguments()[0];
        var processorType = typeof(EnumerableProcessor<>).MakeGenericType(itemType);

        return (IItemsProcessor)Activator.CreateInstance(processorType, items);
    }
}

D) Allow generic constraints:

void GetDisplayValue(Control control)
{
    if (control is LabelControl<?T> label where T : IDisplay) 
    {
        // LabelControl.Value is a property of type T
        // GetDisplayText() is a method on IDisplay
        return label.Value.GetDisplayText(); 
    }
}

This would let you avoid a lot of potential casting and allow method overload resolution to use methods that have compatible constraints on their generic parameters when the unknown value is passed in:

void Serialize<T>(Field<T> field) where T : class
{
    Serializer.Serialize<T>(field);
}

// ERROR: can't differentiate just on generic constraints
void Serialize<T>(Field<T> field) where T : struct
{
    // But I need to be able to call a method with a T : struct constraint!
    Serializer.FastSerializeValueType<T>(field);
}

// But this would work:

void Serialize(Field field)
{
    if (field is Field<?TValue> valueField where TValue : struct)
    {
        Serializer.FastSerializeValueType<TValue>(field);
    }
    else if (field is Field<?TRef> refField where TRef : class)
    {
        Serializer.Serialize<TRef>(field);
    }
}

Implementation

This is a simple demonstration of a roughly equivalent code transform that could be applied to the last example by the compiler to make it work:

void Serialize<T>(Field field)
{
    try 
    {
        Serialize_struct((dynamic)field);
        return;
    }
    catch (RuntimeBinderException) { } 

    try 
    {
        Serialize_class((dynamic)field);
    }
    catch (RuntimeBinderException) { }   
}

void Serialize_struct<T>(Field<T> field) where T : struct
{
    Serializer.FastSerializeValueType<T>(field);
}

void Serialize_class<T>(Field<T> field) where T : class
{
    Serializer.Serialize<T>(field);
}

Dynamic is quite quick here but the dynamic callsite code it outputs could definitely be optimized more (i.e. cutting out the overhead of the try/catch). There may be other optimizations that apply for this specific situation (known single method to call, only varies by generic parameters).

If there are any CLR enhancements that can make it a bit quicker but those can just be stubbed in by newer runtimes but even without any CLR changes this can be very fast.

@HaloFour

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Nov 10, 2018

This only works in Java as the generic type arguments don't exist at runtime. The JRE only recognizes the type Iterable, it is entirely unaware of whether it was Iterable<String> or Iterable<Integer> or Iterable<?>. In the CLR that is not the case. The generic type arguments are reified into the actual type, so IEnumerable<string> and IEnumerable<int> are entirely different and incompatible types.

I will agree that the reflection APIs in the BCL leave a lot to be desired when it comes to generics. I've advocated for better APIs here: https://github.com/dotnet/corefx/issues/644

@ufcpp

This comment has been minimized.

Copy link

@ufcpp ufcpp commented Nov 10, 2018

non-generic IEnumerable interface is not needed:

using System;
using System.Collections.Generic;

public class Program
{
    static void Main()
    {
        Console.WriteLine(IsEnumerable(new string[1])); // true
        Console.WriteLine(IsEnumerable(new Program[1])); // true
        Console.WriteLine(IsEnumerable(new List<string>())); // true
        Console.WriteLine(IsEnumerable(new List<Program>())); // true
    }

    static bool IsEnumerable(object obj) => obj is IEnumerable<object> items;
}
@HaloFour

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Nov 10, 2018

@ufcpp

Console.WriteLine(IsEnumerable(new int[1])); // false
Console.WriteLine(IsEnumerable(new List<int>())); // false
@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

@HaloFour I'm pointing to Java as an example of a similar feature. Regardless of how or why it works on Java, there are ways to make this work cleanly on the CLR as well. When I have a bit more time later today or tomorrow I'll follow up with a demonstration of how it could be done.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

@ufcpp That only works in that particular case because the generic part of IEnumerable is defined as <out T>. Try that with an interface without the out part :)

As I stated, IEnumerable was just a hypothetical example. Sadly I doubt IEnumerable and the mess of other collection interfaces is going anywhere anytime soon, but my framework would benefit immensely from this.

@HaloFour

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Nov 10, 2018

@mikernet

I'm pointing to Java as an example of a similar feature. Regardless of how or why it works on Java

The how is important. The Java language has this ability specifically because of how generics work in that language. C# can't work around the variance limitations very intentionally imposed by the CLR. Nor should it.

there are ways to make this work cleanly on the CLR as well.

Possibly, but it's exceptionally difficult to justify CLR modifications to support a language change unless the benefit is absolutely enormous and it can't be solved any other way. This is especially true given that it seems that the .NET Framework will never see such modifications.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

@HaloFour I think eliminating the need for redundant non-generic interfaces in 99% of situations where they are currently needed would be a huge win. That's obviously subjective. There are ways to make this work fairly well without CLR cooperation but optimally perhaps it might help a bit. Wildcard generic arguments would be useful for more than just C#. Regardless, I presume the runtime isn't doomed to never evolve out of its current state for all eternity just because the .NET Framework isn't evolving anymore. That would be kind of silly.

I never suggested C# should work around the variance limitations imposed by the CLR. I don't think anything I've suggested conceptually requires breaking any variance rules.

I'll go into more detail about potential ways to implement this soon.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

@HaloFour Updated my post with a simple implementation to demonstrate viability. I'll play around with ways to optimize this further on an IL level when I have more time.

Can we agree that my example transformation shows that an implementation is possible which doesn't involve breaking any "variance rules" and everything behaves like you would intuitively expect?

I haven't 100% determined how well it can be optimized yet but my gut tells me that specializing the call site caching code dynamic generates with a custom implementation to handle this particular case optimally (i.e. calling the generated generic method with the correct generic argument) could be "very fast", and any increase in execution time caused by this "generic wildcard cast" (compared to a plain non-generic interface cast) would be made up by avoiding boxing for value types for most non-trivial cases.

The real benefit would come from cleaning up interface related code though. Even if it's at the expense of a tiny amount of performance in some situations that would likely only be practically measurable in microbenchmarks, I still think this is how most people would opt to handle generic interfaces with unknown type arguments in a large majority of cases considering that the only real alternative is a bunch of mindless casting boilerplate interface bloat.

@svick

This comment has been minimized.

Copy link
Contributor

@svick svick commented Nov 10, 2018

if the dynamic-based implementation is acceptable, then I'm not sure this feature is needed, because I think this code:

void IterateIfPossible(object obj)
{
    if (obj is IEnumerable<?T> items where T : IFriendlyDisplay) 
    {
        foreach (T item in items)
            Console.WriteLine($"'{item.FriendlyDisplayText}' is type '{typeof(T)}'");
    }
}

is not that much better than this code, which works today:

void IterateIfPossible(object obj) => IterateIfPossibleImpl((dynamic)obj);

void IterateIfPossibleImpl<T>(IEnumerable<T> items) where T : IFriendlyDisplay
{
    foreach (T item in items)
        Console.WriteLine($"'{item.FriendlyDisplayText}' is type '{typeof(T)}'");
}

void IterateIfPossibleImpl(object _) { }
@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

@svick I tend to disagree that it's not much better. That example was rather simple just to demonstrate the idea and doesn't require capturing locals between any of the methods and only has a single unknown type with a single unknown parameter, doesn't handle mismatched constraints, etc. This gets very messy very quickly and the alternate pattern you've presented is rather obtuse for anything but the simplest case and I can't imagine very many people adopting it for many reasons, especially for more complicated flows. I can give you some examples if you like but most practical usages would require significantly more obtuse implementations. Even in the simple example your code fails if passed an object that implements IEnumerable<T> but the constraint is not met.

The alternate syntax I'm proposing is terse, simple, easily understood and would encourage people to use what I would argue is a much better design that avoids writing redundant non-generic interfaces where they aren't needed. It can be tuned to minimize any performance impact further than the dynamic implementation which is important for encouraging its use even in situations where you're only making one to a few calls on the object you're working with.

If I have to choose between writing code code like the transformation you've shown everywhere (along with all the additional boilerplate you've left off, such as checking for constraint violations) vs creating and implementing a non-generic base interface, I'll do the latter almost 100% of the time. Dynamic has been around for a while but how often do you see code like this? More importantly, I would never write a library that required consumers of my library to write code like this so non-generic interfaces are still added everywhere, resulting in a similar mess as the collection interfaces in .NET.

Conversely, if I got to choose between writing the proposed new syntax vs writing and implementing a non-generic base interface, I would do the former almost 100% of the time. If this became a language feature then it would become the standard way of handling unknown type parameters and we would all be better off for it, dealing with non-generic base interfaces sucks. I've seen many posts complaining about having to implement non-generic versions of IEnumerable or ICollection along with the generic versions. All that backwards compatibility stuff means that will probably never change but at least we can avoid the same problems for new interface hierarchies we're designing.

As I indicated before, that was just a demonstration to show how it could conceptually work. I don't think dynamic is how this would actually be implemented, there are too many issues with it. I know this can be emulated with dynamic and some leg work, but that's not the point - this is more about enabling a shift in how developers design interfaces so that we get nicer code to write and nicer code to consume. Solutions to the "problem" this is solving exist, as we've both shown, but nobody is going to stop writing redundant non-generic interfaces or stop requiring me to implement them with these existing solutions.

This is akin to async/await. We could always write asynchronous code before it, but who actually wanted to? Moving state between each method and splitting the logic between multiple methods was tedious, annoying and made reasoning about the flow difficult. The method you've proposed has all the same problems as async programming did which is why nobody will use it. This proposal fixes that and makes for a more enjoyable C# coding experience :)

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

Here's an example of one more common problem that this transformation could solve nice and cleanly:

        void DoSomething<T>(IEnumerable<T> items) where T : class
        {
        }

        // ERROR: can't differentiate just on generic constraints
        void DoSomething<T>(IEnumerable<T> items) where T : struct
        {
            // But I need to be able to call a method that only takes value types in this case!
            ValueTypeOps.MethodWithValueTypeConstraint<T>(items.First());
        }

        // But this would work:

        void DoSomething<T>(IEnumerable<T> items)
        {
            if (items is IEnumerable<?TValue> valueItems where TValue : struct)
            {
                ValueTypeOps.MethodWithValueTypeConstraint<TValue>(items.First());
                return;
            }

            // do something with reference type items
        }

I believe that would solve this problem in an ideal way, performance and design wise.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

@svick P.S. your version of the transform which eliminates the type checking line is for all intents and purposes just as fast as the non-generic code in my test. This tells me that an optimized implementation of the new syntax could be effectively indistinguishable in performance vs using non-generic interfaces.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

One last point before I dip out for a while.

This would fundamentally change how generic hierarchies are designed and consumed in many cases and eliminate a lot of necessary redundancy and casting boilerplate which propagates throughout some massive class and interface hierarchies. That seems like a much more meaningful proposal than all this pattern matching stuff yet it seems like we are getting pattern matching so I don't understand the negativity towards this. It seems to me this would have wider reaching improvements than pattern matching and the workarounds are more complicated and more tedious than the workarounds for achieving what pattern matching does so I really don't see why this wouldn't be strongly considered.

@svick

This comment has been minimized.

Copy link
Contributor

@svick svick commented Nov 10, 2018

@mikernet I'm not convinced that this is as big a problem as you make it out to be. Sure, a non-generic version of a generic interface is sometimes useful, but creating and using them doesn't require that much boilerplate code (and it will be even less with default interface methods #52).

If you actually need type parameter extraction, then that is harder to do, but I'm not sure that's actually common.

As for negativity, I think that most proposals here will receive a negative initial reaction. If you want to have the proposal to progresses further, it's up to you to convince us, or rather, someone from the Language Design Committee, that it's a change worth making.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

Fair enough regarding the convincing, haha. Default interface implementations are an orthogonal issue to this - we can't create a default non-generic interface implementation that somehow forwards the calls to the unknown generic interface method, so it does nothing to solve this problem.

I think this is a perfect extension to the pattern matching work going on (and could probably help with making that even more powerful) and solves problems and design tradeoffs that I literally face every day building performance critical "lower level" library/framework code.

Even if it's not a big deal (which I disagree about), neither is declaring a variable before you use it for an out parameter or 90% of the other things being added to make programmer's lives easier.

I'll give you an example of some code in my library. I have a bunch of field types, and this is the hierarchy:

FieldComputedFieldComputedField<T>ReferenceFieldEntityCollectionFieldEntityListFieldEntityListField<T>EntitySetFieldEntitySetField<T>EntityFieldEntityField<T>TypeConstantFieldTypeConstantField<T>ValueFieldValueField<T>ValueListFieldValueListField<T>

All the non-generic types (except for Field) along with their associated abstract method definitions and overrides in the generic versions would go away if I could just say if (field is ValueField<?T> valueField) instead of having to do if (field is ValueField valueField) and then using abstract calls that do casting and all sorts of nasty boilerplate to work. It would probably cut the total number of lines in those files by half and make it way easier to reason about what is going on by eliminating the need to jump around between abstract method implementations in the generic type and common functionality in the base type when it could all be contained in the generic type. Lots of abstract method calls would be avoided as well as casting and boxing.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

The particular example above is an especially good one for the following reason: the design of the above classes is exponentially complicated because the hierarchy can't just go Field - Field<T> - [all the field types]. I can't have a base Field<T> because I can't inherit a non-generic ValueField from that generic type, and I need the non-generic type to do things common to value fields when I don't know what the value field type is. Likewise I can't have a generic base class EntityCollectionField<T> that does all common things on collections of T because I need to have a non-generic EntityListField type to support operations on EntityListField<T>.

If the language feature I'm proposing was available, the above hierarchy would simplify down to this:

FieldField<T>ComputedField<T>ReferenceField<T>EntityCollectionField<T>EntityListField<T>EntitySetField<T>EntityField<T>TypeConstantField<T>ValueField<T>ValueListField<T>

If I need to do operations on any EntityListField<T> I can just do so directly without needing a base EntityListField class that declares a bunch of abstract members that only EntityListField<T> implements, does a bunch of casting and conversions and other annoying and useless stuff to make everything work, and splits very tightly coupled logic into two separate classes.

The amount of boilerplate code and error prone inheritance (which requires significant additional testing as well) that this would cut down on would be huge. Even more important than just cutting down boilerplate is how much easier the code would be to understand without the giant inheritance mess. The cognitive load that all this stuff adds to a complex framework like this cannot be understated. Turning all the code in these classes into a flustercuck by "manually" transforming it into the format you suggested as an alternative that works now would be ludicrous and impossible to follow for anyone.

Because of the way I would be able to organize code with this proposal, pretty much all the situations where I really wished I had a flexible form of multiple inheritance would also go away, and this is way easier and cleaner with less gotchas IMO than multiple inheritance and doesn't require CLR changes.

It seems like a no-brainer to me but I understand that's because of the kind of code I work on regularly. There are just so many benefits to this, I'm just scratching the surface here.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 10, 2018

Is that a more suitable example to demonstrate why I really want this? Haha

@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Nov 12, 2018

I wonder if the people who introduced Wildcards to Java think that exercise should be repeated for C#.

@HaloFour

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Nov 12, 2018

@gafter

I wonder if the people who introduced Wildcards to Java think that exercise should be repeated for C#.

That might depend on whether or not you patented it. 😁

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 12, 2018

@gafter I don't follow 😕 Care to elaborate?

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 12, 2018

@gafter Are you genuinely asking if they might have some input or are you making that statement tongue-in-cheek? I can't tell 😆

@YairHalberstadt

This comment has been minimized.

Copy link
Contributor

@YairHalberstadt YairHalberstadt commented Nov 12, 2018

@mikernet.
Take a look at the authors of that paper...

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 12, 2018

@gafter Alright well, what do the people who added it to Java think?

@HaloFour

This comment has been minimized.

Copy link
Contributor

@HaloFour HaloFour commented Nov 12, 2018

@mikernet

Alright well, what do the people who added it to Java think?

In case it was missed, Neal Gafter (@gafter) is a member of the C# language design team, and Mads Torgersen is the program manager of that team.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 12, 2018

@HaloFour I definitely got that now, that's why I asked him what they think :)

@gafter

This comment has been minimized.

Copy link
Member

@gafter gafter commented Nov 13, 2018

I think declaration-site variance (i.e. in and out on the declaration of interface type parameters) is superior to use-site variance (i.e. wildcards), and having variance at both the declaration site and at the use site would be even worse.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

@gafter Why? I don't see how in/out help me do any of the above.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

It also only works on interfaces and reference types, effectively making it useless everywhere I wish I had it :/ Is there some proposal I'm unaware of that will change either of those limitations, and is there anything on the horizon for extracting the unknown type parameter in a meaningful way and/or filtering it based on constraints? If not then this proposal is almost completely unrelated to in / out so I'm not sure how superiority is even relevant, they serve entirely different purposes as far as I can tell.

@huoyaoyuan

This comment has been minimized.

Copy link

@huoyaoyuan huoyaoyuan commented Nov 13, 2018

Everything is boxed in Java's generics. So if you use reference type only, you can get most functionalities via in and out.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

@huoyaoyuan I'm not sure what you're saying, can you clarify please? If you look at my proposed implementation here, nothing gets boxed so I'm not sure how Java's implementation is relevant. I can't get the most functionality via in / out with reference types because I need to support basic value types in my interfaces.

@huoyaoyuan

This comment has been minimized.

Copy link

@huoyaoyuan huoyaoyuan commented Nov 13, 2018

@mikernet Java's generic doesn't support basic value type int. You must use the boxed version Integer.
That's because everything boxed are stored as "a reference/pointer", but int and float are not the same as reference. Only "a reference to int" can be treated same in CPU.
in and out are in fact CPU magics without wrappers, because everything are same when operating a reference.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

@huoyaoyuan Yes I'm aware of all that. I'm still not sure what you're getting at though. I know why in / out only work for reference types, that's part of why this proposal exists...it gets around that limitation in a rather elegant way.

@ufcpp

This comment has been minimized.

Copy link

@ufcpp ufcpp commented Nov 13, 2018

@mikernet
ValueType can't share generic instantiation. So, ValueType can't be variant regardless whether declaration-site or use-site.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

@ufcpp As I've said many times already, I know. What does that have to do with this proposal?

@gafter @ufcpp I think it's important to distinguish my proposal from the Java implementation. This is similar in some respects on the face of it, but it's completely different than what Java has. It doesn't change how variance currently works, can be implemented entirely within the current CLR type system and is 100% compile-time type verifiable in the sense that you can't do anything wacky like try to pass an object into a method that actually takes a string without a compiler error. The wildcard isn't an object variant of the generic type parameter...it's a strongly typed anonymous generic parameter extracted from the type. I think my sample implementation in the OP makes that very clear, and I've stated this many times in this thread. I don't know why we are talking about variance.

This is basically just supporting one more form of dynamic dispatch in a way that applies very well to generics.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

I'll say this one more time so it's clear - this proposal has nothing to do with variance. It is taking advantage of the CLR generic type system to dispatch to the appropriate generic method which handles the wildcard/extracted type parameters in their actual type form. Existing variance rules are NOT being affected here and I'm not asking for variance, I'm asking for a generic dynamic dispatch feature.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

I think the "Java has a similar feature" opening line is throwing everyone off...so I've updated the original post to better reflect what this proposal actually is. I changed some of the examples to demonstrate more uses with classes because I think focusing on interfaces and variance is also throwing things off here.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

Please carefully review the example implementation at the bottom of the first post to understand how variance does not come into play here. It's simply not required at all for this thanks to the CLR generic type system. It could work today, even with classes (which don't support variance) with the transformation I have provided.

@jveselka

This comment has been minimized.

Copy link

@jveselka jveselka commented Nov 13, 2018

@mikernet How do You propose it should work when type implements generic interface multiple times?

class MultipleEnumerable : IEnumerable<string>, IEnumerable<int> { ... }
...
var obj = new MultipleEnumerable()
if (obj is IEnumerable<?T>) 
    {
        return typeof(T); //what does it return?
    }
@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

@zippec Probably the same way dynamic works now in the same situation / how method overload resolution works when trying to match it to a method - throw error "The type arguments cannot be inferred from the usage."

EDIT: Nevermind, I changed my mind further down.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

Or simply pass over it so that you don't have to worry about exceptions being thrown? That probably makes more sense. I don't know - I'm open to whatever, I think this situation is exceedingly rare.

@jnm2

This comment has been minimized.

Copy link
Contributor

@jnm2 jnm2 commented Nov 13, 2018

@zippec Good point. This is logically a foreach kind of operation, not an if kind of operation. This could be glossed over, but should it be?

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

Given that doing this causes all sorts of problems in lots of situations, I don't think anyone ever actually does this, and if they do then they can expect wacky behavior all over the place. Personally, I don't think allowing a type to implement the same generic interface multiple times with different type parameters should have ever been allowed but hey here we are, so whatever behavior is standardized here is fine by me.

I don't think throwing an error is appropriate because you are logically just asking "can I use the object as this type", and that operation should never really throw an error. Either just pick the first matching interface that works, or don't match and pass over it.

Mind you this is different than this situation:

class SingleEnumerable : IEnumerable<string> { ... }
class MultipleEnumerable : SingleEnumerable, IEnumerable<int> { ... }

var obj = new MultipleEnumerable()
if (obj is IEnumerable<?T>) 
    {
        return typeof(T); //what does it return?
    }

In that case obviously just return int.

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

IEnumerable<Type> GetEnumerableTypes(IEnumerable collection)
{
    foreach (IEnumerable<?T> items in obj)
    {
        Debug.WriteLine($"Collection has {items.Count()} number of {typeof(T)} items.");
        yield return typeof(T);
    }
}

🤣 🤣 🤣

@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Nov 13, 2018

I'm laughing, but that's actually not too bad, hahaha. I don't think the rarity of that situation would ever make it worthwhile to implement but hey it's fun to think about.

@VanKrock

This comment has been minimized.

Copy link

@VanKrock VanKrock commented Dec 3, 2018

        void Iterate<T>(IEnumerable<T> items)
        {
            if (typeof(T).IsClass || typeof(T).IsInterface)
            {
                foreach (var item in items)
                {
                    Console.WriteLine($"{item} : class or interface");
                }
            }
            else
            {
                foreach (var item in items)
                {
                    Console.WriteLine($"{item} : class or interface");
                }
            }
        }

        void Iterate(object items)
        {
            //Anything else
        }
@mikernet

This comment has been minimized.

Copy link
Author

@mikernet mikernet commented Dec 3, 2018

@VanKrock You didn't explain what you're trying to show and I can't tell from your code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
10 participants
You can’t perform that action at this time.