Is Operator and Type Safe Lists
In some recent coding I was playing with a custom type system and needed to add some type checking to it. What this means is that at some point I need to verify that some object is really the type I think it should be. Normally when you are coding in AS3 and need to check a type you do something like:
public function testType(object:Object):void { if (object is String) { trace("It is a String"); } else if (object is Array) { trace("It is an Array"); } else { trace("Its type is ?"); } } testType(new String()); testType(new Array()); testType(new Object()); // Output: // It is a String // It is an Array // Its type is ?
Nothing out of the ordinary there. The important thing to examine though is that the right hand side of the is operator is really an expression. Typically you just type in the class you are testing against, since you are likely to cast it on the next line, but why not try something else like:
public function returnClass(type:String):Class { if (type == "String") { return String; } if (type == "Array") { return Array; } throw TypeError("Unknown type " + type); } public function testType2(object:Object):void { if (object is returnClass("String")) { trace("It is a String"); } else if (object is returnClass("Array")) { trace("It is an Array"); } else { trace("Its type is ?"); } } testType2(new String()); testType2(new Array()); testType2(new Object()); // Output: // It is a String // It is an Array // Its type is ?
Why would you want to do such a thing? ActionScript 3 doesn't have support for generics, but by using this expression based format of the is operator you can simulate them. You don't get compile time type checking but runtime checking is better than nothing. Let's consider an application where you want to have type safe lists (you can only add items of a certain type to the list). Instead of having to create a separate list wrapper for every type you might need to support, create a standard type safe list class that knows what type each item should be. There are many different approaches to creating such a class, one possibility that uses composition is this:
package com.example { import flash.events.Event; import flash.events.EventDispatcher; import flash.utils.getQualifiedClassName; import mx.collections.ArrayCollection; import mx.collections.IList; import mx.events.CollectionEvent; // Please note this code is illustrative and has not been fully tested! public class TypeSafeList implements IList { private var _itemType:Class; private var _arrayCollection:ArrayCollection; private var _eventDispatcher:EventDispatcher; public function TypeSafeList(itemType:Class) { _itemType = itemType; _arrayCollection = new ArrayCollection(new Array()); _eventDispatcher = new EventDispatcher(this); _arrayCollection.addEventListener(CollectionEvent.COLLECTION_CHANGE, relayEvent); } private function relayEvent(event:Event):void { dispatchEvent(event); } private function checkItemType(object:Object):void { if ((object != null) && !(object is _itemType)) { throw new TypeError("Item is not correct type. Wanted " + _itemType + " got " + getQualifiedClassName(object) + "."); } } public function addItemAt(item:Object, index:int):void { checkItemType(item); _arrayCollection.addItemAt(item, index); } public function get length():int { return _arrayCollection.length; } public function toArray():Array { return _arrayCollection.toArray(); } public function getItemAt(index:int, prefetch:int = 0.0):Object { return _arrayCollection.getItemAt(index, prefetch); } public function itemUpdated(item:Object, property:Object = null, oldValue:Object = null, newValue:Object = null):void { _arrayCollection.itemUpdated(item, property, oldValue, newValue); } public function removeAll():void { _arrayCollection.removeAll(); } public function getItemIndex(item:Object):int { return _arrayCollection.getItemIndex(item); } public function setItemAt(item:Object, index:int):Object { checkItemType(item); return _arrayCollection.setItemAt(item, index); } public function removeItemAt(index:int):Object { return _arrayCollection.removeItemAt(index); } public function addItem(item:Object):void { checkItemType(item); _arrayCollection.addItem(item); } public function hasEventListener(type:String):Boolean { return _eventDispatcher.hasEventListener(type); } public function willTrigger(type:String):Boolean { return _eventDispatcher.willTrigger(type); } public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0.0, useWeakReference:Boolean = false):void { return _eventDispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference); } public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void { return _eventDispatcher.removeEventListener(type, listener, useCapture); } public function dispatchEvent(event:Event):Boolean { return _eventDispatcher.dispatchEvent(event); } } }
Notice how the addItemAt, setItemAt, and addItem methods call checkItemType before passing the call onto the ArrayCollection which is doing all the dirty work. By checking every place that a user can add new information to the IList we can insure that only objects matching the desired type are added. You may wonder why I'm choosing to implement IList instead of just extending ArrayCollection. In this case I don't want the extra baggage that extending ArrayCollection would bring along (support for ICollectionView and the Proxy access methods). If you did want to extend ArrayCollection you would only need to override methods that add data to the object and not create pass through methods for the other interface methods.
Now with this new TypeSafeList what does using it look like? Just like any other IList object, but with the added benefit of runtime type checking. This is some sample code that creates a TypeSafeList that can only have String items added to it:
var list:IList = new TypeSafeList(String); trace(list.length); var text:String = "An Item"; list.addItem(text); trace(list.getItemAt(0)); list.addItemAt("Before An Item", 0); trace(list.getItemIndex(text)); try { list.addItem(12); } catch (typeError:TypeError) { trace(typeError); } try { list.addItem(new Array()); } catch (typeError:TypeError) { trace(typeError); } // Output: // 0 // An Item // 1 // TypeError: Item is not correct type. Wanted [class String] got int. // TypeError: Item is not correct type. Wanted [class String] got Array.
The nice thing with the is operator is that you can also check against interfaces. Consider wanting to create a TypeSafeList that can only contain IUIComponent objects. Not only could this list contain classes that subclass UIComponent but also custom classes that implement the IUIComponent interface (not that you would really want to do such a thing). Some sample code that uses an interface with the TypeSafeList class:
var list:IList = new TypeSafeList(IUIComponent); list.addItem(new Button()); trace(list.length); list.addItem(new Canvas()); trace(list.length); try { list.addItem("I'm not valid"); } catch (typeError:TypeError) { trace(typeError); } // Output: // 1 // 2 // TypeError: Item is not correct type. Wanted [class IUIComponent] got String.
This entry became a little more than what I had originally planned to write about. I originally planned to stop at explaining the use of an expression for the right hand side of the is operator, but somehow ended up with this TypeSafeList class. I hope that you can see the benefit of the simple is operator and maybe someone will find that sample TypeSafeList class useful, please test it first though.