我试图让我了解Mixin概念,但是我似乎无法理解它的含义。我看到的是,这是一种通过使用继承来扩展类功能的方法。我读过人们将它们称为“抽象子类”。谁能解释为什么?
如果您能根据以下示例解释您的答案,将不胜感激(摘自我的演讲幻灯片之一):
我试图让我了解Mixin概念,但是我似乎无法理解它的含义。我看到的是,这是一种通过使用继承来扩展类功能的方法。我读过人们将它们称为“抽象子类”。谁能解释为什么?
如果您能根据以下示例解释您的答案,将不胜感激(摘自我的演讲幻灯片之一):
Answers:
在进行混搭之前,先描述一下它要解决的问题很有用。假设您有许多要建模的想法或概念。它们可能以某种方式相关,但是它们在大多数情况下是正交的,这意味着它们可以彼此独立地站立。现在,您可以通过继承对其进行建模,并使每个概念都来自某个通用接口类。然后,在实现该接口的派生类中提供具体方法。
这种方法的问题在于,这种设计没有提供任何清晰直观的方式来采用每个具体类并将它们组合在一起。
混入的想法是提供一堆原始类,它们中的每一个都建模一个基本的正交概念,并能够将它们组合在一起,以仅具有所需功能的方式组成更复杂的类,就像乐高玩具一样。原始类本身旨在用作构建块。这是可扩展的,因为稍后您可以将其他原始类添加到集合中,而不会影响现有的原始类。
回到C ++,实现此目的的一种技术是使用模板和继承。这里的基本思想是,通过template参数提供这些构建块,从而将它们连接在一起。然后,将它们链接在一起,例如。通过typedef
,形成包含所需功能的新类型。
以您的示例为例,假设我们要在顶部添加重做功能。可能是这样的:
#include <iostream>
using namespace std;
struct Number
{
typedef int value_type;
int n;
void set(int v) { n = v; }
int get() const { return n; }
};
template <typename BASE, typename T = typename BASE::value_type>
struct Undoable : public BASE
{
typedef T value_type;
T before;
void set(T v) { before = BASE::get(); BASE::set(v); }
void undo() { BASE::set(before); }
};
template <typename BASE, typename T = typename BASE::value_type>
struct Redoable : public BASE
{
typedef T value_type;
T after;
void set(T v) { after = v; BASE::set(v); }
void redo() { BASE::set(after); }
};
typedef Redoable< Undoable<Number> > ReUndoableNumber;
int main()
{
ReUndoableNumber mynum;
mynum.set(42); mynum.set(84);
cout << mynum.get() << '\n'; // 84
mynum.undo();
cout << mynum.get() << '\n'; // 42
mynum.redo();
cout << mynum.get() << '\n'; // back to 84
}
您会注意到我对您的原始内容进行了一些更改:
value_type
为第二个模板参数添加了默认值,以使其使用起来不再那么麻烦。这样,您<foobar, int>
每次粘贴在一起时都不必继续键入。typedef
使用一个简单的类。请注意,这仅是说明混合概念的简单示例。因此,它没有考虑到极端情况和有趣的用法。例如,执行一个undo
从未设置数字的操作可能不会像您期望的那样运行。
作为旁注,您可能还会发现本文很有帮助。
void Number::set(int)
并且int Number::get() const
两者都应是virtual
在使用Number*
指针时获得mixin行为。
set
和get
虚拟是有意义的,因为您可以定义void doubler(Number &n){ n.set(n.get()*2); }
和使用可撤销和可撤销的类
Number::value_type
已经被定义它可以(也应该)也可以用于Number::n
,Number::get
和Number::set
。
int
,std::string
,char
,等在C ++中)本身的混入,对不对?
混合是为指定另一个类提供功能的类,通常通过指定的类提供指定功能所需的基本功能。例如,考虑您的示例:
在这种情况下,mixin提供撤消值类的设置操作的功能。这种稳定性基于get/set
参数化类(Number
在您的示例中为类)提供的功能。
另一个示例(摘自“ C ++中基于Mixin的编程”):
template <class Graph>
class Counting: public Graph {
int nodes_visited, edges_visited;
public:
Counting() : nodes_visited(0), edges_visited(0), Graph() { }
node succ_node (node v) {
nodes_visited++;
return Graph::succ_node(v);
}
edge succ_edge (edge e) {
edges_visited++;
return Graph::succ_edge(e);
}
...
};
在此示例中,mixin提供给定执行遍历操作的图类的计数顶点的功能。
通常,在C ++中,mixin是通过CRTP习惯用法实现的。该线程可以很好地了解C ++中的mixin实现:什么是C ++ Mixin-Style?
这是一个利用CRTP习惯用法的mixin示例(感谢@Simple):
#include <cassert>
#ifndef NDEBUG
#include <typeinfo>
#endif
class shape
{
public:
shape* clone() const
{
shape* const p = do_clone();
assert(p && "do_clone must not return a null pointer");
assert(
typeid(*p) == typeid(*this)
&& "do_clone must return a pointer to an object of the same type"
);
return p;
}
private:
virtual shape* do_clone() const = 0;
};
template<class D>
class cloneable_shape : public shape
{
private:
virtual shape* do_clone() const
{
return new D(static_cast<D&>(*this));
}
};
class triangle : public cloneable_shape<triangle>
{
};
class square : public cloneable_shape<square>
{
};
该混合器提供了异类复制到形状类集(层次结构)的功能。
shape
类,但是派生自该类将cloneable_shape
自动clone
为您实现成员函数,因此您不必为每个派生的类自己编写它。
我喜欢greatwolf的回答,但请注意一点。
greatwolf说:“这里实际上不需要虚拟函数,因为我们确切地知道我们的组合类类型在编译时是什么。” 不幸的是,如果您多态使用对象,则可能会遇到一些不一致的行为。
让我从他的示例中调整主要功能:
int main()
{
ReUndoableNumber mynum;
Undoable<Number>* myUndoableNumPtr = &mynum;
mynum.set(42); // Uses ReUndoableNumber::set
myUndoableNumPtr->set(84); // Uses Undoable<Number>::set (ReUndoableNumber::after not set!)
cout << mynum.get() << '\n'; // 84
mynum.undo();
cout << mynum.get() << '\n'; // 42
mynum.redo();
cout << mynum.get() << '\n'; // OOPS! Still 42!
}
通过将“ set”函数设为虚拟,将调用适当的覆盖,并且不会发生上述不一致的行为。
它的作用与接口相同,也许与抽象的作用相同,但接口更容易获得。
它解决了许多问题,但是我在开发中发现很多问题是外部api。想象一下。
您有一个用户数据库,该数据库具有某种访问其数据的方式。现在想象一下,如果您拥有facebook,那么它也具有某种访问其数据(api)的方式。
在任何时候,您的应用程序可能都需要使用Facebook或数据库中的数据来运行。因此,您要做的就是创建一个界面,上面写着“实现我的任何东西都肯定具有以下方法”,现在您可以将该界面实现到您的应用程序中了...
因为接口保证了实现的存储库将在其中声明方法,所以您知道,无论在何时何地在应用程序中使用该接口,如果切换数据,它将始终拥有您正在定义的方法,从而拥有要处理的数据。
这种工作模式还有很多层次,但本质是它很不错,因为数据或其他此类持久性项已成为应用程序的重要组成部分,如果它们在您不知情的情况下发生更改,则应用程序可能会崩溃:)
这是一些伪代码。
interface IUserRepository
{
User GetUser();
}
class DatabaseUserRepository : IUserRepository
{
public User GetUser()
{
// Implement code for database
}
}
class FacebookUserRepository : IUserRepository
{
public User GetUser()
{
// Implement code for facebook
}
}
class MyApplication
{
private User user;
MyApplication( IUserRepository repo )
{
user = repo;
}
}
// your application can now trust that user declared in private scope to your application, will have access to a GetUser method, because if it isn't the interface will flag an error.
要理解这个概念,请暂时不要上课。考虑(最受欢迎的)JavaScript。对象是方法和属性的动态数组。可通过它们的名称作为符号或字符串文字来调用。您将如何在2018年用标准C ++实现呢?不容易。但这是概念的核心。在JavaScript中,您可以随时随地添加和删除(也称为混合)。非常重要:没有类继承。
现在到C ++。标准C ++具有您所需的全部内容,在这里无助于说明。显然,我不会编写脚本语言来使用C ++实现混入。
是的,这是一篇不错的文章,但仅供参考。CRTP不是万能药。所谓的学术方法是这里,也(本质)基于CRTP。
在拒绝投票之前,也许考虑一下我在魔盒上的POC代码:)