语义版本控制:更改*应该*通过库函数分配的非不透明结构

Semantic versioning: changing a non-opaque struct that *should* be allocated through library functions

我的 C 库,在版本 1.0.0 中,定义了一个结构和一些函数来分配和使用该结构:

typedef struct { int x; int y; } MyStruct;
MyStruct *allocate( int, int );
void destroy( MyStruct* );
void print( MyStruct* );

用户不应该自己分配结构,也不应该按值复制它。这是与问题 的主要区别。例如一个程序应该像这样使用它:

void f(){
  MyStruct *ms = allocate(0,0);
  ms->x += 1;
  print(ms);
  destroy(ms);
}

现在我需要在结构中添加一个新字段,同时函数的签名不变。

typedef struct { int x; int y; int z; } MyStruct;

新结构比旧结构占用更多内存:如果程序试图直接分配一个 MyStruct 实例或按值复制它,如果它链接到不同版本的库,它可能会崩溃从它构建的那个开始。

然而这不是程序使用的方式 MyStruct:只要他们遵循文档,一切都可以正常工作。但是代码中没有任何内容可以阻止他们滥用结构。

我正在使用语义版本控制对我的库进行版本控制。在上述情况下,我应该增加次要版本(以向后兼容的方式添加的功能)还是主要版本(不兼容的 API 更改)?

您对公开可见的结构进行了重大更改,该结构在 C 中不向后兼容早期版本。除了结构大小的变化之外,您自己的示例还准确地说明了为什么这是一个重大变化:

void f(){
  MyStruct *ms = allocate(0,0);
  ms->x += 1;
  print(ms);   // Indicates you are aware of external dependencies.
  destroy(ms);
}

您正在公开一个数据结构,您的客户可能会将其合并到某种数据集中。您可能对如何使用您的图书馆有一些想法,但我可以向您保证,您的客户总会给您带来惊喜。

鉴于您发布的代码,以及您声明的关于如何使用您的库的意图,我认为您需要重新设计您的 API,这样 MyStruct 就完全不透明了用户并且永远不会改变大小(客户经常缓存这样的东西)。借用标准库的一页,用个handle.

typedef int MyHandle;
MyHandle allocate(int x, int y);
void destroy(MyHandle h);
void print(MyHandle h);

句柄可以由您的内部代码进行范围检查,然后用作 table 结构或结构指针的索引,或者它可以是二叉树的键。关键是你可以自由地做任何你想做的事,而不会打扰你的 API.

如果您想让 x y 位可见,请使用可扩展结构:

typedef struct { 
    int x; 
    int y; 
    void* reserved; // For internal use only!
} MyStruct;

MyStruct.reserved 字段应始终为 NULL,直到您需要它供内部使用。请记住,一旦您像这样公开数据字段,您的客户如何使用它们就完全不受您的控制。当您以这种方式公开结构时,您就是在向您的客户做出承诺。

关于 getter 和 setter。

// Using the MyHandle type described earlier:
typedef struct _My_XY {
    int x;
    int y;
} My_XY;

My_XY GetXY(MyHandle h);

问题已解决。


我要补充一点,因为添加 z 字段无论如何都是一个重大变化,而且您似乎觉得有必要扩展您的产品的功能,这将是激进 API重写。但是,如果您必须在不破坏客户群的情况下扩展它,则可以在内部隐藏 z 数据点,例如:

// Public API
typedef struct { int x; int y; } MyStruct;
MyStruct *allocate( int, int );
void destroy( MyStruct* );
void print( MyStruct* );
// Implementation

typedef struct _XY_Node {
    MyStruct* xy;
    int z;
    struct _XY_Node *pNext;
} XY_Node;

XY_Node root = null;

XY_Node* AddNode(int x, int y, int z) {...}
XY_Node* RemoveNode(int x, int y, int z) {...}
XY_Node* FindNode(int x, int y, int z) {...}

int DeriveZ(int x, int y) {...}

MyStruct *allocate(int x, int y)
{
    return AddNode(x, y, DeriveZ(x, y)).xy;
}

// etc...

您可以使用散列 table 或某种形式的二叉树,而不是列表。关键是,您不必为了扩展它而破坏 API。您可以将此作为 1.y.z 系列的最终版本,然后彻底检查您的 API,以便您拥有更清晰的客户体验和更高效的实施。