开发者

Practical advantage of generics vs interfaces

What would be a practical advantage of using generics vs interfaces in this case:

void MyM开发者_StackOverflow中文版ethod(IFoo f) 
{
}

void MyMethod<T>(T f) : where T : IFoo
{
}

I.e. what can you do in MyMethod<T> that you couldn't in the non-generic version? I'm looking for a practical example, I know what the theoretical differences are.

I know that in MyMethod<T>, T will be the concrete type, but nonetheless I will only be able to use it as an IFoo within the body of the method. So what would be a real advantage?


  • Calling a method through an interface is slower than calling it directly on the concrete type
  • If the type implementing IFoo is a value type, the non-generic version will box the value of the parameter, and boxing can negatively affect performance (especially if you call this method very often)
  • If your method returns a value, the generic version can return a T rather than a IFoo, which is convenient if you need to call a method of T on the result


Well, one advantage as mentioned elsewhere, would be the ability to return a specific type of IFoo type if you return a value. But since your question is specifically about void MyMethod(IFoo f), I wanted to give a realistic example of at least one type of situation where using a generic method makes more sense (to me) than the interface. (Yes I spent a bit of time on this, but I wanted to try out some different ideas. :D)

There are two blocks of code, the first is just the generic method itself and some context, the second is the full code for the example, including lots of comments ranging from notes on possible differences between this and an equivalent non-generic implementation, as well as various things I tried while implementing that didn't work, and notes on various choices I made, etc. TL;DR and all that.

Concept

    public class FooChains : Dictionary<IFoo, IEnumerable<IFoo>> { }

    // to manage our foos and their chains. very important foo chains.
    public class FooManager
    {
        private FooChains myChainList = new FooChains();

        // void MyMethod<T>(T f) where T : IFoo
        void CopyAndChainFoo<TFoo>(TFoo fromFoo) where TFoo : IFoo
        {
            TFoo toFoo;

            try {
                // create a foo from the same type of foo
                toFoo = (TFoo)fromFoo.MakeTyped<TFoo>(EFooOpts.ForChain);
            }
            catch (Exception Ex) {
                // hey! that wasn't the same type of foo!
                throw new FooChainTypeMismatch(typeof(TFoo), fromFoo, Ex);
            }

            // a list of a specific type of foos chained to fromFoo
            List<TFoo> typedFoos;

            if (!myChainList.Keys.Contains(fromFoo))
            {
                // no foos there! make a list and connect them to fromFoo
                typedChain = new List<TFoo>();
                myChainList.Add(fromFoo, (IEnumerable<IFoo>)typedChain);
            }
            else
                // oh good, the chain exists, phew!
                typedChain = (List<TFoo>)myChainList[fromFoo];

            // add the new foo to the connected chain of foos
            typedChain.Add(toFoo);

            // and we're done!
        }
    }

