Search code examples
arrayssortingactionscript-3

sortOn(): Trying to sort the copy of an array, but the original is also being sorted


I am trying to sort a copy of an array, but for some reason, both arrays are being sorted. What could be causing this?

var myArray = [["stuff," "stuff"], ["stuff", "stuff"]]
var myArrayCopy = myArray as Array

trace(myArray) //Gives unsorted array
trace(myArrayCopy) //Gives unsorted array

arrayCopy.sortOn(1, Array.CASEINSENSITIVE)

trace(myArray) //Gives sorted array
trace(myArrayCopy) //Gives sorted array

Solution

  • In AS3 there are, simply put, two types of data: primitive (int, uint, Number, Boolean, String) and objects (everything else, even basic Object and Array types). The primitive data are copied by their values:

    var a:int = 1;
    var b:int;
    
    // Now we pass the value of a.
    b = a;
    a = 2;
    
    trace(a); // output: 2
    trace(b); // output: 1
    

    Then, all objects are passed by their reference (like pointers in C/C++ language), so there's only one original instance of the object and multiple references to it:

    var A:Array = [1, 2];
    var B:Array;
    
    // Now we pass the reference to the A.
    B = A;
    A[0] = 2;
    
    trace(A); // output: 2,2
    trace(B); // output: 2,2
    

    In order to copy object data you need a bit of understanding. There are, as DodgerThud mentioned, deep copy and shallow copy terms. Simply, shallow copy creates a clone of top-level container while copying the deeper levels as is. The deep copy clones everything to the bottom so one of copies won't be affected in any way no matter what you do to another.

    To shallow copy an Array:

    var A:Array = [1,2,[3]];
    var B:Array;
    
    // Make a shallow copy.
    B = A.slice();
    
    // Lets change the original and see.
    A[0] = 2;
    A[1] = 3;
    A[2][0] = 4;
    
    trace(A); // output: 2,3,4
    trace(B); // output: 1,2,4
    

    So, A and B are distinct Arrays, but their last element refers to the same Array.

    To shallow copy an Object:

    var A:Object = {a:1,b:2,c:[3]};
    var B:Object;
    
    // Make a shallow copy.
    B = new Object;
    
    // Iterate over keys in A.
    for (var aKey:String in A)
    {
        // Copy members of A one by one.
        B[aKey] = A[aKey];
    }
    
    // Let's change the original and see.
    A.a = 2;
    A.b = 3;
    A.c[0] = 4;
    
    trace(A.a, A.b, A.c); // output: 2 3 4
    trace(B.a, B.b, B.c); // output: 1 2 4
    

    Then, deep copy. Normally, you recursively iterate over top-level object and all its descendants to make sure each and any piece of data goes as a copy and not as a reference to the original data. In the same time you watch for the duplicate entries and circular references.

    However (and luckily) there are some shortcuts in AS3 for that. You can deep copy via ByteArray. This will (tested and confirmed that) handle duplicates and cyclic references just fine:

    function deepCopy(source:*):*
    {
        var BA:ByteArray = new ByteArray;
    
        BA.writeObject(source);
        BA.position = 0;
    
        var result:* = BA.readObject();
    
        BA.clear();
    
        return result;
    }
    
    var A:Array = [1, 2, [3]];
    var B:Array = deepCopy(A);
    
    // Let's change the original and see.
    A[0] = 2;
    A[1] = 3;
    A[2][0] = 4;
    
    trace(A); // output: 2,3,4
    trace(B); // output: 1,2,3
    

    Not sure, if it would be in any way faster, or better, or more optimal, but still, another way:

    function deepCopy(source:*):*
    {
        return JSON.parse(JSON.stringify(source));
    }
    

    There are some insights about both of them.

    JSON deep copy:

    • duplicates: no (clones them as distinct objects)
    • circular references: no (runtime error)
    • custom classes: no
    • supported data types: int, uint, Number, Boolean, String, Object, Array

    ByteArray deep copy:

    • duplicates: yes
    • circular references: yes
    • custom classes: via registerClassAlias method
    • supported data types: int, uint, Number, Boolean, String, Object, Array, ByteArray

    Here's my own version of deepCopy:

    private const SIMPLE:Object =
    {
        "number":true,
        "string":true,
        "boolean":true,
        "undefined":true
    };
    
    private const XNODE:String = getQualifiedClassName(XML);
    private const XLIST:String = getQualifiedClassName(XMLList);
    private const ARRAY:String = getQualifiedClassName(Array);
    private const OBJECT:String = getQualifiedClassName(Object);
    private const BYTES:String = getQualifiedClassName(ByteArray);
    
    private var lock:Dictionary;
    
    private function deepCopy(source:*):*
    {
        // Handle primitive data.
        if (source == null) return source;
        if (source is Class) return source;
        if (SIMPLE[typeof(source)]) return source;
    
        var result:*;
        var aLock:Boolean;
        var aQname:String;
    
        if (!lock)
        {
            // If we're here, then we're at the top level
            // so we should devise cache for handling
            // duplicates and circular references.
            lock = new Dictionary;
            aLock = true;
        }
    
        // If it is cached, then it is either
        // duplicate or circular reference.
        if (lock[source]) return lock[source];
    
        aQname = getQualifiedClassName(source);
    
        if (aQname == BYTES)
        {
            var aBytes:ByteArray;
    
            aBytes = new ByteArray;
            aBytes.writeBytes(source, 0, source.length);
    
            result = aBytes;
            lock[source] = result;
        }
        else if (aQname == ARRAY)
        {
            var aRay:Array;
    
            aRay = new Array;
            aRay.length = source.length;
    
            result = aRay;
            lock[source] = result;
    
            // Copy the elements of the source Array one by one.
            for (var i:int = 0; i < aRay.length; i++)
            {
                aRay[i] = deepCopy(source[i]);
            }
        }
        else if (aQname == OBJECT)
        {
            var aRes:Object;
    
            aRes = new Object;
    
            result = aRes;
            lock[source] = result;
    
            // Copy the members of the source Object one by one.
            for (var aKey:String in source)
            {
                aRes[aKey] = deepCopy(source[aKey]);
            }
        }
        else if ((aQname == XNODE) || (aQname == XLIST))
        {
            // This one is tricky. The object to clone might
            // have a reference to some descendant node of some
            // big XML. There could be several references to
            // different sub-nodes either. Probably you should
            // not rely on this method to clone XML data unless
            // you are totally aware of what you are doing.
    
            result = source.copy();
            lock[source] = result;
        }
        else
        {
            // If we're here, that means that source holds
            // a reference to some class instance. You should
            // define here your own ways to handle them.
    
            result = source;
        }
    
        if (aLock)
        {
            // If we're here, then we're at the top level
            // so we should do some clean-up before we leave.
    
            for (var aRef:* in lock)
            {
                delete lock[aRef];
            }
    
            lock = null;
        }
    
        return result;
    }
    

    Custom deep copy:

    • duplicates: yes
    • circular references: yes
    • custom classes: you should define your own rules of handling them
    • supported data types: int, uint, Number, Boolean, String, Object, Array, ByteArray, XML (limited), class constructors