C#派生类传参为基类类型时new修饰属性类型识别异常
你使用new关键字修饰派生类的Cards属性属于成员隐藏,并非对基类虚属性的重写。此时派生类中声明的Cards和基类的Cards是两个完全独立的类成员:当你以基类类型Blacklist访问实例的Cards属性时,调用的永远是基类定义的IEnumerable<Card>类型属性,和你在派生类中赋值的IEnumerable<CardTwo>集合没有关联,自然无法获取到正确的集合类型。
方案1:泛型基类实现(推荐,类型安全无额外开销)
将黑名单基类改造为泛型类,从类型定义层面固定卡片类型,彻底避免类型不匹配问题:
// 泛型基类,约束卡片类型必须继承自Card public class BlacklistBase<TCard> where TCard : Card { public int Id { get; set; } public virtual IEnumerable<TCard> Cards { get; set; } } // 保留原有非泛型基类的使用场景,直接继承泛型基类即可 public class Blacklist : BlacklistBase<Card> { } // 派生类指定使用CardTwo作为卡片类型 public class BlacklistTwo : BlacklistBase<CardTwo> { }
对应调整Insert方法为泛型方法:
public void Insert<TCard>(BlacklistBase<TCard> blacklist) where TCard : Card { var instanceType = blacklist.GetType(); // 传入BlacklistTwo时返回BlacklistTwo var cardsType = blacklist.Cards.GetType(); // 正确返回List<CardTwo> }
该方案全程是编译期类型检查,没有运行时转换成本,也不需要使用dynamic,是最规范的实现方式。
方案2:正确重写基类虚属性
如果不想改动现有基类的非泛型设计,可以去掉new关键字,改为重写基类的Cards虚属性,利用IEnumerable<T>的协变特性适配派生类的强类型集合:
public class BlacklistTwo : Blacklist { // 派生类专属的强类型卡片集合 public IEnumerable<CardTwo> TypedCards { get; set; } // 重写基类虚属性,直接返回强类型集合(IEnumerable<CardTwo>可协变转换为IEnumerable<Card>) public override IEnumerable<Card> Cards { get => TypedCards; set => TypedCards = value?.Cast<CardTwo>(); } }
这种实现下,哪怕你以基类类型访问Cards属性,实际返回的仍然是你赋值的CardTwo集合,调用GetType()可以获取到正确的集合类型。如果业务上允许集合内混入普通Card类型,可以把Cast<CardTwo>()换成OfType<CardTwo>()避免转换异常。
方案3:方法内做类型转换
如果不想修改现有类的定义,可以在Insert方法中判断实例的实际运行时类型,转换为对应派生类后再访问属性:
public void Insert(Blacklist blacklist) { if (blacklist is BlacklistTwo blacklistTwo) { var cards = blacklistTwo.Cards; // 此处为IEnumerable<CardTwo>类型 // 处理BlacklistTwo的专属逻辑 } else { var cards = blacklist.Cards; // 处理普通Blacklist的逻辑 } }
该方案仅适合派生类数量少、后续扩展不多的场景,如果派生类持续新增,方法内会堆积大量分支判断,维护成本较高。
注意:
new关键字的成员隐藏语法设计初衷是解决基类迭代新增成员和派生类已有成员重名的兼容问题,并非用于实现多态,日常业务开发中尽量不要用它来替代override重写,很容易出现这类类型访问不符合预期的问题。
内容的提问来源于stack exchange,提问作者AverageProgrammer

