Подтвердить что ты не робот

Практическое преимущество дженериков против интерфейсов

Что было бы практическим преимуществом использования generics vs interface в этом случае:

void MyMethod(IFoo f) 
{
}

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

т.е. что вы можете сделать в MyMethod<T>, что вы не могли бы использовать в универсальной версии? Я ищу практический пример, я знаю, что такое теоретические различия.

Я знаю, что в MyMethod<T> T будет конкретным типом, но тем не менее я буду использовать его только как IFoo внутри тела метода. Итак, каково было бы реальное преимущество?

4b9b3361

Ответ 1

Хорошо, одно преимущество, как упоминалось в другом месте, было бы способностью возвращать определенный тип типа IFoo, если вы вернете значение. Но поскольку ваш вопрос конкретно о void MyMethod(IFoo f), я хотел дать реалистичный пример по крайней мере одного типа ситуаций, когда использование общего метода имеет больше смысла (для меня), чем интерфейс. (Да, я потратил немного времени на это, но я хотел попробовать несколько разных идей: D)

Существует два блока кода: первый - это только общий метод и некоторый контекст, второй - полный код для примера, включая множество комментариев, начиная от заметок о возможных различиях между этим и эквивалентным не общим реализация, а также различные вещи, которые я пробовал при реализации, которые не работали, и заметки о различных вариантах, которые я сделал, и т.д. TL; DR и все такое.

Концепция

    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

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

namespace IFooedYouOnce
{
    // IFoo
    //
    // It personality is so magnetic, it 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 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 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 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 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.!

Ответ 2

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

Ответ 3

Делать такие вещи проще:

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

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

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

... в то время как для метода "только для интерфейса" требуется "промежуточный" интерфейс (IFooBar)...

interface IFoo {
}

interface IBar {
}

interface IFooBar : IFoo, IBar {
}

class FooBar : IFooBar {
}

void MyMethod(IFooBar f) {
}

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

Ответ 4

Через 2 года я нашел очень простой и полезный случай. Рассмотрим эту общую схему:

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

Я всегда хотел написать вспомогательный метод, чтобы избежать необходимости писать весь этот шаблон для каждого поля:

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

К сожалению, это незаконно в С#, потому что вы не можете использовать интерфейс для параметров ref, вы должны использовать конкретный тип, который вы передадите, и ничего больше. Поэтому вам нужно написать другой метод для каждого отдельного поля, которое вы хотите использовать. Подождите, что именно то, что генерики делают для вас:

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

Теперь все работает по назначению!

Ответ 5

В этом конкретном случае выгоды нет. В общем случае вы не указали бы это на уровне метода, а на уровне класса. Например.

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

Обратите внимание, что мы требуем, чтобы T реализовал IFoo из-за метода Add, где нам нужно вызвать DoSomethingImportantMethod, реализованный IFoo.

Но заметьте в методе Get, что мы вернем T, предоставленный конечным пользователем этого класса, вместо простого старого IFoo, что уменьшит необходимость того, чтобы разработчик всегда отбрасывал их фактический конкретный T.

Пример:

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

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

Обратите внимание, что если бы я просто возвращал IFoo, тогда мне пришлось бы делать это на последней строке:

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

Ответ 6

Метод интерфейса предоставит вам f типа IFoo, тогда как общая версия предоставит вам тип T с ограничением, которое T должно реализовать IFoo.

Второй метод позволит вам иметь какой-то поиск в зависимости от T, так как у вас есть конкретный тип для работы.

Ответ 7

ссылаясь на контрольный показатель, указанный выше

@Branko, вызов метода через интерфейс на самом деле медленнее, чем > обычный вызов виртуального метода... Вот простой тест: > pastebin.com/jx3W5zWb - Thomas Levesque 29.08.2011 в 0:33

запуск кода в Visual Studio 2015 результат примерно эквивалентен между прямым вызовом и сквозным интерфейсом:

  • Прямой вызов: 90,51 миллисекун; 112,49 миллисекун; 81,22 миллисекунда
  • Через интерфейс: 92,85 миллисекун, 90,14 миллисекунды; 88,56 миллисекунда

код, используемый для сравнения (из http://pastebin.com/jx3W5zWb):

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()
            {
            }
        }
    }
}

Ответ 8

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

Другой (может быть, лучший) вопрос - эквивалентны эти два варианта?