jeeswg's object classes tutorial

Put simple Tips and Tricks that are not entire Tutorials in this forum
User avatar
jeeswg
Posts: 5271
Joined: 19 Dec 2016, 01:58
Location: UK

jeeswg's object classes tutorial

23 Aug 2018, 23:21

==================================================

JEESWG'S OBJECT CLASSES TUTORIAL

==================================================

CONTENTS

[SECTION 1: INTRODUCTION]
> QUICK SUMMARY
> INTRODUCTION: OBJECTS AND KEYS/METHODS/PROPERTIES
> INTRODUCTION: CLASSES/INSTANCES AND AUTOHOTKEY'S BUILT-IN CLASSES
> AUTOHOTKEY'S BASIC OBJECT
> AUTOHOTKEY OBJECT FUNCTIONS
> META-FUNCTIONS
> A ZOO CLASS AND BASE OBJECTS
> SUPERCLASSES/SUBCLASSES
> NESTED CLASSES
> __CLASS KEY, AND BASE OBJECTS: CHASE THE BASE
> BASE OBJECTS: REPLACE THE BASE / MODIFY AN AHK ARRAY

[SECTION 2: KEYS/PROPERTIES/METHODS]
> CLASSES: KEYS
> CLASSES: PROPERTIES
> CLASSES: METHODS
> CLASSES: KEYS/METHODS/PROPERTIES AND PRIORITY

[SECTION 3: META-FUNCTIONS]
> META-FUNCTIONS
> META-FUNCTIONS+: __NEW / __DELETE
> META-FUNCTIONS+: __INIT
> META-FUNCTIONS: __CALL
> META-FUNCTIONS: __GET
> META-FUNCTIONS: __SET
> __GET/__SET: MONITOR EVERY TIME A KEY IS GOTTEN/SET
> __GET/__SET: ADD KEYS AND SUBKEYS (MULTI-DIMENSIONAL ASSIGNMENTS)
> __GET/__SET: GET AND SET AT THE SAME TIME
> GENERAL CLASS EXAMPLES: DATES AND SPLIT PATH

[SECTION 4: ENUMERATORS AND MISCELLANEOUS]
> ENUMERATORS: _NEWENUM AND NEXT
> SINGLETON
> TUTORIALS / LINKS
> THE END

==================================================

[SECTION 1: INTRODUCTION]

> QUICK SUMMARY

Terms: object, key/method/property.
- Objects consist of keys, methods and properties.
- Objects combine the concepts of data (keys) and functions (methods/properties).
- Properties look like keys, but work like functions.

Terms: object/instance/class.
- A class is a blueprint object.
- An instance is an object that is created based on a class blueprint.
- AutoHotkey has a Basic Object class, as well as other classes.
- Users can create custom class blueprints.
- Custom class blueprints are stored as Basic Object instances, this can be seen by doing ListVars or 'View, Variables and their contents'.
- E.g. if you create a basic object instance via 'obj := {}' and define a class called 'MyClass', then IsObject(obj) and IsObject(MyClass) will both report true.

6 methods: __Init/__New/__Get/__Set/__Call/__Delete.
- The following 6 methods are key to customisation when creating custom classes.
•__Init ['should not be used by the script', handles when an object instance is created]
•__New [handles when an object instance is created]
•__Get / __Set [handle when a non-existent key/property is gotten/set]
•__Call [handles when a non-existent method is called (and is invoked even when a method does exist)]
•__Delete [handles when an object instance is deleted]

Terms: base.
- The base object for any object typically contains keys/methods/properties that determine how that object works.

2 methods: _NewEnum/Next.
Terms: enumerator.
- A 'for loop' calls an object's _NewEnum method.
- The _NewEnum method returns an object called an enumerator object.
- The enumerator object will have a Next method which will return keys/values pairs until there are none left.
- An enumerator object might instead return key/value pairs that are not based on an object's contents but which are computed (e.g. square numbers). It could return key/value pairs indefinitely.

Further concepts.
Terms: superclass, subclass, nested class.
- Superclasses/subclasses. A superclass can have some features, a subclass inherits those features and has its own features.
- For an object based on a subclass, if both the superclass and subclass share a feature, the subclass takes priority.
- Nested class. A class that is defined within another class.

==================================================

> INTRODUCTION: OBJECTS AND KEYS/METHODS/PROPERTIES

- Two key concepts in programming are data and functions.
- Objects combine these two concepts: keys (data) and methods/properties (functions).

Code: Select all

;e.g. get the value of a key/property:
var := obj.key
var := obj.property
;e.g. set the value of a key/property:
obj.key := var
obj.property := var
;e.g. use a method.
obj.method(var1, var2)
- Note: keys and properties appear identical externally, when used, however, properties and methods are more similar internally, in terms of how they are defined.

==================================================

> INTRODUCTION: CLASSES/INSTANCES AND AUTOHOTKEY'S BUILT-IN CLASSES

- A class is a blueprint for a type of object, with various keys/methods/properties.
- An instance is an individual object based on a class.

- AutoHotkey provides various built-in classes:
- Basic, BoundFunc, ClipboardAll, Enumerator, Exception, File, Func, GUI, GuiControl, Menu/MenuBar, RegExMatch, SafeArray.
- There are more details here:
list of every object type/property/method - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 37&t=44081

==================================================

> AUTOHOTKEY'S BASIC OBJECT

AutoHotkey combines 3 concepts into 1 built-in basic object type:
- linear arrays (e.g. obj.1 := "value") (AKA list/vector)
- associative arrays (e.g. obj.key := "value") (AKA dictionary/map)
- default class (keys/properties/methods can be added/modified/removed to give custom classes)

- Note: In AutoHotkey, a linear array is an array with 0 or more integer keys, starting at 1.
- Note: The built-in methods for integer keys apply to positive *and* negative integers (and 0).

- When creating a custom class, the AHK basic object is used as the template.
- The built-in keys/methods/properties can be overridden/removed.
- Additional keys/methods/properties can be added.
- Note: by default there is no restriction to the keys that can be added to a custom class (it acts like an associative array). This can be changed by specifying a custom __Set method (which is discussed later on).

- Here is a list of built-in methods and properties for the basic object.

[Basic Object: methods for integer keys only (7):]
•InsertAt / RemoveAt [add/remove keys]
•Push / Pop [add/remove keys]
•MinIndex / MaxIndex / Length [get min/max key]

