Ограничения типа в интерфейсе применяются к базовому классу

У меня есть базовый класс, который определяет такой общий метод:

public class BaseClass
{
    public T DoSomething<T> ()
    { ... }
}

Поскольку этот класс принадлежит третьей стороне и не имеет интерфейса, я определяю интерфейс, который определяет действительно необходимые методы этого класса. Таким образом, я получаю слабую связь и фактически могу заменить этот сторонний класс чем-то другим. Для этого примера рассмотрим следующий интерфейс:

public interface ISomething
{
    T DoSomething<T> ()
        where T : Foo;
}

Как видите, он определяет тот же метод, но также применяет ограничение типа к параметру типа, которое вытекает из некоторых других требований, не имеющих отношения к этому.

Затем я определяю подтип BaseClass, который также реализует ISomething. Этот класс будет использоваться как обычная реализация за интерфейсом, в то время как интерфейс будет тем, к чему будет обращаться остальная часть приложения.

public class Something : BaseClass, ISomething
{
    // ...
}

Поскольку DoSomething в BaseClass уже поддерживает любой параметр типа T, он должен особенно поддерживать параметр типа, который является подтипом Foo. Таким образом, можно было бы ожидать, что подтип BaseClass уже реализует интерфейс. Однако я получаю следующую ошибку:

Ограничения для параметра типа 'T' метода 'BaseClass.DoSomething()' должны совпадать с ограничениями для параметра типа 'T' метода интерфейса 'ISomething.DoSomething()'. Вместо этого рассмотрите возможность использования явной реализации интерфейса.

Теперь у меня есть две возможности; первый - сделать то, что предлагает ошибка, и явно реализовать интерфейс. Второй — скрыть базовую реализацию с помощью new:

// Explicit implementation
T ISomething.DoSomething<T> ()
{
    return base.DoSomething<T>();
}

// Method hiding
public new T DoSomething<T>()
    where T : Foo
{
    return base.DoSomething<T>();
}

Оба работают, хотя я, вероятно, предпочел бы второе решение, чтобы метод оставался доступным из самого класса. Однако он по-прежнему оставляет следующий вопрос:

Зачем мне заново реализовывать метод, если базовый тип уже реализует его с менее строгим (читай: без) ограничением типа? Почему метод должен быть реализован именно так, как он есть?

Изменить: чтобы придать методу больше смысла, я изменил тип возвращаемого значения с void на T. В моем реальном приложении у меня есть как общие аргументы, так и возвращаемые значения.


person poke    schedule 30.01.2013    source источник
comment
Является ли наличие методов с одинаковыми сигнатурами тем же самым, что и реализация интерфейса?   -  person Jodrell    schedule 30.01.2013
comment
Здесь задается тот же вопрос «почему мне нужно повторно объявить ограничение типа в универсальном подклассе»> stackoverflow.com/questions/4634989/ (хотя я не голосую за закрытие этого, поскольку не похоже, что консенсус был достигнут в исходном ), лично я неравнодушен к ответу Ханса Пассана, но на самом деле вам, вероятно, понадобится кто-то вроде Липперта, чтобы взвесить, если вы хотите получить окончательный ответ.   -  person heisenberg    schedule 30.01.2013
comment
Почему подписи должны совпадать? Потому что что-то, что ссылается на экземпляр Something как BaseClass, может попытаться вызвать DoSomething с параметром типа, который не наследует Foo.   -  person JosephHirn    schedule 30.01.2013
comment
@Ginosaji: И в чем проблема с этим?   -  person Jon    schedule 30.01.2013
comment
Это нарушит ограничение Foo для ISomething. Это сложно продемонстрировать на вашем примере, потому что это просто действие. Но представьте, что DoSomething<T> вернул экземпляр T. Как вы гарантируете, что возвращаемое значение DoSomething<T> унаследовано от Foo?   -  person JosephHirn    schedule 30.01.2013
comment
@kekekela Спасибо за ссылку! С трудом искал похожие темы.   -  person poke    schedule 30.01.2013
comment
@Ginosaji Если T DoSomething<T>() реализовано в BaseClass, то я могу быть уверен, что получу Foo, если вызову DoSomething<Foo>().   -  person poke    schedule 30.01.2013
comment
@Ginosaji: Это был бы другой метод. Мы говорим о методе как о данном.   -  person Jon    schedule 30.01.2013
comment
Обратите внимание, что данный метод является просто примером, у меня есть методы, которые принимают и дают Ts.   -  person poke    schedule 30.01.2013
comment
@poke: Тогда у вас наверняка возникнут проблемы с дисперсией этих методов, как говорит Гинозаджи.   -  person Jon    schedule 30.01.2013
comment
Я не вижу никакой логической проблемы с представленным примером. Я могу себе представить, с ограниченными возможностями, что это может быть сложно встроить в С#, и поэтому пока это невозможно.   -  person Jodrell    schedule 30.01.2013


Ответы (3)


Конечно, данный код может быть скомпилирован и безопасно запущен:

Когда экземпляр Something имеет тип Something или BaseClass, компилятор разрешает любой тип для T, а когда тот же экземпляр имеет тип ISomething, он разрешает только типы, наследующие Foo. В обоих случаях вы получаете статическую проверку и безопасность во время выполнения, как обычно.

На самом деле описанный выше сценарий в точности происходит, когда вы реализуете ISomething явно. Итак, давайте посмотрим, какие аргументы мы можем привести за и против текущего положения дел.

