Должен ли я использовать интерфейс, такой как IEnumerable, или конкретный класс, например List <>

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

Скажем, что мне нужно создать и передать контейнер в моей программе. У меня, вероятно, нет сильного мнения об одном виде контейнера по сравнению с другим, по крайней мере на данном этапе, но я выбираю один; ради аргумента, скажем, я собираюсь использовать List < > .

Вопрос: Лучше ли писать мои методы для приема и возврата интерфейса высокого уровня, такого как С# IEnumerable? Или я должен писать методы для принятия и передачи определенного класса контейнера, который я выбрал.

Какие факторы и критерии я должен искать? Какие программы работают от одного или другого? Влияет ли компьютерный язык на ваше решение? Представление? Размер программы? Личный стиль?

(Это даже имеет значение?)

** (Домашнее задание: найдите его, но, пожалуйста, напишите свой ответ здесь, прежде чем искать свою собственную, чтобы не уклонять вас.) *

Ответ 1

Ваш метод должен всегда принимать наименее специфический тип, который должен выполнить свою функцию. Если ваш метод нужно перечислить, примите IEnumerable. Если это нужно сделать IList<> -специфические вещи, по определению вы должны дать ему IList<>.

Ответ 2

Единственное, что должно повлиять на ваше решение, - это то, как вы планируете использовать параметр. Если вы только перебираете его, используйте IEnumerable<T>. Если вы обращаетесь к индексированным членам (например, var x = list[3]) или каким-либо образом изменяете список (например, list.Add(x)), используйте ICollection<T> или IList<T>.

Ответ 3

Всегда есть компромисс. Общее правило заключается в том, чтобы объявить вещи максимально возможными по иерархии. Поэтому, если вам нужен только доступ к методам в IEnumerable, то это то, что вы должны использовать.

Еще одним недавним примером SO-запроса был C API, который вместо имени файла * (или дескриптора файла) взял имя файла. Там имя файла строго ограничено тем, что может быть передано в бозе (есть много вещей, которые вы можете передать с файловым дескриптором, но только с файлом).

После того, как вы начнете кастинг, вы либо зашли слишком высоко, либо вы должны сделать второй метод, который принимает более конкретный тип.

Единственное исключение из этого, о котором я могу думать, это то, что скорость является абсолютной необходимостью, и вы не хотите проходить через вызов виртуального метода. Объявление определенного типа устраняет накладные расходы виртуальных функций (будет зависеть от языка/среды/реализации, но как общего утверждения, которое, скорее всего, будет правильным).

Ответ 4

Это была дискуссия со мной, которая вызвала этот вопрос, поэтому Евро Мичелли уже знает мой ответ, но вот он!:)

Я думаю, что Linq to Objects уже дает отличный ответ на этот вопрос. Используя простейший интерфейс для последовательности элементов, которые он мог бы, он дает максимальную гибкость в отношении того, как вы реализуете эту последовательность, которая позволяет ленивое поколение, повышая производительность, не жертвуя производительностью (не в каком-либо реальном смысле).

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

Есть те, кто скажет вам, что "проще" сделать все данные в классе общедоступными, на всякий случай вам нужно будет получить к нему доступ. Точно так же Euro советовал, что лучше использовать богатый интерфейс для контейнера, такого как IList<T> (или даже конкретный класс List<T>), а затем очистить беспорядок позже.

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

Итак, предположим, что IEnumerable<T> будет представлять последовательность. Затем в тех случаях, когда вам нужно Add или Remove элементы (но все же не нужно искать по индексам), используйте IContainer<T>, который наследует IEnumerable<T> и поэтому будет идеально взаимодействовать с вашим другим кодом.

Таким образом, будет совершенно ясно (только при локальном изучении некоторого кода), что именно этот код сможет сделать с данными.

Маленькие программы требуют меньше абстракции, это правда. Но если они успешны, они, как правило, становятся большими программами. Это намного проще, если они используют простые абстракции в первую очередь.

Ответ 5

Это имеет значение, но правильное решение полностью зависит от использования. Если вам нужно сделать простое перечисление, тогда обязательно используйте IEnumerable таким образом, чтобы вы могли передать любому разработчику доступ к необходимым функциям. Однако, если вам нужна функция списка, и вы не хотите создавать новый экземпляр списка, если случайно каждый раз, когда метод вызывается, перечислимый, который был передан, не был списком, тогда перейдите со списком.

Ответ 6

Я ответил на аналогичный вопрос С# здесь. Я думаю, вы всегда должны предоставить самый простой контракт, который, по моему мнению, обычно является IEnumerable Of T.

Реализация может быть обеспечена внутренним типом BCL - будь то Set, Collection, List и т.д., чьи требуемые члены отображаются вашим типом.

Ваш абстрактный тип всегда может наследовать простые типы BCL, которые реализуются вашими конкретными типами. Это, на мой взгляд, позволяет вам легче придерживаться LSP.