How to easily deep copy some class? Memberwiseclone not work because it is shallow copy. Then manual copy each variables inside class is the way? Serialize->Deserialize is too expensive way.
It is approximately 3. This is the most maintenance-intensive way, but probably can be made the most performant way, assuming solid engineering.
If you're fine with Unity's serialization system, Object.Instantiate can clone anything that's derived from UnityEngine.Object. If your objects aren't derived from Unity's Object, you could stick them into a scriptable object and instantiate that. Otherwise, implement ICloneable and do it manually? There's really no shortcut that doesn't affect performance in some way.
In theory you could reflect all the fields and create a copy of the class that way, then for any non-primitive members loop its fields, and recurse until done. THING IS... there's a lot of implications that arise here. And it's why there is no 'deep-copy' method that just exists out there (and is why memberwiseclone is shallow). For example... what if you have some sort of circular reference in your fields: Code (csharp): public class Foo { public Bar BarRef; } public class Bar { public Foo FooRef; } What do you do here? (note that the circular reference could occur several classes apart... A refs B refs C refs D refs B refs C refs D refs B refs C refs D...) ... What if there is a singleton somewhere in the reference hierarchy? What if there are UnityEngine.Object's somewhere in the reference hierarchy? What if any number of edge cases in regards to how the object expects its encapsulated state to represent something that this clone will break? ... This is why generally .net has the ICloneable interface which allows the class to control how its encapsulated state will be cloned and resolve all these edge cases accordingly to how IT needs to. ... But in the end what your question leads me to is... Why do you need a deep copy of an object? I'm not trying to suggest there isn't ever a need for a deep copy. It's just that when I hear someone needs one... odds are there may be a better approach to solving/coding the task they're attempting to solve by using a deep copy. So what is your end goal here? What is this going to be used for?
Right, those are the main questions because most people would not realise what an actual generic deep copy would mean. It would mean that you would need to clone literally everything that the object may reference. This would also cause countless of issues as soon as any UnityEngine.Object comes into the mix. Just for example: Cloning a MonoBehaviour instance means: also cloning the gameobject. Which means also cloning all other components. The renderer component may have a reference to a Matrial instance, so we clone that as well. The material may reference a Texture2D, so we clone that as well. A Material also has a Shader reference, clone that as well. The MeshFilter component references a Mesh asset, so we clone that as well. This would be a total mess. A generic deep copy / cloning method would be possible to implement with some reflection work. However this would only make sense for some simple pure data classes. As soon as you throw custom classes in the mix you run into all sorts of issues. What if a class doesn't have a public constructor because it was constructed through a factory method? What if there's no default constructor? Generally I would not recommend using any such generic approach. It makes much more sense to go with an ICloneable approach. A class know best what it is made of. Keep in mind if you may use ICloneable already for a shallow copy, you are free to invent your own interface (and so "IDeepCloneable" is born). I think the deep copy example provided in the MSDN documentation for MemberwiseClone is probably the most flexible approach. So you use MemberwiseClone to just get a copy of the object, and after that you take care of things which may need special attention. So this takes care of all the primitive data and you just have to handle the references.
What's your use case for deep clone? Often there is better solutions to the problem. Edit: not the first to point that out
@lordofduct Why do you need a deep copy of an object? -> When I change scene, some class's variable(script)'s become null because scene destroyed. But I need to preserve it for Save & Load function, so I want to copy it.
If you want to save/load it... that implies it being written to disk. That would require serialization. As for clearing between scenes... if it's a unity object, well it's getting destroyed. You can't really clone that due to how unity objects work (there is a engine side unmanaged object that pairs with the .net side object). If you just mean some of the fields of said object... why clone it? Just hold onto a reference to it somewhere that doesn't get destroyed (like say in your gamemanager or something), it'll survive just fine.
@lordofduct So more specific, in scene A, there are many buildings (gameobject) on the field hexagon map tiles, on those each building has script on their own (but same class) and it manage its building's data. I need to keep those whole script's data before leaving scene A, cuz when I return to A after gone to scene B, that script's values should distribute to each same building's data so that each building's status set same with when left. So I referenced those scripts on to the DDS (dontdestroy) manager script, but when I return, it become null because destroyed. So I thought deep copy method. Other way then?
You can't "copy" those because they're Components. Components are UnityEngine.Objects (inherit from). You can't just make copies of those because of how they're integrated with the Unity engine. Basically any GameObject/Component/ScriptableObject has a C# side object and a C++ side object. When you "destroy" them the C++ side object is removed from unmanaged memory on the C++ side and the C# side object doesn't actually become null BUT evaluates as null due to Unity's overload of the == operator and if you access its members that interop with the internal C++ side it'll throw an error. If you cloned these objects... they just wouldn't work. At the very least they'd still be teathered to a destroyed object and evaluate as "null" just like the one you held... OR you'd get worst problems. ... What you need to do is capture the state of the fields in those components. This could be as simple as just returning some other token of that script. Something like so: Code (csharp): public class MyScript : MonoBehaviour { public float SomeNumber; public string SomeString; public Vector3 SomeVector; public SomeDataClassType SomeData; //in this example 'SomeDataClassType' is a pure data class with no potential gotchas like refs to UnityEngine.Objects. Instead it just holds basic fields. public MyScriptToken GetToken() { return new MyScriptToken() { SomeNumber = this.SomeNumber, SomeString = this.SomeString, SomeVector = this.SomeVector, SomeData = this.SomeData }; } public void SetFromToken(MyScriptToken token) { SomeNumber = token.SomeNumber; SomeString = token.SomeString; SomeVector = token.SomeVector; SomeData = token.SomeData; } public struct MyScriptToken { public float SomeNumber; public string SomeString; public Vector3 SomeVector; public SomeDataClassType SomeData; } } Note, this is technically how I resolve issues like saving as well. I use tokens to get the state of objects and then serialize them. I can also store the tokens between scenes. Here you can see that: Spoiler All of these implement an "ITokenizable" interface to generalize the process. This way my save code can just loop over all things that can be tokenized, get the token, and then stick that in a dictionary (the dictionary holds an id so I know what gets it... getting into how that id is done gets into a completely different issue/topic). A medium complexity example like my generic one above: Code (csharp): using UnityEngine; using System.Collections.Generic; using com.spacepuppy.Dynamic; namespace com.mansion.Entities.Actors { public class RecruitAttributes : MonoBehaviour, ITokenizable { public BodyMeshes BodyMesh; public string Name; public string Surname; public int Face; public int HeadAcc; [Range(1, 99)] public int Rank; public Classes Class; public Traits Trait1; public Traits Trait2; public Abilities Ability; public Personalities Personality; [Range(0, 9999)] public int Cash; #region ITokenizable Interface public object CreateStateToken() { return new Token() { BodyMesh = this.BodyMesh, Name = this.Name, Surname = this.Surname, Face = this.Face, HeadAcc = this.HeadAcc, Rank = this.Rank, Class = this.Class, Trait1 = this.Trait1, Trait2 = this.Trait2, Ability = this.Ability, Personality = this.Personality, Cash = this.Cash }; } public void RestoreFromStateToken(object token) { var tok = token as Token; if (tok == null) return; this.BodyMesh = tok.BodyMesh; this.Name = tok.Name; this.Surname = tok.Surname; this.Face = tok.Face; this.HeadAcc = tok.HeadAcc; this.Rank = tok.Rank; this.Class = tok.Class; this.Trait1 = tok.Trait1; this.Trait2 = tok.Trait2; this.Ability = tok.Ability; this.Personality = tok.Personality; this.Cash = tok.Cash; } #endregion #region Special Types public enum BodyMeshes { DevActor = 0, RookieAverageMale = 1, RookieAverageFemale = 2, K9UnitMaleAverage = 3, K9UnitFemaleAverage = 4 } public enum Classes { Rookie = 0, DeskJockey = 1, Officer = 2, BeatCop = 3, K9Unit = 4, SWAT = 5, Detective = 6, Sergeant = 7, Undercover = 8, SecretAgent = 9, ChiefOfPolice = 10, ParkRanger = 11, Fireman = 12, Informant = 13, EscapedConvict = 14, AnActualChild = 15 } public enum Traits { None = 0, Acrobatic = 1, Violent = 2, Bignerd = 3, Deadeye = 4, DoorBasher = 5, Feeble = 6, FlatFooted = 7, GunNut = 8, Leadbelly = 9, Lightweight = 10, MapMaker = 11, Moonwalker = 12, NearSighted = 13, Noir = 14, Pathfinder = 15, Prepared = 16, QuickReload = 17, Slugger = 18, SmallFrame = 19, Tank = 20, TragicStory = 21 } public enum Abilities { None = 0, Dodge = 1, Hide = 2, Lockpick = 3, PlantDNA = 4, PushPull = 5, TaintedBlood = 6, SpringBoots = 7, Vampirism = 8, VentCrawl = 9, K9Unit = 10, Partnered = 11, EscortMission = 12 } public enum Personalities { Silent = 0 } [System.Serializable] private class Token { public BodyMeshes BodyMesh; public string Name; public string Surname; public int Face; public int HeadAcc; [Range(1, 99)] public int Rank; public Classes Class; public Traits Trait1; public Traits Trait2; public Abilities Ability; public Personalities Personality; [Range(0, 9999)] public int Cash; } #endregion } } A very simple example where the state is just a single 'bool': Code (csharp): using UnityEngine; using com.spacepuppy; using com.spacepuppy.Dynamic; using com.spacepuppy.Utils; namespace com.mansion.Entities.Inventory { [CreateAssetMenu(fileName = "InventoryItem", menuName = "Inventory/FuseInventoryItem")] public class FuseInventoryItem : InventoryItem, ITokenizable { #region Fields #endregion #region Properties public bool Blown { get; set; } #endregion #region Methods object ITokenizable.CreateStateToken() { return this.Blown; } void ITokenizable.RestoreFromStateToken(object token) { this.Blown = ConvertUtil.ToBool(token); } #endregion } } A complex example where the state has a lot going on since it relies on a list of known ScriptableObjects which can't be easily serialized to disk. Code (csharp): using UnityEngine; using System.Collections.Generic; using System.Linq; using com.spacepuppy; using com.spacepuppy.Dynamic; using com.spacepuppy.Project; using com.spacepuppy.Scenario; using com.spacepuppy.Utils; namespace com.mansion.Entities.Inventory { public class InventoryPouch : SPComponent, ITokenizable { #region Fields [SerializeField] private DiscreteFloat _maxTrackedItems = DiscreteFloat.PositiveInfinity; [SerializeField] [ReorderableArray] [DisableOnPlay] private List<InventoryItem> _items = new List<InventoryItem>(); [System.NonSerialized] private ItemCollection _itemColl; [SerializeField] private Trigger _onInventoryChanged; [System.NonSerialized] private IEntity _entity; #endregion #region CONSTRUCTOR protected override void Awake() { base.Awake(); _entity = IEntity.Pool.GetFromSource<IEntity>(this); } protected override void Start() { base.Start(); if (!this.IsInitialized) this.InitItems(); } internal virtual void InitItems() { if (this.IsInitialized) return; this.IsInitialized = true; for (int i = 0; i < _items.Count; i++) { var item = _items[i]; if (item == null) { _items.RemoveAt(i); i--; } else { this.OnItemAdded(item); } } } #endregion #region Properties public IEntity Entity { get { return _entity; } } public bool IsInitialized { get; private set; } public DiscreteFloat MaxTrackedItems { get { return _maxTrackedItems; } set { _maxTrackedItems = value; } } public ItemCollection Items { get { if (_itemColl == null) _itemColl = new ItemCollection(this); return _itemColl; } } public Trigger OnInventoryChanged { get { return _onInventoryChanged; } } #endregion #region Methods protected virtual void OnItemAdded(InventoryItem item) { if (!this.IsInitialized) return; //if we haven't been initialized yet, this work should wait until then item.OnItemAddedToInventory(this); if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this); } protected virtual void OnItemRemoved(InventoryItem item) { item.OnItemRemovedFromInventory(this); if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this); } protected virtual void OnClearingItems() { foreach(var item in _items) { if (!object.ReferenceEquals(item, null)) item.OnItemRemovedFromInventory(this); } if (_onInventoryChanged.Count > 0) _onInventoryChanged.ActivateTrigger(this, this); } #endregion #region ITokenizable Interface public object CreateStateToken() { object[] arr = new object[_items.Count + 1]; arr[arr.Length - 1] = (float)_maxTrackedItems;//we stick the max at the end for (int i = 0; i < _items.Count; i++) { var item = _items[i]; var tk = (item is ITokenizable) ? (item as ITokenizable).CreateStateToken() : null; if (tk != null) arr[i] = new Token(item.name, tk); else arr[i] = item.name; } return arr; } public void RestoreFromStateToken(object token) { this.Items.Clear(); var arr = token as object[]; if (arr == null || arr.Length == 0) return; var items = Game.EpisodeSettings.InventorySet; if (object.ReferenceEquals(items, null)) return; int len = arr.Length; if (ConvertUtil.IsNumeric(arr[len - 1])) { //we store max size as the last entry _maxTrackedItems = ConvertUtil.ToSingle(arr[len - 1]); len--; //redact from len so we don't parse this as an InventoryItem } for (int i = 0; i < len; i++) { InventoryItem item = null; object tk = null; if (arr[i] is string) { item = items.GetAsset(arr[i] as string) as InventoryItem; } else if (arr[i] is Token) { var pair = (Token)arr[i]; tk = pair.token; item = items.GetAsset(pair.name) as InventoryItem; } //we don't store the token if the token is null to conserve space, so check if tokenizable even if no token if (item is ITokenizable) (item as ITokenizable).RestoreFromStateToken(tk); if (!object.ReferenceEquals(item, null)) this.Items.Add(item); } } #endregion #region Special Types public class ItemCollection : IList<InventoryItem> { private InventoryPouch _owner; #region CONSTRUCTOR public ItemCollection(InventoryPouch pouch) { if (pouch == null) throw new System.ArgumentNullException("pouch"); _owner = pouch; } #endregion #region Properties public InventoryItem this[int index] { get { return _owner._items[index]; } } public int TrackedCount { get { var e = _owner._items.GetEnumerator(); int cnt = 0; while(e.MoveNext()) { if(e.Current.Usage != InventoryUsage.Untracked) { cnt++; } } return cnt; } } public bool IsFull { get { return (float)this.TrackedCount >= (float)_owner.MaxTrackedItems; } } #endregion #region Methods public bool CanAddItem(InventoryItem inv) { if (inv == null) return false; if (inv.Unique && _owner._items.Contains(inv)) return false; if (inv.Usage != InventoryUsage.Untracked && this.IsFull) return false; return true; } public bool Add(InventoryItem inv) { if (inv == null) throw new System.ArgumentNullException("inv"); if(this.CanAddItem(inv)) { _owner._items.Add(inv); _owner.OnItemAdded(inv); return true; } else { return false; } } public InventoryItem Find(string name) { foreach (var item in _owner._items) { if (item.CompareName(name)) return item; } return null; } #endregion #region IList Interface public int Count { get { return (_owner._items != null) ? _owner._items.Count : 0; } } bool ICollection<InventoryItem>.IsReadOnly { get { return false; } } void ICollection<InventoryItem>.Add(InventoryItem item) { this.Add(item); } public bool Remove(InventoryItem inv) { if (inv == null) return false; if (_owner._items.Remove(inv)) { _owner.OnItemRemoved(inv); return true; } else { return false; } } public void Clear() { _owner.OnClearingItems(); if (_owner._items != null) _owner._items.Clear(); } public bool Contains(InventoryItem inv) { return _owner._items.Contains(inv); } public int CountItem(InventoryItem inv) { if (inv == null) return 0; int cnt = 0; var e = _owner._items.GetEnumerator(); while (e.MoveNext()) { if (e.Current == inv) cnt++; } return cnt; } public void CopyTo(InventoryItem[] array, int arrayIndex) { for (int i = 0; i < _owner._items.Count; i++) { array[arrayIndex + i] = _owner._items[i]; } } public IEnumerator<InventoryItem> GetEnumerator() { for (int i = 0; i < _owner._items.Count; i++) { yield return _owner._items[i]; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public int IndexOf(InventoryItem item) { return _owner._items.IndexOf(item); } public void RemoveAt(int index) { if (index < 0 || index >= _owner._items.Count) return; var weapon = _owner._items[index]; _owner._items.RemoveAt(index); _owner.OnItemRemoved(weapon); } void IList<InventoryItem>.Insert(int index, InventoryItem inv) { if (inv == null) throw new System.ArgumentNullException("inv"); if (this.CanAddItem(inv)) { _owner._items.Insert(index, inv); _owner.OnItemAdded(inv); } } InventoryItem IList<InventoryItem>.this[int index] { get { return _owner._items[index]; } set { throw new System.NotSupportedException(); } } #endregion } [System.Serializable] private struct Token { public string name; public object token; public Token(string nm, object tk) { this.name = nm; this.token = tk; } } #endregion } }