对象切片,有优势吗?

发布于 2024-08-23 23:53:43 字数 289 浏览 9 评论 0原文

对象切片是指当子类分配给基类时,对象会丢失其某些属性或功能。 比如

Class A{

}
Class B extends A{

}

Class SomeClass{
A a = new A();
B b = new B();

// Some where if might happen like this */
a = b; (Object slicing happens)

}

我们说对象切片有什么好处吗? 如果是,有人可以告诉我对象切片对开发有何帮助以及它在哪些方面可能有帮助吗?

Object slicing is some thing that object looses some of its attributes or functions when a child class is assigned to base class.
Some thing like

Class A{

}
Class B extends A{

}

Class SomeClass{
A a = new A();
B b = new B();

// Some where if might happen like this */
a = b; (Object slicing happens)

}

Do we say Object slicing is any beneficial in any ways?
If yes, can any one please tell me how object slicing be a helpful in development and where it might be helpful?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

雨后咖啡店 2024-08-30 23:53:43

在 C++ 中,您应该将对象切片视为从派生类型到基类型[*]的转换。一个全新的物体被创造出来,它的灵感来自于一个真实的故事。

有时这是您想要做的事情,但结果在任何意义上都与原始对象不同。当对象切片出错时,人们没有注意,并认为它是同一个对象或其副本。

通常这没有什么好处。事实上,当有人本打算通过引用传递时却通过值传递,这通常是意外完成的。

很难举出一个例子来说明何时切片绝对是正确的做法,因为很难(尤其是在 C++ 中)举出一个非抽象基类绝对是正确的做法的例子。这是一个重要的设计点,不能轻易忽略 - 如果您发现自己有意或无意地切片了一个对象,很可能您的对象层次结构一开始就是错误的。要么基类不应该用作基类,要么它应该至少有一个纯虚函数,因此不能按值切片或传递。

因此,我给出的任何将对象转换为其基类的对象的示例都会正确地引发反对意见,“等一下,您首先要从具体类继承做什么?”。如果切片是偶然的,那么它可能是一个错误,如果是故意的,那么它可能是“代码味道”。

但答案可能是“是的,好吧,这不应该确实是事物的结构方式,但考虑到它们是这样构建的,我需要从派生类到基类,并且根据定义是一个切片”。本着这种精神,这里有一个示例:

struct Soldier {
    string name;
    string rank;
    string serialNumber;
};

struct ActiveSoldier : Soldier {
    string currentUnit;
    ActiveSoldier *commandingOfficer; // the design errors multiply!
    int yearsService;
};

template <typename InputIterator>
void takePrisoners(InputIterator first, InputIterator last) {
    while (first != last) {
        Soldier s(*first);
        // do some stuff with name, rank and serialNumber
       ++first;
    }
}

现在,takePrisoners 函数模板的要求是其参数是可转换为 Soldier 的类型的迭代器。它不一定是派生类,并且我们不直接访问成员“名称”等,因此 takePrisoners 尝试提供最简单的接口来实现给定的限制(a ) 应该与 Soldier 一起使用,并且 (b) 应该可以编写它也可以使用的其他类型。

ActiveSoldier 就是另一种类型。由于只有该类的作者最清楚的原因,它选择公开继承 Soldier,而不是提供重载的转换运算符。我们可以争论这是否是一个好主意,但我们假设我们坚持这样做。因为它是派生类,所以它可以转换为 Soldier。这种转换称为切片。因此,如果我们调用 takePrisoners 并传入 ActiveSoldiers 向量的 begin()end() 迭代器,那么我们将对它们进行切片。

您可能会为 OutputIterator 提出类似的示例,其中接收者只关心正在传递的对象的基类部分,因此允许在将它们写入迭代器时对它们进行切片。

之所以有“代码味道”,是因为我们应该考虑 (a) 重写 ActiveSoldier,以及 (b) 更改 Soldier,以便可以使用函数而不是成员访问来访问它,这样我们就可以将这组函数抽象为一个接口,其他类型可以独立实现,因此 takePrisoners 不必转换为 Soldier。其中任何一个都将消除对切片的需求,并且对于将来可以轻松扩展我们的代码具有潜在的好处。

[*]因为它是一个。下面的最后两行做同样的事情:

struct A {
    int value;
    A(int v) : value(v) {}
};

struct B : A {
    int quantity;
    B(int v, int q) : A(v), quantity(q) {}
};

int main() {
    int i = 12;  // an integer
    B b(12, 3);  // an instance of B
    A a1 = b;    // (1) convert B to A, also known as "slicing"
    A a2 = i;    // (2) convert int to A, not known as "slicing"
}

唯一的区别是 (1) 调用 A 的复制构造函数(编译器提供,即使代码没有提供),而 (2) 调用 A 的 int 构造函数。

