Різниця в стилі: IDictionary vs Dictionary


76

У мене є друг, який тільки починає займатися розробкою .NET після того, як він розроблявся на Java віками, і, переглянувши деякі його коди, я помічаю, що він досить часто робить наступне:

IDictionary<string, MyClass> dictionary = new Dictionary<string, MyClass>();

Він оголошує словник інтерфейсом, а не класом. Зазвичай я роблю наступне:

Dictionary<string, MyClass> dictionary = new Dictionary<string, MyClass>();

Я б використовував інтерфейс IDictionary лише тоді, коли це потрібно (скажімо, наприклад, щоб передати словник методу, який приймає інтерфейс IDictionary).

Моє запитання: чи є якісь переваги в його способі робити щось? Це поширена практика в Java?


11
Для локальних змінних насправді немає сенсу.
Йорен


5
Хороша практика, Але остерігайтеся вартості виступу: nimaara.com/2016/03/06/beware-of-the-idictionary-tkey-tvalue
MaYaN,

1
Дві основні реалізації IDictionary( Dictionaryі ConcurrentDictionary) мають дуже різні характеристики продуктивності (наприклад Count, дуже швидкі для перших і дуже повільні для других). Як така, моєю рекомендацією було б вказати конкретний клас (а не IDictionary) в C #, де це можливо, щоб споживач знав, як ефективно використовувати словник.
mjwills

1
Крім того, IDictionaryє деякі “примхи” продуктивності - nimaara.com/2016/03/06/beware-of-the-idictionary-tkey-tvalue .
mjwills

Відповіді:


67

Якщо IDictionary є "більш загальним" типом, ніж Dictionary, то має сенс використовувати більш загальний тип для оголошення змінних. Таким чином, вам не потрібно так сильно дбати про клас реалізації, призначений змінній, і ви можете легко змінити тип у майбутньому, не змінюючи багато наступного коду. Наприклад, на Java це часто вважають кращим

List<Integer> intList=new LinkedList<Integer>();

ніж це робити

LinkedList<Integer> intList=new LinkedList<Integer>();

Таким чином, я впевнений, що всі наступні коди розглядають список як Список, а не LinkedList, що полегшує в майбутньому вимкнути LinkedList для Vector або будь-якого іншого класу, який реалізує List. Я б сказав, що це спільне для Java та хорошого програмування загалом.


25
+1, але ви можете скористатися словом "загальний", а не "загальний", до якого вже додано пакет значень :)
AakashM,

28

Ця практика не обмежується лише Java.

Він часто використовується і в .NET, коли ви хочете від'єднати екземпляр об'єкта від класу, який ви використовуєте. Якщо ви використовуєте інтерфейс, а не клас, ви можете змінити тип резервної копії, коли це потрібно, не порушуючи решту коду.

Ви також побачите, що ця практика активно використовується у роботі з контейнерами IoC та інстанціацією за допомогою шаблону Factory.


1
Але ви втрачаєте всю функціональність, яку може надавати Словник, а не в IDictionary.
поводи

10
Але якщо ви замість цього можете використовувати IDictionary ..., ви спочатку не використовуєте функціонал.
Джастін Нісснер,

1
Але якщо вам потрібна лише функціональність в IDictionary ... Легше змінити IDictionary на Dictionary, ніж навпаки :)
Свіш,

1
Скласти Словник на ID Dictionary простіше, ніж навпаки. :)
поводити

10
Втрата функціональних можливостей, які надає Словник, а IDictionary - не вся суть цієї ідіоми. Оголошення вашої локальної змінної як IDictionary змушує вас обмежити використання цієї змінної тим, що доступно через інтерфейс. Потім, якщо пізніше ви виявите, що ви повинні використовувати інший клас, який реалізує IDictionay замість Dictionary, ви можете зробити це, змінивши один рядок, який викликає конструктор конкретних класів (оскільки ви знаєте, що ваш код використовує його лише як ID Dictionary, а не словник ).
Stephen C. Steel

15

Ваш друг дотримується дуже корисного принципу:

"Абстрагуйтесь від деталей реалізації"


