New Fun Blog – Scott Bilas

Take what you want, and leave the rest (just like your salad bar).

Archive for the ‘as3’ Category

Ultimate AS3 Fake Enums

with 6 comments

I just can’t leave this thing alone. This is hopefully the last revision I’m going to make to my fake AS3 enum class (here’s the original posting, then two updates). This is inspired by commenter Mikko Korpela, who asked about an iteration ability. Well, that sounds like a fun idea! Let’s fill in some holes and bring it up to par with the enum support in C#.

Sorry it took me so long to get around to this Miko, but I’ve had my Flex work on hold for a while. But recently I did some Flash work at PopCap and it got me back in the mood again.

New Enum Features

Here’s my Ultimate AS3 Fake Enum class, which has the following new features:

  • Further type safety. Only the enum can construct its own constants now, via enum constructor protection. This should prevent people using the constants incorrectly (e.g. someone is incorrectly new’ing them up instead of using MyEnum.MyConstant).
  • Every constant now gets a zero-based index that corresponds to the order it is created in the class. Access via the Index accessor on the constant. Useful in serialization code, for example.
  • Added a new GetConstants method that returns an array of all the constants, where each constant’s slot in the array corresponds to its Index.
  • Added a new ParseConstant method that does the reverse of toString(). Given a constant’s Name, return the constant. Case sensitivity optional and off by default for convenience.

…all without any extra burden placed on the derivative class! I’m always interested in solutions that keep client code as clean as possible.

I also adjusted the code to my current AS3 coding standards, which is nearly the same as Microsoft’s C# standards. Note that I’d normally use Vector.<Enum> instead of the generic Array class, but I haven’t upgraded my Flex to the newest SDK yet.

The Code

Without further ado, here’s the new Enum.as file. Stick it in a package if you wish, and drop it wherever you like. No system setup is required to make it work.

package
{
	import flash.utils.*;

	public /*abstract*/ class Enum
	{
		public function get Name()  :String { return _name; }
		public function get Index() :int    { return _index; }

		public /*override*/ function toString() :String { return Name; }

		public static function GetConstants(i_type :Class) :Array
		{
			var constants :EnumConstants = _enumDb[getQualifiedClassName(i_type)];
			if (constants == null)
				return null;

			// return a copy to prevent caller modifications
			return constants.ByIndex.slice();
		}

		public static function ParseConstant(
				i_type			:Class,
				i_constantName  :String,
				i_caseSensitive :Boolean = false) :Enum
		{
			var constants :EnumConstants = _enumDb[getQualifiedClassName(i_type)];
			if (constants == null)
				return null;

			var constant :Enum = constants.ByName[i_constantName.toLowerCase()];
			if (i_caseSensitive && (constant != null) && (i_constantName != constant.Name))
				return null;

			return constant;
		}

		/*-----------------------------------------------------------------*/

		/*protected*/ function Enum()
		{
			var typeName :String = getQualifiedClassName(this);

			// discourage people new'ing up constants on their own instead
			// of using the class constants
			if (_enumDb[typeName] != null)
			{
				throw new Error(
					"Enum constants can only be constructed as static consts " +
					"in their own enum class " + "(bad type='" + typeName + "')");
			}

			// if opening up a new type, alloc an array for its constants
			var constants :Array = _pendingDb[typeName];
			if (constants == null)
				_pendingDb[typeName] = constants = [];

			// record
			_index = constants.length;
			constants.push(this);
		}

		protected static function initEnum(i_type :Class) :void
		{
			var typeName :String = getQualifiedClassName(i_type);

			// can't call initEnum twice on same type (likely copy-paste bug)
			if (_enumDb[typeName] != null)
			{
				throw new Error(
					"Can't initialize enum twice (type='" + typeName + "')");
			}

			// no constant is technically ok, but it's probably a copy-paste bug
			var constants :Array = _pendingDb[typeName];
			if (constants == null)
			{
				throw new Error(
					"Can't have an enum without any constants (type='" +
					typeName + "')");
			}

			// process constants
			var type :XML = flash.utils.describeType(i_type);
			for each (var constant :XML in type.constant)
			{
				// this will fail to coerce if the type isn't inherited from Enum
				var enumConstant :Enum = i_type[constant.@name];

				// if the types don't match then probably have a copy-paste error.
				// this is really common so it's good to catch it here.
				var enumConstantType :* = Object(enumConstant).constructor;
				if (enumConstantType != i_type)
				{
					throw new Error(
						"Constant type '" + enumConstantType + "' " +
						"does not match its enum class '" + i_type + "'");
				}

				enumConstant._name = constant.@name;
			}

			// now seal it
			_pendingDb[typeName] = null;
			_enumDb[typeName] = new EnumConstants(constants);
		}

		private var _name :String = null;
		private var _index :int = -1;

		private static var _pendingDb :Object = {};	// typename -> [constants]
		private static var _enumDb	  :Object = {};	// typename -> EnumConstants
	}
}