Gory Details

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IFooedYouOnce
{
    // IFoo
    //
    // It's personality is so magnetic, it's erased hard drives.
    // It can debug other code... by actually debugging other code.
    // It can speak Haskell... in C. 
    //
    // It *is* the most interesting interface in the world.
    public interface IFoo
    {       
        // didn't end up using this but it's still there because some
        // of the supporting derived classes look silly without it.
        bool CanChain { get; }
        string FooIdentifier { get; }

        // would like to place constraints on this in derived methods
        // to ensure type safety, but had to use exceptions instead.
        // Liskov yada yada yada...
        IFoo MakeTyped<TFoo>(EFooOpts fooOpts);
    }

    // using IEnumerable<IFoo> here to take advantage of covariance;
    // we can have lists of derived foos and just cast back and 
    // forth for adding or if we need to use the derived interfaces.

    // made it into a separate class because probably there will be
    // specific operations you can do on the chain collection as a
    // whole so this way there's a spot for it instead of, say, 
    // implementing it all in the FooManager
    public class FooChains : Dictionary<IFoo, IEnumerable<IFoo>> { }

    // manages the foos. very highly important foos.
    public class FooManager
    {
        private FooChains myChainList = new FooChains();

        // would perhaps add a new() constraint here to make the 
        // creation a little easier; could drop the whole MakeTyped
        // method.  but was trying to stick with the interface from
        // the question.
        void CopyAndChainFoo<TFoo>(TFoo fromFoo) where TFoo : IFoo
        // void MyMethod<T>(T f) where T : IFoo
        {
            TFoo toFoo;

            // without generics, I would probably create a factory
            // method on one of the base classes that could return
            // any type, and pass in a type. other ways are possible,
            // for instance, having a method which took two IFoos, 
            // fromFoo and toFoo, and handling the Copy elsewhere.

            // could have bypassed this try/catch altogether because
            // MakeTyped functions throw if the types are not equal,
            // but wanted to make it explicit here. also, this gives
            // a more descriptive error which, in general, I prefer
            try
            {
                // MakeTyped<TFoo> was a solution to allowing each TFoo
                // to be in charge of creating its own objects
                toFoo = 
                    (TFoo)fromFoo.MakeTyped<TFoo>(EFooOpts.ForChain);
            }
            catch (Exception Ex) {
                // tried to eliminate the need for this try/catch, but
                // didn't manage. can't constrain the derived classes'
                // MakeTyped functions on their own types, and didn't
                // want to change the constraints to new() as mentioned
                throw 
                    new FooChainTypeMismatch(typeof(TFoo), fromFoo, Ex);
            }

            // a list of specific type foos to hold the chain
            List<TFoo> typedFoos;

            if (!myChainList.Keys.Contains(fromFoo))
            {
                // we just create a new one and link it to the fromFoo
                // if none already exists
                typedFoos = new List<TFoo>();
                myChainList.Add(fromFoo, (IEnumerable<IFoo>)typedFoos);
            }
            else
                // otherwise get the existing one; we are using the 
                // IEnumerable to hold actual List<TFoos> so we can just
                // cast here.
                typedFoos = (List<TFoo>)myChainList[fromFoo];

            // add it in!
            typedFoos.Add(toFoo);
        }
    }

    [Flags]
    public enum EFooOpts
    {
        ForChain   = 0x01,
        FullDup    = 0x02,
        RawCopy    = 0x04,
        Specialize = 0x08
    }

    // base class, originally so we could have the chainable/
    // non chainable distinction but that turned out to be 
    // fairly pointless since I didn't use it. so, just left
    // it like it was anyway so I didn't have to rework all 
    // the classes again.
    public abstract class FooBase : IFoo
    {
        public string FooIdentifier { get; protected set; }
        public abstract bool CanChain { get; }
        public abstract IFoo MakeTyped<TFoo>(EFooOpts parOpts);
    }

    public abstract class NonChainableFoo : FooBase
    {
        public override bool CanChain { get { return false; } }
    }

    public abstract class ChainableFoo : FooBase
    {
        public override bool CanChain { get { return true; } }
    }

    // not much more interesting to see here; the MakeTyped would
    // have been nicer not to exist, but that would have required
    // a new() constraint on the chains function.  
    //
    // or would have added "where TFoo : MarkIFoo" type constraint
    // on the derived classes' implementation of it, but that's not 
    // allowed due to the fact that the constraints have to derive
    // from the base method, which had to exist on the abstract 
    // classes to implement IFoo.
    public class MarkIFoo : NonChainableFoo
    {
        public MarkIFoo()
            { FooIdentifier = "MI_-" + Guid.NewGuid().ToString(); }

        public override IFoo MakeTyped<TFoo>(EFooOpts fooOpts) 
        {
            if (typeof(TFoo) != typeof(MarkIFoo))
                throw new FooCopyTypeMismatch(typeof(TFoo), this, null);

            return new MarkIFoo(this, fooOpts);
        }

        private MarkIFoo(MarkIFoo fromFoo, EFooOpts parOpts) :
            this() { /* copy MarkOne foo here */ }
    }

    public class MarkIIFoo : ChainableFoo
    {
        public MarkIIFoo()
            { FooIdentifier = "MII-" + Guid.NewGuid().ToString(); }

        public override IFoo MakeTyped<TFoo>(EFooOpts fooOpts)
        {
            if (typeof(TFoo) != typeof(MarkIIFoo))
                throw new FooCopyTypeMismatch(typeof(TFoo), this, null);

            return new MarkIIFoo(this, fooOpts);
        }

        private MarkIIFoo(MarkIIFoo fromFoo, EFooOpts parOpts) :
            this() { /* copy MarkTwo foo here */ }
    }

    // yep, really, that's about all. 
    public class FooException : Exception
    {
        public Tuple<string, object>[] itemDetail { get; private set; }

        public FooException(
            string message, Exception inner,
            params Tuple<string, object>[] parItemDetail
        ) : base(message, inner)
        {
            itemDetail = parItemDetail;
        }

        public FooException(
            string msg, object srcItem, object destType, Exception inner
        ) : this(msg, inner,
            Tuple.Create("src", srcItem), Tuple.Create("dtype", destType)
        ) { }
    }

    public class FooCopyTypeMismatch : FooException
    {
        public FooCopyTypeMismatch(
            Type reqDestType, IFoo reqFromFoo, Exception inner
        ) : base("copy type mismatch", reqFromFoo, reqDestType, inner)
        { }
    }

    public class FooChainTypeMismatch : FooException
    {
        public FooChainTypeMismatch(
            Type reqDestType, IFoo reqFromFoo, Exception inner
        ) : base("chain type mismatch", reqFromFoo, reqDestType, inner)
        { }
    }
}

// I(Foo) shot J.R.!


Doing things like these is easier:

void MyMethod<T>(T f) where T : IFoo, new() {
    var t1 = new T();
    var t2 = default(T);
    // Etc...
}

