Интерфейс ковариантного списка, который позволяет удалять

В C# мы не можем вызывать Function(IList<Subclass>) с IList<Derived>:

class Pet { public string Name { get; set; } }
class Dog : Pet { }
class Cat : Pet { }

static void Remove(IList<Pet> pets, string name) {
    var doomed = pets.FirstOrDefault(pet => pet.Name == name);
    if (doomed != null)
    {
        pets.Remove(doomed);
    }
}

public static void Main() {
    var dogs = new List<Dog> {
        new Dog { Name = "Rover" },
        new Dog { Name = "Spot" },
    };
    Remove(dogs, "Spot"); // illegal
}

IList является инвариантным, потому что потенциально вы можете добавить Cat к List<Dog>, что, очевидно, плохо.

Но в этом случае все, что я хочу сделать, это удалить элементы из списка. Это не представляет угрозы целостности Списка.

Я знаю, что существует IReadOnlyList, который дает вам ковариантное «представление» списка. Есть ли что-то вроде ICovariantList, которое дает вам ковариантно-дружественные методы List, включая метод Remove? Если нет, то кто-нибудь знает, почему?


Обновлено: В ответ на комментарии следующий код более репрезентативен для моего фактического приложения, но мы должны представить, что его менее легко изменить. У нас есть много списков Dogs и Cats и других подклассов Pet, которые объединяются вместе с помощью чего-то вроде GetPetLists, который является перечислением списков домашних животных, с которыми мы хотим работать.

Если бы мы просто хотели просмотреть эти списки, мы могли бы использовать IReadOnlyView. Но мы хотим удалить элемент из одного из списков:

class Pet { public string Name { get; set; } }
class Dog : Pet { }
class Cat : Pet { }

List<Dog> dogs = new List<Dog> {
    new Dog { Name = "Rover" },
    new Dog { Name = "Spot" },
};

List<Cat> cats = new List<Cat> {
    new Cat { Name = "Snowball" },
    new Cat { Name = "Fluffy" },
};

// IEnumerable<IReadOnlyList<Pet>> GetPetLists() // legal, but not what we need
// IEnumerable<ICovariantList<Pet>> GetPetList() // what I'm looking for; allows Remove
IEnumerable<IList<Pet>> GetPetLists() // illegal, for good reason
{
    yield return dogs;
    yield return cats;
}

void RemovePet(string name)
{
    foreach (var list in GetPetLists())
    {
        var doomed = list.FirstOrDefault(pet => pet.Name == name);
        if (doomed != null)
        {
            list.Remove(doomed);
        }
    }
}

void Test()
{
    RemovePet("Spot"); 
}

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

Dai 22.02.2023 00:20

В любом случае, для быстрого исправления, delegate типы в C# поддерживают ковариантность и контравариантность, поэтому вы можете использовать отдельный параметр для передачи делегата в Remove метод.

Dai 22.02.2023 00:20

Кстати, я знаю, что ваш опубликованный код - это просто игрушечный пример, но использование FirstOrDefault плохо для сложности времени выполнения, потому что вы делаете O(n) + O(n) - и Remove не будет генерировать, если его аргумент не является членом коллекции: вместо этого он возвращает true /false, если элемент был найден и удален: learn.microsoft.com/en-us/dotnet/api/…

Dai 22.02.2023 00:22

Я упомянул об этом, потому что не был уверен, верны ли мои замечания о вашем использовании FirstOrDefault, поэтому мой ответ был составлен с учетом контекста, и это было потому, что вы не раскрыли, каков ваш фактический вариант использования, а именно плохая форма на SO, потому что именно так люди вступают в споры о проблемах X/Y.

Dai 22.02.2023 00:35

Вы против этой подписи метода: static void Remove<T>(IList<T> pets, string name) where T : Pet?

moreON 22.02.2023 01:15

@moreON Я добавил код, который больше соответствует моему реальному приложению. Это в большой коммерческой кодовой базе. Существуют десятки различных списков «Pet», все различные подклассы, которые агрегируются через итератор. Я хочу выполнить операцию над этими списками. Пока я не хочу делать какие-либо манипуляции со списками, я мог бы заставить итератор возвращать перечисление IReadOnlyList<Pet>. Но я хочу удалить элемент из списков.