// private support class
class EnumConstants
{
	public function EnumConstants(i_byIndex :Array)
	{
		ByIndex = i_byIndex;

		for (var i :int = 0; i < ByIndex.length; ++i)
		{
			var enumConstant :Enum = ByIndex[i];
			ByName[enumConstant.Name.toLowerCase()] = enumConstant;
		}
	}

	public var ByIndex :Array;
	public var ByName :Object = {};
}

The Samples

Here are a couple sample enums and some code to demonstrate the features.

Simple Enum: State

This is the simplest usage of the Enum class. All you have to do to make a fake enum is:

  1. Create a class that extends the Enum class. Tag it as final too so nobody inherits it (that would be weird).
  2. Put some static init code in there that just calls initEnum(MyTypeName). This will get run on demand, on the first usage of the enum by other code. It sets up the necessary lookup tables and attaches a Name and Index to each enum constant.
  3. Create a public static const for each enum constant you want, following the pattern in the sample.

Here’s the sample enum. Note that it includes a bug on purpose! (It’s clearly marked.)

package
{
	public final class State extends Enum
	{
		{initEnum(State);} // static ctor

		public static const Initializing :State = new State();
		public static const Connecting   :State = new State();
		public static const Loading      :State = new State();
		public static const Ready        :State = new State();

		// illegal; this will fail at runtime
		public static const Test		 :OtherEnum = new OtherEnum();
	}
}

And some test code to walk through the features…

for each (var state :State in Enum.GetConstants(State))
{
	trace(state.Index + ": " + state.Name);
}

trace("connecting (no case): " + Enum.ParseConstant(State, "connecting"));
trace("connecting (with case): " + Enum.ParseConstant(State, "connecting", true));

// illegal; gets a runtime exception
var someEnum :State = new State();

Which produces the following output:

0: Initializing
1: Connecting
2: Loading
3: Ready
connecting (no case): Connecting
connecting (with case): null
Error: Enum constants can only be constructed as static consts in their own enum class (bad type='State')
 ...

Advanced Enum: NetError

Let’s look at a more advanced sample, which I’m calling an “attributed” enum. Let’s say we have a NetError enum with constants for different types of failures on a network request. We want to be able to show a more user-friendly error, and perhaps classify it as a fatal error or not. We could have a table that associates extended attributes for enum constants with those constants, but why not just attach them to the constants directly?

In .NET I’d do this with attributes. Now, AS3 doesn’t support attributes like we need, so we’ll have to fake it. Given that we already have a fake enum constant, which is really an object underneath, we can attach whatever we like to it!

Take a look at the sample enum.

package
{
	public final class NetError extends Enum
	{
		{ initEnum(NetError); } // static ctor

	// Constants.

		public static const None			:NetError = new NetError(false, "no error");
		public static const CantConnect		:NetError = new NetError(false, "can't connect to the server, must be down?");
		public static const VersionMismatch	:NetError = new NetError(true,  "protocol versions different");
		public static const Unknown			:NetError = new NetError(true,  "unexpected error type");

	// Constant attributes.

		public function get IsFatal()	  :Boolean	{ return _isFatal; }
		public function get Description() :String	{ return _description; }		

	// Constant query.

		public static function GetConstants() :Array
			{ return Enum.GetConstants(NetError); }
		public static function ParseConstant(i_constantName :String, i_caseSensitive :Boolean = false) :NetError
			{ return NetError(Enum.ParseConstant(NetError, i_constantName, i_caseSensitive)); }

	// Private.

		/*private*/ function NetError(i_isFatal :Boolean, i_description :String)
			{ _isFatal = i_isFatal; _description = i_description; }
		private var _isFatal	 :Boolean;
		private var _description :String;
	}
}

