本书的原著为:《Design Patterns for Embedded Systems in C ——An Embedded Software Engineering Toolkit 》,讲解的是嵌入式系统设计模式,是一本不可多得的好书。

本系列描述我对书中内容的理解。本文章描述嵌入式安全性和可靠性模式之四:智能数据模式。

在实际的嵌入式 C 语言项目中,我观察到一个最大的问题之一是:函数要想正确执行,必须满足前置条件,但是程序员通常没有明确检查这些前置条件真的满足要求。比如一个求和函数,其前置条件是输入参数必须是数字(在 excel 中很容易输入非法数字),如果没有对检查这个前置条件,计算结果将是错误的。

对前置条件检查属于“防御性编程”范畴,这是一种开发范式,强调在设计时就采取积极的 运行时防御 措施以检测潜在问题。智能数据模式 (Smart Data Pattern) 将这种范式编码应用于 标量 数据元素。

标量:在编程领域中,标量 (scalar data) 指的是由单个不可分割值组成的简单数据类型。这类数据不包含多个部分或内部结构,也就是说它不是一个集合或复合类型。标量数据通常用来表示独立的数值、字符或逻辑状态。比如在 C 语言中标量有:

  • 数值型标量:包括整数(如 int、long)、浮点数(如 float、double)
  • 字符型标量:包括单个字符 (如 char)
  • 逻辑型标量:表示真/假状态 (如 bool)

而结构体、数组等属于 复合类型,复合类型是由 标量类型 构成的。

比如 C 语言提供了基本的数据类型 int,这是一个标量数据类型。直接使用 int 型变量可能忽略对溢出等情况的检查,智能数据模式int 数据类型包装为 SmartInt 类型,关键程序不再使用 int 类型,而是用 SmartInt 类型代替。在 SmartInt 中,包装了溢出等检查,并可以在发生溢出时执行错误处理代码。

摘要

Ada 等语言被认为比 C 语言“更安全”。原因之一在于它们具有 运行时 检查机制。这些检查包括 数组索引范围检查子类型和子范围定义(比如定义 Color 子类型,不仅规定了一组颜色,而且使用非定义的颜色会引发错误),以及参数范围检查 等。实际上,尽管 C++ 按照这一标准并非“天生安全”,但由于数据结构可以与提供运行时检查的操作相结合,通过刻意设计,也能够使其变得“安全”。智能数据模式 虽更倾向于“习惯用法”而非严格的“设计模式”,但它通过创建显式执行这些检查的 C 类,解决了上述关于安全性的问题。这样,即使是在 C 语言环境下,也能通过类的设计实现类似 Ada 等语言中的运行时检查机制,从而提高程序的安全性和稳定性。

子类型和子范围定义:

  • 子类型(Subtype):在支持面向对象或泛型编程的语言中,子类型检查用于确保对象实例可以安全地当作其基类型或接口使用。例如,在 C# 中,如果一个类继承自另一个类或者实现了接口,那么它可以被视为基类型或接口的实例。在方法调用或赋值时,编译器和运行时都会确保类型兼容性,防止类型不匹配的错误。
  • 子范围(Subrange):在某些静态类型或有限类型的编程环境中,子范围检查主要涉及整数或其他有限类型的值域。例如,Ada 语言支持子范围类型,允许定义一个整数范围的子集作为一个新的类型,当尝试赋值或比较时,编译器和运行时会确保值在这个子范围内。此外,在一些数字信号处理或硬件描述语言中,子范围的概念也常用于限定变量的有效值区间。

问题

在 C 语言中,最常见的检查数据范围错误的习惯做法是:成功时返回 0,失败时返回 -1,并将错误代码更新到变量 errno 中。

问题在于,我们经常忽略函数的返回值,这意味着忽略了可能的出错信息,所以导致了难以调试的系统。有些人在开发阶段检测这些错误,而在最终发布版本时却移除了这些检查。我坚持认为,当你在飞行的飞机上时,你才真正想知道飞机上的高度测定程序是否输出了正确的值。

本模式所要解决的问题是构建能够自我检查的函数和数据类型,并提供一种难以忽视的错误检测机制手段。这样一来,即使在最终发布的软件中,也能确保关键模块如飞机高度测定等功能始终进行有效的错误检测,从而提高系统的稳定性和安全性。

