我试图理解 Mixin 的概念,但我似乎无法理解它是什么。我认为它是一种通过使用继承来扩展类功能的方法。我读过人们将它们称为“抽象子类”。谁能解释为什么?
如果您根据以下示例(来自我的演讲幻灯片之一)解释您的答案,我将不胜感激:
在讨论什么是混合之前,描述它试图解决的问题是很有用的。假设您有一堆要建模的想法或概念。它们可能以某种方式相关,但它们在很大程度上是正交的——这意味着它们可以彼此独立地独立存在。现在,您可以通过继承对此进行建模,并让这些概念中的每一个都派生自某个公共接口类。然后在实现该接口的派生类中提供具体方法。
这种方法的问题在于,这种设计没有提供任何清晰直观的方法来获取每个具体类并将它们组合在一起。
mix-ins 的想法是提供一堆原始类,其中每个类都模拟一个基本的正交概念,并且能够将它们粘在一起以组成具有您想要的功能的更复杂的类——有点像乐高积木。原始类本身旨在用作构建块。这是可扩展的,因为稍后您可以将其他原始类添加到集合中,而不会影响现有的类。
回到 C++,实现这一点的一种技术是使用模板和继承。这里的基本思想是通过模板参数将这些构建块连接在一起。然后,您将它们链接在一起,例如。via 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
没有设置数字的情况下执行 an 可能不会像您预期的那样运行。
作为旁注,您可能还会发现这篇文章很有帮助。
mixin 是一个类,旨在为另一个类提供功能,通常通过一个指定的类来提供功能所需的基本特性。例如,考虑您的示例:
在这种情况下,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>
{
};
这个 mixin 为一组形状类(层次结构)提供了异构复制的功能。
我喜欢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”函数为虚拟,将调用适当的覆盖,并且不会发生上述不一致的行为。
C++ 中的 Mixin 是使用Curiously Recurring Template Pattern (CRTP) 来表达的。 这篇文章很好地分解了它们提供的优于其他重用技术的内容……编译时多态性。
为了理解这个概念,暂时忘记类。想想(最流行的)JavaScript。对象是方法和属性的动态数组。可以通过它们的名称作为符号或字符串文字来调用。您将如何在 2018 年用标准 C++ 实现它?不容易。但这就是这个概念的核心。在 JavaScript 中,可以随时随地添加和删除(也称为混合)。非常重要:没有类继承。
现在进入 C++。标准 C++ 有你需要的一切,在这里作为陈述没有帮助。显然,我不会为了使用 C++ 实现混合而编写脚本语言。
是的,这是一篇好文章,但仅供参考。CRTP 不是灵丹妙药。所谓的学术方法也在这里,也(本质上)基于 CRTP。
在否决这个答案之前,也许考虑一下我在魔杖盒上的 poc 代码:)
这与接口相同,可能更像抽象,但接口更容易第一次获得。
它解决了许多问题,但我发现在开发中经常出现的一个问题是外部 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.