【.NET Core】 教程之C#语法新特性入门到进阶:一篇学会现代 C# 写法

如果你刚开始系统学习 .NET 8,或者已经会写一些 C#,但总觉得自己的代码还是"老味道"很重,那么这篇文章就是写给你的。

很多人学 .NET 时,容易把注意力放在 Web API、EF Core、依赖注入这些框架层内容上,却忽略了一件更基础也更重要的事:现代 C# 语法本身,已经足够让你的代码变得更简洁、更安全、更易维护。

这篇文章重点整理几组在 .NET 8 开发中非常常用、也非常值得掌握的现代 C# 特性:

  • 文件作用域命名空间
  • var 的合理使用
  • 可空引用类型 string?
  • record
  • init
  • required
  • 模式匹配
  • 新版 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" 明显是 string
  • 10 明显是 int
  • new UserDto() 明显是 UserDto
  • new 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:不应该为 null
  • NickName:允许为 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);
}

这句代码的意思是:

  • 如果 objstring
  • 就把它作为 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 等于 admin
  • Email 不为空

8.4 范围模式

csharp 复制代码
if (score is >= 90 and <= 100)
{
    Console.WriteLine("优秀");
}

这种写法很适合分数区间、金额区间、年龄区间等判断。

8.5 列表模式的感知

在现代 C# 里,模式匹配已经不只是"判断类型",而是一种更强的结构化判断方式。

你可以先把重点放在下面几类:

  • is null
  • is 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; }
}

这段代码同时体现了:

  • 文件作用域命名空间
  • record
  • required
  • init
  • 可空引用类型

如果你能把这一套写法真正用熟,那么你的 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 你的练习目标

请把它改造成现代风格,并尽量体现这些点:

  • 文件作用域命名空间
  • record
  • required
  • init
  • 合理使用 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. 学习建议:这几个特性该按什么顺序掌握

如果你准备系统学习,建议按这个顺序来:

  1. 先掌握文件作用域命名空间
  2. 再学 var 的合理使用
  3. 再理解可空引用类型 string?
  4. 接着学 initrequired
  5. 然后学习 record
  6. 最后重点练模式匹配和新版 switch

原因很简单:

  • 前面几个特性偏"代码风格升级"
  • 后面几个特性偏"表达能力升级"

先把代码写整洁,再把判断逻辑写优雅,学习节奏会更顺。


15. 一句话总结每个重点

为了方便你复习,我把这几个点再压缩成一句话:

  • 文件作用域命名空间:少一层缩进,让文件更干净
  • var:右值类型明显时用,不明显时别滥用
  • string?:明确谁能为空,减少空引用风险
  • record:更适合 DTO、响应对象、数据模型
  • init:只允许初始化时赋值,更安全
  • required:让必填项在编译期就暴露问题
  • 模式匹配:让判断更结构化
  • 新版 switch:让分支逻辑更简洁、更现代

16. 总结

如果你正在学 .NET 8,那么一定不要只盯着 Web API、EF Core 和各种框架配置。

现代 C# 本身,就是你写出高质量 .NET 代码的基础。

这篇文章讲的内容,看起来都是语法点,但它们背后解决的其实是几个很现实的问题:

  • 代码能不能更简洁
  • 空引用能不能少一点
  • DTO 能不能更安全
  • 分支逻辑能不能更清楚
  • 项目代码风格能不能更现代

只要你把这几个点练熟,你写出来的代码会明显比传统写法更像一个现代 .NET 项目。

如果你准备把这篇内容发到 CSDN,我建议你可以这样排版:

  1. 开头先写"为什么 .NET 8 项目要学现代 C#"
  2. 中间每个知识点配一个小代码示例
  3. 练习题单独成节
  4. 参考答案放在后面
  5. 最后再补一段自己的学习总结

这样的文章很适合收藏,也很适合新手跟着练。


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
  • 合理区分 stringstring?
  • 可以额外写一个新版 switch 方法,对用户类型做分类

进阶挑战

请继续为改写后的 UserModel 编写一个方法:

  • 管理员返回"管理员"
  • 年龄小于 18 返回"未成年用户"
  • 年龄 18 到 59 返回"普通成年用户"
  • 年龄大于等于 60 返回"老年用户"

要求使用:

  • 模式匹配
  • 新版 switch