【问题标题】:Automatically extend object to some inheriting class自动将对象扩展到某个继承类
【发布时间】:2012-08-27 05:47:20
【问题描述】:

假设我有以下声明:

public class ClassA
{
  public string FieldA = "Something first";
}

public class ClassB : ClassA
{
  public string FieldB = "Something second";
}

众所周知,ClassB 的对象可以在需要ClassA 的每个地方使用。例如一个方法

public void DoSthWithObject(ClassA a);

可以这样调用:

ClassB b = new ClassB();
DoSthWithObject(b);

也可以将继承的类型转换为父类型:

ClassA a = (b as ClassA);

所以编译器/框架知道如何“使用”ClassB 的数据,其中需要ClassA

现在的问题是:有可能以某种方式反过来吗?假设我有一个ClassA 类型的变量,我想开始使用它作为ClassB但同时保留ClassB 继承自ClassA 的所有字段并且不实例化新对象?我知道这需要扩展为ClassA 的对象分配的内存以容纳ClassB 的字段。

简单来说我想做这样的事情:

ClassA a = new ClassA();
ClassB b = (a as ClassB); // some magic happens here

(我知道我可以实现IConvertible 并使用Convert 或提供一些克隆机制,但我想避免这样的解决方案。)

编辑

由于答案似乎集中在提出替代解决方案或警告我什至想按照我的意愿去做,我决定稍微改变我的问题并提出赏金,这样也许我可以获得更好的答案。

首先,我希望它与语言无关。

第二:我真的确实明白,不是每个工具都是锤子,我可以(我希望)区分 Jack 和 简。我也不需要关于基本 OOP 的课程(除非有人能证明我犯了一个基本的概念错误,而我目前显然没有意识到)。我想知道的是如果以及为什么自动父类到子类的转换在逻辑上是不可能的,以及它是否可能(从 cmets 看来,有些语言是这样的 em> 可能)然后为什么它很危险/充满缺陷。这也是非常有趣的,它实际上是如何用这些语言在技术上完成的,“支持”它(或至少不禁止它)。

但我确实期待真正的编程示例,而不仅仅是隐喻。

明确一点:我不是在寻找有关向下转换先前向上转换的对象的信息。这不是这里的情况。

如果我是一个希望同时在同一个表列上聚合和分组的人 - 请告诉我这个。我知道我很聪明,可以理解。 :-) 我只是不想让这个问题留下一种我没有理解的关于 OOP 的基本知识。

通知

在大卫回答下的评论中,我提到了 浮点数,但实际上指的是 实数(数学术语)。这是因为我的英语不太好。请停止挑剔。 :]

谢谢

我要感谢大家的回答。我决定悬赏史蒂夫,但这并不意味着我认为这个问题已经结束。我仍在寻找反对自动对象转换的论据。我必须管理的是,我对那些警告我最多的人无法提供与转换相关(而不是与演员表相关)的明确示例感到有点失望。

根据我自己的研究,我决定在单独的答案中注明一些事情(以防有人发现它们有用)。请随时编辑和扩展列表。

【问题讨论】:

  • 这只是一种好奇心。但它来自多层服务,其中每一层都需要在将对象传递给底层之前向对象添加一些信息。我希望每一层都使用各自的继承层次结构(以避免传递空字段)。
  • 然后使用多个接口...或组合。
  • 谢谢,我现在正在使用合成。我只是想知道不同的解决方案。

标签: oop inheritance casting


【解决方案1】:

不!从类的基本描述来看,ClassA 不是 ClassB。您描述的关系不是对称/自反的。可以这样想:每个锤子都是工具,但不是每个工具都是锤子。如果您需要转动螺丝,则需要工具,但锤子无法完成这项工作。用这个类比替换上面的类定义,我认为从表面上看,为什么这种关系不起作用已经很清楚了。

【讨论】:

  • +1 '每个锤子都是工具,但不是每个工具都是锤子' - 完美。
  • 亲爱的大卫,每个整数都是浮点数,但不是每个浮点数都是整数。然而,我们有时需要双向转换。 :]
  • @KubaWyrostek 没有整数也是浮点数。编译器只是在必要时插入转换逻辑。如果您希望类之间的关系具有相同的效果,请定义隐式转换运算符方法。
  • @Kuba ,每个整数都是浮点数?你需要回去忘掉一些东西。地图不是领土
  • 只有在 C# 中具有继承的解决方案是显式转换,这就是我们一直在争执的原因。它不需要详细说明,我拒绝将其作为我的名字反对它的答案,这不是答案。这是一袋你还没有遇到的可怕问题。
