Using Non-String Keys with Object
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
:
Spot a bug? Have a question or suggestion? Post a comment!
#1 by rocksoccer on March 3rd, 2014 ·
Maybe this is to simplify the implementation of normal Array, so int is treated as a very special type.
#2 by Benjamin Guihaire on March 3rd, 2014 ·
It would be interesting to see if there is a speed difference for accessing an object with keys that are “int” and keys that are Strings.
Also, note that the old Array AS3 class also support keys as int (index in the Array), but can also have keys as string.
#3 by jackson on March 3rd, 2014 ·
After finding out about the
int
key support inObject
I immediately made a note to do as you say: test performance ofint
keys againstString
keys. I have a couple of articles that are close to this already, such as Maps With Int Keys and Map Performance. Neither directly testsint
keys againstString
keys though, so a direct test is in order.#4 by adampasz on March 3rd, 2014 ·
Ha! This always confused me about associative arrays. You shouldn’t need a flowchart to assign a variable!
It looks like Dictionaries behave the same way.
var d:Dictionary = new Dictionary();
var o:Object = {};
o[1] = “int_one”;
o[“1”] = “string_one”;
d[1] = “int_one”;
d[“1”] = “string_one”;
trace(‘o[1]=’+o[1]);
trace(‘o[“1”]=’+o[“1”]);
trace(‘d[1]=’+d[1]);
trace(‘d[“1”]=’+d[“1”]);
Output:
[trace] o[1]=string_one
[trace] o[“1”]=string_one
[trace] d[1]=string_one
[trace] d[“1”]=string_one
I confirmed that JavaScript objects also exhibit this behavior.
I wonder if you could use the Proxy class to create a dictionary that allows both string and int indices…
#5 by jackson on March 3rd, 2014 ·
I don’t recall ever seeing any documentation that
Dictionary
does this too, so that’s a surprising find. I’ll investigate more. Thanks for the tip!#6 by Dominik on April 5th, 2014 ·
Great article and nice findings! This is related to another strange behaviour we found with object and dictionary. Hidden object allocations!
If u use integer keys greater then 28 bit (like u mentioned) there is String allocation for each key you are deleting from the Dictionary.
Addionally on writing into that object again there are sometimes new object allocations. It seems the map has to be recreated internally.
Maybe you want to look into this in one of your next articles ;)
#7 by Kyle on August 16th, 2017 ·
Great article.
Since we don’t know the internal implementation, isn’t it possible that all of those keys are actually stored in an Object as strings and that the conversion to int takes place only when you loop with a for in? A speed test might help infer when conversions are taking place.
It’s worth noting that the key iterator in a for in loop allows a string variable, but trying to use an int variable throws a compiler error.