Вывод общего типа с многократно реализованными ковариантными интерфейсами, как обойти его?

Рассмотрим эту глупую программу, которая ничего не делает:

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());
    }
}

Это показывает, что A2 и C2 реализуют как I<A1>, так и I<A2>, а B2 реализует как I<B1>, так и I<B2>.

Однако, изменив это на

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

показывает, что в первой и третьей строках аргумент f generic type не может быть выведен из переданного аргумента, но во второй строке это может быть.

Я понимаю, что здесь делает компилятор, поэтому не нужно объяснять. Но как я могу обойти это? Есть ли способ изменить это, чтобы я мог определить интерфейс как для базового, так и для производного класса, но при работе с производным классом у вас есть операция вывода типа?

Что я имел в виду, так это искать способ "скрыть" интерфейсы, основанные на базовом классе, чтобы компилятор их не видел и не использовал, даже если они существуют. Однако С#, похоже, не дает возможности сделать это.

Уточнение: в моей глупой примерной программе A1 реализует I сам по себе как аргумент общего типа. У меня есть это в моем реальном коде, но у меня также есть классы, которые реализуют I с другим общим аргументом типа и добавили C1 и C2 к моему примеру кода по этой причине.

Ответ 1

Используя два варианта F (второй - только для вывода типа, который вызывает другой) и интерфейс переопределения J, наследующий от I, который ничего не делает, это можно сделать так:

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();
    }
}

Вывод:

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

Ответ 2

Лямбда-выражения иногда могут быть полезны при выборе типа общих аргументов. Это немного запутанный путь, но он позволяет вашим классам A1 и A2 решать, какой интерфейс будет выбран в вызове f2:

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();
    }
}

Ответ 3

Просто для справки, здесь лучшее, что я придумал. Мне это не очень нравится, но я сохранил его как наименее плохую опцию, поскольку при переработке кода, чтобы избежать необходимости в этом, не работает.

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);
    }
}

Ответ 4

Вы можете попробовать изменить подпись f и использовать общее ограничение для эффективного получения аналогичного поведения в f:

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

Трудно сказать, действительно ли это позволит вам выполнить то, что вы хотите. Это также не будет работать для таких типов, как C2 ниже:

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

Если вам нужно иметь возможность обрабатывать эти случаи, я думаю, мне нужно будет рекомендовать просто указать аргументы типа явно. Я не могу думать об этом.