В 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");
}
В любом случае, для быстрого исправления, delegate типы в C# поддерживают ковариантность и контравариантность, поэтому вы можете использовать отдельный параметр для передачи делегата в Remove метод.
Кстати, я знаю, что ваш опубликованный код - это просто игрушечный пример, но использование FirstOrDefault плохо для сложности времени выполнения, потому что вы делаете O(n) + O(n) - и Remove не будет генерировать, если его аргумент не является членом коллекции: вместо этого он возвращает true /false, если элемент был найден и удален: learn.microsoft.com/en-us/dotnet/api/…
Я упомянул об этом, потому что не был уверен, верны ли мои замечания о вашем использовании FirstOrDefault, поэтому мой ответ был составлен с учетом контекста, и это было потому, что вы не раскрыли, каков ваш фактический вариант использования, а именно плохая форма на SO, потому что именно так люди вступают в споры о проблемах X/Y.
Вы против этой подписи метода: static void Remove<T>(IList<T> pets, string name) where T : Pet?
@moreON Я добавил код, который больше соответствует моему реальному приложению. Это в большой коммерческой кодовой базе. Существуют десятки различных списков «Pet», все различные подклассы, которые агрегируются через итератор. Я хочу выполнить операцию над этими списками. Пока я не хочу делать какие-либо манипуляции со списками, я мог бы заставить итератор возвращать перечисление IReadOnlyList<Pet>. Но я хочу удалить элемент из списков.
Возможно, вы могли бы сделать параметр 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 System.Collections.IList и System.Collections.Generic.IList<T> - разные типы, в этом ответе, похоже, используется первый. learn.microsoft.com/en-us/dotnet/api/… Learn.microsoft.com/en-us/dotnet/api/…
Вы правы! Немного взлома, но это работает. Спасибо. :)
Pets
могло возвращать что-то вроде ICovariantList
, которое включало бы только методы, не нарушающие инвариантность типов, что, по крайней мере, есть в IReadOnlyList
и методе IList.Remove()
. Увы.
Мне кажется, что вы решаете свою проблему, просто и исключительно меняя подпись 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(), поскольку этот метод не нарушает инвариантность типов.
"Кто-нибудь знает, почему?" - Я не состою в команде .NET, поэтому не могу говорить авторитетно, но команда, которая управляет библиотекой коллекций .NET, очень не склонна к риску, не только к критическим изменениям, но и избегает добавления новых типов из-за увеличилось навсегда бремя обслуживания, которое будет введено.