c#装饰器需严格遵循接口一致、构造注入、显式转发三原则:必须实现统一接口而非继承具体类,构造函数参数须为接口类型,所有转发调用必须显式写出且不可递归。

在 C# 里,Decorator 不是语法特性,也不是 [Decorator] 特性或编译器自动注入机制——你写不出 @Logging 这种东西。它纯靠接口 + 组合 + 显式委托实现,链式组装、运行时生效,但一旦漏掉转发或传错类型,就会静默失效或栈溢出。
装饰器必须实现统一接口,不能继承具体类
这是最常踩的坑:有人想给 FileReader 加日志,就写 class LoggingFileReader : FileReader。这根本不是装饰器,是继承,破坏开闭原则,且无法套在 NetworkReader 上。
- 所有被装饰对象(如
ConsoleLogger、FileLogger)和所有装饰器(如TimestampLogger、RetryLogger)必须实现同一接口,比如ILogger - 接口定义行为契约,不是为了复用代码,而是为了类型可替换 ——
new TimestampLogger(new FileLogger())能成立,前提是两者都实现了ILogger - 如果某个装饰器需要调用被装饰对象的独有方法(比如
FileLogger.Flush()),说明接口缺方法,应回头补全ILogger,而不是让装饰器依赖具体类型
装饰器构造函数参数必须是接口类型,不能 new 具体实现
装饰器内部绝不能出现 new ConsoleLogger() 或 new HttpService()。否则链就断了,无法嵌套,也无法被 DI 容器管理。
- 正确写法:
public TimestampLogger(ILogger inner)—— 参数是ILogger,不是ConsoleLogger - 错误写法:
private readonly ILogger _inner = new ConsoleLogger();—— 这会让装饰器“锁死”底层,失去灵活性 - 工厂或 DI 注入时,顺序决定执行流:注册
services.Decorate<ilogger retrylogger>()</ilogger>后再注册Decorate<ilogger timestamplogger>()</ilogger>,则RetryLogger包在最外层,最先执行
转发调用必须显式写,不能漏、不能递归调用 this
装饰器不是代理生成器,C# 不会自动帮你转发。漏掉 _inner.Log(),或者误写成 this.Log(),程序不会报错,但要么不执行原逻辑,要么直接栈溢出崩溃。
- 前置增强示例:
Console.WriteLine($"[{DateTime.Now}] start"); var result = _inner.Log(message); - 后置增强示例:
_inner.Log(message); Console.WriteLine("logged."); - 拦截场景(如权限):
if (!IsAuthorized()) throw new UnauthorizedAccessException(); return _inner.Log(message); - 绝对禁止:
return this.Log(message);—— 这是无限递归,编译器不拦,运行时崩
泛型装饰器能复用,但要注意协变与生命周期管理
如果你有一堆 ITransformer<t></t>、IValidator<t></t> 接口,可以抽象出通用基类,但别忽略泛型约束和资源释放问题。
- 基础泛型装饰器:
public abstract class Decorator<t> : ITransformer<t> where T : class</t></t> - 若被装饰对象实现
IDisposable,装饰器也应实现,并在Dispose()中调用_inner.Dispose() - 异步方法(
Task<t> TransformAsync(T input)</t>)需注意 await 时机,避免在前置逻辑里 await 导致异常未被捕获 - 协变/逆变限制:接口若声明为
ITransformer<in t></in>或out T>,装饰器实现时要同步适配,否则类型推导失败
真正难的不是写一个装饰器,而是确保整条链上每个环节都遵守“接口一致、构造注入、显式转发”三原则。一旦某层偷偷用了具体类型或忘了调 _inner,问题往往延迟暴露,调试时得一层层扒调用栈。











