Добавить в корзинуПозвонить
Найти в Дзене
"Мы"-Прогер

Изучаем C# - Деревья и рекурсия (часть 3)

Часть 1: Часть 2: Пусть мы хотим найти все организации в дереве, которые удовлетворяют определённому условию. Подумаем, каким будет метод поиска. Во-первых, он будет принимать на вход корневую организацию и условие поиска. Условие может быть любым куском кода, то есть, это будет функция. Функция условия должна принимать на вход организацию, которую мы хотим проверить, и выдавать в ответ логическое значение true/false. Таким образом, функция условия будет иметь тип данных Func<Organization, bool>. Для функций, возвращающих тип данных bool, есть отдельный тип данных Predicate<Organization>. Наконец, результатом поиска может быть несколько организаций, то есть, будет список из них List<Organization>: Начало метода очевидно: нам нужно завести список, чтобы мы могли выдавать его в ответ. В процессе работы метод будет добавлять в этот список подходящие организации. Далее нужно проверить текущую (корневую) организацию, не подходит ли она под условие, и если подходит, то добавить её в список:
Оглавление

Предыдущие статьи

Часть 1:

Изучаем C# - Деревья и рекурсия (часть 1)
"Мы"-Прогер30 марта

Часть 2:

Изучаем C# - Деревья и рекурсия (часть 2)
"Мы"-Прогер2 апреля

Поиск в дереве

Пусть мы хотим найти все организации в дереве, которые удовлетворяют определённому условию. Подумаем, каким будет метод поиска. Во-первых, он будет принимать на вход корневую организацию и условие поиска. Условие может быть любым куском кода, то есть, это будет функция. Функция условия должна принимать на вход организацию, которую мы хотим проверить, и выдавать в ответ логическое значение true/false. Таким образом, функция условия будет иметь тип данных Func<Organization, bool>. Для функций, возвращающих тип данных bool, есть отдельный тип данных Predicate<Organization>. Наконец, результатом поиска может быть несколько организаций, то есть, будет список из них List<Organization>:

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

-2

Наконец, чтобы у нас была рекурсия, нужно вызвать этот же метод для дочерних организаций. Все найденные там организации нужно добавить к результатам поиска:

-3

Код для тестирования:

-4

NullReferenceException

И... хорошо, что мы тестируем программу, потому что она стала падать:

NullReferenceException: Object reference not set to an instance of an object
NullReferenceException: Object reference not set to an instance of an object

"Ссылка на объект не указывает на экземпляр объекта". Это происходит, когда переменная, которая должна хранить объект, на самом деле хранит null. Найти значение вложенного поля/вызвать метод у объекта нельзя, так как объекта нет - вместо него null. Команда с ошибкой выделена жёлтым. Где в ней ссылка на объект, которая хранит null? Может быть, это organization? У неё мы пытаемся взять поле Childes. Но если навести мышку на organization, мы увидим, что там не null:

-6

Значит, это не причина ошибки. Далее, наведя мышку на Childes, мы понимаем, что там null. Цикл по списку выдаёт такую ошибку, потому что вместо списка ему подали null:

-7

Как исправить эту ошибку? Нужно сделать так, чтобы в случае, если там null, цикл не начинался. Для этого достаточно поместить цикл в условие if:

-8

Важно, что во время написания кода нам подсвечивают это условие, как невыполнимое:

-9

"Выражение всегда истинно в соответствии с аннотациями о возможности хранить null". Хотя в коде мы не поставили вопросик у этого поля, то есть, не разрешили хранить там null:

-10

но во время выполнения там всё равно может оказаться null. Дело в том, что проверка на null в C# не строгая - это всего лишь жёлтые предупреждения, а не красные ошибки. Программа с предупреждениями всё равно соберётся и будет работать.

Запустим программу снова. Теперь всё будет работать:

-11

Поиск в дереве с результатом в виде дерева

Усложним задачу. Теперь надо будет выдавать результаты поиска не в виде списка, а в виде дерева.

Дерево задаётся своим корнем и дочерними элементами, хранящимися внутри него. Поэтому возвращаемый тип данных метода будет Organization?. Вопросик нужен на случай, если корень не подходит под условие фильтрации - тогда надо будет выдавать в ответ null.

Подумаем хорошо над задачей. По сути, надо оставить дерево в том виде, как оно есть, и всего лишь удалить из него узлы, не прошедшие фильтрацию, если у них нет подходящих дочерних.

