如果你刚开始系统学习 .NET 8,或者已经会写一些 C#,但总觉得自己的代码还是"老味道"很重,那么这篇文章就是写给你的。
很多人学 .NET 时,容易把注意力放在 Web API、EF Core、依赖注入这些框架层内容上,却忽略了一件更基础也更重要的事:现代 C# 语法本身,已经足够让你的代码变得更简洁、更安全、更易维护。
这篇文章重点整理几组在 .NET 8 开发中非常常用、也非常值得掌握的现代 C# 特性:
- 文件作用域命名空间
var的合理使用- 可空引用类型
string? recordinitrequired- 模式匹配
- 新版
switch
文章最后我还准备了一个实战练习:把一个旧风格的 Model / DTO 改写成现代风格。
1. 先说清楚:这到底是 .NET 8 特性,还是 C# 特性?
这是很多初学者容易混淆的地方。
严格来说:
.NET 8是运行时和开发平台C#是语言- 我们这篇文章讲的大多数内容,本质上是现代 C# 特性
只是因为你在 .NET 8 项目里通常会直接使用到这些新语法,所以很多人会把它们统称为".NET 8 常用特性"。
你可以简单理解为:
学 .NET 8,不只是学框架,更要学会用现代 C# 写代码。
2. 文件作用域命名空间:少一层缩进,代码更清爽
2.1 旧写法
csharp
namespace DemoApp.Models
{
public class UserDto
{
public string Name { get; set; }
}
}
这种写法在早期 C# 项目里很常见,但有一个明显问题:整个文件都要多缩进一层。
2.2 现代写法
csharp
namespace DemoApp.Models;
public class UserDto
{
public string Name { get; set; } = string.Empty;
}
这就是文件作用域命名空间。
2.3 它的好处
- 少一层花括号
- 少一层整体缩进
- 文件更清爽
- 在 DTO、Model、Service 这类文件里尤其舒服
2.4 使用建议
在新项目里,只要一个 .cs 文件只定义一个命名空间,就优先使用文件作用域命名空间。
这是现代 C# 项目里非常推荐的默认写法。
3. var 的合理使用:不是"能用就全用"
很多初学者对 var 有两个极端:
- 一种是完全不用,觉得不明确
- 另一种是到处都用,导致代码可读性下降
正确的思路是:var 要在"右边已经很清楚"的情况下使用。
3.1 适合用 var 的场景
csharp
var name = "Alice";
var count = 10;
var user = new UserDto();
var numbers = new List<int>();
这些例子里,右边一眼就能看出类型:
"Alice"明显是string10明显是intnew UserDto()明显是UserDtonew List<int>()明显是List<int>
这时使用 var 可以减少重复,让代码更简洁。
3.2 不太适合用 var 的场景
csharp
var result = GetData();
var value = service.Execute();
var item = repository.Find(id);
这些写法的问题是:右边方法返回什么类型,不看定义根本不知道。
如果类型信息对理解代码很重要,那就应该写清楚:
csharp
UserDto result = GetData();
OrderResponse value = service.Execute();
Customer item = repository.Find(id);
3.3 一句话原则
当右值已经把类型表达得很清楚时,用
var;当类型不明显且会影响阅读时,写出明确类型。
3.4 一个实际对比例子
csharp
var user = new UserDto();
var orders = new List<OrderDto>();
这很好读。
但下面这样就一般:
csharp
var data = await service.GetAsync();
var response = mapper.Map(source);
如果你写的是业务核心逻辑,类型不清楚会影响阅读,建议显式声明。
4. 可空引用类型:string? 不是语法装饰,而是质量提升
4.1 为什么需要可空引用类型
在旧项目里,下面这种代码非常常见:
csharp
public string Name { get; set; }
表面上看 Name 是字符串,但实际上它完全有可能是 null。这就容易埋下空引用异常风险。
现代 C# 引入了可空引用类型概念,用来明确表达:
- 这个引用类型是否允许为
null - 编译器是否应该帮你检查潜在空引用问题
4.2 基本写法
csharp
public string Name { get; set; } = string.Empty;
public string? NickName { get; set; }
含义是:
Name:不应该为nullNickName:允许为null
4.3 为什么很重要
这不是单纯为了"写个问号好看",而是为了让代码语义更明确。
比如用户昵称、备注、头像地址,这些字段天然可能为空;而用户名、邮箱、订单号这些字段,通常应该非空。
4.4 示例
csharp
public class UserProfileDto
{
public string UserName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string? AvatarUrl { get; set; }
public string? Remark { get; set; }
}
4.5 使用建议
在 .NET 8 新项目里,尽量养成下面这个习惯:
- 能确定必填的引用类型,不要写成可空
- 真正允许为空的字段,再写
? - 非空字符串尽量给默认值,例如
string.Empty
5. record:更适合表达"数据对象"
5.1 class 不一定错,但 record 更贴近 DTO
很多 Model、DTO、Response 对象,本质上是"数据承载体"。
这种对象通常:
- 主要用来传数据
- 不强调复杂行为
- 更关注值是否相等,而不是引用是否相等
这类场景下,record 往往比 class 更合适。
5.2 基本写法
csharp
public record UserDto(string UserName, string Email);
这是一种非常简洁的写法,叫做位置 record。
5.3 它的优点
- 写法简洁
- 很适合 DTO、查询结果、响应对象
- 默认更偏向值比较语义
- 和不可变风格搭配很好
5.4 常见写法二:属性式 record
csharp
public record UserDto
{
public required string UserName { get; init; }
public required string Email { get; init; }
public string? AvatarUrl { get; init; }
}
这种写法比位置 record 更适合教程和业务项目,因为:
- 可读性更强
- 属性名更清楚
- 更容易加注释、特性、校验标记
5.5 什么时候优先考虑 record
适合:
- DTO
- ViewModel
- 查询结果对象
- API Request / Response
- 不强调可变状态的数据模型
不一定适合:
- 复杂领域实体
- 有大量行为和状态变更逻辑的对象
- 强依赖引用语义的对象
6. init:对象初始化后就不希望再随便改
6.1 旧写法
csharp
public class UserDto
{
public string Name { get; set; } = string.Empty;
}
这样写虽然方便,但也意味着对象创建后,任何地方都能改:
csharp
user.Name = "Tom";
user.Name = "Jerry";
如果某个 DTO 的值应该在创建时确定,后面不应随便改,那就适合用 init。
6.2 现代写法
csharp
public class UserDto
{
public string Name { get; init; } = string.Empty;
}
这时你仍然可以在初始化时赋值:
csharp
var user = new UserDto
{
Name = "Alice"
};
但初始化完成之后,就不能再改了。
6.3 适用场景
init 非常适合:
- DTO
- 配置对象
- 查询结果对象
- 响应模型
也就是说:先赋值,后只读。
这比 set 更安全,也更符合很多数据对象的真实语义。
7. required:必填属性要让编译器帮你兜底
7.1 问题场景
即使你写了非空属性:
csharp
public string UserName { get; init; } = string.Empty;
也不代表调用方一定会认真赋值。有时候他可能忘了填,而 string.Empty 又会把问题掩盖掉。
如果某个字段从业务上就是"创建对象时必须给",那就可以使用 required。
7.2 基本写法
csharp
public class CreateUserRequest
{
public required string UserName { get; init; }
public required string Email { get; init; }
public string? Remark { get; init; }
}
7.3 使用效果
当你这样写时:
csharp
var request = new CreateUserRequest
{
UserName = "alice"
};
编译器就会提醒你:Email 没有赋值。
7.4 为什么它很实用
required 最大的价值,是把"业务上的必填"前移到了编译期。
这能减少:
- 忘记赋值
- 构造出无效对象
- 运行时才发现参数不完整
7.5 与 init 搭配是常见组合
最常见、最现代的 DTO 写法之一就是:
csharp
public record CreateUserRequest
{
public required string UserName { get; init; }
public required string Email { get; init; }
public string? Remark { get; init; }
}
这套组合很值得背下来。
8. 模式匹配:让判断逻辑更清晰
模式匹配是现代 C# 非常值得掌握的一组能力。它能让很多 if/else 和类型判断写得更优雅。
8.1 最基础的类型匹配
csharp
if (obj is string text)
{
Console.WriteLine(text.Length);
}
这句代码的意思是:
- 如果
obj是string - 就把它作为
text使用
比旧写法更简洁。
8.2 空值判断
csharp
if (user is null)
{
return;
}
这比 user == null 更符合现代 C# 风格。
8.3 属性模式
csharp
if (request is { UserName: "admin", Email: not null })
{
Console.WriteLine("管理员请求");
}
这个例子表示:
request不为空UserName等于adminEmail不为空
8.4 范围模式
csharp
if (score is >= 90 and <= 100)
{
Console.WriteLine("优秀");
}
这种写法很适合分数区间、金额区间、年龄区间等判断。
8.5 列表模式的感知
在现代 C# 里,模式匹配已经不只是"判断类型",而是一种更强的结构化判断方式。
你可以先把重点放在下面几类:
is nullis not null- 类型模式
- 属性模式
- 范围模式
把这些掌握住,日常开发就已经非常够用了。
9. 新版 switch:不只是语句,更是表达式
9.1 旧风格 switch
csharp
switch (status)
{
case 0:
return "待支付";
case 1:
return "已支付";
case 2:
return "已取消";
default:
return "未知状态";
}
这当然可以用,但现代 C# 更推荐 switch 表达式。
9.2 新版 switch 表达式
csharp
return status switch
{
0 => "待支付",
1 => "已支付",
2 => "已取消",
_ => "未知状态"
};
优势很明显:
- 更简洁
- 更像"值映射"
- 可读性更好
9.3 结合模式匹配使用
csharp
public static string GetUserLevel(int score) => score switch
{
>= 90 => "优秀",
>= 80 => "良好",
>= 60 => "及格",
_ => "不及格"
};
这就是现代 C# 很典型的写法。
9.4 对象模式 + switch
csharp
public static string GetOrderDescription(OrderDto order) => order switch
{
{ Amount: <= 0 } => "无效订单",
{ IsPaid: true } => "已支付订单",
{ IsPaid: false } => "待支付订单",
_ => "未知订单"
};
这类写法在业务分类、状态转换、规则判断中非常实用。
10. 一组推荐的现代 DTO 写法
如果你问我:在 .NET 8 项目里,最值得形成肌肉记忆的 DTO 写法是什么?
我会推荐下面这种风格:
csharp
namespace DemoApp.Contracts;
public record CreateOrderRequest
{
public required string OrderNo { get; init; }
public required string CustomerName { get; init; }
public decimal Amount { get; init; }
public string? Remark { get; init; }
}
这段代码同时体现了:
- 文件作用域命名空间
recordrequiredinit- 可空引用类型
如果你能把这一套写法真正用熟,那么你的 C# 代码风格会一下子现代很多。
11. 实战练习:把旧的 Model / DTO 改写成现代风格
下面开始做你最需要的练习。
11.1 旧代码
csharp
namespace DemoApp.Models
{
public class UserModel
{
public string UserName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public int Age { get; set; }
public bool IsAdmin { get; set; }
}
}
这段代码的典型问题有:
- 使用了旧式命名空间写法
- 所有字符串都可能空引用,但没表达出来
- 所有属性都可随意修改
- 没体现哪些字段是必填
- 只是数据模型,更适合用
record
11.2 你的练习目标
请把它改造成现代风格,并尽量体现这些点:
- 文件作用域命名空间
recordrequiredinit- 合理使用
string?
你可以先自己动手写一遍,再看下面的参考答案。
12. 练习参考答案
12.1 改写后的版本
csharp
namespace DemoApp.Models;
public record UserModel
{
public required string UserName { get; init; }
public required string Email { get; init; }
public string? Phone { get; init; }
public string? Address { get; init; }
public int Age { get; init; }
public bool IsAdmin { get; init; }
}
12.2 为什么这样改
逐条解释一下:
-
namespace DemoApp.Models;- 使用文件作用域命名空间,让文件更简洁
-
public record UserModel- 它本质是数据对象,
record比普通class更适合
- 它本质是数据对象,
-
required string UserName- 用户名通常是必填项,创建对象时必须赋值
-
required string Email- 邮箱通常也是必填项
-
string? Phone- 电话可能没有,所以允许为空
-
string? Address- 地址也可能没有,所以允许为空
-
init- 这些属性更适合在初始化时赋值,之后保持不变
12.3 使用示例
csharp
var user = new UserModel
{
UserName = "alice",
Email = "alice@example.com",
Age = 25,
IsAdmin = false
};
这种写法的好处是:
- 必填项一眼明确
- 非必填项语义清晰
- 对象创建后不会被随意篡改
- 更适合 DTO / API / 查询结果场景
13. 再升级一步:给练习加上新版 switch 和模式匹配
如果你想继续练手,可以基于上面的 UserModel 再加一个方法,判断用户类型。
13.1 示例代码
csharp
public static string GetUserCategory(UserModel user) => user switch
{
{ IsAdmin: true } => "管理员",
{ Age: < 18 } => "未成年用户",
{ Age: >= 60 } => "老年用户",
{ Age: >= 18 and < 60 } => "普通成年用户",
_ => "未知用户"
};
这里同时用到了:
switch表达式- 属性模式
- 范围模式
这就是现代 C# 判断逻辑非常典型的一种写法。
14. 学习建议:这几个特性该按什么顺序掌握
如果你准备系统学习,建议按这个顺序来:
- 先掌握文件作用域命名空间
- 再学
var的合理使用 - 再理解可空引用类型
string? - 接着学
init和required - 然后学习
record - 最后重点练模式匹配和新版
switch
原因很简单:
- 前面几个特性偏"代码风格升级"
- 后面几个特性偏"表达能力升级"
先把代码写整洁,再把判断逻辑写优雅,学习节奏会更顺。
15. 一句话总结每个重点
为了方便你复习,我把这几个点再压缩成一句话:
- 文件作用域命名空间:少一层缩进,让文件更干净
var:右值类型明显时用,不明显时别滥用string?:明确谁能为空,减少空引用风险record:更适合 DTO、响应对象、数据模型init:只允许初始化时赋值,更安全required:让必填项在编译期就暴露问题- 模式匹配:让判断更结构化
- 新版
switch:让分支逻辑更简洁、更现代
16. 总结
如果你正在学 .NET 8,那么一定不要只盯着 Web API、EF Core 和各种框架配置。
现代 C# 本身,就是你写出高质量 .NET 代码的基础。
这篇文章讲的内容,看起来都是语法点,但它们背后解决的其实是几个很现实的问题:
- 代码能不能更简洁
- 空引用能不能少一点
- DTO 能不能更安全
- 分支逻辑能不能更清楚
- 项目代码风格能不能更现代
只要你把这几个点练熟,你写出来的代码会明显比传统写法更像一个现代 .NET 项目。
如果你准备把这篇内容发到 CSDN,我建议你可以这样排版:
- 开头先写"为什么 .NET 8 项目要学现代 C#"
- 中间每个知识点配一个小代码示例
- 练习题单独成节
- 参考答案放在后面
- 最后再补一段自己的学习总结
这样的文章很适合收藏,也很适合新手跟着练。
17. 文末练习题汇总
最后把练习题单独再放一次,方便你直接复制使用。
练习题
请把下面这段旧风格代码,改写成现代 C# 风格:
csharp
namespace DemoApp.Models
{
public class UserModel
{
public string UserName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public int Age { get; set; }
public bool IsAdmin { get; set; }
}
}
要求
- 使用文件作用域命名空间
- 尽量使用
record - 必填字段使用
required - 属性优先使用
init - 合理区分
string和string? - 可以额外写一个新版
switch方法,对用户类型做分类
进阶挑战
请继续为改写后的 UserModel 编写一个方法:
- 管理员返回"管理员"
- 年龄小于 18 返回"未成年用户"
- 年龄 18 到 59 返回"普通成年用户"
- 年龄大于等于 60 返回"老年用户"
要求使用:
- 模式匹配
- 新版
switch