技术文章

MathWorks 见解

MATLAB 中面向对象的编程简介


作者:

  • Stuart McGarrity, MathWorks
  • Adam Sifounakis, MathWorks

阅读时间:9 分钟

标签:

小结

  • MATLAB 中的面向对象编程 (OOP) 将相关的数据和功能组织到类中,从而创建可重用的独立组件。
  • 对象通过属性和方法封装状态和行为,从而能够通过定义明确的接口进行交互。
  • OOP 通过对相关逻辑进行分组来改进代码结构,降低复杂性,并支持重用和扩展。
  • 一个传感器阵列示例演示了如何使用类对真实系统进行建模,以及如何组织信号分析等工作流。

创建软件应用时,务必将软件的各个构建模块组织成相关的组。例如,一个自定义数值求解器可能需要多个配置参数和例程才能完成全部计算。借助面向对象编程 (OOP),您可以将求解器的配置参数(属性)及其函数(方法)组合到单个定义(即类)中。用户正确执行此求解器所需的一切都在该类中定义。

对象是类的实例。程序执行时,会根据类定义创建对象,对象的行为方式也由类定义。对象的属性表示其状态,其方法表示用户可以执行的所有操作。通过这种方式,代码作者可以轻松地将软件系统的所有相关数据和函数组合在一起,用户也可以轻松找到并使用代码作者开发的所有功能。

以下示例使用面向对象编程构建一个应用,用于分析来自传感器阵列的传感器数据。 

本文中使用的代码可供下载。

应用示例:分析传感器阵列数据

传感器阵列(图 1)是一组传感器,通常排成一条直线,用于对空气、水或地面等介质进行采样,以应用于雷达、声呐或蜂窝通信。通过在空间中的多个点采集时间样本,您可以从被采样的介质中提取更多信息。

图 1. 传感器阵列检测两个角度未知的远距离电磁源。

图 1. 传感器阵列检测两个角度未知的远距离电磁源。

我们的应用使用传感器阵列来确定多个远距离电磁源(例如无线电信标和雷达发射机)的波达方向 (DOA)。在此场景中,我们将尝试估计两个源相对于传感器阵列指向方向的角度 θ1 和 θ2。

梳理数据项和操作

对于此应用,我们需要存储和表示以下数据:

  • 传感器数量和样本数量
  • 采样的传感器数据
  • 传感器采样率
  • 传感器间距
  • 远距离源的波长
  • 波速
  • 传感器数据集名称或描述

我们将使用一种简单的基于快速傅里叶变换 (FFT) 的技术来估计源的 DOA。这项技术可以分解为多个部分,并以一组操作的形式实现。我们将实现少量实用操作,以帮助简化开发工作。例如,我们必须:

  • 基于合成数据或实时采集的数据创建数据集
  • 检查和修改数据集的值和参数
  • 绘制样本数据,以帮助解释和验证
  • 计算并绘制数据集的功率谱(采用简单的 FFT 幅值平方法)
  • 找出功率谱的峰值,以估计源的 DOA

确定了需要表示的数据和需要执行的操作后,我们就可以用类属性表示数据,用类方法表示操作。

用类属性表示数据

首先,我们定义一个类来描述传感器阵列。这个初始表示仅包含数据项,并将它们表示为类属性。

在 MATLAB 中,您可以使用类定义文件来定义类,该文件以 classdef 关键字开头,以 end 关键字结束。在类定义块中,其他关键字块将描述类的不同方面,例如类属性和类方法。图 2 所示的定义文件描述了一个类 sads(传感器阵列数据集),我们需要表示的所有数据项都列在一个属性块中。

图 2. 带有属性的类定义文件 sads.m。

图 2. 带有属性的类定义文件 sads.m。

创建对象和访问属性

要创建我们所定义的类的对象(即实例),可以使用以下语句

>> s = sads;

要设置属性的值,我们可以像指定结构体字段一样指定属性名称,如下所示

>> s.NumSensors = 16;

通过键入对象名称,我们可以显示该对象,查看所有可用属性及其当前值。

>> s
s = 
  sads with properties:

      Wavelength: []
               c: 300000000
      NumSensors: 16
      NumSamples: []
            Data: []
         Spacing: []
      SampleRate: []
            Name: []

除 NumSensors 和 c 外,其他所有属性仍为空。现在,可以使用 class 函数、isa 函数和 whos 命令将该数据集识别为 sads 对象,而这是结构体无法做到的。

>> class(s)
ans =
 'sads'

对于编写代码来处理该数据集的用户来说,能够识别变量的类非常重要,因为这样他们就能确定可以访问哪些数据项,以及可以合法执行哪些操作。

错误检查

如果您使用结构体来表示数据,只需指定新的字段名称并为其赋值,就可以随时添加新字段。当您试验算法和构建算法原型时,这项功能特别方便。但是,如果您拼错了字段名称,系统会在不提示的情况下添加一个新字段,这可能会在之后导致难以诊断的错误。

与结构体不同,您无法仅通过指定新属性名称并为其赋值来向对象动态添加新属性。如果您拼错了对象属性名称,MATLAB 会立即报错。当访问对象的用户不如作者熟悉该对象时,这一额外的错误检查层级就很有用,而这种情况在开发大型应用时很常见。

控制数据访问

类可以让您在很大程度上控制属性访问。例如,您可以禁止修改某个属性、隐藏某个属性,或者让属性动态计算。您可以通过在类定义文件中指定属性特性来控制对属性的访问。我们对图 2 中的类定义文件进行扩展,将当前属性列表划分为多个属性块,每个块都有唯一的属性特性:GetAccess、Constant 和 Dependent(图 3)。

