概念练习


概念练习是用于教授特定(编程)概念的练习。 概念练习所教授的概念构成一份_教学大纲_。 关于如何设计教学大纲的更多信息,请查阅教学大纲文档。

Note

你可以在轨道的根目录下运行以下命令,快速创建一个新的概念练习:

bin/fetch-configlet
bin/configlet create --concept-exercise <slug>

更多信息请查阅 configlet create文档

元数据

概念练习的元数据定义在 config.json 文件的exercises.concept键中。元数据定义了练习的 UUID、slug 等。

示例

{
  "exercises": {
    "concept": [
      {
        "uuid": "93fbc7cf-3a7e-4450-ad22-e30129c36bb9",
        "slug": "cars-assemble",
        "name": "Cars, Assemble!",
        "concepts": ["if-statements", "numbers"],
        "prerequisites": ["basics"]
      }
    ]
  }
}

文件

每个概念练习都在轨道的exercises/concept目录下拥有自己的目录。概念练习目录的名称必须与概念练习的slug属性一致,该属性定义在 config.json 文件中。

概念练习有四类文件:

文档文件

这些文件会展示给学生,用于帮助讲解练习。

  • .docs/introduction.md:向学生介绍该练习所教授的概念(必需)
  • .docs/instructions.md:提供练习的说明(必需)
  • .docs/hints.md:为学生提供提示,帮助他们摆脱练习中的困境(必需)

元数据文件

这些文件_不会_展示给学生,而是用于定义练习的元数据。

  • .meta/config.json:包含练习的元信息(必需)
  • .meta/design.md:描述练习的设计(必需)

解题思路文件

这些文件描述练习的解题思路。

  • .approaches/introduction.md:介绍练习最常见的解题思路(可选)
  • .approaches/config.json:解题思路的元数据(可选)
  • .approaches/<approach-slug>/content.md:解题思路的描述(可选)
  • .approaches/<approach-slug>/snippet.txt:展示解题思路的代码片段(可选)

文章文件

这些文件描述练习的相关文章。

  • .articles/config.json:文章的元数据(可选)
  • .articles/<article-slug>/content.md:文章的描述(可选)
  • .articles/<article-slug>/snippet.md:展示文章的代码片段(可选)

练习文件

与具体语言相关的文件,例如实现文件和测试文件。这些文件的名称因轨道而异。

  • 测试套件:验证提交的解答是否正确(必需)
  • 存根实现:为学生提供起始代码(必需)
  • 范例实现:提供一份能通过所有测试的惯用实现(必需)
  • 附加文件:确保测试能够运行(可选)

示例

exercises
└── concept
    └── cars-assemble
        ├── .approaches
        |   ├── for-loop
        |   |   ├── content.md
        |   |   └── snippet.txt
        |   ├── config.json
        |   └── introduction.md
        ├── .articles
        |   ├── performance
        |   |   ├── content.md
        |   |   └── snippet.md
        |   └── config.json
        ├── .docs
        |   ├── introduction.md
        |   ├── instructions.md
        |   └── hints.md
        ├── .meta
        |   ├── config.json        
        |   ├── design.md
        |   └── Exemplar.cs (exemplar implementation)
        ├── CarsAssemble.cs (stub implementation)
        └── CarsAssemblyTests.cs (tests)

最小有效规格

对于新练习,我们倾向于采用“乐观合并”的方式,轨道可以在“进行中”的状态下开发练习。 最小的有效状态(能够通过 configlet 并在之后允许你合并)是:

  • 在轨道的config.json中有一条有效条目,且status设置为wip。
  • 一个有效的.meta/config.json文件
  • 以下文件存在,尽管它们可以为空:
    • .docs/introduction.md
    • .docs/instructions.md
    • .docs/hints.md
    • 存根实现
    • 测试文件

文件:.docs/introduction.md

用途: 向学生介绍该练习所教授的概念。

是否必需: 必需

  • 所提供的信息应当刚好给学生足够的背景,让他们能自己找出解答。
  • 只提供理解该概念基础并解决练习所需的信息。额外的信息应留到该概念的about.md文档中。
  • 链接应尽量少用,能不用就不用。虽然解释递归这类复杂主题的链接可能有用,但对大多数概念来说,链接提供的信息会超出所需,所以目标应当是在正文中简洁地解释清楚。
  • 应使用恰当的技术术语,以便学生能轻松搜索到更多信息。
  • 代码示例只应用于介绍新语法(学生不必上网搜索语法示例)。其他情况下,应提供描述或链接,而不是代码。

