Является ли обычной (или поощряемой) практикой перегрузка функции для принятия IEnumerable <T>, ICollection <T>, IList <T>и т. Д.? - PullRequest
1 голос
/ 19 ноября 2009

EDIT

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


Предположим, у меня есть три перегрузки функции: одна принимает IEnumerable<T>, одна - ICollection<T>, а другая - IList<T>, что-то вроде следующего:

public static T GetMiddle<T>(IEnumerable<T> values) {
    IList<T> list = values as IList<T>;
    if (list != null) return GetMiddle(list);

    int count = GetCount<T>(values);

    T middle = default(T);
    int index = 0;

    foreach (T value in values) {
        if (index++ >= count / 2) {
            middle = value;
            break;
        }
    }

    return middle;
}

private static T GetMiddle<T>(IList<T> values) {
    int middleIndex = values.Count / 2;
    return values[middleIndex];
}

private static int GetCount<T>(IEnumerable<T> values) {
    // if values is actually an ICollection<T> (e.g., List<T>),
    // we can get the count quite cheaply
    ICollection<T> genericCollection = values as ICollection<T>;
    if (genericCollection != null) return genericCollection.Count;

    // same for ICollection (e.g., Queue<T>, Stack<T>)
    ICollection collection = values as ICollection;
    if (collection != null) return collection.Count;

    // otherwise, we've got to count values ourselves
    int count = 0;
    foreach (T value in values) count++;

    return count;
}

Идея в том, что если у меня есть IList<T>, это облегчает мою работу; с другой стороны, я все еще могу делать эту работу с ICollection<T> или даже IEnumerable<T>; реализация для этих интерфейсов не так эффективна.

Я не был уверен, сработает ли это вообще (если среда выполнения сможет выбирать перегрузку на основе переданного параметра), но я проверил это, и, похоже,

Мой вопрос: есть ли проблема с этим подходом, о котором я не задумывался? С другой стороны, действительно ли это хороший подход, но есть лучший способ выполнить его (возможно, попытавшись привести аргумент values до значения IList<T> и запустить более эффективную перегрузку, если приведение работает)? Мне просто интересно узнать мысли других.

Ответы [ 7 ]

5 голосов
/ 19 ноября 2009

Если вы посмотрите, как методы расширения LINQ реализуются с помощью Reflector, вы можете увидеть, что несколько методов расширения в IEnumerable , например Count (), пытаются преобразовать последовательность в ICollection или IList для оптимизации операции (например, используя свойство ICollection .Count вместо итерации по IEnumerable и подсчета элементов). Таким образом, ваш лучший выбор, скорее всего, примет IEnumerable , а затем проведет этот вид оптимизации, если ICollection или IList доступны.

2 голосов
/ 19 ноября 2009

Я думаю, что одной из версий, принимающих IEnumerable , будет путь, и внутри метода проверьте, является ли параметр одним из более производных типов коллекций. С тремя версиями, которые вы предлагаете, вы теряете преимущество в эффективности, если кто-то передает вам (время выполнения) IList , которое компилятор статически считает IEnumerable :

        IList<string> stringList = new List<string> { "A", "B", "C" };
        IEnumerable<string> seq = stringList;
        Extensions.GetMiddle(stringList); // calls IList version
        Extensions.GetMiddle(seq);        // calls IEnumerable version
1 голос
/ 18 октября 2012

Хотя допустимо перегрузить метод, чтобы он принимал либо базовый тип, либо производный тип со всеми остальными идентичными параметрами, это выгодно делать только в том случае, если компилятор часто сможет идентифицировать последнюю форму как быть лучшим матчем Поскольку для объектов, которые реализуют ICollection<T>, было бы очень распространено передавать код, которому требуется только IEnumerable<T>, было бы очень распространенным для реализаций ICollection<T> передаваться в перегрузку IEnumerable<T>. Следовательно, перегрузка IEnumerable<T>, вероятно, должна проверять, реализует ли переданный объект ICollection<T>, и обрабатывать его специально, если это так.

Если наиболее естественным способом реализации логики для ICollection<T> было бы написание специального метода для него, не было бы ничего особенно плохого в том, чтобы иметь публичную перегрузку, которая принимает ICollection<T>, и иметь IEnumerable<T> перегрузка вызывает ICollection<T> единицу, если дан объект, который реализует ICollection<T>. Если бы такая перегрузка была общедоступной, это не принесло бы особой пользы, но, скорее всего, ничего не повредило бы. С другой стороны, в ситуациях, когда объект реализует и IEnumerable<T>, и ICollection, но не ICollection<T> (например, List<Cat> реализует IEnumerable<Animal> и ICollection, но не ICollection<Animal>), один Возможно, вы захотите использовать оба интерфейса, но этого нельзя было бы сделать, либо приведя тип в методе, который их использует, либо передав метод, который использует их, и ссылку ICollection, и ссылку IEnumerable<T>. Последнее было бы очень уродливым в публичном методе, и первый подход утратил бы преимущества перегрузки.

1 голос
/ 19 ноября 2009

Мне действительно безразлично. Если бы я видел это по-твоему, я бы ничего не думал об этом. Но идея Джо имеет свои достоинства. Это может выглядеть следующим образом.

public static T GetMiddle<T>(IEnumerable<T> values)
{
  if (values is IList<T>) return GetMiddle((IList<T>)values);
  if (values is ICollection<T>) return GetMiddle((ICollection<T>)values);

  // Use the default implementation here.
}

private static T GetMiddle<T>(ICollection<T> values)
{
}

private static T GetMiddle<T>(IList<T> values)
{
}
1 голос
/ 19 ноября 2009

Я бы сказал, что это необычно и потенциально сбивает с толку, поэтому вряд ли будет хорошим выбором для публичного API.

Вы можете принять параметр IEnumerable и внутренне проверить, является ли он на самом деле ICollection или IList , и соответственно оптимизировать.

Это может быть аналогично некоторым оптимизациям некоторых методов расширения IEnumerable в .NET 3.5 Framework.

0 голосов
/ 19 ноября 2009

Нет. Это, конечно, необычно.

В любом случае. Поскольку IList наследуется от ICollection , IEnumerable и ICollection наследуется от IEnumerable , ваша единственная проблема - производительность в типах IEnumerable .

Я просто не вижу смысла перегружать функцию таким образом, предоставляя разные подписи для достижения абсолютно одинакового результата и принимая точно такие же типы, что и параметр (независимо от того, есть ли у вас IEnumerable или IList , Вы сможете передать его любой из трех перегрузок); это могло бы вызвать путаницу.

Когда вы перегружаете функцию, это просто способ передать другой тип параметра, который вы не можете передать в функцию, если бы она не имела этой сигнатуры.

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

0 голосов
/ 19 ноября 2009

Обычно при разработке интерфейсов вы хотите принять тип аргумента «наименьший общий знаменатель». Для возвращаемых типов это вопрос некоторых дискуссий. Я вообще думаю, что создание вышеупомянутых перегрузок является излишним. Самая большая проблема - это введение ненужных путей кода, которые теперь должны быть проверены. Лучше иметь один метод, который выполняет операцию в одну сторону и работает 100% времени. С учетом приведенных выше перегрузок вы можете иметь несоответствие в поведении и даже не осознавать этого, или, что еще хуже, вы можете случайно внести изменение в одну, а не в другие копии.

Если вы можете сделать это с IEnumerable<T>, используйте это, если нет, то используйте наименьший необходимый интерфейс.

...