问题描述
所以我正在设计一个具有多个类的框架,这些类以级联方式从另一个类派生出来,以便执行越来越多的特定任务。每个类也有自己的设置,它们是自己的类。但是,设置应该与类本身具有并行继承关系。对于这个问题的性质来说,重要的是设置必须与类本身分开,因为它们具有特定于我的框架的约束。 (编辑:我还想避免使用设置的组合模式,因为它极大地使我使用的框架中的工作流程复杂化。因此设置必须是单个对象,而不是多个对象的组合。)那是我认为这有点像两个平行的类层次结构。一个相关的例子可能是这样的:
假设您有一个 Vehicle
类和一个伴随类 VehicleSettings
,用于存储每辆车应具有的相关设置,例如 topSpeed
和 acceleration
。>
然后假设您现在有一个 Car
和一个 Airplane
类,它们都继承自 Vehicle
类。但现在您还创建了 CarSettings
,它继承自 VehicleSettings
类,但添加了成员 gear
。然后还有 AirplaneSettings
,继承自 VehicleSettings
,但添加成员 drag
。
为了展示我如何继续这种模式,假设现在我有一个新类 SportsCar
,它继承自 Car
。我创建了相应的设置 SportsCarSettings
,它继承自 CarSettings
并添加了一个成员 sportMode
。
设置这样的类层次结构以便我也可以为设置考虑派生类的最佳方法是什么?
理想情况下,我希望这样的例子像这样工作:
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;
}
}
看几个可能的解决方案
-
使用泛型类型。例如,我可以拥有它以便它是
public class Vehicle<T> : where T : VehicleSettings
和我可以让它有T settings
行,这样当Car
和Airplane
从Vehicle
继承时,他们可以这样做:{{1} } 和public Car<T> : Vehicle<T> where T : CarSettings
,依此类推。 如果我想实例化public Airplane<T> : Vehicle<T> where T : AirplaneSettings
而不插入泛型类型,这确实会使事情变得有些复杂。因为那样我就必须创建一个Car
的子类Car
,如下所示:Car<T>
。而且我必须对每个派生类型都做类似的事情。 -
在必要的方法中使用类型转换。例如,我可以按如下方式修改
public class Car : Car<CarSettings> { }
类:Car
-
我还看到了一个建议使用 this 示例中的属性,但这似乎很笨拙,主要是因为它似乎并没有真正解决问题。即使从属性返回动态类型,您仍然必须将返回的值转换为所需的类型。如果这是正确的方法,我将不胜感激有关如何正确实施的解释。
-
可以使用
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; } }
关键字隐藏父类的new
版本并将其替换为相应子类设置的名为settings
的新变量类型,但我认为通常不建议这样做,主要是为了使与原始父类的“设置”的关系复杂化,这会影响继承方法中该成员的范围。
所以我的问题是这个问题的最佳解决方案是什么?这是其中一种方法,还是我遗漏了 C# 语法中非常重要的一些东西?
编辑:
既然有人提到了 Curiously Recurring Template Pattern 或 CRTP,我想提一下我认为它的不同之处。
在 CRTP 中,您有类似 settings
的内容。或者类似地,您可能会遇到类似 Class<T> : where T : Class<T>
之类的问题。
然而,这更多地是关于一个类需要与一个与其自身类型相同的对象进行交互,或者一个派生类需要与需要与派生类对象交互的基类对象进行交互。 无论哪种方式,那里的关系都是循环的。
在这里,关系是平行的。并不是说汽车会与汽车互动,也不是跑车会与车辆互动。它是汽车需要有汽车设置。 SportsCar 需要有 SportsCar 设置,但是当您向上移动继承树时,这些设置只会发生轻微的变化。所以我认为,如果像 C# 这样的深度面向对象的语言需要跳过这么多圈来支持“并行继承”,或者换句话说,在它们的关系中“进化”的不仅仅是对象本身,这似乎有点荒谬从父母到孩子,还有成员自己这样做。
当您没有强类型语言时,例如 Python,您可以免费获得这个概念,因为对于我们的示例,只要我分配给对象的任何特定实例的设置具有相关属性,它的类型无关紧要。所以我认为有时强类型范式可能会成为一个障碍,因为我希望对象由其可访问的属性定义,而不是在这种情况下通过设置定义的类型。 C# 中的强类型系统迫使我制作模板的模板或其他一些奇怪的结构来解决这个问题。
编辑 2:
我发现选项 1 存在重大问题。假设我想制作一个车辆列表和一个设置列表。假设我有一个 Derived<T> : T where T : Base<Derived>
、一个 Vehicle
、一个 Car
和一个 Airplane
,我想将它们放入一个列表中,然后遍历车辆和设置列表并将每个设置分配给各自的车辆。设置可以放在SportsCar
类型的列表中,但是没有任何类型(除了VehicleSettings
)可以放入车辆,因为Object
的父类是{{ 1}},Car 的父类是 Vehicle
等。因此,泛型丢失的是干净的父子层次结构,这使得将相似的对象分组到列表、字典等中变得如此舒适。
但是,使用选项 2,这是可能的。如果没有办法避免上述问题,选项 2 尽管在某些方面不舒服,但似乎是最容易控制的方法。
解决方法
对于我提出的解决方案,我们将使用强制转换,这使得它类似于解决方案 2。但我们只会在属性中使用它们(而不是像解决方案 3 那样的属性)。我们还将像解决方案 4 一样使用 new
关键字。
我避免了使用泛型的解决方案(解决方案 1),因为该问题的“编辑 2”下描述的问题。但是,我想提一下,您可以使用 class Vehicle<T> : Vehicle where T : VehicleSettings
,我相信这是 Charlieface 在评论中的意思。根据回到解决方案 2 的内容,您可以像我在此答案中所做的那样,如果需要,还可以使用泛型。
建议的解决方案
这是我提出的解决方案:
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
的类,比如说 Car
,但它存储为类型为 Vehicle
的变量,我们尝试将 VehicleSettings
写入 { {1}}... 编译器无法阻止这种情况。据编译器所知,类型是正确的。我们能做的最好的是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;
}
}
现在,我添加了一个新的 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
,所以我们需要覆盖它。 OnSetSettings
确保类型正确很重要。 并且通过使用新属性而不是基类型中的属性,编译器提醒我们进行强制转换。
请注意,如果类型错误,这些强制转换将导致异常。但是,只要 OnSetSettings
是正确的并且您没有在其他任何地方写入 OnSetSettings
,getter 就不应该抛出异常。 您甚至可以考虑使用 _settings
编写 getter。
此外,您只需要这两个演员表。其余代码可以而且应该使用该属性。
第三代班?当然。相同的模式:
base.Settings as CarSettings
如您所见,此解决方案的一个缺点是它需要某种程度的额外纪律。开发人员必须记住,除了 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;
}
}
的 getter 和方法 _settings
之外,不要访问 Settings
。同样,不要访问 SetSetting
属性之外的基础 Settings
属性(在派生类中)。
考虑使用代码生成来实现模式。要么是 T4,要么是新奇的 Source Generators。
如果我们不需要 setter
你需要写设置吗?正如我之前解释过的,setter 可能会抛出异常。如果您有一个 Settings
类型的变量,它有一个 Vehicle
类型的对象,并尝试将 Car
设置为它(或其他东西,如 VehiculeSettings
)。但是,getter 不会抛出。
在 C# 9.0 中,虚方法(和属性,实际上)有返回类型协变。所以你可以这样做:
SportCarSettings
但是如果你添加了 setter,那将无法编译。啊,但您可以轻松添加“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
与提议的解决方案类似,我们将限制对支持字段的访问。不同之处在于现在该属性是虚拟的并且没有 setter(因此我们可以使用适当的类型覆盖它,而不是使用 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;
}
}
关键字),而是我们有一个 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();
}
}
}
这里,setter 强制 class CarSettings: VehicleSettings {}
class Car: Vehicle
{
public Car()
{
SettingsField = new CarSettings();
}
public new CarSettings Settings
{
get => (CarSettings)base.Settings;
set => base.Settings = value;
}
}
的新值必须与它当前的类型相同。这当然会带来一个问题:我们需要使用有效类型对其进行初始化,因此我们需要一个构造函数。 为了方便起见,我仍然添加了一个新属性。
一个重要的优势是没有什么是虚拟的。所以我们不需要担心覆盖和覆盖是否正确。 我们仍然需要记住初始化。
我考虑过使用 Settings
或 IsAssignableFrom
,但这可以缩小类型。也就是说,它允许将 IsInstanceOfType
分配给 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 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
意味着您将无法设置该属性(它会抛出,因为它确实找到了该属性)。
而且,是的,这会延续到后代。只是不要忘记声明一个新的 Settings
:
Settings