第一个 C# 应用

要点

从 dotnet new console 到一个真正有用的命令行小工具,只需要一个 Program.cs。现代 C# 的入口是顶级语句(top-level statements)——没有类、没有 Main 的裸代码,但 Main 一直都在,只是编译器替你写。理解这层语法糖与入口的关系,是从 Java 转过来最值得先搞清楚的一件事。

创建工程

dotnet new console -n wordcount
cd wordcount
dotnet run

三秒后输出 Hello, World!。目录里只有三个值得看的东西:

文件/目录 是什么
Program.cs 入口源码,默认就两行顶级语句
wordcount.csproj 项目文件,SDK 风格,日常几乎不用手改
obj/ 构建中间产物(restore 结果、自动生成的 using),不要提交

没有 src/、没有包结构、没有清单文件——C# 的文件名与类型名不需要对应(这点与 Java 完全不同),一个项目里 .cs 文件怎么组织全凭团队约定。

核心概念:顶级语句与 Main 的关系

默认模板里的 Program.cs 长这样:

// 一行 using 都没有:ImplicitUsings 把常用命名空间隐式引入了
Console.WriteLine("Hello, World!");

编译器把它翻译成一个合成的 Program.Main(string[] args)——入口从未消失,只是不必手写。与 Java 的三种写法对照:

写法 C# Java
经典入口 class Program { static void Main(string[] args) { … } } public class Hello { public static void main(String[] args) { … } }
免类声明入口 顶级语句(C# 9 起,模板默认) 紧凑源文件 void main()(Java 25 起)
入口取参数 顶级语句里直接用 args 标识符 main(String[] args) 声明参数

两条规矩要记:一个项目只能有一个文件用顶级语句(它就是入口);顶级语句必须是该文件最前面的语句,类型声明只能排在它后面。反模式:在同一个项目里既写顶级语句又写显式 Main——编译器直接报 CS8805(顶级语句与显式入口点不能共存),网上不少旧教程混着抄,一抄就中。

第一个应用:词频统计 CLI

目标:读一个文本文件,输出出现次数最多的前 N 个单词。功能小,但覆盖了真实 CLI 的全部要素——参数解析、文件 IO、错误处理、输出格式。完整代码,可直接替换 Program.cs:

// Program.cs —— 顶级语句就是入口,args 由编译器注入的 Main 传入
// 用法: dotnet run -- <文件> [前N,默认10]

if (args.Length < 1)
{
    Console.Error.WriteLine("用法: wordcount <文件> [前N]");
    return 1;                       // 顶级语句最后的 return 值 = 进程退出码
}

var path = args[0];
var top = args.Length > 1 && int.TryParse(args[1], out var n) ? n : 10;

if (!File.Exists(path))
{
    Console.Error.WriteLine($"文件不存在: {path}");
    return 2;
}

var text = await File.ReadAllTextAsync(path);   // IO 是异步的,入口天然支持 await

var topWords = text
    .Split([' ', '\n', '\r', '\t'], StringSplitOptions.RemoveEmptyEntries)
    // Trim 不认标点,这里按字符过滤:只保留字母数字,"dog." 和 "dog" 才会算同一个词
    .Select(w => new string(w.Where(char.IsLetterOrDigit).ToArray()))
    .Where(w => w.Length > 0)
    .GroupBy(w => w, StringComparer.OrdinalIgnoreCase)
    .Select(g => (Word: g.Key, Count: g.Count()))   // 投影成元组,下游直接点出属性
    .OrderByDescending(x => x.Count)
    .ThenBy(x => x.Word, StringComparer.OrdinalIgnoreCase)
    .Take(top);

foreach (var (word, count) in topWords)       // 元组解构
    Console.WriteLine($"{count,5}  {word}");  // 字符串内插,,5 表示右对齐宽度

return 0;

这几行代码里已经出现了 LINQ 方法链、元组投影与解构、字符串内插、out var 模式——都是下一篇语法篇的主角,这里先混个脸熟。

跑起来:

# 造个测试文件
printf 'the quick brown fox the lazy dog the end\n' > sample.txt

dotnet run -- sample.txt 3
1. 输出:
1.     3  the
1.     1  brown
1.     1  dog

注意 --:它之后的内容才传给程序,之前的留给 dotnet 自己(比如 dotnet run -c Release)。这是 CLI 惯例,与 npm run 的 -- 相同。

退出码与返回值

顶级语句里最后的 return 整数 映射为进程退出码——shell 脚本据此判断成败。惯例:0 成功,非零按错误类别区分(上面用 1 表示用法错误、2 表示文件不存在)。在 IDE 里调试时给参数,靠 Properties/launchSettings.json 的 commandArgs 字段,或直接终端 dotnet run。

发布成可执行文件

dotnet publish -c Release -o publish

1. 三种产物口径
1. 1. 框架依赖(默认): publish/ 里的 dll 需要目标机器装有 .NET 10 运行时,产物小
1. 2. 自包含单文件: 加 --self-contained -p:PublishSingleFile=true,免装运行时,一个可执行文件
1. 3. Native AOT: 加 -p:PublishAot=true,编成原生机器码,启动最快、体积最小,限制最多

选型一句话:公司内机器默认有运行时就用框架依赖;发给用户的小工具用自包含单文件;追求极致启动且依赖简单再考虑 AOT。跨平台注意:自包含产物按发布时的操作系统架构生成,给 Linux 服务器发布要加 -r linux-x64。

最佳实践

  • 入口薄,逻辑厚:顶级语句只做参数解析与顶层编排,超过五十行就拆出静态类或服务类
  • 错误输出走 Console.Error,退出码区分错误类别——这是 CLI 与管道协作的礼貌
  • 文件 IO 一律用 Async 版本(File.ReadAllTextAsync 而非 ReadAllText),CLI 也不嫌小
  • 正式工程把 obj/、bin/ 进 .gitignore,csproj 提交

常见问题

CS8805:顶级语句与显式 Main 同时存在

删掉一个。保留顶级语句(推荐,模板默认),或者全项目走 class + Main 风格(老团队规范可能如此),别混。

CS5001:未找到入口点

项目里没有任何顶级语句文件,也没有合法 Main。常见原因:把 Main 写成了小写 main(C# 大小写敏感),或者入口文件被排除出编译(csproj 的 Compile Remove)。

改了代码没生效

dotnet run 每次都重新构建,不存在旧产物问题;但如果你直接运行 bin/ 下的旧 dll,那就是在跑上次编译的结果。开发期统一用 dotnet run 或 dotnet watch run。

练习与实践

  • 练习一:给 wordcount 加 -i 参数控制是否忽略大小写,体会 args 解析膨胀后为什么该引入 System.CommandLine 或 Spectre.Console.Cli 这类库
  • 练习二:用自包含单文件发布,把产物拷到一台没装 .NET 的机器上跑通,对比产物大小与框架依赖版的差异
  • 练习三:把 GroupBy 换成手动遍历 Dictionary 累加计数,体会 LINQ 与手写命令式代码的取舍

延伸阅读

环境没搭好的先回《C# 与 .NET 环境搭建与工具链》;Java 背景的读者推荐同场对照《第一个 Java 程序》——那篇讲了 Java 入口的三次进化(经典 main、单文件直跑、Java 25 紧凑源文件),和本篇的顶级语句是一对镜子。下一篇《C# 语法要点》把本文代码里一闪而过的 LINQ、元组、可空类型逐个讲透。

版本核实(以官方文档为准):顶级语句自 C# 9 / .NET 5 起可用,.NET 10 控制台模板默认启用,对应语言版本 C# 14;Native AOT 与自包含单文件发布为当前 SDK 稳定能力;Java 25 紧凑源文件用于对照(其 GA 为 2025-09)。文件式应用(dotnet run 单文件)为 .NET 10 新增,本文从项目起步、未依赖它。具体以官方文档为准。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