图 3. 带有属性特性的类定义文件 sads.m。

图 3. 带有属性特性的类定义文件 sads.m。

您可以通过设置 Constant 特性来禁止修改属性。在我们的示例中,我们将光速属性 c 设置为常量。由于常量属性不会改变,因此只需引用类名即可访问它们。

>> sads.c
ans =
 300000000

将 SetAccess 特性设置为 private,即可将属性设为只读。将 GetAccess 特性设置为 private,可以使属性仅对操作该属性的方法可见,我们将对 Wavelength 属性执行此操作。

您可以随意更改私有属性的名称或特征,而不会影响对象的用户。这种定义软件的“黑盒”方法称为封装,它可以防止对象的用户依赖于可能发生变化、进而破坏其代码的实现细节或特征。

通过设置 Dependent 特性,您可以指定属性仅在被请求时才计算。

然后,您需要指定一个 get 方法,该方法会在访问属性时自动调用。有关如何指定类方法的详细信息,请参阅本文的“使用 get 和 set 方法访问属性”一节。在我们的应用中,我们将 NumSensors 和 NumSamples 属性设置为依赖属性。

使用类方法实现操作

方法(即可以对对象执行的操作)以函数列表的形式在方法块中指定。一个类可以包含多种类型的方法,每种方法用途不同,指定方式也各不相同。下一节将介绍其中几种类型的方法。

我们将在 sads 定义文件中添加一个方法块,并在此块中添加每个新方法(图 4)。

图 4. 在 MATLAB 编辑器中显示的带有方法的类定义文件 sads.m。为了便于查看,我们使用代码折叠功能隐藏了大部分代码。

图 4. 在 MATLAB 编辑器中显示的带有方法的类定义文件 sads.m。为了便于查看,我们使用代码折叠功能隐藏了大部分代码。

指定构造函数方法

在我们的示例中,我们将指定一个构造函数方法,让用户可以提供用于创建对象的参数。构造函数方法通常会执行数据初始化和验证。现在,可以使用以下语句创建对象

>> s = sads(Data, Wavelength, SampleRate, Spacing, Name); 

实现特定于应用的方法

我们将添加几个方法,以实现要对数据集执行的特定于应用的操作。大多数方法都将对象作为输入参数(例如 obj),并通过引用此变量来访问对象属性(例如 obj.NumSamples),如以下方法所示:

function mag = magfft(obj, zpt)
mag = zeros(obj.NumSamples, zpt); 
...
end

虽然需要额外的语法,但通过对象变量引用属性有助于将它们与局部函数变量(例如上面的 mag)区分开来。

调用方法

方法的调用方式与函数相同,对象作为其中一个参数传入。我们可以通过调用类的 doa 方法来估计源的 DOA 角度。

>> angles = doa(s)
angles =
 -10.1642 18.9953

这两个 DOA 角度近似于图 1 中所示源的真实位置,即 -10° 和 20°。

使用 get 和 set 方法访问属性

通过指定关联的 set 和 get 方法,您可以验证属性或实现依赖属性。以下是 NumSensors 属性的 get 方法。

function NumSensors = get.NumSensors(obj)
NumSensors = size(obj.Data, 2);
end

访问属性时会自动调用 get 和 set 方法,例如

>> N = s.NumSensors;

使用重载为现有 MATLAB 函数指定方法

通过重载,您可以在方法列表中提供与现有 MATLAB 函数同名的函数,从而重新定义这些函数,使其可以作用于您的对象。在我们的应用中,我们将包含一个重载的 plot 方法,以提供一个许多 MATLAB 用户都熟悉的函数来可视化数据集(图 5)。

>> plot(s)
图 5. 专门针对传感器阵列数据集的重载 plot 方法。

图 5. 专门针对传感器阵列数据集的重载 plot 方法。

这个自定义的 plot 方法以最适合此数据集的方式表示信息,并使用所有可用信息对其进行注释。它仅在已为其定义该方法的对象上执行,这比调整路径中目录的顺序要稳健得多。

如果您希望类具有专门的行为,还可以通过使用具有特殊名称的方法来重载基本运算符,甚至重载索引。

进一步开发应用

我们在此示例中创建的类表示传感器阵列数据集,让我们能够轻松地对数据执行复杂的专门分析,包括主要的测向操作。我们可以使用此类来快速评估基于 FFT 的技术在不同场景中的性能。

我们可以使用其他面向对象技术来扩展应用。例如,我们可以执行以下操作:

  • 通过继承定义现有类的子类(重用较宽泛类别的定义来定义更具体的子类别)
  • 指定静态方法,从而为整个类定义操作
  • 使用具有引用行为的句柄类,从而创建链表等数据结构,或者无需复制即可处理大型数据集
  • 定义事件和侦听器,从而监控对象属性或操作

这些技术使我们能够进一步定义应用中的关系和行为,从而增强管理复杂性的能力。

由于应用是使用面向对象技术构建的,它现在已足够稳健,可供他人使用和维护,并能与整个组织中的其他应用集成。

2020 年发布


关于作者

Adam Sifounakis

Adam Sifounakis is a MATLAB language product manager focused on software developer workflows. With over 20 years of MATLAB experience, 10 years at MathWorks, and over 5 years working directly with aerospace and defense customers, Adam helps deliver the capabilities customers need to develop powerful MATLAB applications.

继续探索