简体   繁体   中英

Generic type inference with multiply-implemented covariant interfaces, how to work around it?

Consider this silly program that does nothing:

interface I<out T> { }
class A1 : I<A1> { }
class A2 : A1, I<A2> { }
class B1 { }
class B2 : B1, I<B2> { }
class C1 : I<A1> { }
class C2 : C1, I<A2> { }

static class Program
{
    static void f<T>(I<T> obj)
    {
    }

    static void Main()
    {
        f<A1>(new A2());
        f<A2>(new A2());
        f<B1>(new B2());
        f<B2>(new B2());
        f<A1>(new C2());
        f<A2>(new C2());
    }
}

This shows that A2 and C2 implement both I<A1> and I<A2> , and that B2 implements both I<B1> and I<B2> .

However, modifying this to

static void Main()
{
    f(new A2());
    f(new B2());
    f(new C2());
}

shows that on the first and third lines, f 's generic type argument cannot be inferred from the passed argument, yet on the second line, it can be.

I understand what it is that the compiler is doing here, so that doesn't need explaining. But how can I work around this? Is there some way to modify this so that I can define the interface on both the base and the derived class, yet have type inference work when passing the derived class?

What I had in mind was to look for a way to "hide" a base class's implemented interfaces, so that the compiler doesn't see them and use them, even though they do exist. However, C# doesn't seem to provide an option to do so.

Clarification : in my silly example program, A1 implements I with itself as the generic type argument. I do have that in my real code, but I also have classes that implement I with a different generic type argument, and have added C1 and C2 to my example code for that reason.

Using two F variants (second one just for type inference that calls the other) and an "override" interface J inheriting from I which does nothing it could be done like so:

using System;
using System.Threading;


interface I<out T>
{
    void Print();
}

interface J<out T> : I<T> { }

class A : I<C>
{
    void I<C>.Print()
    {
        Console.WriteLine("A: I<C>");
    }
}

class B {}

class C : B { }

class D1 : I<A>
{
    void I<A>.Print()
    {
        Console.WriteLine("D1: I<A>");
    }
}

class D2 : D1, J<B>
{
    void I<B>.Print()
    {
        Console.WriteLine("D2: I<B>");
    }
}

class D3 : D1, J<C>
{
    void I<C>.Print()
    {
        Console.WriteLine("D3: I<C>");
    }
}

class D4 : A, J<B>
{
    void I<B>.Print()
    {
        Console.WriteLine("D4: I<B>");
    }
}

static class Program
{
    static void f<T>(J<T> obj)
    {
        f((I<T>)obj);
    }

    static void f<T>(I<T> obj)
    {
        obj.Print();
    }

    static void Main()
    {

        f<A>(new D2());
        f(new D2());

        f(new D3());

        f(new D4());
        f<C>(new D4());

        Console.ReadKey();
    }
}

Output:

D1: I<A>
D2: I<B>
D3: I<C>
D4: I<B>
A: I<C>

Lambda expressions may sometimes be helpful in selecting Generic argument type. This is a little convoluted way, but it let your A1 and A2 classes to decide, which interface would be chosen in the f2 call:

interface IChooseGenericArg<T>
{
    T Choose();
}

interface I<out T> 
{
    string M1();
}
class A1 : I<A1>, IChooseGenericArg<A1>
{
    public string M1()
    {
        return "A1.M1";
    }
    public A1 Choose()
    {
        return this;
    }
}
class A2 : A1, I<A2>, IChooseGenericArg<A2>
{
    public string M1()
    {
        return "A2.M1";
    }

    public new A2 Choose()
    {
        return this;
    }
}
static class Program
{
    static void f<T>(I<T> obj)
    {
        Console.WriteLine(obj.M1());
    }

    static void f2<T>(Func<T> selector, I<T> obj)
    {
        Console.WriteLine(obj.M1());
    }

    static void Main()
    {
        f<A1>(new A1());
        f<A2>(new A2());

        var a2 = new A2();
        f2(() => a2.Choose(), a2);
        var a1 = new A1();
        f2(() => a1.Choose(), a1);

        Console.ReadLine();
    }
}

Just for reference, here's the best I've come up with. I don't really like it that much, but I have kept it as a least-bad option, for when reworking the code to avoid a need for this doesn't work.

interface I<out T> { }
class A1 : I<A1> {
  public I<A1> PreferredInterface
  { get { return this; } }
}
class A2 : A1, I<A2> {
  public new I<A2> PreferredInterface
  { get { return this; } }
}
class B1 { }
class B2 : B1, I<B2> {
  public I<B2> PreferredInterface
  { get { return this; } }
}
class C1 : I<A1> {
  public I<A1> PreferredInterface
  { get { return this; } }
}
class C2 : C1, I<A2> {
  public new I<A2> PreferredInterface
  { get { return this; } }
}

static class Program
{
    static void f<T>(I<T> obj)
    {
    }

    static void Main()
    {
        f<A1>(new A2().PreferredInterface);
        f<A2>(new A2().PreferredInterface);
        f<B1>(new B2().PreferredInterface);
        f<B2>(new B2().PreferredInterface);
        f<A1>(new C2().PreferredInterface);
        f<A2>(new C2().PreferredInterface);
        f(new A2().PreferredInterface);
        f(new B2().PreferredInterface);
        f(new C2().PreferredInterface);
    }
}

You could try changing the signature of f and using a generic constraint to effectively get similar behavior within f :

static void f<T>( T obj ) where T : I<T>
{
}

It's difficult to tell if this will actually allow you to accomplish what you want, however. This also won't work for types like C2 below:

class C1 : I<C1> { }
class C2 : C1 { }

If you need to be able to handle these cases, I think I'd have to recommend just specifying the type arguments explicitly. I can't think of a way around this.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM