最近正在逐步将之前的MFEA面向过程式编程风格改为面向对象式,便于以后的封装,或者转Python、C++这些热门语言。
翻阅一些教材,学习了Matlab面向对象编程的一些基础知识,记录于此,防止遗忘,如果能帮助到大家,那就更好了。
我使用的是Obsidian笔记软件,很好用,后面会出相关的介绍。为了便于读者阅读,已经导出了PDF,感兴趣的可以在后台回复:
MATLABOOP基础
,即可自动获取。
参考教材:徐潇. MATLAB 面向对象编程: 从入门到设计模式. 北京航空航天大学出版社, 2017.
新建Point2D.m文件,类的名字应与文件名一致
classdef Point2D < handle
properties % 属性 block 开始
%...
end
methods % 方法 block 开始
%...
end % 方法 block结束
end
classdef Point2D < handle
properties
x
y
end
methods
function obj = Point2D(x0,y0) % Point2D类的构造函数
% 可以用来赋予初值(初始化)
obj.x = x0;
obj.y = y0;
end
function normalize(obj) % Point2D类的normalize方法
r = sqrt(obj.x^2 + obj.y^2);
obj.x = obj.x/r;
obj.y = obj.y/r;
end
end
end
注意:
p = Point2D(1,2)
, 此时 p 就是一个对象,可以调用 Point2D 的属性: p.x
,将返回 1。也可以更改其值,比如:p.x = 3
。classdef Point2D < handle
properties
x = 1.0
y = 2.0
end
end
classdef Point2D < handle
properties(Constant)
x = 1.0
y = 2.0
end
end
若成员方法(非构造函数、折构函数、Static 方法)比较短,可以直接写进类里面,若成员函数较长,可写为单独的文件,如:【注】需要创建一个 @Point2D
的文件夹,将类的定义和成员都放在该文件夹中,然后同级创建一个主函数,如 main.m
用于调用该类的构造函数。
点调用,即 Point.normalize()
,要求成员函数的形参中包括 obj
。
p = Point2D(1, 2); % 声明一个Point2D对象
p.normalize(); % 调用成员方法normalize
p.x % 访问成员变量x
p.y % 访问成员变量y
在 MATLAB 的面向对象编程(OOP)中,disp()
函数常用于显示对象的信息。可以通过定义对象的 disp
方法来控制对象在调用 disp()
时如何展示其信息。
classdef FEMElement < handle
properties
ElementID % 单元编号
ElementType % 单元类型 (例如 'C3D4', 'C3D10')
NodeIDs % 节点编号数组
Material % 材料属性 (例如弹性模量和泊松比)
Properties % 其他相关属性,如厚度
end
methods
% 构造函数
function obj = FEMElement(elementID, elementType, nodeIDs, material, properties)
if nargin > 0
obj.ElementID = elementID;
obj.ElementType = elementType;
obj.NodeIDs = nodeIDs;
obj.Material = material;
obj.Properties = properties;
end
end
% 自定义 disp 方法
function disp(obj)
% 打印单元基本信息
fprintf('FEM Element Information:\n');
fprintf('Element ID: %d\n', obj.ElementID);
fprintf('Element Type: %s\n', obj.ElementType);
% 打印节点编号
fprintf('Node IDs: ');
fprintf('%d ', obj.NodeIDs);
fprintf('\n');
% 打印材料属性
fprintf('Material Properties:\n');
fprintf(' Young''s Modulus: %.2f MPa\n', obj.Material.E);
fprintf(' Poisson''s Ratio: %.2f\n', obj.Material.nu);
% 打印其他属性
fprintf('Additional Properties:\n');
fprintf(' Thickness: %.2f mm\n', obj.Properties.Thickness);
% 添加分隔符以提高可读性
fprintf('-----------------------------\n');
end
end
end
示例运行:
materialProps.E = 210000; % MPa
materialProps.nu = 0.3;
additionalProps.Thickness = 10; % mm
nodeIDs = [1, 2, 3, 4];
elem = FEMElement(101, 'C3D4', nodeIDs, materialProps, additionalProps);
disp(elem);
FEM Element Information:
Element ID: 101
Element Type: C3D4
Node IDs: 1 2 3 4
Material Properties:
Young's Modulus: 210000.00 MPa
Poisson's Ratio: 0.30
Additional Properties:
Thickness: 10.00 mm
-----------------------------
nargin
,判断传入参数的个数。
classdef Point2D < handle
properties
x
y
end
methods
function obj = Point2D(x0,y0) % Point2D类的构造函数
if nargin == 0
obj.x = 0;
obj.y = 0;
elseif nargin == 2
obj.x = x0;
obj.y = y0;
end
end
end
end
打一个比方,我们已经编写了一个 2D 的类,现在想再编写一个 3D 的类,直接的做法就是再重新写一个类,就像下图:
可以看到,两个类的构造极其相似,无非就是 3D 的类多了一个属性,坐标增加了 z。这时候可以用到类的继承,即:
classdef Point3D < Point2D
properties
z
end
methods
function obj = Point3D(x,y,z)
obj = obj@Point2D(x,y);
obj.z = z;
end
function disp(obj)
disp@Point2D(obj);
fprintf('Point3D: (%d, %d, %d)\n', obj.x, obj.y, obj.z);
end
end
end
Point3D
类中只需要添加额外的属性即可;Point3D < Point2D
表示 Point3D(子类)继承于 Point2D(父类)obj = obj@Point2D(x,y)
表示调用父类的构造函数,返回一个叫 obj 的对象,赋值给子类构造函数的对象superMethod@SuperClass(obj,otherArugments)
表示调用父类(SuperClass)方法(superMethod)的对象(obj)以及参数 (otherArugments)。如 disp@Point2D(obj)
比如说 Head 类是由 Nose、Eye、Mouth、Ear 类组合而成,而不是派生而成。那就不需要多重继承,糟糕的继承方式:
classdef Head < Nose & Eye & Mouth & Ear
%...
end
应该采用组合的关系:
classdef Head < handle
properties
eye
nose
mouth
ear
end
methods
function obj = Head()
obj.eye = Eye();
obj.nose = Nose();
obj.mouth = Mouth();
obj.ear = Ear();
end
end
end
组合关系要求 Head 对象一定要在内部拥有 Nose、Eye、Mouth、Ear 对象,可以通过 Head 对象的构造函数来保证,以上代码中,Head 对象被创建时,Nose、Eye、Mouth、Ear 对象也同时被创建。
set 方法为对象的赋值提供了一个中间层。以下面代码为例,在类的外部,任何对属性 Name
的赋值都要经过 set. Name
的中间层。set
方法通常用来检测赋值是否符合要求。
classdef Person
% Person类用于存储和管理个人信息
properties
Name % 人名
end
methods
% 设置人名
function obj = set.Name(obj, value)
if ~ischar(value) || isempty(value)
error('Name must be a non-empty string.');
end
obj.Name = value;
end
end
methods
% 构造函数
function obj = Person(name)
if nargin > 0
obj.Name = name; % 使用 set 方法进行设置
end
end
end
end
运行:
% 创建一个Person对象,使用有效的名字
person1 = Person('Alice');
% 获取并显示人名
disp('Person 1 Name:');
disp(person1.Name);
% 尝试设置一个无效的名字(应该引发错误)
try
person1.Name = ''; % 这将触发 set 方法中的错误
catch ME
disp('Error occurred:');
disp(ME.message);
end
% 尝试设置一个有效的名字
person1.Name = 'Bob';
% 获取并显示更新后的人名
disp('Person 1 Updated Name:');
disp(person1.Name);
结果:
Person 1 Name:
Alice
Error occurred:
Name must be a non-empty string.
Person 1 Updated Name:
Bob
代码解释:
Person
类:Name
属性用于存储个人的名字set.Name
方法确保 Name
属性只能设置为非空字符串。如果尝试设置为空字符串或其他无效输入,会引发错误Person
对象 person1
,并设置一个有效的名字 'Alice'person1.Name
来获取并显示名字Name
设置为空字符串,set
方法 会引发一个错误,示例中捕获并显示该错误消息Name
更新为有效的名字 'Bob',并显示更新后的名字get 方法提供对成员查询操作的一个中间层,对该属性的查询都要经过这个中间方法,与 set 方法类似。
接下来以有限元网格模型为例,阐述 set 方法和 get 方法。
classdef FiniteElementModel
properties
Nodes
Elements
end
methods
% 设置节点位置
function obj = set.Nodes(obj, value)
if ~isnumeric(value) || size(value, 2) ~= 3
error('Nodes must be a numeric matrix with 3 columns.');
end
obj.Nodes = value;
end
% 获取节点位置
function value = get.Nodes(obj)
value = obj.Nodes;
end
% 获取节点的平均位置
function avgPosition = get.AverageNodePosition(obj)
if isempty(obj.Nodes)
avgPosition = [];
return;
end
avgPosition = mean(obj.Nodes, 1); % 计算节点位置的平均值
end
% 设置元素连接信息
function obj = set.Elements(obj, value)
if ~isnumeric(value) || size(value, 2) < 3
error('Elements must be a numeric matrix with at least 3 columns.');
end
obj.Elements = value;
end
% 获取元素连接信息
function value = get.Elements(obj)
value = obj.Elements;
end
end
methods
% 构造函数
function obj = FiniteElementModel(nodes, elements)
if nargin > 0
obj.Nodes = nodes;
obj.Elements = elements;
end
end
end
end
运行:
% 定义节点位置 (每一行代表一个节点的坐标)
nodes = [
0 0 0;
1 0 0;
0 1 0;
0 0 1
];
% 定义元素连接信息 (每一行代表一个元素的节点索引)
elements = [
1 2 3;
1 2 4;
1 3 4;
2 3 4
];
% 创建FiniteElementModel对象
model = FiniteElementModel(nodes, elements);
% 获取并显示节点位置
disp('Nodes:');
disp(model.Nodes);
% 获取并显示元素连接信息
disp('Elements:');
disp(model.Elements);
% 获取并显示节点的平均位置
disp('Average Node Position:');
avgPosition = model.AverageNodePosition();
disp(avgPosition);
结果:
Nodes:
0 0 0
1 0 0
0 1 0
0 0 1
Elements:
1 2 3
1 2 4
1 3 4
2 3 4
Average Node Position:
0.2500 0.2500 0.2500
代码解释:
nodes
是一个 4x3 的矩阵,表示四个节点的三维坐标。elements
是一个 4x3 的矩阵,表示四个三角形元素的节点索引(假设节点是从1开始索引)。FiniteElementModel
对象,并初始化 Nodes
和 Elements
属性。model.Nodes
和model.Elements
来访问属性值。model.AverageNodePosition()
来计算并获取节点的平均位置。disp
函数输出节点位置、元素连接信息和节点的平均位置到命令窗口。通过以上案例可以大致明白这两个方法的意义,这里不禁要问,对于这两种方法,可以直接在类中定义一个功能函数,为什么要单独用到 set 和 get 呢?
set
和get
方法提供了一个统一的接口来访问和修改属性。这有助于保护数据不被随意修改。即使属性是公开的,使用set
方法可以控制对属性的修改,并进行必要的验证和处理。set
方法设置属性值时,可以确保所有输入都经过验证,从而保持对象的内部状态一致。set
方法允许你在设置属性值时进行验证。例如,你可以检查值是否在允许的范围内或是否符合特定的格式。这样可以防止无效数据进入对象。get
方法可以用于计算或转换属性值。在对象内部,属性可能以不同的格式存储,但get
方法可以将其转换为更合适的格式或计算派生数据。get
方法可以动态计算属性值,而不是直接存储。比如,可以在 get
方法中计算元素的几何中心,避免存储重复的数据。set
和get
方法,而不需要修改所有访问这些属性的代码。set
和get
方法可以创建一个一致的接口,使得对象的属性访问和修改行为规范化。这样,用户在与对象交互时会有一致的体验。set
和get
方法,可以将属性的具体实现细节封装起来,只暴露必要的操作给用户。从面向过程到面向对象的一个最显著区别的就是,在面向对象时,把数据和函数组合在一起形成了类,数据变成了类的属性,函数变成了类的成员方法。
对于数据来说,一些计算过程中的内部变量,外部程序并不需要知道,所以需要对访问的权限加以控制。Matlab 提供了关键词:Access = private,protected,public
来声明哪些属性和方法是私有的、可以公开访问的、受保护的。
默认为公有属性,不但可以在类的定义中,该类的成员方法以及该类的子类的成员方法都可以访问这个成员变量,在类之外的函数或者脚本也可以访问这个成员变量。
classdef MyClass
properties
PublicProperty
end
end
properties (Access = private)
classdef MyClass
properties (Access = private)
PrivateProperty
end
end
properties (Access = protected)
classdef MyClass
properties (Access = protected)
ProtectedProperty
end
end
classdef MyClass
properties (SetAccess = private, GetAcess = public)
var
end
end
SetAccess = private, GetAcess = public
等同于 SetAccess = private
SetAccess = ?class
:允许特定类的对象(指定类)设置某个属性的值。这对于在多个类之间共享数据并允许特定类访问属性是非常有用的。
假设我们有两个类:FiniteElementModel
和 MeshOptimizer
。我们希望 MeshOptimizer
类能够设置 FiniteElementModel
类的某些私有属性,但其他类不能直接设置这些属性。
classdef FiniteElementModel
properties (Access = public)
NodeCoordinates
ElementConnectivity
end
properties (Access = private, SetAccess = ?MeshOptimizer)
MeshData % 只有 MeshOptimizer 类可以设置此属性
end
methods
function obj = FiniteElementModel(nodeCoords, elemConn)
obj.NodeCoordinates = nodeCoords;
obj.ElementConnectivity = elemConn;
obj.MeshData = obj.initializeMeshData(); % 初始设置
end
function displayModel(obj)
disp('Node Coordinates:');
disp(obj.NodeCoordinates);
disp('Element Connectivity:');
disp(obj.ElementConnectivity);
disp('Mesh Data:');
disp(obj.MeshData);
end
end
methods (Access = private)
function data = initializeMeshData(obj)
% 初始化 MeshData
data = struct('Nodes', obj.NodeCoordinates, 'Elements', obj.ElementConnectivity);
end
end
end
classdef MeshOptimizer
methods
function optimize(obj, femModel)
% 优化网格并设置 MeshData 属性
% 需要对 FiniteElementModel 的 MeshData 属性进行修改
newData = struct('Nodes', femModel.NodeCoordinates, 'Elements', femModel.ElementConnectivity, 'Optimized', true);
femModel.MeshData = newData; % MeshOptimizer 可以设置 MeshData
end
end
end
解释:
FiniteElementModel
类:NodeCoordinates
和 ElementConnectivity
是公有属性,任何地方都可以读取和设置这些属性。MeshData
是私有属性,SetAccess
被设置为 ?MeshOptimizer
,这意味着只有 MeshOptimizer
类可以修改 MeshData
属性的值。initializeMeshData
是一个私有方法,用于初始化 MeshData
属性。只有 FiniteElementModel
类的内部方法可以调用这个方法。MeshOptimizer
类:optimize
方法用于优化网格并设置 FiniteElementModel
对象的 MeshData
属性。由于 MeshOptimizer
类被允许设置 MeshData
,这个操作是合法的。运行:
% 创建 FiniteElementModel 对象
fem = FiniteElementModel([0, 1; 1, 0], [1, 2]);
% 创建 MeshOptimizer 对象
optimizer = MeshOptimizer();
% 优化网格并设置 MeshData
optimizer.optimize(fem);
% 显示模型
fem.displayModel();
结果:
Node Coordinates:
0 1
1 0
Element Connectivity:
1 2
Mesh Data:
struct with fields:
Nodes: [0 1; 1 0]
Elements: [1 2]
Optimized: 1
默认为公有方法,公有方法可以被类的其他成员方法、子类和外部代码调用。
classdef MyClass
methods
% methods (Access = public)
function publicMethod(obj)
% 代码
end
end
end
methods (Access = private)
classdef MyClass
methods (Access = private)
function privateMethod(obj)
% 代码
end
end
end
methods (Access = protected)
classdef MyClass
methods (Access = protected)
function protectedMethod(obj)
% 代码
end
end
end