Элемент в IEnumerable не равен элементу в списке
Я просто не могу понять, почему элемент в моем отфильтрованном списке не найден. Я упростил пример, чтобы показать его. У меня есть класс...
public class Item
{
public Item(string name)
{
Name = name;
}
public string Name
{
get; set;
}
public override string ToString()
{
return Name;
}
}
... и класс "Элементы", которые должны фильтровать элементы и проверять, находится ли первый элемент в списке...
public class Items
{
private IEnumerable<Item> _items;
public Items(IEnumerable<Item> items)
{
_items = items;
}
public List<Item> Filter(string word)
{
var ret = new List<Item>(_items.Where(x => x.Name.Contains(word)));
Console.WriteLine("found: " + ret.Contains(_items.First()));
// found: false
return ret;
}
}
Исполняющий код выглядит следующим образом:
static void Main(string[] args)
{
string[] itemNames = new string[] { "a", "b", "c" };
Items list = new Items(itemNames.Select(x => new Item(x)));
list.Filter("a");
Console.ReadLine();
}
Теперь, если я запустил программу, Console.WriteLine выводит, что элемент не найден. Но почему?
Если я изменил первую строчку в конструкторе на
_items = items.ToList()
то он может ее найти. Если я отменим эту строку и вызову ToList() позже в методе Filter, она также не сможет найти элемент?!
public class Items
{
private IEnumerable<Item> _items;
public Items(IEnumerable<Item> items)
{
_items = items;
}
public List<Item> FilteredItems
{
get; set;
}
public List<Item> Filter(string word)
{
var ret = new List<Item>(_items.Where(x => x.Name.Contains(word)));
_items = _items.ToList();
Console.WriteLine("found: " + ret.Contains(_items.First()));
// found: false
return ret;
}
}
Почему существует разница, когда и когда выполняется выражение лямбда и почему элемент не найден? Я не понимаю!
Ответы
Ответ 1
Причина отложенное выполнение.
Вы проиндексируете поле _items
в
itemNames.Select(x => new Item(x));
Это запрос, а не ответ на этот запрос. Этот запрос выполняется каждый раз, когда вы перебираете _items
.
Итак, в этой строке вашего метода Filter
:
var ret = new List<Item>(_items.Where(x => x.Name.Contains(word)));
исходный массив перечисляется и создается new Item(x)
для каждой строки. Эти элементы хранятся в вашем списке ret
.
Когда вы вызываете Contains(_items.First())
после этого, First()
снова выполняет запрос в _items
, создавая экземпляры new Item
для каждой исходной строки.
Так как метод Item
Equals
, вероятно, не переопределяется и выполняет простую проверку равенства ссылок, первый Item
, возвращенный со второй итерации, является другим экземпляром Item
, чем тот, что указан в вашем списке.
Ответ 2
Удалите лишний код, чтобы увидеть проблему:
var itemNames = new [] { "a", "b", "c" };
var items1 = itemNames.Select(x => new Item(x));
var surprise = items1.Contains(items1.First()); // False
Коллекция items1
, похоже, не содержит его начального элемента! (демонстрация)
Добавление ToList()
устраняет проблему:
var items2 = itemNames.Select(x => new Item(x)).ToList();
var noSurprise = items2.Contains(items2.First()); // True
Причина, по которой вы видите разные результаты с и без ToList()
, заключается в том, что (1) items1
оценивается лениво, и (2) ваш класс Item
не реализует Equals
/GetHashCode
. Использование ToList()
обеспечивает работу по умолчанию по умолчанию; реализация пользовательской проверки равенства исправит проблему для множественного перечисления.
Основной урок этого упражнения заключается в том, что сохранение IEnumerable<T>
, передаваемого вашему конструктору, опасно. Это только одна из причин; другие причины включают множественное перечисление и возможную модификацию последовательности после того, как ваш код подтвердил свой ввод. Вы должны вызвать ToList
или ToArray
для последовательностей, переданных в конструкторы, чтобы избежать этих проблем:
public Items(IEnumerable<Item> items) {
_items = items.ToList();
}
Ответ 3
В коде есть две проблемы.
Первая проблема заключается в том, что вы каждый раз инициализируете новый элемент. То есть вы не храните фактические предметы здесь, когда пишете.
IEnumerable<Item> items = itemNames.Select(x => new Item(x));
Выполнение Select
отложено. каждый раз, когда вы вызываете .ToList()
, новый набор элементов создается с использованием itemNames
в качестве источника.
Вторая проблема заключается в том, что вы сравниваете элементы по ссылке здесь.
Console.WriteLine("found: " + ret.Contains(_items.First()));
Когда вы используете ToList
, вы храните элементы в списке, и ссылки остаются такими же, поэтому вы найдете элемент со ссылкой.
Если вы не используете ToList
, ссылки уже не такие. потому что каждый раз, когда создается новый элемент. вы не можете найти свой товар с другой ссылкой.