Subclassing MATLAB Built-In Classes

MATLAB Built-In Classes

Built-in classes represent fundamental kinds of data such as numeric arrays, logical arrays, and character arrays. Other classes tend to combine data belonging to these fundamental classes. cell arrays and struct arrays are other fundamental classes.

Built-in classes define methods that enable you to perform many operations on objects of these classes. For example, the MATLAB language enables you to create objects of class double using an assignment statement, to create arrays of doubles, and to perform complex operations on these arrays such as matrix multiplication, indexing, and so on.

See Classes (Data Types) for more information on these classes.

Why Subclass Built-In Classes

You can derive classes from most MATLAB built-in classes (see Built-In Classes You Cannot Subclass). Subclassing a built-in class is useful when you want to extend the operations that you can perform on a particular class of data.

Typically, you create a class that primarily uses data of a built-in class and you want to be able to use the methods of that built-in class directly with objects of your class. For example, if you subclass the built-in class double, you can use an object of your subclass anywhere a double is expected. See Behavior of Built-In Functions with Subclass Objects for information on methods your subclass might require.

Consider a class that defines named constants. It might derive from integral classes and thereby inherit methods that enable you to compare and sort values. For example, integer classes like int32 support all the relational methods (eq, ge, gt, le, lt, ne).

Built-In Classes You Cannot Subclass

You cannot subclass the following built-in MATLAB classes:

To list the methods of a built-in class, use the methods command. For example:

methods double

Behavior of Built-In Functions with Subclass Objects

When you create a class that derives from a MATLAB built-in class, built-in methods and functions become available to the subclass as methods. When you call one of these built-in methods, it generally acts on the built-in part of the subclass. For example, if you subclass double, and then perform addition on two subclass objects, MATLAB adds the parts of the objects that are doubles and the object returned is of class double.

Built-in functions that work on built-in classes behave in different ways, depending on which function you are using and if your subclass defines properties.

Functions that operate on objects of the superclass effectively become subclass methods.

Subclasses that do not define properties inherit the superclass's methods. Adding properties to a subclass of a built-in class prevents some superclass methods from being able to work with your subclass. If your class needs the functionality provided by these methods, you must implement your own versions:

The following sections describe how different categories of methods behave with subclasses:

Extending the Operations of a Built-In Class

The MATLAB built-in class double defines a wide range of methods to perform arithmetic operations, indexing, matrix operation, and so on. Therefore, subclassing double enables you to add specific features without implementing many of the methods that a numeric class needs to function usefully in the MATLAB language.

The following class definition subclasses the built-in class double.

classdef DocSimpleDouble < double
   methods
      function obj = DocSimpleDouble(data)
         obj = obj@double(data); % initialize the base class portion
      end
   end
end

You can create an instance of the class DocSimpleDouble and call any methods of the double class.

>>sc = DocSimpleDouble(1:10);
sc = 
  DocSimpleDouble
  double data:
     1     2     3     4     5     6     7     8     9    10
  Methods, Superclasses

Calling a method inherited from class double that operates on the data, like sum, returns a double and, therefore, uses the display method of class double:

>> sum(sc)
ans =
    55

You can index sc like an array of doubles. Note that the returned value is the class of the subclass, not double:

>> a = sc(2:4)
a = 
  DocSimpleDouble
  double data:
     2     3     4

Indexed assignment also works:

>> sc(1:5) = 5:-1:1
sc = 
  DocSimpleDouble
  double data:
     5     4     3     2     1     6     7     8     9    10
  Methods, Superclasses

Calling a method that modifies the order of the data elements operates on the data, but returns an object of the subclass:

>> sc = DocSimpleDouble(1:10);
>> sc(1:5) = 5:-1:1;
a = sort(sc)
a = 
  DocSimpleDouble
  double data:
     1     2     3     4     5     6     7     8     9    10
  Methods, Superclasses

Built-In Methods That Operate on Data Values

