使用派生类型和相同名称覆盖属性(C#)

13
我试图覆盖基类中具有相同名称的属性,但使用不同的派生类型。我认为可以通过协变或泛型实现,但不确定如何做到。以下代码会出错:“错误1 'Sun.Cache':类型必须是'OuterSpace.Cache'才能匹配重写的成员'OuterSpace.Cache'”。
public class OuterSpace {
    public virtual OuterSpaceData Data {get; set;}
    public virtual OuterSpaceAnalysis Analysis {get; set;}
    public virtual OuterSpaceCache Cache {get; set;}


    public class OuterSpaceData {
        //Lots of basic Data Extraction routines eg
        public virtual GetData();
    }
    public class OuterSpaceAnalysis {
        //Lots of Generic Analysis on Data routines eg
        public virtual GetMean();
    }
    public class OuterSpaceCache {
        //Lots of Caches of Past Analysis Results:
        public Dictionary<AnalysisType, List<Result>> ResultCache;
    }
}

public class Sun : OuterSpace {
    public override SunData Data {get; set;}
    public override SunAnalysis Analysis {get; set;}
    public override SunCache Cache {get; set;}

    public SunData : OuterSpaceData {
        //Routines to specific get data from the sun eg
        public override GetData();
    }

    public SunAnalysis : OuterSpaceAnalysis {
        //Routines specific to analyse the sun: eg
        public double ReadTemperature();
    }
    public SunCache : OuterSpaceCache {
        //Any data cache's specific to Sun's Analysis
        public Dictionary<AnalysisType, List<Result>> TempCache;
    }
}

public class Moon : OuterSpace {} etc.

最终结果是,当我访问Sun对象的“Data”属性时,我不希望有两个数据对象(继承和基类),但是当我尝试覆盖属性时,它要求Sun变量与基类相同类型。例如:

Sun EarthSun = new Sun()
EarthSun.Analyse()  //The OuterSpace Analysis Saves results to Sun Cache:

//Now try use the result:
EarthSun.Cache[0]...

与此类似,但使用派生类型而不是字符串数组:

C#成员变量被基类方法覆盖

这个答案对我来说没有什么意义:

如何在C++中继承后覆盖基类成员

或许这意味着它根本不可能?

我能用派生类型重写吗?

帮帮忙!:) 有什么解决办法吗?


1
略微偏题,但说太阳是一种外层空间有意义吗?这里的一些尴尬可能来自使用继承,而简单的结构就足够了。 - Dan Bryant
我欢迎建议。这是一个问题的演示伪代码,实际项目非常庞大,我只是在尝试为未来做计划。 - JaredBroad
仅从您提供的样本结构中很难说。主要考虑的是您如何实际使用这些类。特别是,缓存、分析和数据类有什么使它们作为基类访问有益的特点?您有一个协调分析的类,但对于每种类型的分析,协调似乎也不同;这提示我可能需要进一步调查隐藏的责任。如果您还没有通用结构的用例,我建议避免使用它;YAGNI。 - Dan Bryant
嘿:我有30-40个复杂算法,它们访问标准化的“OuterSpace”类 - 其中时间序列数据根据基础(DateTime-Value)存储。但例如太阳类型(有许多不同的太阳); 您还有其他数据+其他算法。可以使用基类算法(访问基类存储变量)完成90%的分析,但10%的Sun-Algorithms位于SunAnalysis Class中。通过简单地继承基类,我可以轻松访问它们的算法和数据。 - JaredBroad
听起来你绝对需要可扩展性,因此这只是一个结构的问题。 我个人的第一印象是继承和相关的耦合有点难以理解,可能有更好的方法将组件分离开来以使其相互隔离。 你可以问自己一个问题,是否可以独立测试逻辑的某个部分(例如“额外”逻辑)而不受其他逻辑的影响。 这可能会引导你找到自然的拆分点并减少耦合。 - Dan Bryant
5个回答

14

按照你的想法,这是不可能的。

你可以创建一个同名的“新”属性:

