第一次学习 C#,真正需要建立的不是一张语法清单,而是一条能反复走通的路径:写代码 → 构建 → 运行 → 观察 → 修正。本章会用一个“学习计划统计器”逐步串起 .NET SDK、控制台项目、顶级语句、类型与变量、输入解析、分支、循环、数组和 List<T>。
完成后,你将能够独立创建并运行控制台项目,读懂最小 C# 程序,安全处理用户输入,并用基础控制流和集合完成一个小型命令行应用。
建议边读边敲。代码能运行只是第一步;请刻意更换输入、预测输出,再验证自己的判断。

本章分成三个递进阶段:
不要一开始就追求“记住全部”。更可靠的方法是每引入一个概念,就完成一次小闭环:写最少代码、运行、改变一个值、解释新输出。
这三个名字经常一起出现,但职责不同:
安装 SDK 后,在终端执行:
dotnet --version
dotnet --info第一个命令快速确认版本,第二个命令会显示已安装的 SDK、运行时和环境信息。只要 dotnet --version 能输出版本号,便具备创建基础项目的前提。
C# 源代码不会被处理器直接执行。开发工具读取源码与项目配置,构建出程序;运行时再负责加载并执行它。基础类库则提供 Console、Math、文件与集合等现成能力,让我们不必从零制造常用工具。
控制台项目没有复杂界面,最适合练习语言基础。执行:
dotnet new console -n StudyPlanner
cd StudyPlanner
dotnet rundotnet new console 从模板生成项目,-n StudyPlanner 同时指定项目名和目录名。dotnet run 会在需要时先构建,再启动程序。
典型目录如下:
StudyPlanner/
├── StudyPlanner.csproj
├── Program.cs
├── bin/
└── obj/Program.cs:当前示例的入口逻辑。StudyPlanner.csproj:目标框架与编译选项等项目配置。obj/:构建过程的中间文件。bin/:构建产物。开发时通常不手动修改 bin/ 和 obj/。把“运行”和“只构建”区分开很重要:
dotnet build
dotnet runbuild 只验证并产生构建结果;run 面向开发迭代,构建后还会运行程序。

