找回密码
 立即注册
首页 业界区 业界 C++ 封装 C FFI 接口最佳实践:以 Hugging Face Tokeniz ...

C++ 封装 C FFI 接口最佳实践:以 Hugging Face Tokenizer 为例

准挝 前天 14:50
1. 引入

在现代 AI 工程中,Hugging Face 的 tokenizers 库已成为分词器的事实标准。不过 Hugging Face 的 tokenizers 是用 Rust 来实现的,官方只提供了 python 和 node 的绑定实现。要实现与 Hugging Face tokenizers 相同的行为,最好的办法就是自己封装 Hugging Face tokenizers 的 C 绑定,从而可以被 C++ / C# / Java 这些高级编程语言调用。
2. 封装 C 接口

首先要说明的是,要做的不是完整的封装 Hugging Face tokenizers 的 C 的 FFI(Foreign Function Interface)接口,而是封装自己需要的接口就可以了。比如执行分词接口和计算Token的接口:
  1. use std::ffi::CStr;
  2. use std::os::raw::c_char;
  3. use tokenizers::{PaddingParams, Tokenizer, TruncationParams};
  4. // === 1. 定义 C 兼容的返回结构体 ===
  5. #[repr(C)]
  6. pub struct TokenizerResult {
  7.     pub input_ids: *mut i64,
  8.     pub attention_mask: *mut i64,
  9.     pub token_type_ids: *mut i64,
  10.     pub length: u64,
  11. }
  12. // === 2. 内部状态:包装 Tokenizer ===
  13. struct TokenizerHandle {
  14.     tokenizer: Tokenizer,     // 用于 encode(带 padding)
  15.     raw_tokenizer: Tokenizer, // 用于 count(无 padding)
  16. }
  17. // === 3. 辅助函数:将 Rust Vec 转为 C 可拥有的指针 ===
  18. fn vec_to_c_ptr(vec: Vec<i64>) -> *mut i64 {
  19.     let mut boxed = vec.into_boxed_slice();
  20.     let ptr = boxed.as_mut_ptr();
  21.     std::mem::forget(boxed); // 防止 Rust 自动释放
  22.     ptr
  23. }
  24. // === 4. 创建 tokenizer ===
  25. #[unsafe(no_mangle)] // 禁用 name mangling,让 C 能找到符号
  26. pub extern "C" fn tokenizer_create(tokenizer_json_path: *const c_char) -> *mut std::ffi::c_void {
  27.     if tokenizer_json_path.is_null() {
  28.         return std::ptr::null_mut();
  29.     }
  30.     let path_cstr = unsafe { CStr::from_ptr(tokenizer_json_path) };
  31.     let path_str = match path_cstr.to_str() {
  32.         Ok(s) => s,
  33.         Err(_) => return std::ptr::null_mut(),
  34.     };
  35.     let mut tokenizer = match Tokenizer::from_file(path_str) {
  36.         Ok(t) => t,
  37.         Err(_) => return std::ptr::null_mut(),
  38.     };
  39.     // 设置 padding/truncation 到 512(BGE 默认)
  40.     tokenizer.with_padding(Some(PaddingParams {
  41.         strategy: tokenizers::PaddingStrategy::Fixed(512),
  42.         ..Default::default()
  43.     }));
  44.     if tokenizer
  45.         .with_truncation(Some(TruncationParams {
  46.             max_length: 512,
  47.             ..Default::default()
  48.         }))
  49.         .is_err()
  50.     {
  51.         return std::ptr::null_mut();
  52.     }
  53.     let mut raw_tokenizer = tokenizer.clone();
  54.     raw_tokenizer.with_padding(None);
  55.     raw_tokenizer.with_truncation(None).ok();
  56.     let handle = TokenizerHandle {
  57.         tokenizer,
  58.         raw_tokenizer,
  59.     };
  60.     Box::into_raw(Box::new(handle)) as *mut std::ffi::c_void
  61. }
  62. //计算句子token
  63. #[unsafe(no_mangle)] // 禁用 name mangling,让 C 能找到符号
  64. pub extern "C" fn tokenizer_count(handle: *mut std::ffi::c_void, text: *const c_char) -> u64 {
  65.     if handle.is_null() || text.is_null() {
  66.         return 0;
  67.     }
  68.     let handle_ref = unsafe { &*(handle as *mut TokenizerHandle) };
  69.     let text_cstr = unsafe { CStr::from_ptr(text) };
  70.     let text_str = match text_cstr.to_str() {
  71.         Ok(s) => s,
  72.         Err(_) => return 0,
  73.     };
  74.     match handle_ref.raw_tokenizer.encode(text_str, true) {
  75.         Ok(encoding) => encoding.len() as u64,
  76.         Err(_) => 0,
  77.     }
  78. }
  79. // === 5. 销毁 tokenizer ===
  80. #[unsafe(no_mangle)]
  81. pub extern "C" fn tokenizer_destroy(handle: *mut std::ffi::c_void) {
  82.     if !handle.is_null() {
  83.         unsafe {
  84.             let _ = Box::from_raw(handle as *mut TokenizerHandle);
  85.             // Drop 自动调用
  86.         }
  87.     }
  88. }
  89. // === 6. 执行分词 ===
  90. #[unsafe(no_mangle)]
  91. pub extern "C" fn tokenizer_encode(
  92.     handle: *mut std::ffi::c_void,
  93.     text: *const c_char,
  94. ) -> TokenizerResult {
  95.     let default_result = TokenizerResult {
  96.         input_ids: std::ptr::null_mut(),
  97.         attention_mask: std::ptr::null_mut(),
  98.         token_type_ids: std::ptr::null_mut(),
  99.         length: 0,
  100.     };
  101.     if handle.is_null() || text.is_null() {
  102.         return default_result;
  103.     }
  104.     let handle_ref = unsafe { &*(handle as *mut TokenizerHandle) };
  105.     let text_cstr = unsafe { CStr::from_ptr(text) };
  106.     let text_str = match text_cstr.to_str() {
  107.         Ok(s) => s,
  108.         Err(_) => return default_result,
  109.     };
  110.     let encoding = match handle_ref.tokenizer.encode(text_str, true) {
  111.         Ok(e) => e,
  112.         Err(_) => return default_result,
  113.     };
  114.     let input_ids: Vec<i64> = encoding.get_ids().iter().map(|&x| x as i64).collect();
  115.     let attention_mask: Vec<i64> = encoding
  116.         .get_attention_mask()
  117.         .iter()
  118.         .map(|&x| x as i64)
  119.         .collect();
  120.     let token_type_ids: Vec<i64> = encoding.get_type_ids().iter().map(|&x| x as i64).collect();
  121.     // BGE 不需要,但 C++ 代码传了
  122.     // let token_type_ids: Vec<u32> = vec![0u32; input_ids.len()];
  123.     let len = input_ids.len(); // 应该是 512,但更通用
  124.     TokenizerResult {
  125.         input_ids: vec_to_c_ptr(input_ids),
  126.         attention_mask: vec_to_c_ptr(attention_mask),
  127.         token_type_ids: vec_to_c_ptr(token_type_ids),
  128.         length: len as u64,
  129.     }
  130. }
  131. // === 7. 释放结果内存 ===
  132. #[unsafe(no_mangle)]
  133. pub extern "C" fn tokenizer_result_free(result: TokenizerResult) {
  134.     if !result.input_ids.is_null() {
  135.         unsafe {
  136.             let _ = Vec::from_raw_parts(
  137.                 result.input_ids,
  138.                 result.length as usize,
  139.                 result.length as usize,
  140.             );
  141.         }
  142.     }
  143.     if !result.attention_mask.is_null() {
  144.         unsafe {
  145.             let _ = Vec::from_raw_parts(
  146.                 result.attention_mask,
  147.                 result.length as usize,
  148.                 result.length as usize,
  149.             );
  150.         }
  151.     }
  152.     if !result.token_type_ids.is_null() {
  153.         unsafe {
  154.             let _ = Vec::from_raw_parts(
  155.                 result.token_type_ids,
  156.                 result.length as usize,
  157.                 result.length as usize,
  158.             );
  159.         }
  160.     }
  161. }
复制代码
对应的 C 接口如下:
  1. // tokenizer_result.h
  2. #pragma once
  3. struct TokenizerResult {
  4.   int64_t* input_ids;
  5.   int64_t* attention_mask;
  6.   int64_t* token_type_ids;
  7.   uint64_t length;
  8. };
  9. #ifdef __cplusplus
  10. static_assert(std::is_standard_layout_v<TokenizerResult> &&
  11.                   std::is_trivially_copyable_v<TokenizerResult>,
  12.               "TokenizerResult must be C ABI compatible");
  13. #endif
复制代码
  1. // hf_tokenizer_ffi.h
  2. #pragma once
  3. #include <stdint.h>
  4. #include "tokenizer_result.h"
  5. #ifdef __cplusplus
  6. extern "C" {
  7. #endif
  8. void* tokenizer_create(const char* tokenizer_json_path);
  9. void tokenizer_destroy(void* handle);
  10. TokenizerResult tokenizer_encode(void* handle, const char* text);
  11. uint64_t tokenizer_count(void* handle, const char* text);
  12. void tokenizer_result_free(TokenizerResult result);
  13. #ifdef __cplusplus
  14. }
  15. #endif
复制代码
具体的封装细节笔者就不多说了,因为与本文的主题无关。不过可以稍稍了解一下其中的原理,也就是说,操作系统大多数是由 C 实现的,或者提供了 C 的接口。因此,绝大多数比 C 高级的编程语言都提供了与 C 交互的能力,当然前提是必须得按照 C 得规范组织数据和封装接口。比如这里的struct TokenizerResult就是一个兼容 C 的结构体,#[unsafe(no_mangle)]则表明这是一个 C 语言形式的函数接口。
3. 经典 C++ 封装