举个例子,“strings”练习的引言可能会把字符串简单地描述为“一串 Unicode 字符”或“一串字节”,告诉用户如何创建字符串,并说明字符串拥有可用于操作它的方法。除非学生需要理解更细致的细节才能解决练习,否则这种简短的解释(连同其语法示例)就足以让学生解决练习。

示例

# Introduction

There are two primary ways to assign objects to names in Ruby - using variables or constants. Variables are always written in snake case. A variable can reference different objects over its lifetime. For example, `my_first_variable` can be defined and redefined many times using the `=` operator:

```ruby
my_first_variable = 1
my_first_variable = "Some string"
my_first_variable = SomeComplexObject.new
```

文件:.docs/introduction.md.tpl

用途: 用于生成introduction.md文件的模板。

是否必需: 可选

introduction.md文档向学生介绍练习的概念。每个概念也有_自己的_introduction.md文档,该文档不会在练习情境之外展示。

如果概念的引言需要原样包含在练习的引言中,可以使用introduction.md.tpl文件。该文件允许通过占位符引用概念引言:%{concept:<concept-slug>}。

configlet可以从模板文件生成introduction.md文件。生成的文件中,概念占位符会被替换为该概念的introduction内容。

Exercism 网站只知道introduction.md文档。使用模板文件时,生成introduction.md是轨道的职责。

轨道可以针对每个练习决定是否使用模板。某些情况下,原样使用概念的引言可能并非最佳选择。始终选择能为学生提供最佳学习体验的做法。

示例

# Introduction

%{concept:variables}

文件:.docs/instructions.md

用途: 提供练习的说明。

是否必需: 必需

该文件分为两部分。

  1. 第一部分解释练习的“故事”或“主题”。它通常不应包含代码示例。
  2. 第二部分以一项或多项任务的形式,清楚地说明学生需要完成什么。