public new SunData Data
{
  get { return (SunData) base.Data; }
  set { base.Data = value; }
}

虽然不完全相同,但这可能是你能得到的最接近的。

另一个可能的方法是:

public SunData SunData { get; set; }

public override OuterSpaceData Data
{
  get { return SunData; }
  set { SunData = (SunData)value; }
}
这种方法的优点是可以保证将任何不是SunData的内容放入Data属性中是不可能的。缺点是,您的Data属性没有强类型,并且如果您想要静态地对其进行类型设置为SunData,则必须使用SunData属性。
有一种丑陋的方式可以同时获得“两全其美”的效果,但代价是编写代码令人困惑并且在以后难以理解 - 通过引入一个派生中间类,在基类Data属性上执行“sealed override”,然后将其重定向到一个具有新名称的受保护属性,再由最终派生类添加一个“new”Data属性,然后调用中间属性。虽然我曾经这样做过,但它真的是一个丑陋的黑科技,所以即使我已经这样做了,我也不会推荐这样做。

1
正如您可能已经知道的那样,“new”属性方法在将对象转换为基类时可能会产生不良副作用。 - kbrimington
1
谢谢,我还不太清楚:这是否解决了SunCache为空的问题,因为Analysis正在将结果保存到Base类中?那么SunCache中的额外变量呢?它们会丢失吗? - JaredBroad
另外,请注意在您的第二种方法中,数据是强类型的;我们只是缺乏对属性调用的编译时类型检查。 - kbrimington
@kbrimington - 实际上,我实现“new”属性方法的方式是安全的,因为当对象转换为基类时,它仍然使用底层的“Data”属性作为其存储。@JaredBroad:我不确定我理解问题-保存的对象是具有所有额外字段和属性的SunData对象。除非您通过将其替换为“new OuterSpaceWhatever()”而积极丢弃该额外信息,否则您会没问题-第二种方法可以防止这种情况发生。如果这是C++代码,则可能会因为“切片”而丢失数据,但在C#中不会。 - Stuart
@Stuart,很抱歉,但“新”的实现并不“安全”。看看这段代码:OuterSpace sun = new Sun(); //这将编译,因为它使用了基类属性 sun.Data = new OuterSpaceData(); //这将编译,因为它使用了Sun的属性 //然而,它将导致运行时转换错误 SunData data = ((Sun)sun).Data; - Rune FS
显示剩余2条评论

10

尝试使用泛型进行操作。下面的类为OuterSpace提供了一个通用的基类,并对其参数施加了约束,以强制执行属性类型的继承规则。出于清晰起见,我省略了小类型中的方法。

public class OuterSpace<TData, TAnalysis, TCache>
    where TData : OuterSpaceData
    where TAnalysis : OuterSpaceAnalysis
    where TCache : OuterSpaceCache
{
    public virtual TData Data { get; set; }
    public virtual TAnalysis Analysis { get; set; }
    public virtual TCache Cache { get; set; }
}
public class OuterSpaceData { }
public class OuterSpaceAnalysis { }
public class OuterSpaceCache { }

public class Sun : OuterSpace<SunData, SunAnalysis, SunCache>
{
    public override SunData Data { get; set; }
    public override SunAnalysis Analysis { get; set; }
    public override SunCache Cache { get; set; }
}
public class SunData : OuterSpaceData { }
public class SunAnalysis : OuterSpaceAnalysis { }
public class SunCache : OuterSpaceCache { }

这种方法的吸引力在于它允许您返回强类型属性,并对这些属性的类型强制执行基本继承约束。这些方法是完全可重写的;然而,请注意,为了避免与基类实现的转换问题,可能需要覆盖重写。


这看起来很漂亮,也许它能够工作,但是我尝试实现后,它变得非常混乱。程序实际上有50-100k长,不得不重新输入每个“OuterSpace”让我有点害怕 :) 谢谢。 - JaredBroad
@Jared:感谢您的回复。这很漂亮,而且确实有效;但是,您可能会发现使用@stuart的方法(第二种方法--尽可能避免new成员声明)并在必要时使用类型转换来获取专门的方法是足够的(例如:var data = mySunDataObject.Data as SunData;)。 - kbrimington
@Jared:还要注意的是,Sun类中的三个重写只有在你打算在getter和setter上有不同的行为时才是必需的。对于像我在示例中所示的普通空getter和setter,您可以删除这些重写并获得相同的效果。 - kbrimington
谢谢,它们最初只是变量,但读到某个地方说你不能重写成员变量? - JaredBroad
@Jared:没错。虽然有些人认为空的getter-setter属性和公共字段之间没有太大区别,但至少属性可以是虚拟/可重写的。如果基类无法知道属性(或字段)的类型,则泛型提供了灵活性,并提供了优越的编译时类型检查;但是,正如您所观察到的那样,维护复杂的泛型类型结构可能具有挑战性。想象一个名为MyBaseClass<TProp1, TProp2, TProp3, TProp4,...>的类型。显然,过度抽象的类是不可取的。知道这是可能的是好事。 - kbrimington

1
如果你正在寻找协方差路线,类似这样的代码应该可以解决问题:
public class OuterSpace<TCacheType> where TCacheType : OuterSpaceCache {
    public virtual OuterSpaceData Data {get; set;}
    public virtual OuterSpaceAnalysis Analysis {get; set;}
    public virtual TCacheType Cache {get; set;}


    public class OuterSpaceData {
        //Lots of basic Data Extraction routines eg
        public virtual GetData();
    }
    public class OuterSpaceAnalysis {
        //Lots of Generic Analysis on Data routines eg
        public virtual GetMean();
    }
    public class OuterSpaceCache {
        //Lots of Caches of Past Analysis Results:
        public Dictionary<AnalysisType, List<Result>> ResultCache;
    }
}

public class Sun : OuterSpace<SunCache> {
    public override SunData Data {get; set;}
    public override SunAnalysis Analysis {get; set;}

    public SunData : OuterSpaceData {
        //Routines to specific get data from the sun eg
        public override GetData();
    }

    public SunAnalysis : OuterSpaceAnalysis {
        //Routines specific to analyse the sun: eg
        public double ReadTemperature();
    }
    public SunCache : OuterSpaceCache {
        //Any data cache's specific to Sun's Analysis
        public Dictionary<AnalysisType, List<Result>> TempCache;
    }
}

提醒一下,这是未编译的代码,我可能完全错误 :) 但这是我认为你想要的尝试...

哦,而且我认为这是一个仅适用于 .Net 4.0 的解决方案,我不认为你可以在早期版本中实现协变性...


0

关于泛型不确定,但是你可以通过简单的多态性实现有限的效果(假设SunData继承自Data等)

public class Sun : OuterSpace 
{
  void SomeInitializationMethod()
  {
    Data = new SunData();
    Analysis = new SunAnalysis(); // etc
  }

    }

// You could also make casting simpler with a generic method on the base
public T GetData<T>()
{
  if (Data is T)
  {
    return Data as T;
  }
  return null;
};

谢谢,最初有类似的东西,但是尽可能避免在代码中散布转换。至少通过选择的答案,转换是集中的.. :\ - JaredBroad

0
如果SunData与Outerspace Data(继承自)是赋值兼容的,那么您无需修改OuterSpaceData的类型。

我需要访问SunCache / SunAnalysis特定的变量/方法(最好不用每次使用它们时都进行强制转换)。这难道不意味着类型需要正确设置吗? - JaredBroad
@JaredBroad,你的意思是你有一个设计上的问题。你确信“从不”(也就是非常非常少)需要向下转型。看起来你正在为功能而派生类,请查看策略模式,并将通用逻辑/功能放在单独的类中并注入这些类型的对象。 - Rune FS
感谢您的建议,我觉得界面也可能有所帮助。等有时间了我会尝试一下。http://en.wikipedia.org/wiki/Strategy_pattern#C.23 - JaredBroad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接