{ }codelper

学习 › Java

进阶 · 25

记录列表与枚举

把记录收进列表,给固定的几种取值起名字。

把由几个值组成的记录一条条放进列表,就成了一张表。
而像“低、中、高”这样只能是固定几种之一的值,要给它们起名字(枚举,enum)。
下面的示例读入学生的姓名和分数,把带有等级的记录收进列表,逐行输出,然后只输出等级为 HIGH 的学生姓名。

import java.util.*; public class Main {    enum Level { LOW, MID, HIGH }   // 정해진 세 가지 중 하나     record Student(String name, int score, Level level) {}     static Level levelOf(int score) {        if (score >= 80) return Level.HIGH;        if (score >= 60) return Level.MID;        return Level.LOW;    }     public static void main(String[] args) {        Scanner in = new Scanner(System.in);        int n = in.nextInt();        List<Student> students = new ArrayList<>();   // 묶음 자료(이름, 점수, 등급)의 목록        for (int i = 0; i < n; i++) {            String name = in.next();            int score = in.nextInt();            students.add(new Student(name, score, levelOf(score)));        }         for (Student s : students) {            System.out.println(s.name() + " " + s.score() + " " + s.level());        }         List<String> high = new ArrayList<>();        for (Student s : students) {            if (s.level() == Level.HIGH) high.add(s.name());        }        System.out.println("HIGH: " + String.join(" ", high));    }}
stdin
4
mina 92
jun 58
sora 75
theo 81
stdout
mina 92 HIGH
jun 58 LOW
sora 75 MID
theo 81 HIGH
HIGH: mina theo

要点

  • 一条记录是关于一个人(一件东西)的一组值。
    把记录放进列表,就得到每一格是一条记录的表。
    从 students 里取出的 s,姓名、分数、等级是在一起的。
  • 记录要在循环里面、每一轮新建一条再放进列表。
    比起分别保存姓名列表和分数列表,一个人的值不会散开。
  • 枚举(enum)是给固定的几种取值起了名字的类型。
    不直接写字符串 "HIGH" 或数字 2,而是写 Level.HIGH。
    在有枚举的语言里,写了像 Level.HIHG 这样不存在的名字,会报错提醒你(Python 是在运行时,其他语言是在编译时)。
  • 枚举里面的值因语言而异。
    C、C++、TypeScript、C# 里面是 0、1、2 这样的数。
    输出名字时,Java 和 C# 直接输出即可,Python 用 .name,TypeScript 用 Level[值],C 和 C++ 从写好名字的数组里取。
  • 没有枚举的 JavaScript 和 Craykon 用有名字的常量做同样的事。
    不在代码各处写 2,而是先定好 HIGH 这个名字再用。
  • 把分数换成等级的工作集中在一个函数(levelOf)里。
    标准变了,也只需要改一处。

各语言的写法

Python记录用字典 {"name": name, "score": score} 创建,用 students.append(...) 放入,用 s["name"] 取出。
用加了 @dataclass 的类来做,就可以用 s.name 取出。
枚举先写 from enum import Enum,再在 class Level(Enum): 里写 HIGH = 2 这样的行。
名字是 level.name,里面的值是 level.value,按名字字符串查找用 Level["HIGH"]
JavaScript记录用对象 { name: name, score: score } 创建,用 students.push(...) 放入,用 s.name 取出。
变量名和键相同时可以简写成 { name, score }。
没有 enum,所以用冻结的对象:const Level = Object.freeze({ LOW: "LOW", MID: "MID", HIGH: "HIGH" });。
冻结的对象即使执行 Level.HIGH = "x" 也不会变。
像本站评测那样作为普通脚本运行时,这次赋值会被悄悄忽略;在严格模式("use strict" 或 ES 模块)下则以 TypeError 停止。
不存在的名字 Level.HIHG 不是错误,而是 undefined
TypeScript记录的形状用 type Student = { name: string; score: number; level: Level }; 规定,列表是 Student[]。
枚举是 enum Level { LOW, MID, HIGH },里面是 0、1、2。
名字用 Level[s.level] 得到。
规定为几个字符串之一的 type Level = "LOW" | "MID" | "HIGH"; 也很常用
Java记录用 record Student(String name, int score, Level level) {} 定义,用 new Student(...) 创建,取出时写 s.name()。
列表是 List<Student>。
枚举是 enum Level { LOW, MID, HIGH },输出时直接得到名字。
序号是 level.ordinal(),按名字字符串查找用 Level.valueOf("HIGH")
C#记录用 record Student(string Name, int Score, Level Level); 定义,用 new Student(...) 创建,取出时写 s.Name。
列表是 List<Student>。
枚举是 enum Level { LOW, MID, HIGH },输出时得到名字。
里面的数是 (int)level,按名字字符串查找用 Enum.Parse<Level>("HIGH")
C++记录是 struct Student { std::string name; int score; Level level; };,列表是 std::vector<Student>,用 push_back({name, score, levelOf(score)}) 放入。
枚举是 enum class Level { LOW, MID, HIGH };,写作 Level::HIGH。
enum class 不能直接送进 std::cout,也不会自己变成 int,所以从名字数组里取:LEVEL_NAMES[static_cast<int>(s.level)]
C记录是 typedef struct { char name[32]; int score; Level level; } Student;,列表是数组 Student students[100];,像 students[i].score 这样使用。
枚举是 typedef enum { LOW, MID, HIGH } Level;,里面是整数 0、1、2。
没有输出名字的功能,所以像 LEVEL_NAMES[students[i].level] 这样从名字数组里取
Craykon没有结构体,所以记录用字典来做。
每一轮用 s = 字典(); 新建,用 s["name"] = ... 填好,再 students.添加(s)。
也没有 enum,所以在最外层写 LOW = 0; MID = 1; HIGH = 2; 这样有名字的常量,名字从列表 LEVEL_NAMES[s["level"]] 里取。
没有阻止修改常量的功能,所以要约定不去改它

常见错误

把枚举值和字符串或数字比较Python

from enum import Enum
class Level(Enum):
    LOW = 0
    MID = 1
    HIGH = 2
level = Level.HIGH
print(level == "HIGH", level == 2)

→ False False — Level.HIGH 既不是字符串 "HIGH" 也不是数字 2,而是另一个值。它不报错就变成假,所以很难发现

像 level == Level.HIGH 这样在枚举之间比较。
从输入得到的字符串,先用 Level["HIGH"] 转成枚举。

输出枚举,得到的是数字而不是名字TypeScript · C · C++

enum Level { LOW, MID, HIGH }
console.log(Level.HIGH);

→ 2 — 里面的数被原样输出。C 的 printf("%d\n", level);、C++ 把 enum Level 送进 std::cout,也都是 2。把 C++ 的 enum class 送进 std::cout 则是编译错误

TypeScript 用 Level[Level.HIGH] 得到名字。
C 和 C++ 准备一个 {"LOW", "MID", "HIGH"} 这样的名字数组,从里面取。

一边修改同一条记录,一边反复放入JavaScript · TypeScript · Python · Craykon

const s = {};
const students = [];
for (const name of ["mina", "jun"]) {
  s.name = name;
  students.push(s);
}
console.log(students);

→ [ { name: 'jun' }, { name: 'jun' } ] — 列表的两格指向同一条记录,所以只剩下最后写入的值

在循环里面创建记录(const s = { name: name };)。
这样每一轮都会有一条新记录。

各语言的差异
是枚举,并不代表只有规定的值才能放进去。
在 C 里,Level x = 7; 照样能编译,并输出 7。
在 C# 里输出 (Level)7 也是 7。
C++ 的 enum class 会用编译错误拦住 Level x = 7;。
把外面来的数转成枚举时,先检查范围。

练习题