如上接口是一个标准的 C 风格式的接口:将分词器封装成一个 Handle ,也就是俗称的句柄。而后续具体的分词操作就通过这个句柄来进行,包括最后对资源的释放。在 C++ 中,当然也可以直接使用这种形式的接口,不过这样就需要遵循 C 的资源控制规则:资源申请和释放必须成对出现——比如这里的 tokenizer_create 和 tokenizer_destroy。
3.1 RAII 机制

不过这样就会有一个问题,过程式的流程中很难保证 tokenizer_create 和 tokenizer_destroy 能够成对调用,例如:
  1. tokenizer_create()
  2. if(...){
  3.     return;
  4. }
  5. tokenizer_destroy()
复制代码
只要在 tokenizer_create 和 tokenizer_destroy 之间出现分支,程序提前返回,就会导致资源没有释放而内存泄漏。为了避免这个问题,就需要在每次 return 之前,都调用 tokenizer_destroy()——这当然是非常不优雅的,既容易忘掉又是冗余代码。
为了解决这种资源管理难题,C++ 提供了一种强大而优雅的机制:RAII(Resource Acquisition Is Initialization,资源获取即初始化)。它的核心思想是:将资源的生命周期绑定到对象的生命周期上。具体来说,就是利用面向对象的思想,将资源控制的行为封装成一个类对象,并且保证资源在对象构造函数中获取,在析构函数中自动释放。由于 C++ 中栈对象在离开作用域时会自动调用析构函数,在离开作用域时会自动调用析构函数。因此这些资源总是可以被正确释放,从根本上杜绝内存泄漏或资源泄露。例如:
  1. Tokenizer tokenizer;
  2. //...操作
  3. if(...){
  4.     return;
  5. }
  6. //...更多操作
复制代码
3.2 拷贝语义

复习一下 C++ 面向对象设计的经典五法则(Rule of Five),如果一个类自定义了以下任意一个函数:

  • 析构函数(Destructor)
  • 拷贝构造函数(Copy Constructor)
  • 拷贝赋值运算符(Copy Assignment Operator)
  • 移动构造函数(Move Constructor)
  • 移动赋值运算符(Move Assignment Operator)
那么大概率也需要自定义另外四个函数,或者显式 = default / = delete 来控制行为。很多 C++ 程序员并不理解移动语义,但这并没有关系,我们可以先假定不定义移动构造函数和移动赋值运算符(或者显式 = default),此时移动操作就会退化为拷贝语义的行为。
而关于拷贝语义,绝大多数 C++ 程序员应该都知道这个问题:当在类对象中管理资源时,编译器生成的默认拷贝行为是“浅拷贝”,可能导致双重释放、内存泄漏等问题,因此需要自定义拷贝构造函数和拷贝赋值运算符来实现“深拷贝”的行为。因此,这个链条就很明确了:因为类中需要定义析构函数,所以需要同时定义拷贝构造函数和拷贝赋值运算符。
3.3 移动语义

