Предыдущие статьи
Часть 1:
Часть 2:
Поиск в дереве
Пусть мы хотим найти все организации в дереве, которые удовлетворяют определённому условию. Подумаем, каким будет метод поиска. Во-первых, он будет принимать на вход корневую организацию и условие поиска. Условие может быть любым куском кода, то есть, это будет функция. Функция условия должна принимать на вход организацию, которую мы хотим проверить, и выдавать в ответ логическое значение true/false. Таким образом, функция условия будет иметь тип данных Func<Organization, bool>. Для функций, возвращающих тип данных bool, есть отдельный тип данных Predicate<Organization>. Наконец, результатом поиска может быть несколько организаций, то есть, будет список из них List<Organization>:
Начало метода очевидно: нам нужно завести список, чтобы мы могли выдавать его в ответ. В процессе работы метод будет добавлять в этот список подходящие организации. Далее нужно проверить текущую (корневую) организацию, не подходит ли она под условие, и если подходит, то добавить её в список:
Наконец, чтобы у нас была рекурсия, нужно вызвать этот же метод для дочерних организаций. Все найденные там организации нужно добавить к результатам поиска:
Код для тестирования:
NullReferenceException
И... хорошо, что мы тестируем программу, потому что она стала падать:
"Ссылка на объект не указывает на экземпляр объекта". Это происходит, когда переменная, которая должна хранить объект, на самом деле хранит null. Найти значение вложенного поля/вызвать метод у объекта нельзя, так как объекта нет - вместо него null. Команда с ошибкой выделена жёлтым. Где в ней ссылка на объект, которая хранит null? Может быть, это organization? У неё мы пытаемся взять поле Childes. Но если навести мышку на organization, мы увидим, что там не null:
Значит, это не причина ошибки. Далее, наведя мышку на Childes, мы понимаем, что там null. Цикл по списку выдаёт такую ошибку, потому что вместо списка ему подали null:
Как исправить эту ошибку? Нужно сделать так, чтобы в случае, если там null, цикл не начинался. Для этого достаточно поместить цикл в условие if:
Важно, что во время написания кода нам подсвечивают это условие, как невыполнимое:
"Выражение всегда истинно в соответствии с аннотациями о возможности хранить null". Хотя в коде мы не поставили вопросик у этого поля, то есть, не разрешили хранить там null:
но во время выполнения там всё равно может оказаться null. Дело в том, что проверка на null в C# не строгая - это всего лишь жёлтые предупреждения, а не красные ошибки. Программа с предупреждениями всё равно соберётся и будет работать.
Запустим программу снова. Теперь всё будет работать:
Поиск в дереве с результатом в виде дерева
Усложним задачу. Теперь надо будет выдавать результаты поиска не в виде списка, а в виде дерева.
Дерево задаётся своим корнем и дочерними элементами, хранящимися внутри него. Поэтому возвращаемый тип данных метода будет Organization?. Вопросик нужен на случай, если корень не подходит под условие фильтрации - тогда надо будет выдавать в ответ null.
Подумаем хорошо над задачей. По сути, надо оставить дерево в том виде, как оно есть, и всего лишь удалить из него узлы, не прошедшие фильтрацию, если у них нет подходящих дочерних.
Как проверить, подходит ли узел дерева под условие, мы знаем:
Однако пока у нас нет никакой рекурсии. Нужно проверять дочерние узлы. По сути, нужно отфильтровать список дочерних узлов, оставив в нём только "хорошие" узлы (то есть либо сами проходящие фильтрацию, либо имеющие дочерние, проходящие фильтрацию):
Строки 72-75 занимает одна команда. Разберём её подробно:
- Возьмём список дочерних узлов (строка 72).
- Сначала с помощью Select() (строка 73) преобразуем каждый узел в него самого, если он "хороший", или в null, если нет.
- Затем удалим null из списка с помощью Where() (строка 74).
- Не будем забывать, что методы Linq дают в ответ перечисление IEnumerable<...>, а не список List<...>, поэтому нужно привести перечисление к списку с помощью ToList() (строка 75).
- Результат положим в organization.Childes (строка 72).
Однако здесь нас ждёт жёлтое предупреждение. Наученные горьким опытом, постараемся писать код без предупреждений. Наведём мышку на него:
"Возможность хранить null в типе List<Organization?> не соответствует целевому типу List<Organization>". Дело в том, что после Select() тип данных элементов стал "Organization?", потому что вместо элемента может оказаться null. Так этот тип у элементов и остался. Синтаксис не настолько хорош, чтобы понять, что все null мы отфильтровали - он думает, что null могут остаться. К счастью, для этого есть другой метод Linq - OfType<T>() оставляет в перечислении только те элементы, которые имеют тип T. Заменим Where() на OfType():
Оставляем организацию в результатах поиска, если она сама подходит или же подходит хотя бы одна из дочерних организаций:
Начинаем тестировать:
ArgumentNullException
Ошибка:
"Значение не может быть null (Параметр 'source')". То есть, некий метод принимает на вход параметр под названием source, и ему дают вместо него null. Только вот где этот метод? В строках 72-75 одна команда, в ней вызывается много разных методов: Select(), TreeSearch(), OfType(), ToList(). К счастью, более точное место ошибки можно узнать с помощью стека вызовов, который напечатан тут же: мы видим, что ошибка произошла внутри метода ThrowArgumentNullException(argument), который был вызван изнутри Select(source, selector), который был вызван изнутри TreeSearch() и так далее... Внимательно вглядываясь в это, мы понимаем, что параметр под названием source есть у Select(). Вообще, у него два входных параметра: source и selector. Но как, если мы передаём ему только один (лямбду; она выделена красной рамкой)?
Дело в том, что Select() и многие другие методы из Linq - это методы расширения. Кликнем на Select() с нажатым Ctrl:
Откроются исходники этого метода:
Здесь видно, что он принимает 2 входных параметра: source (подчёркнут красным) и selector (подчёркнут жёлтым), причём source - это объект, к которому применяется метод расширения.
Тут же мы видим и кидание ошибки в случае, если source is null (точнее, код не кидает ошибку сам, а вызывает метод ThrowArgumentNullException(), который мы видели верхним в стеке ошибки).
А к чему у нас применяется метод Select()? К organization.Childes. Наведём на него мышку и... обнаружим, что он - null:
То есть, это проявление предыдущей ошибки, когда мы забыли проверить на null organization.Childes в другом месте программы. Добавим проверку, которая будет вызывать Select() и прочее, только если список не null, а иначе - выдавать пустой список:
Нажмём мышкой с Ctrl на Childes, чтобы перейти туда, и наконец отметим, что он может быть null:
Запустим теперь наш код. Наведём мышку на treeSearchResult:
В дереве остался только "Научный отдел", который мы искали, и его родительские организации.
Далее
Оглавление