6
Але ми маємо справу з деталями реалізації. Ми не створюємо інтерфейс для класу - ми просто оголошуємо локальну змінну.
поводи

8
Не дуже, ІМХО. Якщо вам достатньо членів інтерфейсу, я справді рекомендую дотримуватися інтерфейсу. Зробіть свій код якомога загальнішим.
Віталій Липчинський

8

Завжди потрібно намагатися програмувати на інтерфейс, а не на конкретний клас.

На Java або будь-якій іншій об'єктно-орієнтованій мові програмування.

У світі .NET поширене використання, Iщоб вказати, що це інтерфейс, який ви використовуєте. Я думаю, що це частіше, оскільки в C # вони не мають implementsі extendsпосилаються на клас проти інтерфейсу успадкування.

Я думаю, що набирала сироватка

 class MyClass:ISome,Other,IMore 
 { 
 }

І ви можете сказати, ISomeщо IMoreце інтерфейси, поки Otherце клас

У Java у такому немає потреби

 class MyClass extends Other implements Some, More {
 }

Концепція все ще застосовується, спробуйте кодувати інтерфейс.


3
Я не думаю, що його запитання стосувалось того, як працюють інтерфейси чи робота, чи чому вони починаються з I. Крім того, базові класи повинні бути оголошені перед інтерфейсами. Отже, public class MyClass : BaseClass, IFirstInterface, ISecondInterfaceбуло б правильно. На початку інтерфейсу в I немає нічого особливого. Компілятор не ставиться до цього інакше. Це маркер для інших програмістів. Ви можете назвати свої інтерфейси без них. . . але інші програмісти можуть вас ненавидіти.
tandrewnichols

7

Для локальних змінних та приватних полів, які вже є деталями реалізації, краще використовувати конкретні типи, ніж інтерфейси для оголошень, оскільки конкретні класи пропонують підвищення продуктивності (пряме відправлення швидше, ніж віртуальне / відправлення інтерфейсу). JIT також зможе легше вбудовувати методи, якщо ви без необхідності не призначаєте типи інтерфейсу в локальних деталях реалізації. Якщо екземпляр конкретного типу повертається із методу, який повертає інтерфейс, приведення здійснюється автоматично.


4
Це мікрооптимізація, яка повинна виконуватися лише в тому випадку, якщо у вас є певне вузьке місце щодо продуктивності, яке не повинно стимулювати дизайн.
Yishai,

8
Це не мікрооптимізація. Ви вже в реалізації, де обрали конкретну реалізацію. Чому робити вигляд, що цього не зробили за рахунок продуктивності?
Sam Harwell

5

Наскільки я бачив, розробники Java, як правило, використовують абстракцію (і шаблони дизайну) частіше, ніж розробники .NET. Це здається ще одним його прикладом: навіщо оголошувати конкретний клас, коли він, по суті, буде працювати лише з членами інтерфейсу?


Я був з вами до другого речення. Як ще потрібно отримати посилання на реалізатора інтерфейсу? Ви повинні створити інстанцію того, що це реалізує. new IDictionary<string, double>()не буде працювати, і в будь-якому випадку не має сенсу.
Tom W

Я насправді не розумію ... моя думка полягала в тому, що той, хто коли-небудь користується словником, захоче лише використовувати можливості IDictionary. Очевидно, що вам потрібно щось створити, але якщо ви використовуєте цей шаблон, наступні коди означатимуть те саме для тих, хто коли-небудь використовував клас: IDictionary <T1, T2> dictionary = new Dictionary <T1, T2> (); та ID Dictionary <T1, T2> словник = новий ConcurrentDictionary <T1, T2> (); Якщо ви не будете використовувати конкретні члени Dictionary або ConcreteDictionary, має сенс не оголошувати їх як тип змінної класу, додаючи шар абстракції.
Гергелі Орош

4

