Pop quiz: what’s the difference between an Object and a Dictionary? If you said “Dictionary can have non-String keys”, you bought into a common myth. Today’s article shows the cases where the lowly Object class will use non-String keys whether you like it or not. Read on for the details.

First off, you shouldn’t feel too bad for believing that Object always had String keys. After all, Adobe’s ActionScript 3 fundamentals: Associative arrays, maps, and dictionaries says the following:

While any type of value may be stored inside of a map, the key is always a String.

And Adobe’s Associative Arrays has two sections: Associative arrays with string keys where Object is discussed and Associative arrays with object keys (Dictionaries) that starts off like this:

You can use the Dictionary class to create an associative array that uses objects for keys rather than strings.

But let’s learn what’s really going on. I’ve made a small test app that adds key-value pairs to an Object, loops over the Object with a for-in loop, and prints out some information about what it finds:

for (var k:* in obj)
{
	row("key: " + k);
	row("\tkey type: " + describeType(k).@name);
	row("\tkey plus 1: " + (k+1));
	row("\tvalue: " + obj[k]);
}

To start, let’s try to use an int key:

var key1:int = 1;
obj[key1] = "1 (value for just an int)";

Here’s the output:

key: 1
	key type: int
	key plus 1: 2
	value: 1 (value for just an int)

The key is an int, not a String! If you add 1, it behaves like an int and gives you 2 and not like a String where you would have got 11 in a concatenation.

Well, does this work for uint?

var key2:uint = 2;
obj[key2] = "2 (value for just a uint)";
key: 2
	key type: int
	key plus 1: 3
	value: 2 (value for just a uint)

The uint is being converted to an int. Since int has a “sign bit” and therefore can’t hold as large of values, let’s see how big of a uint it can handle. Here’s the biggest 28-bit integer:

var keyUint28Bit:uint = 0xfffffff;
obj[keyUint28Bit] = "0xfffffff (value for a 28-bit uint)";
key: 268435455
	key type: int
	key plus 1: 268435456
	value: 0xfffffff (value for a 28-bit uint)

That worked. Now let’s try adding one to use 29 bits:

var keyUint29Bit:uint = 0x10000000;
obj[keyUint29Bit] = "0x10000000 (value for a 29-bit uint)";
key: 268435456
	key type: String
	key plus 1: 2684354561
	value: 0x10000000 (value for a 29-bit uint)

Now our uint is being converted to a String. It’s hard to say for sure what’s happening behind the scenes, but it looks like three or four bits of the key are being used for some other purpose. Since those bits are the ones used for the sign bit, let’s try a negative int:

obj[-1] = "-1 (value for a negative int)";
key: -1
	key type: String
	key plus 1: -11
	value: -1 (value for a negative int)

That too was converted into a String.

The next question in my mind is about the interaction of int and uint keys with the same value. Does uint override int?

var key3int:int = 3;
var key3uint:uint = 3;
obj[key3int] = "3 (value for int overridden by uint)";
obj[key3uint] = "3 (value for uint overriding int)";
key: 3
	key type: int
	key plus 1: 4
	value: 3 (value for uint overriding int)

Yes, uint overrides int. How about the other way around?

var key4int:int = 4;
var key4uint:uint = 4;
obj[key4uint] = "4 (value for int overridden by int)";
obj[key4int] = "4 (value for uint overriding uint)";
key: 4
	key type: int
	key plus 1: 5
	value: 4 (value for uint overriding uint)

Yes, they both override each other. The uint and int types are not enough to differentiate them in an Object‘s keys.

Next up: String keys that could be converted to int:

obj["5"] = "\"5\" (value for String)";
key: 5
	key type: int
	key plus 1: 6
	value: "5" (value for String)

The String got converted to int! Now when you’ve explicitly used a String key in an Object that is repeatedly described as having only String keys, you find out that it is actually an int when performing as simple an operation as adding 1.

How often does this conversion happen? Let’s try to fool it by adding a leading zero:

obj["06"] = "\"06\" (value for String with leading zero)";
key: 06
	key type: String
	key plus 1: 061
	value: "06" (value for String with leading zero)

The conversion did not take place here, so we’ve successfully tricked Object into preserving our String.

Now to explore what happens when Object does the conversion from String to int and there’s a conflict with an existing int:

obj[7] = "7 (value for int overridden by String)";
obj["7"] = "\"7\" (value for string overriding int)";
key: 7
	key type: int
	key plus 1: 8
	value: "7" (value for string overriding int)

The existing int key was overridden by the String key after the String was converted to the same int value. For completeness, let’s check if this works the other way around:

obj["8"] = "\"8\" (value for String overridden by int)";
obj[8] = "8 (value for int overriding String)";
key: 8
	key type: int
	key plus 1: 9
	value: 8 (value for int overriding String)

Yes, int keys can also override String keys.

Lastly, let’s try some other types to see if they can be converted to int. Let’s start with an unlikely candidate, an Object:

obj[obj] = "obj (value for Object)";
key: [object Object]
	key type: String
	key plus 1: [object Object]1
	value: obj (value for Object)

As expected of an Object, it couldn’t be converted to int. Instead, it’s toString() method was invoked and that was used as the key. This is similar to when you concatenate an Object with a String.

Next up is Number:

obj[3.14] = "3.14 (value for Number)";
key: 3.14
	key type: String
	key plus 1: 3.141
	value: 3.14 (value for Number)

Number is also not convertible to an int, so it too is converted to a String. How about Boolean?

obj[true] = "true (value for Boolean)"
key: true
	key type: String
	key plus 1: true1
	value: true (value for Boolean)

While it seems clear that true could easily be converted to 1, Object refuses to do it and instead converts true to the String “true”.

Before we wrap up, let’s look at one final oddity. Here’s the output for all of these tests:

key: 1
	key type: int
	key plus 1: 2
	value: 1 (value for just an int)
key: 2
	key type: int
	key plus 1: 3
	value: 2 (value for just a uint)
key: 3
	key type: int
	key plus 1: 4
	value: 3 (value for uint overriding int)
key: 4
	key type: int
	key plus 1: 5
	value: 4 (value for uint overriding uint)
key: 7
	key type: int
	key plus 1: 8
	value: "7" (value for string overriding int)
key: 8
	key type: int
	key plus 1: 9
	value: 8 (value for int overriding String)
key: 5
	key type: int
	key plus 1: 6
	value: "5" (value for String)
key: 268435455
	key type: int
	key plus 1: 268435456
	value: 0xfffffff (value for a 28-bit uint)
key: 268435456
	key type: String
	key plus 1: 2684354561
	value: 0x10000000 (value for a 29-bit uint)
key: -1
	key type: String
	key plus 1: -11
	value: -1 (value for a negative int)
key: true
	key type: String
	key plus 1: true1
	value: true (value for Boolean)
key: 06
	key type: String
	key plus 1: 061
	value: "06" (value for String with leading zero)
key: 3.14
	key type: String
	key plus 1: 3.141
	value: 3.14 (value for Number)
key: [object Object]
	key type: String
	key plus 1: [object Object]1
	value: obj (value for Object)

Notice that all of the int keys come first and all of the String keys come last. This is not because the tests are ordered that way, but instead probably indicative of an internal difference in the implementation of Object. Perhaps there are actually two maps inside Object: one for int keys and one for String keys.

In conclusion, here’s a flowchart based on the above tests showing the process that takes place when you add a new key-value pair to an Object:

Object Keys Flowchart

Spot a bug? Have a question or suggestion? Post a comment!