Most built-in functions used with built-in classes are actually methods of the built-in class. For example, the double and single classes both have a sin method. All of these built-in class methods work with subclasses of the built-in class.

When you call a built-in method on a subclass object, only the built-in (i.e., superclass) parts of the subclass object are used as inputs to the method, and the value returned is same class as the built-in class, not your subclass.

Built-In Methods That Operate on Data Organization

This group of built-in methods reorders or reshapes the input argument array. These methods operate on the built-in (i.e., superclass) part of the subclass object, but return an object of the same type as the subclass. Methods in this group include:

Indexing Methods

Built-in classes use specially implemented versions of the subsref, subsasgn, and subsindex methods to implement indexing (subscripted reference and assignment). When you index a subclass object, only the built-in data is referenced (not the properties defined by your subclass). For example, indexing element 2 in the DocSimpleDouble subclass object returns the second element in the vector:

>> sc = DocSimpleDouble(1:10);
>> a = sc(2)
a = 
  DocSimpleDouble
  double data:
     2
  Methods, Superclasses

The value returned from an indexing operation is an object of the subclass. You cannot make subscripted references if your subclass defines properties unless your subclass overrides the default subsref method.

Assigning a new value to the second element in the DocSimpleDouble object operates only on the built-in data:

>> sc(2) = 12
sc = 
  DocSimpleDouble
  double data:
     1    12     3     4     5     6     7     8     9    10
  Methods, Superclasses

However, the data property of the built-in part of the DocSimpleDouble object is hidden:

>> sc.data
??? Error using ==> subsref
No appropriate method, property, or field data for class DocSimpleDouble.

The subsref method also implements dot notation for methods. See Example — Adding Properties to a Built-In Subclass for an example of a subsref method.

Concatenation Functions

Built-in classes use the functions horzcat, vertcat, and cat to implement concatenation. When you use these function with subclass objects of the same type, the built-in data parts are concatenated to form a new object. For example, you can concatenate objects of the DocSimpleDouble class:

>> sc1 = DocSimpleDouble(1:10);
>> sc2 = DocSimpleDouble(11:20);
>> [sc1 sc2]
ans = 
  DocSimpleDouble
  double data:
  Columns 1 through 13
     1     2     3     4     5     6     7     8     9    10    11    12    13
  Columns 14 through 20
    14    15    16    17    18    19    20
  Methods, Superclasses
>> [sc1; sc2]
ans = 
  DocSimpleDouble
  double data:
     1     2     3     4     5     6     7     8     9    10
    11    12    13    14    15    16    17    18    19    20
  Methods, Superclasses

Concatenate two objects along a third dimension:

>> c = cat(3,sc1,sc2)
>> c = cat(3,sc1,sc2)
c = 
  DocSimpleDouble
  double data:
(:,:,1) =
     1     2     3     4     5     6     7     8     9    10
(:,:,2) =
    11    12    13    14    15    16    17    18    19    20
  Methods, Superclasses

Note that you cannot concatenate subclass objects of built-in classes if the subclass defines properties. Such an operation does not make sense if there are properties defined because there is no way to know how to combine properties of different objects. However, your subclass can define custom horzcat and vertcat methods support concatenation in whatever way makes sense for your subclass. See Concatenating DocExtendDouble Objects for an example.

Example — A Class to Manage uint8 Data

This example shows how deriving a class from the built-in uint8 class can simplify the process of maintaining a collection of intensity image data defined by uint8 values. The basic operations of the class include:

The class data are matrices of intensity image data, which are stored in the built-in part of the subclass so no properties are required.

The DocUint8 class stores the image data in the built-in (uint8) part of the object, which is initialized in the constructor after the data has been converted, if necessary:

classdef DocUint8 < uint8
   methods
      function obj = DocUint8(data)
      % Support no argument case
         if nargin == 0
            data = uint8([]);
         % If image data is not uint8, convert to uint8
         elseif ~strcmp('uint8',class(data))
            switch class(data)
               case 'uint16'
                  t = double(data)/65535;
                  data = uint8(round(t*255));
               case 'double'
                  data = uint8(round(data*255));
               otherwise
                  error('Not a supported image class')
            end
         end
         % assign data to built-in part of object
         obj = obj@uint8(data);
      end
      % Get uint8 data and setup call to imagesc
      function h = showImage(obj)
         data = uint8(obj);
         figure; colormap(gray(256))
         h = imagesc(data,[0 255]);
         axis image
         brighten(.2)
      end
   end
end

Using the DocUint8 Class

The DocUint8 class contains its own conversion code and provide a method to display all images stored as DocUint8 objects in a consistent way. You can only call subclass (DocUint8) methods using dot notation, because the uint8 class does not define dot notation of its methods. For example:

>> cir = imread('circuit.tif');
>> img1 = DocUint8(cir);
>> img1.showImage;

Because DocUint8 is derived from uint8, you can use any of its methods. For example,

>> size(img1)
ans =
   280   272

returns the size of the image data.

Indexing Operations

Inherited methods perform indexing operations, but return objects of the same class as the subclass.

You can index into the image data:

>> showImage(img1(100:200,1:160));

Subscripted reference operations (controlled by the inherited subsref method) return a DocUint8 object.

You can assign values to indexed elements:

>> img1(100:120,140:160) = 255;

Subscripted assignment operations (controlled by the inherited subsasgn method) return a DocUint8 object.

Concatenation Operations

Concatenation operations work on DocUint8 objects because this class inherits the uint8 horzcat and vertcat methods, which return a DocUint8 object:

>> showImage([img1 img1]);

Data Operations

Methods that operate on data values, such as arithmetic operators, always return an object of the built-in type (not of the subclass type). For example, multiplying DocUint8 objects returns a uint8 object:

>> showImage(img1.*.8);
??? Undefined function or method 'showImage' for input arguments of type 'uint8'.

If you need to be able to perform operations of this type, you must implement a subclass method to override the inherited method. The times method implements array (element-by-element) multiplication. See Implementing Operators for Your Class for a list of operator method names.

For example:

function o = times(obj,val)
   u8 = uint8(obj).*val; % Call uint8 times method
   o = DocUint8(u8);
end

Keep in mind that when you override a uint8 method, MATLAB software calls the subclass method and no longer dispatches to the base class method. Therefore, you must explicitly call the uint8 times method or an infinite recursion can occur. The explicit call is made in this statement of the DocUint8 times method:

>> u8 = uint8(obj).*val;

After adding the times method to DocUint8, you can use the showImage method in expressions like:

>> showImage(img1.*1.8);

Example — Adding Properties to a Built-In Subclass

When your subclass defines properties, indexing and concatenation do not work by default. There is really no way for the default subsref, horzcat, and vertcat methods to work with unknown property types and values. The following example subclasses the double class and defines a single property intended to contain a descriptive character string.

Methods Implemented

The following methods modify the behavior of the DocExtendDouble class:

Property Added

The DocExtendDouble class defines the DataString property to contain text that describes the data contained in instances of the DocExtendDouble class. Keep in mind that the built-in part (double) of the class contains the data.

Subclass with Properties

The DocExtendDouble class extends double and implements methods to support subscripted reference and concatenation.