Найчастіше ви бачите тип інтерфейсу (IDictionary), який використовується, коли учасник піддається впливу зовнішнього коду, незалежно від того, знаходиться він поза збіркою або просто поза класом. Як правило, більшість розробників використовують конкретний тип внутрішньо для визначення класу, тоді як вони виставляють інкапсульоване властивість, використовуючи тип інтерфейсу. Таким чином, вони можуть використовувати можливості конкретного типу, але якщо вони змінюють конкретний тип, інтерфейс класу оголошення не повинен змінюватися.

віджет публічного класу
{
    приватний словник <рядок, рядок> map = новий словник <рядок, рядок> ();
    загальнодоступний словник <рядок, рядок> Карта
    {
        отримати {повернути карту; }
    }
}

пізніше може стати:

клас SpecialMap <TKey, TValue>: IDictionary <TKey, TValue> {...}

віджет публічного класу
{
    private SpecialMap <рядок, рядок> map = new SpecialMap <рядок, рядок> ();
    загальнодоступний словник <рядок, рядок> Карта
    {
        отримати {повернути карту; }
    }
}

без зміни інтерфейсу віджета і необхідності змінювати інший код, що вже його використовує.


3

У описаній ситуації майже кожен розробник Java використовував би інтерфейс для оголошення змінної. Спосіб використання колекцій Java, мабуть, один із найкращих прикладів:

Map map = new HashMap();
List list = new ArrayList();

Думаю, це просто робить вільне зчеплення у багатьох ситуаціях.


3

Колекції Java включають безліч реалізацій. Тому мені набагато легше цим скористатися

List<String> myList = new ArrayList<String>();

Тоді в майбутньому, коли я зрозумію, що мені потрібно "myList", щоб бути безпечним для потоку, щоб просто змінити цей один рядок на

List<String> myList = new Vector<String>();

І не змінюйте жодного іншого рядка коду. Сюди входять також геттери / сетери. Якщо ви подивитесь на кількість реалізацій Map, наприклад, ви можете уявити, чому це може бути гарною практикою. В інших мовах, де є лише пара реалізацій для чогось (вибачте, не великий .NET-хлопець), але в Objective-C насправді є лише NSDictionary та NSMutableDictionary. Отже, це не має такого сенсу.

Редагувати:

Не вдалося вдарити по одному з моїх ключових моментів (просто натякнув на це з геттером / сеттерами).

Вищевказане дозволяє мати:

public void setMyList(List<String> myList) {
    this.myList = myList;
}

І клієнту, який використовує цей виклик, не потрібно турбуватися про основну реалізацію. Використовуючи будь-який об’єкт, який відповідає інтерфейсу List, який вони можуть мати.


зміна цього рядка на: Vector <String> myList = new Vector <String> (); також змінюється лише один рядок.
tster

1
@tster - але які ще коди викликають методи, специфічні для ArrayList, які потрібно було б змінити, оскільки Vector не має точно такого самого методу?
Гордон Такер,

public void setMyList (List <String> list) {myList = list; } Дійсний при оголошенні його як Списку, не так при використанні прямої реалізації. Тобто клієнт може використовувати будь-який список, який у них є, не турбуючись про перехід на вашу реалізацію.
MarkPowell

3

Виходячи зі світу Java, я погоджуюсь з тим, що у вас вбудована мантра "програма до інтерфейсу". Програмуючи інтерфейс, а не реалізацію, ви робите ваші методи більш розширюваними до майбутніх потреб.


1
якщо майбутня потреба не полягає у зміні підпису договору, такому як нова властивість або спосіб.
Matthew Whited

3

IDictionaryє інтерфейсом і Dictionaryє класом.

Dictionaryзнаряддя праці IDictionary.

Це означає, що це можливо для посилання на Dictionaryекземпляр за допомогою / за IDictionaryекземпляром і викликати більшість Dictionaryметодів та властивостей через IDictionaryекземпляр.

Дуже рекомендується використовувати якомога більше інтерфейсів, оскільки інтерфейси абстрагують модулі та збірки додатків, дозволяють поліморфізм, який є дуже поширеним і корисним у багатьох ситуаціях та випадках, і дозволяє замінювати один модуль іншим, не торкаючись інших модулів .

Припустимо, що в даний час програміст написав:

IDictionary<string> dictionary = new Dictionary<string>();

