方法链和继承不能很好地结合使用?

16

请考虑:

// member data omitted for brevity

// assume that "setAngle" needs to be implemented separately
// in Label and Image, and that Button does need to inherit
// Label, rather than, say, contain one (etc)

struct Widget {
    Widget& move(Point newPos) { pos = newPos; return *this; }
};

struct Label : Widget {
    Label& setText(string const& newText) { text = newText; return *this; }
    Label& setAngle(double newAngle) { angle = newAngle; return *this; }
};

struct Button : Label {
    Button& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

int main() {
    Button btn;

    // oops: Widget::setText doesn't exist
    btn.move(Point(0,0)).setText("Hey");

    // oops: calling Label::setAngle rather than Button::setAngle
    btn.setText("Boo").setAngle(.5); 
}

有没有什么技巧可以解决这些问题?

例如:使用模板魔法使Button::move返回Button&或其他内容。

编辑:通过使setAngle虚拟,第二个问题已经得到解决。

但第一个问题仍然无法以合理的方式解决!

编辑:好吧,我想在C++中正确地解决它是不可能的。不管怎样,感谢你们的努力。


为什么你不使用虚方法? - Andy Dent
15个回答

16
你可以扩展CRTP来处理这个问题。monjardin的解决方案朝着正确的方向发展。现在你所需要的就是一个默认的Label实现,以便将其用作叶子类。
#include <iostream>

template <typename Q, typename T>
struct Default {
    typedef Q type;
};

template <typename T>
struct Default<void, T> {
    typedef T type;
};

template <typename T>
void show(char const* action) {
    std::cout << typeid(T).name() << ": " << action << std::endl;
}

template <typename T>
struct Widget {
    typedef typename Default<T, Widget<void> >::type type;
    type& move() {
        show<type>("move");
        return static_cast<type&>(*this);
    }
};

template <typename T = void>
struct Label : Widget<Label<T> > {
    typedef typename Default<T, Widget<Label<T> > >::type type;
    type& set_text() {
        show<type>("set_text");
        return static_cast<type&>(*this);
    }
};

template <typename T = void>
struct Button : Label<Button<T> > {
    typedef typename Default<T, Label<Button<T> > >::type type;
    type& push() {
        show<type>("push");
        return static_cast<type&>(*this);
    }
};

int main() {
    Label<> lbl;
    Button<> btt;

    lbl.move().set_text();
    btt.move().set_text().push();
}

话虽如此,考虑这样的努力是否值得获得小的语法奖励。请考虑替代方案。


2
+1,好的技巧——将最派生类型作为类型参数T传递,使用"void"作为标志来表示"实际上* this *是最派生类型"。但我同意这需要很多努力... - j_random_hacker

8

对于第二个问题,将setAngle设置为虚函数应该可以解决问题。

对于第一个问题,没有简单的解决方案。Widget::move返回一个Widget对象,它没有setText方法。你可以创建一个纯虚的setText方法,但这将是一个相当丑陋的解决方案。你可以在button类上重载move()方法,但这将很难维护。最后,你可能可以使用模板来解决问题。也许像这样:

// Define a move helper function
template <typename T>
T& move(T& obj, Point& p){ return obj.move(p); };

// And the problematic line in your code would then look like this:
move(btn, Point(0,0)).setText("Hey");

我会让你决定哪个解决方案最干净。但是,你需要链式调用这些方法的特定原因吗?


4

我建议放弃链式编程。首先,这需要进行一些相对恶劣的黑客攻击才能实现。但是,最大的问题在于这会使代码难以阅读和维护,并且你很可能最终会发现人们滥用它,创建一个巨大的代码行来完成多个任务(回想一下高中代数中那些巨大的加减乘除运算,你总是会在某个时候遇到,如果你让他们这样做)。

另一个问题是,因为系统中的大多数函数都将返回对自身的引用,所以所有这些函数都应该这样做。当(而不是如果)你最终开始实现应该返回值的函数时(不仅仅是访问器,还有一些变异器和其他通用函数),你将面临一个困境,要么打破你的约定(这会像雪球一样滚动,使得未来其他函数的实现方式变得不清晰),要么被迫通过参数返回值(我敢肯定你会讨厌这种方式,就像我认识的大多数程序员一样)。


4

解决问题的一种简单但烦人的方法是在子类中重新实现所有公共方法。这并不能解决多态性的问题(例如,如果您将Label强制转换为Widget),这可能是一个重大问题或不重要的问题。

struct Widget {
    Widget& move(Point newPos) { pos = newPos; return *this; }
};

struct Label : Widget {
    Label& setText(string const& newText) { text = newText; return *this; }
    Label& setAngle(double newAngle) { angle = newAngle; return *this; }
    Label& move(Point newPos) { Widget::move(newPos); return *this; }
};

struct Button : Label {
    Button& setText(string const& newText) { Label::setText(newText); return *this; }
    Button& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
    Button& move(Point newPos) { Label::move(newPos); return *this; }
};

请确保在您的文档中包含此内容,这是必须完成链式调用的。

但实际上,为什么要使用方法链呢?很多时候,这些函数将被不太琐碎地调用,并且需要更长的行。这会影响可读性。每行只执行一个操作——这是涉及++--时的一般规则。


好的,让我从我的代码中粘贴一些东西。http://pastie.org/389837 它比另一种选择更合理 :) 不过,它不能自动化,因为所有的魔数基本上都来自设计部门 :) - Iraimbilanja
有一天我可能会转向由设计师应用程序生成的CSS/XML形式的描述,但今天不是那一天。 - Iraimbilanja
我之前确实考虑过你的想法,但它会带来维护上的噩梦和“脆弱基类”问题 :) - Iraimbilanja
创建一个函数,makeButton(float angle, std::string text, int x, int y);然后将其添加到addTo(this)中。一点也不难! - strager
@Iraimbilanja,这是你使用链式调用(你粘贴的代码)的唯一原因吗?还是你用它来处理更复杂的事情?如果是这样,你可以将makeButton函数改为非链式调用。 - strager
如果您有命名参数或大约三个参数需要初始化,则使用makeButton(或完整的构造函数)是可以的。五个或更多参数时,链接非常好-特别是如果您通常仅初始化子集且子集变化很大。 - peterchen

