MATLAB field access with inheritance modifying all

2019-08-08 02:09发布

问题:

I'm trying to develop a data structure for a mesh using the MATLAB OOP functionalities. Long story short, I'm modifying a field from an instance that inherits from the same base class as another instance, and both instances are being modified, as if the field was declared static!

I have this code inside an abstract base class (m_element) in MATLAB:

properties(Access = protected)
    nodes = containers.Map('KeyType','int64', 'ValueType', 'any');
    faces = containers.Map('KeyType','int64', 'ValueType', 'any');
end

These field represent the connectivity of each element. For example, which nodes are neighbors of the n'th node, or which faces are adjacent to the n'th node.

I also have two other classes: m_face and m_node, each one inheriting from m_element. m_node is very simple:

classdef m_node < m_element
    properties
        x = 0;
        y = 0;
        z = 0;
    end

    methods
        function node = m_node(gmsh_id, x, y, z)
            node = node@m_element(gmsh_id);

            node.x = x;
            node.y = y;
            node.z = z;
        end
    end
end

But when it comes to m_face, I'm facing an issue. Here's the constructor, where the problem is arising:

function face = m_face(varargin)

    face = face@m_element(varargin{1});


    for k = 2:nargin
        nod = varargin{k};


        if(~isa(nod, 'm_node'))
            error('Algum dos argumentos não é um node!');
        elseif (~isvalid(nod))
            error('Algum dos argumentos não é válido!');
        else

            face.nodes(nod.gmsh_id) = nod;

            nod.faces(face.gmsh_id) = face;
        end

    end
end

The m_face constructor expects the face ID to come as the first argument, and the rest should be the nodes that forms the face. The line face.nodes(nod.gmsh_id) = nod; seems to be causing my problem. I have a m_mesh class which shall hold every node and face:

classdef m_mesh < handle
    properties(SetAccess = private)
        nodes = containers.Map('KeyType','int64', 'ValueType', 'any');
        faces = containers.Map('KeyType','int64', 'ValueType', 'any');
    end

    methods

        function theMesh = m_mesh(msh)
            for idx = 1:numel(msh.POS(:,1))
                n = msh.POS(idx,:);
                theMesh.nodes(idx) = m_node(idx, n(1), n(2), n(3));
            end

            for idx = 1:numel(msh.TRIANGLES(:,1))
                ele = msh.TRIANGLES(idx,:);
                nod(1) = theMesh.nodes(ele(1));
                nod(2) = theMesh.nodes(ele(2));
                nod(3) = theMesh.nodes(ele(3));

                theMesh.faces(idx) = m_face(idx, nod(1), nod(2), nod(3));
            end
        end
    end
end

The msh argument to the constructor holds the nodes spatial positions, and also the nodes that composes each face (which in this case are triangles).

Here's what I get when I build the mesh:

>> mesh = m_mesh(m)

mesh = 

m_mesh with properties:

nodes: [5x1 containers.Map]
edges: [0x1 containers.Map]
faces: [4x1 containers.Map]

>> nod = mesh.nodes.values();
>> nod{1}.i_nodes

ans = 

[1x1 m_node]    [1x1 m_node]    [1x1 m_node]    [1x1 m_node]    [1x1 m_node]

The i_nodes returns the instance map values. Now, how's that possible? Why does my first (and all the others!) node have FIVE adjacent nodes, if I haven't set this yet? Why should MATLAB change an non-static field for all instances and all subclasses when I access this field from a random instance?

回答1:

You should initialise your nodes and faces properties in the constructor, not as a default property value.

So what's going on here? The first thing to note is that containers.Map is a handle class.

Normal variables in MATLAB have value behaviour:

>> a = 1;
>> b = a;
>> a = 2;
>> b
b =
     1

Note that b has not changed when you changed a - it is a copy of a, and a and b are value variables that have pass-by-value behaviour.

Some other variables have handle behaviour:

>> a = figure;
>> b = a;
>> get(a, 'Name')
ans =
     ''
>> get(b, 'Name')
ans =
     ''
>> set(a, 'Name', 'hello')
>> get(b, 'Name')
ans =
hello

Note that b has changed when you changed a - it is a reference to a, and a and b are handle variables that have pass-by-reference behaviour.

containers.Map variables are handle variables.

The second thing to note is that property default values are evaluated once, at the first time the class is instantiated. (If you clear the class definition with clear classes, it will be evaluated again subsequently when you next instantiate it). They are not evaluated every time you create an object.

So what's happening is that the first time you create an object, the class is instantiated, the containers.Map is evaluated, and each object after that is getting the same containers.Map as its property. Since it's a handle variable, changes that you then make to the property in one object are referenced in the properties of others.

You don't want that: instead, you should initialise the values in the class constructor. Then it will get evaluated separately each time you construct an object, and each will get a separate one.

This behaviour (i.e., when you have handle variables as property defaults) can be confusing, but I think it's the right behaviour - it is documented, although I think it could be more clearly signposted. Here's an article where the topic is discussed - in the comments, the issue is debated between the author, myself, and a MathWorks developer responsible for OO syntax and design.