【解决方案2】:

这是特定语言的实现细节,而不是面向对象思维的固有限制。我可以想象许多需要这种行为的场景——也许 Shape 是 Rectangle,但我们决定将其更改为 Square(因为正方形是矩形的一种)。正如您所指出的,在 Java 或 C# 等经典 OO 语言中做这种事情是很痛苦的。

语言设计都是关于权衡的,而这个特殊的特性在 C# 或 Java 和许多其他 OO 语言中并没有实现。看看为什么...

让我们考虑一下我的 Rectangle => Square 示例。也许在我的新语言 Z## 中,我选择在内部将对象实例存储为它们继承的对象的组合。所以我的 Rectangle 是内存中的一个 Shape 实例,+一些属性和方法。那么 Square 只是一个 Rectangle 形状实例 + 一些属性和方法。将 Rectangle 更改为 Square 很容易,因为我不需要转换任何东西 - 只需在实例化 Square 时提供现成的 Rectangle 即可。

像 C# 和 Java 这样的语言实际上并没有这样做 - 它们存储“扁平化”的对象实例 - 在 C# 中,Square 实例包含 Rectangle 实例在内存中 -它只是共享一个矩形的接口。所以在这些语言中实现这个特性要慢得多,因为它们实际上必须将整个扁平结构复制到新的内存位置。

【讨论】:

  • 史蒂夫,我发现你的答案与我在这里的小型研究最密切相关,尽管它在理论上并不实用。 :] 谢谢。
【解决方案3】:

我和@DavidW 在一起。
但有时继承的问题可以通过组合来解决。

请阅读此wikipedia artice

另一种选择是使用接口。

public interface IProvideSomeData
{
    string Data { get; }
}

public class ClassA, IProvideSomeData
{
  public string FieldA = "Something first";

  string IProvideSomeData.Data { 
     get { return FieldA; }
  }
}

public class ClassB : ClassA, IProvideSomeData
{
  public string FieldB = "Something second";

  string IProvideSomeData.Data { 
     get { return FieldB; }
  }
}

public class ClassC : IProvideSomeData
{
  public string FieldC = "Something second";

  string IProvideSomeData.Data { 
     get { return FieldC; }
  }
}    

public void DoSthWithObject(IProvideSomeData a);

这种方式DoSthWithObject 依赖于接口,因此可重用性更高。

