练习题


实践练习是让学生解决任意题目的练习,目的在于让他们运用迄今为止学到的概念。

想为某个赛道添加第一个实践练习?查看添加实践练习的文档,或观看我们的讲解视频 👇

Note

只需在赛道的根目录下运行以下命令,就能快速搭建一个新实践练习的框架:

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

更多信息请查看 configlet create文档

元数据

实践练习的元数据在 config.json 文件的exercises.practice键中定义。这些元数据定义了练习的 UUID、slug 等信息。

示例

{
  "exercises": {
    "practice": [
      {
        "uuid": "8ba15933-29a2-49b1-a9ce-70474bad3007",
        "slug": "leap",
        "name": "Leap",
        "practices": ["if-statements", "numbers", "operator-precedence"],
        "prerequisites": ["if-statements", "numbers"],
        "difficulty": 1
      }
    ]
  }
}

practices

practices键应列出该实践练习能让学生切实练习的概念的 slug。

  • 这些会显示在界面中,形式为“在以下练习中练习该概念:TwoFer、Leap 等”
  • 尽量为每个概念选择 3 到 8 个练习。
  • 尽量选择至少两个练习,让人有机会练习某个概念的基础。
  • 有些概念非常常见(例如 strings)。这种情况下,我们建议挑选几个好练习,让人们从有趣的角度思考这些概念。例如,需要使用 UTF-8、字符串拼接、字符枚举等的练习都是不错的例子。

prerequisites

prerequisites键列出了学生必须完成才能访问该实践练习的概念。

  • 这些会显示在界面中,形式为“学习 Strings 来解锁 TwoFer”
  • 它应包含学生要至少以一种地道方式完成该练习所必须掌握的所有概念。例如,对于 Ruby 中的 TwoFer 练习,前置条件可能包括 strings、optional-params、implicit-return。
  • 对于可以用其他概念完成的练习(例如可以用 loops 或 recursion 解决的练习),维护者应考虑学生在该赛道中的学习历程,选择一种他们希望用来解锁该练习的方法。例如,在上面循环/递归的例子中,他们可能认为这个练习是早期练习 loops 的好机会,也可能想把它留到后面用来教递归。他们还可以借助分析器,提示学生尝试另一种方法:“用 loops 解决得很棒。你也可以试试用 Recursion 来解决。”

文件

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

一个实践练习有四类文件:

文档文件

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

  • .docs/introduction.md:介绍练习的设定和背景(可选)
  • .docs/introduction.append.md:在现有介绍之后追加的补充介绍文字(可选)
  • .docs/instructions.md:提供练习的说明(必需)
  • .docs/instructions.append.md:在现有说明之后追加的补充说明文字(可选)
  • .docs/hints.md:为学生提供提示,帮助他们在练习中摆脱困境(可选)

元数据文件

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

  • .meta/config.json:包含练习的元信息(必需)
  • .meta/design.md:描述练习的设计(可选)
  • .meta/tests.toml:包含已实现测试的信息(可选)

方法文件

这些文件描述练习的各种方法。

  • .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
└── practice
    └── isogram
        ├── .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
        |   ├── tests.toml
        |   └── Example.cs (example implementation)
        ├── Isogram.cs (stub implementation)
        └── IsogramTests.cs (tests)

文件:.docs/introduction.md

用途: 向学生介绍练习的设定和背景。

是否必需: 如果该练习实现了带有 introduction.md 文件的 Problem Specifications 练习,则必需

如果该练习实现了 Problem Specifications 练习,则该文件的内容应与该 Problem Specifications 练习的 introduction.md 文件一致。configlet 可以自动同步该文件的内容。

如果该练习_并非_基于 Problem Specifications 练习,请考虑以下几点:

我们非常重视让 Exercism 的内容对所有人都安全,因此在判断故事题材是否合适时,常常会偏向谨慎。虽然我们会对合并的内容格外留心,但我们也明白,很难完全察觉哪些内容可能被视为有问题,所以我们始终假定你是出于善意,并会在评审中尽力以不带对立情绪的方式发现任何问题。如果你想和我们一起核查某个故事,请 @exercism/leadership,我们会一起看看。以下是一些指导要点:

  • 尽量确保故事对所有人友好、人人都能理解。如果故事里含有只有特定圈子才懂的梗或地区性俚语,试着换一种说法。
  • 尽量编写对所有人都包容的例子。例如,可以考虑使用来自其他文化的名字,以及混合性别。
  • 问问自己,你个人是否认识会因为这个故事而感到被冒犯的人。如果有,考虑改一改,避免这种情况。

示例

# Introduction

Bob is a lackadaisical teenager. In conversation, his responses are very limited.

文件:.docs/introduction.append.md

用途: 在现有介绍之后追加的补充介绍文字。

是否必需: 可选

在少数(罕见)情况下,你可能想对练习的 introduction.md 文件加以扩充,例如当练习实现的测试没有被现有说明覆盖时。

如果某个赛道不希望 Bob 支持非 ASCII 消息,可能会添加以下内容:

# Introduction append

## Note

As part of his teenage rebellion, Bob has decided to only communicate using ASCII.

追加文件应以 H1 标题开头。 该标题不会显示出来,但仍应保留。 H1 标题后面通常跟一个 H2 标题,用来把通用内容与该赛道特有的内容区分开。

文件:.docs/instructions.md

用途: 提供练习的说明。

是否必需: 必需

如果该练习实现了 Problem Specifications 练习,则该文件的内容应与该 Problem Specifications 练习的 instructions.md 文件一致(如果没有 instructions.md 文件,则为 description.md 文件)。configlet 可以自动同步该文件的内容。

如果该练习_并非_基于 Problem Specifications 练习,请考虑以下几点:

我们非常重视让 Exercism 的内容对所有人都安全,因此在判断故事题材是否合适时,常常会偏向谨慎。虽然我们会对合并的内容格外留心,但我们也明白,很难完全察觉哪些内容可能被视为有问题,所以我们始终假定你是出于善意,并会在评审中尽力以不带对立情绪的方式发现任何问题。如果你想和我们一起核查某个故事,请 @exercism/leadership,我们会一起看看。以下是一些指导要点:

  • 尽量确保故事对所有人友好、人人都能理解。如果故事里含有只有特定圈子才懂的梗或地区性俚语,试着换一种说法。
  • 尽量编写对所有人都包容的例子。例如,可以考虑使用来自其他文化的名字,以及混合性别。
  • 问问自己,你个人是否认识会因为这个故事而感到被冒犯的人。如果有,考虑改一改,避免这种情况。

示例

# Instructions

Bob answers 'Sure.' if you ask him a question, such as "How are you?".

He answers 'Whoa, chill out!' if you YELL AT HIM (in all capitals).

He answers 'Calm down, I know what I'm doing!' if you yell a question at him.

He says 'Fine. Be that way!' if you address him without actually saying anything.

He answers 'Whatever.' to anything else.

文件:.docs/instructions.append.md

用途: 在现有说明之后追加的补充说明文字。

是否必需: 可选

在少数(罕见)情况下,你可能想对练习的 instructions.md 文件加以扩充,例如当练习实现的测试没有被现有说明覆盖时。

# Instructions append

## Note

Bob's conversational partner is a purist when it comes to written communication and always follows normal rules regarding sentence punctuation in English.

追加文件应以 H1 标题开头。 该标题不会显示出来,但仍应保留。 H1 标题后面通常跟一个 H2 标题,用来把通用内容与该赛道特有的内容区分开。

文件:.docs/hints.md

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

是否必需: 可选

  • 如果学生卡住了,我们会允许他们点击一个按钮来请求提示,这会显示文件中相关的部分。
  • 提示应以项目符号的形式列在标题下方。
  • 提示应足以让几乎所有学生都能继续下去。
  • 提示不应直接给出解答,而应指向描述解答的资料(例如链接到要使用的函数的文档)。
  • 提示可以用代码示例来解释概念,但不能用来勾勒解答。例如,在列表练习中,可以展示某个列表函数如何工作的代码片段,但不能是能直接复制粘贴到解答里的形式。
  • 提示必须以 Markdown 列表的形式出现在 ## General标题下方。
  • 如果没有提示,则应省略该标题。

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