Как проверить, подходит ли узел дерева под условие, мы знаем:

-12

Однако пока у нас нет никакой рекурсии. Нужно проверять дочерние узлы. По сути, нужно отфильтровать список дочерних узлов, оставив в нём только "хорошие" узлы (то есть либо сами проходящие фильтрацию, либо имеющие дочерние, проходящие фильтрацию):

-13

Строки 72-75 занимает одна команда. Разберём её подробно:

  • Возьмём список дочерних узлов (строка 72).
  • Сначала с помощью Select() (строка 73) преобразуем каждый узел в него самого, если он "хороший", или в null, если нет.
  • Затем удалим null из списка с помощью Where() (строка 74).
  • Не будем забывать, что методы Linq дают в ответ перечисление IEnumerable<...>, а не список List<...>, поэтому нужно привести перечисление к списку с помощью ToList() (строка 75).
  • Результат положим в organization.Childes (строка 72).

Однако здесь нас ждёт жёлтое предупреждение. Наученные горьким опытом, постараемся писать код без предупреждений. Наведём мышку на него:

Nullability of reference types in value of type 'System.Collections.Generic.List<HelloWorld.Trees.Organization?>' does not match target type 'System.Collections.Generic.List<HelloWorld.Trees.Organization>'
Nullability of reference types in value of type 'System.Collections.Generic.List<HelloWorld.Trees.Organization?>' does not match target type 'System.Collections.Generic.List<HelloWorld.Trees.Organization>'

"Возможность хранить null в типе List<Organization?> не соответствует целевому типу List<Organization>". Дело в том, что после Select() тип данных элементов стал "Organization?", потому что вместо элемента может оказаться null. Так этот тип у элементов и остался. Синтаксис не настолько хорош, чтобы понять, что все null мы отфильтровали - он думает, что null могут остаться. К счастью, для этого есть другой метод Linq - OfType<T>() оставляет в перечислении только те элементы, которые имеют тип T. Заменим Where() на OfType():

-15

Оставляем организацию в результатах поиска, если она сама подходит или же подходит хотя бы одна из дочерних организаций:

-16

Начинаем тестировать:

-17

ArgumentNullException

Ошибка:

ArgumentNullException: Value cannot be null. (Parameter 'source')
ArgumentNullException: Value cannot be null. (Parameter 'source')

"Значение не может быть null (Параметр 'source')". То есть, некий метод принимает на вход параметр под названием source, и ему дают вместо него null. Только вот где этот метод? В строках 72-75 одна команда, в ней вызывается много разных методов: Select(), TreeSearch(), OfType(), ToList(). К счастью, более точное место ошибки можно узнать с помощью стека вызовов, который напечатан тут же: мы видим, что ошибка произошла внутри метода ThrowArgumentNullException(argument), который был вызван изнутри Select(source, selector), который был вызван изнутри TreeSearch() и так далее... Внимательно вглядываясь в это, мы понимаем, что параметр под названием source есть у Select(). Вообще, у него два входных параметра: source и selector. Но как, если мы передаём ему только один (лямбду; она выделена красной рамкой)?

-19

Дело в том, что Select() и многие другие методы из Linq - это методы расширения. Кликнем на Select() с нажатым Ctrl:

-20

Откроются исходники этого метода:

-21

Здесь видно, что он принимает 2 входных параметра: source (подчёркнут красным) и selector (подчёркнут жёлтым), причём source - это объект, к которому применяется метод расширения.

Тут же мы видим и кидание ошибки в случае, если source is null (точнее, код не кидает ошибку сам, а вызывает метод ThrowArgumentNullException(), который мы видели верхним в стеке ошибки).

А к чему у нас применяется метод Select()? К organization.Childes. Наведём на него мышку и... обнаружим, что он - null:

-22

То есть, это проявление предыдущей ошибки, когда мы забыли проверить на null organization.Childes в другом месте программы. Добавим проверку, которая будет вызывать Select() и прочее, только если список не null, а иначе - выдавать пустой список:

-23

Нажмём мышкой с Ctrl на Childes, чтобы перейти туда, и наконец отметим, что он может быть null:

-24

Запустим теперь наш код. Наведём мышку на treeSearchResult:

-25

В дереве остался только "Научный отдел", который мы искали, и его родительские организации.

Далее

Оглавление

Изучаем C# с нуля - Очень краткий курс - Оглавление
"Мы"-Прогер27 января