Given a constant, perhaps returned from a function as an error code, or thrown as part of an exception, we can ask it for its friendlier name by accessing its Description member.

Also in this class note the helper functions GetConstants and ParseConstant that wrap the Enum class versions, automatically passing in the local type. This isn’t necessary, but is a nice convenience. It is a bit more code to copy-paste around when adding new enums, though. If only AS3 supported templates, we could have a TEnum base that does all this for us. Ah well.

Here is some test code to play with the features

for each (var netError :NetError in NetError.GetConstants())
{
	trace(
		netError.Index + ": " + netError.Name +
		" (" + netError.Description + ") " +
		(netError.IsFatal ? "[FATAL]" : ""));
}

Which produces the following output:

0: None (no error)
1: CantConnect (can't connect to the server, must be down?)
2: VersionMismatch (protocol versions different) [FATAL]
3: Unknown (unexpected error type) [FATAL]

Future?

I hope this is the last word on fake enums in Actionscript. Not because I don’t think anyone else can improve on it, but because I’m hoping that the next version of the language will have native support for enums. Then we can put all this fakery behind us and get on with our lives.

The enum posts are among the top hitters on my blog, and they are popular topics elsewhere on the web. I’m hoping that Adobe will strongly consider native enum support in their next Flash player.

Written by Scott

December 24th, 2009 at 2:00 pm

Posted in as3, enum, flex

Update 2: Faking Enums in AS3

with 9 comments

[Update: the final version of my enum class is here.]

Well, I couldn’t help but improve on the fake enums just a bit more (see previous article and the original one).

Because the system does involve some repetition in its pattern, it’s very likely that a copy-paste error is going to result in unwanted behavior not detected by the compiler. In fact, I did that very thing myself as I was working on an app I’m doing on the side.

Consider this code:

public class EError extends EEnum
{
    {initEnum(EState);} // static ctor

    public static const None            :EState = new EState();
    public static const CantConnect     :EState = new EState();
    public static const VersionMismatch :EState = new EState();
    public static const Unknown         :EState = new EState();
}

Whoops! We have two major classes of mistakes here.

  • The type of each constant is wrong. Hopefully the compiler will eventually catch this if you do write some type-safe code such as var err :EError = EError.None, but this isn’t guaranteed. It’s also not going to be obvious what the problem is.
  • The type sent into initEnum() is wrong. This means that EError’s constants will never get their Text fields set right. Any code that relies on this (such as for logging) will fail, not to mention the difficulty in debugging.Most likely this will get noticed right when it’s super-important: in the middle of a deep debugging session where you absolutely need to know what a certain enum is set to. Can work around it, but let’s try to avoid that happening in the first place.

Another issue I’d like to fix is to make the Text field read-only so it doesn’t get modified by accident. And we might as well rename it to a better name like Name while we’re at it.

So here’s my final solution:

import flash.utils.describeType;

public class EEnum
{
    public function get Name() :String
        { return _name; }

    public function toString() :String // override
        { return Name; }

    protected static function initEnum(i_type :*) :void
    {
        var type :XML = flash.utils.describeType(i_type);
        for each (var constant :XML in type.constant)
        {
            var enumConstant :EEnum = i_type[constant.@name];

            // if 'text' is already initialized, then we're probably
            // calling initEnum() on the same type twice by accident,
            // likely a copy-paste bonehead mistake.
            if (enumConstant.Name != null)
            {
                throw new Error("Can't initialize '" + i_type + "' twice");
            }

            // if the types don't match then probably have another
            // copy-paste error.
            var enumConstantObj :* = enumConstant;
            if (enumConstantObj.constructor != i_type)
            {
                throw new Error(
                    "Constant type '" + enumConstantObj.constructor + "' " +
                    "does not match its enum class '" + i_type + "'");
            }

            enumConstant._name = constant.@name;
        }
    }

    private var _name :String = null;
}

This has the following fixes:

  • Changed Text to _name, and made it private so only initEnum() will mess with it.
  • Added an accessor for _name called Name for convenience, and threw in a toString() to be more ActionScripty.
  • Added double-init testing. I put this on the constant _name init but could have also had a bool _initted attached to the EEnum. Same result either way, but I think this is a little bit safer because it avoids the (admittedly unlikely) scenario of an enum pulling constants from another. It’s certainly not any harder to understand.
  • Added a test to make sure that each constant initted in the enum is actually of the type requested.

There. Now I’ve done this simple bit of code to death and we can move on. :)