进一步讨论,反正移动语义可以默认,那么是不是只用定义拷贝语义就行了呢?这个要看资源的定义:如果只是管理内存资源,那么这样做是没有问题的,至少是安全的。但是资源管理不仅仅指的是内存资源,还可以是一些文件句柄、网络连接等等。这些资源往往是独占性的,进行深拷贝往往会出现问题。因此就出现了 C++ 11 开始规定的移动语义:可以安全得实现“浅拷贝”的行为。同时还可以解决“深拷贝”的性能问题。
基于以上的思想,笔者封装的分词器对象如下:
  1. // HfTokenizer.h
  2. #pragma once
  3. #include <string>
  4. #include "hf_tokenizer_ffi.h"
  5. namespace hf {
  6. class Tokenizer {
  7. public:
  8.   explicit Tokenizer(const std::string& path);
  9.   // 析构函数
  10.   ~Tokenizer() noexcept;
  11.   // 禁止拷贝
  12.   Tokenizer(const Tokenizer&) = delete;
  13.   Tokenizer& operator=(const Tokenizer&) = delete;
  14.   // 移动语义
  15.   Tokenizer(Tokenizer&& rhs) noexcept;
  16.   Tokenizer& operator=(Tokenizer&& rhs) noexcept;
  17.   // 其他接口方法
  18.   // TokenizerResult Encode(const char* text) const;
  19.   // uint64_t Count(const char* text) const;
  20. private:
  21.   void* handle;  // 来自 tokenizer_create 的指针
  22. };
  23. }  // namespace hf
复制代码
  1. // HfTokenizer.cpp
  2. #include "HfTokenizer.h"
  3. #include <iostream>
  4. namespace hf {
  5. Tokenizer::Tokenizer(const std::string& path)
  6.     : handle(tokenizer_create(path.c_str())) {
  7.   if (!handle) {
  8.     throw std::runtime_error("Failed to create tokenizer from " + path);
  9.   }
  10. }
  11. Tokenizer::~Tokenizer() noexcept {
  12.   if (handle) {
  13.     tokenizer_destroy(handle);
  14.   }
  15. }
  16. // 移动语义
  17. Tokenizer::Tokenizer(Tokenizer&& rhs) noexcept : handle(rhs.handle) {
  18.   rhs.handle = nullptr;
  19. }
  20. Tokenizer& Tokenizer::operator=(Tokenizer&& rhs) noexcept {
  21.   if (this != &rhs) {
  22.     if (handle) {
  23.       tokenizer_destroy(handle);
  24.     }
  25.     handle = rhs.handle;
  26.     rhs.handle = nullptr;
  27.   }
  28.   return *this;
  29. }
  30. }  // namespace hf
复制代码
如前所述,因为封装的是一个句柄,为了避免资源控制的麻烦,就禁止掉拷贝语义:
  1. // 禁止拷贝
  2. Tokenizer(const Tokenizer&) = delete;
  3. Tokenizer& operator=(const Tokenizer&) = delete;
复制代码
进行()拷贝构造或者=赋值构造看起来似乎很简单,其实在代码层层嵌套之后,就可能很难分析出是不是调用了默认的拷贝的行为,比如函数传参、容器操作等等。当然深拷贝的实现也不是性能最优,因此干脆就直接删除掉拷贝构造函数和拷贝赋值运算符。
没有拷贝语义,那么就需要移动语义来进行传递对象了。其实移动语义没那么难,我们只要把握住一点,移动语义的目的是安全地实现“浅拷贝”。以移动赋值运算符的实现来说,如果要实现如下移动赋值:
  1. Tokenizer A();
  2. Tokenizer B();
  3. B = std::move(A);
复制代码
就需要以下的行为:

  • 释放掉B管理的资源。
  • 将A中的成员“浅拷贝”到B中,让B接管A的资源。
  • 将A中成员初始化。
具体实现就是如下所示:
  1. Tokenizer& Tokenizer::operator=(Tokenizer&& rhs) noexcept {
  2.   if (this != &rhs) {
  3.     if (handle) {
  4.       tokenizer_destroy(handle);
  5.     }
  6.     handle = rhs.handle;
  7.     rhs.handle = nullptr;
  8.   }
  9.   return *this;
  10. }
复制代码
移动构造函数就更加简单了,因为B对象在移动构造之前成员并没有初始化:
  1. Tokenizer A();
  2. Tokenizer B(std::move(A));