За:

  • предлагаемое решение не будет применимо ко всем случаям; это зависит от точных сигнатур метода (является ли аргумент типа ковариантным? контравариантным? инвариантным?)
  • не требуется, чтобы спецификация была дополнена новым текстом, указывающим, как обрабатываются такие случаи.
  • это делает код самодокументируемым — вам не нужно заучивать указанный текст; текущих правил относительно явной реализации интерфейса достаточно
  • это не требует затрат на разработку для команды компилятора C # (документация, реализация функций, тестирование и т. д.)

Против:

  • вам нужно напечатать больше

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

person Jon    schedule 30.01.2013
comment
Спасибо, что действительно попытались ответить на вопрос почему :) Я понимаю, что это потребует некоторых усилий, чтобы заставить это работать в языке, но опять же, это верно для каждой новой функции языка. Но я также понимаю, что это редкий вариант использования, и как явная реализация, так и сокрытие метода прекрасно решают эту проблему. Было просто странно наткнуться на это, так как я действительно ожидал, что это сработает. — У вас есть какой-нибудь пример для меня, где применяется контравариантность аргумента типа? Это также в некоторой степени связано с комментарием @Ginosaji в комментариях к вопросу (см. Также мой ответ там). - person poke; 30.01.2013

Попробуйте использовать композицию вместо наследования для реализации Something:

public class Something : ISomething
{
    private readonly BaseClass inner = ...;

    void DoSomething<T>() where T : Foo
    {
        inner.DoSomething<T>();
    }
}
person Paul Bellora    schedule 30.01.2013
comment
Это не может быть общим решением, оно имеет множество проблем (например, не является BaseClass, требует сохранения повторяющегося состояния, не имеет доступа к protected членам и т. д.). - person Jon; 30.01.2013
comment
@Jon Понял об ограничениях. Но если ОП хочет слабой связи, нет необходимости, чтобы он был BaseClass, на самом деле этого не должно быть. Чтобы получить доступ к защищенным членам, он может расширить его и расширить доступ к членам, а затем вместо этого обернуть экземпляр этого класса (я понимаю, грубо). Не вижу, где находится дублирующее состояние, о котором вы упомянули. - person Paul Bellora; 30.01.2013
comment
Предполагая, что BaseSomething имеет состояние, в таких сценариях вы можете легко быть вынуждены дублировать его. В любом случае, я говорю об общем случае (и только потому, что вы предложили конкретное решение - здесь много неизвестных). - person Jon; 30.01.2013
comment
Я согласен, что композиция может быть полезна для решения этой проблемы, но для меня это не очень хорошее решение. Только несколько методов, определенных в интерфейсе, конфликтуют с методами, определенными в BaseClass, поэтому его создание будет означать репликацию каждого метода. И, как сказал Джон, я потерял бы возможность доступа к защищенным членам, что необходимо в текущей реализации. Таким образом, мне больше подходит явная реализация интерфейса или скрытие методов. - Кстати. Я понимаю, что оставил много неизвестных, но это было задумано, поскольку меня больше интересует общий случай. - person poke; 30.01.2013

Вы можете получить то, что хотите, с кодом ниже. Включив параметр типа в определение интерфейса, вы можете сделать его ковариантным, что, по-видимому, удовлетворит компилятор. Класс Base остается нетронутым, и вы можете затенить реализацию Base и реализовать интерфейс с помощью одного метода.

class Program
{
    static void Main()
    {
        var something = new Something<Foo>();
        var baseClass = (BaseClass)something;
        var isomething = (ISomething<Foo>)something;

        var baseResult = baseClass.DoSomething<Bar>();
        var interfaceResult = isomething.DoSomething<Bar>();
        var result = something.DoSomething<Bar>();
    }
}

class Foo 
{
}

class Bar : Foo
{
}

class BaseClass
{
    public T DoSomething<T>()
    {
        return default(T);
    }
}

interface ISomething<out T> where T : Foo
{
    T DoSomething<T>();
}

class Something<T> : BaseClass, ISomething<T> where T : Foo
{
    public new T DoSomething<T>()
    {
        return default(T);
    }
}

Или, если вы действительно не хотите указывать Foo в экземпляре

class Program
{
    static void Main()
    {
        var something = new Something();
        var baseClass = (BaseClass)something;
        var isomething = (ISomething)something;

        var baseResult = baseClass.DoSomething<Bar>();
        var interfaceResult = isomething.DoSomething<Bar>();
        var result = something.DoSomething<Bar>();
    }
}

class Foo 
{
}

class Bar : Foo
{
}

class BaseClass
{
    public T DoSomething<T>()
    {
        return default(T);
    }
}

interface ISomething
{
    T DoSomething<T>;
}

interface ISomething<S> : ISomething where S : Foo
{
    new R DoSomething<R>() where R : Foo;
}

class Something : BaseClass, ISomething
{
    public new T DoSomething<T>()
    {
        return default(T);
    }
}
person Jodrell    schedule 30.01.2013
comment
Спасибо за ответ. К сожалению, мне нужны общие методы, и я не могу создать экземпляр объекта в зависимости от того, с каким типом мне нужно работать (т. Е. Класс Doer или Something сам по себе не является универсальным). - person poke; 30.01.2013
comment
@poke, как вы можете видеть в моем расширенном ответе, вы можете создать его экземпляр на основе Foo, а не того типа, с которым вам нужно работать. Поскольку вы уже должны знать Foo, чтобы наложить ограничение на интерфейс, я считаю, что это удовлетворяет вашим требованиям. - person Jodrell; 30.01.2013
comment
@poke или с небольшой косвенностью, которой можно избежать за определенную плату. - person Jodrell; 30.01.2013