简介:面向C#初学者的背单词应用源代码,演示如何利用TextReader/StreamReader读取txt单词库,将英文单词与中文释义逐行解析,并通过正序、倒序、随机三种模式展示,支持自定义刷新速度,适合课程设计、课后练习或自学练手。资源共37个文件,以9个C#源文件、7个单词数据文本、3个配置文件及编译生成的exe程序为主,另含Visual Studio解决方案文件,压缩包约290KB,轻量易用,打开即可查看完整项目结构。目前已有948人学习/下载,是C#语法巩固与桌面应用入门的实用参考,尤其适合刚开始学习文件操作和WinForms的读者。项目完整覆盖文件读取、字符串Split拆分、Word类封装、List泛型集合、Random随机索引、WinForms按钮事件、try-catch异常处理、定时刷新等核心知识点,帮助读者理解从数据解析到界面交互的完整链路;在此基础上还可扩展数据库存储或多线程功能,进一步提升工程能力。
1. C#学习之背单词(源代码)到底是什么,以及为什么值得跑一遍
C#学习相关的教程视频一抓一大把,但真正能让你完整读完、跑起来、还能动手改的可运行项目,反而稀缺。“背单词(源代码)”这类标题的项目,恰好补上了这个缺口:它把C#的类、集合、文件读写、异常处理和一个真实的循环业务场景揉在了一起。你在运行时会看到自己输入的单词被存进文件、再次启动时被按记忆曲线重新安排出场顺序——这就是学 C# 最常见也最可靠的第一类练手项目,比单纯抄语法示例有用得多。
它的适用人群很明确:语法学了但没做过完整小程序的初学者,以及想找一个带读写文件、带算法逻辑的面试作业题的从业者。这个项目解决的核心问题不是一个“更好的背单词 App”,而是一个“你能看懂、能改、能跑通”的 C# 教学载体。标题里的“源代码”三个字,意味着你可以打开每个文件逐行看实现,而不是对着黑盒按钮瞎猜。今天这篇就按“先立住概念、再动手复现、最后讲坑在哪”的顺序展开,中间每一步都有可直接抄的代码和参数说明。
2. 拆解“背单词”项目:控制台架构与数据模型设计
2.1 选型的理由:为什么控制台程序是学习阶段最稳的骨架
常见的 C# 背单词项目有两种承载形式:带窗体的 WinForms/WPF,和不带窗体的控制台程序。我见过不少初学 C# 的同学一上来就想做带界面的版本,结果界面画了两天,按钮点击事件里的逻辑却一直对不上,最后整个项目烂尾。控制台版本对初学者有三个不可替代的优势:代码从头到尾只有一个顺序执行入口,你不必被事件驱动打断思路;控制台能直接看到每次循环的输入输出,方便对照代码理解数据怎么流动;调试时不会出现“界面线程”和“业务逻辑线程”互相纠缠的混乱情况。
所以我的建议很直接:先让控制台版本完整跑通,再谈换成 WinForms 或 WPF。绝大多数 C# 学习的翻车现场,都发生在“连逻辑都没跑通就开始画界面”这个环节。控制台版本能让你把注意力集中在数据模型、文件读写、循环控制这些真正的语言基本功上。项目做完之后,你自然会发现界面只是在同一个业务逻辑外面换了一层壳,那时候再补 UI 反而顺理成章。
2.2 核心数据模型:单词、复习状态与运行配置要分开建
背单词程序的根基是单词数据。一个单词至少需要拼写、音标、释义、例句四个字段。如果将来要支持多词库,还应该有一个词库标识,但现在先保持简单,从单个 Word 类开始:
public class Word { public int Id { get; set; } public string Spelling { get; set; } = string.Empty; public string Phonetic { get; set; } = string.Empty; public string Meaning { get; set; } = string.Empty; public string Example { get; set; } = string.Empty; }这个类的每个属性都对应背单词时用户看到的信息。Spelling 是题干,Meaning 是答案,Phonetic 和 Example 是辅助记忆内容。写成属性而不是公共字段,是为了将来能加只读逻辑或者序列化控制,也是 C# 里数据对象的标准写法。
光有单词类还不够,程序必须记录用户对每个单词的记忆状态。这个状态如果和单词混在一个类里,会导致一个单词每背一遍就多一条重复数据,词库文件越来越臃肿,而且背诵进度和词库本身的生命周期不同。词库可以整体替换,但用户的复习进度必须跟着用户走。所以我通常拆成两个类:
public class WordReviewState { public int WordId { get; set; } public int Level { get; set; } public int CorrectCount { get; set; } public int WrongCount { get; set; } public DateTime NextReviewTime { get; set; } }WordReviewState 里的 Level 表示当前记忆等级,等级越高,下一次复习的间隔越长。CorrectCount 和 WrongCount 用于统计正确率,NextReviewTime 决定这个单词“哪天该出现”。把词库数据与复习进度分开存储,意味着你换词库时不会被旧进度污染,清空学习记录时也不用动词库文件。
运行配置我建议单独用一个简单类或者在主程序里定义常量,不要散落在各处。比如每天新学几个词、复习几个词、答错几次算重新学习,这些数值集中放在一个类里,后续改参数时不用全文搜索。这一点在初学阶段容易被忽略,但当你跑了两周发现想调整学习强度时,会庆幸当时做了集中管理。
2.3 数据格式选择:JSON 零依赖,SQLite 留给第二阶段
控制台项目的数据文件格式,常见的有 JSON、XML、SQLite 三种。我一般会直接选 JSON,原因很实际:System.Text.Json 是 .NET 内置的,零第三方依赖,不会在初学阶段引入 NuGet 包安装和连接字符串的概念,而中文释义和音标符号在 JSON 里不会像 XML 那样被转义成奇怪的实体序列。
我用 System.Text.Json 序列化时,会把所有数据包在一个外层对象里,这样词库和复习进度能存进同一个文件,加载时一次读入,保存时一次写出。实现的仓储类大概长这样:
public class WordRepository { private readonly string _filePath; private List<Word> _words = new List<Word>(); private List<WordReviewState> _states = new List<WordReviewState>(); public WordRepository(string filePath) { _filePath = filePath; } public List<Word> LoadWords() { if (!File.Exists(_filePath)) { _words = CreateDefaultWords(); SaveAll(); return _words; } string json = File.ReadAllText(_filePath, Encoding.UTF8); var data = JsonSerializer.Deserialize<WordDatabase>(json); _words = data?.Words ?? new List<Word>(); _states = data?.States ?? new List<WordReviewState>(); return _words; } public void SaveAll() { var data = new WordDatabase { Words = _words, States = _states }; string json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(_filePath, json, Encoding.UTF8); } }LoadWords 里先判断文件存不存在,不存在就生成默认词库并立刻保存,这是为了让程序在首次运行时不抛异常,直接给出可用的初始数据。保存时把 Words 和 States 包进同一份 JSON,加载时一次性反序列化回来,比维护两个文件省很多事。WriteIndented 参数让生成的 JSON 带缩进,方便你用记事本直接查看内容,这对初学者跟踪数据状态特别有用。
如果词库达到几万条,JSON 全量读写的开销会变大,那时候再迁移到 SQLite 也不迟。SQLite 查询和更新可以按条件精确操作,但引入的复杂度对第一阶段的 C# 学习者来说,会明显分散注意力。先把 JSON 方案用熟,理解“序列化”和“反序列化”这两个概念,以后再接触数据库会更快。
3. 从零搭一个能跑的背单词程序:主流程与核心命令
3.1 项目骨架与目录结构
在终端里创建一个新的控制台项目,我习惯把项目命名为 WordTrainer,这样看到解决方案文件名就知道用途。创建命令:
dotnet new console -n WordTrainer cd WordTrainer然后手动建立三个目录:Models 放数据类,Services 放业务逻辑和数据访问,Data 放外部词库文件。最终目录结构是这样:
WordTrainer/ ├── Models/ │ ├── Word.cs │ └── WordReviewState.cs ├── Services/ │ ├── WordRepository.cs │ ├── ReviewEngine.cs │ └── ImportService.cs └── Data/ └── words.json这种“模型—服务—数据”三层的划分,对于控制台项目来说是最不容易过度设计的结构。模型只负责承载数据,服务负责读写和算法,主程序只放交互循环。以后把控制台界面换成 WinForms 或 WPF,这几个服务类不需要改动,只需要把界面层替换掉。这个好处在初学阶段体会不深,但当你第一次把控制台程序“改造成带界面”时就会明白。
3.2 背单词的主循环:用队列驱动问答流程
背单词程序的核心是“问—答—判定—记状态”。我选择 Queue 作为会话队列,因为队列先进先出的特性天然适合“答错的词重新排队”的需求。主循环代码可以直接放进 Program.cs,也可以抽成一个 Session 类,初学阶段放 Program.cs 里更容易看清全貌:
using WordTrainer.Models; using WordTrainer.Services; Console.OutputEncoding = System.Text.Encoding.UTF8; var repo = new WordRepository(Path.Combine("Data", "words.json")); var words = repo.LoadWords(); var queue = new Queue<Word>(words); int sessionRight = 0; int sessionTotal = 0; while (queue.Count > 0) { var word = queue.Dequeue(); Console.WriteLine($"[{sessionTotal + 1}] {word.Spelling}"); Console.Write("请输入中文释义:"); string? user = Console.ReadLine(); bool isRight = CompareAnswer(user, word.Meaning); if (isRight) { sessionRight++; Console.WriteLine("回答正确"); } else { queue.Enqueue(word); Console.WriteLine($"回答错误,正确释义:{word.Meaning}"); } sessionTotal++; repo.SaveAll(); } Console.WriteLine($"本轮完成,正确率:{sessionRight * 100.0 / sessionTotal:F1}%");这段代码里有两处值得停下来细看。第一,答错的单词被重新 Enqueue 到队尾,这意味着它会在这场会话里再次出现,实现“错题重复直到答对”的效果,但又不至于让一个单词反复打断同一批学习流程。第二,正确率的分母是 sessionTotal 而不是单词总数,这是有意为之,统计口径是“用户回答了多少次”,而不是“背了多少个不同的词”,避免答错被重复计次时正确率出现误导。
CompareAnswer 方法实现如下:
private static bool CompareAnswer(string? input, string correct) { if (string.IsNullOrWhiteSpace(input)) return false; return input.Trim().Equals(correct.Trim(), StringComparison.OrdinalIgnoreCase); }这个方法的重点是 Trim 和 OrdinalIgnoreCase。用户输入时常常带出多余空格,或者把首字母大写,比如“Apple”和“apple”。直接使用 == 比较会把这些情况全部判错,而 Trim 去掉两侧空白,OrdinalIgnoreCase 进行不区分大小写的逐字符比较。这里没有用 ToLower 的原因是不同语言的区域规则存在差异,OrdinalIgnoreCase 在“用户输入与标准答案比对”这种场景下行为更稳定。
3.3 词库文件不存在时,如何生成默认数据
第一次运行程序时 Data 目录里可能什么都没有,WordRepository 的 LoadWords 处理的就是这种场景。我一般会在仓储类里内置一组默认单词,让程序开箱即用,而不是要求用户先去找词库:
private List<Word> CreateDefaultWords() { return new List<Word> { new Word { Id = 1, Spelling = "abandon", Phonetic = "/əˈbændən/", Meaning = "v. 抛弃;放弃", Example = "He abandoned his plan." }, new Word { Id = 2, Spelling = "ability", Phonetic = "/əˈbɪləti/", Meaning = "n. 能力", Example = "She has the ability to lead." }, new Word { Id = 3, Spelling = "absorb", Phonetic = "/əbˈzɔːrb/", Meaning = "v. 吸收;吸引", Example = "The sponge absorbs water." } }; }这里 Id 从 1 开始连续编号。需要注意,Id 只应在创建新词时递增,不能在加载文件时重新按顺序赋值,否则复习状态和单词的对应关系会错乱。正确的做法是:如果新词没有设置 Id,就取当前最大 Id 加一。我会在 ImportService 里统一处理这个逻辑,而不是在 WordRepository 里散落处理。
3.4 CSV 词库导入:把数据迁出代码之外
内置默认单词只能用来跑通流程,真正背单词必须有自己的词库。常见做法是提供一个 CSV 导入接口,让用户从第三方词表复制数据进来。CSV 的格式约定为每行四个字段:拼写、音标、释义、例句。
public static class ImportService { public static List<Word> ImportFromCsv(string csvPath) { var words = new List<Word>(); int maxId = 0; foreach (var raw in File.ReadAllLines(csvPath, Encoding.UTF8)) { if (string.IsNullOrWhiteSpace(raw)) continue; var parts = raw.Split(','); if (parts.Length < 3) continue; words.Add(new Word { Id = ++maxId, Spelling = parts[0].Trim(), Phonetic = parts[1].Trim(), Meaning = parts[2].Trim(), Example = parts.Length > 3 ? parts[3].Trim() : string.Empty }); } return words; } }这里有一个容易踩坑的约定:CSV 的第三个字段释义里不能带逗号,否则 Split(',') 会把释义拆成两段导致导入错位。如果确实需要包含逗号,就得改用更严谨的 CSV 解析库,但初学阶段建议直接约定“释义里不用逗号,用分号或顿号代替”。另外,导入后要检查重复词,我一般用 Spelling 去重,因为同一个拼写重复导入会把复习状态彻底搞乱。
运行导入的逻辑可以做成主程序里的一个分支命令,比如启动时询问用户是“直接开始背”还是“导入新词”。这一步把词库从“写死在代码里”变成“外部数据驱动”,是整个项目从演示品走向工具的关键分界。
4. 记忆算法与数据持久化:把背单词程序从“演示”变成“真能记”
4.1 艾宾浩斯间隔复习:用 Level 和 NextReviewTime 驱动单词的出场顺序
如果单词每次都按固定顺序出现,那就不叫背单词,叫读单词表。一个能被记忆的背单词程序,必须让用户“在快要忘掉的时候再次看到这个词”。常见的做法是参考艾宾浩斯遗忘曲线,用一组递增的复习间隔来控制单词的出场频率。我用一个 Level 字段代表当前记忆等级,每个等级对应一个间隔天数:
public class ReviewEngine { private static readonly int[] Intervals = { 0, 1, 3, 7, 15, 30 }; public void RecordAnswer(WordReviewState state, bool correct) { if (correct) { state.Level = Math.Min(state.Level + 1, Intervals.Length - 1); } else { state.Level = 0; } state.NextReviewTime = DateTime.Today.AddDays(Intervals[state.Level]); state.CorrectCount += correct ? 1 : 0; state.WrongCount += correct ? 0 : 1; } }这段代码回答了两个核心问题:答对这个单词,它的复习间隔会从“当天”变成“1 天后”、再变成“3 天后”“7 天后”,逐步拉长;答错则 Level 直接归零,意味着这个词要重新从“当天复习”开始练。把 Level 归零而不是减一,是因为一次遗忘说明原来的记忆痕迹已经断裂,降级到重新学习的状态更符合实际记忆规律。
Math.Min 的作用是防止 Level 超出数组长度,到达最高等级 30 天后就维持在 30 天这个最大间隔,不会越界异常。Intervals 数组是记忆算法的核心参数,你完全可以换成自己的节奏,比如 { 0, 2, 5, 10, 20 },只要保持单调递增即可。这里我建议初学者先不要改间隔,用经典的 1、3、7、15、30 跑两周,观察复习节奏再决定怎么调。
4.2 每日复习队列编排:到期词、新词穿插,避免一上来全是旧词
有了 NextReviewTime,主程序在启动时需要筛选“今天该复习的单词”。这个筛选逻辑直接放在 WordRepository 里更合适,因为它需要读取全部状态并做日期比对:
public Queue<Word> BuildDailyQueue(int newWordLimit, int reviewLimit) { var today = DateTime.Today; var due = _words.Where(w => { var state = _states.FirstOrDefault(s => s.WordId == w.Id); return state != null && state.NextReviewTime <= today; }).ToList(); var fresh = _words.Where(w => { var state = _states.FirstOrDefault(s => s.WordId == w.Id); return state == null || state.CorrectCount == 0; }).Take(newWordLimit).ToList(); due = due.OrderBy(_ => Guid.NewGuid()).Take(reviewLimit).ToList(); var queue = new Queue<Word>(); foreach (var w in fresh) queue.Enqueue(w); foreach (var w in due) queue.Enqueue(w); return queue; }这里有两个参数需要说明:newWordLimit 控制每天新学多少个词,reviewLimit 控制每天复习多少个旧词。我一般把新词设为 10、复习设为 30,这个比例适合每天能抽出二十分钟的学习者。如果你时间充裕,可以调到 20+50,但不要盲目加大,因为复习队列过大会导致每天要答几百道题,反而打击坚持的意愿。
新词排在复习词前面,是为了保证“学新词”这件事不会被进入队列的旧词挤掉。词序用了 Guid.NewGuid 做随机打乱,这一招在小型项目里比完整的 Fisher-Yates 洗牌算法实现起来简单得多,效果也够用。导出的队列在会话循环里被消费时,答错的词会被重新 Enqueue 到队尾,保证本轮内再次出现,这个编排逻辑和上一章的主循环自然衔接。
4.3 状态保存的完整性:先写临时文件再替换,防止写一半崩溃
数据持久化最怕的不是数据写错,而是写的过程被打断导致整个文件损坏。JSON 是全量写入的,如果程序在 File.WriteAllText 执行到一半时进程被杀掉,words.json 就会变成截断的半截内容,下次启动直接反序列化失败。这个问题在背单词这种高频保存场景里尤其明显,因为每次答题我都在调用 SaveAll。
我采用的方案是“临时文件 + 替换”两步走,先把新数据写入临时文件,确认写完后再用临时文件覆盖正式文件:
public void SaveAll() { var data = new WordDatabase { Words = _words, States = _states }; string json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }); string tmpPath = _filePath + ".tmp"; File.WriteAllText(tmpPath, json, Encoding.UTF8); File.Copy(tmpPath, _filePath, true); File.Delete(tmpPath); }这段实现的要点是:主文件永远是上一次完整写入的结果,因为复制操作要么成功要么不执行,不会留下半个文件的状态。“先写临时文件再覆盖”是后端开发里常用的原子写入手法,拿到控制台项目里同样适用,这是一笔性价比很高的习惯投资,学会了以后处理任何文件型数据都能用上。
另外,我强烈建议你在每次运行程序前把 words.json 备份一份,比如手动复制为 words_backup.json。初学阶段改代码最容易不小心改坏数据文件,有了备份就有后悔药。等以后项目引入 Git,这个备份环节可以用版本管理替代,但现在手动备份成本极低,收益却很大。
5. 避坑指南:初次跑背单词源代码最常见的 5 个坑
5.1 换个目录运行,程序就找不到词库文件了
现象:在 IDE 里直接运行一切正常,但双击 exe 或者把项目文件夹拷到别的路径后,程序报 FileNotFoundException,词库像被清空了一样。
原因:File.ReadAllText("words.json") 用的是相对路径,相对的是“当前工作目录”,而不是“exe 所在的目录”。控制台程序从不同方式启动时工作目录可能完全不同。这个问题几乎是所有 C# 初学项目的第一个拦路虎。
解决:使用 AppContext.BaseDirectory 定位数据目录,这个属性返回的是程序集所在路径,无论从哪里启动都指向同一个位置:
string dataDir = Path.Combine(AppContext.BaseDirectory, "Data"); Directory.CreateDirectory(dataDir); string filePath = Path.Combine(dataDir, "words.json");顺便说一句,Directory.CreateDirectory 在目录已存在时不会报错,只会静默返回,所以可以放心直接调用。
5.2 中文释义在控制台显示乱码,保存到文件打开也是乱码
现象:单词释义里明明写的是“放弃”,控制台输出却变成“鏀惧純”之类的一串乱码,用记事本打开 words.json 同样乱码。
原因:控制台窗口和文件读写默认使用的代码页不一致。控制台可能用 GBK 或 ASCII,而 JSON 文件是 UTF-8 编码,两边对接不上就显示乱码。File.WriteAllText 如果不指定编码,在某些环境下也不会默认带上 UTF-8。
解决:程序入口处强制设置控制台输出编码为 UTF-8,文件读写统一指定 Encoding.UTF8:
Console.OutputEncoding = System.Text.Encoding.UTF8;同时把仓储类里所有 ReadAllText 和 WriteAllText 都改成带 Encoding.UTF8 参数的重载。这两步做完,中文显示和保存就一致了。
5.3 背过的单词第二天又冒出来,感觉总在同一批词里打转
现象:昨天已经答对的词,今天启动程序后还在队列里,而且复习列表几乎天天一样。
原因:最常见的有两种。第一种是答对时没有更新 NextReviewTime,导致这个词永远处于“到期”状态被捞进今日队列;第二种是加载时给没有复习状态的单词初始化 NextReviewTime 用了 DateTime.MinValue,MinValue 肯定小于今天,所以所有新词全部被当成到期词。
解决:在 BuildDailyQueue 里,对拿不到复习状态的单词默认按“新词”处理,强制走 fresh 列表而不是到期列表。加载旧数据时如果发现 NextReviewTime 是默认值,说明这是不完整的数据,应当修正为今天而不是继续用 MinValue:
state.NextReviewTime = DateTime.Today;这个修正在加载逻辑里加一次校验即可,别指望用户会主动去改 JSON 文件。
5.4 循环里把单词重新入队,抛出了 InvalidOperationException
现象:答错一道题后程序直接异常退出,异常信息提示集合已修改,无法执行该操作。
原因:你是不是用 foreach 遍历一个 List,然后在循环体里又向这个 List 添加单词或者从中移除单词了?List 不允许在遍历过程中修改自身结构。初学者很容易写出类似foreach (var word in wordList)然后wordList.Add(word)的代码,一跑必然翻车。
解决:改用 Queue 作为会话队列。Queue 的 Dequeue 和 Enqueue 是本类自己的操作,不违反“遍历时不允许修改”的规则。如果你确实需要一个能修改的 List 遍历,也要先复制一份快照再遍历副本:
foreach (var word in wordList.ToList()) { // 这里可以安全地修改 wordList }ToList 会生成一个新的 List,遍历的是快照,修改的是原列表,两者互不干扰。几百个单词的列表性能开销可以忽略不计。
5.5 序列化时堆栈溢出,程序直接崩溃
现象:调用 SaveAll 保存状态时抛出 StackOverflowException,程序毫无征兆地退出,连异常信息都看不到。
原因:StackOverflowException 通常由无限递归引起。典型场景是你在 Word 类里加了一个 WordReviewState 属性,同时在 WordReviewState 里又加了一个 Word 属性,形成word.State.Review.Word的循环引用。System.Text.Json 序列化循环引用时默认不支持,会一直递归下去直到堆栈爆炸。
解决:让复习状态类只存 WordId,不存 Word 对象引用,查询关联时用_words.FirstOrDefault(w => w.Id == state.WordId)。如果确实需要导航属性,就给其中一个方向的属性加上 [JsonIgnore]:
[JsonIgnore] public Word? Word { get; set; }我建议初学者采用第一种方式,因为“用 Id 关联”是理解和实践数据库主外键概念的前置练习,对后续学习更重要。
6. 进阶玩法与性能优化:把一个能跑的背单词程序变成“自己的作品”
控制台版本跑通之后,有几处值得继续打磨。首先把 4.2 节里 NewWordLimit、ReviewLimit 和复习间隔数组这些魔法数抽成配置文件。建立 config.json,启动时加载,这样你不想重新编译代码也能调整学习强度。这个改动虽然小,却是从“写给自己玩”到“做成一个工具”的分水岭。
其次给程序加一个简单统计界面:正确率走势、最容易背错的单词排行、最近七天的学习时长。实现时不需要引入任何图表库,直接用控制台输出字符条形图,比如用new string('#', percent / 10)画一根进度条,数据从 CorrectCount 和 WrongCount 里聚合。这个功能会让用户直观看到“这个程序真的在帮我记忆”,而不是每天机械地答一百道题。
如果你有余力,可以把控制台交互层换成 WinForms 或 WPF。你会发现服务层几乎不用改,只需要把Console.ReadLine()替换成文本框输入,把Console.WriteLine替换成界面控件的更新,再把主循环改成事件驱动。这个迁移过程比从零画界面要轻松得多,因为业务逻辑已经和界面分离了。我当初学 C# 时就是把一个模拟项目 X 的控制台版本改造成带界面的版本,才真正理解了“界面只是外层壳”这句话的含义。
词库来源也可以扩展。除了 CSV 导入,从公开的词频表或本地词书里取数,做成批量导入工具。导入时建议先用 Spelling 去重并输出重复清单,让用户自己决定保留哪一条释义。再到后面,你可以把 JSON 换成 SQLite,用数据库的索引代替全量遍历,这一步才是真正的性能优化场景——但前提是你的词库数量真的到了几万、几十万条才需要,几千条词用 JSON 完全没问题。
我个人的习惯是:学 C# 最怕的就是看完十个教程,却没有一个能在自己电脑上跑起来改一改的项目。这类的背单词源代码恰好是一个能跑、能改、能扩展的训练场,跑通它、修改它、最后把它变成你自己的工具,性价比非常高。希望帮到你。
本文还有配套的精品资源,点击获取