ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C++ explicit关键字详解:从隐式转换陷阱到工程最佳实践

C++ explicit关键字详解:从隐式转换陷阱到工程最佳实践 1. 项目概述为什么我们需要关注explicit这个“小”关键字在C的日常开发中我们常常被各种复杂的设计模式、性能优化和内存管理所吸引却容易忽略一些看似简单、实则影响深远的语言细节。explicit关键字就是这样一个典型的例子。它只有短短几个字母却直接关系到代码的健壮性、可读性和安全性。很多C程序员尤其是初学者对它的理解可能停留在“防止隐式转换”这个模糊的概念上但具体到为什么需要防止、在什么场景下防止、以及如何正确使用往往一知半解。我见过不少项目因为构造函数或类型转换运算符缺少一个explicit导致代码行为变得诡异且难以调试。例如一个接受int的构造函数可能会在你毫无察觉的情况下将一个double值“悄悄”转换并构造出对象这种隐式行为是许多潜在Bug的温床。explicit关键字就是C赋予我们的一把锁它要求程序员必须显式地表达转换意图从而让编译器帮助我们捕获那些可能导致逻辑错误的模糊操作。这篇文章我将从一个资深C开发者的视角彻底拆解explicit关键字。我们不仅要知道它“是什么”更要深挖它“为什么”存在以及在实际项目中“如何”精准、有效地使用它。这不仅仅是语法学习更是一种编写更安全、更清晰、更易于维护的C代码的工程实践。2.explicit关键字的核心概念与设计初衷2.1 隐式转换便利背后的陷阱要理解explicit必须先理解C中的隐式转换。C为了提供灵活性允许编译器在某些情况下自动进行类型转换而无需程序员显式写出转换代码。这主要发生在两种场景通过单参数构造函数进行的转换以及通过类型转换运算符进行的转换。让我们先看一个没有explicit的典型例子class MyString { public: // 单参数构造函数允许从 const char* 隐式转换为 MyString MyString(const char* str) { std::cout MyString constructed from: \ str \ std::endl; // ... 实际的内存分配和拷贝操作 } void print() const { std::cout Printing MyString std::endl; } }; void displayString(const MyString str) { str.print(); } int main() { // 场景1直接构造没问题 MyString s1(Hello); // 场景2隐式转换发生 // 编译器发现 displayString 需要一个 MyString 对象 // 但传入的是 World (const char[6] 类型可退化为 const char*)。 // 于是它“默默地”调用 MyString(const char*) 构造函数 // 生成一个临时 MyString 对象然后传递给函数。 displayString(World); // 场景3赋值初始化中的隐式转换 MyString s2 C; // 这看起来像赋值实际上是初始化同样触发隐式转换 return 0; }运行这段代码你会看到三次构造输出。对于displayString(World)这行代码从人类阅读的角度意图可能是模糊的我们是真的想创建一个临时的MyString对象还是不小心传错了参数这种隐式行为降低了代码的清晰度。更危险的情况出现在逻辑更复杂的类中。假设我们有一个DatabaseConnection类其构造函数接受一个int作为端口号。如果这个构造函数不是explicit的那么下面这种代码就能编译通过void connectToDatabase(const DatabaseConnection conn); // ... int someValue getSomeIntegerValue(); // 可能返回一个错误码比如 -1 connectToDatabase(someValue); // 糟糕将错误码隐式转换成了一个数据库连接对象这种Bug非常隐蔽因为编译器不会报错它“好心”地帮你做了转换但运行时的行为完全不符合预期。2.2explicit的设计哲学要求显式意图explicit关键字的设计哲学就是对抗这种“过度的便利性”。它强制要求任何可能产生歧义或非预期的类型转换必须由程序员在代码中明确写出。这体现了C“不为不需要的特性付出代价”和“让错误在编译期暴露”的核心思想。当你在构造函数或类型转换运算符前加上explicit就等于告诉编译器和代码的后续读者“这个转换是有代价的、或是有特定语义的不能随意发生。如果你想转换请明确地写出来。”将上面的MyString构造函数改为explicitclass MyString { public: explicit MyString(const char* str) { std::cout MyString explicitly constructed from: \ str \ std::endl; } // ... }; void displayString(const MyString str) { str.print(); } int main() { MyString s1(Hello); // 仍然OK这是直接初始化 // displayString(World); // 错误无法将 ‘const char*’ 转换为 ‘const MyString’ displayString(MyString(World)); // 正确必须显式构造 // MyString s2 C; // 错误拷贝初始化不允许隐式转换 MyString s2 MyString(C); // 正确显式构造虽然用了但右边是显式转换 MyString s3{C}; // 正确C11的统一初始化语法也是直接初始化允许调用explicit构造函数 return 0; }现在任何隐式转换的尝试都会被编译器阻止。代码的意图变得清晰无比displayString(MyString(World))明确地表示“我要用一个字符串构造一个临时MyString对象并传入”。这消除了阅读时的歧义也从根本上杜绝了因意外转换而引入的Bug。注意explicit对拷贝初始化用的影响与对直接初始化用括号或花括号的影响是不同的。这是理解explicit行为的一个关键细节。3.explicit关键字的两种主要应用场景3.1 应用于构造函数这是explicit最常用、最重要的场景。它用于修饰单参数构造函数或者除了第一个参数外都有默认值的多参数构造函数即可以被单个参数调用的构造函数。3.1.1 单参数构造函数的陷阱为什么单参数构造函数特别需要关注因为它是隐式转换的“入口”。多参数构造函数在C11之前通常无法用于隐式转换因为调用时需要多个参数不匹配常见的一对一转换场景。但单参数构造函数天然就定义了一种从参数类型到类类型的转换路径。考虑一个表示“缓冲区大小”的类class BufferSize { int size_; public: BufferSize(int size) : size_(size) { if (size 0) { // 可能抛异常或记录错误 std::cerr Warning: Non-positive buffer size. std::endl; } } int get() const { return size_; } }; class NetworkBuffer { BufferSize size_; public: NetworkBuffer(BufferSize size) : size_(size) { std::cout Buffer created with size: size.get() std::endl; } }; void processBuffer(NetworkBuffer buf) { // 处理缓冲区 } int main() { // 看似合理的调用 processBuffer(1024); // 隐式转换int - BufferSize - NetworkBuffer // 但如果有这样一个函数重载呢 void processBuffer(int rawSize); // 另一个重载版本 // 此时 processBuffer(1024) 就会产生歧义编译错误。 // 更糟糕的是如果没有重载这里 silently 构造了一个 BufferSize(1024) // 而1024这个魔法数字的语义被隐藏了。 return 0; }processBuffer(1024)这行代码数字1024的语义是模糊的。它是缓冲区大小吗还是其他什么标识符通过添加explicit我们强制调用者说明意图class BufferSize { int size_; public: explicit BufferSize(int size) : size_(size) { // ... 校验逻辑 } int get() const { return size_; } }; int main() { // processBuffer(1024); // 错误不允许隐式转换 processBuffer(BufferSize(1024)); // 正确显式表明了“1024是一个缓冲区大小” processBuffer(NetworkBuffer(BufferSize(1024))); // 更清晰的链式显式构造 return 0; }现在代码的意图一目了然。BufferSize(1024)明确地创建了一个具有“大小”语义的对象。3.1.2 多参数构造函数与explicit(C11起)在C11之前explicit主要作用于单参数构造函数。但从C11开始explicit可以用于任何构造函数包括多参数构造函数。这主要用于防止列表初始化使用花括号{}时的隐式转换。class Rectangle { int width, height; public: // 两个参数的构造函数 Rectangle(int w, int h) : width(w), height(h) {} }; void draw(const Rectangle rect) { // 绘制矩形 } int main() { // C11 列表初始化 draw({10, 20}); // 在C11中这会隐式调用 Rectangle(10, 20) 构造一个临时对象 // 这可能不是我们想要的尤其是当存在其他重载时。 return 0; }如果你不希望{10, 20}被隐式地解释为一个Rectangle就应该将构造函数声明为explicitclass Rectangle { int width, height; public: explicit Rectangle(int w, int h) : width(w), height(h) {} }; int main() { // draw({10, 20}); // 错误不允许从初始化列表隐式转换 draw(Rectangle{10, 20}); // 正确必须显式构造 return 0; }实操心得对于值类型如Point,Color,Rectangle有时允许隐式转换可以提高代码简洁性如draw({10, 20})。但对于具有重要不变式、资源所有权或特定语义的类如BufferSize,FilePath,DatabaseHandle将其构造函数设为explicit是更安全的选择。这是一个设计权衡核心原则是如果隐式转换可能掩盖错误或导致歧义就使用explicit。3.2 应用于类型转换运算符 (C11起)C11 允许将explicit用于用户定义的类型转换运算符转换函数。这解决了另一个历史问题意外的、不受欢迎的转换到其他类型。假设我们有一个SmartBool类它封装了一个布尔值但希望控制它到bool的转换比如只在特定上下文中允许class SmartBool { bool value_; public: SmartBool(bool b) : value_(b) {} // C11 之前的转换运算符允许在任何需要bool的地方隐式转换 operator bool() const { return value_; } }; void check(bool condition) { if (condition) std::cout True std::endl; } int main() { SmartBool sb{true}; check(sb); // 隐式调用 operator bool()输出 True // 一些令人意外的场景 int i sb; // 哦SmartBool 先隐式转为 bool然后 bool 提升为 int。i 现在是 1。 std::cout i std::endl; // 输出 1 // 更离谱的 int j sb 10; // sb - bool - int (1)然后 1 10 11 std::cout j std::endl; // 输出 11 // 这通常不是类的设计者想要的行为。 return 0; }这种无限制的隐式转换可能导致非常令人困惑的代码和潜在的错误。C11 的explicit转换运算符解决了这个问题class SmartBool { bool value_; public: SmartBool(bool b) : value_(b) {} // explicit 转换运算符禁止隐式转换 explicit operator bool() const { return value_; } }; int main() { SmartBool sb{true}; // check(sb); // 错误不能隐式转换为 bool check(static_castbool(sb)); // 正确显式转换 check(bool(sb)); // 正确函数式显式转换 // 以下都会编译错误阻止了意外的算术操作 // int i sb; // int j sb 10; // 但是在明确的布尔语境中explicit operator bool 仍然可以被调用 if (sb) { // 正确if 语句是布尔语境允许调用 explicit operator bool std::cout sb is true in if statement std::endl; } bool flag sb ? true : false; // 正确条件运算符也是布尔语境 return 0; }explicit operator bool是现代C中实现“安全布尔”Safe Bool惯用法的标准方式。它允许类在布尔语境如if,while,for,!,,||,?:中被使用但阻止了其向其他类型如int的不必要转换。标准库中的智能指针如std::unique_ptr和流类型如std::ifstream都使用了这一技术来检查其有效性如if (ptr)或if (stream)。注意事项explicit对转换运算符和构造函数的影响有一个细微差别。对于构造函数explicit禁止了拷贝初始化中的隐式转换但允许直接初始化。对于转换运算符explicit禁止了所有隐式转换但允许在特定上下文布尔语境、条件运算符、!、、||等中被隐式调用。这是语言特意为explicit operator bool开的后门以实现“上下文转换”。4. 深入解析explicit与各种初始化方式的交互理解explicit如何与C复杂的初始化规则交互是掌握其用法的关键。C有拷贝初始化、直接初始化、列表初始化等多种方式explicit对它们的影响各不相同。4.1 拷贝初始化 vs. 直接初始化这是受explicit影响最显著的一对概念。拷贝初始化 (Copy Initialization)使用等号进行初始化。语法是T obj other;。它要求初始化器other可以隐式转换为T类型。直接初始化 (Direct Initialization)使用括号()或花括号{}C11起进行初始化。语法是T obj(other);或T obj{other};。它直接调用构造函数允许调用explicit构造函数。让我们通过一个例子来对比class ExplicitClass { public: explicit ExplicitClass(int) {} }; class ImplicitClass { public: ImplicitClass(int) {} // 非 explicit }; int main() { int val 5; // 对于非 explicit 构造函数 ImplicitClass ic1 val; // 正确拷贝初始化允许隐式转换 ImplicitClass ic2(val); // 正确直接初始化 ImplicitClass ic3{val}; // 正确直接初始化列表初始化 // 对于 explicit 构造函数 // ExplicitClass ec1 val; // 错误拷贝初始化不允许调用 explicit 构造函数 ExplicitClass ec2(val); // 正确直接初始化允许调用 explicit 构造函数 ExplicitClass ec3{val}; // 正确直接初始化列表初始化允许调用 explicit 构造函数 // 一个特例即使使用如果右边是显式的类型转换也是允许的。 // 但这本质上不是隐式转换而是直接初始化的另一种写法。 ExplicitClass ec4 ExplicitClass(val); // 正确右边是显式构造的临时对象 ExplicitClass ec5 (ExplicitClass)val; // 正确C风格强制转换本质是显式请求转换 return 0; }核心规则explicit构造函数不能用于拷贝初始化中的隐式转换。它可以用于所有形式的直接初始化。4.2 列表初始化 (C11) 与explicitC11引入的列表初始化使用花括号{}行为比较复杂它旨在提供统一的初始化语法并防止“最令人烦恼的解析”问题。它与explicit的交互需要特别注意。class Widget { public: explicit Widget(int) {} Widget(int, int) {} // 非 explicit 的双参数构造函数 }; void process(const Widget w) {} int main() { // 单参数情况 Widget w1{5}; // 正确直接列表初始化可以调用 explicit 构造函数 Widget w2 {5}; // 错误拷贝列表初始化不能调用 explicit 构造函数 // process({5}); // 错误{5}作为参数是拷贝列表初始化不能调用 explicit 构造函数 process(Widget{5}); // 正确显式构造 // 多参数情况假设构造函数非 explicit Widget w3{1, 2}; // 正确直接列表初始化 Widget w4 {1, 2}; // 正确拷贝列表初始化因为 Widget(int,int) 非 explicit process({1, 2}); // 正确{1,2}作为参数可以调用非 explicit 的 Widget(int,int) // 如果 Widget(int,int) 也是 explicit 的 class WidgetEx { public: explicit WidgetEx(int) {} explicit WidgetEx(int, int) {} }; // WidgetEx we1 {1, 2}; // 错误拷贝列表初始化不能调用 explicit 构造函数 // process({1, 2}); // 错误同上 WidgetEx we2{1, 2}; // 正确直接列表初始化可以 return 0; }总结列表初始化规则直接列表初始化 (T obj{args...};)行为类似于直接初始化可以调用explicit构造函数。拷贝列表初始化 (T obj {args...};或函数调用f({args...}))行为类似于拷贝初始化不能调用explicit构造函数。实操心得在C11及以后的代码中我倾向于使用直接列表初始化{}来代替旧的括号()初始化。因为它更安全防止窄化转换、更统一并且能避免一些语法歧义。当你的构造函数是explicit时记住在函数传参或使用初始化时必须显式构造对象不能依赖{args}进行隐式转换。4.3explicit与转换序列的抑制explicit不仅阻止单步的隐式转换它还能阻止包含该转换的多步隐式转换序列。class A { public: explicit A(int) {} }; class B { public: B(const A) {} // B可以从A构造且这个构造函数不是 explicit 的 }; void takeB(const B) {} int main() { // takeB(10); // 错误转换序列是int - A - B。 // 虽然 B(const A) 非 explicit但第一步 int - A 是 explicit 的因此整个序列被禁止。 takeB(A(10)); // 正确显式完成 int - A 的转换 takeB(B(A(10))); // 更显式的写法 return 0; }这个特性非常重要它保证了explicit的“防火墙”作用不会被绕过。只要转换路径上有一个环节是explicit的整个隐式转换链就被切断。5. 实战指南何时使用与避免使用explicit理解了原理和语法最终要落实到工程决策上什么时候该用explicit5.1 强烈建议使用explicit的场景单参数构造函数且参数类型是基本类型或常见类型这是最经典的场景。例如String(const char*),BufferSize(int),PortNumber(uint16_t),FilePath(const std::string)。这些转换很容易意外发生且可能掩盖错误。“包装器”或“代理”类例如智能指针虽然标准库实现了、类型安全的typedef如using Meter StrongTypedouble, struct MeterTag;其构造函数应为explicit。资源管理类如文件句柄、网络连接、数据库连接、锁守卫等。它们的构造通常涉及资源获取隐式转换可能导致资源泄漏或双重释放。具有明确不变式或验证逻辑的类如表示角度0-360、百分比0-100、非空字符串等的类。隐式转换可能绕过构造函数中的验证逻辑。operator bool转换函数几乎总是应该声明为explicit除非你有非常特殊的理由需要它像普通bool一样参与算术运算这很少见。5.2 可以考虑不使用explicit的场景拷贝构造函数和移动构造函数它们几乎从不应该是explicit的。explicit拷贝/移动构造函数会阻止按值传参和返回破坏许多语言特性。值类型或“透明”包装类例如std::complexT,std::pairT,U或者你自己定义的Point2D、ColorRGB类。这些类的存在主要是为了组合数据隐式转换可以大大简化代码如draw(Point{10, 20})可以写成draw({10, 20})。旨在提供无缝互操作性的类例如一个自定义的字符串类可能希望与const char*和std::string无缝交互以方便替换或混合使用。默认构造函数explicit对默认构造函数也有影响C11起允许。explicit默认构造函数会阻止T obj {};这样的初始化。通常不需要这样做除非你有特殊理由禁止默认构造的隐式使用。5.3 一个实用的决策流程面对一个构造函数你可以问自己以下几个问题这个转换是“自然”的吗就像double转int那样自然还是说它代表了一个重要的、有副作用的操作例如打开文件、分配内存、建立连接。如果是后者用explicit。隐式转换会掩盖常见的错误吗比如会不会不小心把一个整数错误地当作某种句柄来使用如果是用explicit。这个类会被用在函数重载解析的敏感场景吗如果存在多个重载函数隐式转换会不会导致令人惊讶的重载选择如果是用explicit可以减少歧义。为了代码的清晰性我是否希望调用者明确写出转换如果答案是“是”那么就用explicit。个人经验法则对于非平凡的类即不仅仅是数据聚合而有自己的行为或不变量其单参数构造函数默认应该考虑设为explicit。只有当隐式转换能带来显著且安全的便利性并且不会引入歧义时才省略它。这是一个“默认拒绝谨慎允许”的策略。6. 常见问题、陷阱与最佳实践6.1 模板与explicit的交互在模板编程中explicit的行为需要仔细考虑。特别是当使用std::enable_if、std::is_constructible等类型特征时。templatetypename T class Box { T value; public: // 我们可能希望根据 T 的特性来决定构造函数是否 explicit // 例如如果 T 可以从 int 构造但构造可能不平凡我们可能希望 Box 的构造函数是 explicit 的。 // 但这通常需要复杂的 SFINAE 或 C20 的 concepts。 templatetypename U, typename std::enable_if_tstd::is_constructible_vT, U explicit Box(U u) : value(std::forwardU(u)) {} // 注意这是一个通用转发构造函数它被声明为 explicit。 // 这意味着对于任何 U转换都是显式的。 }; int main() { Boxint b1 42; // 错误构造函数是 explicit 的 Boxint b2(42); // 正确 Boxstd::string b3 hello; // 错误即使 std::string 可以从 const char* 隐式转换 // 但 Box 的构造函数是 explicit 的阻止了它。 Boxstd::string b4(hello); // 正确 return 0; }在编写模板类时是否将转发构造函数设为explicit是一个设计决策。标准库的std::optional,std::variant等它们的转换构造函数通常是explicit的以提供更强的类型安全。6.2explicit与继承explicit属性不会被继承。如果基类有一个explicit构造函数派生类在定义自己的构造函数时需要重新指定explicit。class Base { public: explicit Base(int) {} }; class Derived : public Base { public: // 使用 using 声明继承构造函数 (C11) using Base::Base; // 这会继承 Base::Base(int)并且继承其 explicit 属性 // Derived(int) 现在也是 explicit 的。 }; class Derived2 : public Base { public: // 或者手动定义构造函数 Derived2(int x) : Base(x) {} // 这个构造函数不是 explicit 的除非你加上关键字 // 注意这里 Derived2(int) 不是 explicit 的尽管它调用了 Base 的 explicit 构造函数。 }; void takeBase(const Base) {} void takeDerived(const Derived) {} void takeDerived2(const Derived2) {} int main() { // takeBase(42); // 错误Base(int) is explicit takeBase(Base(42)); // 正确 // takeDerived(42); // 错误继承来的构造函数保持了 explicit takeDerived(Derived(42)); // 正确 takeDerived2(42); // 正确因为 Derived2(int) 不是 explicit 的 // 这可能会让人困惑Derived2 可以从 int 隐式构造但它的基类部分需要 explicit 构造。 // 这种不一致性可能导致设计上的混乱需要警惕。 return 0; }最佳实践当派生类继承或实现与基类类似的构造语义时最好保持explicit属性的一致性以避免令人困惑的接口。6.3 重载决议与explicit的影响explicit构造函数虽然禁止了隐式转换但它仍然参与重载决议。在某些边缘情况下这可能导致令人惊讶的结果。class MyClass { public: MyClass(int) { std::cout MyClass(int) std::endl; } explicit MyClass(double) { std::cout MyClass(double) std::endl; } }; void func(MyClass) {} int main() { func(10); // 调用 MyClass(int)隐式转换 // func(3.14); // 错误MyClass(double) 是 explicit 的不能用于隐式转换。 // 而 int 版本虽然参数不匹配double - int 需要窄化转换 // 但编译器宁愿尝试这个不完美的匹配窄化转换也不会考虑 explicit 的 double 版本。 // 实际上窄化转换在列表初始化 {} 中是被禁止的但在函数参数匹配中 // double 到 int 是标准转换而 explicit 构造函数根本不在考虑范围内。 // 所以这里会尝试用 MyClass(int)但 3.14 转 int 是窄化在某些编译器设置下会警告或错误。 func(static_castint(3.14)); // 如果强制转换则调用 MyClass(int) MyClass mc1 10; // 调用 MyClass(int) // MyClass mc2 3.14; // 错误拷贝初始化两个构造函数都不合适 // 实际上MyClass(int) 需要 double-int 窄化MyClass(double) 是 explicit。 // 编译器通常报错“歧义”或“没有可行的转换”。 MyClass mc3(3.14); // 直接初始化两个构造函数都可行。 // 重载决议double 参数精确匹配 MyClass(double)而匹配 MyClass(int) 需要标准转换。 // 因此选择更匹配的 MyClass(double)。 std::cout --- std::endl; mc3 mc1; // 与 explicit 无关使用隐式生成的拷贝赋值运算符 return 0; }这个例子说明了重载决议的复杂性。explicit构造函数在拷贝初始化中直接被排除但在直接初始化中它完全参与重载决议并且可能因为更好的匹配而被选中。6.4 现代C中的相关特性与explicit delete如果你想要完全禁止某种转换而不仅仅是要求显式可以使用 delete。例如禁止从double构造class StrictInt { int val; public: StrictInt(int x) : val(x) {} StrictInt(double) delete; // 完全禁止从 double 构造 }; // StrictInt si 5.0; // 错误使用已删除的函数consteval/constexpr与explicitexplicit可以与constexpr或consteval(C20) 一起使用。constexpr构造函数可以是explicit的这很常见因为编译期构造的对象也应当遵循明确的转换规则。概念 (Concepts, C20)概念可以更精确地控制模板构造函数的行为有时可以替代或补充explicit的使用通过约束来要求更明确的接口。6.5 代码审查清单在代码审查中对于explicit可以关注以下几点[ ] 所有单参数构造函数是否都考虑了explicit[ ] 对于表示资源、句柄、单位、不变式的类其构造函数是否标记为explicit[ ] 用户定义的operator bool是否标记为explicit[ ] 拷贝/移动构造函数是否没有被误标记为explicit[ ] 在需要隐式转换以提高便利性的地方如简单值类型explicit是否被合理地省略[ ] 派生类的构造函数是否与基类的explicit属性保持一致7. 总结与个人体会explicit关键字是C中一个“小身材大能量”的特性。它不像内存管理或模板元编程那样引人注目但它对于构建健壮、清晰、易于维护的接口至关重要。它的核心价值在于提升代码的清晰度和安全性通过将潜在的、可能引起误解的隐式操作转变为明确的、意图清晰的显式操作。从我多年的项目经验来看过度依赖隐式转换是许多难以调试的Bug的根源。一个数字被意外地当作尺寸、一个字符串被意外地当作路径、一个布尔包装类被意外地用于算术运算……这些错误往往在代码审查中容易被忽略直到运行时才暴露出来。坚持使用explicit就像是给代码加了一道编译期的安全检查迫使开发者在编写代码时就思考类型转换的合理性。在现代C实践中我的建议是偏向于保守除非有充分理由允许隐式转换否则将构造函数声明为explicit。对于转换运算符尤其是operator bool总是使用explicit。这个习惯可能会让你在开始时多敲几次键盘进行显式构造但它为项目长期的可读性和稳定性带来的收益是巨大的。最后记住explicit不是银弹它是一项需要结合具体场景使用的工具。理解它背后的哲学——让接口的用法更明确、更安全——比死记语法规则更重要。当你设计一个类时问问自己“这个转换应该是显而易见的、无代价的还是一个需要调用者明确知晓的重要操作” 答案会指引你是否使用explicit。
返回列表