简体   繁体   中英

What is the purpose of constraining a type to an interface?

What is the purpose of allowing the following?

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

How is this meaningfully different from just declaring A to require an IFoo wherever it needs one?

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

The only difference that I can see is that in the first case I'm guaranteed both that A<T> will always be instantiated with a T that implements IFoo and that all of the objects in A will be of the same base type. But for the life of me I can't figure out why I'd need such a constraint.

The main difference in your 2 examples is that in class A whenever you define a variable as T you can use all properties/functions on that variable that are also defined in IFoo.

However in class B the IFoo is just a name for the generic type parameter and thus whenever you declare a variable inside the class as IFoo you can only use it as if it's an object type.

for example if

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

then you can do this in class A

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

While if you'd try the same in class B you'll get a compiler error that the type IFoo does not contain a property Value or something similar. The reason is that the <IFoo> in class B is just a name and has no relation to the interface, you could've called it anything you like.

Update:

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

This construct is indeed basically the same, except when you expose IFoo back to the outside again, Consider the following property in both classes

class A:

public T Foo { get { return foo; }}

class B:

public IFoo Foo { get { return foo; }}

Now consider you initialized both classes with a class C which is defined as

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

then consider 2 variables defined as

var foo = new FooClass();
var a = new A<FooClass>(foo);
var b = new B(foo);

now to set the SomeOtherValue using a you can do

a.Foo.SomeOtherValue = 2;

while for b you have to do

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

Hope that makes sense ;-)

The biggest benefit is to create a contract between libraries. In your instance where you're enforcing IFoo, you're saying that your class will accept any class that implements the IFoo contract. This means your class doesn't care how IFoo works or is implemented, just that it IS implemented. This has the benefit of allowing changes to the inherited classes or the function of IFoo methods without breaking dependent classes. This is fundamental in Dependency Injection .

Edit
The reason that an interface is different from inheriting a class is that an interface implements specific methods whereas a class is not required to do so:

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

In developing applications for smaller groups or applications, this is less of a problem. You have your base class and you know what it does. You know who and when it changes, so it's easier to manage. In larger applications and development groups, this structure breaks down because you have class libraries developed in one group consumed by another group, and if the dll is changed without the interface contract, it could create a lot of problems. The interface becomes an architectural promise of sorts, and in large development environments becomes a critical component of development. This is not the only reason for them, but IMO it's the biggest/best reason.

Edit : At first I thought you were daft: the first example surely wouldn't even compile. Only after trying it myself (and seeing that it does compile) did I realize what Doggett already pointed out : your class B<IFoo> example actually has no relationship to the IFoo interface; it's just a generic type whose type parameter happens to be called IFoo .

Maybe you were aware of this, and you really were asking, "Why would I want to constrain a generic type parameter at all?" If that's the case then I think the other answers have addressed this to some extent. But it sounded like you were asking, "Why would I define my type like that , instead of like this (since they are practically the same)?" The answer to this is, quite simply: they are not the same.


Now, here's another question—one you didn't ask, but the one I originally set out to answer ;)

Why define a type like this:

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

...instead of this?

class A
{
    IFoo GetFoo();
}

Here's one reason that springs to my mind (because it resembles scenarios I've dealt with in the past): you are designing not one class, but a small hierarchy of classes, and IFoo is simply the "base line" interface all of your classes will require, while some may leverage specific implementations, or more derived interfaces.

Here's a dumb example:

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

While both statements are saying essentially the same thing, the minor semantic difference does have at least one maintenance effect.

class A<T> where T : IFoo {}

This says that A will work on any type, so long as it has IFoo implemented.

class B<IFoo> {}

This says that B will only work on types that implement IFoo.

The distinction is that you can still expand A to also require other things, like that IBar also be implemented, so you can do more things in A.

B on the other hand works better as a base class, B<T> , and then you can extend C<IFoo> from it. C is now a B that works on IFoo. Later on, you could extend class D<IBar> to have a B that works on only IBars (not both IBar and IFoo).

So you'd use the first form if you're writing one class that always needs to work on IFoo. This use case is pretty common, maybe writing a helper class for some interface types you wrote. You'd use the second form if you're writing a generic base class that can work on anything that you specify the interface for.

I've used this form a fair bit, for example I just wrote a generic abstract serializer to handle the basic operations of serialization, but leaving abstract methods (like int GetKeyField(T record) ) in it (the template method pattern) to allow specialization. So then I can have IFooSerialize : Serializer<IFoo> that serializes IFoos according to what is specific about IFoo.

One case where this makes a difference is if you use a value type for T. In this case, the value type does not need to be boxed in order to call interface methods, which has a performance impact. It also impacts the way the contents of the class will be stored, so if your class has a T[] internally, you'll get more efficient storage for the value type T than you would for an array of boxed IFoo .

Also, you can specify constraints like where T: IEquatable<T> , taking advantage of type-safe generic interfaces.

If a class will only take in items of a given type, will only ever use them as implementations of a single interface (eg IFoo), and will never expose them to outsiders, then there is essentially no difference between a class whose parameters and fields are of type IFoo, or are of a generic type T:IFoo. Adding constrained generic types, however, offers at least three advantages:

  1. As Dan Bryant suggests, if the type passed to a function is a value type which implements an interface, using a constrained generic will avoid boxing.
  2. A type may be constrained to two or more unrelated interfaces, or to a base class and one or more interfaces, and a type so constrained may frequently the members of all the types to which it is constrained without needing typecasts.
  3. If a class returns to the outside world any objects that are passed into it, the caller may benefit from retained type information.

The last point is in some ways the most significant. Consider, for example, an AnimalCollection which stored things as type IAnimal; IAnimal includes a method Feed(), and AnimalCollection includes a method FeedEverybody(). Such a collection work work just fine if one put in a Cat, a Dog, a Goldfish, and a Hamster. On the other hand, suppose a collection would be used for storing nothing but instances of Cat, and a caller wanted to use the Meow() method on every item in the collection. If the collection stored its contents as type Animal, the caller would have to typecast each item to type Cat in order for it to Meow. By contrast, if the collection were an AnimalCollection<T:Animal>, one could retrieve items of type Cat from an AnimalCollection<Cat>. and call Meow directly.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM