繁体   English   中英

C# 使用派生属性级联派生类

[英]C# Cascading Derived Classes with Derived Properties

因此,我正在设计一个具有多个类的框架,这些类以级联方式相互派生,以执行越来越多的特定任务。 每个 class 也有自己的设置,它们是自己的类。 但是,这些设置应该与类本身的设置具有平行的 inheritance 关系。 这个问题的性质很重要,设置必须与 class 本身分开,因为它们具有特定于我的框架的限制。 编辑:我还想避免使用设置的组合模式,因为它使我正在使用的框架中的工作流程变得非常复杂。所以设置必须是单个对象,而不是多个对象的组合。)我认为这是一种就像两个并行的 class 层次结构。 一个相关的例子可能是这样的:

假设您有一个Vehicle class 和一个随附的 class VehicleSettings ,它存储了每辆车应该具有的相关设置,例如topSpeedacceleration

然后假设您现在有一辆Car和一架Airplane class,它们都继承自Vehicle class。 但是现在您还创建CarSettings ,它继承自VehicleSettings class 但添加了成员gear 然后还有AirplaneSettings ,继承自VehicleSettings ,但添加了成员drag

为了展示我如何继续这种模式,假设现在我有一个新的 class SportsCar ,它继承自Car 我创建了相应的设置SportsCarSettings ,它继承自CarSettings并添加了一个成员sportMode

设置这样的 class 层次结构的最佳方法是什么,以便我还可以考虑设置的派生类?

理想情况下,我希望这样的示例能够像这样工作:

public class VehicleSettings
{
    public float topSpeed;
    public float acceleration;
    // I am explicitly not adding a constructor as these settings get initialized and
    // modified by another object
}

public class Vehicle
{
    public float speed = 0;
    protected VehicleSettings settings;
    
    // Similarly here, there is no constructor since it is necessary for my purposes 
    // to have another object assign and change the settings
    
    protected virtual float SpeedEquation(float dt)
    {
        return settings.acceleration * dt;
    }
    
    public virtual void UpdateSpeed(float dt)
    {
        speed += SpeedEquation(dt)
        if(speed > settings.topSpeed)
        {
            speed = settings.topSpeed;
        }
    }
}

public class CarSettings : VehicleSettings
{
    public int gear;
}
    
public class Car : Vehicle
{
    // Won't compile
    public CarSettings settings;
    
    // Example of the derived class needing to use
    // its own settings in an override of a parent
    // method
    protected override float SpeedEquation(float dt)
    {
        return base.SpeedEquation(dt) * settings.gear;
    }
}

public class AirplaneSettings : VehicleSettings
{
    public float drag;
}
    
public class Airplane : Vehicle
{
    // Won't compile
    public Airplane settings;
    
    // Another example of the derived class needing to use
    // its own settings in an override of a parent
    // method
    public override void UpdateSpeed(float dt)
    {
        base.UpdateSpeed(dt) 
        speed -= settings.drag * dt;
    }
}

public class SportsCarSettings : CarSettings
{
    public bool sportMode;
}
    
public class SportsCar : Car
{
    // Won't compile
    public SportsCarSettings settings;
    
    // Here is again an example of a further derived class needing
    // to use its own settings to override a parent method
    // This example is here to negate the notion of using generic
    // types only once and not in the more complicated way mentioned
    // below
    public override float SpeedEquation(float dt)
    {
        return (settings.acceleration + (settings.sportMode ? 2 : 1)) * dt;
    }
}

查看一些可能的解决方案

  1. 使用泛型类型。 例如,我可以让它成为public class Vehicle<T>: where T: VehicleSettings我可以让它有T settings ,这样当CarAirplaneVehicle继承时,他们可以这样做: public Car<T>: Vehicle<T> where T: CarSettingspublic Airplane<T>: Vehicle<T> where T: AirplaneSettings等等。 如果我想实例化一个Car而不插入泛型类型,这确实会使事情变得有些复杂。 因为那时我将不得不创建Car<T>的子 class Car如下: public class Car: Car<CarSettings> { } 对于每个派生类型,我都必须做类似的事情。

  2. 在必要的方法中使用类型转换。 例如,我可以修改Car class 如下:

     public class Car: Vehicle { // Don't reassign settings and instead leave them // as VehicleSettings // Cast settings to CarSettings and store that copy // locally for use in the method protected override float SpeedEquation(float dt) { CarSettings settings = (CarSettings)this.settings; return base.SpeedEquation(dt) * settings.gear; } }
  3. 我还看到了一个使用示例中的属性的建议,但这似乎很笨拙,主要是因为它似乎并没有真正解决问题。 即使从属性返回动态类型,您仍然必须将返回的值转换为所需的类型。 如果这是 go 关于它的正确方法,我希望能解释一下如何正确实现它。

  4. 可以使用new关键字来隐藏父类的settings版本,并将其替换为对应子类设置类型的名为settings的新变量,但我认为通常不建议这样做,主要是因为关系复杂化到原始父类的“设置”,这会影响继承方法中该成员的 scope。

所以我的问题是哪个是这个问题的最佳解决方案? 它是这些方法之一,还是我在 C# 语法中遗漏了一些非常重要的东西?

编辑:

由于有人提到了奇怪重复的模板模式或 CRTP,我想提一下我认为这是如何不同的。

在 CRTP 中,您有类似Class<T>: where T: Class<T>的内容。 或者类似地,您可能会遇到类似Derived<T>: T where T: Base<Derived>的情况。

However, that is more about a class which needs to interact with an objects that are of the same type as itself or a derived class that needs to interact with base class objects that need to interact with derived class objects. 无论哪种方式,那里的关系都是循环的。

在这里,关系是平行的。 并不是说 Car 会与 Car 交互,也不是 SportsCar 会与 Vehicle 交互。 它是汽车需要有汽车设置。 SportsCar 需要具有 SportsCar 设置,但是当您向上移动 inheritance 树时,这些设置只会略有变化。 所以我认为如果像 C# 这样的深度 OO 语言需要跳过这么多圈来支持“并行继承”,或者换句话说,不仅仅是 object 本身在它们的关系中“进化”,这似乎有点荒谬从父母到孩子,也就是会员自己这样做。

当您没有强类型语言时,例如 Python,您可以免费获得这个概念,因为对于我们的示例,只要我分配给 object 的任何特定实例的设置具有相关属性,它的类型就会无关紧要。 因此,我认为有时强类型范式可能会成为障碍,因为我希望 object 由其可访问属性而不是在这种情况下通过设置定义的类型来定义。 C# 中的强类型系统迫使我制作模板模板或其他一些奇怪的结构来解决这个问题。

编辑2:

我发现选项 1 存在重大问题。假设我想列出车辆列表和设置列表。 假设我有VehicleCarAirplaneSportsCar ,我想将它们放入列表中,然后遍历车辆列表和设置并将每个设置分配给其各自的车辆。 设置可以放在VehicleSettings类型的列表中,但是没有可以放入车辆的类型(除了Object ),因为 Vehicle 的父 class 是Vehicle Vehicle<VehicleSettings> ,父Car<CarSettings>等。因此,您使用 generics 失去的是干净的父子层次结构,它使得将相似的对象分组到列表、字典等中非常舒适。

但是,使用选项 2,这是可能的。 如果没有办法避免上述问题,选项 2 尽管在某些方面不舒服,但似乎是最易于管理的方法。

对于我提出的解决方案,我们将使用 cast,这使其类似于解决方案 2。但我们只会将它们放在属性中(而不是像解决方案 3 这样的属性)。 我们还将像解决方案 4 中一样使用new关键字。

我避免使用 generics(解决方案 1)的解决方案,因为该问题的“编辑 2”下描述的问题。 但是,我想提一下,您可以拥有class Vehicle<T>: Vehicle where T: VehicleSettings ,我相信这就是 Charlieface 在评论中的意思。 根据回到解决方案 2,你可以像我在这个答案中所做的那样加上 generics 如果你愿意。


建议的解决方案

这是我提出的解决方案:

class VehicleSettings{}

class Vehicle
{
    private VehicleSettings _settings;

    public VehicleSettings Settings{get => _settings; set => SetSettings(value);}
    
    protected virtual void SetSettings(VehicleSettings settings)
    {
        _settings = settings;
    }
}

注意,我正在创建一个private字段。 这个很重要。 它允许我们控制对它的访问。 我们将控制我们如何阅读它,以及我们如何编写它。

我们只会在Settings getter 中读取它:

public VehicleSettings Settings{get => _settings; set => SetSettings(value);}

如果您想象_settings可能包含派生类型,那么从派生类型到基类型没有问题。 因此,我们不需要派生类来处理那部分。

另一方面,我们只会将其编写为SetSettings方法:

    protected virtual void SetSettings(VehicleSettings settings)
    {
        _settings = settings;
    }

这个方法是virtual的。 如果其他代码试图设置错误的设置,这将允许我们抛出。 例如,如果我们有一个从Vehicle派生的 class ,比如Car ,但它存储为Vehicle类型的变量,我们尝试将VehicleSettings写入Settings ...编译器无法阻止这种情况。 据编译器所知,类型是正确的。 我们能做的最好的就是throw

请记住,这些必须是唯一使用_settings的地方。 因此,任何其他方法都必须使用该属性:

class VehicleSettings
{
    public float Acceleration;
}

class Vehicle
{
    private VehicleSettings _settings;

    public VehicleSettings Settings{get => _settings; set => OnSetSettings(value);}
    
    protected virtual void OnSetSettings(VehicleSettings settings)
    {
        _settings = settings;
    }
    
    public virtual float SpeedEquation(float dt)
    {
        return Settings.Acceleration * dt;
    }
}

显然,这将扩展到派生类。 这就是 go 关于实现它们的方式:

class CarSettings : VehicleSettings
{
    public int Gear;
}

class Car : Vehicle
{   
    public new CarSettings Settings
    {
        get => (CarSettings)base.Settings; // should not throw
        set => base.Settings = value;
    }
    
    protected override void OnSetSettings(VehicleSettings settings)
    {
        Settings = (CarSettings)settings; // can throw
    }
    
    public override float SpeedEquation(float dt)
    {
        return base.SpeedEquation(dt) * Settings.Gear;
    }
}

现在,我添加了一个新的Settings属性,它具有正确的类型。 这样,使用它的代码就会得到它。 但是,我们不想添加新的支持字段。 所以这个属性将简单地委托给基础属性。 那应该是我们访问基本属性的唯一地方。

我们还知道基础 class 将调用OnSetSettings ,因此我们需要覆盖它。 OnSetSettings确保类型正确很重要。 通过使用新属性而不是基类型中的属性,编译器提醒我们进行强制转换。

请注意,如果类型错误,这些转换将导致异常。 但是,只要OnSetSettings正确并且您不写入_settings其他任何地方,getter 就不应抛出。 您甚至可以考虑将 getter 与base.Settings as CarSettings

此外,您只需要这两个演员表。 代码的 rest 可以并且应该使用该属性。

第三代class? 当然。 相同的模式:

class SportsCarSettings : CarSettings
{
    public bool SportMode;
}

class SportsCar : Car
{
    public new SportsCarSettings Settings
    {
        get => (SportsCarSettings)base.Settings;
        set => base.Settings = value;
    }
    
    protected override void OnSetSettings(VehicleSettings settings)
    {
        Settings = (SportsCarSettings)settings;
    }
    
    public override float SpeedEquation(float dt)
    {
        return (Settings.Acceleration + (Settings.SportMode ? 2 : 1)) * dt;
    }
}

如您所见,此解决方案的一个缺点是它需要一定程度的额外纪律。 开发人员必须记住,除了Settings的 getter 和方法SetSetting _settings 同样,不要访问Settings Settings (在派生类中)。

考虑使用代码生成来实现该模式。 要么是 T4,要么是新出现的 Source Generators。


如果我们不需要二传手

需要写设置吗? 正如我之前解释的,二传手可能会抛出。 如果您有一个Vehicle类型的变量,该变量具有Car类型的 object 并尝试将VehiculeSettings设置为它(或其他东西,如SportCarSettings )。 但是,getter 不会抛出。

在 C# 9.0 中,虚拟方法(实际上是属性)存在返回类型协方差。 所以你可以这样做:

class VehicleSettings {}

class CarSettings: VehicleSettings {}

class Vehicle
{
    public virtual VehicleSettings Settings
    {
        get;
        //set; // won't compile
    }
}

class Car: Vehicle
{
    public override CarSettings Settings
    {
        get;
        //set; // won't compile
    }
}

但是,如果您添加设置器,则不会编译。 啊,但是您可以轻松添加“ SetSettings ”方法。 它会使用法有点倒退,但实现起来更容易:

class VehicleSettings {}

class Vehicle
{
    private VehicleSettings _settings;
    
    public virtual VehicleSettings Settings => _settings;
    
    public void SetSettings(VehicleSettings Settings)
    {
        OnSetSettings(Settings);
    }
    
    protected virtual void OnSetSettings(VehicleSettings settings)
    {
        _settings = settings;
    }
}

与建议的解决方案类似,我们将限制对支持字段的访问。 不同之处在于,现在属性是虚拟的并且没有 setter(因此我们可以使用适当的类型覆盖它,而不是使用new关键字),而是我们有一个SetSettings方法。

class CarSettings: VehicleSettings {}

class Car: Vehicle
{
    public override CarSettings Settings{get;}
    
    public void SetSettings(CarSettings settings)
    {
        base.OnSetSettings(settings);
    }

    protected override void OnSetSettings(VehicleSettings settings)
    {
        SetSettings((CarSettings)settings);
    }
}

另请注意,我们不必在方法上使用关键字new 相反,我们正在添加一个新的重载。 就像提议的解决方案一样,这个解决方案需要纪律。 它的缺点是不强制转换不是编译错误,它只是调用重载(导致堆栈溢出,我不是在谈论这个网站)。


如果我们可以有构造函数

构造函数是禁忌吗? 因为你可以这样做:

class VehicleSettings {}

class Vehicle
{
    protected VehicleSettings SettingsField = new VehicleSettings();

    public VehicleSettings Settings
    {
        get => SettingsField;
        set
        {
            if (SettingsField.GetType() == value.GetType())
            {
                SettingsField = value;
                return;
            }

            throw new ArgumentException();            
        }
    }
}

并派生出class:

class CarSettings: VehicleSettings {}

class Car: Vehicle
{
    public Car()
    {
        SettingsField = new CarSettings();
    }

    public new CarSettings Settings
    {
        get => (CarSettings)base.Settings;
        set => base.Settings = value;
    }
}

在这里,setter 强制Settings的新值必须与它当前具有的类型相同。 这当然会带来一个问题:我们需要使用有效类型对其进行初始化,因此我们需要一个构造函数。 为了方便起见,我仍然添加了一个新属性。

一个重要的优势是没有什么是虚拟的。 所以我们不需要担心是否正确覆盖和覆盖。 我们仍然需要记住初始化。


我考虑使用IsAssignableFromIsInstanceOfType ,但这将允许缩小类型。 也就是说,它允许将SportsCarSettings分配给Car ,但是您将无法重新分配CarSettings

一种解决方法是存储类型......从构造函数(或自定义属性)设置它。 所以你要声明protected Type SettingsType = typeof(VehicleSettings); 使用SettingsType = typeof(CarSettings); 并使用SettingsType.IsAssignableFrom(value?.GetType())进行比较。

现在,如果你问我,那是很多重复。


如果我们能有反思

看看这段代码:

class CarSettings: VehicleSettings {}

class Car: Vehicle
{
    public new CarSettings Settings
    {
        get => (CarSettings)base.Settings;
        set => base.Settings = value;
    }
}

它可以工作吗? 用反思。 通过声明这个属性,我们指定了基础 class 应该测试的类型。

基础 class 是这个生物:

class VehicleSettings {}

class Vehicle
{
    protected VehicleSettings? SettingsField;
    private Type? SettingsType;

    public VehicleSettings Settings
    {
        get => SettingsField ??= new VehicleSettings();
        set
        {
            SettingsType ??= this.GetType().GetProperties()
                .First(p => p.DeclaringType == this.GetType() && p.Name == nameof(Settings))
                .PropertyType;
            if (SettingsType.IsAssignableFrom(value?.GetType()))
            {
                SettingsField = value;
                return;
            }

            throw new ArgumentException();            
        }
    }
}

这次我选择了惰性初始化,所以没有构造函数。 但是,是的,这种反射在构造函数中起作用。 它通过反射获取在派生类型上声明的属性的类型,并检查我们尝试设置的值是否匹配。

我不得不使用GetProperties ,因为GetProperty要么给我System.Reflection.AmbiguousMatchException要么null ,这取决于绑定标志。

结果是忘记在派生的 class 中声明Settings意味着您将无法设置该属性(它会抛出,因为它确实找到了该属性)。

而且,是的,这将持续到后代。 只是不要忘记声明一个新的Settings

class SportsCarSettings : CarSettings{}

class SportsCar: Car
{
    public new SportsCarSettings Settings
    {
        get => (SportsCarSettings)base.Settings;
        set => base.Settings = value;
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM