Elixir递归处理刮刮卡时出现KeyError:nil中无:owned_numbers键
问题场景
定义了如下Card结构体:
defmodule Card do defstruct id: nil, result_numbers: [], owned_numbers: [] end
通过CardEvaluator.evaluate_card/1计算卡片中owned_numbers与result_numbers的匹配数量,接着根据结果构建范围(card.id + 1)..(card.id + value),递归调用process_card_reward/2处理该范围内的卡片,最终统计总卡片数。
实现代码如下:
def process_scratchcards(card_strings) do cards = card_strings |> Enum.map(&CardParser.parse_card/1) |> IO.inspect() total_reward = cards |> Enum.map(fn card -> process_card_reward(card, cards) end) |> Enum.sum() IO.puts(total_reward) total_reward end def process_card_reward(card, all_cards) do value = card |> CardEvaluator.evaluate_card() victory_range = (card.id + 1)..(card.id + value) IO.inspect(victory_range) num_children = Enum.reduce(victory_range, 0, fn n, acc -> acc + process_card_reward(Enum.at(all_cards,n), all_cards) end) IO.puts(num_children) num_children + 1 end
运行时出现以下异常:
test Processes test data correctly (ScratchcardsTest)
test/scratchcards_test.exs:5
** (KeyError) key :owned_numbers not found in: nilIf you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
code: |> Scratchcards.process_scratchcards()
stacktrace:
(scratchcards 0.1.0) lib/card_evaluator.ex:3: CardEvaluator.evaluate_card/1
(scratchcards 0.1.0) lib/scratchcards.ex:19: Scratchcards.process_card_reward/2
(scratchcards 0.1.0) lib/scratchcards.ex:26: anonymous fn/3 in Scratchcards.process_card_reward/2
(elixir 1.15.7) lib/enum.ex:4379: Enum.reduce/3
(scratchcards 0.1.0) lib/scratchcards.ex:25: Scratchcards.process_card_reward/2
...
错误原因
异常核心是Enum.at(all_cards, n)返回了nil,传入CardEvaluator.evaluate_card/1后,函数尝试访问nil的:owned_numbers字段导致KeyError。
触发原因有两点:
- 索引不匹配:卡片
id通常从1开始,但Elixir列表是0-based索引,直接用n(卡片id)作为索引取元素,会导致索引越界或取到错误元素。 - 范围超出卡片总数:当
card.id + value超过现有卡片的最大id时,Enum.at会返回nil。
修复方案
方案1:使用Map按id查找(推荐)
将卡片列表转换为以id为键的Map,直接通过id安全查找卡片,同时过滤不存在的id:
修改后的代码:
def process_scratchcards(card_strings) do cards = card_strings |> Enum.map(&CardParser.parse_card/1) |> IO.inspect() # 将卡片列表转为id为键的Map cards_map = Map.new(cards, &{&1.id, &1}) total_reward = cards |> Enum.map(fn card -> process_card_reward(card, cards_map) end) |> Enum.sum() IO.puts(total_reward) total_reward end def process_card_reward(card, cards_map) do value = CardEvaluator.evaluate_card(card) victory_range = (card.id + 1)..(card.id + value) IO.inspect(victory_range) num_children = victory_range # 过滤出存在的卡片id,避免传入nil |> Enum.filter(&Map.has_key?(cards_map, &1)) |> Enum.reduce(0, fn id, acc -> acc + process_card_reward(cards_map[id], cards_map) end) IO.puts(num_children) num_children + 1 end
方案2:修正列表索引(仅适用于id连续从1开始的场景)
若确定卡片id连续且从1开始,可将索引转换为n-1,同时添加越界检查:
def process_card_reward(card, all_cards) do value = CardEvaluator.evaluate_card(card) victory_range = (card.id + 1)..(card.id + value) IO.inspect(victory_range) num_children = Enum.reduce(victory_range, 0, fn n, acc -> # 转换为0-based索引,且检查索引是否在列表范围内 index = n - 1 if index >= 0 and index < length(all_cards) do acc + process_card_reward(Enum.at(all_cards, index), all_cards) else acc end end) IO.puts(num_children) num_children + 1 end
方案1优势在于查找效率更高(O(1)),且无需依赖id的连续性,更健壮。
内容的提问来源于stack exchange,提问作者ScottishTapWater