Written by Scott

December 16th, 2008 at 6:01 pm

Posted in as3, enum, flex

Update: Faking Enums in AS3

with 4 comments

[Update: the final version of my enum class is here.]

Cardin posted a comment (thanks!) with an update to Faking Enums in AS3, and rather than post a comment reply, I figured I’d do a new post instead. Then I can get syntax highlighting. :)

I did some tests in Flex and unfortunately, this simplification won’t work. A couple problems arise.

First, if you don’t init those constants in EState and just leave them unassigned, then the Flex compiler gives a warning for each with a little hazard icon next to each constant. Not is this annoying, for me it’s a deal-breaker. I insist on having warning-clean projects. Warnings are so useful for catching bugs, that I always set them to the max. Also, if an option is available on the compiler I’m using to turn warnings into errors, then I always set it to make sure those warnings get fixed. We do the same at my work. Catches lots of great stuff.

So let’s just assign each of those constants to null. It’s not ideal but it is still a bit simpler than ‘new EState()’. Now we run into the second problem. The VM refuses to let you modify those constants. This was actually a pleasant surprise for me. I’m so accustomed to older versions of ActionScript letting you do whatever sloppy stuff you want. Powerful but dangerous in the long run. The new compiler throws an exception:

ReferenceError: Error #1074: Lectura no permitida de la propiedad Ready de sólo lectura en class EState.
at EEnum$/initEnum()[C:\Users\sbilas\Proj\tt\main\test-flex\src\EEnum.as:14]

Sorry it’s in español, but that’s how I have my machine configured (me gusta practicar). Roughly translated it says that writing isn’t permitted from the read-only property. Well actually it says that reading isn’t permitted but I’m pretty sure that’s a translation bug (should be “escritura”). I tested with a trace statement and reading works just fine.

Anyway, the exception happens when trying to assign in that “new inType()”. So the “= new EState() can’t be left out for each constant, unfortunately, as far as I can tell. We could remove the ‘const’ but that’s starting to sacrifice safety for a small amount of programmer convenience. The enum is going to be used far more often than it will be written, so we’ll leave the const in there.

However, there is one tiny improvement I was able to make, and it looks like this:

import flash.utils.describeType;

public class EEnum
{
    public var Text :String;

    protected static function initEnum(i_type :*) :void
    {
        var type :XML = flash.utils.describeType(i_type);
        for each (var constant :XML in type.constant)
        {
            i_type[constant.@name].Text = constant.@name;
        }
    }
}

public class EState extends EEnum
{
    // static ctor
    {initEnum(EState);}

    public static const Initializing :EState = new EState();
    public static const Connecting   :EState = new EState();
    public static const Loading      :EState = new EState();
    public static const Ready        :EState = new EState();
}

So, we have a base class to extend that holds the Text field, and we might as well move the constant init code into the base as well instead of a general utility class. It’s probably only ever going to be used in this way anyway.

Overall this is a bit simpler now. This reduces the requirements for an enum to three fairly minimal things:

  • Extend EEnum.
  • Call initEnum(EnumType) in a static ctor.
  • Set all enum “constants” as public static consts assigned to a new EnumType().

If you can get it any smaller than this, then please post a comment!

Written by Scott

December 11th, 2008 at 8:00 am

Posted in as3, enum, flex

Switch to our mobile site