【问题标题】:Object vs Reference for class members and dependency injection类成员和依赖注入的对象与参考
【发布时间】:2019-11-17 00:25:05
【问题描述】:

我是 C++ 新手,具有 C# 背景。我正在尝试在 C++ 中使用 C# 中常用的依赖注入,我对声明依赖项并将它们传递的不同方式以及为什么我会使用其中一种方式感到困惑。假设A 依赖于B

方法 #1 - 声明为对象并获取对象

class A
{
  private:
    B _b;
  public:
    A(B b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

方法 #2 - 声明为对象并引用

class A
{
  private:
    B _b;
  public:
    A(B &b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

方法 #3 - 声明为引用并获取对象 - 从头开始​​ - 未定义的行为

class A
{
  private:
    B &_b;
  public:
    A(B b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

方法 #4 - 声明为引用并获取引用

class A
{
  private:
    B &_b;
  public:
    A(B &b): _b(b) { }

    void Foo()
    {
        _b.Bar();
    }
}

我已经尝试了以上所有方法,它们似乎都有效。它们之间有什么区别吗?如果是这样,我为什么要选择一个而不是另一个?我的偏好是 #1,因为我发现它是最熟悉和最直观的。

我发现了一个类似的讨论讨论 here,但更多的是关于指针与引用。

【问题讨论】:

  • 如果ba 之前消失,那么方法4 会造成麻烦,因为你有一个悬空的引用。
  • 1 和 2 因为slicing 而无用。 3 是undefined behavior,4 可以工作但根本不安全,因为引用可能会悬空。如果你坚持使用依赖注入,你应该使用智能指针
  • 感谢所有 cmets。我现在看到 3 恰好工作。我仍然没有完全理解 #1 和 #2 的切片问题,但我正在阅读它。
  • @alterigel 1 和 2 之间有什么区别吗?
  • @YetAnotherCoder 1 和 2 都存储您传递给构造函数的对象的 副本。这可能不是你想要的。在任何情况下,您都不能将派生自 B 的类传递给构造函数,因为复制到基类类型将 slice,这意味着只复制对象的基础部分。这实际上意味着您不能在方法 1 或 2 中传递除 B 之外的任何类型。这似乎也不像依赖注入应该如何工作。

标签: c++ dependency-injection


【解决方案1】:

在这方面,C# 和 C++ 之间的主要区别之一是,在 C# 中,类是引用类型,而在 C++ 中,它们是值类型。

方法 1 和 2 基于复制构造。如果传递给构造函数的对象恰好是B,这可能会起作用。但是如果你使用 B 的派生类,它可能会因为 object slicing 而失败。如果源对象是常量,方法 2 也可能会失败。

方法3:不再考虑(在您的问题中划伤)

方法4:可以,只要通过引用传递的对象继续存在,只要该对象。但是,如果函数B::Bar() 不是虚函数,那么B::Bar() 将始终在Foo() 中调用,即使您使用从B 派生的类以及它自己的Bar() 实现。

方法 5:使用智能指针并克隆通过引用传递的对象(使用 clone() 成员,或使用模板构造函数):

class A
{
  private:
    unique_ptr<B> _b;
  public:
    template<class T>A(const T&b): _b(make_unique<T>(b)) { }

    void Foo()
    {
        _b->Bar();
    }
};

Online demo

【讨论】:

  • 引用成员也会使A 不可赋值,这可能会出现问题,具体取决于类的使用方式。
  • @uneven_mark 感谢您的编辑。事实上,参考成员有几个缺点。我还添加了方法 5 来克服可能具有较短生命周期的 injectect b 对象的问题。变体可以使用 shared_ptr 而不是 unique_ptr 来让对象可分配。当然,所有这些都假设 B 类允许复制构造。
  • 您还可以为不可复制的对象添加 std::move 选项。
【解决方案2】:

在您的情况下,我想说所有提到的最佳解决方案是获取和存储参考。依赖注入的全部意义在于使用纯虚拟接口,甚至不知道如何实现。这意味着在下面您需要 vtable 查找,它将执行所引用对象的任何实现。通过这种方式,您可以期待 Animal 的参考,其中调用者将提供 Animal 的 Elephant 或 Cat 实现。而且,如果你使用纯虚接口(内部至少有一个纯虚函数),你甚至不能按值传递对象,因为你不能创建纯虚类型的对象。

据我所知,与 Java 类似,C# 将对象与内置类型区分开来,默认情况下对象是通过引用传递的。在 C/C++ 中,您明确地以您想要的方式传递所有内容,因此在您的情况下,您有:

  1. B 的临时副本 您制作的副本超出了您的需要。如果您的对象很大,则会导致性能损失。
  2. B 的副本 如果您只是想存储某个对象的副本,这似乎是更好的方法,但您在这里创建了不需要的限制。如果您复制对象,则不会修改原始对象。通过非常量引用传递它,您会创建此限制,因此您不能使用 const 对象。如果您只是想通过引用获取某个对象并存储它的副本,那么大多数时候您需要 const 引用。不可复制的对象可能会出现例外情况。
  3. 对 B 的临时副本的引用(无效)
  4. 参考 B 是完全不同的故事。在这里,您不存储对象的副本,而是指向原始对象,因此您对该对象所做的每项更改都将对有权访问您收到的对象的任何其他人可见。这或多或少是对象在 Java 等语言中的默认工作方式。此外,您可以使用我之前提到的虚拟化。唯一的缺点是,只要您使用它,您就必须确保该对象存在 - 您不是 B 的所有者。您只是使用它,但有人可以控制它的存在。

如您所见,在第一种情况下,第二种情况会产生一些不需要的限制。您创建非常量的副本

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-01-19
    • 1970-01-01
    • 1970-01-01
    • 2016-06-19
    • 1970-01-01
    • 2021-09-01
    相关资源
    最近更新 更多