打开 Program.cs,把内容改为:
Console.WriteLine("Hello, C#!");短短一行包含多个基本概念:
"Hello, C#!" 是字符串字面量,也就是直接写在代码中的文本值。Console 是处理控制台输入输出的类。. 是成员访问运算符。WriteLine(...) 是方法调用,括号中的文本是传入方法的参数。现代控制台模板允许直接在文件中写顶级语句。虽然看不到 class Program 和 static void Main,编译器仍会生成程序入口点。等价的传统结构可以理解为:
class Program
{
static void Main()
{
Console.WriteLine("Hello, C#!");
}
}顶级语句只是减少入门和小程序的样板代码,不是取消类、方法或入口点。语句默认按出现顺序执行:
Console.WriteLine("1. 读取计划");
Console.WriteLine("2. 计算进度");
Console.WriteLine("3. 输出结果");1. 读取计划
2. 计算进度
3. 输出结果变量可以看成带标签的存储位置,拥有类型、名字和值。类型决定它能保存什么值,以及允许参与哪些操作。
string topic = "C# 基础";
int plannedMinutes = 90;
double progress = 0.625;
decimal budget = 19.90m;
char level = 'A';
bool completed = false;单引号表示 char,双引号表示 string。带小数点的数字默认是 double;decimal 字面量要加 m。
表达式是“计算后产生一个值”的代码。下面右侧的加法、乘法和插值内容都是表达式:
int breakMinutes = 10;
int totalMinutes = plannedMinutes + breakMinutes;
double learnedMinutes = plannedMinutes * progress;
Console.WriteLine($"主题:{topic}");
Console.WriteLine($"已学习:{learnedMinutes:F1} 分钟");
Console.WriteLine($"完成率:{progress主题:C# 基础
已学习:56.2 分钟
完成率:62.5%var 可以省略重复的类型名,但类型仍是编译期确定的:
var message = "开始学习"; // 推断为 string
// message = 3; // 编译错误:不能把 int 放入 string 变量命名优先表达意图。局部变量通常采用 camelCase:plannedMinutes 比 pm 或 data1 更容易维护。
基本算术运算符包括 +、-、*、/ 和 %。乘、除、余先于加减;不确定时用括号直接表达意图:
double focusMinutes = (45 + 35) * 0.75;
Console.WriteLine(focusMinutes); // 60整数除法是最常见的入门陷阱:
int completedTasks = 5;
int allTasks = 8;
double wrongRate = completedTasks / allTasks;
double rightRate = (double)completedTasks / allTasks;
Console.WriteLine(wrongRate); // 0
Console.WriteLine(rightRate); // 0.625第一行先执行整数除法,5 / 8 的小数部分被丢弃,得到 0 后才放进 double。第二行先把一个操作数显式转换为 double,因此使用浮点除法。
转换时先问“会不会丢信息”:
int minutes = 90;
double preciseMinutes = minutes; // 扩大范围,隐式转换
double rawScore = 92.8;
int wholeScore = (int)rawScore; // 显式转换,结果 92,不是四舍五入更新变量时可用复合赋值:
int total = 10;
total += 5; // 等价于 total = total + 5;
total++;% 返回余数,常用于奇偶判断:number % 2 == 0 表示偶数。除数可能为零时,要在运算前验证,不能把异常或无穷值当成正常业务结果。
Console.ReadLine() 返回用户输入的一整行文本。即使用户输入 120,程序最初拿到的仍是字符串;并且输入可能为空、缺失或格式错误。
Console.Write("计划学习多少分钟? ");
string? rawInput = Console.ReadLine();
if (int.TryParse(rawInput, out int minutes) && minutes > 0)
{
Console.WriteLine($"已记录:{minutes} 分钟");
}
else
{
Console.WriteLine
TryParse 同时提供两条信息:返回的 bool 表示是否成功,out 变量保存解析结果。与直接 int.Parse 相比,它让“输入不合法”成为可预期分支,而不是让程序突然中断。
字符串还有三种常用写法:
string escaped = "第一行\n第二行";
string path = @"C:\study\csharp";
string summary = $"计划:{minutes,4} 分钟";@ 逐字字符串适合路径等包含大量反斜杠的文本。$ 插值字符串把表达式结果嵌入文本;逗号可对齐,冒号后可指定格式。
条件表达式产生 bool。if/else 适合范围、组合条件与不对称处理:
int score = 86;
string grade;
if (score >= 90)
{
grade = "A";
}
else if (score >= 80)
{
grade = "B";
}
else if (score >= 60)
{
grade = "C"
顺序很关键:范围通常从最严格到最宽松,否则前面的宽条件会提前截走后续路径。
当输入是一组离散选项时,switch 更清楚:
Console.Write("模式(study/review/rest):");
string mode = Console.ReadLine()?.Trim().ToLowerInvariant() ?? "";
string action = mode switch
{
"study" => "学习新知识",
"review" => "复习错题",
"rest" => "休息十分钟",
_ => "未知模式"
_ 是兜底分支。只有两个简短结果时可以用三元表达式,但嵌套三元表达式往往会降低可读性。

选择循环时,先看“重复何时停止”:
for。while。foreach。for 把初始化、继续条件和状态变化集中在一行:
for (int day = 1; day <= 3; day++)
{
Console.WriteLine($"第 {day} 天:完成 30 分钟");
}while 适合输入验证:
int minutes;
while (true)
{
Console.Write("输入 1~180 分钟:");
if (int.TryParse(Console.ReadLine(), out minutes) && minutes is >= 1 and <= 180)
{
break;
}
Console.WriteLine("输入无效,请重试。");
foreach 直接表达“处理每一项”:
string[] topics = ["类型", "分支", "循环"];
foreach (string topic in topics)
{
Console.WriteLine($"待学习:{topic}");
}每个循环都要能回答三个问题:初始状态是什么、什么条件下继续、每轮如何靠近结束。遗漏第三点通常会制造无限循环。
数组适合元素数量在创建后保持不变的场景:
int[] dailyMinutes = [35, 40, 25];
Console.WriteLine(dailyMinutes[0]); // 35
Console.WriteLine(dailyMinutes.Length); // 3索引从 0 开始,所以最后一个合法索引是 Length - 1。访问 dailyMinutes[3] 会越界。
List<T> 适合需要动态增删的列表:
List<string> tasks = ["安装 SDK", "创建项目"];
tasks.Add("完成输入练习");
tasks.Remove("安装 SDK");
Console.WriteLine(tasks.Count);
foreach (string task in tasks)
{
Console.WriteLine($"- {task}");
}T 表示元素类型。List<string> 只能存放字符串,这让错误更早暴露给编译器。
数组和列表的常见对照:

一个可靠的最小反馈闭环是:
修改一个小点 → dotnet build → 读第一条错误/警告 → dotnet run
→ 用边界输入验证 → 对比预期 → 再修改编译错误表示当前代码无法构建。优先处理第一条错误,因为后续错误可能只是它的连锁反应。警告表示编译器仍能构建,但发现潜在风险;例如 ReadLine 可能返回 null,不应仅为“看起来干净”而忽略。
程序能运行但结果不符合预期时:
常见陷阱速查:
= 当成比较;相等比较使用 ==。var 是动态类型。Parse 不可信输入,没有失败分支。Length 当成最后一个索引。while 中忘记更新状态,无法结束。下面的程序把本章积木组合起来。它读取每日分钟数,拒绝无效输入,保存到列表,最后计算总时长、平均值和评价:
Console.WriteLine("=== 学习计划统计器 ===");
List<int> dailyMinutes = [];
while (dailyMinutes.Count < 3)
{
Console.Write($"第 {dailyMinutes.Count + 1} 天分钟数(1~180):");
string? input = Console.ReadLine();
if (int
一次可能的运行:
=== 学习计划统计器 ===
第 1 天分钟数(1~180):45
第 2 天分钟数(1~180):abc
输入无效,请重试。
第 2 天分钟数(1~180):60
第 3 天分钟数(1~180):30
总时长:135 分钟
日均:45.0 分钟
建议:节奏稳定,继续保持继续扩展时,可以把固定 3 天改为由用户输入,把最高/最低值算出来,或允许输入 quit 提前结束。每次只增加一个需求,并保留无效输入和边界测试。
现在你已经拥有一个可重复的 C# 最小开发闭环:SDK 提供工具,项目文件描述构建方式,顶级语句形成入口逻辑;类型和变量保存数据,表达式产生新值,TryParse 把不可信文本变成可验证输入;if/switch 选择路径,for/while/foreach 重复动作,数组和 List<T> 管理同类型数据。
下一步不必立刻追求更多语法。先独立重写一次“学习计划统计器”,至少测试:空输入、非数字、0、181、三个有效值,以及能产生小数的平均值。只要你能解释每条路径为什么发生,本章目标就真正完成了。