复制代码
因此可以省略掉释放自身资源的步骤,具体实现也就是如下所示:
  1. Tokenizer::Tokenizer(Tokenizer&& rhs) noexcept : handle(rhs.handle) {
  2.   rhs.handle = nullptr;
  3. }
复制代码
最后还有一个问题:A通过移动语义转移到B了,A还能使用吗?不能也没必要使用A了,无论是A对象和B对象其实是一个栈对象(当然内部管理的数据成员可能放在堆上),或者说是一个值对象;这跟引用对象或者地址对象完全不同。移动语义的本质是对象所有权的转移,转移之后原对象中资源所有权就不存在了,即使强行访问,要么访问不到,要么会程序崩溃。
4. 高级 C++ 封装

4.1 零法则

使用 RAII 机制 + 经典五法则来设计一个类对象,还有一个优点,就是使用这个类对象作为数据成员的类,就不用再显式实现析构函数。不用显式实现析构函数,也就意味着不用实现拷贝语义和移动语义,完全可以依赖类对象拷贝和移动的默认行为。举例来说,一个MyResource对象,管理着一段内存 buffer ,它的类定义为:
  1. class MyResource {
  2. public:
  3.     // 构造:申请资源
  4.     MyResource() {
  5.         data = new int[100];
  6.     }
  7.     // 析构:释放资源
  8.     ~MyResource() {
  9.         delete[] data;
  10.     }
  11.     // 拷贝构造:深拷贝
  12.     MyResource(const MyResource& other) {
  13.         data = new int[100];
  14.         copy(other.data, other.data + 100, data);
  15.     }
  16.     // 拷贝赋值
  17.     MyResource& operator=(const MyResource& other) {
  18.         if (this != &other) {
  19.             delete[] data;
  20.             data = new int[100];
  21.             copy(other.data, other.data + 100, data);
  22.         }
  23.         return *this;
  24.     }
  25.     // 移动构造:接管资源
  26.     MyResource(MyResource&& other) noexcept {
  27.         data = other.data;
  28.         other.data = nullptr;
  29.     }
  30.     // 移动赋值
  31.     MyResource& operator=(MyResource&& other) noexcept {
  32.         if (this != &other) {
  33.             delete[] data;
  34.             data = other.data;
  35.             other.data = nullptr;
  36.         }
  37.         return *this;
  38.     }
  39. private:
  40.     int* data = nullptr;
  41. };
复制代码
但是如果我使用 std 容器vector ,相应的代码就可以简写为:
  1. #include <vector>
  2. class MyResource {
  3. public:
  4.     // 构造:自动分配内存
  5.     MyResource() : data(100) {}  // vector<int> 自动初始化为 100 个元素
  6.     // ✅ 无需显式定义析构函数
  7.     // ✅ 无需自定义拷贝构造 / 拷贝赋值
  8.     // ✅ 无需自定义移动构造 / 移动赋值
  9.     // 编译器自动生成的版本已正确、高效、异常安全
  10. private:
  11.     std::vector<int> data;  // RAII 自动管理内存
  12. };
复制代码
这不是因为 vector 使用了什么魔法,而是 vector 本身就是使用了 RAII 机制 + 经典五法则来设计的一个模板类对象!在 MyResource 对象进行拷贝或者移动的时候,作为数据成员,std::vector data也会采取同样的拷贝或者移动的行为,并且默认的、由编译器自动生成的版本就可以正确处理。
以上这个思想,就是现代 C++ 更推荐的零法则(Rule of Zero):尽量不要手动管理资源,而是使用 RAII 类型让编译器自动生成所有特殊成员函数。而这个 RAII 类型,可以是 std 的任何容器对象、智能指针,也可以是自己按照五法则实现的类对象。
4.2 智能指针

回到本文引入的问题,如果我的分词器实现不像写拷贝语义和移动语义怎么办呢?毕竟都是样板代码,写不好还容易出问题。此时我们就可以使用智能指针 unique_ptr 。常规意义上,我们都知道智能指针可以在没有任何其他对象引用的情况下自动 delete ,其实智能指针还可以自定义资源的释放行为:
  1. #pragma once
  2. #include <memory>
  3. #include <string>
  4. namespace hf {
  5. class Tokenizer {
  6. public:
  7.   explicit Tokenizer(const std::string& path);
  8.   // 编译器自动生成:
  9.   // - 析构函数
  10.   // - 移动构造 / 移动赋值
  11.   // - 禁止拷贝(因为 unique_ptr 不可拷贝)
  12. private:
  13.   std::unique_ptr<void, void (*)(void*)> handle;
  14. };
  15. }  // namespace hf
