You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Elixir递归处理刮刮卡时出现KeyError:nil中无:owned_numbers键

修复Elixir刮刮卡递归处理中的KeyError错误

问题场景

定义了如下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: nil

If 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 16:05:58