正如其他人所说,Java 不进行对象切片。如果您提供的代码被转换为 Java,那么就不会发生任何类型的对象切片。 Java 变量是引用,而不是对象,因此 a = b 的后置条件只是变量“a”与变量“b”引用同一个对象 - 通过一个引用进行的更改可以通过以下方式查看其他参考,等等。他们只是用不同的类型来引用它,这是多态性的一部分。一个典型的类比是,我可能将一个人视为“我的兄弟”[**],而其他人可能将同一个人视为“我的牧师”。相同的对象,不同的接口。

你可以使用指针或引用在 C++ 中获得类似 Java 的效果:

B b(24,7);
A *a3 = &b; // No slicing - a3 is a pointer to the object b
A &a4 = b;  // No slicing - a4 is a reference to (pseudonym for) the object b

[**] 事实上,我的兄弟不是牧师。

In C++, you should think of an object slice as a conversion from the derived type to the base type[*]. A brand new object is created, which is "inspired by a true story".

Sometimes this is something that you would want to do, but the result is not in any sense the same object as the original. When object slicing goes wrong is when people aren't paying attention, and think it is the same object or a copy of it.

It's normally not beneficial. In fact it's normally done accidentally when someone passes by value when they meant to pass by reference.

It's quite hard to come up with an example of when slicing is definitively the right thing to do, because it's quite hard (especially in C++) to come up with an example where a non-abstract base class is definitively the right thing to do. This is an important design point, and not one to pass over lightly - if you find yourself slicing an object, either deliberately or accidentally, quite likely your object hierarchy is wrong to start with. Either the base class shouldn't be used as a base class, or else it should have at least one pure virtual function and hence not be sliceable or passable by value.

So, any example I gave where an object is converted to an object of its base class, would rightly provoke the objection, "hang on a minute, what are you doing inheriting from a concrete class in the first place?". If slicing is accidental then it's probably a bug, and if it's deliberate then it's probably "code smell".

But the answer might be "yes, OK, this shouldn't really be how things are structured, but given that they are structured that way, I need to convert from the derived class to the base class, and that by definition is a slice". In that spirit, here's an example:

struct Soldier {
    string name;
    string rank;
    string serialNumber;
};

struct ActiveSoldier : Soldier {
    string currentUnit;
    ActiveSoldier *commandingOfficer; // the design errors multiply!
    int yearsService;
};

template <typename InputIterator>
void takePrisoners(InputIterator first, InputIterator last) {
    while (first != last) {
        Soldier s(*first);
        // do some stuff with name, rank and serialNumber
       ++first;
    }
}

Now, the requirement of the takePrisoners function template is that its parameter be an iterator for a type convertible to Soldier. It doesn't have to be a derived class, and we don't directly access the members "name", etc, so takePrisoners has tried to offer the easiest possible interface to implement given the restrictions (a) should work with Soldier, and (b) should be possible to write other types that it also works with.

ActiveSoldier is one such other type. For reasons best known only to the author of that class, it has opted to publicly inherit from Soldier rather than providing an overloaded conversion operator. We can argue whether that's ever a good idea, but let's suppose we're stuck with it. Because it's a derived class, it is convertible to Soldier. That conversion is called a slice. Hence, if we call takePrisoners passing in the begin() and end() iterators for a vector of ActiveSoldiers, then we will slice them.

You could probably come up with similar examples for an OutputIterator, where the recipient only cares about the base class part of the objects being delivered, and so allows them to be sliced as they're written to the iterator.

The reason it's "code smell" is that we should consider (a) rewriting ActiveSoldier, and (b) changing Soldier so that it can be accessed using functions instead of member access, so that we can abstract that set of functions as an interface that other types can implement independently, so that takePrisoners doesn't have to convert to Soldier. Either of those would remove the need for a slice, and would have potential benefits for the ease with which our code can be extended in future.

[*] because it is one. The last two lines below are doing the same thing:

struct A {
    int value;
    A(int v) : value(v) {}
};

struct B : A {
    int quantity;
    B(int v, int q) : A(v), quantity(q) {}
};

int main() {
    int i = 12;  // an integer
    B b(12, 3);  // an instance of B
    A a1 = b;    // (1) convert B to A, also known as "slicing"
    A a2 = i;    // (2) convert int to A, not known as "slicing"
}

The only difference is that (1) calls A's copy constructor (that the compiler provides even though the code doesn't), whereas (2) calls A's int constructor.

As someone else said, Java doesn't do object slicing. If the code you provide were turned into Java, then no kind of object slicing would happen. Java variables are references, not objects, so the postcondition of a = b is just that the variable "a" refers to the same object as the variable "b" - changes via one reference can be seen via the other reference, and so on. They just refer to it by a different type, which is part of polymorphism. A typical analogy for this is that I might think of a person as "my brother"[**], and someone else might think of the same person as "my vicar". Same object, different interface.

You can get the Java-like effect in C++ using pointers or references:

B b(24,7);
A *a3 = &b; // No slicing - a3 is a pointer to the object b
A &a4 = b;  // No slicing - a4 is a reference to (pseudonym for) the object b

[**] In point of fact, my brother is not a vicar.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文