Какова цель ограничения типа интерфейсом?

какова цель, что следующие?

class A<T> where T : IFoo {
    private T t;
    A(T t) { this.t = t; }
    /* etc */
}

как это существенно отличается от простого объявления A требовать IFoo где это нужно?

class A {
    private IFoo foo;
    A(IFoo foo) { this.foo = foo; }
    /* etc */
}

единственная разница, которую я вижу, заключается в том, что в первом случае я гарантирую и это A<T> всегда будет создан экземпляр с T что реализует IFoo и что все объекты в A будет иметь тот же базовый тип. Но хоть убей, я не могу понять, зачем мне такое ограничение.

6 ответов


редактировать: сначала я подумал, что вы глупы: первый пример, конечно, даже не будет компилироваться. Только попробовав его сам (и увидев, что это тут compile) я понял, что Доггетт уже указал: ваш class B<IFoo> пример на самом деле имеет никакого отношения до IFoo интерфейс; это просто общий тип, параметр типа которого называется IFoo.

может быть, вы знали об этом, а вы действительно были спрашивая: "почему я вообще хочу ограничить параметр универсального типа?"Если это так, то я думаю, что другие ответы в какой-то степени касались этого. Но это!--10-->звучит как вы спрашивали: "почему я должен определять свой тип как это, а как этой (поскольку они практически одинаковы)?- Ответ на этот вопрос очень прост: они ... --10-->не то же самое.


теперь, вот еще один вопрос-один вы не спросил, но тот, который я изначально намеревался ответить ;)

зачем определять такой тип:

class A<T> where T : IFoo
{
    T GetFoo();
}

...вместо этого?

class A
{
    IFoo GetFoo();
}

вот одна из причин, которая приходит к мой ум (потому что он напоминает сценарии, с которыми я имел дело в прошлом): вы разрабатываете не один класс, но маленький иерархия классов, и IFoo просто интерфейс" базовой линии " все ваши классы будут требовать, в то время как некоторые могут использовать определенные реализации или более производные интерфейсы.

вот глупый пример:

class SortedListBase<T, TList> where TList : IList<T>, new()
{
    protected TList _list = new TList();

    // Here's a method I can provide using any IList<T> implementation.
    public T this[int index]
    {
        get { return _list[index]; }
    }

    // Here's one way I can ensure the list is always sorted. Better ways
    // might be available for certain IList<T> implementations...
    public virtual void Add(T item)
    {
        IComparer<T> comparer = Comparer<T>.Default;
        for (int i = 0; i < _list.Count; ++i)
        {
            if (comparer.Compare(item, _list[i]) < 0)
            {
                _list.Insert(i, item);
                return;
            }
        }

        _list.Add(item);
    }
}

class SortedList<T> : SortedListBase<T, List<T>>
{
    // Here is a smarter implementation, dependent on List<T>'s
    // BinarySearch method. Note that this implementation would not
    // be possible (or anyway, would be less direct) if SortedListBase's
    // _list member were simply defined as IList<T>.
    public override void Add(T item)
    {
        int insertionIndex = _list.BinarySearch(item);

        if (insertionIndex < 0)
        {
            insertionIndex = ~insertionIndex;
        }

        _list.Insert(insertionIndex, item);
    }
}

основное различие в ваших 2 примерах заключается в том, что в class A всякий раз, когда вы определяете переменную как T, вы можете использовать все свойства/функции для этой переменной, которые также определены в IFoo.

однако в class B на IFoo - это просто имя параметра универсального типа и, следовательно, всякий раз, когда вы объявляете переменную внутри класса, как IFoo вы можете использовать его только как object тип.

например, если

public interface IFoo
{
   int Value { get; set; }
}

затем вы можете сделать это в class A

class A<T> where T : IFoo 
{
     public void DoSomething(T value)
     {
          value.Value++;
     }
}

в то время как, если бы вы попробовали то же самое в class B вы получите ошибку компилятора, что типа IFoo не содержит свойства Value или что-то подобное. Причина в том, что <IFoo> в классе B это просто имя и не имеет никакого отношения к интерфейсу, вы могли бы назвать его как угодно.

обновление:

class B {
    private IFoo foo;
    B(IFoo foo) { this.foo = foo; }
    /* etc */
}

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

класс:

public T Foo { get { return foo; }}

класс B:

public IFoo Foo { get { return foo; }}

теперь рассмотрим, что вы инициализировали оба класса с классом C, который определяется как

public class FooClass : IFoo
{
    public int Value { get; set; }
    public int SomeOtherValue { get; set; }
}

тогда рассмотрим 2 переменные, определенные как

var foo = new FooClass();
var a = new A<FooClass>(foo);
var b = new B(foo);
установить SomeOtherValue используя a можно сделать
a.Foo.SomeOtherValue = 2;

в то время как для b вы должны сделать

((FooClass)b.Foo).SomeOtherValue = 2;

надеюсь, что это имеет смысл ; -)