示例

## General

- There are many [built-in methods][integers] to simplify working with integers.

[integers]: https://ruby-doc.org/core-2.7.0/Integer.html

文件:.meta/design.md

用途: 描述练习的设计。

是否必需: 可选

该文件包含练习设计的相关信息,包括练习的目标、教学目标、不应教授的内容等。

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

示例

# Design

## Goal

The goal of this exercise is help students practice how to work with strings.

## Notes

This exercise does not contain any error handling tests.

文件:.meta/config.json

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

是否必需: 必需

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

  • authors:练习作者的 GitHub 用户名(可选)
    • 如果评审者的评审大幅改动了练习(到了让人觉得“你们是一起完成的”的程度),也包括这些评审者
  • contributors:练习贡献者的 GitHub 用户名(可选)
    • 如果评审者的评审有意义、可操作或已被采纳,也包括这些评审者。
  • files:该练习所用文件的位置,相对于练习目录(必需)
    • solution:桩实现文件(必需)
    • test:测试文件(必需)
    • example:示例实现文件(必需)
    • editor:在编辑器中以只读方式显示的附加文件(可选)
    • invalidator:当这些文件发生变化时,会使提交的解答过时的文件(可选)
  • language_versions:语言版本要求(可选)
  • blurb:该练习的简短描述。长度必须 <= 350。_不_支持 Markdown(必需)
  • source:该练习所依据的来源(可选)
  • source_url:该练习所依据来源的 URL(可选)
  • test_runner:指示该练习的解答是否应在测试运行器中测试。未指定时默认为 true。(可选)
  • representer:关于 representer 如何处理该文件的元信息(可选)
    • version:用于该练习的 representer 版本的整数(如果存在父键则必需)
  • icon:图标的 slug(参见完整图标列表)。如果未指定,将使用练习的 slug(可选)
  • custom:任何练习特有的非标准数据。可用于按练习自定义赛道工具的行为(可选)

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

示例

{
  "authors": ["FSharpForever"],
  "files": {
    "solution": ["Bob.fs"],
    "test": ["BobTests.fs"],
    "example": [".meta/Example.fs"]
  },
  "blurb": "Bob is a lackadaisical teenager. In conversation, his responses are very limited"
}

请注意:

  • 作者和贡献者的顺序并不重要,也没有任何含义。
  • language_versions是一个自由格式的字符串,赛道可以随意使用和解释。

文件:.meta/tests.toml

用途: 包含已实现测试的信息。

是否必需: 可选

如果练习在 problem-specifications 仓库中的 canonical-data.json 文件里定义了测试,则该文件包含正在实现哪些测试的信息。

它的存在是为了帮助维护者跟踪已实现哪些测试,并(可选地)记录某个测试为什么没有实现。 它还可以用来检测尚未实现的测试。

configlet 工具通过 configlet sync 命令,负责用 problem-specifications 仓库中的数据来更新/同步该文件。 同步时,configlet 会针对每个尚未实现的测试,询问是否要包含该测试。

示例

# This is an auto-generated file.
#
# Regenerating this file via `configlet sync` will:
# - Recreate every `description` key/value pair
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
# - Preserve any other key/value pair
#
# As user-added comments (using the # character) will be removed when this file
# is regenerated, comments can be added via a `comment` key.

[3e5c30a8-87e2-4845-a815-a49671ade970]
description = "empty strand"

[a0ea42a6-06d9-4ac6-828c-7ccaccf98fec]
description = "can count one nucleotide in single-character input"

[eca0d565-ed8c-43e7-9033-6cefbf5115b5]
description = "strand with repeated nucleotide"

