Lesson 10: C# 10 — 小特性集

File-scoped Namespaces · Global Usings · Constant Interpolated Strings——消灭项目中每个文件都重复的样板

前置:已完成 Lesson 09(Record Structs)Lesson 08(C# 9 小特性集——Top-level Statements)
C# 10 ↔ .NET 6 LTS:2021.11 发布。
本课目标:File-scoped Namespaces、Global Usings(含隐式导入)、Constant Interpolated Strings——以及它们如何组合成 .NET 6 一行代码的项目模板。

一、从 11 行到 1 行——少了什么?

😣 .NET 5 控制台项目

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace MyConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello");
        }
    }
}

😎 .NET 6 控制台项目

Console.WriteLine("Hello, World!");

Top-level Statements(C# 9)消掉了 class Program 和 Main。但那些 using 去哪了?namespace 去哪了?——这就是 C# 10 的答案。

二、文件范围命名空间 File-scoped Namespaces

2.1 痛点——整个文件白缩进一级

// 传统:所有代码被 namespace 大括号包着——全体缩进 4 格
namespace MyApp.Services
{
    public class OrderService  // ← 4 格
    {
        public async Task<Order> GetAsync(int id)  // ← 8 格
        {
            // ... 真正的代码 ← 12 格
        }
    }
}

// C# 10:分号结尾,不缩进
namespace MyApp.Services;

public class OrderService  // ← 顶格写
{
    public async Task<Order> GetAsync(int id)
    {
        // ... ← 少一级缩进
    }
}

纯粹的语法糖——编译产物完全一样。一个文件一个 namespace 是 99% 的场景,为什么要为这 99% 多缩进一级?

限制:一个文件只能有一个 File-scoped Namespace。嵌套 namespace(namespace A { namespace B { } })不能用此语法。

2.2 隐式命名空间——SDK 自动注入的 namespace

.NET 6 的 Program.cs 连 namespace 都没有写——但编译器仍然把它放进了命名空间。这是 SDK 的隐式命名空间Implicit Namespace

// 你看到的 Program.cs:
Console.WriteLine("Hello");

// 编译器实际编译的等价代码:
// namespace MyApp;  ← SDK 根据 .csproj 的 RootNamespace 自动注入
// internal partial class Program {
//     private static void $Main(string[] args) {
//         Console.WriteLine("Hello");
//     }
// }

根命名空间来自 .csproj 中的 <RootNamespace> 或项目文件名。只有 Top-level Statements 文件且没有显式 namespace 声明时,隐式命名空间才生效。

三、全局引用 Global Usings

3.1 痛点——每个文件头顶都重复同样的 10 行 using

// C# 10 之前——每个 .cs 文件都以这堆开头:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
// ... 重复 N 次

// C# 10——GlobalUsings.cs 一个文件管整个项目:
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using Microsoft.Extensions.Logging;
// 其他 .cs 文件不再需要这些 using

3.2 完整语法

// ① 基本:整个项目可见
global using System.Text.Json;

// ② global + static:全局静态导入
global using static System.Math;  // 所有文件都能直接用 Abs(), PI

// ③ global + alias:全局别名
global using MyDict = Dictionary<string, object>;

3.3 GlobalUsings.cs vs .csproj <Using>

GlobalUsings.cs 推荐.csproj <Using>
可见性Solution Explorer 里一眼看到藏在 .csproj 里,多数人不点开
新人上手打开文件立刻知道全局有什么没人告诉就不知道这里藏了 using
Code ReviewPR 里改这个文件很明显.csproj 改动容易被忽略
编译产物完全一样——都生成程序集级 using 指令
社区共识:用 GlobalUsings.cs。代码是写给人看的。.NET 官方项目模板也都采用此模式。.csproj 的 <Using> 主要是 SDK 的隐式导入在用——那是 SDK 的活,不是你该管的。

3.4 隐式导入 Implicit Usings

.NET 6+ SDK 根据项目类型自动注入一套 global using:

// 控制台/类库 → SDK 自动注入:
//   System, System.Collections.Generic, System.IO,
//   System.Linq, System.Net.Http, System.Threading,
//   System.Threading.Tasks

// ASP.NET Core → 额外注入:
//   Microsoft.AspNetCore.Builder,
//   Microsoft.Extensions.DependencyInjection,
//   Microsoft.Extensions.Hosting,
//   Microsoft.Extensions.Logging, ...

隐式导入是 SDK 自动生成的 global using——机制相同,来源不同。你自己的 global using 会叠加在上面。通过 <ImplicitUsings>disable</ImplicitUsings> 可关闭。

四、常量内插字符串 Constant Interpolated Strings

4.1 痛点——const 和 $ 水火不容

// C# 9:const + 插值 = 编译错误
const string Base = "https://api.example.com";
const string Ver = "v1";
const string Url = $"{Base}/{Ver}";  // ❌ C# 9: 编译错误
const string Url = Base + "/" + Ver;  // ✅ 只能用 +

// C# 10:所有插值部分都是常量 → 合法
const string Url = $"{Base}/{Ver}";  // ✅ 编译时求值 → "https://api.example.com/v1"
规则示例
所有插值部分必须是常量$"pi = {Math.PI}" ❌——Math.PI 是静态字段,不是 const
可以用数字常量$"v{3}"
可以用 nameof()$"Class:{nameof(MyClass)}" ✅——nameof 是编译时常量表达式
零运行时开销IL 直接是最终拼接好的字符串——和手写一样
// ✅ 真实场景——URL 模板、日志前缀、连接名
const string BaseUrl = "https://api.example.com";
const string UsersEndpoint = $"{BaseUrl}/users";
const string AppName = "MyApp";
const string ConnName = $"{AppName}Db";  // "MyAppDb"

五、组合拳——.NET 6 项目模板全貌

// ===== Program.cs (.NET 6 console) =====
// 你看不到 using(隐式导入 + GlobalUsings)
// 你看不到 namespace(隐式命名空间 = SDK 自动注入的 File-scoped Namespace)
Console.WriteLine("Hello, World!");  // Top-level Statements (C# 9)

// ===== ASP.NET Core Minimal API 全貌 =====
// GlobalUsings.cs 里有:
//   global using Microsoft.AspNetCore.Builder; ...

namespace MyApi;  // File-scoped (C# 10)

var builder = WebApplication.CreateBuilder(args);  // Top-level (C# 9)
builder.Services.AddSingleton<Database>(new());     // Target-typed new (C# 9)
var app = builder.Build();
app.MapGet("/", () => "Hello");
app.Run();

从 11 行样板到 1 行业务逻辑——不是某个特性特别强,是 C# 9 + 10 组合起来消灭了几乎所有的仪式代码。

六、小测验

1/3 · File-scoped Namespace 和传统 namespace,编译产物有什么区别?
2/3 · 隐式导入和 global using 的关系是?
3/3 · const string s = $"{nameof(MyClass)}_v3"; 能编译吗?

七、下一步

💬 有问题?不确定 GlobalUsings.cs 里该放哪些 using?对隐式导入和手写 global using 的优先级有疑问?随时问。