4

一个 Button 真的是一个 Label 吗?你似乎违反了Liskov替换原则。也许你应该考虑使用装饰器模式来为小部件添加行为。

如果您坚持使用当前结构,可以像这样解决您的问题:

struct Widget {
    Widget& move(Point newPos) { pos = newPos; return *this; }
    virtual ~Widget();  // defined out-of-line to guarantee vtable
};

struct Label : Widget {
    Label& setText(string const& newText) { text = newText; return *this; }
    virtual Label& setAngle(double newAngle) { angle = newAngle; return *this; }
};

struct Button : Label {
    virtual Label& setAngle(double newAngle) {
        backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

int main() {
    Button btn;

    // Make calls in order from most-specific to least-specific classes
    btn.setText("Hey").move(Point(0,0));

    // If you want polymorphic behavior, use virtual functions.
    // Anything that is allowed to be overridden in subclasses should
    // be virtual.
    btn.setText("Boo").setAngle(.5); 
}

我的观点是btn.move(Point(0,0)).setText("Hey")应该按预期工作。重新排序调用显然可以解决问题,但这不应该是必需的。我同意虚拟的看法,并且我会考虑使用装饰器,谢谢。 - Iraimbilanja

3

C++确实支持虚拟方法的返回值协变。因此,您可以通过一些工作获得类似于您想要的结果:

#include <string>
using std::string;

// member data omitted for brevity

// assume that "setAngle" needs to be implemented separately
// in Label and Image, and that Button does need to inherit
// Label, rather than, say, contain one (etc)


struct Point
{
    Point() : x(0), y(0) {};
    Point( int x1, int y1) : x( x1), y( y1) {};

    int x;
    int y;
};

struct Widget {
    virtual Widget& move(Point newPos) { pos = newPos; return *this; }
    virtual ~Widget() {};

    Point pos;
};

struct Label : Widget {
    virtual ~Label() {};
    virtual Label& move( Point newPos) { Widget::move( newPos); return *this; }

    // made settext() virtual, as it seems like something 
    // you might want to be able to override
    // even though you aren't just yet
    virtual Label& setText(string const& newText) { text = newText; return *this; }
    virtual Label& setAngle(double newAngle) { angle = newAngle; return *this; }

    string text;
    double angle;
};

struct Button : Label {
    virtual ~Button() {};
    virtual Button& move( Point newPos) { Label::move( newPos); return *this; }
    virtual Button& setAngle(double newAngle) {
        //backgroundImage.setAngle(newAngle);
        Label::setAngle(newAngle);
        return *this;
    }
};

int main()
{
    Button btn;

    // this works now
    btn.move(Point(0,0)).setText("Hey");

    // this works now, too
    btn.setText("Boo").setAngle(.5); 

  return 0;
}

请注意,您应该使用虚拟方法来执行此类操作。如果它们不是虚拟方法,则“重新实现”的方法将导致名称隐藏,调用的方法将取决于变量、指针或引用的静态类型,因此,如果正在使用基本指针或引用,则可能不是正确的方法。


2

[抱怨]

是的。不要再使用方法链,只需按顺序调用函数即可。

说真的,允许这种语法确实会付出代价,而我并不明白它所提供的好处。

[/抱怨]


或者你可以定义一个适当的构造函数,在一行中接受所有需要的参数。 :) - jalf
1
不?你喜欢在代码库中散布半构造的对象吗?如果对象需要在有效之前放置,那么它应该在构造函数中给出位置。 - jalf
@jalf,我来自Qt背景,那里的构造函数非常简单。为什么一个对象需要被分配位置才能有效?默认位置可用(0, 0),以及默认文本(""),字体(系统),方向(0度)等。构造函数创建对象,仅此而已。 - strager
1
“空”的状态并不总是有效的。对于某些对象而言可能是有效的,但肯定不是对于所有对象都是如此。对于GUI具体而言,什么是有效的呢?当应用程序看起来正确时(那么空就不行)?还是仅当它不崩溃时(那么空/空白就可以)? - jalf
1
但是,事实上,你试图跳过这些步骤来清理GUI初始化的事实表明,在你的情况下,空不是一个有效的状态。;)如果你想让初始化更容易,可以编写一个构造函数(或者执行相同操作的自由函数)。 - jalf
显示剩余9条评论

