首页/文章/ 详情

Matlab面向对象编程基础

2月前浏览2067


最近正在逐步将之前的MFEA面向过程式编程风格改为面向对象式,便于以后的封装,或者转Python、C++这些热门语言。

翻阅一些教材,学习了Matlab面向对象编程的一些基础知识,记录于此,防止遗忘,如果能帮助到大家,那就更好了。

我使用的是Obsidian笔记软件,很好用,后面会出相关的介绍。为了便于读者阅读,已经导出了PDF,感兴趣的可以在后台回复:

MATLABOOP基础,即可自动获取。

 

参考教材:徐潇. MATLAB 面向对象编程: 从入门到设计模式. 北京航空航天大学出版社, 2017.


定义类 Class

模板

新建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

注意:

  • 定义了两个方法:第一个是构造方法(Contructor), 与 class 同名,创建并返回 Point2D 的对象,第二个是“普通方法”,用于实现某一特定功能。
  • normalize 方法形参是 obj,表示将对象当做参数传入 normalize 中。
  • 调用方式:p = Point2D(1,2), 此时 p 就是一个对象,可以调用 Point2D 的属性: p.x,将返回 1。也可以更改其值,比如:p.x = 3

初始化

  • 在构造方法中进行赋值,进行初始化
  • 在 properties block 中进行初始化:
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(12); % 声明一个Point2D对象
p.normalize(); % 调用成员方法normalize
p.x % 访问成员变量x
p.y % 访问成员变量y

disp 定制化显示类的信息

在 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 = [1234];
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
  1. Point3D 类中只需要添加额外的属性即可;
  2. “<”表示继承关系,Point3D < Point2D 表示 Point3D(子类)继承于 Point2D(父类)
  3. obj = obj@Point2D(x,y) 表示调用父类的构造函数,返回一个叫 obj 的对象,赋值给子类构造函数的对象
  4. 在子类中调用与父类同名的方法: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 和 get 方法

set

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

代码解释:

  1. Person
    1. Name 属性用于存储个人的名字
    2. set.Name 方法确保 Name 属性只能设置为非空字符串。如果尝试设置为空字符串或其他无效输入,会引发错误
  2. 运行脚本
    1. 创建一个 Person 对象 person1,并设置一个有效的名字 'Alice'
    2. 使用 person1.Name 来获取并显示名字
    3. 尝试将 Name 设置为空字符串,set 方法 会引发一个错误,示例中捕获并显示该错误消息
    4. 最后,将 Name 更新为有效的名字 'Bob',并显示更新后的名字

get

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 对象,并初始化 NodesElements 属性。
  • 获取属性
    • 使用model.Nodesmodel.Elements来访问属性值。
    • 使用 model.AverageNodePosition() 来计算并获取节点的平均位置。
  • 显示结果
    • 通过 disp 函数输出节点位置、元素连接信息和节点的平均位置到命令窗口。

通过以上案例可以大致明白这两个方法的意义,这里不禁要问,对于这两种方法,可以直接在类中定义一个功能函数,为什么要单独用到 set 和 get 呢?

  1. 封装数据:对于大型程序来说,setget方法提供了一个统一的接口来访问和修改属性。这有助于保护数据不被随意修改。即使属性是公开的,使用set方法可以控制对属性的修改,并进行必要的验证和处理。
  2. 一致性:通过 set 方法设置属性值时,可以确保所有输入都经过验证,从而保持对象的内部状态一致。
  3. 验证输入set方法允许你在设置属性值时进行验证。例如,你可以检查值是否在允许的范围内或是否符合特定的格式。这样可以防止无效数据进入对象。
  4. 计算或转换get方法可以用于计算或转换属性值。在对象内部,属性可能以不同的格式存储,但get方法可以将其转换为更合适的格式或计算派生数据。
  5. 灵活性get 方法可以动态计算属性值,而不是直接存储。比如,可以在 get 方法中计算元素的几何中心,避免存储重复的数据。
  6. 实现变更:如果你决定改变属性的存储方式或计算方式,只需要修改setget方法,而不需要修改所有访问这些属性的代码。
  7. 一致的接口:使用setget方法可以创建一个一致的接口,使得对象的属性访问和修改行为规范化。这样,用户在与对象交互时会有一致的体验。
  8. 封装细节:通过setget方法,可以将属性的具体实现细节封装起来,只暴露必要的操作给用户。
  9. 但是,也不要随便的使用这两种方法,使用 set 和 get 方法的调用时间要大于直接访问属性的时间,所以,应该在仅需要时定义属性的 set 和 get 方法,避免出现没有任何附加价值的 set 和 get。