模式结构

模式结构如下图所示:

模式详情

ErrorCodeType

ErrorCodeType 是一组可能的错误代码枚举类型。虽然通常可以用整型数值来实现这一点,但使用枚举(enums)可以更清晰地表达这些错误代码的目的和用途。这有助于提高代码的可读性和维护性,同时也减少了因误解错误代码含义而导致的潜在问题。而且编译器也会检查枚举变量的值是否合法。

错误管理者

错误管理者 类负责处理在 子范围 类型 (也就是 ErrorCodeType,这个枚举类型定义了一个错误集,它是整数的一个子范围) 中识别到的错误;检查到错误后,采取的行动取决于系统应用要求。

服务类

使用 SmartDataType 的元素。

SmartDataType

数据结构的一大缺点是,虽然它们都有各自的前提条件和正确使用的规则,但却不具备内在的行为机制去强制执行这些条件和规则。而这个类(SmartDataType)的作用就是将数据(类型为PrimitiveType)与其确保数据保持在合理范围内的函数绑定在一起。这意味着该类实例在操作数据时,会自动执行相关的检查和限制,从而确保数据始终满足预设的条件和规则,增强了数据结构的健壮性和安全性。

此类提供了多种功能。

  • Init() 函数接受五个参数:
    • me:指针,指向实例数据,注意模式示意图中的函数一般不体现这个指针。
    • val:初始设置的值
    • low:有效数据子范围的低值
    • high:有效数据子范围的高值
    • ErrorManager:处理错误的函数地址。
      如果有效数据子范围与数据类型的整个范围相同,比如 short 类型,如果有效子范围用到了 short 类型所能表示的全部数值(一般是 -32768~32767),则低值和高值使用该类型的最小值和最大值,对于 short 类型,可以使用 中声明的 SHRT_MINSHRT_MAX
  • SetValue()getValue():设置和读取数据
  • cmp() :智能数据类型值的比较函数,该函数会比较两个智能数据类型值的边界值 (边界值就是指有效数据子范围的低值和高值)。
    关于原始数据类型和智能数据类型:比如原始数据类型为 int ,增加检查功能的智能数据类型为 SmartInt
  • pCmp():智能数据类型值的比较函数,只比较原始数据类型值,不比较智能数据类型值的边界值。
  • setPrimitive()getPrimitive():设置和获取智能数据类型值的原始数据类型值。
  • setLowBoundary()getLowBoundary()setHighBoundary()getHighBoundary():设置和获取低边界值和高边界值。

效果

使用 智能数据类型 的一个缺点是执行这些操作时会有性能开销。然而,其优点在于数据具有自我保护功能,当设置数据时会自动进行检查。不过,程序员如果愿意的话,仍然可以选择避开这些函数,直接访问值,但这将会违背使用智能数据类型的目的。智能数据类型旨在通过内建的验证机制确保数据始终处于有效范围,减少因数据错误引发的问题。因此,应当尽量遵循设计规范,充分利用智能数据类型提供的错误检测和防护功能。

实现策略

关键的实现策略是创建一个带有预定义操作的结构体,然后仅通过这些操作来访问值。当处理带有单位的数字,例如货币(如欧元和美元)、质量(如磅、千克)和距离(如英寸、英尺、米)时,可以将此模式扩展为包含一个表示单位的枚举类型。这种扩展在处理需要转换的不同来源数据时特别有用。可以将转换功能内置到类中,以便既能按指定单位设置值,也能按指定单位获取值。这样,无论是存储还是操作带单位的数据,都能确保其一致性与准确性。

相关模式

构建 智能类型 的思路在 智能指针模式 中得到了延伸,智能指针是一种扩展了普通指针功能的数据类型。智能指针模式通过包装原始指针,并在其析构时自动释放所指向的对象,从而解决了内存管理中的资源泄漏问题。智能指针不仅跟踪对象的所有权,还能在必要时执行自定义清理操作。类似于智能数据类型,智能指针通过对原始指针的管理和控制,增强了代码的健壮性和安全性,降低了程序员手动管理内存时产生的错误可能性。

C++11 标准首次引入了智能指针。

实例

见原书。


读后有收获,资助博主养娃 – 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)