самое большое преимущество заключается в создании договора между библиотеками. В вашем случае, когда вы применяете IFoo, вы говорите, что ваш класс примет любой класс, который реализует контракт IFoo. Это означает, что ваш класс не заботится о том, как IFoo работает или реализуется, просто что он реализован. Это имеет то преимущество, что позволяет изменять унаследованные классы или функцию методов IFoo без нарушения зависимых классов. Это фундаментально в зависимость Инъекция.

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

public interface IFoo
{
    public int myMethod1();
    public void myMethod2(); 
}

public class A : IFoo
{
    // This class MUST IMPLEMENT myMethod1 and myMethod2 to be valid
    // This means any class that relies on A can depend that myMethod1
    // and myMethod2 will be there. That's the contract, and it can't
    // be broken because the interface requires it.
}

======

public class IFoo
{
    public int myMethod1();
}

public class A : IFoo
{
    // There is now no requirement that myMethod1 or myMethod2 is
    // implemented. Any class inheriting or relying on A no longer
    // can trust that it IS implemented. If a consuming dll is 
    // relying on class A and there is no interface involved, the
    // consuming classes can't trust that the base class won't change.
}

в разработке приложений для небольших групп или приложений, это меньшая проблема. У вас есть базовый класс, и вы знаете, что он делает. Вы знаете, кто и когда он меняется, поэтому им легче управлять. В более крупных приложениях и группах развития , эта структура ломается, потому что у вас есть библиотеки классов, разработанные в одной группе, используемой другой группой, и если dll изменяется без контракта интерфейса, это может создать много проблем. Интерфейс становится своего рода архитектурным обещанием, и в больших средах разработки становится критическим компонентом развития. Это не единственная причина для них, но IMO это самая большая/лучшая причина.


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

class A<T> where T : IFoo {}

это говорит о том, что A будет работать на любом типе, пока он реализован IFoo.

class B<IFoo> {}

это говорит о том, что B будет работать только с типами, реализующими IFoo.

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

B, с другой стороны, лучше работает как базовый класс,B<T>, а затем вы можете расширить C<IFoo> от него. C теперь B, который работает на IFoo. Позже вы можете расширить class D<IBar> иметь B, который работает только на IBars (не только IBar и IFoo).

таким образом, вы бы использовали первую форму, если вы пишете один класс, который всегда должен работать над IFoo. Этот вариант использования довольно распространен, возможно, написание вспомогательного класса для некоторых типов интерфейса, которые вы написали. Вы бы использовали вторую форму если вы пишете общий базовый класс, который может работать над всем, для чего вы указываете интерфейс.

я использовал эту форму немного, например, я только что написал общий абстрактный сериализатор для обработки основных операций сериализации, но оставил абстрактные методы (например,int GetKeyField(T record)) в нем (шаблон метода шаблона), чтобы разрешить специализацию. Так что я могу иметь IFooSerialize : Serializer<IFoo> это сериализует IFoos в соответствии с тем, что конкретно о IFoo.


один случай, когда это имеет значение, если вы используете тип значения для T. В этом случае тип значения не должен быть упакован для вызова методов интерфейса, что оказывает влияние на производительность. Это также влияет на способ хранения содержимого класса, поэтому, если ваш класс имеет T[] внутренне, вы получите более эффективное хранение тип значения T чем вы бы для массива boxed IFoo.

кроме того, вы можете указать ограничения, такие как where T: IEquatable<T>, использование типобезопасных универсальных интерфейсов.


Если класс будет принимать только элементы данного типа, будет использовать их только как реализации одного интерфейса (например, IFoo) и никогда не будет подвергать их посторонним, то по существу нет никакой разницы между классом, параметры и поля которого имеют тип IFoo или имеют общий тип T:IFoo. Однако добавление ограниченных универсальных типов дает по крайней мере три преимущества:

  1. как предполагает Дэн Брайант, если тип, переданный функции, является типом значения, который реализует интерфейс, используя ограниченный универсальный будет избегать бокса.
  2. тип может быть ограничен двумя или более несвязанными интерфейсами, или базовым классом и одним или несколькими интерфейсами, и тип, настолько ограниченный, может часто члены всех типов, к которым он ограничен, не нуждаясь в типографиях.
  3. Если класс возвращает во внешний мир любые объекты, которые передаются в него, вызывающий может извлечь выгоду из сохраненного типа информация.

последний пункт в некотором смысле наиболее значим. Рассмотрим, например, AnimalCollection, который хранит вещи как тип IAnimal; IAnimal включает метод Feed(), а AnimalCollection включает метод FeedEverybody(). Такая коллекционная работа отлично работает, если положить в нее кошку, собаку, Золотую Рыбку и хомяка. С другой стороны, предположим, что коллекция будет использоваться для хранения только экземпляров Cat, а вызывающий объект хочет использовать метод Meow() по каждому предмету в коллекции. Если бы коллекция хранила свое содержимое как тип Animal, вызывающий должен был бы напечатать каждый элемент для типа Cat, чтобы он мяукал. Напротив, если коллекция была AnimalCollection, можно получить элементы типа Cat из AnimalCollection. и позвоните Мяу напрямую.