[Basic Object: methods for all keys (8):]
•Delete [remove keys]
•Count [get key count]
•SetCapacity / GetCapacity [get/set an object's key count capacity, or a value's string size capacity]
•GetAddress [get the address of a value's string buffer (if the value is a string)]
•_NewEnum [returns an enumerator object used for looping through object keys/values]
•HasKey [check if an object contains a key]
•Clone [create a shallow copy of an object]

[Basic Object: properties:]
•base [contains key information about an object instance]

[additional methods:]
•__Init ['should not be used by the script', handles when an object instance is created]
•__New [handles when an object instance is created]
•__Get / __Set [handle when a non-existent key/property is gotten/set]
•__Call [handles when a non-existent method is called]
•__Delete [handles when an object instance is deleted]

[additional methods (further):]
•Next [used for looping through object keys]
•ToString [the AHK v2 String function checks an object for a 'ToString' method]

[additional properties:]
•__Class [the class name of an object instance]

- For more details see:
Basic Object - Methods & Properties | AutoHotkey
https://autohotkey.com/docs/objects/Object.htm

SOME FURTHER NOTES

- Key names can be of integer type (positive/0/negative) or string type.
- Key names must be unique.
- When integers beyond a certain size are used, these key names are stored as string names.
- Key names are case-insensitive. An alternative is to use a Scripting.Dictionary object or a custom __Call method which can take actions based on the case of the key name passed to it.
- Values can be strings/integers/floats/object references/function references. (The AutoHotkey basic object is very flexible, there are no type restrictions on what the values can be.)
- When a for loop is done on an object, integer keys (and their values) are returned in numeric order, then string keys in alphabetical order. (Thus to treat numbers as strings, using a prefix character can be useful.)

- Warning: At present, in AHK v2, you cannot have both an integer key called '1', and a string key called '1'.
- Warning: Creating a key with the same name as a method/property, can block access to that method/property. At present, if you create a key called 'HasKey', this interferes with the HasKey method.

- For more information on key names:
Objects - Definition & Usage | AutoHotkey
https://autohotkey.com/docs/Objects.htm#keys

==================================================

> AUTOHOTKEY OBJECT FUNCTIONS

- For reference here is a list of functions relating to objects.
- (The list is intended to be complete, but a complete list is debatable.)

FUNCTIONS WITH A METHOD EQUIVALENT

- Note: these functions are used to bypass any custom class behaviour, where a method has been overridden, and instead use the default behaviour.

[Basic Object: methods for integer keys only (7):]
•ObjInsertAt / ObjRemoveAt
•ObjPush / ObjPop
•ObjMinIndex / ObjMaxIndex / ObjLength

[Basic Object: methods for all keys (8):]
•ObjDelete
•ObjCount
•ObjSetCapacity / ObjGetCapacity
•ObjGetAddress
•ObjNewEnum [note: no underscore cf. '_NewEnum']
•ObjHasKey
•ObjClone

FURTHER FUNCTIONS

•Array / Object [create an array, equivalent to obj := [] and obj := {} respectively]
•ComObjActive / ComObjArray / ComObjConnect / ComObjCreate / ComObject / ComObjError / ComObjFlags / ComObjGet / ComObjQuery / ComObjType / ComObjValue [COM objects are not discussed in this tutorial]
•IsFunc [check if a function with a particular name exists]
•IsObject [check if a variable is an object]
•ObjAddRef / ObjRelease [modify an object's reference count (use both to retrieve it)]
•ObjBindMethod [create a BoundFunc object for an object method]
•ObjGetBase / ObjSetBase [get/set an object's base object]
•ObjRawGet / ObjRawSet [get/set a key's value, bypass any custom class behaviour]
•String [converts variables to strings, will check an object for a 'ToString' method]
•StrSplit [create an array by splitting a string based on delimiters]
•Type [get a variable's type]

==================================================

> META-FUNCTIONS

- The following 3 methods are known as meta-functions.
•__Get / __Set / __Call

Objects - Definition & Usage | AutoHotkey
https://autohotkey.com/docs/Objects.htm
Meta-functions define what happens when a key is requested but not found within the target object. For example, if obj.key has not been assigned a value, it invokes the __Get meta-function. Similarly, obj.key := value invokes __Set and obj.key() invokes __Call. These meta-functions (or methods) would need to be defined in obj.base, obj.base.base or such.
- The following 3 methods are also sometimes referred to as meta-functions.
•__Init / __New
•__Delete

Objects - Definition & Usage | AutoHotkey
https://autohotkey.com/docs/Objects.htm
To run code when the last reference to an object is being released, implement the __Delete meta-function.
From the AHK v1.1.28.00 source code:
LPTSTR Object::sMetaFuncName[] = { _T("__Get"), _T("__Set"), _T("__Call"), _T("__Delete"), _T("__New") };
- The term 'meta-function' is not particularly important, and all 6 shall be referred to as methods throughout this document.

Here are some examples of the 5 methods (6 minus __Init) in use:
I.e. actions that trigger the methods:
__New: obj := new MyClass
__Get: var := obj.key ;__Get is called only if 'key' doesn't exist
__Set: obj.key := "value" ;__Set is called only if 'key' doesn't exist
__Call: obj.Method() ;__Call is invoked whenever an existent/non-existent method is called (it is intended to handle non-existent methods)
__Delete: obj := ""

==================================================

> A ZOO CLASS AND BASE OBJECTS

- Here is an example custom class, with multiple keys, methods and properties. So that you can see what one looks like.
- After defining the class we use for loops to list the contents of 4 objects (2 pairs):
An instance object (an individual object created from the class blueprint), and its base object.
The class object (the class blueprint is stored as an object), and its base object.
- Every object has a base object, base objects store contents that affect how an object works.
- (We can say that conceptually AHK basic objects have a standard base object, but that when listing keys they have no base object.)

Code: Select all

class MyZooClass
{
	MyKey := "MyKeyValue"

	static MyStaticKey := "MyStaticKeyValue"

	MyMethod()
	{
		return "MyMethodValue"
	}

	MyProperty
	{
		get
		{
			return "MyPropertyValue"
		}
		set
		{
		}
	}

	__New()
	{
	}
	__Get()
	{
	}
	__Set()
	{
	}
	__Call()
	{
	}
	__Delete()
	{
	}
}

obj := new MyZooClass

vOutput := ""

vOutput .= "instance object:`r`n"
for vKey, vValue in obj
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

vOutput .= "instance object base:`r`n"
for vKey, vValue in obj.base
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

vOutput .= "class object:`r`n"
for vKey, vValue in MyZooClass
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

vOutput .= "class object base:`r`n"
for vKey, vValue in MyZooClass.base
	vOutput .= vKey " " vValue "`r`n"
vOutput .= "`r`n"

Clipboard := vOutput
MsgBox, % vOutput

MsgBox, % IsObject(obj) " " IsObject(obj.base)
MsgBox, % IsObject(MyZooClass) " " IsObject(MyZooClass.base)

/*
;some additional tests to retrieve types (Type is AHK v2-only)

MsgBox(Type(MyZooClass.MyKey)) ;String
MsgBox(Type(MyZooClass.MyProperty)) ;String
MsgBox(Type(MyZooClass.MyMethod)) ;Func

oKey := ObjRawGet(MyZooClass, "MyKey")
oProp := ObjRawGet(MyZooClass, "MyProperty")
oMeth := ObjRawGet(MyZooClass, "MyMethod")
MsgBox(Type(oKey)) ;String
MsgBox(Type(oProp)) ;Property
MsgBox(Type(oMeth)) ;Func
*/
- Here are the results of listing the contents of the 4 objects:

Code: Select all

;instance object:
;MyKey MyKeyValue

;instance object base:
;__Call
;__Class MyZooClass [note: created automatically, we didn't define this]
;__Delete
;__Get
;__Init [note: created automatically, we didn't define this]
;__New
;__Set
;MyMethod
;MyProperty
;MyStaticKey MyStaticKeyValue

;[identical to the contents of the instance object base]
;class object:
;__Call
;__Class MyZooClass [note: created automatically, we didn't define this]
;__Delete
;__Get
;__Init [note: created automatically, we didn't define this]
;__New
;__Set
;MyMethod
;MyProperty
;MyStaticKey MyStaticKeyValue

;class object base:
;(empty, it has no base object)
- What we learn from inspecting the contents of the objects is this:
- When an instance object is created, based on a class object, the instance object's base object is a clone of the class object.
- Although we defined 'MyKey' in the class object, we don't see it listed in the class object's contents. ('MyKey' is created via the __Init method.)
- An '__Init' method was listed, even though we didn't specify one. The __Init method creates 'MyKey'. If we remove the line MyKey := "MyKeyValue" from the class definition, then no __Init method will appear in the contents list.
- A '__Class' key is created, corresponding to the class name specified at the start of the class definition.

- Ordinarily, custom classes do not have base objects.
- However, instance objects do have base objects, which are references to the class objects that they're derived from.
- Be careful when editing a base object, it will affect existing and future instance objects that share that base object.

- One minor point: the availability of the 'base' keyword in class definitions:

Code: Select all

obj := new MyClass

obj.key := 1
MyClass.base := {key:2}
MyClass.key := 3
obj.MyMethod()

obj.base.key := 4
obj.MyMethod()

class MyClass
{
	MyMethod()
	{
		MsgBox, % this.key ;1
		MsgBox, % base.key ;2
		MsgBox, % MyClass.base.key ;2
		MsgBox, % this.base.key ;3 then 4
		MsgBox, % MyClass.key ;3 then 4
	}
}
- This chapter has hinted at virtually everything that we will look in the following chapters.
- 3 other things that will be mentioned are: superclasses/subclasses (parent/child classes, extending classes), nested classes (classes within classes), and enumerator objects.
- Superclasses/subclasses and nested classes will be introduced in the next two chapters.
- After that we will look more closely at specific aspects of custom classes.
- At the very end we will consider enumerator objects. This is because issues relating to enumerators are somewhat separate from those applying to classes generally.

==================================================

> SUPERCLASSES/SUBCLASSES

- When an instance of a superclass is created, the subclass is irrelevant.
- When an instance of a subclass is created, if a key/method/property does not exist in the subclass, the superclass is checked.
- E.g. in an instance of a subclass, if the superclass and subclass each have a method with the same name, the subclass method takes precedence.

Code: Select all

class MySuperClass
{
	Method1()
	{
		return A_ThisFunc
	}
	Method2()
	{
		return A_ThisFunc
	}
}

class MySubClass extends MySuperClass
{
	Method2()
	{
		return A_ThisFunc
	}
	Method3()
	{
		return A_ThisFunc
	}
}

oArray1 := new MySuperClass
oArray2 := new MySubClass
vOutput := oArray1.__Class ;MySuperClass
. "`r`n" oArray1.base.__Class ;MySuperClass
. "`r`n" oArray1.base.base.__Class ;(blank)
. "`r`n" oArray1.Method1() ;MySuperClass.Method1
. "`r`n" oArray1.Method2() ;MySuperClass.Method2
. "`r`n" oArray1.Method3() ;(blank)
. "`r`n" "==============="
. "`r`n" oArray2.__Class ;MySubClass
. "`r`n" oArray2.base.__Class ;MySubClass
. "`r`n" oArray2.base.base.__Class ;MySuperClass
. "`r`n" oArray2.Method1() ;MySuperClass.Method1
. "`r`n" oArray2.Method2() ;MySubClass.Method2
. "`r`n" oArray2.Method3() ;MySubClass.Method3
Clipboard := vOutput
MsgBox, % vOutput

;oArray2 can access both versions of 'Method2'
MsgBox, % oArray2.Method2() ;MySubClass.Method2
MsgBox, % oArray2.base.base.Method2() ;MySuperClass.Method2

;output:
;MySuperClass
;MySuperClass
;
;MySuperClass.Method1
;MySuperClass.Method2
;
;===============
;MySubClass
;MySubClass
;MySuperClass [base of base is superclass]
;MySuperClass.Method1
;MySubClass.Method2 [subclass Method2 takes precedence over superclass Method2]
;MySubClass.Method3
- When a subclass extends a superclass, the superclass is the base object of the subclass.
- E.g. here C.base is B, C.base.base is A.

Code: Select all

obj := new C
MsgBox, % obj.__Class ;C ;(this points to obj.base.__Class)
MsgBox, % obj.base.__Class ;C
MsgBox, % obj.base.base.__Class ;B
MsgBox, % obj.base.base.base.__Class ;A

MsgBox, % A.base.__Class ;(blank)
MsgBox, % B.base.__Class ;A
MsgBox, % C.base.__Class ;B

class A
{
}
class B extends A
{
}
class C extends B
{
}
==================================================

> NESTED CLASSES

- A nested class is accessible from the class that contains it, but is not listed amongst its contents.

Code: Select all

class MyOuterClass
{
	class MyInnerClass
	{
	}
}
obj1 := new MyOuterClass
obj2 := new MyOuterClass.MyInnerClass
;obj3 := new MyInnerClass ;causes error in AHK v2

;MyInnerClass is not listed in obj1's contents
vOutput := ""
for vKey, vValue in obj1
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

vOutput := ""
for vKey, vValue in obj2
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;however, obj1.MyInnerClass is listed as an object
MsgBox, % IsObject(obj1.MyInnerClass)
- This post has a neat comparison of subclassing v. nesting:
Question About Classes - Ask for Help - AutoHotkey Community
https://autohotkey.com/board/topic/8552 ... ntry545206

==================================================

> __CLASS KEY, AND BASE OBJECTS: CHASE THE BASE

- If we define an empty class, it will produce an object identical to a standard AutoHotkey array, with one difference, the class name.
- Any instance object created from a class object will be have a key called '__Class', in its base object (the base object pointing to the class object by default). It's value is the name of the class.
- In the example below we will also demonstrate modifying the value of base's key directly, and assigning an object for the base via 'obj.base := ' and ObjSetBase.
- We will also demonstrate listing the key/value pairs of an object via a for loop.

Code: Select all

class MyEmptyClass
{
}

obj1 := {} ;standard AutoHotkey associative array
obj2 := new MyEmptyClass
obj3 := new MyEmptyClass() ;equivalent to the line above

MsgBox, % obj1.__Class ;(blank)
MsgBox, % obj2.__Class ;MyEmptyClass
MsgBox, % obj3.__Class ;MyEmptyClass

MsgBox, % obj1.base.__Class ;(blank)
MsgBox, % obj2.base.__Class ;MyEmptyClass
MsgBox, % obj3.base.__Class ;MyEmptyClass

;obj1 does not contain a __Class key
vOutput := ""
for vKey, vValue in obj1
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;obj1's base does not contain a __Class key
vOutput := ""
for vKey, vValue in obj1.base
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;obj2 does not contain a __Class key
vOutput := ""
for vKey, vValue in obj2
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)

;obj2's base *does* contain a __Class key
vOutput := ""
for vKey, vValue in obj2.base
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;__Class MyEmptyClass

MsgBox, % IsObject(obj1.base) ;0 ;reports that the AHK basic object has no base object
MsgBox, % IsObject(obj2.base) ;1

;change the objects' class names
obj1.base.__Class := "NewClassName1" ;doesn't work
obj2.base.__Class := "NewClassName2"
MsgBox, % obj1.__Class ;(blank)
MsgBox, % obj2.__Class ;NewClassName2

obj1.base := {__Class:"NewClassName3"}
obj2.base := {__Class:"NewClassName4"}
MsgBox, % obj1.__Class ;NewClassName3
MsgBox, % obj2.__Class ;NewClassName4

ObjSetBase(obj1, {__Class:"NewClassName5"})
ObjSetBase(obj2, {__Class:"NewClassName6"})
MsgBox, % obj1.__Class ;NewClassName5
MsgBox, % obj2.__Class ;NewClassName6
- From the tests above we saw that obj2.base has a key called '__Class', but obj2 doesn't have such a key.
- So, that 'obj2.base.__Class' returns a value, is unsurprising.
- However, that 'obj2.__Class' returns a value, might be surprising, since it does not have a key called '__Class'.
- In general, for objects, if obj.MyKey doesn't exist, a check is done for obj.base.MyKey and obj.base.base.MyKey etc. (You could call this to 'chase the base'.)
- If such a key is found, the value of the key is returned.
- If no key is found, a blank string is returned.

- Note: When a class is defined, a variable with the same name as the class is created that contains the object. Viewable via ListVars or 'View, Variables and their contents'.
- So you can have multiple instance objects for a class, but also one instance object which defines the class.

Code: Select all

class MyClass
{
}

;create an instance object
obj := new MyClass

;refer to an instance object
MsgBox, % IsObject(obj) ;1

;refer to the class object
MsgBox, % IsObject(MyClass) ;1

;get the key name
MsgBox, % obj.__Class ;MyClass
MsgBox, % MyClass.__Class ;MyClass
MsgBox, % obj.base.__Class ;MyClass
MsgBox, % MyClass.base.__Class ;(blank)
- Note: we learnt in the 'A ZOO CLASS AND BASE OBJECTS' chapter, that class objects have a standard base object.

- Some further tests comparing the base object and a custom class:

Code: Select all

class MyClass
{
}

obj := new MyClass
MsgBox, % obj.HasKey("base") ;0
MsgBox, % IsObject(obj.base) ;1
MsgBox, % obj.base.__Class ;MyClass

;for comparison, a standard AHK array
oArray := {}
MsgBox, % obj.HasKey("base") ;0
MsgBox, % IsObject(obj.base) ;0
MsgBox, % obj.base.__Class ;(blank)
==================================================

> BASE OBJECTS: REPLACE THE BASE / MODIFY AN AHK ARRAY

- Here we try some tests regarding setting the base object.

Code: Select all

class MyClass
{
}

vOutput := ""

obj := new MyClass
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := new MyClass
obj.base := {__Class:"MyClassNew"}
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := new MyClass
ObjSetBase(obj, {__Class:"MyClassNew"})
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

obj := new MyClass
ObjRawSet(obj, "base", {__Class:"MyClassNew"})
vOutput .= obj.HasKey("base") " " obj.__Class " " obj.base.__Class "`r`n"

Clipboard := vOutput
MsgBox, % vOutput

;results:
;0 MyClass MyClass [base unchanged]
;0 MyClassNew MyClassNew
;0 MyClassNew MyClassNew
;1 MyClass MyClassNew
- What the examples show is this:
- If you create a key called 'base', this overrides the 'base' property.
- So 'obj.base.__Class' gets the contents from the key called 'base'.
- However, when 'obj.__Class' fails to find a key called '__Class', it checks the base object, it does not check the key called 'base'. So in this respect, creating the key 'base' has not overridden the property 'base'.

- Some further examples re. modifying an AHK array.

Code: Select all

;modify an AHK array

;edit the base object
oArray.base := {}
oArray.base.__Class := "hello"
vOutput := ""
for vKey, vValue in oArray.base
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput

;==============================

;add a __Get property, and thus change the default return value
oArray := {}
MsgBox, % oArray.1 ;(blank)
oArray.base := {"__Get":"MyFuncReturnHello"}
MsgBox, % oArray.1 ;hello

MyFuncReturnHello()
{
	return "hello"
}

;==============================

;temporarily add a __Set method, and thus prevent new keys being created
oArray := {}
oArray.1 := "a"
oArray.base := {"__Set":"MyFuncObjPreventSet"}
oArray.2 := "b"
oArray.base.Delete("__Set")
oArray.3 := "c"
MsgBox, % oArray.1 "_" oArray.2 "_" oArray.3

MyFuncObjPreventSet()
{
	MsgBox, % A_ThisFunc
	return
}

;==============================

;some failed fixes instead of 'oArray.base.Delete("__Set")'
;that did not restore the ability to create new keys
oArray.Delete("base")
oArray.base := {"__Set":""}
oArray.base := {}
oArray.base := ""
- Some points re. changing the base.
Classes in AHK, a Dissection (Advanced) - Page 3 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 441#p55441
Classes in AHK, a Dissection (Advanced) - Page 3 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 477#p55477

==================================================

[SECTION 2: KEYS/PROPERTIES/METHODS]

> CLASSES: KEYS

- Here are some classic things to do with keys and classes, which will be covered.
- Create an object pre-populated with some keys.
- ... Change those keys for future instances but leave existing instances unchanged.
- ... Change those keys for future instances *and* existing instances.
- Log each time an object is created from a particular class.

PRE-POPULATE AN OBJECT WITH KEYS

- An example of defining keys within the class body. With/without using 'static'.
- When 'static' is used, the key is placed into the base object.
- When 'static' is omitted, the key is placed into the object proper.

Code: Select all

class MyKeyClass
{
	static key1 := "value1"
	key2 := "value2"
}

obj := new MyKeyClass
MsgBox, % obj.key1 ;value1 ;value retrieved from base object
MsgBox, % obj.key2 ;value2
MsgBox, % obj.base.key1 ;value1
MsgBox, % obj.base.key2 ;(blank)
obj.Delete("key1") ;does nothing, 'key1' exists in the base object, but not the object proper
obj.Delete("key2") ;key2 is deleted
MsgBox, % obj.key1 ;value1
MsgBox, % obj.key2 ;(blank)

obj.base.Delete("key1") ;key1 is deleted
MsgBox, % obj.base.key1 ;(blank)
vOutput := ""
for vKey, vValue in obj.base
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput

;this shows that MyKeyClass.key1 was also deleted
vOutput := ""
for vKey, vValue in MyKeyClass
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
- An attempt is made to delete 'key1', only the object proper is checked.
- An attempt is made to retrieve the contents of 'key1', the object proper is checked, there is no such key, the base object is then checked.

LOG EACH TIME AN OBJECT IS CREATED

- Here is an example of a class that keeps an instance count.
- And gives each object an issue number.
- It uses the __New method, discussed in a later chapter.

Code: Select all

class MyIncrementClass
{
	static issued := 0
	__New()
	{
		this.num := 1+(MyIncrementClass.issued++)
	}
}

;create new instances each with an issue number
obj1 := new MyIncrementClass
obj2 := new MyIncrementClass
obj3 := new MyIncrementClass
MsgBox, % obj1.num ;1
MsgBox, % obj2.num ;2
MsgBox, % obj3.num ;3

;get total number of classes issued
MsgBox, % MyIncrementClass.issued ;3
AN OBJECT WITH KEYS (CHANGE THE KEY CONTENT FOR EXISTING AND FUTURE INSTANCES)

- In this example, the key content is changed for all instances.

Code: Select all

class MyKeyClass
{
	static MyKey := "MyValue"
}
obj1 := new MyKeyClass
obj2 := new MyKeyClass
MsgBox, % obj1.MyKey ;MyValue
MsgBox, % obj2.MyKey ;MyValue
MyKeyClass.MyKey := "MyValueNEW"
MsgBox, % obj1.MyKey ;MyValueNEW
MsgBox, % obj2.MyKey ;MyValueNEW

MsgBox

;note: if we assign to obj1.MyKey this will create obj1.MyKey (and not override MyKeyClass.MyKey aka obj.base.MyKey)
obj1.MyKey := "hello"
MsgBox, % obj1.MyKey ;hello
MsgBox, % obj1.base.MyKey ;MyValueNEW
MsgBox, % obj2.MyKey ;MyValueNEW ;reads obj2.base.MyKey
MsgBox, % MyKeyClass.MyKey ;"MyValueNEW"
- We could use a property to refer to a global variable. Changing the value of the global variable would give us a way to affect future and existing instances.

Code: Select all

global vMyValue := "MyValue"
class MyKeyClass
{
	MyProperty
	{
		get
		{
			global vMyValue
			return vMyValue
		}
		set
		{
		}
	}
}

obj1 := new MyKeyClass
obj2 := new MyKeyClass
MsgBox, % obj1.MyProperty ;MyValue
MsgBox, % obj2.MyProperty ;MyValue
vMyValue := "MyValueNEW"
MsgBox, % obj1.MyProperty ;MyValueNEW
MsgBox, % obj2.MyProperty ;MyValueNEW
- Instead of using a static variable, we could keep a list of all object instances as they are created, and update the values for each key.

Code: Select all

class MyKeepTrackClass
{
	static instances := []
	__New()
	{
		this.MyKey := "MyValue"
		this.instances.Push(&this)
	}
	MyUpdateAllMethod(vValue)
	{
		this.MyKey := vValue
		for _, vAddr in this.instances
		{
			obj := Object(vAddr)
			if IsObject(obj)
				obj.MyKey := vValue
		}
	}
}

;create new instances each with an issue number
obj1 := new MyKeepTrackClass
obj2 := new MyKeepTrackClass
MsgBox, % obj1.MyKey ;MyValue
MsgBox, % obj2.MyKey ;MyValue
obj1.MyUpdateAllMethod("MyValueNEW")
MsgBox, % obj1.MyKey ;MyValueNEW
MsgBox, % obj2.MyKey ;MyValueNEW
- When multiple instances are derived from a class. Modifying the base of one instance affects all instances.
- Here is a way to preserve the features of an instance, but allowing any edits to it's base, to *not* affect other instances.

Code: Select all

class MyKeyClass
{
	static MyKey := "MyValue"
}
obj1 := new MyKeyClass
obj2 := new MyKeyClass
MsgBox, % obj1.MyKey ;MyValue
MsgBox, % obj2.MyKey ;MyValue

MyKeyClass.MyKey := "MyValueNEW"
MsgBox, % obj1.MyKey ;MyValueNEW
MsgBox, % obj2.MyKey ;MyValueNEW

obj2base := ObjGetBase(obj2)
ObjSetBase(obj2, ObjClone(obj2base))

MyKeyClass.MyKey := "MyValueNEWER"
MsgBox, % obj1.MyKey ;MyValueNEWER"
MsgBox, % obj2.MyKey ;MyValueNEW" ;this time the value is unaffected
PRE-POPULATE AN OBJECT WITH KEYS (CHANGE FUTURE INSTANCES BUT NOT EXISTING INSTANCES)
- We can change a key's value for future instances like so:
- Each time an instance is created, it creates a key which is a snapshot of the class's key.

Code: Select all

class MyKeyClass
{
	static MyKey := "MyValue"
	__New()
	{
		this.MyKey := this.base.MyKey
	}
}
obj1 := new MyKeyClass
MyKeyClass.MyKey := "MyValueNEW"
obj2 := new MyKeyClass
MsgBox, % obj1.MyKey
MsgBox, % obj2.MyKey
THE USE OF 'STATIC' WITHIN CLASSES VERSUS ELSEWHERE

- When 'static' is used within a class body, it has a special meaning. It means that a key will be created inside the class object, and in the base of each instance of that class.
- That 'static' key will be accessible, its value can be changed at a later date.
- A static variable in a method or property behaves the same way as a static variable in any function. It can't be accessed via 'obj.key'.
- Note: a method, a property.get, and a property.set, these are all functions and their function names can be displayed by using A_ThisFunc.

==================================================

> CLASSES: PROPERTIES

- Here is a simple class with a property that works almost identically to a key.
- There is one difference, a key is created which stores the value used by the property.
- Note that the special variable 'this' is available in property getters/setters. And in property setters, the special variable 'value' is available.

Code: Select all

class MyPropertyClass
{
	MyProperty
	{
		get
		{
			return this._MyProperty
		}
		set
		{
			this._MyProperty := value
			return value
		}
	}
}
obj := new MyPropertyClass
MsgBox, % obj.MyKey := "value"
MsgBox, % obj.MyKey
MsgBox, % obj.MyProperty := "value"
MsgBox, % obj.MyProperty
- Properties look like keys, and work like methods.
- They allow 'keys' with values that are generated dynamically, and allow for read-only 'keys'.
- A read-only property should define what happens when an attempt it made to set its value, otherwise it would be possible for a key to be created, which had the same name as the property, and that key would take precedence over the property. The key's value would be returned, not the property's.
- Note: it's quite common in programming for keys/methods/properties that are intended for internal use only, and not for external use, for their names to be preceded with an underscore.
- Note: properties make use of functions with names of the form 'class.property.get' and 'class.property.set'.

Code: Select all

class MyPropertyClass
{
	;MyProperty works much like a normal key
	MyProperty
	;MyProperty[] ;equivalent to line above (square brackets are optional)
	{
		get
		{
			;MsgBox, % A_ThisFunc ;MyPropertyClass.MyProperty.get
			return this._MyProperty ;a special variable called 'this' is used, which represents an object instance
		}
		set
		{
			;MsgBox, % A_ThisFunc ;MyPropertyClass.MyProperty.set
			this._MyProperty := value ;a special variable called value is used, e.g. for: 'obj.MyProperty := "hello"', value is equal to 'hello'
		}
	}
	MyReadOnlyProperty
	{
		get
		{
			return "ReadOnlyValue"
		}
		set ;this is needed to make it read-only, otherwise you could create a key called 'MyReadOnlyProperty'
		{
		}
	}
	;a write-only properties would be highly unusual
	MyWriteOnlyProperty
	{
		get
		{
		}
		set
		{
			_MyWriteOnlyProperty := value
		}
	}
	Now
	{
		get
		{
			return A_Now
		}
		set
		{
		}
	}
}

obj := new MyPropertyClass

;MyProperty works just like a key
obj.MyProperty := "MyPropertyValue"
MsgBox, % obj.MyProperty ;MyPropertyValue
obj.MyKey := "MyKeyValue"
MsgBox, % obj.MyKey ;MyKeyValue

;however, MyProperty also uses a key called '_MyProperty' to store its value
MsgBox, % obj._MyProperty ;MyPropertyValue

obj.MyReadOnlyProperty := "new value"
MsgBox, % obj.MyReadOnlyProperty ;read-only

MsgBox, % obj.Now
Sleep, 1000
MsgBox, % obj.Now

;obj's contains various keys
vOutput := ""
for vKey, vValue in obj
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
;output:
;_MyProperty MyPropertyValue
;MyKey MyKeyValue

;obj's base contains various properties
vOutput := ""
for vKey, vValue in ObjGetBase(obj)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
;output:
;__Class MyPropertyClass
;MyProperty
;MyReadOnlyProperty
;MyWriteOnlyProperty
;Now
Here is an example for getting information about properties.

Code: Select all

;AHK v2
;using AHK v2 to use the Type function
class MyPropertyClass
{
	MyProperty
	{
		get
		{
			;MsgBox, % A_ThisFunc
			return "MyPropertyValue"
		}
		set
		{
			;MsgBox, % A_ThisFunc
		}
	}
}

;copy a property from a class to an array

;some tests:
MsgBox(IsFunc("MyPropertyClass.MyProperty")) ;0
MsgBox(IsObject(MyPropertyClass.MyProperty)) ;0
MsgBox(Type(MyPropertyClass.MyProperty)) ;String ;note: the return value is a string
oProp := ObjRawGet(MyPropertyClass, "MyProperty")

MsgBox(IsFunc("oProp")) ;0
MsgBox(IsObject(oProp)) ;1
MsgBox(Type(oProp)) ;Property

MsgBox(IsFunc("MyPropertyClass.MyProperty.get")) ;2
;MsgBox(IsObject(MyPropertyClass.MyProperty.get)) ;Error:  No object to invoke.
;MsgBox(Type(MyPropertyClass.MyProperty.get)) ;Property

;assign a property to an object
obj := {}
MsgBox(obj.MyProperty) ;(blank) ;as expected
oProp := ObjRawGet(MyPropertyClass, "MyProperty")
ObjRawSet(obj, "MyProperty", oProp)
MsgBox(obj.MyProperty) ;MyPropertyValue

;assign a property to an object
obj := {}
MsgBox(obj.MyProperty) ;(blank) ;as expected
for vKey, vValue in MyPropertyClass
	if (vKey = "MyProperty")
		ObjRawSet(obj, "MyProperty", vValue)
MsgBox(obj.MyProperty) ;MyPropertyValue
==================================================

> CLASSES: METHODS

- Here is a simple class with some methods.
- Methods are essentially functions stored within a class.
- Note: in AutoHotkey, methods are functions with names of the form 'class.method'.
- Note: commented out code demonstrates that methods are objects with type 'Func'.

Code: Select all

class MyMethodClass
{
	MyMethod()
	{
		;MsgBox, % A_ThisFunc ;MyMethodClass.MyMethod
		return "MyMethod"
	}
	Add(num1, num2)
	{
		return num1 + num2
	}
	Concatenate(text1, text2)
	{
		return text1 text2
	}
}
obj := new MyMethodClass
;MsgBox, % IsFunc(obj.MyMethod) ;2 (a func with 1 parameter)
;MsgBox(Type(obj.MyMethod)) ;Func ;AHK v2

MsgBox, % obj.MyMethod()
MsgBox, % obj.Add(1, 2)
MsgBox, % obj.Concatenate("abc", "def")

;calling methods with a dynamic method name (computed method name)
var := "MyMethod"
MsgBox, % obj[var]()
MsgBox, % obj["Add"](1, 2)
MsgBox, % obj["Concatenate"]("abc", "def")
- Here is an example of taking a function, and adding it as a method to an object.

Code: Select all

myobj := {}
myobj.MyKey := "value"

;the function expects 1 parameter, but we pass 0 parameters
;the object is passed implicitly as the 1st parameter
myobj.MyMethod := Func("MyFunc")
myobj.MyMethod()
myobj.MyMethod.Call(myobj) ;equivalent to line above (but with the object passed explicitly)

;the function expects 4 parameters, but we pass 3 parameters
;the object is passed implicitly as the 1st parameter
myobj.MyMethod2 := Func("MyFunc2")
myobj.MyMethod2(1, 2, 3)
myobj.MyMethod2.Call(myobj, 1, 2, 3) ;equivalent to line above (but with the object passed explicitly)

;myobj.MyMethod2(1, 2) ;too few parameters, this silently fails in AHK v1 (but gives 'Error:  Missing a required parameter.' in AHK v2)
;myobj.MyMethod2.Call(myobj, 1, 2) ;too few parameters, this silently fails in AHK v1 (but gives 'Error:  Missing a required parameter.' in AHK v2)

MyFunc(obj)
{
	MsgBox, % obj.MyKey
}

MyFunc2(obj, var1, var2, var3)
{
	MsgBox, % obj.MyKey " " var1 " " var2 " " var3
}
- Some further examples of dynamic method calls.

Code: Select all

oArray := ["a", "b", "c"]
vMethod := "Length"
MsgBox, % oArray.Length() ;3
;MsgBox, % oArray.%vMethod%() ;Error: Ambiguous or invalid use of "."
MsgBox, % oArray[vMethod]() ;3
;MsgBox, % oArray[vMethod] ;(blank)
;MsgBox, % ObjBindMethod(oArray, vMethod).() ;3 ;deprecated syntax
MsgBox, % ObjBindMethod(oArray, vMethod).Call() ;3
oFunc := ObjBindMethod(oArray, vMethod)
;MsgBox, % oFunc.() ;3 ;deprecated syntax
MsgBox, % oFunc.Call() ;3
MsgBox, % %oFunc%() ;3
==================================================

> CLASSES: KEYS/METHODS/PROPERTIES AND PRIORITY

- You cannot have a class with a method and a property that share the same name. You get a 'Duplicate declaration' error.
- If a key and a method both share the same name, the key takes priority.
- If a key and a property both share the same name, the key takes priority.

Code: Select all

class MySameNameClass
{
	MyKeyOrProperty
	{
		get
		{
			return "property"
		}
		set
		{
		}
	}
	MyKeyOrMethod()
	{
		return "method"
	}
}

obj := new MySameNameClass

;test what happens when keys with the same name as the property/method don't exist
MsgBox, % "TEST 0: default behaviour"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(blank)
MsgBox, % obj.MyKeyOrMethod() ;method

;create keys with the same names
;test what happens when keys with the same name do exist
ObjRawSet(obj, "MyKeyOrProperty", "key")
ObjRawSet(obj, "MyKeyOrMethod", "key")
MsgBox, % "TEST 1A"
MsgBox, % obj.MyKeyOrProperty ;key
MsgBox, % obj.MyKeyOrProperty() ;(blank)
MsgBox, % obj.MyKeyOrMethod ;key
MsgBox, % obj.MyKeyOrMethod() ;(blank)

;delete the keys, and the original behaviour is restored
obj.Delete("MyKeyOrProperty")
obj.Delete("MyKeyOrMethod")
MsgBox, % "TEST 1B"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(blank)
MsgBox, % obj.MyKeyOrMethod() ;method

;creating a key called 'base', also blocks the methods/properties
ObjRawSet(obj, "base", "key")
MsgBox, % "TEST 2A"
MsgBox, % obj.MyKeyOrProperty ;(blank)
MsgBox, % obj.MyKeyOrProperty() ;(blank)
MsgBox, % obj.MyKeyOrMethod ;(blank)
MsgBox, % obj.MyKeyOrMethod() ;(blank)

;delete the key, and the original behaviour is restored
obj.Delete("base")
MsgBox, % "TEST 2B"
MsgBox, % obj.MyKeyOrProperty ;property
MsgBox, % obj.MyKeyOrProperty() ;property
MsgBox, % obj.MyKeyOrMethod ;(blank)
MsgBox, % obj.MyKeyOrMethod() ;method

;creating a key called 'base', also blocks the methods/properties
obj.base := "key"
MsgBox, % "TEST 3A"
MsgBox, % obj.MyKeyOrProperty ;(blank)
MsgBox, % obj.MyKeyOrProperty() ;(blank)
MsgBox, % obj.MyKeyOrMethod ;(blank)
MsgBox, % obj.MyKeyOrMethod() ;(blank)

;attempt to delete the key, but the original behaviour is not restored
obj.Delete("base")
MsgBox, % "TEST 3B"
MsgBox, % obj.MyKeyOrProperty ;(blank)
MsgBox, % obj.MyKeyOrProperty() ;(blank)
MsgBox, % obj.MyKeyOrMethod ;(blank)
MsgBox, % obj.MyKeyOrMethod() ;(blank)

;setting the base to the class object restores the original behaviour
obj.base := MySameNameClass
MsgBox, % "TEST 3C"
MsgBox, % obj.MyKeyOrProperty ;(blank)
MsgBox, % obj.MyKeyOrProperty() ;(blank)
MsgBox, % obj.MyKeyOrMethod ;(blank)
MsgBox, % obj.MyKeyOrMethod() ;(blank)
- Here is an example of 'replacing a method with itself'.
- Adding a key with the same name as a method, can prevent the method from working.
- However, if that added key is a func object, which acts like the method, then, even though you've added a key, calling the method works like before.

Code: Select all

obj := ["a", "b", "c"]
;MsgBox, % obj.Length() ;3
;MsgBox, % obj.Length ;(blank)

;if we create a key called 'Length', the method fails
obj.Length := "value"
;MsgBox, % obj.Length() ;(blank)
;MsgBox, % obj.Length ;value

;if we delete the key called 'Length', the method works again
obj.Delete("Length")
;MsgBox, % obj.Length() ;3
;MsgBox, % obj.Length ;(blank)

;again, if we create a key called 'Length', the method fails
obj.Length := "value"

;if we create a func object based on ObjLength,
;and assign that to the key called 'Length',
;that is another way to get a working method again
obj.Length := Func("ObjLength")
MsgBox, % obj.Length() ;3
MsgBox, % obj.Length ;(blank)
==================================================

[SECTION 3: META-FUNCTIONS]

> META-FUNCTIONS

- By this definition, __Call/__Get/__Set are meta-functions.
Objects - Definition & Usage | AutoHotkey
https://autohotkey.com/docs/Objects.htm
Meta-functions define what happens when a key is requested but not found within the target object.
- Although it is true that __Call is intended to handle calls to non-existent methods. The __Call function is also invoked when calls are made to methods that do exist.

- According to other sources, __Delete/__Init/__New are also meta-functions.
- We will refer to these in chapter headings as 'meta-functions+' (with a plus sign).
list of every object type/property/method - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 37&t=44081

- Which of these 6 methods is or isn't a 'meta-function' isn't especially important.
- The important thing to know is that they are all methods, and that they are generally used internally by the class, but are not normally referred to directly within a script.
- Creating custom methods called __Call/__Get/__Set or __Delete/__New gives you more control over your class.
- Note: __Init is undocumented and should not be used.

The 6 methods were introduced earlier, here is the same text agin.
6 methods: __Init/__New/__Get/__Set/__Call/__Delete.
- The following 6 methods are key to customisation when creating custom classes.
•__Init ['should not be used by the script', handles when an object instance is created]
•__New [handles when an object instance is created]
•__Get / __Set [handle when a non-existent key/property is gotten/set]
•__Call [handles when a non-existent method is called (and is invoked even when a method does exist)]
•__Delete [handles when an object instance is deleted]
- Note: __Get/__Set are only invoked when certain things don't exist.
- If you want to log each time a key or property is gotten or set, there are workarounds discussed below.
- (__Call can be used to log each time a method is called.)

- Here is an example script, where a MsgBox reports each time a method is invoked.

Code: Select all

class MyNotifyClass
{
	__Init() ;new object created
	{
		MsgBox, % A_ThisFunc
	}
	__New() ;new object created
	{
		MsgBox, % A_ThisFunc
	}
	__Get() ;attempt to retrieve value of a key that doesn't exist
	{
		MsgBox, % A_ThisFunc
	}
	__Set() ;attempt to set value of a key that doesn't exist
	{
		MsgBox, % A_ThisFunc
	}
	__Call() ;attempt to use a method that doesn't exist
	{
		MsgBox, % A_ThisFunc
	}
	__Delete() ;object is deleted
	{
		MsgBox, % A_ThisFunc
	}
}

obj := new MyNotifyClass ;__Init and __New
var := obj.key ;__Get (if key doesn't exist)
obj.key := "value" ;__Set (if key doesn't exist)
obj.method() ;__Call (if method doesn't exist)
obj := "" ;__Delete
- Note: A line like 'obj := ""', will sometimes delete an object, sometimes it won't.
- If prior to 'obj := ""', the object pointed to by 'obj' has a reference count of 1, the object is deleted. The __Delete method is invoked.
- If prior to 'obj := ""', the object pointed to by 'obj' has a reference count greater than 1, the object's reference count is decreased by 1.

Code: Select all

obj1 := {}
obj2 := obj1
obj3 := obj1
;ObjAddRef increases the reference count by 1
;ObjRelease decreases the reference count by 1 and returns the current count
MsgBox, % Format("{2}", ObjAddRef(&obj1), ObjRelease(&obj1)) ;3
obj3 := ""
MsgBox, % Format("{2}", ObjAddRef(&obj1), ObjRelease(&obj1)) ;2
obj2 := ""
MsgBox, % Format("{2}", ObjAddRef(&obj1), ObjRelease(&obj1)) ;1
obj1 := "" ;object deleted
- Return:
- Some subtleties of using 'return' in one of the 6 methods.
- Here are 3 possible uses of 'return':
- end a function prematurely
- end a function prematurely, and prevent certain unseen actions from occurring
- output something
- For each of the 6 'meta-functions', the possible effects of 'return' will be discussed.

- Parameter counts (min/max):
__New() [min: 0, max: unlimited][parameters e.g. 2 parameters: obj := new MyClass(param1, param2)]
__Get() [min: 0, max: unlimited][key/subkeys (key to get from) e.g. 2 parameters: var := obj.key1.key2]
__Set() [min: 0, max: unlimited][key/subkeys, new value (key to set) e.g. 3 parameters: obj.key1.key2 := "value"]
__Call() [min: 0, max: unlimited][method name, parameters e.g. 2 parameters var := obj.MyMethod(param1, param2)]
__Delete() [min: 0, max: unlimited]

- Note: 'meta-functions', and methods generally, are implemented as normal functions, but each function is passed the instance object implicitly (when defined in classes or called as methods directly).
- When you define a method inside a class, you do not specify a parameter for the instance object.
- Any normal function, intended as a method, should have 1 additional parameter, the first parameter: to receive the instance object.

Code: Select all

MyFunc(obj, params*)
{
	MsgBox, % A_ThisFunc
}

class MyClass
{
	;'obj' param not needed, use 'this' if needed, an implicit parameter
	__Delete(params*)
	{
		MsgBox, % A_ThisFunc
	}
}

fn := Func("MyFunc")
obj1 := new MyClass
obj2 := {}, obj2.base := {__Delete:fn}
obj1 := ""
obj2 := ""
- The example above demonstrates two ways to define a class, function syntax (old school) and method syntax (new school).
- Also demonstrated at this link:
Objects - Definition & Usage | AutoHotkey
https://autohotkey.com/docs/Objects.htm#Meta_Functions

- Also, any normal function, intended as a method, should have one required parameter, and the other parameters should all be optional. Otherwise a call could fail (silently fail in AHK v1).
- An example demonstrating that any custom function used as a __Delete method should have 1 required parameter, and the rest optional.

Code: Select all

fn1 := Func("MyFunc1")
fn2 := Func("MyFunc2")
obj1 := {}, obj1.base := {__Delete:fn1}
obj2 := {}, obj2.base := {__Delete:fn2}
obj1 := ""
obj2 := "" ;Error:  Missing a required parameter. ;(AHK v2 error)

MyFunc1(obj, params*)
{
	MsgBox, % A_ThisFunc
}

MyFunc2(obj, param1, params*)
{
	MsgBox, % A_ThisFunc
}
==================================================

> META-FUNCTIONS+: __NEW / __DELETE

- The __New method allows an action to be taken when an object is created.
- The __Delete method allows an action to be taken when an object is deleted.

Code: Select all

global g_CountObjCurrent := 0
global g_CountObjEver := 0
class MyCountClass
{
	__New()
	{
		g_CountObjCurrent++
		g_CountObjEver++
	}
	__Delete()
	{
		g_CountObjCurrent--
	}
}

MsgBox, % g_CountObjCurrent " " g_CountObjEver ;0 0
obj1 := new MyCountClass
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;1 1
obj2 := new MyCountClass
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;2 2
obj3 := new MyCountClass
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;3 3
obj1 := ""
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;2 3
obj2 := ""
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;1 3
obj3 := ""
MsgBox, % g_CountObjCurrent " " g_CountObjEver ;0 3
- The __Delete method can be used as a hack that allow you to perform certain actions when an object is deleted.

Code: Select all

class MyTempSetSCSClass
{
	__New(vMode)
	{
		this.orig := A_StringCaseSense
		StringCaseSense, % vMode
	}
	__Delete()
	{
		StringCaseSense, % this.orig
	}
}

;we write 3 functions that remove capital a's

;this function sets StringCaseSense to On, but doesn't restore it to its previous state
Func1(vText)
{
	StringCaseSense, On
	return StrReplace(vText, "A")
}

;this function sets StringCaseSense to On, and then restores it to its previous state
Func2(vText)
{
	vSCS := A_StringCaseSense
	StringCaseSense, On
	vRet := StrReplace(vText, "A")
	StringCaseSense, % vSCS
	return vRet
}

;like Func2, this function sets StringCaseSense to On, and then restores it to its previous state
;when return is done, obj is deleted, and the state is reset
Func3(vText)
{
	obj := new MyTempSetSCSClass("On")
	return StrReplace(vText, "A")
}

vOutput := ""

StringCaseSense, Off
vOutput .= Func1("Aa") " " A_StringCaseSense "`r`n"

StringCaseSense, Off
vOutput .= Func2("Aa") " " A_StringCaseSense "`r`n"

StringCaseSense, Off
vOutput .= Func3("Aa") " " A_StringCaseSense "`r`n"

;we see that each function removed capital a's
;and that Func1 failed to reset A_StringCaseSense
MsgBox, % vOutput
- Note: if you use 'return' in a __Delete method, the object will still be deleted.
- Note: if you use 'return' in a __New method, that will override what object is created. You could even return a string instead of an object.

Code: Select all

class MyNewReturnClass1
{
	__New()
	{
		return ["a", "b", "c"]
	}
}
class MyNewReturnClass2
{
	__New()
	{
		return "hello"
	}
}
class MyNewReturnClass3
{
	__New()
	{
		return this
	}
}
class MyNewReturnClass4 ;equivalent to MyNewReturnClass3 (apart from the class name)
{
	__New()
	{
	}
}
obj1 := new MyNewReturnClass1
obj2 := new MyNewReturnClass2
obj3 := new MyNewReturnClass3
obj4 := new MyNewReturnClass4
MsgBox, % obj1.Length() ;3
MsgBox, % obj2 ;hello
MsgBox, % obj3.__Class
MsgBox, % obj4.__Class
- Two functionally equivalent classes that create a key when the object is created.
- One specifies the assignment in the main body of the class, the other within the __New method.

Code: Select all

class MyClass1
{
	key := "value"
}
class MyClass2
{
	__New()
	{
		this.key := "value"
	}
}
obj1 := new MyClass1
obj2 := new MyClass2
MsgBox, % obj1.key
MsgBox, % obj2.key
- An example of creating objects, demonstrating specifying parameters, and specifying a class name.

Code: Select all

class MyClass1
{
	__New()
	{
		MsgBox, % A_ThisFunc
	}
}
class MyClass2
{
	__New(vText1, vText2)
	{
		MsgBox, % A_ThisFunc "`r`n" vText1 " " vText2
	}
}

obj := new MyClass1
obj := new MyClass2("a", "b")
class1 := "MyClass1"
class2 := "MyClass2"
obj := new %class1%
obj := new %class2%("a", "b")
==================================================

> META-FUNCTIONS+: __INIT

- __Init is an undocumented method that is called when a script starts.
- Here are some experiments with __Init that demonstrate some of its features.

- __Init adds keys to the instance object.

Code: Select all

;this gives a 'Duplicate declaration' error
class MyInitClass
{
	var := 1
	__Init()
	{
	}
}
- __Init does not add keys to the base object.

Code: Select all

;this does not give a 'Duplicate declaration' error
class MyInitClass
{
	static var := 1
	__Init()
	{
	}
}
- __Init is called before __New.

Code: Select all

;__Init is called before __New
class MyInitAndNewClass
{
	__Init()
	{
		MsgBox, % A_ThisFunc
	}
	__New()
	{
		MsgBox, % A_ThisFunc
	}
}
obj := new MyInitAndNewClass
- Defining a variable in the class body causes an __Init method to be present.

Code: Select all

;defining a variable in the class body
class MyKeyNoClass
{
}
class MyKeyYesClass
{
	key := 1
}
class MyKeyStaticClass
{
	static key := 1
}

obj1 := new MyKeyNoClass
obj2 := new MyKeyYesClass
obj3 := new MyKeyStaticClass

;test MyKeyNoClass:
vOutput := ""
for vKey, vValue in ObjGetBase(obj1)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
;output:
;__Class MyKeyNoClass

;test MyKeyYesClass:
;a MyKeyYesClass instance has an __Init class
vOutput := ""
for vKey, vValue in ObjGetBase(obj2)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)
;output:
;__Class MyKeyYesClass
;__Init

;test MyKeyStaticClass:
;a MyKeyStaticClass instance does not have an __Init class
vOutput := ""
for vKey, vValue in ObjGetBase(obj3)
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput ;(blank)
;output:
;__Class MyKeyStaticClass
;key 1
- Tests re. the __Class key.

Code: Select all

;note:
;class MyClass
;{
;}
;effectively does this:
;class MyClass
;{
;	static __Class := "MyClass"
;}

class MyAssignClassClass
{
	;static __Class := "hello" ;error: Duplicate declaration
	;__Class := "hello" ;error: Duplicate declaration
}
- This example gives an indication of what __Init does.
- Note: When you use Call, you must specify the object explicitly, the object is passed implicitly in various other contexts.
- An object is passed to the __Init method, a key is assigned to that object.

Code: Select all

class MyClass
{
	key := "value"
}

MyClass.__Init.Call(obj:={})
MsgBox, % obj.key
- Using 'return' appears to have no effect on __Init.
- The expected key is created.

Code: Select all

class MyClass
{
	__Init()
	{
		this.key := "value"
		return
	}
}
obj := new MyClass
MsgBox, % obj.key

MyClass.__Init.Call(obj:={})
;MyClass.__Init.(obj:={}) ;deprecated syntax
MsgBox, % obj.key
==================================================

> META-FUNCTIONS: __CALL

- __Call is a method that is called when the script tries to execute a non-existent (or existent) method.

Code: Select all

class MyCallClass
{
	__Call(vMethod)
	{
		MsgBox, % "attempt to call non-existent method:`r`n" vMethod
	}
}
obj := new MyCallClass
obj.MyMethod()
- One use for Call is to save code lines by combining multiple methods into one, instead of having multiple separate methods.

Code: Select all

class MyCaseClass1
{
	Upper(vText)
	{
		return Format("{:U}", vText)
	}
	Title(vText)
	{
		return Format("{:T}", vText)
	}
	Lower(vText)
	{
		return Format("{:L}", vText)
	}
}
class MyCaseClass2
{
	__Call(vMethod, vText)
	{
		if vMethod in Upper,Title,Lower
			return Format("{:" SubStr(vMethod, 1, 1) "}", vText)
		else
			return
	}
}
obj1 := new MyCaseClass1
obj2 := new MyCaseClass2
MsgBox, % obj1.Upper("hello")
MsgBox, % obj2.Upper("hello")
- Another use for Call is to have case-sensitive methods names. Ordinarily in AutoHotkey, method names are case insensitive.

Code: Select all

class MyCallClass
{
	__Call(vMethod)
	{
		if (vMethod == "HELLO")
			return "HELLO WORLD"
		else if (vMethod == "Hello")
			return "Hello World"
		else if (vMethod == "hello")
			return "hello world"
	}
}
obj := new MyCallClass
MsgBox, % obj.HELLO() ;HELLO WORLD
MsgBox, % obj.Hello() ; Hello World
MsgBox, % obj.hello() ;hello world

;if a key is created with the same name as one of the anticipated values in the __Call method, this prevents the __Call method from being called
ObjRawSet(obj, "hello", "MyValue")
MsgBox, % obj.HELLO() ;(blank)
MsgBox, % obj.Hello() ;(blank)
MsgBox, % obj.hello() ;(blank)
- This example demonstrates that every time something that looks like a method is called, the __Call method is invoked.
- Even if a method (or property) exists, the __Call method is invoked.
- If a key exists with the same name as the method, the __Call method is not invoked.
- One use of the __Call method is to monitor each time a method or property is called.

Code: Select all

class MyCallClass
{
	MyKey := "MyKeyValue"

	__Call(vMethod)
	{
		MsgBox, % "method called:`r`n" vMethod
		;return "x"
	}

	MyMethod()
	{
		return "MyMethodValue"
	}

	MyProperty
	{
		get
		{
			return "MyPropertyValue"
		}
		set
		{
		}
	}
}

obj := new MyCallClass

MsgBox, % obj.MyMethod() ;MyMethodValue ;__Call invoked
MsgBox, % obj.MyProperty() ;MyPropertyValue ;__Call invoked
MsgBox, % obj.MyKey() ;(blank)
MsgBox, % obj.MyNonExistentEntity() ;(blank) ;__Call invoked

MsgBox, % obj.MyMethod ;(blank)
MsgBox, % obj.MyProperty ;MyPropertyValue
MsgBox, % obj.MyKey ;MyKeyValue
MsgBox, % obj.MyNonExistentEntity ;(blank)
- If a 'return' is done inside the __Call method, then any method/property that would have been invoked, is not invoked.
- Also, the value specified by 'return' is then returned.

==================================================

> META-FUNCTIONS: __GET

- __Get is a method that is called when the script tries to retrieve the value of a non-existent key.
- Here we change the default value returned for a non-existent key from a blank string, to the number 0.
- The effect of 'return' in a __Get method is to specify a default value for when a key is not found.
- A default return value of 0 is useful for creating tallies (frequency counts).

Code: Select all

class MyGetClass
{
	__Get(vKey)
	{
		return 0
	}
}

obj1 := {}
obj2 := new MyGetClass
MsgBox, % obj1.NonExistentKey ;(blank)
MsgBox, % obj2.NonExistentKey ;0

obj1.key++
obj2.key++
MsgBox, % obj1.key ;(blank)
MsgBox, % obj2.key ;1

;otherwise to increment a key:
obj3 := {}
obj3.key := obj3.HasKey("key") ? obj3.key+1 : 1
MsgBox, % obj3.key ;1
- Here we use the Format function to change the default value for an array to 0.

Code: Select all

;here is a simple 'switch statement' via an object

;using the HasKey method
obj := {a:"A",b:"B",c:"C"}
MsgBox, % obj.HasKey("a") ? obj["a"] : "ERROR"
MsgBox, % obj.HasKey("d") ? obj["d"] : "ERROR"

;using a bound func
obj := {a:"A",b:"B",c:"C",base:{__Get:Func("Format").Bind("{}", "ERROR")}}
MsgBox, % obj.a
MsgBox, % obj.d
- Here we use the Format function to change the default value for an array to 'ERROR'.

Code: Select all

obj1 := {}
obj2 := {base:{__Get:Func("Format").Bind("{}", 0)}}
MsgBox, % obj1.key ;(blank)
MsgBox, % obj2.key ;0
MsgBox, % obj2.key+3 ;3
- This example demonstrates that the __Get method is only called for a key that doesn't exist.

Code: Select all

class MyGetClass
{
	__Get(vKey)
	{
		MsgBox, % A_ThisFunc
	}
}

obj := new MyGetClass
MsgBox, % obj.key ;(blank)
obj.key := "value"
MsgBox, % obj.key ;value
==================================================

> META-FUNCTIONS: __SET

- __Set is a method that is called when the script tries to set the value of a non-existent key.
- In this class we only allow positive integers to be assigned as keys.

Code: Select all

class MySetClass
{
	__Set(vKey, vValue)
	{
		if RegExMatch(vValue, "^\d+$")
			ObjRawSet(this, vKey, vValue)
		else
			MsgBox, % "error: invalid value:`r`n" vValue
		return ;without this line the class would assign this[vKey] := vValue
	}
}
obj := new MySetClass
obj.key1 := 123
obj.key2 := "abc"
MsgBox, % obj.key1 ;123
MsgBox, % obj.key2 ;(blank)
- Note if we tried to assign a key like this, inside the class:
this[vKey] := vValue
instead of this:
ObjRawSet(this, vKey, vValue)
That would trigger the __Set method in an infinite loop.
ObjRawSet bypasses the __Set method.

==================================================

> __GET/__SET: MONITOR EVERY TIME A KEY IS GOTTEN/SET

- Here are workarounds to, in effect, invoke __Get/__Set even if the key already exists.
- Here are two ways to be able to monitor each time get/set is done.

- In this example we use a global object.
- Any gets/sets are done to an external object, and since no keys are created in the instance object, the __Get/__Set meta-functions will keep getting invoked.

Code: Select all

global oData := {}
class MyClass
{
	__New()
	{
		global oData
		oData[&this] := {}
	}
	__Get(vKey)
	{
		global oData
		MsgBox, % A_ThisFunc "`r`n" vKey "=" oData[&this, vKey]
		return oData[&this, vKey]
	}
	__Set(vKey, vValue)
	{
		global oData
		oData[&this, vKey] := vValue
		MsgBox, % A_ThisFunc "`r`n" vKey "=" vValue
		return ;without this line a key would be automatically assigned to the instance object
	}
	__Delete()
	{
		global oData
		oData.Delete(&this)
	}
}

obj := new MyClass
;the __Set method is invoked each time
Loop, 3
	obj.key := "value" A_Index
;the __Get method is invoked each time
Loop, 3
	MsgBox, % obj.key
;MsgBox, % oData.Count() ;1
;obj := ""
;MsgBox, % oData.Count() ;0
- In this example we use the base of a class object to store data.

Code: Select all

class MyClass
{
	static Data := {}
	__New()
	{
		ObjRawSet(MyClass.Data, &this, {})
	}
	__Get(vKey)
	{
		MsgBox, % A_ThisFunc "`r`n" vKey "=" MyClass.Data[&this, vKey]
		return MyClass.Data[&this, vKey] ;does work
		;return this.base.Data[&this, vKey] ;doesn't work
	}
	__Set(vKey, vValue)
	{
		MyClass.Data[&this, vKey] := vValue
		MsgBox, % A_ThisFunc "`r`n" vKey "=" vValue
		return ;without this line a key would be automatically assigned to the instance object
	}
	__Delete()
	{
		MyClass.Data.Delete(&this)
	}
}

obj := new MyClass
;the __Set method is invoked each time
Loop, 3
	obj.key := "value" A_Index
;the __Get method is invoked each time
Loop, 3
	MsgBox, % obj.key
MsgBox, % MyClass.Data.Count() ;1
obj := ""
MsgBox, % MyClass.Data.Count() ;0
- It's also possible to store data in property getters/setters and methods by using static variables.
- The property getters/setters and methods are just functions. And any static variables inside those functions are unique to the functions, but accessible by any instance objects that use those p. getters/p. setters/methods.
- One issue is that a property getters and setters are separate functions, so they can't share data unless a common variable is used e.g. a global variable.
- Another issue is that you might want a way to delete keys from those static variables, something you might want to do when an object instance is deleted.

Code: Select all

class MyClass
{
	__New()
	{
		;call the method and property on script start
		this.MyMethod()
		var := this.MyProperty
	}
	MyMethod()
	{
		static oData := {}
		if !oData[&this]
			oData[&this] := A_Now
		return oData[&this]
	}

	MyProperty
	{
		get
		{
			static oData := {}
			if !oData[&this]
				oData[&this] := A_Now
			return oData[&this]
		}
		set
		{
		}
	}
}

;the instance creation date is stored in a method/property
obj1 := new MyClass
Sleep, 1000
obj2 := new MyClass
vOutput := obj1.MyMethod() "`r`n" obj1.MyProperty "`r`n"
vOutput .= obj2.MyMethod() "`r`n" obj2.MyProperty "`r`n"
MsgBox, % vOutput
==================================================

> __GET/__SET: ADD KEYS AND SUBKEYS (MULTI-DIMENSIONAL ASSIGNMENTS)

- The documentation mentions such assignments like so:
Objects - Definition & Usage | AutoHotkey
https://autohotkey.com/docs/Objects.htm ... _of_Arrays
Multi-dimensional assignments such as table[a, b, c, d] := value
- There is a difference between the following:
var := obj.a.b.c
var := obj["a", "b", "c"]
when certain keys don't already exist.
- If obj.a.b.c does not exist, it fails immediately.
- obj["a", "b", "c"] checks obj.a, then obj.a.b, then obj.a.b.c.
- This example demonstrates the difference:

Code: Select all

class MyClass
{
	__Get(oParams*)
	{
		vOutput := ""
		for vKey, vValue in oParams
			vOutput .= vKey " " vValue "`r`n"
		MsgBox, % A_ThisFunc "`r`n" vOutput
		if (oParams.1 = "g")
			return {h:{i:"MyValue"}}
	}
	__Set(oParams*)
	{
		vOutput := ""
		for vKey, vValue in oParams
			vOutput .= vKey " " vValue "`r`n"
		MsgBox, % A_ThisFunc "`r`n" vOutput
	}
}
obj := new MyClass

;there is a difference in function between the following 2 syntaxes:
MsgBox, % obj["a", "b", "c"] ;(blank)
;MsgBox, % obj.d.e.f ;Error:  No object to invoke. ;(AHK v2 error)

;an unusual condition was added to the class to handle 'obj.g'
MsgBox, % obj.g.h.i ;MyValue

MsgBox

;again, there is a difference in function between the following 2 syntaxes:
obj["a", "b", "c"] := "KeyABC"
;obj.d.e.f := "KeyDEF" ;Error:  No object to invoke. ;(AHK v2 error)
;obj.d.e := "KeyDE" ;Error:  No object to invoke. ;(AHK v2 error)
obj.d := "KeyD"
MsgBox, % obj.a.b.c ;KeyABC
MsgBox, % obj.d ;KeyD
- When creating keys and subkeys, the classes have to be written carefully.
- One thing that needs to be considered is that when an object is created as a subkey of an object. Should the child object have the same class as the parent object, or should it be an AHK basic object.
- The documentation describes the issue thusly:
Objects - Definition & Usage | AutoHotkey
https://autohotkey.com/docs/Objects.htm#Subclassing_aoa
When a multi-parameter assignment such as table[x, y] := content implicitly causes a new object to be created, the new object ordinarily has no base and therefore no custom methods or special behaviour.
- This example demonstrates two classes: one where the child objects are basic AHK objects, and one where the child objects have the same class as the parent object.

Code: Select all

class MyClass
{
}

class MyClass2
{
	__Set(oParams*)
	{
		;MsgBox, % StrJoin("`r`n", oParams*)
		if (oParams.Length() = 2)
		{
			ObjRawSet(this, oParams.1, oParams.2)
			return oParams.2
		}
		else if (oParams.Length() < 2)
			return
		vValue := oParams.Pop()
		ObjRawSet(this, oParams.1, new MyClass2)
		return this[oParams*] := vValue
	}
}

obj := new MyClass
obj["a", "b", "c", "d", "e"] := "value"

vOutput := ""
vOutput .= obj.__Class "`r`n"
vOutput .= obj.a.__Class "`r`n"
vOutput .= obj.a.b.__Class "`r`n"
vOutput .= obj.a.b.c.__Class "`r`n"
vOutput .= obj.a.b.c.d.__Class "`r`n"
MsgBox, % vOutput

obj := new MyClass2
obj["a", "b", "c", "d", "e"] := "value"

vOutput := ""
vOutput .= obj.__Class "`r`n"
vOutput .= obj.a.__Class "`r`n"
vOutput .= obj.a.b.__Class "`r`n"
vOutput .= obj.a.b.c.__Class "`r`n"
vOutput .= obj.a.b.c.d.__Class "`r`n"
MsgBox, % vOutput
==================================================

> __GET/__SET: GET AND SET AT THE SAME TIME

- 'When is a get not a get? When it's a set.'
- We consider what is returned when you do var := obj.key := value
- We have a set: obj.key := value
- And a get: var := obj.key

Code: Select all

class MyGetSetClass
{
	__Get(k)
	{
		return "key not found"
	}
	__Set(k, v)
	{
		ObjRawSet(this, k, v)
		return "key given initial value"
	}
}

;__Get output v. __Set output
obj := new MyGetSetClass
var1 := obj.key
var2 := obj.key := "MyValue" ;it appears to get and set at the same time
var3 := obj.key
var4 := obj.key := "MyValue" ;it appears to get and set at the same time
var5 := obj.key
MsgBox, % var1 ;key not found
MsgBox, % var2 ;key given initial value
MsgBox, % var3 ;MyValue
MsgBox, % var4 ;MyValue ;not 'key given initial value' because __Set is only invoked for a key that does not exist
MsgBox, % var5 ;MyValue
==================================================

> GENERAL CLASS EXAMPLES: DATES AND SPLIT PATH

Code: Select all

class MyDateClass
{
	__New(vDate:="")
	{
		if (vDate = "")
		{
			ObjRawSet(this, "date", A_Now)
		}
		else
		{
			FormatTime, vDate, % vDate, yyyyMMddHHmmss ;expand truncated dates e.g. 'yyyyMMdd' to 14 characters
			if vDate is not date
				return ;a blank string is returned, no object is returned
			ObjRawSet(this, "date", vDate)
		}
	}
	Format(vFormat)
	{
		FormatTime, vDate, % vDate, yyyyMMddHHmmss
		return vDate
	}
	Add(vNum, vUnit, vFormat="yyyyMMddHHmmss")
	{
		vDate1 := this.date
		EnvAdd, vDate1, % vNum, % vUnit
		if !(vFormat == "yyyyMMddHHmmss")
			FormatTime, vDate1, % vDate1, % vFormat
		return vDate1
	}
	Diff(vDate2, vUnit)
	{
		vDate1 := this.date
		EnvSub, vDate1, % vDate, % vUnit
		return vDate1
	}
	__Get(vKey)
	{
		FormatTime, vDate, % this.date, % vKey
		return vDate
	}
	__Set()
	{
		return
	}
}

oDate := new MyDateClass
MsgBox, % oDate.date
MsgBox, % oDate.yyyy
MsgBox, % oDate.ddd " " oDate["HH:mm:ss dd/MM/yyyy"]
MsgBox, % oDate.Add(365, "d", "ddd") " " oDate.Add(365, "d", "HH:mm:ss dd/MM/yyyy")
oDate.date := 20030303030303
MsgBox, % oDate.ddd " " oDate["HH:mm:ss dd/MM/yyyy"]
oDate := ""

oDate := new MyDateClass(20060504030201)
MsgBox, % oDate.date
MsgBox, % oDate.ddd " " oDate["HH:mm:ss dd/MM/yyyy"]

;if we delete the 'return' line in __Set
;we can write arbitrary keys:
;oDate.abc := "hello"
;MsgBox, % oDate.abc
- Another example.
[split path class cf. SplitPath command]
object classes: coding style - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=5&t=48938

==================================================

[SECTION 4: ENUMERATORS AND MISCELLANEOUS]

> ENUMERATORS: _NEWENUM AND NEXT

- There are two important methods relating to looping through each item in an object: _NewEnum and Next.
- When a for loop is applied to an object, the object's _NewEnum method is invoked. The method creates an enumerator object which is passed to the for loop.
- The enumerator object needs one method only, a Next method.
- The Next method determines which key/value pairs are output during the for loop, and in which order. It does this by specifying two ByRef variables: key and value.
- Also, the Next method determines when the for loop ends. It does this by specifying 0, as the method's (function's) return value, to end the loop, else a non-zero integer.
- Note: Often an enumerator object class will have two methods: a __New method, to prepare the information, and a Next method, to output it.

- In this example we try to recreate the functionality of the AHK basic object, as regards for loops.
- Note: two ways to loop through objects are: a for loop, and, using a Next method directly. Both are demonstrated in the example.
- (Another way, for linear arrays, e.g. 'obj', is to retrieve obj.Length() and loop through var := obj[A_Index].)

Code: Select all

class MyClass
{
	_NewEnum()
	{
		MsgBox, % A_ThisFunc
		return new MyEnumClass(this)
	}
}

class MyEnumClass
{
	__New(obj)
	{
		MsgBox, % A_ThisFunc
		this.data := obj
		this.temp := []

		;we can't do this:
		;because 'for k in obj' would request an enumerator
		;from MyEnumClass, causing an infinite loop
		;for k in obj
		;	this.temp.Push(k)

		;instead we do this:
		oEnum := ObjNewEnum(obj)
		while oEnum.Next(k)
			this.temp.Push(k)

		this.count := this.temp.Length()
		this.index := 1
	}
	Next(ByRef k:="", ByRef v:="")
	{
		if (this.index > this.count)
			MsgBox, % A_ThisFunc
		if (this.index > this.count)
			return 0
		k := this.temp[this.index]
		v := this.data[k]
		this.index++
		MsgBox, % A_ThisFunc "`r`n" k " " v
		return 1
	}
}

oArray := new MyClass
oArray.q := "Q", oArray.w := "W", oArray.e := "E"
for vKey, vValue in oArray
	vOutput .= vKey " " vValue "`r`n"
MsgBox, % vOutput
- Using the Enum method directly makes it possible to do 2 simultaneous 'for loops'.
- Loop through 2 objects at the same time.

Code: Select all

oArray1 := {a:1, b:2, c:3}
oArray2 := {d:4, e:5, f:6, g:7, h:8}
oEnum1 := ObjNewEnum(oArray1)
oEnum2 := ObjNewEnum(oArray2)

vOutput := ""
Loop
{
	if vRet1 := oEnum1.Next(vKey, vValue)
		vOutput .= vKey " " vValue "`r`n"
	if vRet2 := oEnum2.Next(vKey, vValue)
		vOutput .= vKey " " vValue "`r`n"
	if !vRet1 && !vRet2
		break
}
MsgBox, % vOutput
- Custom enumerators can be used to create an infinite for loop.
- This example generates the square numbers.

Code: Select all

class MyClass
{
	_NewEnum()
	{
		return new MyEnumClass
	}
}

class MyEnumClass
{
	__New()
	{
		this.index := 1
	}
	Next(ByRef k:="", ByRef v:="")
	{
		k := this.index
		v := this.index ** 2
		this.index++
		return 1
	}
}

oArray := new MyClass
vOutput := ""
for vKey, vValue in oArray
{
	vOutput .= vKey " " vValue "`r`n"
	if (vKey = 20)
		break
}
MsgBox, % vOutput

;we can use a for loop on an enumerator object class like so:
for vKey, vValue in {_NewEnum:RetNewEnum}
{
	vOutput .= vKey " " vValue "`r`n"
	if (vKey = 20)
		break
}
MsgBox, % vOutput

RetNewEnum(obj)
{
	return new MyEnumClass
}
- Using the Next function allows a loop to generate more than 2 output values.
- This example generates squares, cubes and fourth powers.

Code: Select all

class MyEnumClass
{
	__New()
	{
		this.index := 1
	}
	Next(ByRef n1:="", ByRef n2:="", ByRef n3:="", ByRef n4:="")
	{
		n1 := this.index
		n2 := this.index ** 2
		n3 := this.index ** 3
		n4 := this.index ** 4
		this.index++
		return 1
	}
}

oEnum := new MyEnumClass
vOutput := ""
while oEnum.Next(vNum1, vNum2, vNum3, vNum4)
{
	vOutput .= Format("{}`t{}`t{}`t{}", vNum1, vNum2, vNum3, vNum4) "`r`n"
	if (A_Index = 20)
		break
}
MsgBox, % vOutput
- Here are some links to further enumerator examples.
- One thing to notice in the links is that the classes/methods needed for an enumerator object can appear in different places: e.g. as a separate class, as a nested class, within the class (alongside other methods).

[loop reverse, and, loop and delete]
objects: enumerator object queries - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 78#p230778

[numerical examples][enumerator methods within class]
enum type and while loop - Ask for Help - AutoHotkey Community
https://autohotkey.com/board/topic/7886 ... ntry500848
For loop question - Ask for Help - AutoHotkey Community
https://autohotkey.com/board/topic/6691 ... ntry423515

[numerical example][enumerator as nested class]
objects: enumerator object queries - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 79#p230779

[an enumerator is specified by creating an array, i.e. a class defined via the old-school 'function syntax' approach: {methodname:"funcname"}]
find previous key in associated array - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 741#p31741

[passing an enumerator into a for loop]
notation: objects: key names to avoid clashes with methods/properties - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 59#p198159

[replace a basic object's enumerator with a custom enumerator]
How to create custom enumerator objects - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=7&t=7199

[Next example on a COM object]
AccViewer Basic - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=6&t=32039

==================================================

> SINGLETON

[slightly simpler example]
objects: backport AHK v2 Gui/Menu classes to AHK v1 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 57#p197457

[slightly more complex example]
object classes: redefine __Set() temporarily / general queries - Page 3 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 32#p198332

==================================================

> TUTORIALS / LINKS

[big collections of links]
object classes: redefine __Set() temporarily / general queries - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 31#p193931
objects: enumerator object queries - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 71#p198571

[investigating the binary of AHK objects]
object classes: redefine __Set() temporarily / general queries - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 33#p194533

[dictionary object attempt, where no key names causes clashes with method names]
Possible AHK bug - the word "base" is missed in an array for-loop - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 27#p113927

[advanced base example]
Parentheses when calling gets? - AutoHotkey Community
https://autohotkey.com/boards/viewtopic ... 116#p58116

==================================================

> THE END

Special thanks to Helgef, lexikos and nnnik.

The long and binding road that leads to your for.

==================================================
iseahound
Posts: 321
Joined: 13 Aug 2016, 21:04
GitHub: iseahound

Re: jeeswg's object classes tutorial

24 Aug 2018, 02:13

- The __New method allows an action to be taken when an object is created.
- The __Delete method allows an action to be taken before an object is deleted.
The __Delete() function is used to clean up resources allocated when the object was created. It should be avoided in an ideal situation.
- Note: if you use 'return' in a __Delete method, the object will still be deleted.
Note: The __Delete() function never returns a value.
That's right. Put any value you want in he return statement and you can never access it.

Property Syntax is "dirty" to be honest, it's an AutoHotkey unique feature.
Helgef
Posts: 3221
Joined: 17 Jul 2016, 01:02
Contact:

Re: jeeswg's object classes tutorial

24 Aug 2018, 02:17

Hello jeeswg, impressive collection :shock:. Thanks for sharing :thumbup:.

I only sifted through the top content, I'm sure there is a lot of good information here.
- Key names can be of integer type (positive/0/negative) or string type.

Note,
Keys can be strings, integers or objects
(my bold)
Eg, o := {[]:1}

Cheers.
swagfag
Posts: 1371
Joined: 11 Jan 2017, 17:59

Re: jeeswg's object classes tutorial

24 Aug 2018, 05:52

cool, i liked the custom enum bit. here's fibonacci(int overflow not handled):

Code: Select all

new InfiniteFibonacci()
Esc::ExitApp

class InfiniteFibonacci
{
	__New() {
		for index, fib in this._NewEnum()
			MsgBox % "index: " index "`nnumber: " fib
	}

	_NewEnum() {
		return new this.InfFibEnum()
	}

	class InfFibEnum
	{
		__New() {
			this.prevFib := 0
			this.nextFib := 1
			this.index := 0
		}

		Next(ByRef k := "", ByRef v := "") {
			if (this.index == 0)
				v := 0
			else
			{
				v := this.nextFib
				this.nextFib += this.prevFib
				this.prevFib := v
			}

			k := this.index++

			return 1
		}
	}
}

Return to “Tips and Tricks”

Who is online

Users browsing this forum: No registered users and 3 guests