2

不适用于C++。

C++不支持返回类型的变化,因此即使你重载它,也无法改变从Widget.move()返回的引用的静态类型,使其更具体化而不是Widget&。

C++需要在编译时检查事物,所以你不能利用move实际上返回的是一个按钮这一事实。

最多,你可以进行一些运行时转换,但这看起来并不美观。只能分开调用。

编辑:是的,我非常清楚C++标准规定返回值协变是合法的。然而,在我教授和实践C++的时候,一些主流编译器(例如VC++)没有支持。因此,为了可移植性,我们建议不要使用它。现在的编译器可能已经解决了这个问题。


C++确实支持返回类型的变异(协变,这也是问题所寻找的)。 - Michael Burr
协方差是C++标准的一部分,但在我教授C++时,并不属于主流的可视化编译器(比如Visual C++ 5/6)。因此我们将其作为不支持的内容进行教学。不确定新的编译器如何处理这个问题。 - Uri
你说得对,VC6不支持协变。我现在能轻松使用的所有其他编译器都支持。 - Michael Burr
我添加了澄清。在过去的8年中,我主要做Java...当时,我们用于教学的所有工具都不支持它。 - Uri

2
有一段时间我认为可以通过重载略微不同的operator->()来实现方法链,而不是使用.,但是这种尝试失败了,因为编译器要求->右边的标识符必须属于左边表达式的静态类型。好吧。

穷人版方法链

暂停一下,方法链的目的是避免反复输入长对象名称。我建议采用以下快速而简单的方法:
不使用“长手形式”:
btn.move(Point(0,0)); btn.setText("Hey");

您可以编写:

{Button& _=btn; _.move(Point(0,0)); _.setText("Hey");}

不,它不像真正的使用“.”链接那样简洁,但在需要设置许多参数时,它可以节省一些打字时间,并且它有一个好处,即无需更改现有类的代码。因为您将整个方法调用组包装在“{}”中以限制引用的范围,所以您始终可以使用相同的短标识符(例如“_”或“x”)代表特定的对象名称,从而增加可读性。最后,编译器不会有问题地优化掉“_”。

1
是的,确实如此。但是,既然你在使用C++,你期望什么呢? :) - j_random_hacker
1
从现在开始,我将使用“_”、“__”、“___”和“____”作为我的临时变量名称。 - shuhalo

1

在编程中,方法链式调用确实是一个坏主意,因为它的异常安全性较差。你真的需要使用它吗?


请解释一下 { btn.setText("Hey").move(p); } 比 { btn.setText("Hey"); btn.move(p); } 更不安全的原因?在这两种情况下,如果为“Hey”分配 std::string 失败,则不会调用 move(p)(这是好的),如果 move(p) 失败,则文本不会回滚。就我所知,后者无论如何都无法解决,除非使用可怕的 ScopeGuard 技巧,例如 { SCOPE_GUARD(guard, &Button::setText, btn, btn.currentText()); btn.setText("hey"); btn.move(p); guard.dismiss(); }。 - Iraimbilanja

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接