Какова цель ограничения типа интерфейсом? - PullRequest
6 голосов
/ 04 декабря 2010

Для чего нужно следующее?

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 ]

7 голосов
/ 04 декабря 2010

Основное различие в ваших 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 обратно наружу. Рассмотрим следующее свойство в обоих классах

класса A:

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

, а затем рассмотрите две переменные, определенные как

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;

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

2 голосов
/ 04 декабря 2010

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

Может быть, вы знали об этом, и вы действительно спрашивали: «Зачем мне вообще ограничивать параметр универсального типа?»Если это так, то я думаю, что другие ответы касались этого в некоторой степени.Но это звучало , как вы спрашивали: «Почему я определяю свой тип как , что , а не как это (поскольку они практически одинаковы)?"Ответ на этот вопрос довольно прост: они не одинаковы.


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

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

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 голосов
/ 04 декабря 2010

Самым большим преимуществом является заключение договора между библиотеками.В вашем случае, когда вы применяете 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 изменяется без контракта интерфейса, это может создать много проблем.Интерфейс становится своего рода архитектурным обещанием, а в больших средах разработки становится критически важным компонентом разработки.Это не единственная причина для них, но ИМО это самая большая / лучшая причина.

1 голос
/ 04 декабря 2010

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

class A<T> where T : IFoo {}

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

class B<IFoo> {}

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

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

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

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

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

0 голосов
/ 01 августа 2011

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

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

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

0 голосов
/ 04 декабря 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...