А тепер dictionaryвикликає методи та властивості Dictionary<string>.

У майбутньому бази даних збільшилися в розмірах, і ми виявляємо, що Dictionary<string>це занадто повільно, тому ми хочемо замінити Dictionary<string>на RedBlackTree<string>, що швидше.

Отже, все, що потрібно зробити, - це замінити наведену вище інструкцію на:

IDictionary<string> dictionary = new RedBlackTree<string>();

Звичайно, що якщо RedBlackTreeреалізуєIDictionary тоді весь код програми успішно компілюється, і у вас є нова версія програми, де програма тепер працює швидше та ефективніше, ніж попередня версія.

Без інтерфейсів таку заміну було б зробити важче, і програмісти та розробники вимагали б змінити більше коду, який є потенційним для помилок.


1

Я виявив, що для локальних змінних, як правило, не має великого значення, використовуєте ви інтерфейс або конкретний клас.

На відміну від членів класу або підписів методів, дуже мало зусиль з рефакторингу, якщо ви вирішили змінити типи, а також змінна не видно за межами місця її використання. Насправді, коли ви використовуєте varдля декларування місцевих жителів, ви отримуєте не тип інтерфейсу, а тип класу (якщо ви явно не привели до інтерфейсу).

Однак, оголошуючи методи, членів класу або інтерфейси, я думаю, що це заощадить вам трохи головного болю, якщо використовувати тип інтерфейсу заздалегідь, а не поєднувати загальнодоступний API з певним типом класу.


2
Це була і моя думка - для визначень методів важливіше спробувати використовувати загальний тип, оскільки метод можна використовувати повторно. Для локальних змінних я не бачу переваг.
поводи

1
Особисто я думаю, що заслугою є те, що ви дуже легко виявите можливі залежності від класів реалізації. Як, наприклад, якщо ви передаєте свою локальну змінну утилітному методу чи щось інше.
NickDK

2
Щодо локальних змінних, аргументи, які я бачив, полягають у тому, що 1) він тримає вас дисципліновано у використанні інтерфейсів, і 2) дозволяє вашому коду бути менш чутливим до типів повернення викликів функцій, які призначаються локалам - це означає, що функція може змініть тип конкретного, не впливаючи на ваш код (доки тип повернення все ще відповідає інтерфейсу, звичайно).
Л.Бушкін,

1
@LBushkin, я також виявляю, що коли ви розробляєте метод, і ви робите рефакторинг, як метод екстрактування тощо, конкретний тип може мати тенденцію до поширення, хоча інтерфейс повинен був бути використаний.
Yishai,

1

Використання інтерфейсів означає, що "словник" у наступному коді може бути будь-якою реалізацією IDictionary.

Dictionary1 dictionary = new Dictionary1();
dictionary.operation1(); // if operation1 is implemented only in Dictionary1() this will fail for every other implementation

Це найкраще видно, коли ви приховуєте конструкцію об’єкта:

IDictionary dictionary = DictionaryFactory.getDictionary(...);

Я б погодився, що при використанні фабрики для повернення неспецифічного словника це гарна ідея.
поводи

1
Я мав на увазі те, що використання інтерфейсу змушує вас мислити "загальними" термінами, забезпечуючи роз'єднання класів (у моєму прикладі метод, що використовує Dictionary1, повинен бути знайомим із внутрішньою структурою класу, а не з інтерфейсом)
Manrico Corazzi

1

Я зіткнувся з такою ж ситуацією з розробником Java. Він створює екземпляри колекцій І об'єктів для їх інтерфейсу таким же чином. Наприклад,

IAccount account = new Account();

Властивості завжди отримуються / встановлюються як інтерфейси. Це викликає проблеми з сериализации, що пояснюється дуже добре тут


1
Це проблема лише з "чарівною" серіалізацією, яку .NET використовує в інтерфейсах веб-служб. Практика використання інтерфейсів замість конкретних класів є цілком обґрунтованою.
Даніель Приден

У нас виникла проблема із серіалізацією ViewState в ASP.NET 1.1
Джим Шуберт,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.