每项任务都必须符合以下标准:

  • 以二级标题开头,标题以数字开头(例如## 1. Do X、## 2. Do Y)。
  • 标题应当描述要_实现什么_,而不是_如何实现_(例如## 1. Check if an appointment has already passed)。
  • 说明学生需要定义/实现哪个函数或方法(例如Implement method X(...) that takes an A and returns a Z),
  • 用代码提供该函数的使用示例。这些示例应与测试中给出的示例不同。

我们非常重视让 Exercism 的内容对每个人都安全,因此在判断故事是否合适时,往往宁可谨慎一些。虽然我们在合并时会小心把关,但也理解很难意识到哪些内容可能被视为有问题,所以我们会始终假定你是出于善意,并尽力在评审中以不对抗的方式发现问题。如果你想和我们一起检查某个故事,请提及 @exercism/leadership,我们会一起看看。以下是一些指导要点:

  • 尽量确保故事友好、人人都能理解。如果故事包含圈内梗或地区俚语,试着想一些替代说法。
  • 尽量编写对每个人都有包容性的示例。例如,考虑使用来自其他文化的名字和混合性别。
  • 问问自己,是否认识某个会因为这个故事而感到被冒犯的人。如果是这样,考虑修改它以避免冒犯。

示例

# Instructions

In this exercise you're going to write some code to help you cook a brilliant lasagna from your favorite cooking book.

## 1. Calculate the remaining oven time in minutes

Define the `Lasagna#remaining_minutes_in_oven` method that takes the actual minutes the lasagna has been in the oven as a parameter and returns how many minutes the lasagna still has to remain in the oven, based on the expected oven time in minutes from the previous task.

```ruby
lasagna = Lasagna.new
lasagna.remaining_minutes_in_oven(30)
# => 10
```

文件:.docs/hints.md

用途: 为学生提供提示,帮助他们摆脱练习中的困境。

是否必需: 必需

  • 如果学生卡住了,我们会允许他们点击一个请求提示的按钮,它会显示文件中相关的那部分。
  • 提示应以项目符号的形式列在标题下方。
  • 提示应当足以让几乎任何学生都能继续下去。
  • 提示不应直接给出解答,而应指向描述解答的资源(例如链接到所用函数的文档)。
  • 提示可以用代码示例来解释概念,但不能用来说明解答的梗概。例如,在一个数组练习中,它们可以展示某个数组函数如何工作的片段,但不能以可以直接复制粘贴进解答的方式呈现。
  • 关于练习的通用提示可以作为 Markdown 列表出现在## General标题下。
  • 针对具体任务的提示应作为 Markdown 列表出现在与其在instructions.md中任务标题相匹配的标题下方(例如## 2. Do Y)。
  • 如果没有通用提示,或某项任务没有提示,则应省略相应标题。每个标题后面都必须跟一个 Markdown 列表。
  • 优先使用针对具体任务的提示,而非通用提示,因为针对具体任务的提示比通用提示更有可能帮助学生继续下去。
  • 任务标题应描述任务的_是什么_,而不是_怎么做_。
  • 任务标题应使用常规的句子大小写(例如## 2. Check if a book can be borrowed)。
  • 任务应明确说明要实现哪个方法/函数/类型及其期望值(例如Implement the 'canBorrowBook' function to check if a book can be borrowed. The function takes a book as its parameter and returns `true` if the book has not already been borrowed; otherwise, return `false`)。

查看提示不会是“推荐”路径,除非学生没有它就无法继续,否则我们会(温和地)劝阻使用它。因此,值得考虑到,阅读提示的学生会有点困惑、不知所措,或许还会感到沮丧。

示例

# Hints

## General

- You need to define a [constant][constant] which should contain the [integer][integers] value specified in the recipe.

## 1. Calculate the remaining oven time in minutes

- You need to define a [method][methods] with a single parameter for the actual time so far.

[constants]: https://www.rubyguides.com/2017/07/ruby-constants/
[integers]: https://ruby-doc.org/core-2.7.0/Integer.html
[methods]: https://launchschool.com/books/ruby/read/methods

文件:.meta/design.md

用途: 描述练习的设计。

是否必需: 必需

该文件包含练习设计的相关信息,包括其目标、教学目的、不应教授的内容等。这些信息可以从练习对应的 GitHub issue 中提取。

它存在的目的是让未来的维护者或贡献者了解某个练习的范围和限制,避免练习随时间推移而变得越来越复杂的自然趋势。

示例

# Design

## Goal

The goal of this exercise is to teach the student the basics of programming in Ruby.

## Learning objectives

- Know what a variable is.
- Know how to define a variable.
- Know how to update a variable.

## Out of scope

- Memory and performance characteristics.
- Method overloads.

## Concepts

The Concepts this exercise unlocks are:

- `basics`: know what a variable is; know how to define a variable; know how to update a variable.

## Prerequisites

There are no prerequisites.

文件:.meta/config.json

用途: 包含练习的元信息。

是否必需: 必需

该文件包含练习的元信息:

  • authors:练习作者的 GitHub 用户名(必需)
    • 如果评审者的评审对练习有实质性改动(到了让人觉得“你们是一起完成的”的程度),也一并列入
  • contributors:练习贡献者的 GitHub 用户名(可选)
    • 如果评审者的评审有意义、可操作或已被采纳,也一并列入。
  • forked_from:该练习 fork 自哪些练习(如果练习是 fork 来的则必需)
  • files:该练习所用文件的位置,相对于练习的目录(必需)
    • solution:存根实现文件(必需)
    • test:测试文件(必需)
    • exemplar:范例实现文件(必需)
    • editor:在编辑器中以只读方式显示的其他文件(可选)
    • invalidator:一旦更改,就会导致解答过期的文件(可选)
  • language_versions:语言版本要求(可选)
  • blurb:该练习的简短描述。长度必须 <= 350。_不_支持 Markdown(必需)
  • source:该练习所基于的来源(可选)
  • source_url:该练习所基于来源的 URL(可选)
  • representer:与 representer 如何处理该文件相关的元信息(可选)
    • version:该练习要使用的 representer 版本的整数(如果父键存在则必需)
  • icon:图标的 slug(参见完整图标列表)。如果未指定,将使用练习的 slug(可选)
  • custom:任何练习特定的、非标准数据。可用于按练习定制轨道工具的行为(可选)

如果某人既是作者_又_是贡献者,只将其列为作者。

最小示例

{
  "authors": ["FSharpForever"],
  "files": {
    "solution": ["Lasagna.fs"],
    "test": ["LasagnaTests.fs"],
    "exemplar": [".meta/Exemplar.fs"]
  },
  "blurb": "Learn the basics of F# by cooking Lucian's Luscious Lasagna"
}

完整示例

假设用户FSharpForever为 F# 轨道编写了一个名为log-levels的练习。PythonProfessor将该练习改编到 Python 轨道。后来,用户GladToHelp改进了该练习。

{
  "authors": ["PythonProfessor"],
  "contributors": ["GladToHelp"],
  "files": {
    "solution": ["log_levels.py"],
    "test": ["log_levels_test.py"],
    "exemplar": [".meta/exemplar.py"],
    "editor": ["test_helper.py"]
  },
  "forked_from": ["fsharp/log-levels"],
  "language_versions": ">=3.7",
  "blurb": "Learn how to work with strings by processing log lines.",
  "source": "Wikipedia",
  "source_url": "https://en.wikipedia.org/wiki/Log_file",
  "representer": {
    "version": 2
  },
  "icon": "logs",
  "custom": {
    "parallel": true
  }
}

请注意:

  • 作者和贡献者的顺序并不重要,也没有含义。
  • 如果你在 fork 一个练习,不要引用原作者或原贡献者。只需确保forked_from正确即可。
  • 虽然不常见,但_确实_可以从多个练习 fork。
  • language_versions是一个自由形式的字符串,轨道可以随意使用和解释。

文件:.approaches/introduction.md

用途: 介绍练习最常见的解题思路

是否必需: 可选

该文件描述练习最常见的解题思路。 关于该文件中应包含哪些内容,请查阅文档。

示例

# Introduction

The key to this exercise is to deal with C# strings being immutable, which means that a `string`'s value cannot be changed.
Therefore, to reverse a string you'll need to create a _new_ `string`.

## Using LINQ

```csharp
public static string Reverse(string input)
{
    return new string(input.Reverse().ToArray());
}
```

For more information, check the [LINQ approach][approach-linq].

## Which approach to use?

If readability is your primary concern (and it usually should be), the LINQ-based approach is hard to beat.

文件:.approaches/config.json

用途: 解题思路的元数据

是否必需: 可选(当存在解题思路引言或解题思路时必需)

该文件包含练习解题思路的元信息:

  • introduction:练习解题思路引言作者的 GitHub 用户名(可选)

    • authors:练习解题思路引言作者的 GitHub 用户名(必需)
      • 如果评审者的评审对解题思路引言有实质性改动(到了让人觉得“你们是一起完成的”的程度),也一并列入
    • contributors:练习解题思路引言贡献者的 GitHub 用户名(可选)
      • 如果评审者的评审有意义、可操作或已被采纳,也一并列入。
  • approaches:列出各详细解题思路的数组(可选)

    • uuid:唯一标识该解题思路的 V4 UUID。该 UUID 在轨道内以及所有轨道之间都必须唯一,且绝不能更改
    • slug:解题思路的 slug,是小写的 kebab-case 字符串。该 slug 在轨道内所有解题思路 slug 中必须唯一。其长度必须 <= 255。
    • title:解题思路的标题。其长度必须 <= 255。
    • blurb:该解题思路的简短描述。长度必须 <= 350。_不_支持 Markdown(必需)
    • authors:练习解题思路作者的 GitHub 用户名(必需)
      • 如果评审者的评审对练习解题思路有实质性改动(到了让人觉得“你们是一起完成的”的程度),也一并列入
    • contributors:练习解题思路贡献者的 GitHub 用户名(可选)
      • 如果评审者的评审有意义、可操作或已被采纳,也一并列入。
    • tags:指定提交在什么条件下会关联到某个解题思路。(可选)
      • all:提交上必须全部存在的标签数组(可选,除非any没有元素)
      • any:提交上必须至少存在其中一个的标签数组(可选,除非all没有元素)
      • not:提交上必须一个都不存在的标签(可选)

示例

{
  "introduction": {
    "authors": ["erikschierboom"]
  },
  "approaches": [
    {
      "uuid": "448fb2b4-18ab-4e55-aa54-ad4ed6d5f7f6",
      "slug": "span",
      "title": "Use Span<T>",
      "blurb": "Use Span<T> to efficiently reverse a string.",
      "authors": ["erikschierboom"]
    }
  ]
}

文件:.approaches/<approach-slug>/content.md

用途: 解题思路的详细描述

是否必需: 可选(对于解题思路必需)

该文件包含解题思路的详细描述。 关于该文件中应包含哪些内容,请查阅文档。

示例

# Span

```csharp
Span<char> chars = stackalloc char[input.Length];
for (var i = 0; i < input.Length; i++)
{
    chars[input.Length - 1 - i] = input[i];
}
return new string(chars);
```

This `Span<T>` approach uses a `for` loop.

文件:.approaches/<approach-slug>/snippet.txt

用途: 展示解题思路的代码片段

是否必需: 可选(对于解题思路必需)

该文件包含一个展示该解题思路的小片段。 该片段会显示在练习的 dig deeper 页面上。

其行数必须 <= 8。

关于该文件中应包含哪些内容,请查阅文档。

示例

Span<char> chars = stackalloc char[input.Length];
for (var i = 0; i < input.Length; i++)
{
    chars[input.Length - 1 - i] = input[i];
}
return new string(chars);

文件:.article/config.json

用途: 文章的元数据

是否必需: 可选(当存在文章时必需)

该文件包含练习文章的元信息:

  • articles:列出各详细文章的数组(可选)
    • uuid:唯一标识该文章的 V4 UUID。该 UUID 在轨道内以及所有轨道之间都必须唯一,且绝不能更改
    • slug:文章的 slug,是小写的 kebab-case 字符串。该 slug 在轨道内所有文章 slug 中必须唯一。其长度必须 <= 255。
    • title:文章的标题。其长度必须 <= 255。
    • blurb:该文章的简短描述。长度必须 <= 350。_不_支持 Markdown(必需)
    • authors:练习文章作者的 GitHub 用户名(必需)
      • 如果评审者的评审对练习文章有实质性改动(到了让人觉得“你们是一起完成的”的程度),也一并列入
    • contributors:练习文章贡献者的 GitHub 用户名(可选)
      • 如果评审者的评审有意义、可操作或已被采纳,也一并列入。

示例

{
  "articles": [
    {
      "uuid": "6db71962-62d5-448b-a980-c20ae41013ed",
      "slug": "performance",
      "title": "Optimizing performance",
      "blurb": "Explore how to most efficiently reverse a string and what the trade-offs are.",
      "authors": ["erikschierboom"]
    }
  ]
}

文件:.articles/<article-slug>/content.md

用途: 解题思路的详细描述

是否必需: 可选(对于解题思路必需)

该文件包含解题思路的详细描述。 关于该文件中应包含哪些内容,请查阅文档。

示例

# Performance

In this document, we'll find out which approach is the most performant one.

## Benchmark results

| Method |      Mean |     Error |    StdDev |    Median | Allocated |
| -----: | --------: | --------: | --------: | --------: | --------: |
|   Linq | 29.133 ns | 0.5865 ns | 0.5486 ns | 28.984 ns |      80 B |
|  Array |  4.806 ns | 0.4999 ns | 1.4739 ns |  3.967 ns |         - |

文件:.articles/<article-slug>/snippet.txt

用途: 展示解题思路的代码片段

是否必需: 可选(对于文章必需)

该文件包含一个展示该文章的小片段。 该片段会显示在练习的 dig deeper 页面上。

其行数必须 <= 8。

关于该文件中应包含哪些内容,请查阅文档。

示例

| Method |      Mean | Allocated |
| -----: | --------: | --------: |
|   Linq | 29.133 ns |      80 B |
|  Array |  4.806 ns |         - |

文件:存根实现

用途: 为学生提供起始代码。

是否必需: 必需

  • 设计存根时,要让学生知道在哪里添加代码。
  • 为练习中未介绍的任何语法定义存根。对大多数练习来说,这意味着定义存根函数/方法。
  • 对于编译型语言,考虑让代码可以编译,因为编译器消息有时会让刚接触该语言的学生难以理解。
  • 代码应尽可能简单。
  • 只使用该练习或其前置内容(以及前置内容的前置内容,依此类推)所介绍的语言特性。
  • 在浏览器内编写代码时,存根文件会展示给学生;使用 CLI 时,它会下载到学生的文件系统。
  • 存根实现文件的相对路径必须在.meta/config.json文件的"files.solution"键中指定。

示例

class Lasagna
  def remaining_minutes_in_oven(actual_minutes_in_oven)
    raise NotImplementedError, 'Please implement the Lasagna#remaining_minutes_in_oven method'
  end

  def preparation_time_in_minutes(layers)
    raise NotImplementedError, 'Please implement the Lasagna#preparation_time_in_minutes method'
  end
end

文件:测试

用途: 验证提交的解答是否正确。

是否必需: 必需

  • 测试不应使用instructions.md文件中的示例。
  • 代码应尽可能简单。
  • 只使用该练习的前置内容(以及前置内容的前置内容,依此类推)所介绍的语言特性。
  • 在浏览器内编写代码时,测试文件_不会_展示给学生;但使用 CLI 时,它_会_下载到学生的文件系统。
  • 测试文件的相对路径必须在.meta/config.json文件的"files.test"键中指定。

示例

require 'minitest/autorun'
require_relative 'lasagna'

class LasagnaTest < Minitest::Test
  def test_remaining_minutes_in_oven
    assert_equal 15, Lasagna.new.remaining_minutes_in_oven(25)
  end

  def test_preparation_time_in_minutes_with_one_layer
    assert_equal 2, Lasagna.new.preparation_time_in_minutes(1)
  end

  def test_preparation_time_in_minutes_with_multiple_layers
    assert_equal 8, Lasagna.new.preparation_time_in_minutes(4)
  end
end

文件:范例实现

用途: 提供学生应当努力实现的目标代码。

是否必需: 必需

  • 这份实现是我们希望学生努力实现的目标代码。
  • 导师在撰写反馈时,会看到这段代码作为“目标”
  • 该实现只应使用练习或其前置内容(以及前置内容的前置内容,依此类推)所介绍的语言特性。
  • 在浏览器内编写代码时,范例文件_不会_展示给学生;使用 CLI 时,它也_不会_下载到学生的文件系统。
  • 导师在评论解答或 representation 时,会看到范例文件。
  • 范例实现文件的相对路径必须在.meta/config.json文件的"files.exemplar"键中指定。

示例

class Lasagna
  EXPECTED_MINUTES_IN_OVEN = 40
  PREPARATION_MINUTES_PER_LAYER = 2

  def remaining_minutes_in_oven(actual_minutes_in_oven)
    EXPECTED_MINUTES_IN_OVEN - actual_minutes_in_oven
  end

  def preparation_time_in_minutes(layers)
    layers * PREPARATION_MINUTES_PER_LAYER
  end
end

文件:附加文件

用途: 确保测试能够运行。

是否必需: 如果默认文件不足以运行测试则必需

有些语言需要额外的文件才能运行测试。例如 C# 的项目文件和 Node 的package.json文件,没有它们就无法运行测试。

共享文件

有些文件并非针对单个练习,而是适用于_所有_练习。更多信息请查阅文档。

命名

概念练习应以其故事/主题命名,而_不是_以其概念命名。

好的名称示例:

  • Tim from Marketing
  • Lucian's Luscious Lasagna
  • Calculator Conundrum

不允许的名称:

  • Booleans:使用了概念名,而不是故事名
  • Exercise #1:练习不是故事/主题

在没有重大改动地 fork 练习时,尽可能使用原始名称。

Slug

每个练习还有一个 slug,它是按照以下规则对练习名称进行规范化后的版本:

  1. 使用小写。
  2. 使用 kebab-case。
  3. 使用拉丁字母数字字符和连字符(正则表达式:[a-z0-9-]+)
  4. 优先使用文字形式的数字,而非阿拉伯数字,除非有特定理由优先使用阿拉伯数字(例如two-fer优于2-fer)

好的 slug 示例:

  • tim-from-marketing
  • lucians-luscious-lasagna
  • calculator-conundrum

不允许的 slug:

  • TIM-FROM-MARKETING:未使用小写(即tim-from-marketing)
  • TimFromMarketing:未使用 kebab-case(即tim-from-marketing)
  • floating-point-numbers:使用了概念名,而不是故事名

呈现

使用浏览器内编辑器与使用 CLI 时,练习文档展示给学生的形式有所不同。更多信息请参阅这份文档。

图标

每个练习都有一个配套图标。 默认情况下,显示的图标是其名称与练习 slug 匹配的那个。 可以通过在练习的.meta/config.json文件中指定icon属性来覆盖这一设置。

如果你在 fork 一个已有练习,那么该练习很可能已经有图标了。 如果没有,请在 website-icons 代码仓库中提交 issue。