Самое важное различие между вашим первым примером и вторым называется type safety .
Я предполагаю, что вы задаете вопрос с точки зрения языка статического типа, такого как C # или Java.
В версии, в которой используются обобщения, ваш компилятор гарантирует, что вы всегда работаете с правильными типами, тогда как во второй версии (той, которая использует более общий тип) вы должны вручную проверить это самостоятельно. Также компилятор будет постоянно заставлять вас приводить ваш общий тип (например, Entity
) к более конкретному (например, Customer
), чтобы фактически использовать службы, предоставляемые объектом.
Другими словами, вам приходится постоянно бороться с компилятором, поскольку он будет постоянно требовать, чтобы мы приводили типы, чтобы он мог проверять код, который мы пишем.
с родовыми элементами
В первом примере используется переменная типа T
на уровне интерфейса. Это означает, что когда вы когда-либо определяете переменную для этого типа интерфейса (или когда вы ее реализуете), вы также будете вынуждены предоставить аргумент типа для T
.
Например
IRepository<Customer> custRepo = ...;
То есть везде, где появляется T
, его необходимо заменить на Customer
, верно?
Как только вы определили аргумент типа для T
как Customer
, это выглядит так, как будто объявление вашего интерфейса изменится, в глазах компилятора, на что-то вроде этого:
public interface IRepository
{
Customer Get(int key);
IQueryable<Customer> Get();
void Save(Customer obj);
void Delete(Customer obj);
}
Поэтому, когда вы используете его, компилятор заставит вас уважать аргумент типа:
Customer customer = custRepo.Get(10);
customer.setEmail("lskywalker@gmail.com");
custRepo.Save(customer);
В приведенном выше примере все методы репозитория работают только с типом Customer
, и поэтому я не могу передать неправильный тип (например, Employee) методам, поскольку компилятор будет обеспечивать безопасность типов везде:
Employee employee = empRepo.Get(10);
custRepo.Save(employee); //Uh oh! Compiler error here
без универсальных элементов
С другой стороны, во втором примере все, что вы сделали, - это решили, что вы будете использовать более общий тип. Этим вы жертвуете безопасностью типов в обмен на некоторую гибкость:
Например:
IRepository custRepo = ...;
Entity customer = custRepo.Get(10);
((Customer) customer).setEmail("lskywalker@gmail.com"); //Now you'll need casting
custRepo.Save(customer);
В приведенном выше случае вы вынуждены всегда приводить свой Entity
к более удобному для использования типу, например Customer
, чтобы иметь возможность использовать то, что он предлагает. Это приведение является небезопасной операцией и потенциально может привести к ошибкам (если мы когда-либо допустим ошибку в наших предположениях о типах, которые мы используем).
Кроме того, типы репозитория не мешают передавать неправильные типы в методы, и вы можете делать семантические ошибки:
Entity employee = empRepo.Get(10);
custRepo.Save(employee); //Uh Oh!
Если вы сделаете это таким образом, вам, вероятно, придется в вашем CustomerRepo
удостовериться, что ваша сущность на самом деле соответствует Customer
, чтобы предотвратить ошибку, подобную той, что в последней строке примера выше.
Другими словами, вы бы вручную реализовали тип безопасности типов, который компилятор мог бы дать вам автоматически, если бы вы использовали обобщенные значения.
Это начинает звучать так, как будто мы пытаемся использовать наш статически типизированный язык, как если бы он был динамически типизированным, верно? Вот почему мы должны бороться с компилятором до конца, чтобы обеспечить соблюдение этого стиля программирования.
О параметрическом полиморфизме
Теперь вы можете изучить идею о том, что дженерики также являются формой полиморфизма, известной как параметрический полиморфизм . Вы также можете прочитать этот другой ответ Я дал еще один вопрос, в котором я цитирую большую статью о полиморфизме, которая может помочь вам расширить ваше понимание темы за пределы простого наследования классов и интерфейсов.
Языки с динамическим типом и языки со статическим типом Дебаты
Теперь, интересный вывод, который может помочь вам изучить это немного дальше, заключается в том, что динамические языки (например, JavaScript, Python, Ruby и т. Д.), Где вам не нужно делать явные объявления типов, на самом деле работают как ваши бесплатный пример.
Динамические языки работают так, как будто все ваши переменные имеют тип Object
, и они просто позволяют вам делать с этим объектом все, что вы хотите, чтобы избежать необходимости постоянно приводить ваши объекты к различным типам. Программист обязан писать тесты, чтобы убедиться, что объект всегда используется надлежащим образом.
Всегда существовали серьезные споры между защитниками статически типизированных языков и тех, кто предпочитает динамически типизированные языки. Это то, что мы обычно называем войной падубов .
Я полагаю, что на самом деле это тема, которую вы, возможно, захотите изучить более глубоко, чтобы по-настоящему понять фундаментальные различия между вашими двумя примерами и узнать, как статическая типизация и безопасность типов из дженериков сравниваются с большой гибкостью простого использования. динамические типы.
Я бы порекомендовал вам прочитать удивительную статью под названием Динамически типизированные языки Лоуренса Тратта из Борнмутского университета.
Теперь в языках со статической типизацией, таких как C # или Java, от нас обычно ожидают, что мы будем использовать безопасность типов как можно лучше. Но ничто не мешает нам писать код, как вы это делаете на динамическом языке, просто компилятор будет постоянно бороться с нами. Это ваш второй пример.
Если это так, и если это способ программирования, который больше резонирует с вами и вашим стилем работы, или если он предлагает вам гибкость, к которой вы стремитесь, то, возможно, вам следует подумать об использовании языка динамического типа, возможно, даже тот, который можно комбинировать поверх вашей текущей платформы (например, IronRuby или IronPython ), так что вы также можете повторно использовать ранее существующий код из вашего текущего статически типизированного языка.