Lambda 默认参数 · 任意类型别名 · Experimental · Interceptors
C# 11 的 Lambda 不支持默认参数。碰到需要"有可选配置的回调"时,你只有三条路——每条都痛:
| 路 | 做法 | 痛在哪儿 |
|---|---|---|
| ① 每次全传 | retry(() => CallApi(), 3, 1000) |
大部分调用用默认值,重复传参数是噪音 |
| ② 写方法组 | async Task Retry3(...){ … } |
不能捕获上下文变量;为一个简单逻辑写完整方法——太重 |
| ③ 多写重载 Lambda | var retry3 = (act) => retry(act, 3, 1000); |
参数组合爆炸——N 个可选参数需要 2^N 个重载 |
这是微服务/HTTP 调用中最常见的模式——封装重试逻辑,默认重试 3 次、间隔 1 秒,但允许调用方覆盖:
// 你只能把重试逻辑写成普通方法:
async Task<T> RetryAsync<T>(
Func<Task<T>> action,
int maxRetries = 3,
int delayMs = 1000)
{
for (int i = 0; i <= maxRetries; i++)
{
try { return await action(); }
catch when (i < maxRetries)
{ await Task.Delay(delayMs); }
}
throw new InvalidOperationException();
}
// 问题:要单独定义方法,不能捕获局部变量
// 也不能内联到调用链中
// 就地在调用链中定义,捕获上下文:
var retry = async (
Func<Task> action,
int maxRetries = 3, // 温和重试
int delayMs = 1000, // 1 秒间隔
bool throwOnFail = true) => // 默认抛异常
{
for (int i = 0; i <= maxRetries; i++)
{
try { await action(); return; }
catch when (i < maxRetries)
{ await Task.Delay(delayMs); }
}
if (throwOnFail) throw; // 否则静默
};
// 90% 的调用:用默认值,干干净净
await retry(() => httpClient.GetAsync(url));
// 特殊情况:覆盖重试次数和间隔
await retry(() => CallPaymentApi(), maxRetries: 0);
await retry(() => CallSlowSvc(), delayMs: 5000, maxRetries: 5);
每个项目都会写一个"带默认日志级别"的 log helper——C# 12 之后一行搞定:
// 定义:msg 必传,level 和 ts 可选
var log = (
string msg, // 必传
string level = "INFO", // 默认 INFO
DateTime? ts = null) => // 默认当前时间
{
var time = ts ?? DateTime.Now;
Console.WriteLine(
$"[{time:HH:mm:ss}] [{level}] {msg}");
};
// 日常使用:只传消息,level 和时间自动填充
log("订单处理完成");
// → [14:32:05] [INFO] 订单处理完成
// 出错时:覆盖日志级别
log("支付接口超时", level: "ERROR");
// → [14:32:10] [ERROR] 支付接口超时
// 补日志时:手动指定时间戳
log("历史记录", ts: new DateTime(2026, 6, 10));
httpContext 自动附加请求 ID,或捕获 ILogger 实例写入结构化日志。普通 static 方法做不到;实例方法又太重。
// 典型场景:创建 API 客户端时大部分配置用默认值
var createClient = (
string baseUrl,
int timeoutSec = 30,
bool useAuth = true,
string apiVersion = "v1") =>
{
var client = new HttpClient { BaseAddress = new Uri(baseUrl) };
client.Timeout = TimeSpan.FromSeconds(timeoutSec);
if (useAuth) client.DefaultRequestHeaders.Add("Authorization", GetToken());
client.DefaultRequestHeaders.Add("Api-Version", apiVersion);
return client;
};
// 日常:一行搞定
var api = createClient("https://api.mysvc.com");
// 调用旧版 API:覆盖版本号
var legacyApi = createClient("https://api.mysvc.com", apiVersion: "v0");
// 长时间批量任务:拉长超时
var batchApi = createClient("https://api.mysvc.com", timeoutSec: 300);
| 规则 | 说明 |
|---|---|
| 默认参数放最后 | 和普通方法一样:有默认值的参数必须在无默认值参数之后 |
| 默认值在调用时求值 | 不是定义时。如果默认表达式引用变量,取调用时刻的值(见下例) |
| 支持 params | C# 13 进一步允许 params 修饰符在 Lambda 中(下节课) |
// ⚠ 默认值在调用时求值——不是定义时
int x = 10;
var get = (int offset = 1) => x + offset;
int r1 = get(); // 11 —— x=10 + offset=1
x = 20;
int r2 = get(); // 21 —— x=20 + offset=1(取调用时的 x)
// 这与方法的可选参数行为一致:
// 默认值是语法糖——调用时编译器补上省略的参数
// ✅ 默认参数必须在最后
var f1 = (int a, int b = 1) => a + b; // ✅
var f2 = (int a = 1, int b) => a + b; // ❌ CS1737
// ❌ C# 11:这些全部编译失败
// using Ints = int[];
// using Point = (int X, int Y);
// using unsafe Ptr = int*;
// ✅ C# 12:全部合法
using Ints = int[]; // 数组
using Point2D = (int X, int Y); // 元组——最实用!
using Coordinate = (double Lat, double Lng);
using unsafe IntPtr = int*; // 指针(需 unsafe 上下文)
using MyMap = Dictionary<string, object>; // 泛型实例(以前也可以)
// C# 11:元组类型散落各处,没有统一名字
public (int x, int y) Transform((int x, int y) input) { ... }
// 如果某天要加第三个值?改 N 处。编译不报错但逻辑错。
// C# 12:定义一次,到处使用
using Point2D = (int X, int Y);
public Point2D Transform(Point2D input) { ... }
// 改结构只改一行。编译帮你在所有使用处报错。
using MyList<T> = List<T>; 不行。语言团队还在讨论语义。
// GlobalUsings.cs——全局可用
global using Coordinate = (double Lat, double Lng);
global using UserId = System.Guid;
?. 可选链 C# 先出(2015→2016),元组类型 TS 先出(2014→2017),模式匹配各自演化。真正的功能孵化器是 F#——记录、模式匹配、可区分联合(提案中)在 F# 里已有十年历史。using 别名只是语法糖——底层类型系统纹丝不动。标记类型、方法、程序集为"实验性"。使用者编译时收到警告——不是错误,但明确提示"这个 API 可能变"。
// 库作者——标记实验 API:
[Experimental("DIAG001")]
public class NewFeature { }
[Experimental("DIAG002", UrlFormat = "https://docs.mylib.com/migration/{0}")]
public void BetaMethod() { }
// 使用者——编译时收到警告:
var x = new NewFeature(); // ⚠ 警告 DIAG001: 'NewFeature' is experimental
// 有意识地抑制警告(表示你接受实验性 API 的风险):
#pragma warning disable DIAG001
var x = new NewFeature();
#pragma warning restore DIAG001
// 也可以在 .csproj 中全局抑制:
// <NoWarn>DIAG001;DIAG002</NoWarn>
拦截器允许在编译时把某个方法调用替换为另一个方法调用——是源生成器Source Generator的高级玩法。
<InterceptorsPreviewNamespaces> + Features=InterceptorsPreview);C# 13 保持预览;C# 14 / .NET 10 转正——去掉 [Experimental] 标记,配置项改名为 <InterceptorsNamespaces>,API 稳定。// C# 12 / 13(预览):csproj 配置
// <Features>InterceptorsPreview</Features>
// <InterceptorsPreviewNamespaces>
// $(InterceptorsPreviewNamespaces);MyGen
// </InterceptorsPreviewNamespaces>
// C# 14 / .NET 10(稳定):简化为
// <InterceptorsNamespaces>
// $(InterceptorsNamespaces);MyGen
// </InterceptorsNamespaces>
// 原理:
// 1. 源生成器扫描代码,找到特定的方法调用位置
// 2. 生成拦截器方法,声明要"替换"该位置的调用
// 3. 编译时(IL 生成前),Roslyn 将调用重定向到拦截器
// → 零运行时开销,零反射,Native AOT 完全兼容
拦截器的目标是库作者和框架作者,不是日常应用代码。转正后的几个典型用途:
| 场景 | 说明 |
|---|---|
| 零分配日志 | 把 ILogger.LogInformation(msg, args) 编译时替换为 LoggerMessage.Define 委托——消除装箱和字符串拼接 |
| 透明链路追踪 | 拦截每个 HttpClient.SendAsync 调用,自动注入 correlation header——调用方代码零修改 |
| AOT 反射消除 | 将依赖反射的调用在编译时替换为直接代码——Native AOT 的关键技术 |
| 编译时配置校验 | 拦截 builder.Configuration["key"],编译时检查 key 是否存在于 appsettings.json |
[InterceptsLocation] 包含文件路径、行号、列号和内容哈希,源码一改全变,无法手写)。使用者需在 csproj 中显式声明允许的命名空间——第三方包不能静默拦截调用。面向对象:NuGet 包作者 / 框架作者 / AOT 优化场景。
int threshold = 5;
var check = (int value, int min = threshold) => value >= min;
threshold = 10;
bool result = check(7);result 是 true 还是 false?为什么?using Point = (int X, int Y); 在 C# 11 中是什么状态?| 课程 | 内容 | 日常频率 |
|---|---|---|
| 14. Primary Constructors | 消灭 DI 样板;捕获语义;class vs record 差异 | ⭐⭐⭐⭐⭐ |
| 15. Collection Expressions | [ ] 统一语法;.. 展开;编译器优化 | ⭐⭐⭐⭐⭐ |
| 16. ref readonly + Inline Arrays | 参数语义三角;安全固定缓冲区 | ⭐⭐⭐ |
| 17. 小特性集(本课) | Lambda 默认参数;类型别名;Experimental;拦截器 | ⭐⭐⭐⭐ |
\e 转义、ref struct 实现接口、方法组自然类型改进。