13

在 Google 的Java协议缓冲区API 中,他们使用这些漂亮的 Builder 来创建对象(请参阅此处):

Person john =
  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();

但是对应的 C++ API 没有使用这样的 Builders(见这里

C++ 和 Java API 应该做同样的事情,所以我想知道他们为什么不在 C++ 中使用构建器。这背后是否有语言原因,即它不是惯用的,或者它在 C++ 中不受欢迎?或者可能只是编写 C++ 版本 Protocol Buffers 的人的个人喜好?

4

6 回答 6

8

在 C++ 中实现类似的东西的正确方法是使用返回对 *this 的引用的 setter。

class Person {
  std::string name;
public:
  Person &setName(string const &s) { name = s; return *this; }
  Person &addPhone(PhoneNumber const &n);
};

假设类似定义的 PhoneNumber,可以像这样使用该类:

Person p = Person()
  .setName("foo")
  .addPhone(PhoneNumber()
    .setNumber("123-4567"));

如果需要一个单独的构建器类,那么也可以这样做。当然,这样的构建器应该在堆栈中分配。

于 2010-02-19T07:58:29.060 回答
4

我会选择“不习惯”,尽管我已经在 C++ 代码中看到了这种流利界面样式的示例。

这可能是因为有多种方法可以解决相同的潜在问题。通常,这里要解决的问题是命名参​​数的问题(或者更确切地说是它们的缺乏)。可以说,这个问题的一个更像C++ 的解决方案可能是Boost 的 Parameter library

于 2010-02-19T08:18:20.970 回答
2

差异部分是惯用的,但也是 C++ 库被更大量优化的结果。

您在问题中没有注意到的一件事是 protoc 发出的 Java 类是不可变的,因此必须具有具有(可能)非常长的参数列表且没有 setter 方法的构造函数。不可变模式在 Java 中常用来避免与多线程相关的复杂性(以牺牲性能为代价),而构建器模式用于避免在大型构造函数调用时眯着眼睛看的痛苦,并且需要同时拥有所有值点在代码中。

protoc 发出的 C++ 类不是不可变的,其设计目的是使对象可以在多个消息接收中重复使用(请参阅C++ 基础页面上的“优化技巧”部分);因此,它们使用起来更难、更危险,但效率更高。

当然,这两种实现可以用相同的风格编写,但开发人员似乎认为易用性对 Java 更重要,而性能对 C++ 更重要,也许反映了这些语言的使用模式谷歌。

于 2011-02-04T17:54:01.573 回答
1

要跟进我的评论...

struct Person
{
   int id;
   std::string name;

   struct Builder
   {
      int id;
      std::string name;
      Builder &setId(int id_)
      {
         id = id_;
         return *this;
      }
      Builder &setName(std::string name_)
      {
         name = name_;
         return *this;
      }
   };

   static Builder build(/* insert mandatory values here */)
   {
      return Builder(/* and then use mandatory values here */)/* or here: .setId(val) */;
   }

   Person(const Builder &builder)
      : id(builder.id), name(builder.name)
   {
   }
};

void Foo()
{
   Person p = Person::build().setId(2).setName("Derek Jeter");
}

这最终被编译成与等效代码大致相同的汇编程序:

struct Person
{
   int id;
   std::string name;
};

Person p;
p.id = 2;
p.name = "Derek Jeter";
于 2010-02-26T16:46:23.580 回答
1

您声称“C++ 和 Java API 应该做同样的事情”是没有根据的。他们没有记录做同样的事情。每种输出语言都可以对 .proto 文件中描述的结构创建不同的解释。这样做的好处是,您在每种语言中获得的内容都是该语言的惯用。它最大程度地减少了您正在“用 C++ 编写 Java”的感觉。如果每个消息类都有一个单独的构建器类,那肯定是我的感受。

对于整数字段,来自protocfoo的 C++ 输出将在给定消息的类中包含一个方法。void set_foo(int32 value)

Java 输出将改为生成两个类。一个直接代表消息,但只有该字段的吸气剂。另一个类是构建器类,只有该字段的设置器。

Python 输出仍然不同。生成的类将包含一个您可以直接操作的字段。我预计 C、Haskell 和 Ruby 的插件也有很大不同。只要它们都可以表示可以转换为线路上等效位的结构,它们就完成了它们的工作。请记住,这些是“协议缓冲区”,而不是“API 缓冲区”。

C++ 插件的源代码随protoc发行版一起提供。如果您想更改set_foo函数的返回类型,欢迎您这样做。我通常会避免类似“它是开源的,所以任何人都可以修改它”的回答,因为建议某人学习一个全新的项目以进行重大更改以解决问题通常没有帮助。但是,我不认为在这种情况下会很难。最难的部分是找到为字段生成设置器的代码部分。一旦发现这一点,进行所需的更改可能会很简单。更改返回类型,并return *this在生成的代码末尾添加一条语句。然后,您应该能够以Hrnt'中给出的样式编写代码.

于 2010-02-26T16:17:58.753 回答
0

在 C++ 中,您必须显式管理内存,这可能会使该成语使用起来更加痛苦 - 要么build()必须为构建器调用析构函数,要么必须保留它以在构造Person对象后将其删除。要么对我来说有点吓人。

于 2010-02-19T07:47:22.873 回答