复制代码
  1. #include "HfTokenizer.h"
  2. #include <stdexcept>
  3. #include "hf_tokenizer_ffi.h"
  4. namespace hf {
  5. static void HandleDeleter(void* handle) noexcept {
  6.   if (handle) {
  7.     tokenizer_destroy(handle);
  8.   }
  9. }
  10. Tokenizer::Tokenizer(const std::string& path)
  11.     : handle(tokenizer_create(path.c_str()), HandleDeleter) {
  12.   if (!handle) {
  13.     throw std::runtime_error("Failed to create tokenizer from " + path);
  14.   }
  15. }
  16. }  // namespace hf
复制代码
如上实现所示,函数 HandleDeleter 就是 std::unique_ptr handle 的自定义析构行为,在类对象析构的时候就会自动调用这个函数释放资源。既然资源被智能托管了,那么自然就不用写析构函数;析构函数不用写,那么拷贝构造函数、拷贝赋值运算符、移动构造函数以及移动赋值运算符都可以不用实现,全部可以依赖编译器自动生成。当然,由于 unique_ptr 只能移动不能拷贝,Tokenizer也就只能移动不能拷贝。
5. 总结

最后,笔者就给出 C++ 封装 C FFI 接口的完整实现,如下所示:
  1. // HfTokenizer.h
  2. #pragma once
  3. #include <memory>
  4. #include <string>
  5. #include "tokenizer_result.h"
  6. namespace hf {
  7. class Tokenizer {
  8. public:
  9.   explicit Tokenizer(const std::string& path);
  10.   // 编译器自动生成:
  11.   // - 析构函数(调用 Deleter)
  12.   // - 移动构造 / 移动赋值
  13.   // - 禁止拷贝(因为 unique_ptr 不可拷贝)
  14.   // 其他接口方法
  15.   uint64_t Count(const std::string& text) const;
  16.   // 向量化
  17.   using ResultPtr =
  18.       std::unique_ptr<TokenizerResult, void (*)(TokenizerResult*)>;
  19.   ResultPtr Encode(const std::string& text) const;
  20. private:
  21.   std::unique_ptr<void, void (*)(void*)> handle;
  22. };
  23. }  // namespace hf
复制代码
  1. // HfTokenizer.cpp
  2. #include "HfTokenizer.h"
  3. #include <stdexcept>
  4. #include "hf_tokenizer_ffi.h"
  5. namespace hf {
  6. static void HandleDeleter(void* handle) noexcept {
  7.   if (handle) {
  8.     tokenizer_destroy(handle);
  9.   }
  10. }
  11. static void ResultDeleter(TokenizerResult* p) noexcept {
  12.   if (p) {
  13.     tokenizer_result_free(*p);
  14.     delete p;
  15.   }
  16. }
  17. Tokenizer::Tokenizer(const std::string& path)
  18.     : handle(tokenizer_create(path.c_str()), HandleDeleter) {
  19.   if (!handle) {
  20.     throw std::runtime_error("Failed to create tokenizer from " + path);
  21.   }
  22. }
  23. uint64_t Tokenizer::Count(const std::string& text) const {
  24.   return tokenizer_count(handle.get(), text.c_str());
  25. }
  26. Tokenizer::ResultPtr Tokenizer::Encode(const std::string& text) const {
  27.   auto result = std::make_unique<TokenizerResult>(
  28.       tokenizer_encode(handle.get(), text.c_str()));
  29.   return {result.release(), ResultDeleter};
  30. };
  31. }  // namespace hf
复制代码
不仅是句柄,连传递的数据对象笔者都托管给智能指针,从而避免大量写特殊成员函数这些样板代码。不得不说,RAII 的设计思路非常精妙,同时保证了安全性与简洁性,给人一种回归编程原始状态的感觉。所谓“大道至简”,不是代码越繁复就越安全,也不是代码越抽象就越厉害;真正好的代码,是在正确性、可维护性与简洁性之间取得平衡,让资源管理如呼吸般自然,而非负担。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

昨天 03:40

举报

10 小时前

举报

您需要登录后才可以回帖 登录 | 立即注册