[40a45eac-c83f-4740-901a-20b22d15a39f]
description = "strand with multiple nucleotides"

[b4c47851-ee9e-4b0a-be70-a86e343bd851]
description = "strand with invalid nucleotides"
include = false
comment = "error handling omitted on purpose"

文件:.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

用途: 展示该方法的代码片段

是否必需: 可选(对于方法则必需)

该文件包含一小段展示该方法的代码片段。 该片段会显示在练习的“深入探索”页面上。

它的行数必须 <= 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

用途: 展示该文章的代码片段

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

该文件包含一小段展示该文章的代码片段。 该片段会显示在练习的“深入探索”页面上。

它的行数必须 <= 8。

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

示例

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

文件:桩实现

用途: 为学生提供起点。

是否必需: 必需

  • 设计桩时,要让学生知道该在哪里添加代码。
  • 对于编译型语言,可以考虑让代码能够通过编译,因为编译器消息有时很难让初学该语言的学生理解。
  • 代码应尽可能简单。
  • 只使用前置条件(以及前置条件的前置条件,依此类推)所引入的语言特性。
  • 在浏览器内编写代码时,桩文件会展示给学生;使用 CLI 时,它会被下载到学生的文件系统中。
  • 桩实现文件的相对路径必须在 .meta/config.json 文件的 "files.solution" 键中指定。

示例

using System;

public static class Isogram
{
    public static bool IsIsogram(string word)
    {
        throw new NotImplementedException("You need to implement this function.");
    }
}

文件:测试

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

是否必需: 必需

  • 代码应尽可能简单。
  • 只使用练习前置条件(以及前置条件的前置条件,依此类推)所引入的语言特性。
  • 在浏览器内编写代码时,测试文件会展示给学生;使用 CLI 时,它会被下载到学生的文件系统中。
  • Exercism 倾向于让实践练习通过测试驱动开发来完成。为此,有两种方案:
    • 测试运行器必须按文件中定义的顺序运行测试,且测试套件必须在首次失败时中止;或者
    • 默认情况下,除第一个测试外的所有测试都应被跳过。
  • 测试文件的相对路径必须在 .meta/config.json 文件的 "files.test" 键中指定。

示例

using Xunit;

public class IsogramTest
{
    [Fact]
    public void Empty_string() =>
        Assert.True(Isogram.IsIsogram(""));

    [Fact(Skip = "Remove this Skip property to run this test")]
    public void Isogram_with_only_lower_case_characters() =>
        Assert.True(Isogram.IsIsogram("isogram"));

    [Fact(Skip = "Remove this Skip property to run this test")]
    public void Word_with_one_duplicated_character() =>
        Assert.False(Isogram.IsIsogram("eleven"));
}

文件:示例实现

用途: 提供一个能通过所有测试的示例实现。

是否必需: 必需

  • 该实现用来验证确实存在能通过测试的实现。它有意_不是_我们希望学生追求的那种代码。
  • 每个赛道都应在其持续集成环境中验证示例实现能通过测试。
  • 导师不会看到这段代码。
  • 在浏览器内编写代码时,示例文件_不会_展示给学生;使用 CLI 时,它_不会_被下载到学生的文件系统中。
  • 示例实现文件的相对路径必须在 .meta/config.json 文件的 "files.example" 键中指定。

示例

using System.Linq;

public static class Isogram
{
    public static bool IsIsogram(string word)
    {
        var lowerCaseLetters = word.ToLower().Where(char.IsLetter).ToList();
        return lowerCaseLetters.Distinct().Count() == lowerCaseLetters.Count;
    }
}

文件:附加文件

用途: 为了让测试能够运行而需要的额外项目文件、构建文件或支持文件。

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

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

共享文件

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

呈现方式

使用浏览器内编辑器和使用 CLI 时,练习文档呈现给学生的方式有所不同。更多信息请参见这篇文档。

图标

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

如果你正在根据 problem-specifications 元数据实现某个练习,那么该练习很可能已经有图标了。 如果没有,请在 website-icons 仓库中提交 issue。