Also, as you introduce more interfaces, generics may be more "gentle" to callers. For example, you can inherit a class from 2 interfaces and pass it directly, like this...

interface IFoo {
}

interface IBar {
}

class FooBar : IFoo, IBar {
}

void MyMethod<T>(T f) where T : IFoo, IBar {
}

void Test() {
    FooBar fb = new FooBar();
    MyMethod(fb);
}

...while "interface-only" method would require an "intermediary" interface (IFooBar)...

interface IFoo {
}

interface IBar {
}

interface IFooBar : IFoo, IBar {
}

class FooBar : IFooBar {
}

void MyMethod(IFooBar f) {
}

void Test() {
    FooBar fb = new FooBar();
    MyMethod(fb);
}


2 years later I found a very simple and useful case. Consider this common pattern:

class MyClass : IDisposable {

     public void Dispose() {
         if (m_field1 != null) {
             m_field1.Dispose();
             m_field1 = null;
         }
         if (m_field2 != null) {
             m_field2.Dispose();
             m_field2 = null;
         }
         // etc
     }
}

I've always wanted to write a helper method to avoid having to write all this boilerplate for every field:

class MyClass : IDisposable {

    static void IfNotNullDispose(ref IDisposable disposable) {
        if (disposable != null) {
            disposable.Dispose();
            disposable = null;
        }
    }

    public void Dispose() {
         IfNotNullDispose(ref m_field1);
         IfNotNullDispose(ref m_field2);
         // etc
    }
}

Unfortunately this is illegal in C# because you cannot use an interface for ref parameters, you must use the concrete type you'll pass in and nothing else. So you'd have to write a different method for every single type of field you want to dispose. Oh wait that's exactly what generics do for you:

static void IfNotNullDispose<T>(ref T disposable) where T: class, IDisposable {
    if (disposable != null) {
        disposable.Dispose();
        disposable = null;
    }
}

Now everything works as intended!


In this particular case, there is no benefit. In general you wouldn't specify this at a method level, but at a class level. E.g.,

public interface IFoo {
        void DoSomethingImportant();
    }

    public class MyContainer<T> where T : IFoo {
        public void Add(T something){
            something.DoSomethingImportant();
            AddThisThing(something);
        }

        public T Get() {
            T theThing = GetSomeKindOfThing();
            return theThing;
        }
    }

Notice that we require T to implement IFoo because of the Add method where we need to call the DoSomethingImportantMethod implemented by IFoo.

But notice in the Get method that we will return the T provided by end user of this class instead of a plain old IFoo, which alleviates the need for the developer to always cast to their actual concrete T.

Example:

public class Bar : IFoo{
  //....
}

MyContainer<Bar> m = new MyContainer<Bar>();
//stuff happens here
Bar b = m.Get();

Note that if I was just returning an IFoo, then I would have to do this at the last line instead:

Bar b = (Bar) m.Get();


The interface method will supply you an f of type IFoo, whereas the generic version will supply you a type T with the constraint that T has to implement IFoo.

The second method would allow you to have some kind of lookup depending on T, as you have a concrete type to work with.


referring to the benchmark reported above

@Branko, calling a method through an interface is actually slower than >a "normal" virtual method call... Here's a simple benchmark: >pastebin.com/jx3W5zWb – Thomas Levesque Aug 29 '11 at 0:33

running the code in Visual Studio 2015 the result are roughly equivalent between Direct call and Through interface:

  • Direct call: 90,51 millisec; 112,49 millisec; 81,22 millisec
  • Through interface: 92,85 millisec;90,14 millisec; 88,56 millisec

the code used to benchmark (from http://pastebin.com/jx3W5zWb ) is:

using System;
using System.Diagnostics;

namespace test

{
    class MainApp
    {
        static void Main()
        {
            Foo f = new Foo();
            IFoo f2 = f;

            // JIT warm-up
            f.Bar();
            f2.Bar();

            int N = 10000000;
            Stopwatch sw = new Stopwatch();

            sw.Start();
            for (int i = 0; i < N; i++)
            {
                f2.Bar();
            }
            sw.Stop();
            Console.WriteLine("Through interface: {0:F2}", sw.Elapsed.TotalMilliseconds);

            sw.Reset();

            sw.Start();
            for (int i = 0; i < N; i++)
            {
                f.Bar();
            }
            sw.Stop();
            Console.WriteLine("Direct call: {0:F2}", sw.Elapsed.TotalMilliseconds);

            Console.Read();

        }

        interface IFoo
        {
            void Bar();
        }

        class Foo : IFoo
        {
            public virtual void Bar()
            {
            }
        }
    }
}


The generic version allows you to use any type as T - which you for some reason restricted back by using the where clause, whereas your non-generic version supports only something implementing IFoo.

Another (maybe better) question is - are these two options equivalent?

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