将类型约束到接口的目的是什么?

时间:2010-12-04 16:54:17

标签: c# generics interface constraints

允许以下内容的目的是什么?

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将始终使用实现{{1}的IFoo进行实例化} {em>和 A中的所有对象都具有相同的基类型。但对于我的生活,我无法弄清楚为什么我需要这样的约束。

6 个答案:

答案 0 :(得分:7)

两个示例的主要区别在于,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或类似内容。原因是B类中的<IFoo>只是一个名称而且与界面无关,你可以把它称为任何你想要的。

更新

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

然后考虑定义为

的2个变量
var foo = new FooClass();
var a = new A<FooClass>(foo);
var b = new B(foo);

现在使用您可以执行的SomeOtherValue设置a

a.Foo.SomeOtherValue = 2;

而你必须这样做

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

希望有意义; - )

答案 1 :(得分:2)

最大的好处是在图书馆之间建立合同。在你执行IFoo的实例中,你说你的类将接受任何实现IFoo契约的类。这意味着您的课程并不关心IFoo如何工作或实施,只是它已经实现。这样做的好处是允许更改继承的类或IFoo方法的功能,而不会破坏依赖类。这是Dependency Injection中的基础。

修改
接口与继承类不同的原因是接口实现了特定的方法,而类不需要这样做:

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是最大/最好的原因。

答案 2 :(得分:2)

编辑:起初我以为你是愚蠢的:第一个例子肯定不会编译。只有在自己尝试之后(并且看到它 编译)我才意识到Doggett already pointed out:你的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);
    }
}

答案 3 :(得分:1)

虽然两个陈述基本上都是一样的,但是小的语义差异确实至少有一个维护效果。

class A<T> where T : IFoo {}

这表示A可以在任何类型上工作,只要它实现了IFoo。

class B<IFoo> {}

这表示B只适用于实现IFoo的类型。

区别在于你仍然可以扩展A以同样需要其他东西,比如也可以实现IBar,这样你就可以在A中做更多的事情。

另一方面,

B更适合作为基类B<T>,然后您可以从中扩展C<IFoo>。 C现在是一个适用于IFoo的B.稍后,您可以扩展类D<IBar>以使B仅适用于IBars(不同于IBar和IFoo)。

如果您正在编写一个始终需要在IFoo上工作的类,那么您将使用第一个表单。这个用例很常见,可能是为你编写的某些接口类型编写了一个帮助器类。如果您正在编写可以处理指定接口的任何内容的通用基类,则可以使用第二种形式。

我已经使用了这个表格,例如我只是编写了一个通用的抽象序列化程序来处理序列化的基本操作,但是在其中留下抽象方法(如int GetKeyField(T record))(模板方法模式)允许专业化。那么我可以让IFooSerialize : Serializer<IFoo>根据IFoo的具体内容序列化IFo。

答案 4 :(得分:0)

这会产生影响的一种情况是,如果您使用T的值类型。在这种情况下,值类型不需要加框以调用具有性能影响的接口方法。它还会影响类的内容存储方式,因此如果您的类在内部具有T[],那么您将为值类型T获得比对数组类型更高效的存储。盒装IFoo

此外,您可以使用类型安全的通用接口来指定where T: IEquatable<T>之类的约束。

答案 5 :(得分:0)

如果一个类只接受给定类型的项,只会将它们用作单个接口的实现(例如IFoo),并且永远不会将它们暴露给外人,那么一个类之间基本上没有区别。参数和字段的类型为IFoo,或者是泛型类型T:IFoo。但是,添加受约束的泛型类型至少有三个优点:

  1. 正如Dan Bryant建议的那样,如果传递给函数的类型是实现接口的值类型,则使用约束泛型将避免装箱。
  2. 一个类型可能被约束到两个或多个不相关的接口,或者基类和一个或多个接口,并且受限制的类型可能经常被约束的所有类型的成员而不需要类型转换。
  3. 如果一个类将任何传递给它的对象返回到外部世界,则调用者可能会从保留的类型信息中受益。

最后一点在某些方面是最重要的。例如,考虑一个将事物存储为IAnimal类型的AnimalCollection; IAnimal包含方法Feed(),AnimalCollection包含方法FeedEverybody()。如果放入猫,狗,金鱼和仓鼠,这样的收集工作就可以了。另一方面,假设一个集合将仅用于存储Cat的实例,并且调用者想要对集合中的每个项目使用Meow()方法。如果集合将其内容存储为Animal类型,则调用者必须对每个项目进行类型转换以键入Cat以使其为Meow。相反,如果集合是AnimalCollection&lt; T:Animal&gt;,则可以从AnimalCollection&lt; Cat&gt;中检索Cat类型的项目。并直接打电话给喵。