IEC 61131-3 ST言語 - オブジェクト指向プログラミング

← IEC 61131-3 ST言語 トップへ戻る

このページでは IEC 61131-3 Ed.4 のオブジェクト指向機能について解説します。クラス・継承・インタフェース・オブジェクト指向ファンクションブロックの各機能を利用すると、PLC プログラムをより再利用可能で拡張しやすい構造で記述できます。

クラス

クラス(class ~ end_class)は、オブジェクト指向プログラミングを実現するためのPOUです。メンバ変数とメソッドを持ちます。クラスのインスタンスは変数として宣言します。

クラスの構成(Counter クラスの例) Counter var _value : int method setValue(value : int) getValue() : int increment() reset()
図1. クラスの構成(Counter クラスの例)
class Counter
	var protected
		_value: int := 0;
	end_var

	method public setValue
		var_input value: int; end_var
		_value := value;
	end_method

	method public getValue: int
		getValue := _value;
	end_method

	method public increment
		_value := _value + 1;
	end_method

	method public reset
		_value := 0;
	end_method
end_class

program Main
	var
		c: Counter; // クラスのインスタンスを宣言
		v: int;
	end_var

	c.increment();
	c.increment();
	v := c.getValue(); // vは2
	c.reset();
	v := c.getValue(); // vは0
	// v := c._value; // Compile time error! Inaccessible element.
end_program

クラスにはコンストラクタの概念がありません。必要に応じて初期化用のメソッドを定義して呼び出してください。constructorinit という名前のメソッドはユーザーが自由に定義できます。

メンバ変数

クラスのメンバ変数はvarブロックで宣言します。アクセス修飾子(publicprivateprotectedinternal)で可視性を制御します。デフォルトはprotectedです。

アクセス修飾子
修飾子アクセス可能な範囲デフォルト
publicどこからでもアクセス可
internal同一名前空間内のみ名前空間をサポートする場合のみ有効(実装依存
protected同一クラスおよび派生クラス
private同一クラス内のみ

クラスインスタンス宣言時に外部から初期値を指定できるのはpublic変数のみです。protectedinternalprivate変数の初期値はクラス定義内で指定します。

class Account
	var private
		balance: lreal := 0.0;    // 同一クラス内のみアクセス可
	end_var
	var protected
		account_number: string;   // 同一クラスと派生クラスからアクセス可(デフォルト)
	end_var
	var public
		owner: string;            // どこからでもアクセス可
	end_var
end_class

// インスタンス宣言時の初期値指定はpublic変数のみ可
var
	acc: Account := (owner := 'Alice');    // OK
//	acc2: Account := (balance := 100.0);  // エラー: privateは外部から初期化不可
end_var

メソッド

メソッド(method ~ end_method)はクラスに属するファンクションです。戻り値の型は method メソッド名 : 型名 で宣言し、メソッド名への代入で戻り値を設定します。戻り値のないメソッドは式の中で呼び出せません。

アクセス修飾子 publicprivateprotectedinternal でメソッドの可視性を制御します。デフォルトは protected です。internal は名前空間をサポートする実装でのみ有効です(実装依存)。

メソッド内の変数(varvar_inputvar_outputvar_in_out)はすべて一時的であり、呼び出しをまたいで値は保持されません。メソッドはクラスのメンバ変数(varブロック)への読み書きアクセスを持ちます。なお、var_tempvar は同義です。

メソッドのオーバーロード(同名で引数の型が異なる複数のメソッド定義)は、IEC 61131-3 の範囲外です。

class Stack
	var private
		data: array[0..9] of int;
		top: int := 0;
	end_var

	method public push
		var_input val: int; end_var
		if top < 10 then
			data[top] := val;
			top := top + 1;
		end_if;
	end_method

	method public pop: int   // 戻り値型はint
		if top > 0 then
			top := top - 1;
			pop := data[top]; // メソッド名に代入して戻り値を設定
		end_if;
	end_method
end_class
this と super

this はメソッドが呼び出されているインスタンス自身を指します。同一クラス内のメソッドを内部呼び出しする際は this.メソッド名() を使います。super は親クラスのメソッドを呼び出します。this および super は、メソッド・アクション・ファンクションブロック本体の外では使用できません。また、super.super.メソッド名() のように祖先をたどった呼び出しはサポートされていません。

class Counter
	var private
		cv: uint := 0;
		max_val: uint := 1000;
	end_var

	method public up: uint
		var_input inc: uint; end_var
		if cv <= max_val - inc then cv := cv + inc; end_if;
		up := cv;
	end_method

	method public up5: uint
		up5 := this.up(inc := 5);  // 同一インスタンスのupを内部呼び出し
	end_method
end_class
override と動的束縛

派生クラスで親クラスのメソッドを上書きするには override キーワードが必要です。override により、変数の宣言型が親クラスであっても、実際のインスタンス型のメソッドが呼び出されます(動的束縛)。

上書きできるのは publicprotectedinternal の各アクセス修飾子を持つメソッドのみです。private メソッドは上書きできません。上書きするメソッドは、上書き元と同じアクセス修飾子を使う必要があります。override のないメソッドの上書きや、final メソッドのオーバーライドはエラーになります。

class LightRoom
	var light: bool; end_var
	method public nighttime
		light := true;
	end_method
end_class

class Light2Room extends LightRoom
	var light2: bool; end_var
	method public override nighttime  // 親クラスのnighttimeを上書き
		super.nighttime();             // 親クラスのメソッドを呼び出す
		light2 := true;
	end_method
end_class

メソッドの省略可能なパラメータに指定されたデフォルト値は、動的束縛の対象外であり、宣言型のシグネチャから静的に決まります。インタフェース型変数または基底クラス型変数経由で呼び出す場合、省略したパラメータのデフォルト値は宣言型のメソッドシグネチャから取得されます。実際に呼ばれるメソッドの実装は派生クラスのものでも、デフォルト値は宣言型から取られる点に注意が必要です。

abstract と final

abstract をメソッドに付けると抽象メソッドになり、本体は空にします。abstract クラスはインスタンス化できず、派生クラスですべての抽象メソッドを実装する必要があります。ただし、派生クラス自身が abstract である場合はその限りではありません。final メソッドは派生クラスでのオーバーライドを禁止します。final クラスはそれ以上継承できません。

抽象クラスには1つ以上の抽象メソッドが必要です。また、abstractoverridefinal と組み合わせて使用できません。abstract は抽象クラスのメソッドにのみ使用できます。

class abstract Shape   // abstractクラスはインスタンス化不可
	method public abstract area: lreal   // 抽象メソッド(本体は空)
	end_method
end_class

class Circle extends Shape
	var public r: lreal := 1.0; end_var
	method public override area: lreal   // 抽象メソッドの実装(必須)
		area := 3.14159 * r * r;
	end_method
end_class

プロパティ

プロパティは、変数のように読み書きできる言語要素です。その裏で get/set メソッドが実行されます。property_get ~ end_property で読み取り処理を、property_set ~ end_property で書き込み処理を定義します。どちらか一方だけを定義することもでき、get のみなら読み取り専用、set のみなら書き込み専用になります。

プロパティは var_inputvar_outputvar_in_out セクションを持てません。一時変数には varvar_temp を使用します。プロパティ名と同じ変数がスコープ内に暗黙的に定義されており、get では戻り値として使用し、set では入力値として読み取ります。

class Length
	var public
		cm: lreal;               // 内部保持値(センチメートル)
	end_var

	// 読み書き可能プロパティ: inch
	property_get public inch: lreal
		inch := cm / 2.54;       // プロパティ名に代入して戻り値を設定
	end_property
	property_set public inch: lreal
		cm := inch * 2.54;       // プロパティ名で入力値を参照
	end_property

	// 読み取り専用プロパティ: valid
	property_get public valid: bool
		valid := cm >= 0.0;
	end_property

	// 書き込み専用プロパティ: reset
	property_set public reset: bool
		if reset then
			cm := 0.0;
		end_if;
	end_property
end_class

program Main
	var
		len: Length;
		val: lreal;
	end_var
	len.inch := 10.0;              // property_set inch を呼び出す
	val := len.inch;               // property_get inch を呼び出す
	if not len.valid then          // property_get valid を呼び出す
		len.reset := true;         // property_set reset を呼び出す
	end_if;
	// len.reset := false;         // OK(property_set は存在)
	// if len.reset then           // エラー: property_get が未定義
	// len.valid := true;          // エラー: property_set が未定義
end_program

プロパティはクラス、ファンクションブロック、インタフェースに定義できます。インタフェースに定義するプロパティは本体を持たないプロトタイプのみです。

継承クラス・インタフェース

クラスはextendsで他のクラス(スーパークラス)を継承し、スーパークラスのメソッドとメンバ変数を引き継ぎます。implementsでインタフェースを実装できます。インタフェース interface ~ end_interface はメソッドのシグネチャのみを定義します。ポリモーフィズムの実現には、インタフェース型変数または基底クラスへの参照型 ref_to、もしくは var_in_out 変数を使用します。

継承は単一継承のみサポートします。複数のクラスからの多重継承は不可です。インタフェースについては、複数実装できます。なお、クラスはファンクションブロックを継承することはできません。ファンクションブロックはクラスまたはファンクションブロックを継承できます。

継承関係の例(Circle は abstract Shape を継承) «abstract» Shape area() : lreal «abstract» Circle area() : lreal «override» extends
図2. 継承関係の例(Circle は abstract Shape を継承)
interface IShape
	method area: lreal        // プロトタイプにアクセス修飾子は不要(暗黙的にpublic)
	end_method

	method perimeter: lreal
	end_method
end_interface

class Circle implements IShape
	var private constant PI : lreal := 3.141592653589793; end_var
	var public r: lreal := 1.0; end_var

	method public init
		var_input radius: lreal; end_var
		r := radius;
	end_method

	method public area: lreal          // インタフェース実装メソッドはpublic必須
		area := PI * r * r;
	end_method

	method public perimeter: lreal
		perimeter := 2.0 * PI * r;
	end_method
end_class

class Rectangle implements IShape
	var public
		w: lreal := 1.0;
		h: lreal := 1.0;
	end_var

	method public init
		var_input
			width: lreal;
			height: lreal;
		end_var
		w := width;
		h := height;
	end_method

	method public area: lreal
		area := w * h;
	end_method

	method public perimeter: lreal
		perimeter := 2.0 * (w + h);
	end_method
end_class

// インタフェース型変数によるポリモーフィズム
// インタフェース型変数はクラスインスタンスへの参照を保持する(null が初期値)
function_block AreaPrinter
	var_input shape: IShape; end_var  // インタフェース型を変数の型として使用
	var area_val: lreal; end_var
	if shape <> null then
		area_val := shape.area();     // デリファレンス不要
	end_if;
end_function_block

program Main
	var
		c: Circle;
		r: Rectangle;
		printer: AreaPrinter;
		s: IShape;
	end_var
	c.init(radius := 2.0);
	r.init(width := 4.0, height := 3.0);
	s := c;                            // クラスインスタンスをインタフェース型変数に代入
	printer(shape := s);               // Circle.area() を呼び出す(動的束縛)
	s := r;
	printer(shape := s);               // Rectangle.area() を呼び出す(動的束縛)
end_program

インタフェース型変数は、インタフェースを実装したクラスのインスタンスへの参照を保持します。初期値は null です。null チェックを行わずにメソッドを呼び出すとエラーになります。なお、インタフェース型は var_in_out 変数には使用できません。

インタフェースの継承

インタフェースは extends で他のインタフェースを継承できます。複数のインタフェースを同時に継承することも可能です。派生インタフェースは基底インタフェースのメソッドプロトタイプをすべて継承します。その派生インタフェースを実装するクラスは、継承されたメソッドを含むすべてのメソッドを実装する必要があります。なお、インタフェースはクラスや FB を継承することはできません。

interface IReadable
	method read: int
	end_method
end_interface

interface IWritable
	method write
		var_input val: int; end_var
	end_method
end_interface

// IReadable と IWritable の両方を継承
interface IReadWrite extends IReadable, IWritable
end_interface

class Register implements IReadWrite
	var private data: int; end_var

	method public read: int    // IReadable からの継承
		read := data;
	end_method

	method public write        // IWritable からの継承
		var_input val: int; end_var
		data := val;
	end_method
end_class

代入試行

代入試行(?=)は、インタフェース型変数または参照型変数に対してダウンキャストを安全に行う演算子です。キャスト先の型と互換性がある場合は有効な参照が設定され、互換性がない場合は null が設定されます。結果の利用前に null チェックを行う必要があります。

代入試行(?=)の動作 Circle implements IShape ✓ r : lreal s : IShape 参照 ?= ref_to Circle ✓ 成功 (実体は Circle) ?= ref_to Rectangle null (実体は Rectangle ではない)
図3. 代入試行(?=)の動作
interface ITF1 end_interface
interface ITF2 end_interface
interface ITF3 end_interface

class MyClass implements ITF1, ITF2
end_class

program Main
	var
		inst: MyClass;
		itf1: ITF1;
		itf2: ITF2;
		itf3: ITF3;
	end_var
	itf1 := inst;        // MyClass は ITF1 を実装しているので代入可
	itf2 ?= itf1;        // itf1 の実体は MyClass → ITF2 も実装 → 代入成功
	itf3 ?= itf1;        // itf1 の実体は MyClass → ITF3 は未実装 → null になる
	if itf2 <> null then
		// itf2 を通じて ITF2 のメソッドを呼び出せる
	end_if;
	// itf3 は null なので、null チェックなしで使うとエラー
end_program

オブジェクト指向ファンクションブロック(OOFB)

OOFBは、既存のファンクションブロックに継承・インタフェース実装・メソッドのOOP機能を追加したものです。var_inputvar_outputによる入出力宣言やインスタンス化といったFBとしての基本機能はそのまま保持されます。通常のFBとの違いは、extendsによる継承・implementsによるインタフェース実装・methodによる追加メソッドが使用できる点です。

interface IDevice
	method init   // アクセス修飾子なし(暗黙的にpublic)
	end_method
	method execute
	end_method
end_interface

function_block BaseDevice implements IDevice
	var public enabled: bool := false; end_var
	method public init        // インタフェース実装メソッドはpublic必須
		enabled := true;
	end_method
	method public execute
		// 基底クラスの処理
	end_method
end_function_block

function_block Motor extends BaseDevice
	var public speed: int := 0; end_var
	method public override execute   // 継承メソッドを上書きするにはoverrideが必要
		if enabled then
			speed := 100;
		end_if;
	end_method
end_function_block
super() によるボディ継承

FB を継承した場合、基底 FB のボディは自動的には実行されません。基底 FB のボディを実行するには、派生 FB のボディ内で super() を呼び出します。super() は引数なしで呼び出します。super() は FB のボディ内でのみ使用でき、メソッド内では使用できません。

function_block BaseFB
	var_input a: int; end_var
	var_output x: int; end_var
	x := a + 1;                 // 基底FBのボディ
end_function_block

function_block DerivedFB extends BaseFB
	var_input b: int; end_var
	super();                    // 基底FBのボディを呼び出す(x := a + 1 が実行される)
	x := 3 * x + b;            // その結果を使ってさらに処理
end_function_block