classdef DocExtendDouble < double
   
   properties
      DataString
   end
   
   methods
      function obj = DocExtendDouble(data,str)
         if nargin == 0
            data = [];
            str = '';
         end
         obj = obj@double(data);
         obj.DataString = str;
      end
     
      function sref = subsref(obj,s)
         % Implements dot notation for DataString and Data
         % as well as indexed reference
         switch s(1).type
            case '.'
               switch s(1).subs
                  case 'DataString'
                     sref = obj.DataString;
                  case 'Data'
                     sref = double(obj);
                     if length(s)>1 && strcmp(s(2).type, '()')
                        sref = subsref(sref,s(2:end));
                     end
               end
            case '()'
               sf = double(obj);
               if ~isempty(s(1).subs)
                  sf = subsref(sf,s(1:end));
               else
                  error('Not a supported subscripted reference')
               end
               sref = DocExtendDouble(sf,obj.DataString);
         end
      end
     
      function newobj = horzcat(varargin)
         % Horizontal concatenation - cellfun calls double
         % on all object to get built-in part. cellfun call local char
         % to get DataString and the creates new object that combines
         % doubles in vector and chars in cell array and creates new object
         d1 = cellfun( @double,varargin,'UniformOutput',false );
         data = horzcat(d1{:});
         str = horzcat(cellfun( @char, varargin,'UniformOutput',false));
         newobj = DocExtendDouble(data,str);
      end
      
      function newobj = vertcat(varargin)
         % Need both horzcat and vertcat
        d1 = cellfun( @double,varargin,'UniformOutput',false );
        data = vertcat(d1{:});
        str = vertcat(cellfun( @char, varargin,'UniformOutput',false));
        newobj = DocExtendDouble(data,str);
      end

      function str = char(obj)
         % Used for cat functions to return DataString
         str = obj.DataString;
      end

      function disp(obj)
         % Change the default display
         disp(obj.DataString)
         disp(double(obj))
      end
   end
end

Create an instance of DocExtendDouble and notice that the display is different from the default:

>> ed = DocExtendDouble(1:10,'One to ten')
ed = 
One to ten
     1     2     3     4     5     6     7     8     9    10

The sum function continues to operate on the built-in part of the object:

>> sum(ed)
ans =
    55

Subscripted assignment works on the built-in part of the object:

>> ed(1:5) = 5:-1:1
ed = 
One to ten
     5     4     3     2     1     6     7     8     9    10

The sort function works on the built-in part of the object:

>> sort(ed)
ans = 
One to ten
     1     2     3     4     5     6     7     8     9    10

Indexed Reference of a DocExtendDouble Object

While subscripted assignment (performed by subsasgn) operates on the built-in part of the DocExtendDouble object by default, subscripted reference (performed by subsref) requires the subclass to implement its own subsref method.

>> ed = DocExtendDouble(1:10,'One to ten');
>> a = ed(2)
a = 
One to ten
     2
>> whos
  Name      Size            Bytes  Class           
  a         1x1                84  DocExtendDouble 
  ed        1x10              156  DocExtendDouble  

You can access the property data:

>> c = ed.DataString
c =
One to ten
>> whos
  Name      Size            Bytes  Class         
  c         1x10               20  char           
  ed        1x10              156  DocExtendDouble 

You can access the built-in part of the object using dot notation with Data because this capability is provided by the DocExtendDouble subsref method:

>> d = ed.Data
d =
     1     2     3     4     5     6     7     8     9    10
>> whos
  Name      Size            Bytes  Class          
  d         1x10               80  double          
  ed        1x10              156  DocExtendDouble 

Concatenating DocExtendDouble Objects

Given the following two objects:

ed1 = DocExtendDouble([1:10],'One to ten');
ed2 = DocExtendDouble([10:-1:1],'Ten to one');

You can concatenate these objects along the horizontal dimension:

>> hcat = [ed1 ed2]
hcat = 
    'One to ten'    'Ten to one'
  Columns 1 through 13
     1     2     3     4     5     6     7     8     9    10    10     9     8
  Columns 14 through 20
     7     6     5     4     3     2     1
>> whos
  Name      Size            Bytes  Class           
  ed1       1x10              156  DocExtendDouble 
  ed2       1x10              156  DocExtendDouble
  hcat      1x20              376  DocExtendDouble  

Vertical concatenation works in a similar way:

>> vcat = [ed1;ed2]
vcat = 
    'One to ten'    'Ten to one'
     1     2     3     4     5     6     7     8     9    10
    10     9     8     7     6     5     4     3     2     1

Both horzcat and vertcat return a new object of the same class as the subclass.

Understanding size and numel

Both size and numel work by default with subclasses of built-in classes, whether or not the subclasses define properties. However, the returned values are based on different parts of the subclass object. For example, consider subclass objects, one that defines no properties and one that does define a property:

>> sd = DocSimpleDouble(1:10);
>> ed = DocExtendDouble(1:10,'One to ten');

The size function returns the size of the built-in part of the objects:

>> size(sd)
ans =
     1    10
>> size(ed)
ans =
     1    10

The numel function treats objects differently:

>> numel(sd)
ans =
     1
>> numel(ed)
ans =
     1

In each case, numel counts the number of objects, not the size of the built-in array:

>> size([ed;ed])
ans =
     2    10
>> numel([ed;ed])
ans =
     1

Overriding size

Subclasses of built-in classes inherit a size method, which as stated above, operates on the built-in part of the subclass object. If you want size to behave in another way, you can override it by defining your own size method in your subclass.

Keep in mind that other MATLAB functions use the values returned by size. If you change the way size behaves, ensure that the values returned make sense for the intended use of your class.

Avoid Overloading numel

It is important to understand the significance of numel with respect to indexing. Both subsref and subsasgn use numel:

Subclasses of built-in classes always return scalar objects as a result of subscripted reference and always use scalar objects for subscripted assignment. The numel function returns the correct value for these operations and there is, therefore, no reason to overload numel.

If you define a class in which nargout for subsref or nargin for subsasgn is different from the value returned by the default numel, then you must overload numel for that class to ensure it returns the correct values.

Example — A Class to Represent Hardware

This example shows the implementation of a class to represent an optical multiplex card. These cards typically have a number of input ports, which are represented here by their data rates and their names. There is also an output port. The output rate of a multiplex card is the sum of the input port data rates.

The DocMuxCard class defines the output rate as a dependent property, and then defines a get access method for this property. This means the actual output rate is calculated when it is required. See Property Get Methods for more information on this technique.

Why Derive from int32

The DocMuxCard class is derived from the int32 class because the input port data rates are being represented by 32–bit integers. The DocMuxCard class inherits the methods of the int32 class, which simplifies the implementation of this subclass.

Class Definition

Here is the definition of the DocMuxCard class. Notice that the input port rates are used to initialize the int32 portion of class.

classdef DocMuxCard < int32
   properties
      InPutNames % cell array of strings
      OutPutName % a string
   end
   properties (Dependent = true)
      OutPutRate
   end
   methods
      function obj = DocMuxCard(inptnames, inptrates, outpname)
         obj = obj@int32(inptrates); % initial the int32 class portion
         obj.InPutNames = inptnames;
         obj.OutPutName = outpname;
      end
      function x = get.OutPutRate(obj)
         x = sum(obj); % calculate the value of the property
      end
      function x = subsref(card, s)
           if strcmp(s(1).type,'.')
              base = subsref@int32(card, s(1));
              if isscalar(s)
                 x = base;
              else
                 x = subsref(base, s(2:end));
              end
           else
              x = subsref(int32(card), s);
           end
        end
   end
end

Using the Class with Methods of int32

The constructor takes three arguments:

>> omx = OMuxCard({'inp1','inp2','inp3','inp4'},[3 12 12 48],'outp')
omx = 
  DocMuxCard
  Properties:
    InPutNames: {'inp1'  'inp2'  'inp3'  'inp4'}
    OutPutName: 'outp'
    OutPutRate: 75
  int32 data:
           3          12          12          48
  Methods, Superclasses

You can treat an OMuxCard object like an int32. For example, this statement accesses the int32 data in the object to determine the names of the input ports that have a rate of 12:

>> omx.InPutNames(find(omx==12))
ans = 
    'inp2'    'inp3'

Indexing the OMuxCard object accesses the int32 vector of input port rates:

>> omx(1:2)
ans =
           3          12

The OupPutRate property get access method makes use of sum to sum the output port rates:

>> omx.OutPutRate
ans =
    75
  


 © 1984-2008- The MathWorks, Inc.    -   Site Help   -   Patents   -   Trademarks   -   Privacy Policy   -   Preventing Piracy   -   RSS