4

假设我有以下声明:

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

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

众所周知,objectClassB可以在任何需要的地方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或提供一些克隆机制,但我想避免这样的解决方案。)

编辑

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

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

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

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

只是为了说清楚:我不是在寻找有关向下转换先前向上转换的对象的信息。这不是这里的情况。

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

注意

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

谢谢你

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

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

4

9 回答 9

5

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

于 2012-09-01T22:07:24.923 回答
1

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

请阅读这篇维基百科文章

另一种选择是使用接口。

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取决于接口,因此可重用性更高。

于 2012-09-01T22:26:40.070 回答
1

这是特定语言的实现细节,而不是面向对象思维的固有限制。我可以想象许多需要这种行为的场景——也许 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 实例——它只是共享 Rectangle 的接口。所以在这些语言中实现这个特性要慢得多,因为它们实际上必须将整个扁平结构复制到新的内存位置。

于 2012-09-04T17:00:17.463 回答
1

我在 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

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

于 2012-09-10T13:57:23.500 回答
0

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

事实上,假设你有这个:

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();
于 2012-09-04T08:28:22.267 回答
0

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实际上只是aClassA并且不知道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);

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

于 2012-09-07T15:14:07.613 回答
0

我不知道你会如何在 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 的方法。

于 2012-09-09T15:08:04.773 回答
0

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

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

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 的问题...

于 2012-09-10T12:14:53.623 回答
0

只是我自己研究中可能存在的自动转换缺陷的一些注释。如果我不精确或说 BS,请原谅我。:]

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

    • 它要么是一种合成糖,对实际的克隆过程几乎没有控制;
    • 或者(如果要通过扩展为 分配的内存来就地转换对象A)它的效率低于实例化B、复制数据和丢弃A
  2. 如果B有它自己的构造函数(无论是否调用超级构造函数),B那么BA.

  3. 如果在某些情况下BA行为不同,并且 aC持有对 的引用A,那么(在A转换为之后B)被引用对象的行为可能会改变 - 令人惊讶的是C这里的预期一致性。

  4. 某些语言(例如 C#)允许重新引入继承的成员。看来这里可能会导致逻辑上的不一致(int ain Ais not new int a in B)。

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

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

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

于 2012-09-10T13:03:23.353 回答