尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#继承实战详解:从代码复用到Attribute高级应用

C#继承实战详解:从代码复用到Attribute高级应用 每次在技术群里看到有人问“继承到底是个啥”我都能理解那种感觉。网上搜继承教程十有八九从动物开始讲猫是动物狗是动物所以猫继承动物……例子没错但看完还是不知道自己代码里该怎么用。我写这篇就是为了把这个事彻底说人话。今天就聊透一个问题面向对象里的继承到底是什么、它解决什么麻烦、有哪些不同的继承方式以及C#里继承Attribute这种进阶玩法。这篇适合三类人刚开始学面向对象、写代码时拿不准该不该用继承、以及已经在用继承但反复被坑的。我会绕开教科书废话直接给结论、给代码、给踩坑经验保证你读完就能上手判断什么场景该继承什么场景千万别用继承。1. 继承到底解决什么问题从重复代码说起1.1 先忘掉动物记住“复用”二字很多人对继承的第一印象是“子类拥有父类的属性和方法”。这话没错但光记住这句没用因为你不清楚为什么需要这种机制。我换个说法继承是程序员偷懒的最正规手段。举个例子。假设你给公司写员工系统里面有程序员、产品经理、设计师三种角色。程序员有姓名、工号、写代码()产品经理有姓名、工号、画原型()设计师有姓名、工号、做图()。你发现没有三个类里都有一模一样的姓名、工号字段以及上班打卡()这种通用方法。如果每个类都复制粘贴一遍后期一旦改了字段名三处都得跟着改漏一处就出事。这时候你就可以抽一个基类Employee把姓名、工号、上班打卡()放进去让三个类分别继承它。三者自动拥有这些公共成员各自只管自己独有的东西。这个动作的意义不是“让猫变成动物”而是消除重复、统一入口。以后要加一个工资条()方法只改基类一处所有子类全都有了。1.2 没有继承的时代代码是怎么腐烂的我早期写代码时干过一件蠢事在项目里复制了三份相同的工具类只是名字不同。后来要改里面的一个算法逻辑我得同时打开三个文件改完之后还因为复制时改坏了一处导致生产环境出了个隐蔽的Bug。那次之后我才真正明白抽象公共代码不是“设计洁癖”而是防止代码在多处悄悄分叉。所谓“分叉”就是复制出来的代码开始有了细微差异。一个人给A版本修了BugB版本没修一个人给B版本加了新功能A版本没有。几个月后这两份代码已经长得不像同一个人了。继承的价值在于公共逻辑只维护一份子类只写差异。这不是锦上添花是大型项目的生存底线。1.3 继承体系里的三个角色要理解继承必须先认清三个角色基类父类被继承的类放公共特征。在C#里用class Employee {}定义。派生类子类继承基类的类可以追加自己的成员。用class Programmer : Employee {}定义。实例new出来的具体对象。继承关系发生在类与类之间不是对象与对象之间。这三个名字后面我会反复用。你只要记住一条主线基类管共性派生类管特性。后面的所有内容都是在解释这条主线怎么落地。2. 继承在代码里的落地姿势语法与调用链2.1 用C#写一个最简继承先看一段完整可跑的代码你照着敲一遍就能体会public class Employee { public string Name { get; set; } public string EmployeeId { get; set; } public void ClockIn() { Console.WriteLine(${Name} 上班打卡); } } public class Programmer : Employee { public void WriteCode() { Console.WriteLine(${Name} 在写代码); } } public class ProductManager : Employee { public void DrawPrototype() { Console.WriteLine(${Name} 在画原型); } }注意看Programmer类和ProductManager类的声明后面跟了一个冒号和Employee。这就是“继承”的语法表示读作“Programmer继承Employee”。写完这句之后Programmer就自动拥有了Name、EmployeeId、ClockIn()三个成员。使用方式var p new Programmer { Name 张三, EmployeeId 001 }; p.ClockIn(); // 张三 上班打卡来自基类 p.WriteCode(); // 张三 在写代码来自自身这里有个新手常犯的误解以为子类复制了一份基类成员。不是复制是引用。子类对象在内存里包含基类部分加自身部分理解这点对后面讲构造会很有帮助。2.2 构造函数调用链基类先跑子类后跑很多初学者在继承上踩的第一个坑就是构造函数。看这个例子public class Employee { public Employee(string name) { Name name; } } public class Programmer : Employee { public Programmer(string name) : base(name) { } }子类的构造函数声明里那个: base(name)意思是“我要调用基类的带参构造函数”。这条规则必须记住创建子类对象时先从基类构造函数开始再轮到子类构造函数。为什么会这样因为子类对象里有一块区域属于基类这块区域必须由基类构造函数初始化。否则基类的私有字段可能没被赋值子类方法用到它们时就可能拿到垃圾值。这就像盖房子地基得先打好二楼的墙才敢往上砌。如果基类只有带参构造函数子类构造函数又没写base(...)编译器会直接报错。解决方式有两个给基类补一个无参构造函数或者在子类构造函数后面显式指定base(...)。推荐后者因为显式传参更清楚。2.3 重写还是隐藏这两个不是一回事子类里可以定义和基类同名的方法。C#给了两种处理方式重写override和隐藏new。很多人傻傻分不清我用代码演示public class Employee { public virtual void Work() { Console.WriteLine(员工在干活); } } public class Programmer : Employee { public override void Work() { Console.WriteLine(程序员在写代码); } } public class Designer : Employee { public new void Work() { Console.WriteLine(设计师在做图); } }这两个类的区别使用的时候才会暴露Employee a new Programmer(); Employee b new Designer(); a.Work(); // 输出程序员在写代码 b.Work(); // 输出员工在干活a明明是Employee类型的变量但它指向的Programmer对象重写了Work所以调用时走的是子类实现。b的情况就完全不同Designer用了new隐藏但变量类型是Employee所以调用结果找的是基类版本。重写是继承体系内的“动态派发”隐藏是声明一个恰好同名的新方法。两者的语义天差地别。我的建议非常简单除非你明确知道自己在做什么否则一律用virtual加override不要用new去隐藏方法。隐藏容易制造“看起来调A实则在调B”的混乱代码评审时看到这种写法我都会专门追问一句。3. 不同的继承方式不只是“继承一个类”这一种3.1 C#的单继承限制与多层继承链C#里一个类只能继承一个基类这叫单继承。它不像C那样允许一个类同时继承多个类。很多人觉得这是限制我反而觉得是保护。多继承一旦出现两个基类有同名方法你根本不知道子类该用谁的。连“钻石继承”这种经典问题都能逼疯一票人。C#干脆一刀切省掉这些麻烦。单继承不意味着只能有一层。你可以让A继承B再让C继承A形成多层继承链。实际项目中我见过最长的继承链有五六层每一层都在抽象级别上往前走一步。但要注意链越长理解成本越高。我给的默认建议是三层以内能解决的问题不要硬设计成四层。后面第六节我会详细说这个判断。3.2 接口继承C#应对多继承的方案既然不能多继承类那多种“能力”怎么组合答案是接口。比如一个类既需要“能被序列化”又需要“能被日志记录”可以分别定义两个接口让类一次性实现多个。public interface ISerializable { string ToJson(); } public interface ILoggable { void Log(); } public class Order : ISerializable, ILoggable { public string ToJson() { orderId: 1 }; public void Log() Console.WriteLine(订单被操作了); }接口继承和类继承最大的区别在于类继承带来“实现代码”接口继承只带来“契约承诺”。你继承Employee类就自动拿到ClockIn()的现成实现你实现ISerializable接口ToJson()的代码得自己写编译器只保证你这人“承诺了会写”。设计接口时有个小技巧接口要小。一个接口两三个方法最好最多别超过五六个。接口一大实现者就得被迫写一堆用不到的空方法这种“接口污染”在维护期相当烦人。3.3 继承体系里的访问修饰符哪些能被继承经常有人把“继承”理解为“子类拥有父类的一切”。这里必须澄清继承能拿到的是非私有成员。private字段和方法是真正意义上的“基类私事”子类摸都摸不到更别提调用了。C#给了几个选择public所有类可见子类能继承。protected基类自己和子类可见外部不可见。这是继承专用设计。internal同一程序集可见子类在别的程序集就继承不到。protected internal同一程序集内或子类可见。private仅基类可见子类不可继承。给字段用protected还是private我经验是能private就尽量private。你开放访问权限给子类等于允许子类和基类成员耦合在一起后续改基类内部逻辑时所有子类都可能被波及。字段能用private加属性暴露就用这个组合。3.4 sealed和abstract继承链上的两把枷锁继承也不是无限开放的。C#给了两个反向关键字abstract抽象类无法实例化专门让人继承的。sealed密封类禁止被继承到此为止。用法上看抽象类是“模板半成品”里面可以有抽象方法让子类必须实现也可以有普通方法直接继承。密封类则是“设计终点”防止别人拿你的类当父类搞二次开发。public abstract class Shape { public abstract double GetArea(); } public class Circle : Shape { private double _radius; public Circle(double radius) _radius radius; public override double GetArea() Math.PI * _radius * _radius; } public sealed class ImmutableConfig { public string Value { get; } }写类库时我倾向于凡是没想好要不要被人继承的类一律标记sealed。这样别人就不能随意继承你的实现避免以后的改动变成破坏性变更。4. 封装、继承、多态三兄弟是怎么配合的4.1 封装给继承画了一条安全边界封装、继承、多态常被一起提到但它们不是三个并列选项而是一个递进体系。封装是第一步——把数据和行为收进类里对外只露出必要的方法。继承发生在两个类之间前提是类本身就封装得当。举个例子。如果基类把字段全都公开成public子类可以随意修改那继承就失去了安全意义。正确的姿势是基类用private保存字段通过protected方法或public属性让子类安全操作。这样一来子类能用基类的能力但改不了基类的内部状态封装这堵墙依然立得住。实际操作中我看到最多的坏习惯是为了图省事把基类字段全部设成protected子类直接改字段。这样短期快长期痛——因为所有改字段的地方散落在各个子类里你想给字段加个校验逻辑得一个个子类翻。4.2 继承是多态的基石多态是继承的灵魂多态这个概念本质上是一句话同一个变量类型指向不同子类对象时调用同一个方法行为不一样。前面那节代码里的Employee a new Programmer();就是多态。变量类型是Employee实际对象是Programmer调Work()时执行的是子类版本。把代码写得面向基类而不是面向具体子类就是经典的“面向抽象编程”。这个能力的价值在于扩展性。假如你现在有两百个员工类都继承自Employee你可以写一个统一的方法void HandleWork(Employee e) { e.ClockIn(); e.Work(); }不管传入的是Programmer、Designer还是以后新增的Manager这方法都不用改。新增一个子类整个系统自动支持。这就是为什么说继承撑起了不修改旧代码就能扩展新功能的体系。4.3 串起来看订单系统里的三兄弟我用一个订单的例子把三者串一遍。订单有普通订单、VIP订单、团购订单public class Order { protected decimal Amount { get; set; } // 封装金额受保护 public virtual decimal CalculatePayable() Amount; // 多态入口 } public class VipOrder : Order // 继承复用公共字段 { public override decimal CalculatePayable() Amount * 0.85m; // 多态子类改写规则 } public class GroupOrder : Order { public override decimal CalculatePayable() Amount * 0.70m; }封装让金额只在订单体系内可见继承让子类复用订单的核心结构多态让计算规则自动分派到对应子类。三兄弟缺一不可这套设计才能在业务变化时只加类、不改老逻辑。5. C#里的继承进阶Attribute继承与反射的组合拳5.1 Attribute本身就是一个类继承规则同样适用很多人写了两三年C#都不清楚一件事Attribute在底层就是一个类。比如你用过的[Obsolete]、[Serializable]它们都是Attribute的子类。既然是类就可以继承。由继承而来的Attribute继承问题就出现了子类上标注的Attribute父类能不能读到父类上标注的Attribute子类能不能读到先看自定义Attribute怎么写[AttributeUsage(AttributeTargets.Class, AllowMultiple true, Inherited true)] public class TagAttribute : Attribute { public string Name { get; } public TagAttribute(string name) Name name; }AttributeUsage是至关重要的一行配置它有三个参数AttributeTargets规定这个Attribute能贴在哪里类、方法、属性……。AllowMultiple允许一个成员上贴多个同样的Attribute。Inherited决定这个Attribute能不能被派生类继承。很多人只写类名不写AttributeUsage然后用的时候发现“继承来的Attribute读不到”多半就是忘了配Inherited。5.2 贴上Tag之后怎么通过反射读取Attribute本身没有任何行为真正起作用的是“谁去读它”。读取靠反射反射这块不会你就没法玩转Attribute。看代码public class ParentEntity { } [Tag(订单模块)] public class OrderEntity : ParentEntity { }接着写一段读取代码public static Liststring GetTags(Type type) { var tags new Liststring(); var attributes type.GetCustomAttributes(typeof(TagAttribute), true); foreach (TagAttribute attr in attributes) { tags.Add(attr.Name); } return tags; }注意GetCustomAttributes的第二个参数——true。这个布尔值就是“是否沿继承链向上查找”。传true时如果你去查OrderEntity的父类ParentEntity上的[Tag]也能找到。传false就只找当前类型自己。5.3 实际场景把Attribute继承用在批量校验上我做过一个项目要给几十个业务实体统一做权限标记。我在基类上定义了一个[RequiredPermission(xxx)]所有子类默认继承这个权限。但个别子类需要覆盖成更严格的权限就在自己头上重新标一个同名Attribute配合AllowMultiple策略读取时要注意合并逻辑。这里有个我踩过的坑Inheritedtrue搭配AllowMultiplefalse时子类上标注同名Attribute会“覆盖”继承来的而不是“共存”。那结合起来的效果就是你以为你会同时读到两个结果只读到子类自己那一个。如果业务逻辑需要“基类权限子类额外权限”合并必须把AllowMultiple设为true读取时再手动决定怎么处理。再提醒一个实操细节GetCustomAttributes沿继承链查找时对同一Attribute默认“就近原则”——子类存在同名Attribute时基类的不会再返回来。想拿全部的话你要在循环里调用type.BaseType往上自己爬爬一层查一次。6. 继承的实战经验什么时候用以及那些反复出现的坑6.1 组合和继承的边界组合优先不是口号面向对象设计里流传着一句话组合优于继承。我一开始不理解觉得继承这么好用凭什么要少用后来被现实教育了。继承最大的隐患是脆弱的基类问题。你在基类里加一个新方法所有子类都自动获得。如果某个子类并不想拥有这个能力你没办法因为继承是“全有或全无”。轻则污染子类接口重则因为子类和基类内部逻辑耦合改动传递到整个体系。组合则是“按需装配”一个类包含另一个类的实例需要什么能力就注入什么对象。这样依赖关系显式且干净。举个例子假设有Dog和Bird类。直觉上你可能想建个Animal基类让两者都继承。但Bird需要Fly()Dog不需要。如果你把Fly()塞进AnimalDog就得被迫实现一个无意义方法。这时候更好的做法是把“飞”抽成IFlyable接口Bird实现它或者干脆组合一个FlyBehavior对象。判断标准很简单共同点必须是真正的“是其”关系而不是恰好有几个相同功能。6.2 八个反复出现的坑看一下省一年教训我在代码评审里见多了这些问题列成清单供你对照基类改了个方法签名所有子类编译报错。这是正常现象但如果你发现报错面太大说明继承关系太紧。继承链太深调试时不知道方法在哪层实现。解决办法是IDE里右键Go To Base一层层找。真找到头皮发麻就该考虑重构了。子类构造函数忘了传参数。编译器会要求你显式调用base(...)千万别为了省事加无参构造函数绕过。new隐藏方法被当成重写。同一个方法名调用结果却因变量类型而异这是最隐蔽的Bug来源之一。把abstract类搞成万能类。有人习惯把公共方法全塞进抽象基类结果基类又大又乱。抽象基类应该小专管核心抽象逻辑。忽视了接口与继承的区别。类继承为的是复用实现接口继承为的是约定能力。两个不要混用。反射读取Attribute时Inherited参数传false导致子类上的Attribute全读不到排查半天发现是这个参数没传。在继承体系里用静态方法救急。静态方法不参与多态子类无法重写。如果后续需要根据子类类型做不同处理静态方法就卡死你了。6.3 用一句话做试用建议到了下结论的时候我不会说“继承很好”或“继承要少用”这种空话。实用标准是三条确实存在“子是父的一种”的关系比如程序员是员工的一种而不是“程序员有写代码的能力”。你确实能抽出稳定不变的公共逻辑并且这个逻辑不会让子类被迫实现不相关东西。你已经想好基类日后怎么演化。至少考虑过基类加方法时所有子类的接受度。如果符合放心用继承如果拿不准默认组合加接口。这两个策略不冲突可以混合用。最后再说一点个人体会继承这个特性不是越炫越好而是抽象得越贴近业务越好。我见过同行为了展示面向对象功底硬造了五层继承抽象最后代码是挺漂亮改需求时却没人敢动。反而是那些保持两层三层、逻辑坦诚的继承设计在真实项目里活得更久。挑最简单能解决问题的抽象才是“说人话”的继承。
返回列表