类属性和方法的访问权限

从面向过程到面向对象的一个最显著区别的就是,在面向对象时,把数据和函数组合在一起形成了类,数据变成了类的属性,函数变成了类的成员方法。

对于数据来说,一些计算过程中的内部变量,外部程序并不需要知道,所以需要对访问的权限加以控制。Matlab 提供了关键词:Access = private,protected,public 来声明哪些属性和方法是私有的、可以公开访问的、受保护的。

类属性的访问权限

公有属性 public

默认为公有属性,不但可以在类的定义中,该类的成员方法以及该类的子类的成员方法都可以访问这个成员变量,在类之外的函数或者脚本也可以访问这个成员变量。

classdef MyClass
    properties
        PublicProperty
    end
end

私有属性 private

  • 定义: properties (Access = private)
  • 说明: 私有属性只有该类的成员方法可以访问,外部代码和子类不能访问。
classdef MyClass
    properties (Access = private)
        PrivateProperty
    end
end

受保护属性 protected

  • 定义: properties (Access = protected)
  • 说明: 受保护的属性只能被类的实例和子类访问,外部代码不能访问。
classdef MyClass
    properties (Access = protected)
        ProtectedProperty
    end
end

SetAcess 和 GetAcess

基础使用
classdef MyClass
    properties (SetAccess = private, GetAcess = public)
        var
    end
end
  1. 表示该类属性可以被外部程序查询,但不能被外部程序赋值,赋值只能在类的内部进行。
  2. SetAccess = private, GetAcess = public 等同于 SetAccess = private
进阶使用

SetAccess = ?class:允许特定类的对象(指定类)设置某个属性的值。这对于在多个类之间共享数据并允许特定类访问属性是非常有用的。

假设我们有两个类:FiniteElementModelMeshOptimizer。我们希望 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:
    • NodeCoordinatesElementConnectivity 是公有属性,任何地方都可以读取和设置这些属性。
    • MeshData 是私有属性,SetAccess 被设置为 ?MeshOptimizer,这意味着只有 MeshOptimizer 类可以修改 MeshData 属性的值。
    • initializeMeshData 是一个私有方法,用于初始化 MeshData 属性。只有 FiniteElementModel 类的内部方法可以调用这个方法。
  • MeshOptimizer:
    • optimize 方法用于优化网格并设置 FiniteElementModel 对象的 MeshData 属性。由于 MeshOptimizer 类被允许设置 MeshData,这个操作是合法的。

运行:

% 创建 FiniteElementModel 对象
fem = FiniteElementModel([0110], [12]);

% 创建 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 11 0]
    Elements: [1 2]
    Optimized: 1

类方法的访问权限

公有方法 public

默认为公有方法,公有方法可以被类的其他成员方法、子类和外部代码调用。

classdef MyClass
 methods 
    % methods (Access = public)
        function publicMethod(obj)
            % 代码
        end
    end
end

私有方法 private

  • 定义: methods (Access = private)
  • 说明: 私有方法只能在定义该类中调用,外部代码和子类不能调用。
classdef MyClass
    methods (Access = private)
        function privateMethod(obj)
            % 代码
        end
    end
end

受保护方法 protected

  • 定义: methods (Access = protected)
  • 说明: 受保护的方法只能被类和子类调用,外部代码不能调用。
classdef MyClass
    methods (Access = protected)
        function protectedMethod(obj)
            % 代码
        end
    end
end

总结

  • Public: 任何地方都可以访问。
  • Protected: 只能在当前类和子类中访问。
  • Private: 只能在定义类的文件内部访问。


来源:易木木响叮当
航空航天MATLABUGpythonUM材料控制
著作权归作者所有,欢迎分享,未经许可,不得转载
首次发布时间:2024-09-08
最近编辑:2月前
易木木响叮当
硕士 有限元爱好者
获赞 220粉丝 263文章 349课程 2
点赞
收藏
作者推荐
未登录
还没有评论
课程
培训
服务
行家
VIP会员 学习 福利任务 兑换礼品
下载APP
联系我们
帮助与反馈