Mud 22.02.2023 01:31
Калькулятор CGPA 12 для семестра
Калькулятор CGPA 12 для семестра
Чтобы запустить этот код и рассчитать CGPA, необходимо сохранить код как HTML-файл, а затем открыть его в веб-браузере. Для этого выполните следующие...
Как собрать/развернуть часть вашего приложения Angular
Как собрать/развернуть часть вашего приложения Angular
Вам когда-нибудь требовалось собрать/развернуть только часть вашего приложения Angular или, возможно, скрыть некоторые маршруты в определенных средах?
Запуск PHP на IIS без использования программы установки веб-платформы
Запуск PHP на IIS без использования программы установки веб-платформы
Установщик веб-платформы, предлагаемый компанией Microsoft, перестанет работать 31 декабря 2022 года. Его закрытие привело к тому, что мы не можем...
Оптимизация React Context шаг за шагом в 4 примерах
Оптимизация React Context шаг за шагом в 4 примерах
При использовании компонентов React в сочетании с Context вы можете оптимизировать рендеринг, обернув ваш компонент React в React.memo сразу после...
Библиотека для работы с мороженым
Библиотека для работы с мороженым
Лично я попрощался с операторами print() в python. Без шуток.
Настройка шаблона Metronic с помощью Webpack и Gulp
Настройка шаблона Metronic с помощью Webpack и Gulp
Я пишу эту статью, чтобы поделиться тем, как настроить макет Metronic с помощью Sass, поскольку Metronic предоставляет так много документации, и они...
4
6
54
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Возможно, вы могли бы сделать параметр IEnumerable<Pet>, который является ковариантным, начиная с C# 4 (поскольку вы не можете изменить его напрямую), затем вы можете попробовать преобразовать его в IList, а затем использовать метод Remove:

static void Remove(IEnumerable<Pet> pets, string name) {
    var doomed = pets.FirstOrDefault(pet => pet.Name == name);
    if (doomed != null)
    {
        if (pets is IList list && !list.IsReadOnly) 
        {
            list.Remove(doomed);
        }
    }
}

Насколько я понимаю, этот код не работает. IList требуется параметр типа. Я бы хотел сделать это IList<Pet>, но is не будет считаться верным для IList<Pet>, если я пропущу IList<Dog>.

Mud 22.02.2023 01:54

@Mud System.Collections.IList и System.Collections.Generic.IList<T> - разные типы, в этом ответе, похоже, используется первый. learn.microsoft.com/en-us/dotnet/api/… Learn.microsoft.com/en-us/dotnet/api/…

moreON 22.02.2023 03:20

Вы правы! Немного взлома, но это работает. Спасибо. :)

Mud 22.02.2023 06:47
dotnetfiddle.net/gxC7d8 Предпочло бы, чтобы свойство Pets могло возвращать что-то вроде ICovariantList, которое включало бы только методы, не нарушающие инвариантность типов, что, по крайней мере, есть в IReadOnlyList и методе IList.Remove(). Увы.
Mud 22.02.2023 07:01

Мне кажется, что вы решаете свою проблему, просто и исключительно меняя подпись Remove с void Remove(IList<Pet> pets, string name) на void Remove<P>(IList<P> pets, string name) where P : Pet.

Теперь это работает:

class Pet { public string Name { get; set; } }
class Dog : Pet { }
class Cat : Pet { }

static void Remove<P>(IList<P> pets, string name) where P : Pet
{
    var doomed = pets.FirstOrDefault(pet => pet.Name == name);
    if (doomed != null)
    {
        pets.Remove(doomed);
    }
}

public static void Main()
{
    var dogs = new List<Dog> {
        new Dog { Name = "Rover" },
        new Dog { Name = "Spot" },
    };
    Remove(dogs, "Spot"); // Now Legal without any change to this code.
}

Ваше редактирование эффективно меняет ваш вопрос на то, как сделать этот незаконный код законным:

IEnumerable<IList<Pet>> GetPetLists() // illegal, for good reason
{
    yield return dogs;
    yield return cats;
}

Это не может быть законным, поэтому остальная часть вашего кода становится спорной. Поэтому это не может быть лучшим представлением вашего фактического кода. Можете ли вы дать нам что-нибудь, что представляет ваш код?

«Это не может быть законным». Это возможно, если вы измените IList<Pet> на IEnumerable<Pet> или IReadOnly<Pet>. Вопрос в том, существует ли эквивалентный ковариантный интерфейс, который также допускает Remove(), поскольку этот метод не нарушает инвариантность типов.

Mud 22.02.2023 17:01

Другие вопросы по теме