本书的原著为:《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_MIN
和SHRT_MAX
。
SetValue()
和getValue()
:设置和读取数据cmp()
:智能数据类型值的比较函数,该函数会比较两个智能数据类型值的边界值 (边界值就是指有效数据子范围的低值和高值)。
关于原始数据类型和智能数据类型:比如原始数据类型为int
,增加检查功能的智能数据类型为SmartInt
。pCmp()
:智能数据类型值的比较函数,只比较原始数据类型值,不比较智能数据类型值的边界值。setPrimitive()
和getPrimitive()
:设置和获取智能数据类型值的原始数据类型值。setLowBoundary()
、getLowBoundary()
、setHighBoundary()
、getHighBoundary()
:设置和获取低边界值和高边界值。
效果
使用 智能数据类型
的一个缺点是执行这些操作时会有性能开销。然而,其优点在于数据具有自我保护功能,当设置数据时会自动进行检查。不过,程序员如果愿意的话,仍然可以选择避开这些函数,直接访问值,但这将会违背使用智能数据类型的目的。智能数据类型旨在通过内建的验证机制确保数据始终处于有效范围,减少因数据错误引发的问题。因此,应当尽量遵循设计规范,充分利用智能数据类型提供的错误检测和防护功能。
实现策略
关键的实现策略是创建一个带有预定义操作的结构体,然后仅通过这些操作来访问值。当处理带有单位的数字,例如货币(如欧元和美元)、质量(如磅、千克)和距离(如英寸、英尺、米)时,可以将此模式扩展为包含一个表示单位的枚举类型。这种扩展在处理需要转换的不同来源数据时特别有用。可以将转换功能内置到类中,以便既能按指定单位设置值,也能按指定单位获取值。这样,无论是存储还是操作带单位的数据,都能确保其一致性与准确性。
相关模式
构建 智能类型
的思路在 智能指针模式
中得到了延伸,智能指针是一种扩展了普通指针功能的数据类型。智能指针模式通过包装原始指针,并在其析构时自动释放所指向的对象,从而解决了内存管理中的资源泄漏问题。智能指针不仅跟踪对象的所有权,还能在必要时执行自定义清理操作。类似于智能数据类型,智能指针通过对原始指针的管理和控制,增强了代码的健壮性和安全性,降低了程序员手动管理内存时产生的错误可能性。
C++11 标准首次引入了智能指针。
实例
见原书。
读后有收获,资助博主养娃 – 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)