Deep copy a List

Hi

Im making a database framework for my games, so I created a bunch of lists as references then I tried to copy those lists but when I tried to modify the list copies the originals got modified as well, I then thought “Oh ok I need to create a new list then copy the list there” so I did, but I had the same result, I didnt understand what was happening but after much research I discovered there is two types of copies “shallow copy” and “deep copy” it seems even if you copy a list on a new list the new list still reference the original objects, so this is “shallow copy”, for a “deep copy” you need to copy the objects one by one, after much thought I come up with this Function:

void UpdateActors() {
     foreach (Actor _Actor in MyDataBase.Actors) {
     List<ArmorSlot> NewSlotList = new List<ArmorSlot>();
            foreach (ArmorSlot _ArmorSlot in MyDataBase.ArmorSlots) {
            NewSlotList.Add(new ArmorSlot() {Name = _ArmorSlot.Name });
            }
     List<BaseStat> NewStatList = new List<BaseStat>();
            foreach (BaseStat _BaseStat in MyDataBase.BaseStats) {
            NewStatList.Add(new BaseStat() {Name = _BaseStat.Name,BaseValue = _BaseStat.BaseValue });
            }
        _Actor.BaseStats = NewStatList;
        _Actor.EquipedArmors = NewSlotList;
        }
}

This doesnt copy the original object just creates new ones with some of the properties from the originals (just the ones I need) but this isnt instantaneous, also it resets the lists completely. It works al right I guess but I was wondering if there was a better way around this.

You can use LINQ:

_Actor.EquipedArmors = MyDataBase.ArmorSlots.Select(armor => new ArmorSlot() {Name = armor.Name}).ToList();
1 Like

One common approach is to have your classes implement a “Clone” method, which creates an identical object. Every class in your object graph should implement Clone and be responsible for setting all the values of the cloned object. This allows you to keep all the state internal, and maintain encapsulation.

I don’t know what your object graph looks like but let’s look at a simple example.

Say your Actor class has two properties: name (string) and armours (List)
Say your Armour class has two properties: name (string) and stats (Stats)
Say your Stats class also has two properties: rating (int) and durability (int)

class Actor
{
  private readonly string name;
  private readonly List<Armour> armours;

  public Armour(sting name, IEnumerable<Armour> armours)
  {
    this.name = name;
    this.armours = armours.ToList();
  }
}

class Armour
{
  private readonly string name;
  private readonly Stats stats;

  public Armour(sting name, Stats stats)
  {
    this.name = name;
    this.stats = stats;
  }
}

class Stats
{
  private readonly int rating;
  private readonly int durability;

  public Stats(int rating, int durability)
  {
    this.rating = rating;
    this.durability = durability;
  }
}

To implement Clone, Actor would create a new Actor object, set the name and then call Clone on the Armour objects:

class Actor
{
  // ...

  public Actor Clone()
  {
    return new Actor(name, armours.Select(a => a.Clone());
  }
}

Likewise for Armour

class Armour
{
  // ...

  public Armour Clone()
  {
    return new Armour(name, stats.Clone());
  }
}

And Stats …

class Stats
{
  // ...

  public Stats Clone()
  {
    return new Stats(rating, durability);
  }
}

Your client code would then call clone on the top level objects. If you had a list of Actor objects, it would look like so:

void UpdateActors()
{
  List<Actor> heros = new List<Actor>(MyDataBase.Actors.Count());
  foreach (Actor actor in MyDataBase.Actors)
    heros.Add(actor.Clone());
}

If your objects inherit from Unity.Object (the base class of every object in the unity framework, including Component and MonoBehaviour), then there is already a clone method … except it is called Instantiate. You might have luck just using that instead.

All that said, it’s actually pretty rare to need to deep copy anything unless you are using a prototype pattern such as the one Unity already provides. If you share a bit more about your “big picture”, someone may have an idea to avoid the deep copy problem altogether.

Regarding your comments:

There is no way to make a deep copy instantaneous. You are recreating the entire object graph – that’s what a deep copy is. If there are a lot of objects in the original graph then this will take a lot of time.

I don’t really understand what you mean by “it resets the lists completely”. Could you explain that in a different way?

3 Likes

Hi Thanks for the response, I will try to explain better what Im doing:

Basically what Im doing is an editor extension which allows to store data into a custom asset file, the list I want to copy are list the user can modify to include new stats, amor slots, Item types, elements, etc. Maybe this picture helps to understand a little better what Im doing:
2545130--176889--EditorRPG.png

The custom asset file its an scriptable object, everything is stored in one file, in a serie of lists, which includes the reference lists I want to copy.
2545130--176890--Custom file.png
Some of the newly copied object might already been on the old lists before updating them so the values I entered get reset to the default ones on the reference lists, thats what I meant by “the lists reset”

Are you creating the extension or using it?

So I understand, you’re saying you call UpdateActors() multiple times, and each time the list grows with copies form the “prototype list”?

Is there any reason you can’t just throw away the list before calling UpdateActors()? You will get an entirely new group of objects, all of them up to date with the prototypes.

Im creating the extension. (My first one)

No I mean the opposite, I only call UpdateActos() once and the old lists gets deleted and replaced with a new one, the problem is if I change the value on one stat on an Actor before updating that stat gets its value back to the default and I have to enter that value on that actor again. Its not a big problem but it would be cool if I could keep the stats that are on both lists and add and delete the ones that dont.

Okay, so would like the cloned list to receive updates from the prototype list but only for properties that have changed on the prototypes?

What if the value has changed on both the clone and the prototype?

Here’s one possibility:
Create a hidden value to uniquely identify each object in your database. This is normally known as the “unique identifier” and it’s common to use an incrementing integer or GUID for this purpose. When you’re updating your list of objects, you can compare the cloned actor’s with the prototype actor’s identifier to “join” the clones up with their originals.

From there, you can add some logic to determine which values you will keep and which you will throw away. One option is to include an “original value”, which never changes, for every property on the clone. Once you’ve joined the clone with the prototype, you can compare the clone’s original value, current value and the prototypes value to determine what value the final “updated clone” should have.

1 Like

Hi, thanks for the response and sorry it took me this long to reply but I dont have internet everyday,

I added a “ID” (an int) to every object in the database, now I need a way to:

a) Delete the objects from the actors that are not on the original list.
b) Add the new objects added to the original lists to the actors lists.
c) Change the name of the objects with are on both lists so the names match.

This is where Im at:

void UpdateActors() {
  HashSet<int> DBStatsIDs = new HashSet<int>(MyDataBase.BaseStats.Select(x => x.ID));  
  foreach (Actor _Actor in MyDataBase.Actors.ToArray()) {  
  _Actor.BaseStats.RemoveAll(x => !DBStatsIDs.Contains(x.ID));
  HashSet<int> ActorStatsIDs = new HashSet<int>(_Actor.BaseStats.Select(x => x.ID));
  List<BaseStat> StatsToClone = MyDataBase.BaseStats.Where(x=> !ActorStatsIDs.Contains(x.ID)).ToList();
  List<BaseStat> StatsToUpdate = MyDataBase.BaseStats.Where(x=> ActorStatsIDs.Contains(x.ID)).ToList();  
  _Actor.BaseStats.AddRange(StatsToClone.Select(_Stat => new BaseStat() {Name = _Stat.Name, BaseValue = _Stat.BaseValue, ID = _Stat.ID}).ToList());  
    }
  }

I removed the stats and added the new ones, now I need to update the stats that match. I havent implemented the clone system yet, I will use this for now but I plan to use it.