【讨论】:

    【解决方案4】:

    我在 C# 中尝试解决您的问题。我尝试使用泛型(但可以使用类型参数和强制类型转换)来实现语言不可知(无 c# 静态扩展方法)。

    对于这样的全自动系统,应该创建一个新的泛型类并将其用于具有以下逻辑的所有类属性:如果存在则搜索内部类,如果不存在则返回私有类(可能还需要一些自省)。

    我尝试回答的场景是您第一次提交的场景:

    ClassA a = new ClassA(); ClassB b = (a as ClassB); // 一些魔法 发生在这里

    using System;
    namespace test
    {
    /// <summary>
    /// Base class A
    /// </summary>
    public class ClassA
    {
        private string prop1;
    
        public ClassA InternalClassA { get; set; }
    
        public string Prop1
        {
            get
            {
                if (this.InternalClassA == null)
                {
                    return prop1;
                }
                else
                {
                    return this.InternalClassA.Prop1;
                }
            }
    
            set
            {
                if (this.InternalClassA == null)
                {
                    this.prop1 = value;
                }
                else
                {
                    this.InternalClassA.Prop1 = value;
                }
            }
        }
    
        public T AsClass<T>() where T : ClassA
        {
            T obj = Activator.CreateInstance<T>();
            obj.InternalClassA = this;
            return obj;
        }
    }
    }
    
    namespace test
    {
        /// <summary>
        /// Child class B
        /// </summary>
        public class ClassB : ClassA
        {
            public string Prop2 { get; set; }
        }
    }
    
    using System;
    namespace test
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Instantiate A
                ClassA a = new ClassA();
                a.Prop1 = "foo";
    
                Console.WriteLine("Base a.prop1: " + a.Prop1);
                // a as ClassB
                ClassB b = a.AsClass<ClassB>();
                b.Prop2 = "bar";
    
                Console.WriteLine("Base b.prop1: " + b.Prop1);
                Console.WriteLine("Base b.prop2: " + b.Prop2);
    
                // Share memory
                b.Prop1 = "foo2";
    
                Console.WriteLine("Modified a.prop1: " + a.Prop1);
                Console.WriteLine("Modified b.prop1: " + b.Prop1);
                Console.Read();
            }
        }
    }
    

    结果:

    Base a.prop1: foo
    Base b.prop1: foo
    Base b.prop2: bar
    Modified a.prop1: foo2
    Modified b.prop1: foo2
    

    我希望它能给你一些想法来实现你想要的。

    【讨论】:

    • 感谢文斯,感谢您提供这种出色的替代方法。 self-composition 这个想法绝对令人惊叹的是B 不需要知道A 的详细信息就可以成功“克隆”(包装)它。我能想到的唯一限制是 A 必须准备好按设计“可包装”(只要您记得以这种方式包装每个字段,这不是限制)。
    【解决方案5】:

    这里的问题是您想要实现的目标与继承关系不大。

    事实上,假设你有这个:

    class A {
    
        int a;
    
        public A(int a){
    
            this.a = a;
    
        }
    
        public int get(){
    
            return a;
    
        }
    
        public String whoAmI(){
    
           return "I am class A";
    
        }
    
    }
    
    class B extends A {
    
        int b;
    
        public B(int a, int b){
    
            super(a);
    
            this.b = b;
    
        }
    
        public int getA(){
    
            return super.get();
    
        }
    
        public int getB(){
    
            return b;
    
        }
    
        public String whoAmI(){
    
           return "I am class B";
    
        }
    
    }
    

    所以现在你可以做休闲的事了:

    A a = new A(2); // Create a new instance of A
    

    B b = new B(3,2); // Create a new instance of B
    

    请注意,创建 B 的新实例时,您还必须传递用于创建超 A 类的参数,就像同时实例化它们一样。这就是继承的意思,构建 B 实际上是在构建新的 A 元素以及 B 特有的一些扩展特征。

    你想做的更像是:

    A a = new A(2);
    B b = new B(3,a);
    

    让我们看看 B 类的样子:

    class B {
    
        A a;
        int b;
    
        public B(int b, A a){
    
            this.b = b;
    
            this.a = a;
    
        }
    
        public int getA(){
    
            return a.get();
    
        }
    
        public int getB(){
    
            return b;
    
        }
    
        public String whoAmI(){
    
            return "I am class B";
    
        }
    
    }
    

    在这里你看到你可以实现你想要的东西,但在这种情况下,B 不是 A 的孩子,它更像是它的包装器,A 只是 B 属性。 您还可以向 B 添加如下方法:

    public A a(){
    
        return a;
    
    }
    

    然后只需调用:

    b.a.get(); // which would be just like calling  b.getA()
    

    看到这与继承无关,因为后者更多的是关于“当我创建一个子类时,我实际上也一起创建了一个父类,因此能够在任何地方使用子类好像 它是父母” ...

    编辑

    好吧,我想出了一个例子,用代码显示@David W 在他的回答中所说的内容:

    class Tool {
    
        int age;
    
        int value;
    
        public Tool(int age, int value){
    
            this.age = age;
            this.value = value;
    
        }
    
        public void use(){
    
            this.age++;
            this.value--;
    
        }
    
    }
    
    class ScrewDriver extends Tool{
    
        boolean starTip;
    
        int tipJamming = 0;
    
        public ScrewDriver(int age, int value, boolean starTip){
    
            super(age,value);
            this.starTip = starTip;
    
        }
    
        public void screw(){
    
            super.use();
    
            this.tipJamming++;
    
        }
    
    }
    
    class Hammer extends Tool{
    
        int weight;
    
        public Hammer(int age, int value, int weight){
    
            super(age,value);
            this.weight = weight;
    
        }
    
        public void hammer(){
    
            super.use();
    
        }
    
    }
    

    所以你有一个父类 Tool 和两个子类 ScrewDriver 和 Hammer。如果你说的是可能的,那么你可以这样写:

    Tool screwDriver = new screwDriver(1,10,true); //simple inheritance polymorphism
    // Now if you could cast Tool to Hammer you could write
    ((Hammer)screwDriver).hammer();
    

    这意味着您实际上将螺丝刀用作锤子,但这实际上打破了继承背后的所有想法,因为如果您可以将任何工具用作彼此,那么定义它们不同的工具有什么需要呢?您可以在 Tool 类中实现您需要的所有方法,并在需要时根据上下文使用它们,例如:

    class Tool{
    
        ... attributes and constructor ...
    
        pulic void use(){
    
            this.age++;
            this.value--;
    
        }
    
        public void screw(){
    
            use();
            this.tipJamming++;
    
        }
    
        public void hammer(){
    
            use();
    
        }
    
    }
    

    然后

    Tool screwDriver = new Tool(...);
    Tool hammer = new Tool(...);
    
    screwDriver.screw();
    hammer.hammer();
    

    【讨论】:

    • 嗯...如果我错了,请纠正我,但在我看来,这实际上是相反的。在第一种情况下(继承),您创建了两个不共享任何内容的不同对象。在第二种情况下(包装),如果您更改A 的状态,则b.getA() 返回的值也会更改,因此保持连接(不是状态,而是连接)。是吗?
    • 在我看来,我正在寻找“介于”您的两个示例之间的东西。所以class B extends Apublic B(int b, A a) 然后是this.b = b; this.a = a.get();。所以它是一个副本。但是由于不再需要A(我们在B 中有数据),我希望避免重新实例化。
    • 谢谢你顺便说一句,这就是我正在寻找的答案。 :]
    • 好吧,当我同时写 A a = new A(2);和 B b = 新 B(3,2);实际上,在这里您不需要编写两者,就好像您创建了 B 的实例一样,您实际上也在创建 A 的实例,并且每当您更改“位于任何 B 中的小 A 部分”时,您都在更改 B同样,这些更改将保留为 B 中的更改,假设 B 中有一个 setA 方法更改父级的 a 属性,然后编写 B.setA 也会更改 B。
    • 当你写 A a = new A(2); B b = 新 B(3,a);在正在进行的代码中,您可以编写 a.set(5) 以便将 a 的 a 属性设置为 5,但在 b 中看不到任何变化(甚至在 ba 中也没有),您必须为变化也会影响 b。继承也是关于像使用 a 一样使用 b,但是您已经有一个可以使用的对象,那么为什么要使用 b 代替呢?
    【解决方案6】:

    ClassB 在某些情况下需要ClassA 不会提供的构造信息。

    扩展您的示例:

    class ClassA {
      int member;
    }
    
    class ClassB : ClassA {
      int bs_special_member;
    }
    

    有两种方法可以实现您想要实现的目标,这两种方法都被普遍接受(至少据我所知)。

    1 动态投射。我假设您知道,但如果不是,它仅在您实际拥有的基本类型引用已经引用了正确类型的对象时才有效。例如你已经这样做了:

    ClassA a = new ClassB();
    ((dynamic_cast)a).bs_special_member;
    

    我不知道这是否与语言无关,但我见过的大多数语言都有一些类似的实现。

    2构造函数。

    如果你有一个ClassA 的实例,实际上只是一个ClassA 并且不知道bs_special_member 的值应该是什么,那么你必须在构造时提供这个值ClassB.

    我会这样做(在 c++ 中,我认为您几乎可以在任何适当的 oo 语言中这样做):

    class ClassB : ClassA {
      int bs_special_member;
      ClassB(ClassA base) {
        member = base.member;
    
        // Apply default value
        bs_special_member = 456;
      }
      ClassB(ClassA base, int value_for_bs_special_member) {
        member = base.member;
        bs_special_member = value_for_bs_special_member;
      }
    };
    

    这会让你写:

    ClassA a;
    a.member = 672;
    ClassB b(a);
    print(b.bs_special_member);
    ClassB b2(a, 35);
    print(b2.bs_special_member);
    

    在大多数语言中,第一个构造函数将允许自动进行转换,第二个构造函数只是为了方便。如果你不能允许默认值,那么你做错了。

    【讨论】:

    • 谢谢sji。我知道基于构造函数的克隆解决方案,但这实际上是:克隆。我一直在寻找的是是否可以扩展现有对象而不实际重新实例化它。
    【解决方案7】:

    我不知道你会如何在 C++ 中做这样的事情(这是你正在使用的吗?),但你可能想要阅读 / google 以获取更多信息的概念称为“协方差” ”和“逆变”。最近我做了很多 Scala,它有一个强大的(虽然有点令人困惑)类型系统,它允许更复杂的类型规范,包括接受指定类型的父级的方法参数。

    例如,在 Scala 中,我可以定义一个带有类型参数的方法,有点像 C++ 模板,像这样:

    def myFunction[T <: MyClass]( a:T ):T ={
      //this method accepts an instance of any subclass of MyClass as a parameter,
      //and returns an object of the same type, whatever that type may be.
      return a
    }
    def myFunction[T >: MyClass]( a:T ):T ={
      //this method accepts an instance of any *parent* class of MyClass as a parameter,
      //and returns an object of the same type, whatever that type may be.
      return a
    }
    

    您还可以指定多个类型参数,并指定它们之间的关系。例如,如果我有一个“Dog”对象列表,并且我附加了一个“Cat”对象,我应该返回一个“Animal”对象列表。在列表的情况下,返回类型将始终是参数的相同类型或超类型,但如果需要,scala 还允许您在自己的函数中指定相反的类型。

    当然,如果函数在编译时不知道您正在使用哪个特定子类,那么您将无法在该上下文中使用该子类中的方法......但由于它在 scala 中工作,客户端调用此类函数时的代码可以检索到一个未指定的窄类型的实例作为返回类型,因此可以在函数中执行操作后继续使用子类的方法。

    它也可以在以下情况下工作,例如,如果您在 Cat 类中尝试覆盖返回动物列表的方法,您可以以允许子类返回的方式参数化您的类型'Cat' 类型列表,不违反父接口的约定。在这种情况下,覆盖子类的实现将可以访问 Cat 的方法。

    【讨论】:

    • 谢谢,这令人印象深刻。但我知道在 Scala 中 Dog 仍然是 DogAnimal 仍然是 Animal,所以内部对象绝不会转换,它们只是根据语言和/或合同规则铸造?
    • 是的,我的意思是,你不能在任何语言(afaik)中将Animal 视为Dog,除非它实际上是Dog 或至少具有Dog。在 Scala 中你可以做的是这样说:trait Dog{ val isDog=true} ; class Animal; val myAnimal = new Animal with Dog; ...现在myAnimal 可以作为Dog 传递,并且具有Dog 的属性。 Traits 有点像超类,有点像接口,但在实例化对象时也可以“混入”。
    【解决方案8】:

    实现这一点的唯一一致方法是让继承的类有一个包含基类的构造函数。如果你不这样做,你最终会遇到各种潜在的溢出问题,因为对象的内存打印现在与最初创建的不同。

    如果不这样做,您会遇到以下问题:

    ClassA
    {
        int A;
    }
    
    ClassB : ClassA
    {
        int B;
    }
    
    ClassA a1 = new ClassA();
    a1.A = 1;
    
    ClassA a2 = new ClassA();
    a2.A = 2;
    
    ClassB b = (ClassB)a;  
    b.B = 3;
    

    在这种情况下,您可能会遇到 a2.A 突然取值为 3 的问题...

    【讨论】:

      【解决方案9】:

      仅就我自己的研究中可能存在的自动转换缺陷提出几点说明。如果我不精确或说 BS,请原谅我。 :]

      1. 如果我们将AB : A 的原子转换定义为create B and pass A as argument so B may copy all data from A into B 的简写,那么:

        • 它要么是一种合成糖,对实际克隆过程几乎没有控制;
        • 或者(如果要通过扩展为A 分配的内存来就地转换对象)它的效率低于实例化B、复制数据并丢弃A
      2. 1234563 /p> 1234563被引用对象的数量可能会改变 - 令人惊讶的是 C 在这里预期的一致性。
      3. 某些语言(例如 C#)允许重新引入继承的成员。看来这里可能会导致逻辑上的不一致(int a in A is not new int a in B)。

      4. 这种转换可能会从外部代码(而不是从内部代码)改变对象的状态,因此它不利于封装。

      5. 每个对象都应该是AB(而不是介于两者之间),因此转换应该是原子操作。这导致了一个规则,即在将 A 转换为 B 时,不应在其上运行任何代码。我不确定是否可以实现这样的自动锁定。 (这类似于具有不完全构造对象的竞争条件)。

      虽然对于主要设计为数据包装器(结构、ORM 类)并公开与状态无关的方法的类来说,自动转换是一个不错的主意。但这似乎很容易通过反射来实现(在支持它的语言中)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多